diff --git a/README.md b/README.md index 03c3fc0..c484b79 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,53 @@ If you wish to rename, remove or otherwise change the installed files of a packa If possible, remove/rename such files in `%prep`. If not possible, avoid using `%pyproject_save_files` or edit/replace `%{pyproject_files}`. + +Performing an import check on all importable modules +---------------------------------------------------- + +If the upstream test suite cannot be used during the package build +and you use `%pyproject_save_files`, +you can benefit from the `%pyproject_check_import` macro. +If `%pyproject_save_files` is not used, calling `%pyproject_check_import` will fail. + +When `%pyproject_save_files` is invoked, +it creates a list of all valid and public (i.e. not starting with `_`) +importable module names found in the package. +This list is then usable by `%pyproject_check_import` which performs an import check for each listed module. +When a module fails to import, the build fails. + +The modules are imported from both installed and buildroot's `%{python3_sitearch}` +and `%{python3_sitelib}`, not from the current directory. + +Use the macro in `%check`: + + %check + %pyproject_check_import + +By using the `-e` flag, you can exclude module names matching the given glob(s) from the import check +(put it in single quotes to prevent Shell from expanding it). +The flag can be used repeatedly. +For example, to exclude all submodules ending with `config` and all submodules starting with `test`, you can use: + + %pyproject_check_import -e '*.config' -e '*.test*' + +There must be at least one module left for the import check; +if, as a result of greedy excluding, no modules are left to check, the check fails. + +When the `-t` flag is used, only top-level modules are checked, +qualified module names with a dot (`.`) are excluded. +If the modules detected by `%pyproject_save_files` are `requests`, `requests.models`, and `requests.packages`, this will only perform an import of `requests`: + + %pyproject_check_import -t + +The modifying flags should only be used when there is a valid reason for not checking all available modules. +The reason should be documented in a comment. + +The `%pyproject_check_import` macro also accepts positional arguments with +additional qualified module names to check, useful for example if some modules are installed manually. +Note that filtering by `-t`/`-e` also applies to the positional arguments. + + Generating Extras subpackages ----------------------------- diff --git a/macros.pyproject b/macros.pyproject index 6cb35f3..11fddc7 100644 --- a/macros.pyproject +++ b/macros.pyproject @@ -11,6 +11,7 @@ %_pyproject_builddir %{_builddir}%{?buildsubdir:/%{buildsubdir}}/.pyproject-builddir %pyproject_files %{_builddir}/pyproject-files +%_pyproject_modules %{_builddir}/pyproject-modules %_pyproject_ghost_distinfo %{_builddir}/pyproject-ghost-distinfo %_pyproject_record %{_builddir}/pyproject-record @@ -70,7 +71,8 @@ fi %pyproject_save_files() %{expand:\\\ %{__python3} %{_rpmconfigdir}/redhat/pyproject_save_files.py \\ - --output "%{pyproject_files}" \\ + --output-files "%{pyproject_files}" \\ + --output-modules "%{_pyproject_modules}" \\ --buildroot "%{buildroot}" \\ --sitelib "%{python3_sitelib}" \\ --sitearch "%{python3_sitearch}" \\ @@ -79,6 +81,16 @@ fi %{*} } +# -t - Process only top-level modules +# -e - Exclude the module names matching given glob, may be used repeatedly +%pyproject_check_import(e:t) %{expand:\\\ +if [ ! -f "%{_pyproject_modules}" ]; then + echo 'ERROR: %%%%pyproject_check_import only works when %%%%pyproject_save_files is used' >&2 + exit 1 +fi +%py3_check_import -f "%{_pyproject_modules}" %{?**} +} + %default_toxenv py%{python3_version_nodots} %toxenv %{default_toxenv} diff --git a/pyproject-rpm-macros.spec b/pyproject-rpm-macros.spec index d243e1b..ba94282 100644 --- a/pyproject-rpm-macros.spec +++ b/pyproject-rpm-macros.spec @@ -6,7 +6,7 @@ License: MIT # Keep the version at zero and increment only release Version: 0 -Release: 48%{?dist} +Release: 49%{?dist} # Macro files Source001: macros.pyproject @@ -113,6 +113,11 @@ export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856 %license LICENSE %changelog +* Tue Oct 19 2021 Karolina Surma - 0-49 +- %%pyproject_save_files: Save %%_pyproject_modules file with importable module names +- Introduce %%pyproject_check_import which passes %%_pyproject_modules to %%py3_check_import +- Introduce -t, -e filtering options to %%pyproject_check_import + * Sat Oct 16 2021 Miro HronĨok - 0-48 - %%pyproject_buildrequires: Accept installed pre-releases for all requirements - Fixes: rhbz#2014639 diff --git a/pyproject_save_files.py b/pyproject_save_files.py index 57d5548..e3ffc9f 100644 --- a/pyproject_save_files.py +++ b/pyproject_save_files.py @@ -4,6 +4,7 @@ import json import os from collections import defaultdict +from keyword import iskeyword from pathlib import PosixPath, PurePosixPath from importlib.metadata import Distribution @@ -144,12 +145,81 @@ def add_lang_to_module(paths, module_name, path): return True +def is_valid_module_name(s): + """Return True if a string is considered a valid module name and False otherwise. + + String must be a valid Python name, not a Python keyword and must not + start with underscore - we treat those as private. + Examples: + + >>> is_valid_module_name('module_name') + True + + >>> is_valid_module_name('12module_name') + False + + >>> is_valid_module_name('module-name') + False + + >>> is_valid_module_name('return') + False + + >>> is_valid_module_name('_module_name') + False + """ + if (s.isidentifier() and not iskeyword(s) and not s.startswith("_")): + return True + return False + + +def module_names_from_path(path): + """Get all importable module names from given path. + + Paths containing ".py" and ".so" files are considered importable modules, + and so their respective directories (ie. "foo/bar/baz.py": "foo", "foo.bar", + "foo.bar.baz"). + Paths containing invalid Python strings are discarded. + + Return set of all valid possibilities. + """ + # Discard all files that are not valid modules + if path.suffix not in (".py", ".so"): + return set() + + parts = list(path.parts) + + # Modify the file names according to their suffixes + if path.suffix == ".py": + parts[-1] = path.stem + elif path.suffix == ".so": + # .so files can have two suffixes - cut both of them + parts[-1] = PosixPath(path.stem).stem + + # '__init__' indicates a module but we don't want to import the actual file + # It's unclear whether there can be __init__.so files in the Python packages. + # The idea to implement this file was raised in 2008 on Python-ideas mailing list + # (https://mail.python.org/pipermail/python-ideas/2008-October/002292.html) + # and there are a few reports of people compiling their __init__.py to __init__.so. + # However it's not officially documented nor forbidden, + # so we're checking for the stem after stripping the suffix from the file. + if parts[-1] == "__init__": + del parts[-1] + + # For each part of the path check whether it's valid + # If not, discard the whole path - return an empty set + for path_part in parts: + if not is_valid_module_name(path_part): + return set() + else: + return {'.'.join(parts[:x+1]) for x in range(len(parts))} + + def classify_paths( record_path, parsed_record_content, metadata, sitedirs, python_version ): """ For each BuildrootPath in parsed_record_content classify it to a dict structure - that allows to filter the files for the %files section easier. + that allows to filter the files for the %files and %check section easier. For the dict structure, look at the beginning of this function's code. @@ -165,6 +235,7 @@ def classify_paths( }, "lang": {}, # %lang entries: [module_name or None][language_code] lists of .mo files "modules": defaultdict(list), # each importable module (directory, .py, .so) + "module_names": set(), # qualified names of each importable module ("foo.bar.baz") "other": {"files": []}, # regular %file entries we could not parse :( } @@ -190,6 +261,9 @@ def classify_paths( for sitedir in sitedirs: if sitedir in path.parents: + # Get only the part without sitedir prefix to classify module names + relative_path = path.relative_to(sitedir) + paths["module_names"].update(module_names_from_path(relative_path)) if path.parent == sitedir: if path.suffix == ".so": # extension modules can have 2 suffixes @@ -453,11 +527,13 @@ def dist_metadata(buildroot, record_path): dist = Distribution.at(real_dist_path) return dist.metadata -def pyproject_save_files(buildroot, sitelib, sitearch, python_version, pyproject_record, varargs): + +def pyproject_save_files_and_modules(buildroot, sitelib, sitearch, python_version, pyproject_record, varargs): """ Takes arguments from the %{pyproject_save_files} macro - Returns list of paths for the %files section + Returns tuple: list of paths for the %files section and list of module names + for the %check section """ # On 32 bit architectures, sitelib equals to sitearch # This saves us browsing one directory twice @@ -467,6 +543,7 @@ def pyproject_save_files(buildroot, sitelib, sitearch, python_version, pyproject parsed_records = load_parsed_record(pyproject_record) final_file_list = [] + all_module_names = set() for record_path, files in parsed_records.items(): metadata = dist_metadata(buildroot, record_path) @@ -477,12 +554,16 @@ def pyproject_save_files(buildroot, sitelib, sitearch, python_version, pyproject final_file_list.extend( generate_file_list(paths_dict, globs, include_auto) ) + all_module_names.update(paths_dict["module_names"]) - return final_file_list + # Sort values, so they are always checked in the same order + all_module_names = sorted(all_module_names) + + return final_file_list, all_module_names def main(cli_args): - file_section = pyproject_save_files( + file_section, module_names = pyproject_save_files_and_modules( cli_args.buildroot, cli_args.sitelib, cli_args.sitearch, @@ -491,13 +572,15 @@ def main(cli_args): cli_args.varargs, ) - cli_args.output.write_text("\n".join(file_section) + "\n", encoding="utf-8") + cli_args.output_files.write_text("\n".join(file_section) + "\n", encoding="utf-8") + cli_args.output_modules.write_text("\n".join(module_names) + "\n", encoding="utf-8") def argparser(): parser = argparse.ArgumentParser() r = parser.add_argument_group("required arguments") - r.add_argument("--output", type=PosixPath, required=True) + r.add_argument("--output-files", type=PosixPath, required=True) + r.add_argument("--output-modules", type=PosixPath, required=True) r.add_argument("--buildroot", type=PosixPath, required=True) r.add_argument("--sitelib", type=BuildrootPath, required=True) r.add_argument("--sitearch", type=BuildrootPath, required=True) diff --git a/pyproject_save_files_test_data.yaml b/pyproject_save_files_test_data.yaml index 06ff207..08dffd0 100644 --- a/pyproject_save_files_test_data.yaml +++ b/pyproject_save_files_test_data.yaml @@ -7421,6 +7421,19 @@ dumped: - /usr/lib64/python3.7/site-packages/tensorflow_core/tools/pip_package/__pycache__/__init__.cpython-37{,.opt-?}.pyc - /usr/lib64/python3.7/site-packages/tensorflow_core/tools/pip_package/__pycache__/setup.cpython-37{,.opt-?}.pyc - /usr/lib64/python3.7/site-packages/tensorflow_core/tools/pip_package/setup.py + - - tensorflow + - tensorflow_core + - tensorflow_core.lite + - tensorflow_core.lite.experimental + - tensorflow_core.python + - tensorflow_core.python.autograph + - tensorflow_core.python.autograph.converters + - tensorflow_core.python.autograph.converters.arg_defaults + - tensorflow_core.tools + - tensorflow_core.tools.common + - tensorflow_core.tools.docs + - tensorflow_core.tools.pip_package + - tensorflow_core.tools.pip_package.setup - - kerberos - ke?ber* - - '%dir /usr/lib64/python3.7/site-packages/kerberos-1.3.0.dist-info' @@ -7429,6 +7442,7 @@ dumped: - /usr/lib64/python3.7/site-packages/kerberos-1.3.0.dist-info/WHEEL - /usr/lib64/python3.7/site-packages/kerberos-1.3.0.dist-info/top_level.txt - /usr/lib64/python3.7/site-packages/kerberos.cpython-37m-x86_64-linux-gnu.so + - - kerberos - - requests - requests - - '%dir /usr/lib/python3.7/site-packages/requests' @@ -7475,6 +7489,22 @@ dumped: - /usr/lib/python3.7/site-packages/requests/status_codes.py - /usr/lib/python3.7/site-packages/requests/structures.py - /usr/lib/python3.7/site-packages/requests/utils.py + - - requests + - requests.adapters + - requests.api + - requests.auth + - requests.certs + - requests.compat + - requests.cookies + - requests.exceptions + - requests.help + - requests.hooks + - requests.models + - requests.packages + - requests.sessions + - requests.status_codes + - requests.structures + - requests.utils - - tldr - '*' - - '%dir /usr/lib/python3.7/site-packages/tldr-0.5.dist-info' @@ -7487,6 +7517,7 @@ dumped: - /usr/lib/python3.7/site-packages/tldr-0.5.dist-info/WHEEL - /usr/lib/python3.7/site-packages/tldr-0.5.dist-info/top_level.txt - /usr/lib/python3.7/site-packages/tldr.py + - - tldr - - mistune - mistune - - '%dir /usr/lib64/python3.7/site-packages/mistune-0.8.3.dist-info' @@ -7498,6 +7529,7 @@ dumped: - /usr/lib64/python3.7/site-packages/mistune-0.8.3.dist-info/top_level.txt - /usr/lib64/python3.7/site-packages/mistune.cpython-37m-x86_64-linux-gnu.so - /usr/lib64/python3.7/site-packages/mistune.py + - - mistune - - ipykernel - ipykernel - - '%dir /usr/lib/python3.7/site-packages/ipykernel' @@ -7646,6 +7678,63 @@ dumped: - /usr/share/jupyter/kernels/python3/kernel.json - /usr/share/jupyter/kernels/python3/logo-32x32.png - /usr/share/jupyter/kernels/python3/logo-64x64.png + - - ipykernel + - ipykernel.codeutil + - ipykernel.comm + - ipykernel.comm.comm + - ipykernel.comm.manager + - ipykernel.connect + - ipykernel.datapub + - ipykernel.displayhook + - ipykernel.embed + - ipykernel.eventloops + - ipykernel.gui + - ipykernel.gui.gtk3embed + - ipykernel.gui.gtkembed + - ipykernel.heartbeat + - ipykernel.inprocess + - ipykernel.inprocess.blocking + - ipykernel.inprocess.channels + - ipykernel.inprocess.client + - ipykernel.inprocess.constants + - ipykernel.inprocess.ipkernel + - ipykernel.inprocess.manager + - ipykernel.inprocess.socket + - ipykernel.inprocess.tests + - ipykernel.inprocess.tests.test_kernel + - ipykernel.inprocess.tests.test_kernelmanager + - ipykernel.iostream + - ipykernel.ipkernel + - ipykernel.jsonutil + - ipykernel.kernelapp + - ipykernel.kernelbase + - ipykernel.kernelspec + - ipykernel.log + - ipykernel.parentpoller + - ipykernel.pickleutil + - ipykernel.pylab + - ipykernel.pylab.backend_inline + - ipykernel.pylab.config + - ipykernel.serialize + - ipykernel.tests + - ipykernel.tests.test_async + - ipykernel.tests.test_connect + - ipykernel.tests.test_embed_kernel + - ipykernel.tests.test_eventloop + - ipykernel.tests.test_heartbeat + - ipykernel.tests.test_io + - ipykernel.tests.test_jsonutil + - ipykernel.tests.test_kernel + - ipykernel.tests.test_kernelspec + - ipykernel.tests.test_message_spec + - ipykernel.tests.test_pickleutil + - ipykernel.tests.test_serialize + - ipykernel.tests.test_start_kernel + - ipykernel.tests.test_zmq_shell + - ipykernel.tests.utils + - ipykernel.trio_runner + - ipykernel.zmqshell + - ipykernel_launcher - - zope - zope - - '%dir /usr/lib/python3.7/site-packages/zope' @@ -7666,6 +7755,10 @@ dumped: - /usr/lib/python3.7/site-packages/zope/event/__pycache__/tests.cpython-37{,.opt-?}.pyc - /usr/lib/python3.7/site-packages/zope/event/classhandler.py - /usr/lib/python3.7/site-packages/zope/event/tests.py + - - zope + - zope.event + - zope.event.classhandler + - zope.event.tests - - django - django - - '%dir /usr/lib/python3.7/site-packages/Django-3.0.7.dist-info' @@ -14338,6 +14431,821 @@ dumped: - /usr/lib/python3.7/site-packages/django/views/templates/technical_404.html - /usr/lib/python3.7/site-packages/django/views/templates/technical_500.html - /usr/lib/python3.7/site-packages/django/views/templates/technical_500.txt + - - django + - django.apps + - django.apps.config + - django.apps.registry + - django.conf + - django.conf.global_settings + - django.conf.locale + - django.conf.locale.ar + - django.conf.locale.ar.formats + - django.conf.locale.az + - django.conf.locale.az.formats + - django.conf.locale.bg + - django.conf.locale.bg.formats + - django.conf.locale.bn + - django.conf.locale.bn.formats + - django.conf.locale.bs + - django.conf.locale.bs.formats + - django.conf.locale.ca + - django.conf.locale.ca.formats + - django.conf.locale.cs + - django.conf.locale.cs.formats + - django.conf.locale.cy + - django.conf.locale.cy.formats + - django.conf.locale.da + - django.conf.locale.da.formats + - django.conf.locale.de + - django.conf.locale.de.formats + - django.conf.locale.de_CH + - django.conf.locale.de_CH.formats + - django.conf.locale.el + - django.conf.locale.el.formats + - django.conf.locale.en + - django.conf.locale.en.formats + - django.conf.locale.en_AU + - django.conf.locale.en_AU.formats + - django.conf.locale.en_GB + - django.conf.locale.en_GB.formats + - django.conf.locale.eo + - django.conf.locale.eo.formats + - django.conf.locale.es + - django.conf.locale.es.formats + - django.conf.locale.es_AR + - django.conf.locale.es_AR.formats + - django.conf.locale.es_CO + - django.conf.locale.es_CO.formats + - django.conf.locale.es_MX + - django.conf.locale.es_MX.formats + - django.conf.locale.es_NI + - django.conf.locale.es_NI.formats + - django.conf.locale.es_PR + - django.conf.locale.es_PR.formats + - django.conf.locale.et + - django.conf.locale.et.formats + - django.conf.locale.eu + - django.conf.locale.eu.formats + - django.conf.locale.fa + - django.conf.locale.fa.formats + - django.conf.locale.fi + - django.conf.locale.fi.formats + - django.conf.locale.fr + - django.conf.locale.fr.formats + - django.conf.locale.fy + - django.conf.locale.fy.formats + - django.conf.locale.ga + - django.conf.locale.ga.formats + - django.conf.locale.gd + - django.conf.locale.gd.formats + - django.conf.locale.gl + - django.conf.locale.gl.formats + - django.conf.locale.he + - django.conf.locale.he.formats + - django.conf.locale.hi + - django.conf.locale.hi.formats + - django.conf.locale.hr + - django.conf.locale.hr.formats + - django.conf.locale.hu + - django.conf.locale.hu.formats + - django.conf.locale.id + - django.conf.locale.id.formats + - django.conf.locale.it + - django.conf.locale.it.formats + - django.conf.locale.ja + - django.conf.locale.ja.formats + - django.conf.locale.ka + - django.conf.locale.ka.formats + - django.conf.locale.km + - django.conf.locale.km.formats + - django.conf.locale.kn + - django.conf.locale.kn.formats + - django.conf.locale.ko + - django.conf.locale.ko.formats + - django.conf.locale.lt + - django.conf.locale.lt.formats + - django.conf.locale.lv + - django.conf.locale.lv.formats + - django.conf.locale.mk + - django.conf.locale.mk.formats + - django.conf.locale.ml + - django.conf.locale.ml.formats + - django.conf.locale.mn + - django.conf.locale.mn.formats + - django.conf.locale.nb + - django.conf.locale.nb.formats + - django.conf.locale.nl + - django.conf.locale.nl.formats + - django.conf.locale.nn + - django.conf.locale.nn.formats + - django.conf.locale.pl + - django.conf.locale.pl.formats + - django.conf.locale.pt + - django.conf.locale.pt.formats + - django.conf.locale.pt_BR + - django.conf.locale.pt_BR.formats + - django.conf.locale.ro + - django.conf.locale.ro.formats + - django.conf.locale.ru + - django.conf.locale.ru.formats + - django.conf.locale.sk + - django.conf.locale.sk.formats + - django.conf.locale.sl + - django.conf.locale.sl.formats + - django.conf.locale.sq + - django.conf.locale.sq.formats + - django.conf.locale.sr + - django.conf.locale.sr.formats + - django.conf.locale.sr_Latn + - django.conf.locale.sr_Latn.formats + - django.conf.locale.sv + - django.conf.locale.sv.formats + - django.conf.locale.ta + - django.conf.locale.ta.formats + - django.conf.locale.te + - django.conf.locale.te.formats + - django.conf.locale.th + - django.conf.locale.th.formats + - django.conf.locale.tr + - django.conf.locale.tr.formats + - django.conf.locale.uk + - django.conf.locale.uk.formats + - django.conf.locale.uz + - django.conf.locale.uz.formats + - django.conf.locale.vi + - django.conf.locale.vi.formats + - django.conf.locale.zh_Hans + - django.conf.locale.zh_Hans.formats + - django.conf.locale.zh_Hant + - django.conf.locale.zh_Hant.formats + - django.conf.urls + - django.conf.urls.i18n + - django.conf.urls.static + - django.contrib + - django.contrib.admin + - django.contrib.admin.actions + - django.contrib.admin.apps + - django.contrib.admin.checks + - django.contrib.admin.decorators + - django.contrib.admin.exceptions + - django.contrib.admin.filters + - django.contrib.admin.forms + - django.contrib.admin.helpers + - django.contrib.admin.migrations + - django.contrib.admin.models + - django.contrib.admin.options + - django.contrib.admin.sites + - django.contrib.admin.templatetags + - django.contrib.admin.templatetags.admin_list + - django.contrib.admin.templatetags.admin_modify + - django.contrib.admin.templatetags.admin_urls + - django.contrib.admin.templatetags.base + - django.contrib.admin.templatetags.log + - django.contrib.admin.tests + - django.contrib.admin.utils + - django.contrib.admin.views + - django.contrib.admin.views.autocomplete + - django.contrib.admin.views.decorators + - django.contrib.admin.views.main + - django.contrib.admin.widgets + - django.contrib.admindocs + - django.contrib.admindocs.apps + - django.contrib.admindocs.middleware + - django.contrib.admindocs.urls + - django.contrib.admindocs.utils + - django.contrib.admindocs.views + - django.contrib.auth + - django.contrib.auth.admin + - django.contrib.auth.apps + - django.contrib.auth.backends + - django.contrib.auth.base_user + - django.contrib.auth.checks + - django.contrib.auth.context_processors + - django.contrib.auth.decorators + - django.contrib.auth.forms + - django.contrib.auth.handlers + - django.contrib.auth.handlers.modwsgi + - django.contrib.auth.hashers + - django.contrib.auth.management + - django.contrib.auth.management.commands + - django.contrib.auth.management.commands.changepassword + - django.contrib.auth.management.commands.createsuperuser + - django.contrib.auth.middleware + - django.contrib.auth.migrations + - django.contrib.auth.mixins + - django.contrib.auth.models + - django.contrib.auth.password_validation + - django.contrib.auth.signals + - django.contrib.auth.tokens + - django.contrib.auth.urls + - django.contrib.auth.validators + - django.contrib.auth.views + - django.contrib.contenttypes + - django.contrib.contenttypes.admin + - django.contrib.contenttypes.apps + - django.contrib.contenttypes.checks + - django.contrib.contenttypes.fields + - django.contrib.contenttypes.forms + - django.contrib.contenttypes.management + - django.contrib.contenttypes.management.commands + - django.contrib.contenttypes.management.commands.remove_stale_contenttypes + - django.contrib.contenttypes.migrations + - django.contrib.contenttypes.models + - django.contrib.contenttypes.views + - django.contrib.flatpages + - django.contrib.flatpages.admin + - django.contrib.flatpages.apps + - django.contrib.flatpages.forms + - django.contrib.flatpages.middleware + - django.contrib.flatpages.migrations + - django.contrib.flatpages.models + - django.contrib.flatpages.sitemaps + - django.contrib.flatpages.templatetags + - django.contrib.flatpages.templatetags.flatpages + - django.contrib.flatpages.urls + - django.contrib.flatpages.views + - django.contrib.gis + - django.contrib.gis.admin + - django.contrib.gis.admin.options + - django.contrib.gis.admin.widgets + - django.contrib.gis.apps + - django.contrib.gis.db + - django.contrib.gis.db.backends + - django.contrib.gis.db.backends.base + - django.contrib.gis.db.backends.base.adapter + - django.contrib.gis.db.backends.base.features + - django.contrib.gis.db.backends.base.models + - django.contrib.gis.db.backends.base.operations + - django.contrib.gis.db.backends.mysql + - django.contrib.gis.db.backends.mysql.base + - django.contrib.gis.db.backends.mysql.features + - django.contrib.gis.db.backends.mysql.introspection + - django.contrib.gis.db.backends.mysql.operations + - django.contrib.gis.db.backends.mysql.schema + - django.contrib.gis.db.backends.oracle + - django.contrib.gis.db.backends.oracle.adapter + - django.contrib.gis.db.backends.oracle.base + - django.contrib.gis.db.backends.oracle.features + - django.contrib.gis.db.backends.oracle.introspection + - django.contrib.gis.db.backends.oracle.models + - django.contrib.gis.db.backends.oracle.operations + - django.contrib.gis.db.backends.oracle.schema + - django.contrib.gis.db.backends.postgis + - django.contrib.gis.db.backends.postgis.adapter + - django.contrib.gis.db.backends.postgis.base + - django.contrib.gis.db.backends.postgis.const + - django.contrib.gis.db.backends.postgis.features + - django.contrib.gis.db.backends.postgis.introspection + - django.contrib.gis.db.backends.postgis.models + - django.contrib.gis.db.backends.postgis.operations + - django.contrib.gis.db.backends.postgis.pgraster + - django.contrib.gis.db.backends.postgis.schema + - django.contrib.gis.db.backends.spatialite + - django.contrib.gis.db.backends.spatialite.adapter + - django.contrib.gis.db.backends.spatialite.base + - django.contrib.gis.db.backends.spatialite.client + - django.contrib.gis.db.backends.spatialite.features + - django.contrib.gis.db.backends.spatialite.introspection + - django.contrib.gis.db.backends.spatialite.models + - django.contrib.gis.db.backends.spatialite.operations + - django.contrib.gis.db.backends.spatialite.schema + - django.contrib.gis.db.backends.utils + - django.contrib.gis.db.models + - django.contrib.gis.db.models.aggregates + - django.contrib.gis.db.models.fields + - django.contrib.gis.db.models.functions + - django.contrib.gis.db.models.lookups + - django.contrib.gis.db.models.proxy + - django.contrib.gis.db.models.sql + - django.contrib.gis.db.models.sql.conversion + - django.contrib.gis.feeds + - django.contrib.gis.forms + - django.contrib.gis.forms.fields + - django.contrib.gis.forms.widgets + - django.contrib.gis.gdal + - django.contrib.gis.gdal.base + - django.contrib.gis.gdal.datasource + - django.contrib.gis.gdal.driver + - django.contrib.gis.gdal.envelope + - django.contrib.gis.gdal.error + - django.contrib.gis.gdal.feature + - django.contrib.gis.gdal.field + - django.contrib.gis.gdal.geometries + - django.contrib.gis.gdal.geomtype + - django.contrib.gis.gdal.layer + - django.contrib.gis.gdal.libgdal + - django.contrib.gis.gdal.prototypes + - django.contrib.gis.gdal.prototypes.ds + - django.contrib.gis.gdal.prototypes.errcheck + - django.contrib.gis.gdal.prototypes.generation + - django.contrib.gis.gdal.prototypes.geom + - django.contrib.gis.gdal.prototypes.raster + - django.contrib.gis.gdal.prototypes.srs + - django.contrib.gis.gdal.raster + - django.contrib.gis.gdal.raster.band + - django.contrib.gis.gdal.raster.base + - django.contrib.gis.gdal.raster.const + - django.contrib.gis.gdal.raster.source + - django.contrib.gis.gdal.srs + - django.contrib.gis.geoip2 + - django.contrib.gis.geoip2.base + - django.contrib.gis.geoip2.resources + - django.contrib.gis.geometry + - django.contrib.gis.geos + - django.contrib.gis.geos.base + - django.contrib.gis.geos.collections + - django.contrib.gis.geos.coordseq + - django.contrib.gis.geos.error + - django.contrib.gis.geos.factory + - django.contrib.gis.geos.geometry + - django.contrib.gis.geos.io + - django.contrib.gis.geos.libgeos + - django.contrib.gis.geos.linestring + - django.contrib.gis.geos.mutable_list + - django.contrib.gis.geos.point + - django.contrib.gis.geos.polygon + - django.contrib.gis.geos.prepared + - django.contrib.gis.geos.prototypes + - django.contrib.gis.geos.prototypes.coordseq + - django.contrib.gis.geos.prototypes.errcheck + - django.contrib.gis.geos.prototypes.geom + - django.contrib.gis.geos.prototypes.io + - django.contrib.gis.geos.prototypes.misc + - django.contrib.gis.geos.prototypes.predicates + - django.contrib.gis.geos.prototypes.prepared + - django.contrib.gis.geos.prototypes.threadsafe + - django.contrib.gis.geos.prototypes.topology + - django.contrib.gis.management + - django.contrib.gis.management.commands + - django.contrib.gis.management.commands.inspectdb + - django.contrib.gis.management.commands.ogrinspect + - django.contrib.gis.measure + - django.contrib.gis.ptr + - django.contrib.gis.serializers + - django.contrib.gis.serializers.geojson + - django.contrib.gis.shortcuts + - django.contrib.gis.sitemaps + - django.contrib.gis.sitemaps.kml + - django.contrib.gis.sitemaps.views + - django.contrib.gis.utils + - django.contrib.gis.utils.layermapping + - django.contrib.gis.utils.ogrinfo + - django.contrib.gis.utils.ogrinspect + - django.contrib.gis.utils.srs + - django.contrib.gis.views + - django.contrib.humanize + - django.contrib.humanize.apps + - django.contrib.humanize.templatetags + - django.contrib.humanize.templatetags.humanize + - django.contrib.messages + - django.contrib.messages.api + - django.contrib.messages.apps + - django.contrib.messages.constants + - django.contrib.messages.context_processors + - django.contrib.messages.middleware + - django.contrib.messages.storage + - django.contrib.messages.storage.base + - django.contrib.messages.storage.cookie + - django.contrib.messages.storage.fallback + - django.contrib.messages.storage.session + - django.contrib.messages.utils + - django.contrib.messages.views + - django.contrib.postgres + - django.contrib.postgres.aggregates + - django.contrib.postgres.aggregates.general + - django.contrib.postgres.aggregates.mixins + - django.contrib.postgres.aggregates.statistics + - django.contrib.postgres.apps + - django.contrib.postgres.constraints + - django.contrib.postgres.fields + - django.contrib.postgres.fields.array + - django.contrib.postgres.fields.citext + - django.contrib.postgres.fields.hstore + - django.contrib.postgres.fields.jsonb + - django.contrib.postgres.fields.mixins + - django.contrib.postgres.fields.ranges + - django.contrib.postgres.fields.utils + - django.contrib.postgres.forms + - django.contrib.postgres.forms.array + - django.contrib.postgres.forms.hstore + - django.contrib.postgres.forms.jsonb + - django.contrib.postgres.forms.ranges + - django.contrib.postgres.functions + - django.contrib.postgres.indexes + - django.contrib.postgres.lookups + - django.contrib.postgres.operations + - django.contrib.postgres.search + - django.contrib.postgres.serializers + - django.contrib.postgres.signals + - django.contrib.postgres.utils + - django.contrib.postgres.validators + - django.contrib.redirects + - django.contrib.redirects.admin + - django.contrib.redirects.apps + - django.contrib.redirects.middleware + - django.contrib.redirects.migrations + - django.contrib.redirects.models + - django.contrib.sessions + - django.contrib.sessions.apps + - django.contrib.sessions.backends + - django.contrib.sessions.backends.base + - django.contrib.sessions.backends.cache + - django.contrib.sessions.backends.cached_db + - django.contrib.sessions.backends.db + - django.contrib.sessions.backends.file + - django.contrib.sessions.backends.signed_cookies + - django.contrib.sessions.base_session + - django.contrib.sessions.exceptions + - django.contrib.sessions.management + - django.contrib.sessions.management.commands + - django.contrib.sessions.management.commands.clearsessions + - django.contrib.sessions.middleware + - django.contrib.sessions.migrations + - django.contrib.sessions.models + - django.contrib.sessions.serializers + - django.contrib.sitemaps + - django.contrib.sitemaps.apps + - django.contrib.sitemaps.management + - django.contrib.sitemaps.management.commands + - django.contrib.sitemaps.management.commands.ping_google + - django.contrib.sitemaps.views + - django.contrib.sites + - django.contrib.sites.admin + - django.contrib.sites.apps + - django.contrib.sites.management + - django.contrib.sites.managers + - django.contrib.sites.middleware + - django.contrib.sites.migrations + - django.contrib.sites.models + - django.contrib.sites.requests + - django.contrib.sites.shortcuts + - django.contrib.staticfiles + - django.contrib.staticfiles.apps + - django.contrib.staticfiles.checks + - django.contrib.staticfiles.finders + - django.contrib.staticfiles.handlers + - django.contrib.staticfiles.management + - django.contrib.staticfiles.management.commands + - django.contrib.staticfiles.management.commands.collectstatic + - django.contrib.staticfiles.management.commands.findstatic + - django.contrib.staticfiles.management.commands.runserver + - django.contrib.staticfiles.storage + - django.contrib.staticfiles.testing + - django.contrib.staticfiles.urls + - django.contrib.staticfiles.utils + - django.contrib.staticfiles.views + - django.contrib.syndication + - django.contrib.syndication.apps + - django.contrib.syndication.views + - django.core + - django.core.asgi + - django.core.cache + - django.core.cache.backends + - django.core.cache.backends.base + - django.core.cache.backends.db + - django.core.cache.backends.dummy + - django.core.cache.backends.filebased + - django.core.cache.backends.locmem + - django.core.cache.backends.memcached + - django.core.cache.utils + - django.core.checks + - django.core.checks.caches + - django.core.checks.compatibility + - django.core.checks.database + - django.core.checks.messages + - django.core.checks.model_checks + - django.core.checks.registry + - django.core.checks.security + - django.core.checks.security.base + - django.core.checks.security.csrf + - django.core.checks.security.sessions + - django.core.checks.templates + - django.core.checks.translation + - django.core.checks.urls + - django.core.exceptions + - django.core.files + - django.core.files.base + - django.core.files.images + - django.core.files.locks + - django.core.files.move + - django.core.files.storage + - django.core.files.temp + - django.core.files.uploadedfile + - django.core.files.uploadhandler + - django.core.files.utils + - django.core.handlers + - django.core.handlers.asgi + - django.core.handlers.base + - django.core.handlers.exception + - django.core.handlers.wsgi + - django.core.mail + - django.core.mail.backends + - django.core.mail.backends.base + - django.core.mail.backends.console + - django.core.mail.backends.dummy + - django.core.mail.backends.filebased + - django.core.mail.backends.locmem + - django.core.mail.backends.smtp + - django.core.mail.message + - django.core.mail.utils + - django.core.management + - django.core.management.base + - django.core.management.color + - django.core.management.commands + - django.core.management.commands.check + - django.core.management.commands.compilemessages + - django.core.management.commands.createcachetable + - django.core.management.commands.dbshell + - django.core.management.commands.diffsettings + - django.core.management.commands.dumpdata + - django.core.management.commands.flush + - django.core.management.commands.inspectdb + - django.core.management.commands.loaddata + - django.core.management.commands.makemessages + - django.core.management.commands.makemigrations + - django.core.management.commands.migrate + - django.core.management.commands.runserver + - django.core.management.commands.sendtestemail + - django.core.management.commands.shell + - django.core.management.commands.showmigrations + - django.core.management.commands.sqlflush + - django.core.management.commands.sqlmigrate + - django.core.management.commands.sqlsequencereset + - django.core.management.commands.squashmigrations + - django.core.management.commands.startapp + - django.core.management.commands.startproject + - django.core.management.commands.test + - django.core.management.commands.testserver + - django.core.management.sql + - django.core.management.templates + - django.core.management.utils + - django.core.paginator + - django.core.serializers + - django.core.serializers.base + - django.core.serializers.json + - django.core.serializers.python + - django.core.serializers.pyyaml + - django.core.serializers.xml_serializer + - django.core.servers + - django.core.servers.basehttp + - django.core.signals + - django.core.signing + - django.core.validators + - django.core.wsgi + - django.db + - django.db.backends + - django.db.backends.base + - django.db.backends.base.base + - django.db.backends.base.client + - django.db.backends.base.creation + - django.db.backends.base.features + - django.db.backends.base.introspection + - django.db.backends.base.operations + - django.db.backends.base.schema + - django.db.backends.base.validation + - django.db.backends.ddl_references + - django.db.backends.dummy + - django.db.backends.dummy.base + - django.db.backends.dummy.features + - django.db.backends.mysql + - django.db.backends.mysql.base + - django.db.backends.mysql.client + - django.db.backends.mysql.compiler + - django.db.backends.mysql.creation + - django.db.backends.mysql.features + - django.db.backends.mysql.introspection + - django.db.backends.mysql.operations + - django.db.backends.mysql.schema + - django.db.backends.mysql.validation + - django.db.backends.oracle + - django.db.backends.oracle.base + - django.db.backends.oracle.client + - django.db.backends.oracle.creation + - django.db.backends.oracle.features + - django.db.backends.oracle.functions + - django.db.backends.oracle.introspection + - django.db.backends.oracle.operations + - django.db.backends.oracle.schema + - django.db.backends.oracle.utils + - django.db.backends.oracle.validation + - django.db.backends.postgresql + - django.db.backends.postgresql.base + - django.db.backends.postgresql.client + - django.db.backends.postgresql.creation + - django.db.backends.postgresql.features + - django.db.backends.postgresql.introspection + - django.db.backends.postgresql.operations + - django.db.backends.postgresql.schema + - django.db.backends.postgresql.utils + - django.db.backends.signals + - django.db.backends.sqlite3 + - django.db.backends.sqlite3.base + - django.db.backends.sqlite3.client + - django.db.backends.sqlite3.creation + - django.db.backends.sqlite3.features + - django.db.backends.sqlite3.introspection + - django.db.backends.sqlite3.operations + - django.db.backends.sqlite3.schema + - django.db.backends.utils + - django.db.migrations + - django.db.migrations.autodetector + - django.db.migrations.exceptions + - django.db.migrations.executor + - django.db.migrations.graph + - django.db.migrations.loader + - django.db.migrations.migration + - django.db.migrations.operations + - django.db.migrations.operations.base + - django.db.migrations.operations.fields + - django.db.migrations.operations.models + - django.db.migrations.operations.special + - django.db.migrations.operations.utils + - django.db.migrations.optimizer + - django.db.migrations.questioner + - django.db.migrations.recorder + - django.db.migrations.serializer + - django.db.migrations.state + - django.db.migrations.utils + - django.db.migrations.writer + - django.db.models + - django.db.models.aggregates + - django.db.models.base + - django.db.models.constants + - django.db.models.constraints + - django.db.models.deletion + - django.db.models.enums + - django.db.models.expressions + - django.db.models.fields + - django.db.models.fields.files + - django.db.models.fields.mixins + - django.db.models.fields.proxy + - django.db.models.fields.related + - django.db.models.fields.related_descriptors + - django.db.models.fields.related_lookups + - django.db.models.fields.reverse_related + - django.db.models.functions + - django.db.models.functions.comparison + - django.db.models.functions.datetime + - django.db.models.functions.math + - django.db.models.functions.mixins + - django.db.models.functions.text + - django.db.models.functions.window + - django.db.models.indexes + - django.db.models.lookups + - django.db.models.manager + - django.db.models.options + - django.db.models.query + - django.db.models.query_utils + - django.db.models.signals + - django.db.models.sql + - django.db.models.sql.compiler + - django.db.models.sql.constants + - django.db.models.sql.datastructures + - django.db.models.sql.query + - django.db.models.sql.subqueries + - django.db.models.sql.where + - django.db.models.utils + - django.db.transaction + - django.db.utils + - django.dispatch + - django.dispatch.dispatcher + - django.forms + - django.forms.boundfield + - django.forms.fields + - django.forms.forms + - django.forms.formsets + - django.forms.models + - django.forms.renderers + - django.forms.utils + - django.forms.widgets + - django.http + - django.http.cookie + - django.http.multipartparser + - django.http.request + - django.http.response + - django.middleware + - django.middleware.cache + - django.middleware.clickjacking + - django.middleware.common + - django.middleware.csrf + - django.middleware.gzip + - django.middleware.http + - django.middleware.locale + - django.middleware.security + - django.shortcuts + - django.template + - django.template.backends + - django.template.backends.base + - django.template.backends.django + - django.template.backends.dummy + - django.template.backends.jinja2 + - django.template.backends.utils + - django.template.base + - django.template.context + - django.template.context_processors + - django.template.defaultfilters + - django.template.defaulttags + - django.template.engine + - django.template.exceptions + - django.template.library + - django.template.loader + - django.template.loader_tags + - django.template.loaders + - django.template.loaders.app_directories + - django.template.loaders.base + - django.template.loaders.cached + - django.template.loaders.filesystem + - django.template.loaders.locmem + - django.template.response + - django.template.smartif + - django.template.utils + - django.templatetags + - django.templatetags.cache + - django.templatetags.i18n + - django.templatetags.l10n + - django.templatetags.static + - django.templatetags.tz + - django.test + - django.test.client + - django.test.html + - django.test.runner + - django.test.selenium + - django.test.signals + - django.test.testcases + - django.test.utils + - django.urls + - django.urls.base + - django.urls.conf + - django.urls.converters + - django.urls.exceptions + - django.urls.resolvers + - django.urls.utils + - django.utils + - django.utils.archive + - django.utils.asyncio + - django.utils.autoreload + - django.utils.baseconv + - django.utils.cache + - django.utils.crypto + - django.utils.datastructures + - django.utils.dateformat + - django.utils.dateparse + - django.utils.dates + - django.utils.datetime_safe + - django.utils.deconstruct + - django.utils.decorators + - django.utils.deprecation + - django.utils.duration + - django.utils.encoding + - django.utils.feedgenerator + - django.utils.formats + - django.utils.functional + - django.utils.hashable + - django.utils.html + - django.utils.http + - django.utils.inspect + - django.utils.ipv6 + - django.utils.itercompat + - django.utils.jslex + - django.utils.log + - django.utils.lorem_ipsum + - django.utils.module_loading + - django.utils.numberformat + - django.utils.regex_helper + - django.utils.safestring + - django.utils.termcolors + - django.utils.text + - django.utils.timesince + - django.utils.timezone + - django.utils.topological_sort + - django.utils.translation + - django.utils.translation.reloader + - django.utils.translation.template + - django.utils.translation.trans_null + - django.utils.translation.trans_real + - django.utils.tree + - django.utils.version + - django.utils.xmlutils + - django.views + - django.views.csrf + - django.views.debug + - django.views.decorators + - django.views.decorators.cache + - django.views.decorators.clickjacking + - django.views.decorators.csrf + - django.views.decorators.debug + - django.views.decorators.gzip + - django.views.decorators.http + - django.views.decorators.vary + - django.views.defaults + - django.views.generic + - django.views.generic.base + - django.views.generic.dates + - django.views.generic.detail + - django.views.generic.edit + - django.views.generic.list + - django.views.i18n + - django.views.static - - printrun - printrun - - '%dir /usr/lib/python3.7/site-packages/Printrun-2.0.0rc6.dist-info' @@ -14486,6 +15394,50 @@ dumped: - /usr/share/pronterface/reset.png - /usr/share/pronterface/zoom_in.png - /usr/share/pronterface/zoom_out.png + - - printrun + - printrun.eventhandler + - printrun.excluder + - printrun.gcodeplater + - printrun.gcoder + - printrun.gcview + - printrun.gl + - printrun.gl.libtatlin + - printrun.gl.libtatlin.actors + - printrun.gl.panel + - printrun.gl.trackball + - printrun.gui + - printrun.gui.bufferedcanvas + - printrun.gui.controls + - printrun.gui.graph + - printrun.gui.log + - printrun.gui.toolbar + - printrun.gui.utils + - printrun.gui.viz + - printrun.gui.widgets + - printrun.gui.xybuttons + - printrun.gui.zbuttons + - printrun.gviz + - printrun.injectgcode + - printrun.objectplater + - printrun.packer + - printrun.plugins + - printrun.plugins.sample + - printrun.power + - printrun.power.osx + - printrun.printcore + - printrun.projectlayer + - printrun.pronsole + - printrun.pronterface + - printrun.rpc + - printrun.settings + - printrun.spoolmanager + - printrun.spoolmanager.spoolmanager + - printrun.spoolmanager.spoolmanager_gui + - printrun.stlplater + - printrun.stltool + - printrun.stlview + - printrun.utils + - printrun.zscaper metadata: requests: diff --git a/test_pyproject_save_files.py b/test_pyproject_save_files.py index df2d05c..98b880e 100755 --- a/test_pyproject_save_files.py +++ b/test_pyproject_save_files.py @@ -8,6 +8,7 @@ from pyproject_preprocess_record import parse_record, read_record, save_parsed_r from pyproject_save_files import argparser, generate_file_list, BuildrootPath from pyproject_save_files import main as save_files_main +from pyproject_save_files import module_names_from_path DIR = Path(__file__).parent BINDIR = BuildrootPath("/usr/bin") @@ -61,9 +62,13 @@ def prepare_pyproject_record(tmp_path, package=None, content=None): @pytest.fixture -def output(tmp_path): +def output_files(tmp_path): return tmp_path / "pyproject_files" +@pytest.fixture +def output_modules(tmp_path): + return tmp_path / "pyproject_modules" + def test_parse_record_tldr(): record_path = BuildrootPath(TEST_RECORDS["tldr"]["path"]) @@ -109,15 +114,15 @@ def remove_others(expected): @pytest.mark.parametrize("include_auto", (True, False)) -@pytest.mark.parametrize("package, glob, expected", EXPECTED_FILES) -def test_generate_file_list(package, glob, expected, include_auto): +@pytest.mark.parametrize("package, glob, expected_files, expected_modules", EXPECTED_FILES) +def test_generate_file_list(package, glob, expected_files, include_auto, expected_modules): paths_dict = EXPECTED_DICT[package] modules_glob = {glob} if not include_auto: - expected = remove_others(expected) + expected_files = remove_others(expected_files) tested = generate_file_list(paths_dict, modules_glob, include_auto) - assert tested == expected + assert tested == expected_files def test_generate_file_list_unused_glob(): @@ -130,10 +135,41 @@ def test_generate_file_list_unused_glob(): assert "kerb" not in str(excinfo.value) -def default_options(output, mock_root, pyproject_record): +@pytest.mark.parametrize( + "path, expected", + [ + ("foo/bar/baz.py", {"foo", "foo.bar", "foo.bar.baz"}), + ("foo/bar.py", {"foo", "foo.bar"}), + ("foo.py", {"foo"}), + ("foo/bar.so.2", set()), + ("foo.cpython-37m-x86_64-linux-gnu.so", {"foo"}), + ("foo/_api/v2/__init__.py", set()), + ("foo/__init__.py", {"foo"}), + ("foo/_priv.py", set()), + ("foo/_bar/lib.so", set()), + ("foo/bar/baz.so", {"foo", "foo.bar", "foo.bar.baz"}), + ("foo/bar/baz.pth", set()), + ("foo/bar/baz.pyc", set()), + ("def.py", set()), + ("foo-bar/baz.py", set()), + ("foobar/12baz.py", set()), + ("foo/\nbar/baz.py", set()), + ("foo/+bar/baz.py", set()), + ("foo/__init__.cpython-39-x86_64-linux-gnu.so", {"foo"}), + ("foo/bar/__pycache__/abc.cpython-37.pyc", set()), + ], +) +def test_module_names_from_path(path, expected): + tested = Path(path) + assert module_names_from_path(tested) == expected + + +def default_options(output_files, output_modules, mock_root, pyproject_record): return [ - "--output", - str(output), + "--output-files", + str(output_files), + "--output-modules", + str(output_modules), "--buildroot", str(mock_root), "--sitelib", @@ -148,62 +184,68 @@ def default_options(output, mock_root, pyproject_record): @pytest.mark.parametrize("include_auto", (True, False)) -@pytest.mark.parametrize("package, glob, expected", EXPECTED_FILES) -def test_cli(tmp_path, package, glob, expected, include_auto, pyproject_record): +@pytest.mark.parametrize("package, glob, expected_files, expected_modules", EXPECTED_FILES) +def test_cli(tmp_path, package, glob, expected_files, expected_modules, include_auto, pyproject_record): prepare_pyproject_record(tmp_path, package) - output = tmp_path / "files" + output_files = tmp_path / "files" + output_modules = tmp_path / "modules" globs = [glob, "+auto"] if include_auto else [glob] - cli_args = argparser().parse_args([*default_options(output, tmp_path, pyproject_record), *globs]) + cli_args = argparser().parse_args([*default_options(output_files, output_modules, tmp_path, pyproject_record), *globs]) save_files_main(cli_args) if not include_auto: - expected = remove_others(expected) - tested = output.read_text() - assert tested == "\n".join(expected) + "\n" + expected_files = remove_others(expected_files) + tested_files = output_files.read_text() + assert tested_files == "\n".join(expected_files) + "\n" + + tested_modules = output_modules.read_text().split() + + assert tested_modules == expected_modules def test_cli_no_pyproject_record(tmp_path, pyproject_record): - output = tmp_path / "files" - cli_args = argparser().parse_args([*default_options(output, tmp_path, pyproject_record), "tldr*"]) + output_files = tmp_path / "files" + output_modules = tmp_path / "modules" + cli_args = argparser().parse_args([*default_options(output_files, output_modules, tmp_path, pyproject_record), "tldr*"]) with pytest.raises(FileNotFoundError): save_files_main(cli_args) -def test_cli_too_many_RECORDS(tldr_root, output, pyproject_record): +def test_cli_too_many_RECORDS(tldr_root, output_files, output_modules, pyproject_record): # Two calls to simulate how %pyproject_install process more than one RECORD file prepare_pyproject_record(tldr_root, content=("foo/bar/dist-info/RECORD", [])) prepare_pyproject_record(tldr_root, content=("foo/baz/dist-info/RECORD", [])) - cli_args = argparser().parse_args([*default_options(output, tldr_root, pyproject_record), "tldr*"]) + cli_args = argparser().parse_args([*default_options(output_files, output_modules, tldr_root, pyproject_record), "tldr*"]) with pytest.raises(FileExistsError): save_files_main(cli_args) -def test_cli_bad_argument(tldr_root, output, pyproject_record): +def test_cli_bad_argument(tldr_root, output_files, output_modules, pyproject_record): cli_args = argparser().parse_args( - [*default_options(output, tldr_root, pyproject_record), "tldr*", "+foodir"] + [*default_options(output_files, output_modules, tldr_root, pyproject_record), "tldr*", "+foodir"] ) with pytest.raises(ValueError): save_files_main(cli_args) -def test_cli_bad_option(tldr_root, output, pyproject_record): +def test_cli_bad_option(tldr_root, output_files, output_modules, pyproject_record): prepare_pyproject_record(tldr_root.parent, content=("RECORD1", [])) cli_args = argparser().parse_args( - [*default_options(output, tldr_root, pyproject_record), "tldr*", "you_cannot_have_this"] + [*default_options(output_files, output_modules, tldr_root, pyproject_record), "tldr*", "you_cannot_have_this"] ) with pytest.raises(ValueError): save_files_main(cli_args) -def test_cli_bad_namespace(tldr_root, output, pyproject_record): +def test_cli_bad_namespace(tldr_root, output_files, output_modules, pyproject_record): cli_args = argparser().parse_args( - [*default_options(output, tldr_root, pyproject_record), "tldr.didntread"] + [*default_options(output_files, output_modules, tldr_root, pyproject_record), "tldr.didntread"] ) with pytest.raises(ValueError): diff --git a/tests/python-distroinfo.spec b/tests/python-distroinfo.spec index 0630478..c2945cc 100644 --- a/tests/python-distroinfo.spec +++ b/tests/python-distroinfo.spec @@ -16,6 +16,8 @@ BuildRequires: git-core This package uses setuptools and pbr. It has setup_requires and tests that %%pyproject_buildrequires correctly handles that including runtime requirements. +Run %%pyproject_check_import with top-level modules filtering. + %package -n python3-distroinfo Summary: %{summary} @@ -43,6 +45,7 @@ Summary: %{summary} %check %pytest +%pyproject_check_import -t %files -n python3-distroinfo -f %{pyproject_files} diff --git a/tests/python-ipykernel.spec b/tests/python-ipykernel.spec index 92f3256..6ef7010 100644 --- a/tests/python-ipykernel.spec +++ b/tests/python-ipykernel.spec @@ -15,6 +15,12 @@ BuildRequires: python3-devel This package contains data files. Building this tests that data files are not listed when +auto is not used with %%pyproject_save_files. +Run %%pyproject_check_import on installed package and exclude unwanted modules +(if they're not excluded, build fails). +- We don't want to pull test dependencies just to check import +- The others fail to find `gi` and `matplotlib` which weren't declared + in the upstream metadata + %package -n python3-ipykernel Summary: %{summary} @@ -26,7 +32,7 @@ Summary: %{summary} %autosetup -p1 -n ipykernel-%{version} %generate_buildrequires -%pyproject_buildrequires +%pyproject_buildrequires -r %build %pyproject_wheel @@ -35,6 +41,9 @@ Summary: %{summary} %pyproject_install %pyproject_save_files 'ipykernel*' +auto +%check +%pyproject_check_import -e '*.test*' -e 'ipykernel.gui*' -e 'ipykernel.pylab.backend_inline' + %files -n python3-ipykernel -f %{pyproject_files} %license COPYING.md %doc README.md diff --git a/tests/python-mistune.spec b/tests/python-mistune.spec index 9967d88..a187c7c 100644 --- a/tests/python-mistune.spec +++ b/tests/python-mistune.spec @@ -20,6 +20,7 @@ Has a script (.py) and extension (.so) with identical name. Building this tests: - installing both a script and an extension with the same name - default build backend without pyproject.toml +Check %%pyproject_check_import basic functionality. %package -n python3-mistune @@ -47,6 +48,8 @@ Summary: %summary %check +%pyproject_check_import + # Internal check for our macros # making sure that pyproject_install outputs these files so that we can test behaviour of %%pyproject_save_files # when a package has multiple files with the same name (here script and extension)