Blob Blame History Raw
from import_all_modules import argparser, exclude_unwanted_module_globs
from import_all_modules import main as modules_main
from import_all_modules import read_modules_from_cli, filter_top_level_modules_only

from pathlib import Path

import pytest
import shlex
import sys


@pytest.fixture(autouse=True)
def preserve_sys_path():
    original_sys_path = list(sys.path)
    yield
    sys.path = original_sys_path


@pytest.fixture(autouse=True)
def preserve_sys_modules():
    original_sys_modules = dict(sys.modules)
    yield
    sys.modules = original_sys_modules


@pytest.mark.parametrize(
    'args, imports',
    [
        ('six', ['six']),
        ('five  six seven', ['five', 'six', 'seven']),
        ('six,seven, eight', ['six', 'seven', 'eight']),
        ('six.quarter  six.half,, SIX', ['six.quarter', 'six.half', 'SIX']),
        ('six.quarter  six.half,, SIX \\ ', ['six.quarter', 'six.half', 'SIX']),
    ]
)
def test_read_modules_from_cli(args, imports):
    argv = shlex.split(args)
    cli_args = argparser().parse_args(argv)
    assert read_modules_from_cli(cli_args.modules) == imports


@pytest.mark.parametrize(
    'all_mods, imports',
    [
        (['six'], ['six']),
        (['five', 'six', 'seven'], ['five', 'six', 'seven']),
        (['six.seven', 'eight'], ['eight']),
        (['SIX', 'six.quarter', 'six.half.and.sth', 'seven'], ['SIX', 'seven']),
    ],
)
def test_filter_top_level_modules_only(all_mods, imports):
    assert filter_top_level_modules_only(all_mods) == imports


@pytest.mark.parametrize(
    'globs, expected',
    [
        (['*.*'], ['foo', 'boo']),
        (['?oo'], ['foo.bar', 'foo.bar.baz', 'foo.baz']),
        (['*.baz'], ['foo', 'foo.bar', 'boo']),
        (['foo'], ['foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']),
        (['foo*'], ['boo']),
        (['foo*', '*bar'], ['boo']),
        (['foo', 'bar'], ['foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']),
        (['*'], []),
    ]
)
def test_exclude_unwanted_module_globs(globs, expected):
    my_modules = ['foo', 'foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']
    tested = exclude_unwanted_module_globs(globs, my_modules)
    assert tested == expected


def test_cli_with_all_args():
    '''A smoke test, all args must be parsed correctly.'''
    mods = ['foo', 'foo.bar', 'baz']
    files = ['-f', './foo']
    top = ['-t']
    exclude = ['-e', 'foo*']
    cli_args = argparser().parse_args([*mods, *files, *top, *exclude])

    assert cli_args.filename == [Path('foo')]
    assert cli_args.top_level is True
    assert cli_args.modules == ['foo', 'foo.bar', 'baz']
    assert cli_args.exclude == ['foo*']


def test_cli_without_filename_toplevel():
    '''Modules provided on command line (without files) must be parsed correctly.'''
    mods = ['foo', 'foo.bar', 'baz']
    cli_args = argparser().parse_args(mods)

    assert cli_args.filename is None
    assert cli_args.top_level is False
    assert cli_args.modules == ['foo', 'foo.bar', 'baz']


def test_cli_with_filename_no_cli_mods():
    '''Files (without any modules provided on command line) must be parsed correctly.'''

    files = ['-f', './foo', '-f', './bar', '-f', './baz']
    cli_args = argparser().parse_args(files)

    assert cli_args.filename == [Path('foo'), Path('./bar'), Path('./baz')]
    assert not cli_args.top_level


def test_main_raises_error_when_no_modules_provided():
    '''If no filename nor modules were provided, ValueError is raised.'''

    with pytest.raises(ValueError):
        modules_main([])


