From 2d0673afb17814e4bd6b7a281574b1d0e1fdb3e3 Mon Sep 17 00:00:00 2001 From: Karolina Surma Date: Mon, 18 Oct 2021 16:33:04 +0200 Subject: [PATCH] Add new options for %%py{3}_check_import: -f, -t, -e -f: optionally read a file with module names to test -t: bool flag - if set, filter only top-level modules -e: optionally exclude module names matching the given glob (Unix shell-style wildcards) Importing all modules may cause bogus failures in some cases, eg. when the imported code assumes there is an existing graphical window. Such behaviour may be by design, hence for automatic processing it's more convinient to - in some cases - check only for top-level modules or filter out the troublemakers. --- import_all_modules.py | 152 ++++++++++++ macros.python | 13 +- macros.python3 | 13 +- python-rpm-macros.spec | 11 +- tests/test_evals.py | 25 +- tests/test_import_all_modules.py | 392 +++++++++++++++++++++++++++++++ tests/tests.yml | 2 +- 7 files changed, 581 insertions(+), 27 deletions(-) create mode 100644 import_all_modules.py create mode 100644 tests/test_import_all_modules.py diff --git a/import_all_modules.py b/import_all_modules.py new file mode 100644 index 0000000..7536133 --- /dev/null +++ b/import_all_modules.py @@ -0,0 +1,152 @@ +'''Script to perform import of each module given to %%py_check_import +''' +import argparse +import importlib +import fnmatch +import re +import sys + +from contextlib import contextmanager +from pathlib import Path + + +def read_modules_files(file_paths): + '''Read module names from the files (modules must be newline separated). + + Return the module names list or, if no files were provided, an empty list. + ''' + + if not file_paths: + return [] + + modules = [] + for file in file_paths: + file_contents = file.read_text() + modules.extend(file_contents.split()) + return modules + + +def read_modules_from_cli(argv): + '''Read module names from command-line arguments (space or comma separated). + + Return the module names list. + ''' + + if not argv: + return [] + + # %%py3_check_import allows to separate module list with comma or whitespace, + # we need to unify the output to a list of particular elements + modules_as_str = ' '.join(argv) + modules = re.split(r'[\s,]+', modules_as_str) + return modules + + +def filter_top_level_modules_only(modules): + '''Filter out entries with nested modules (containing dot) ie. 'foo.bar'. + + Return the list of top-level modules. + ''' + + return [module for module in modules if '.' not in module] + + +def any_match(text, globs): + '''Return True if any of given globs fnmatchcase's the given text.''' + + return any(fnmatch.fnmatchcase(text, g) for g in globs) + + +def exclude_unwanted_module_globs(globs, modules): + '''Filter out entries which match the either of the globs given as argv. + + Return the list of filtered modules. + ''' + + return [m for m in modules if not any_match(m, globs)] + + +def read_modules_from_all_args(args): + '''Return a joined list of modules from all given command-line arguments. + ''' + + modules = read_modules_files(args.filename) + modules.extend(read_modules_from_cli(args.modules)) + if args.exclude: + modules = exclude_unwanted_module_globs(args.exclude, modules) + + if args.top_level: + modules = filter_top_level_modules_only(modules) + + # Error when someone accidentally managed to filter out everything + if len(modules) == 0: + raise ValueError('No modules to check were left') + + return modules + + +def import_modules(modules): + '''Procedure to perform import check for each module name from the given list of modules. + ''' + + for module in modules: + print('Check import:', module, file=sys.stderr) + importlib.import_module(module) + + +def argparser(): + parser = argparse.ArgumentParser( + description='Generate list of all importable modules for import check.' + ) + parser.add_argument( + 'modules', nargs='*', + help=('Add modules to check the import (space or comma separated).'), + ) + parser.add_argument( + '-f', '--filename', action='append', type=Path, + help='Add importable module names list from file.', + ) + parser.add_argument( + '-t', '--top-level', action='store_true', + help='Check only top-level modules.', + ) + parser.add_argument( + '-e', '--exclude', action='append', + help='Provide modules globs to be excluded from the check.', + ) + return parser + + +@contextmanager +def remove_unwanteds_from_sys_path(): + '''Remove cwd and this script's parent from sys.path for the import test. + Bring the original contents back after import is done (or failed) + ''' + + cwd_absolute = Path.cwd().absolute() + this_file_parent = Path(__file__).parent.absolute() + old_sys_path = list(sys.path) + for path in old_sys_path: + if Path(path).absolute() in (cwd_absolute, this_file_parent): + sys.path.remove(path) + try: + yield + finally: + sys.path = old_sys_path + + +def main(argv=None): + + cli_args = argparser().parse_args(argv) + + if not cli_args.modules and not cli_args.filename: + raise ValueError('No modules to check were provided') + + modules = read_modules_from_all_args(cli_args) + + with remove_unwanteds_from_sys_path(): + import_modules(modules) + + +if __name__ == '__main__': + main() diff --git a/macros.python b/macros.python index 39b4120..6c0ad5b 100644 --- a/macros.python +++ b/macros.python @@ -69,16 +69,17 @@ } # With $PATH and $PYTHONPATH set to the %%buildroot, -# try to import the given Python module(s). +# try to import the Python module(s) given as command-line args or read from file (-f). +# Filter and check import on only top-level modules using -t flag. +# Exclude unwanted modules by passing their globs to -e option. # Useful as a smoke test in %%check when running tests is not feasible. -# Use spaces or commas as separators. -%py_check_import() %{expand:\\\ - (cd %{_topdir} &&\\\ +# Use spaces or commas as separators if providing list directly. +# Use newlines as separators if providing list in a file. +%py_check_import(e:tf:) %{expand:\\\ PATH="%{buildroot}%{_bindir}:$PATH"\\\ PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python_sitearch}:%{buildroot}%{python_sitelib}}"\\\ PYTHONDONTWRITEBYTECODE=1\\\ - %{__python} -c "import %{lua:local m=rpm.expand('%{?*}'):gsub('[%s,]+', ', ');print(m)}" - ) + %{__python} -%{py_shebang_flags} %{_rpmconfigdir}/redhat/import_all_modules.py %{?**} } %python_provide() %{lua: diff --git a/macros.python3 b/macros.python3 index 7db969e..d9a4c06 100644 --- a/macros.python3 +++ b/macros.python3 @@ -67,16 +67,17 @@ } # With $PATH and $PYTHONPATH set to the %%buildroot, -# try to import the given Python 3 module(s). +# try to import the Python 3 module(s) given as command-line args or read from file (-f). +# Filter and check import on only top-level modules using -t flag. +# Exclude unwanted modules by passing their globs to -e option. # Useful as a smoke test in %%check when running tests is not feasible. -# Use spaces or commas as separators. -%py3_check_import() %{expand:\\\ - (cd %{_topdir} &&\\\ +# Use spaces or commas as separators if providing list directly. +# Use newlines as separators if providing list in a file. +%py3_check_import(e:tf:) %{expand:\\\ PATH="%{buildroot}%{_bindir}:$PATH"\\\ PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\ PYTHONDONTWRITEBYTECODE=1\\\ - %{__python3} -c "import %{lua:local m=rpm.expand('%{?*}'):gsub('[%s,]+', ', ');print(m)}" - ) + %{__python3} -%{py3_shebang_flags} %{_rpmconfigdir}/redhat/import_all_modules.py %{?**} } # This only supports Python 3.5+ and will never work with Python 2. diff --git a/python-rpm-macros.spec b/python-rpm-macros.spec index 0b73c3b..373e389 100644 --- a/python-rpm-macros.spec +++ b/python-rpm-macros.spec @@ -15,6 +15,7 @@ Source201: python.lua # Python code %global compileall2_version 0.7.1 Source301: https://github.com/fedora-python/compileall2/raw/v%{compileall2_version}/compileall2.py +Source302: import_all_modules.py # BRP scripts # This one is from redhat-rpm-config < 190 @@ -31,6 +32,7 @@ Source402: brp-python-hardlink Source403: brp-fix-pyc-reproducibility # macros and lua: MIT +# import_all_modules.py: MIT # compileall2.py: PSFv2 # brp scripts: GPLv2+ License: MIT and Python and GPLv2+ @@ -47,7 +49,7 @@ elseif posix.stat('macros.python-srpm') then end } Version: %{__default_python3_version} -Release: 11%{?dist} +Release: 12%{?dist} BuildArch: noarch @@ -105,6 +107,7 @@ install -p -m 644 -t %{buildroot}%{_rpmluadir}/fedora/srpm python.lua mkdir -p %{buildroot}%{_rpmconfigdir}/redhat install -m 644 compileall2.py %{buildroot}%{_rpmconfigdir}/redhat/ +install -m 644 import_all_modules.py %{buildroot}%{_rpmconfigdir}/redhat/ install -m 755 brp-* %{buildroot}%{_rpmconfigdir}/redhat/ @@ -131,6 +134,7 @@ install -m 755 brp-* %{buildroot}%{_rpmconfigdir}/redhat/ %files -n python-srpm-macros %{rpmmacrodir}/macros.python-srpm %{_rpmconfigdir}/redhat/compileall2.py +%{_rpmconfigdir}/redhat/import_all_modules.py %{_rpmconfigdir}/redhat/brp-python-bytecompile %{_rpmconfigdir}/redhat/brp-python-hardlink %{_rpmconfigdir}/redhat/brp-fix-pyc-reproducibility @@ -141,6 +145,11 @@ install -m 755 brp-* %{buildroot}%{_rpmconfigdir}/redhat/ %changelog +* Mon Oct 25 2021 Karolina Surma - 3.10-12 +- Introduce -f (read from file) option to %%py{3}_check_import +- Introduce -t (filter top-level modules) option to %%py{3}_check_import +- Introduce -e (exclude module globs) option to %%py{3}_check_import + * Wed Oct 20 2021 Tomas Orsava - 3.10-11 - Define a new macros %%python_wheel_dir and %%python_wheel_pkg_prefix diff --git a/tests/test_evals.py b/tests/test_evals.py index f86284d..d136cc1 100644 --- a/tests/test_evals.py +++ b/tests/test_evals.py @@ -671,35 +671,36 @@ def test_python3_sitearch_value_alternate_python(lib, alt_x_y): @pytest.mark.parametrize( - 'args, imports', + 'args', [ - ('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', + '-f foo.txt', + '-t -f foo.txt six, seven', + '-e "foo*" -f foo.txt six, seven', + 'six.quarter six.half,, SIX', ] ) @pytest.mark.parametrize('__python3', [None, f'/usr/bin/python{X_Y}', '/usr/bin/pythonX.Y']) -def test_py3_check_import(args, imports, __python3, lib): +def test_py3_check_import(args, __python3, lib): x_y = X_Y - macors = { + macros = { 'buildroot': 'BUILDROOT', - '_topdir': 'TOPDIR', + '_rpmconfigdir': 'RPMCONFIGDIR', } if __python3 is not None: if 'X.Y' in __python3: __python3 = __python3.replace('X.Y', get_alt_x_y()) - macors['__python3'] = __python3 + macros['__python3'] = __python3 # If the __python3 command has version at the end, parse it and expect it. # Note that the command is used to determine %python3_sitelib and %python3_sitearch, # so we only test known CPython schemes here and not PyPy for simplicity. if (match := re.match(r'.+python(\d+\.\d+)$', __python3)): x_y = match.group(1) - lines = rpm_eval(f'%py3_check_import {args}', **macors) + lines = rpm_eval(f'%py3_check_import {args}', **macros) # An equality check is a bit inflexible here, # every time we change the macro we need to change this test. @@ -707,11 +708,9 @@ def test_py3_check_import(args, imports, __python3, lib): # At least, let's make the lines saner to check: lines = [line.rstrip('\\').strip() for line in lines] expected = textwrap.dedent(fr""" - (cd TOPDIR && PATH="BUILDROOT/usr/bin:$PATH" PYTHONPATH="${{PYTHONPATH:-BUILDROOT/usr/{lib}/python{x_y}/site-packages:BUILDROOT/usr/lib/python{x_y}/site-packages}}" PYTHONDONTWRITEBYTECODE=1 - {__python3 or '/usr/bin/python3'} -c "import {imports}" - ) + {__python3 or '/usr/bin/python3'} -s RPMCONFIGDIR/redhat/import_all_modules.py {args} """) assert lines == expected.splitlines() diff --git a/tests/test_import_all_modules.py b/tests/test_import_all_modules.py new file mode 100644 index 0000000..490ef1d --- /dev/null +++ b/tests/test_import_all_modules.py @@ -0,0 +1,392 @@ +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']), + ] +) +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 diff --git a/tests/tests.yml b/tests/tests.yml index 1c0478a..11bb3ae 100644 --- a/tests/tests.yml +++ b/tests/tests.yml @@ -15,7 +15,7 @@ tests: - pytest: dir: . - run: ALTERNATE_PYTHON_VERSION=3.6 pytest -v + run: PYTHONPATH=/usr/lib/rpm/redhat ALTERNATE_PYTHON_VERSION=3.6 pytest -v - manual_byte_compilation: dir: . run: rpmbuild -ba pythontest.spec