def test_import_all_modules_does_not_import():
    '''Ensure the files from /usr/lib/rpm/redhat cannot be imported and
    checked for import'''

    # We already imported it in this file once, make sure it's not imported
    # from the cache
    sys.modules.pop('import_all_modules')
    with pytest.raises(ModuleNotFoundError):
        modules_main(['import_all_modules'])


def test_modules_from_cwd_not_found(tmp_path, monkeypatch):
    test_module = tmp_path / 'this_is_a_module_in_cwd.py'
    test_module.write_text('')
    monkeypatch.chdir(tmp_path)
    with pytest.raises(ModuleNotFoundError):
        modules_main(['this_is_a_module_in_cwd'])


def test_modules_from_sys_path_found(tmp_path):
    test_module = tmp_path / 'this_is_a_module_in_sys_path.py'
    test_module.write_text('')
    sys.path.append(str(tmp_path))
    modules_main(['this_is_a_module_in_sys_path'])
    assert 'this_is_a_module_in_sys_path' in sys.modules


def test_modules_from_file_are_found(tmp_path):
    test_file = tmp_path / 'this_is_a_file_in_tmp_path.txt'
    test_file.write_text('math\nwave\nsunau\n')

    # Make sure the tested modules are not already in sys.modules
    for m in ('math', 'wave', 'sunau'):
        sys.modules.pop(m, None)

    modules_main(['-f', str(test_file)])

    assert 'sunau' in sys.modules
    assert 'math' in sys.modules
    assert 'wave' in sys.modules


def test_modules_from_files_are_found(tmp_path):
    test_file_1 = tmp_path / 'this_is_a_file_in_tmp_path_1.txt'
    test_file_2 = tmp_path / 'this_is_a_file_in_tmp_path_2.txt'
    test_file_3 = tmp_path / 'this_is_a_file_in_tmp_path_3.txt'

    test_file_1.write_text('math\nwave\n')
    test_file_2.write_text('sunau\npathlib\n')
    test_file_3.write_text('logging\nsunau\n')

    # Make sure the tested modules are not already in sys.modules
    for m in ('math', 'wave', 'sunau', 'pathlib', 'logging'):
        sys.modules.pop(m, None)

    modules_main(['-f', str(test_file_1), '-f', str(test_file_2), '-f', str(test_file_3), ])
    for module in ('sunau', 'math', 'wave', 'pathlib', 'logging'):
        assert module in sys.modules


def test_nonexisting_modules_raise_exception_on_import(tmp_path):
    test_file = tmp_path / 'this_is_a_file_in_tmp_path.txt'
    test_file.write_text('nonexisting_module\nanother\n')
    with pytest.raises(ModuleNotFoundError):
        modules_main(['-f', str(test_file)])


def test_nested_modules_found_when_expected(tmp_path, monkeypatch, capsys):

    # This one is supposed to raise an error
    cwd_path = tmp_path / 'test_cwd'
    Path.mkdir(cwd_path)
    test_module_1 = cwd_path / 'this_is_a_module_in_cwd.py'

    # Nested structure that is supposed to be importable
    nested_path_1 = tmp_path / 'nested'
    nested_path_2 = nested_path_1 / 'more_nested'

    for path in (nested_path_1, nested_path_2):
        Path.mkdir(path)

    test_module_2 = tmp_path / 'this_is_a_module_in_level_0.py'
    test_module_3 = nested_path_1 / 'this_is_a_module_in_level_1.py'
    test_module_4 = nested_path_2 / 'this_is_a_module_in_level_2.py'

    for module in (test_module_1, test_module_2, test_module_3, test_module_4):
        module.write_text('')

    sys.path.append(str(tmp_path))
    monkeypatch.chdir(cwd_path)

    with pytest.raises(ModuleNotFoundError):
        modules_main([
            'this_is_a_module_in_level_0',
            'nested.this_is_a_module_in_level_1',
            'nested.more_nested.this_is_a_module_in_level_2',
            'this_is_a_module_in_cwd'])

    _, err = capsys.readouterr()
    assert 'Check import: this_is_a_module_in_level_0' in err
    assert 'Check import: nested.this_is_a_module_in_level_1' in err
    assert 'Check import: nested.more_nested.this_is_a_module_in_level_2' in err
    assert 'Check import: this_is_a_module_in_cwd' in err


def test_modules_both_from_files_and_cli_are_imported(tmp_path):
    test_file_1 = tmp_path / 'this_is_a_file_in_tmp_path_1.txt'
    test_file_1.write_text('this_is_a_module_in_tmp_path_1')

    test_file_2 = tmp_path / 'this_is_a_file_in_tmp_path_2.txt'
    test_file_2.write_text('this_is_a_module_in_tmp_path_2')

    test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
    test_module_2 = tmp_path / 'this_is_a_module_in_tmp_path_2.py'
    test_module_3 = tmp_path / 'this_is_a_module_in_tmp_path_3.py'

    for module in (test_module_1, test_module_2, test_module_3):
        module.write_text('')

    sys.path.append(str(tmp_path))
    modules_main([
        '-f', str(test_file_1),
        'this_is_a_module_in_tmp_path_3',
        '-f', str(test_file_2),
    ])

    expected = (
        'this_is_a_module_in_tmp_path_1',
        'this_is_a_module_in_tmp_path_2',
        'this_is_a_module_in_tmp_path_3',
    )
    for module in expected:
        assert module in sys.modules


def test_non_existing_module_raises_exception(tmp_path):

    test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
    test_module_1.write_text('')
    sys.path.append(str(tmp_path))

    with pytest.raises(ModuleNotFoundError):
        modules_main([
            'this_is_a_module_in_tmp_path_1',
            'this_is_a_module_in_tmp_path_2',
        ])


def test_module_with_error_propagates_exception(tmp_path):

    test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py'
    test_module_1.write_text('0/0')
    sys.path.append(str(tmp_path))

    # The correct exception must be raised
    with pytest.raises(ZeroDivisionError):
        modules_main([
            'this_is_a_module_in_tmp_path_1',
        ])


def test_correct_modules_are_excluded(tmp_path):
    test_module_1 = tmp_path / 'module_in_tmp_path_1.py'
    test_module_2 = tmp_path / 'module_in_tmp_path_2.py'
    test_module_3 = tmp_path / 'module_in_tmp_path_3.py'

    for module in (test_module_1, test_module_2, test_module_3):
        module.write_text('')

    sys.path.append(str(tmp_path))
    test_file_1 = tmp_path / 'a_file_in_tmp_path_1.txt'
    test_file_1.write_text('module_in_tmp_path_1\nmodule_in_tmp_path_2\nmodule_in_tmp_path_3\n')

    modules_main([
        '-e', 'module_in_tmp_path_2',
        '-f', str(test_file_1),
        '-e', 'module_in_tmp_path_3',
        ])

    assert 'module_in_tmp_path_1' in sys.modules
    assert 'module_in_tmp_path_2' not in sys.modules
    assert 'module_in_tmp_path_3' not in sys.modules


def test_excluding_all_modules_raises_error(tmp_path):
    test_module_1 = tmp_path / 'module_in_tmp_path_1.py'
    test_module_2 = tmp_path / 'module_in_tmp_path_2.py'
    test_module_3 = tmp_path / 'module_in_tmp_path_3.py'

    for module in (test_module_1, test_module_2, test_module_3):
        module.write_text('')

    sys.path.append(str(tmp_path))
    test_file_1 = tmp_path / 'a_file_in_tmp_path_1.txt'
    test_file_1.write_text('module_in_tmp_path_1\nmodule_in_tmp_path_2\nmodule_in_tmp_path_3\n')

    with pytest.raises(ValueError):
        modules_main([
            '-e', 'module_in_tmp_path*',
            '-f', str(test_file_1),
            ])


def test_only_toplevel_modules_found(tmp_path):

    # Nested structure that is supposed to be importable
    nested_path_1 = tmp_path / 'nested'
    nested_path_2 = nested_path_1 / 'more_nested'

    for path in (nested_path_1, nested_path_2):
        Path.mkdir(path)

    test_module_1 = tmp_path / 'this_is_a_module_in_level_0.py'
    test_module_2 = nested_path_1 / 'this_is_a_module_in_level_1.py'
    test_module_3 = nested_path_2 / 'this_is_a_module_in_level_2.py'

    for module in (test_module_1, test_module_2, test_module_3):
        module.write_text('')

    sys.path.append(str(tmp_path))

    modules_main([
        'this_is_a_module_in_level_0',
        'nested.this_is_a_module_in_level_1',
        'nested.more_nested.this_is_a_module_in_level_2',
        '-t'])

    assert 'nested.this_is_a_module_in_level_1' not in sys.modules
    assert 'nested.more_nested.this_is_a_module_in_level_2' not in sys.modules


def test_only_toplevel_included_modules_found(tmp_path):

    # Nested structure that is supposed to be importable
    nested_path_1 = tmp_path / 'nested'
    nested_path_2 = nested_path_1 / 'more_nested'

    for path in (nested_path_1, nested_path_2):
        Path.mkdir(path)

    test_module_1 = tmp_path / 'this_is_a_module_in_level_0.py'
    test_module_4 = tmp_path / 'this_is_another_module_in_level_0.py'

    test_module_2 = nested_path_1 / 'this_is_a_module_in_level_1.py'
    test_module_3 = nested_path_2 / 'this_is_a_module_in_level_2.py'

    for module in (test_module_1, test_module_2, test_module_3, test_module_4):
        module.write_text('')

    sys.path.append(str(tmp_path))

    modules_main([
        'this_is_a_module_in_level_0',
        'this_is_another_module_in_level_0',
        'nested.this_is_a_module_in_level_1',
        'nested.more_nested.this_is_a_module_in_level_2',
        '-t',
        '-e', '*another*'
    ])

    assert 'nested.this_is_a_module_in_level_1' not in sys.modules
    assert 'nested.more_nested.this_is_a_module_in_level_2' not in sys.modules
    assert 'this_is_another_module_in_level_0' not in sys.modules
    assert 'this_is_a_module_in_level_0' in sys.modules


def test_module_list_from_relative_path(tmp_path, monkeypatch):

    monkeypatch.chdir(tmp_path)
    test_file_1 = Path('this_is_a_file_in_cwd.txt')
    test_file_1.write_text('wave')

    sys.modules.pop('wave', None)

    modules_main([
        '-f', 'this_is_a_file_in_cwd.txt'
    ])

    assert 'wave' in sys.modules


@pytest.mark.parametrize('arch_in_path', [True, False])
def test_pth_files_are_read_from__PYTHONSITE(arch_in_path, tmp_path, monkeypatch, capsys):
    sitearch = tmp_path / 'lib64'
    sitearch.mkdir()
    sitelib = tmp_path / 'lib'
    sitelib.mkdir()

    for where, word in (sitearch, "ARCH"), (sitelib, "LIB"), (sitelib, "MOD"):
        module = where / f'print{word}.py'
        module.write_text(f'print("{word}")')

    pth_sitearch = sitearch / 'ARCH.pth'
    pth_sitearch.write_text('import printARCH\n')

    pth_sitelib = sitelib / 'LIB.pth'
    pth_sitelib.write_text('import printLIB\n')

    if arch_in_path:
        sys.path.append(str(sitearch))
    sys.path.append(str(sitelib))

    # we always add sitearch to _PYTHONSITE
    # but when not in sys.path, it should not be processed for .pth files
    monkeypatch.setenv('_PYTHONSITE', f'{sitearch}:{sitelib}')

    modules_main(['printMOD'])
    out, err = capsys.readouterr()
    if arch_in_path:
        assert out == 'ARCH\nLIB\nMOD\n'
    else:
        assert out == 'LIB\nMOD\n'