diff --git a/SOURCES/README.md b/SOURCES/README.md index 03c3fc0..888c144 100644 --- a/SOURCES/README.md +++ b/SOURCES/README.md @@ -4,10 +4,6 @@ pyproject RPM macros These macros allow projects that follow the Python [packaging specifications] to be packaged as RPMs. -They are still *provisional*: we can make non-backwards-compatible changes to -the API. -Please subscribe to Fedora's [python-devel list] if you use the macros. - They work for: * traditional Setuptools-based projects that use the `setup.py` file, @@ -17,20 +13,21 @@ They work for: These macros replace `%py3_build` and `%py3_install`, which only work with `setup.py`. [packaging specifications]: https://packaging.python.org/specifications/ -[python-devel list]: https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/ Usage ----- -To use these macros, first BuildRequire them: +To use these macros, first BuildRequire the devel package for the Python you +are building against. In Fedora, that's `python3-devel`. - BuildRequires: pyproject-rpm-macros + BuildRequires: python3-devel -Also BuildRequire the devel package for the Python you are building against. -In Fedora, that's `python3-devel`. -(In the future, we plan to make `python3-devel` itself require -`pyproject-rpm-macros`.) +The macros will be pulled in as a dependency on Fedora and EPEL 9+. +In other distributions you need to BuildRequire the macros as well: + + BuildRequires: python3-devel + BuildRequires: pyproject-rpm-macros Next, you need to generate more build dependencies (of your projects and the macros themselves) by running `%pyproject_buildrequires` in the @@ -40,7 +37,8 @@ the macros themselves) by running `%pyproject_buildrequires` in the %pyproject_buildrequires This will add build dependencies according to [PEP 517] and [PEP 518]. -To also add run-time and test-time dependencies, see the section below. +This also adds run-time dependencies by default and +can add test-time dependencies, see the section below. If you need more dependencies, such as non-Python libraries, BuildRequire them manually. @@ -66,17 +64,22 @@ And install the wheel in `%install` with `%pyproject_install`: Adding run-time and test-time dependencies ------------------------------------------ -To run tests in the `%check` section, the package's runtime dependencies -often need to also be included as build requirements. -This can be done using the `-r` flag: +To run tests or import checks in the `%check` section, +the package's runtime dependencies need to also be included as build requirements. - %generate_buildrequires - %pyproject_buildrequires -r +Hence, `%pyproject_buildrequires` also generates runtime dependencies by default. For this to work, the project's build system must support the [`prepare-metadata-for-build-wheel` hook](https://www.python.org/dev/peps/pep-0517/#prepare-metadata-for-build-wheel). The popular buildsystems (setuptools, flit, poetry) do support it. +This behavior can be disabled +(e.g. when the project's build system does not support it) +using the `-R` flag: + + %generate_buildrequires + %pyproject_buildrequires -R + For projects that specify test requirements using an [`extra` provide](https://packaging.python.org/specifications/core-metadata/#provides-extra-multiple-use), these can be added using the `-x` flag. @@ -226,6 +229,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 ----------------------------- @@ -256,6 +306,73 @@ These arguments are still required: Multiple subpackages are generated when multiple names are provided. +PROVISIONAL: Importing just-built (extension) modules in %build +--------------------------------------------------------------- + +Sometimes, it is desired to be able to import the *just-built* extension modules +in the `%build` section, e.g. to build the documentation with Sphinx. + + %build + %pyproject_wheel + ... build the docs here ... + +With pure Python packages, it might be possible to set `PYTHONPATH=${PWD}` or `PYTHONPATH=${PWD}/src`. +However, it is a bit more complicated with extension modules. + +The location of just-built modules might differ depending on Python version, architecture, pip version. +Hence, the macro `%{pyproject_build_lib}` exists to be used like this: + + %build + %pyproject_wheel + PYTHONPATH=%{pyproject_build_lib} ... build the docs here ... + +This macro is currently **provisional** and the behavior might change. +Please subscribe to Fedora's [python-devel list] if you use the macro. + +The `%{pyproject_build_lib}` macro expands to an Shell `$(...)` expression and does not work when put into single quotes (`'`). + +Depending on the pip version, the expanded value will differ: + +[python-devel list]: https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/ + +### New pip 21.3+ with in-tree-build (Fedora 36+) + +Always use the macro from the same directory where you called `%pyproject_wheel` from. +The value will expand to something like: + +* `/builddir/build/BUILD/%{name}-%{version}/build/lib.linux-x86_64-3.10` for wheels with extension modules +* `/builddir/build/BUILD/%{name}-%{version}/build/lib` for pure Python wheels + +If multiple wheels were built from the same directory, +some pure Python and some with extension modules, +the expanded value will be combined with `:`: + +* `/builddir/build/BUILD/%{name}-%{version}/build/lib.linux-x86_64-3.10:/builddir/build/BUILD/%{name}-%{version}/build/lib` + +If multiple wheels were built from different directories, +the value will differ depending on the current directory. + + +### Older pip with out-of-tree-build (Fedora 34, 35, and EL 9) + +The value will expand to something like: + +* `/builddir/build/BUILD/%{name}-%{version}/.pyproject-builddir/pip-req-build-xxxxxxxx/build/lib.linux-x86_64-3.10` for wheels with extension modules +* `/builddir/build/BUILD/%{name}-%{version}/.pyproject-builddir/pip-req-build-xxxxxxxx/build/lib` for pure Python wheels + +Note that the exact value is **not stable** between builds +(the `xxxxxxxx` part is randomly generated, +neither you should consider the `.pyproject-builddir` directory to remain stable). + +If multiple wheels are built, +the expanded value will always be combined with `:` regardless of the current directory, e.g.: + +* `/builddir/build/BUILD/%{name}-%{version}/.pyproject-builddir/pip-req-build-xxxxxxxx/build/lib.linux-x86_64-3.10:/builddir/build/BUILD/%{name}-%{version}/.pyproject-builddir/pip-req-build-yyyyyyyy/build/lib.linux-x86_64-3.10:/builddir/build/BUILD/%{name}-%{version}/.pyproject-builddir/pip-req-build-zzzzzzzz/build/lib` + +**Note:** If you manage to build some wheels with in-tree-build and some with out-of-tree-build option, +the expanded value will contain all relevant directories. + + Limitations ----------- diff --git a/SOURCES/compare_mandata.py b/SOURCES/compare_mandata.py new file mode 100644 index 0000000..08d4c6b --- /dev/null +++ b/SOURCES/compare_mandata.py @@ -0,0 +1,83 @@ +'''Check whether the manpage extensions and directories list hardcoded in brp-compress +are the same as the lists stored in pyproject_save_files.py. +There is an open issue for RPM to provide them both as macros: +https://github.com/rpm-software-management/rpm/issues/1865 +Once that happens, this script can be removed. +''' + +import argparse +import re +import sys + +from pathlib import PosixPath + +from pyproject_buildrequires import print_err +from pyproject_save_files import prepend_mandirs, MANPAGE_EXTENSIONS + + + +def read_brp_compress(filename): + + contents = filename.read_text() + # To avoid duplicity of the manpage extensions which are listed a few times + # in the source file, they are stored in set and then retyped to a sorted list + manpage_exts = sorted( + set(re.findall(r'\(?(\w+)\\+\)?\$?', contents)) + ) + + # Get rid of ${PREFIX} when extracting the manpage directories + mandirs = [ + entry.replace('.${PREFIX}', '/PREFIX') + for entry in contents.split() + if entry.startswith('.${PREFIX}') + ] + + return manpage_exts, sorted(mandirs) + + +def compare_mandirs(brp_compress_mandirs): + ''' + Check whether each of brp-compress mandirs entry is present in the list + stored in pyproject_save_files.py + ''' + + pyp_save_files_mandirs = sorted(prepend_mandirs(prefix='/PREFIX')) + if brp_compress_mandirs == pyp_save_files_mandirs: + return True + else: + print_err('Mandir lists don\'t match, update the list in pyproject_save_files.py') + print_err('brp-compress list:', brp_compress_mandirs) + print_err('pyproject_save_files list:', pyp_save_files_mandirs) + return False + + +def compare_manpage_extensions(brp_compress_manpage_exts): + ''' + Check whether each of brp-compress manpage extension is present in the list + stored in pyproject_save_files.py + ''' + + if brp_compress_manpage_exts == sorted(MANPAGE_EXTENSIONS): + return True + else: + print_err('Manpage extension lists don\'t match, update the list in pyproject_save_files.py') + print_err('brp-compress list:', brp_compress_manpage_exts) + print_err('pyproject_save_files list:', sorted(MANPAGE_EXTENSIONS)) + return False + + +def main(args): + src_manpage_exts, src_mandirs = read_brp_compress(args.filename) + extension_check_successful = compare_manpage_extensions(src_manpage_exts) + mandir_check_successful = compare_mandirs(src_mandirs) + if extension_check_successful and mandir_check_successful: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-f', '--filename', type=PosixPath, required=True, + help='Provide location of brp-compress file') + main(parser.parse_args()) diff --git a/SOURCES/macros.pyproject b/SOURCES/macros.pyproject index 6cb35f3..09bbe2d 100644 --- a/SOURCES/macros.pyproject +++ b/SOURCES/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 @@ -27,6 +28,25 @@ CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}" TMPDI } +%pyproject_build_lib %{expand:\\\ +$( +pyproject_build_lib=() +if [ -d build/lib.%{python3_platform}-%{python3_version} ]; then + pyproject_build_lib+=( "${PWD}/build/lib.%{python3_platform}-%{python3_version}" ) +fi +if [ -d build/lib ]; then + pyproject_build_lib+=( "${PWD}/build/lib" ) +fi +for directory in $(find "%{_pyproject_builddir}" -type d -wholename "%{_pyproject_builddir}/pip-req-build-*/build/lib.%{python3_platform}-%{python3_version}" 2>/dev/null); do + pyproject_build_lib+=( "${directory}" ) +done +for directory in $(find "%{_pyproject_builddir}" -type d -wholename "%{_pyproject_builddir}/pip-req-build-*/build/lib" 2>/dev/null); do + pyproject_build_lib+=( "${directory}" ) +done +echo $(IFS=:; echo "${pyproject_build_lib[*]}") +)} + + %pyproject_install() %{expand:\\\ specifier=$(ls %{_pyproject_wheeldir}/*.whl | xargs basename --multiple | sed -E 's/([^-]+)-([^-]+)-.+\\\.whl/\\\1==\\\2/') TMPDIR="%{_pyproject_builddir}" %{__python3} -m pip install --root %{buildroot} --no-deps --disable-pip-version-check --progress-bar off --verbose --ignore-installed --no-warn-script-location --no-index --no-cache-dir --find-links %{_pyproject_wheeldir} $specifier @@ -70,21 +90,34 @@ 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}" \\ --python-version "%{python3_version}" \\ --pyproject-record "%{_pyproject_record}" \\ + --prefix "%{_prefix}" \\ %{*} } +# -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} -%pyproject_buildrequires(rxtNe:) %{expand:\\\ +%pyproject_buildrequires(rRxtNe:) %{expand:\\\ +%{-R:%{-r:%{error:The -R and -r options are mutually exclusive}}} %{-N: %{-r:%{error:The -N and -r options are mutually exclusive}} %{-x:%{error:The -N and -x options are mutually exclusive}} @@ -92,6 +125,7 @@ fi %{-t:%{error:The -N and -t options are mutually exclusive}} } %{-e:%{expand:%global toxenv %(%{__python3} -s %{_rpmconfigdir}/redhat/pyproject_construct_toxenv.py %{?**})}} +echo 'pyproject-rpm-macros' # we already have this installed, but this way, it's repoqueryable echo 'python%{python3_pkgversion}-devel' echo 'python%{python3_pkgversion}dist(pip) >= 19' echo 'python%{python3_pkgversion}dist(packaging)' diff --git a/SOURCES/pyproject_buildrequires.py b/SOURCES/pyproject_buildrequires.py index 3b51ba8..5f55b01 100644 --- a/SOURCES/pyproject_buildrequires.py +++ b/SOURCES/pyproject_buildrequires.py @@ -127,6 +127,11 @@ class Requirements: print_err(f'Ignoring alien requirement:', requirement_str) return + # We need to always accept pre-releases as satisfying the requirement + # Otherwise e.g. installed cffi version 1.15.0rc2 won't even satisfy the requirement for "cffi" + # https://bugzilla.redhat.com/show_bug.cgi?id=2014639#c3 + requirement.specifier.prereleases = True + try: # TODO: check if requirements with extras are satisfied installed = self.get_installed_version(requirement.name) @@ -362,8 +367,12 @@ def main(argv): description='Generate BuildRequires for a Python project.' ) parser.add_argument( - '-r', '--runtime', action='store_true', - help='Generate run-time requirements', + '-r', '--runtime', action='store_true', default=True, + help='Generate run-time requirements (default, disable with -R)', + ) + parser.add_argument( + '-R', '--no-runtime', action='store_false', dest='runtime', + help="Don't generate run-time requirements (implied by -N)", ) parser.add_argument( '-e', '--toxenv', metavar='TOXENVS', action='append', @@ -400,6 +409,9 @@ def main(argv): args = parser.parse_args(argv) + if not args.use_build_system: + args.runtime = False + if args.toxenv: args.tox = True diff --git a/SOURCES/pyproject_buildrequires_testcases.yaml b/SOURCES/pyproject_buildrequires_testcases.yaml index 6d98fcf..5e4b5e5 100644 --- a/SOURCES/pyproject_buildrequires_testcases.yaml +++ b/SOURCES/pyproject_buildrequires_testcases.yaml @@ -30,6 +30,7 @@ No pyproject.toml, empty setup.py: installed: setuptools: 50 wheel: 1 + include_runtime: false setup.py: | expected: | python3dist(setuptools) >= 40.8 @@ -42,6 +43,7 @@ Default build system, empty setup.py: setuptools: 50 wheel: 1 toml: 1 + include_runtime: false pyproject.toml: | # empty setup.py: | @@ -159,13 +161,13 @@ Build system dependencies in pyproject.toml with extras: build-backend = "foo.build" expected: | python3dist(foo) - python3dist(bar) > 5 - python3dist(bar[baz]) > 5 + python3dist(bar) > 5.0 + python3dist(bar[baz]) > 5.0 (python3dist(ne) < 1 or python3dist(ne) > 1) python3dist(ge) >= 1.2 python3dist(le) <= 1.2.3 - python3dist(lt) < 1.2.3.4 - python3dist(gt) > 1.2.3.4.5 + python3dist(lt) < 1.2.3.4~~ + python3dist(gt) > 1.2.3.4.5.0 (python3dist(compatible) >= 0.4 with python3dist(compatible) < 0.5) python3dist(equal) = 0.5 python3dist(arbitrary-equal) = 0.6 @@ -174,7 +176,7 @@ Build system dependencies in pyproject.toml with extras: python3dist(multi) = 6 python3dist(multi[extras1]) = 6 python3dist(multi[extras2]) = 6 - ((python3dist(combo) < 3 or python3dist(combo) > 3) with python3dist(combo) < 5 with python3dist(combo) > 2) + ((python3dist(combo) < 3 or python3dist(combo) > 3) with python3dist(combo) < 5~~ with python3dist(combo) > 2.0) python3dist(py3) stderr_contains: "WARNING: Simplifying 'appdirs@https://github.com/ActiveState/appdirs/archive/8eacfa312d77aba28d483fbfb6f6fc54099622be.zip' to 'appdirs'." result: 0 @@ -193,7 +195,7 @@ Build system dependencies in pyproject.toml without extras: ] build-backend = "foo.build" expected: | - python3dist(bar) > 5 + python3dist(bar) > 5.0 python3dist(multi) = 6 result: 0 @@ -201,6 +203,7 @@ Default build system, build dependencies in setup.py: installed: setuptools: 50 wheel: 1 + include_runtime: false setup.py: | from setuptools import setup setup( @@ -223,7 +226,6 @@ Default build system, run dependencies in setup.py: setuptools: 50 wheel: 1 pyyaml: 1 - include_runtime: true setup.py: | from setuptools import setup setup( @@ -237,8 +239,8 @@ Default build system, run dependencies in setup.py: python3dist(wheel) python3dist(wheel) python3dist(pyyaml) - python3dist(inst) > 1 - python3dist(inst2) < 3 + python3dist(inst) > 1.0 + python3dist(inst2) < 3~~ result: 0 Run dependencies with extras (not selected): @@ -246,7 +248,6 @@ Run dependencies with extras (not selected): setuptools: 50 wheel: 1 pyyaml: 1 - include_runtime: true setup.py: &pytest_setup_py | # slightly abriged copy of pytest's setup.py from setuptools import setup @@ -430,7 +431,7 @@ Tox extras: python3dist(tox-current-env) >= 0.0.6 python3dist(toxdep) python3dist(inst) - python3dist(dep11) > 11 + python3dist(dep11) > 11.0 python3dist(dep12) python3dist(dep21) python3dist(dep22) @@ -470,8 +471,8 @@ Tox provision unsatisfied: python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(tox) >= 3.999 - python3dist(setuptools) > 40 - python3dist(wheel) > 2 + python3dist(setuptools) > 40.0 + python3dist(wheel) > 2.0 result: 0 Tox provision satisfied: @@ -504,7 +505,7 @@ Tox provision satisfied: python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(tox) >= 3.5 - python3dist(setuptools) > 40 + python3dist(setuptools) > 40.0 python3dist(toxdep1) python3dist(toxdep2) python3dist(inst) @@ -564,7 +565,7 @@ Default build system, met deps in requirements file: python3dist(ncclient) (python3dist(cryptography) < 1.5.2 or python3dist(cryptography) > 1.5.2) python3dist(paramiko) - (python3dist(sqlalchemy) < 1.1 with python3dist(sqlalchemy) >= 1.0.10) + (python3dist(sqlalchemy) < 1.1~~ with python3dist(sqlalchemy) >= 1.0.10) python3dist(setuptools) >= 40.8 python3dist(wheel) python3dist(wheel) @@ -716,3 +717,26 @@ Weird and complex requirements file: # but that's good enough: - "WARNING: Simplifying 'ugly@git+https://github.com/monty/spam.git@master#egg=ugly' to 'ugly'." result: 0 + + +Pre-releases are accepted: + installed: + setuptools: 50 + wheel: 1 + toml: 1 + cffi: 1.15.0rc2 + pyproject.toml: | + [build-system] + requires = [ + "setuptools", + "wheel", + "cffi", + ] + build-backend = "setuptools.build_meta" + expected: | + python3dist(setuptools) + python3dist(wheel) + python3dist(cffi) + python3dist(wheel) + stderr_contains: "Requirement satisfied: cffi" + result: 0 diff --git a/SOURCES/pyproject_convert.py b/SOURCES/pyproject_convert.py index 942dc21..ea01bc3 100644 --- a/SOURCES/pyproject_convert.py +++ b/SOURCES/pyproject_convert.py @@ -36,6 +36,12 @@ class RpmVersion(): self.pre = version._version.pre self.dev = version._version.dev self.post = version._version.post + # version.local is ignored as it is not expected to appear + # in public releases + # https://www.python.org/dev/peps/pep-0440/#local-version-identifiers + + def is_legacy(self): + return isinstance(self.version, str) def increment(self): self.version[-1] += 1 @@ -45,7 +51,7 @@ class RpmVersion(): return self def __str__(self): - if isinstance(self.version, str): + if self.is_legacy(): return self.version if self.epoch: rpm_epoch = str(self.epoch) + ':' @@ -68,6 +74,9 @@ def convert_compatible(name, operator, version_id): if version_id.endswith('.*'): return 'Invalid version' version = RpmVersion(version_id) + if version.is_legacy(): + # LegacyVersions are not supported in this context + return 'Invalid version' if len(version.version) == 1: return 'Invalid version' upper_version = RpmVersion(version_id) @@ -93,17 +102,29 @@ def convert_not_equal(name, operator, version_id): if version_id.endswith('.*'): version_id = version_id[:-2] version = RpmVersion(version_id) - lower_version = RpmVersion(version_id).increment() + if version.is_legacy(): + # LegacyVersions are not supported in this context + return 'Invalid version' + version_gt = RpmVersion(version_id).increment() + version_gt_operator = '>=' + # Prevent dev and pre-releases from satisfying a < requirement + version = '{}~~'.format(version) else: version = RpmVersion(version_id) - lower_version = version - return '({} < {} or {} > {})'.format( - name, version, name, lower_version) + version_gt = version + version_gt_operator = '>' + return '({} < {} or {} {} {})'.format( + name, version, name, version_gt_operator, version_gt) def convert_ordered(name, operator, version_id): if version_id.endswith('.*'): # PEP 440 does not define semantics for prefix matching # with ordered comparisons + # see: https://github.com/pypa/packaging/issues/320 + # and: https://github.com/pypa/packaging/issues/321 + # This style of specifier is officially "unsupported", + # even though it is processed. Support may be removed + # in version 21.0. version_id = version_id[:-2] version = RpmVersion(version_id) if operator == '>': @@ -114,6 +135,14 @@ def convert_ordered(name, operator, version_id): operator = '<' else: version = RpmVersion(version_id) + # For backwards compatibility, fallback to previous behavior with LegacyVersions + if not version.is_legacy(): + # Prevent dev and pre-releases from satisfying a < requirement + if operator == '<' and not version.pre and not version.dev and not version.post: + version = '{}~~'.format(version) + # Prevent post-releases from satisfying a > requirement + if operator == '>' and not version.pre and not version.dev and not version.post: + version = '{}.0'.format(version) return '{} {} {}'.format(name, operator, version) OPERATORS = {'~=': convert_compatible, diff --git a/SOURCES/pyproject_save_files.py b/SOURCES/pyproject_save_files.py index dac8b3f..6cd53e7 100644 --- a/SOURCES/pyproject_save_files.py +++ b/SOURCES/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 @@ -11,6 +12,28 @@ from importlib.metadata import Distribution # From RPM's build/files.c strtokWithQuotes delim argument RPM_FILES_DELIMETERS = ' \n\t' +# RPM hardcodes the lists of manpage extensions and directories, +# so we have to maintain separate ones :( +# There is an issue for RPM to provide the lists as macros: +# https://github.com/rpm-software-management/rpm/issues/1865 +# The original lists can be found here: +# https://github.com/rpm-software-management/rpm/blob/master/scripts/brp-compress +MANPAGE_EXTENSIONS = ['gz', 'Z', 'bz2', 'xz', 'lzma', 'zst', 'zstd'] +MANDIRS = [ + '/man/man*', + '/man/*/man*', + '/info', + '/share/man/man*', + '/share/man/*/man*', + '/share/info', + '/kerberos/man', + '/X11R6/man/man*', + '/lib/perl5/man/man*', + '/share/doc/*/man/man*', + '/lib/*/man/man*', + '/share/fish/man/man*', +] + class BuildrootPath(PurePosixPath): """ @@ -144,12 +167,131 @@ def add_lang_to_module(paths, module_name, path): return True +def prepend_mandirs(prefix): + """ + Return the list of man page directories prepended with the given prefix. + """ + return [str(prefix) + mandir for mandir in MANDIRS] + + +def normalize_manpage_filename(prefix, path): + """ + If a path is processed by RPM's brp-compress script, strip it of the extension + (if the extension matches one of the listed by brp-compress), + append '*' to the filename and return it. If not, return the unchanged path. + Rationale: https://docs.fedoraproject.org/en-US/packaging-guidelines/#_manpages + + Examples: + + >>> normalize_manpage_filename(PosixPath('/usr'), BuildrootPath('/usr/share/man/de/man1/linkchecker.1')) + BuildrootPath('/usr/share/man/de/man1/linkchecker.1*') + + >>> normalize_manpage_filename(PosixPath('/usr'), BuildrootPath('/usr/share/doc/en/man/man1/getmac.1')) + BuildrootPath('/usr/share/doc/en/man/man1/getmac.1*') + + >>> normalize_manpage_filename(PosixPath('/usr'), BuildrootPath('/usr/share/man/man8/abc.8.zstd')) + BuildrootPath('/usr/share/man/man8/abc.8*') + + >>> normalize_manpage_filename(PosixPath('/usr'), BuildrootPath('/usr/kerberos/man/dir')) + BuildrootPath('/usr/kerberos/man/dir') + + >>> normalize_manpage_filename(PosixPath('/usr'), BuildrootPath('/usr/kerberos/man/dir.1')) + BuildrootPath('/usr/kerberos/man/dir.1*') + + >>> normalize_manpage_filename(PosixPath('/usr'), BuildrootPath('/usr/bin/getmac')) + BuildrootPath('/usr/bin/getmac') + """ + + prefixed_mandirs = prepend_mandirs(prefix) + for mandir in prefixed_mandirs: + # "dir" is explicitly excluded by RPM + # https://github.com/rpm-software-management/rpm/blob/rpm-4.17.0-release/scripts/brp-compress#L24 + if fnmatch.fnmatch(str(path.parent), mandir) and path.name != "dir": + # "abc.1.gz2" -> "abc.1*" + if path.suffix[1:] in MANPAGE_EXTENSIONS: + return BuildrootPath(path.parent / (path.stem + "*")) + # "abc.1 -> abc.1*" + else: + return BuildrootPath(path.parent / (path.name + "*")) + else: + return path + + +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 + record_path, parsed_record_content, metadata, sitedirs, python_version, prefix ): """ 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 +307,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 +333,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 @@ -227,6 +373,7 @@ def classify_paths( if path.suffix == ".mo": add_lang_to_module(paths, None, path) or paths["other"]["files"].append(path) else: + path = normalize_manpage_filename(prefix, path) paths["other"]["files"].append(path) return paths @@ -240,7 +387,8 @@ def escape_rpm_path(path): unless we put it in "quotes". Or a literal % symbol in path might be expanded as a macro if not escaped. - Due to limitations in RPM, paths with spaces and double quotes are not supported. + Due to limitations in RPM, + some paths with spaces and other special characters are not supported. Examples: @@ -263,6 +411,11 @@ def escape_rpm_path(path): Traceback (most recent call last): ... NotImplementedError: ... + + >>> escape_rpm_path('/usr/share/data/spaces and [square brackets]') + Traceback (most recent call last): + ... + NotImplementedError: ... """ orig_path = path = str(path) if "%" in path: @@ -275,6 +428,10 @@ def escape_rpm_path(path): # As far as we know, RPM cannot list such file individually # See this thread http://lists.rpm.org/pipermail/rpm-list/2021-June/002048.html raise NotImplementedError(f'" symbol in path with spaces is not supported by %pyproject_save_files: {orig_path!r}') + if "[" in path or "]" in path: + # See https://bugzilla.redhat.com/show_bug.cgi?id=1990879 + # and https://github.com/rpm-software-management/rpm/issues/1749 + raise NotImplementedError(f'[ or ] symbol in path with spaces is not supported by %pyproject_save_files: {orig_path!r}') return f'"{path}"' return path @@ -381,16 +538,25 @@ def parse_varargs(varargs): >>> parse_varargs(['mod', 'mod.*']) Traceback (most recent call last): ... - ValueError: Attempted to use a namespaced package with dot in the glob: mod.*. ... + ValueError: Attempted to use a namespaced package with . in the glob: mod.*. ... >>> parse_varargs(['my.bad', '+bad']) Traceback (most recent call last): ... - ValueError: Attempted to use a namespaced package with dot in the glob: my.bad. ... + ValueError: Attempted to use a namespaced package with . in the glob: my.bad. ... + + >>> parse_varargs(['mod/submod']) + Traceback (most recent call last): + ... + ValueError: Attempted to use a namespaced package with / in the glob: mod/submod. ... """ include_auto = False globs = set() - + namespace_error_template = ( + "Attempted to use a namespaced package with {symbol} in the glob: {arg}. " + "That is not (yet) supported. Use {top} instead and see " + "https://bugzilla.redhat.com/1935266 for details." + ) for arg in varargs: if arg.startswith("+"): if arg == "+auto": @@ -399,11 +565,10 @@ def parse_varargs(varargs): raise ValueError(f"Invalid argument: {arg}") elif "." in arg: top, *_ = arg.partition(".") - msg = ( - f"Attempted to use a namespaced package with dot in the glob: {arg}. " - f"That is not (yet) supported. Use {top} instead and/or file a Bugzilla explaining your use case." - ) - raise ValueError(msg) + raise ValueError(namespace_error_template.format(symbol=".", arg=arg, top=top)) + elif "/" in arg: + top, *_ = arg.partition("/") + raise ValueError(namespace_error_template.format(symbol="/", arg=arg, top=top)) else: globs.add(arg) @@ -435,11 +600,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, prefix, 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 @@ -449,42 +616,51 @@ 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) paths_dict = classify_paths( - record_path, files, metadata, sitedirs, python_version + record_path, files, metadata, sitedirs, python_version, prefix ) final_file_list.extend( generate_file_list(paths_dict, globs, include_auto) ) + all_module_names.update(paths_dict["module_names"]) + + # Sort values, so they are always checked in the same order + all_module_names = sorted(all_module_names) - return final_file_list + 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, cli_args.python_version, cli_args.pyproject_record, + cli_args.prefix, 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) r.add_argument("--python-version", type=str, required=True) r.add_argument("--pyproject-record", type=PosixPath, required=True) + r.add_argument("--prefix", type=PosixPath, required=True) parser.add_argument("varargs", nargs="+") return parser diff --git a/SOURCES/pyproject_save_files_test_data.yaml b/SOURCES/pyproject_save_files_test_data.yaml index 06ff207..eeed5d3 100644 --- a/SOURCES/pyproject_save_files_test_data.yaml +++ b/SOURCES/pyproject_save_files_test_data.yaml @@ -217,6 +217,7 @@ classified: files: - /usr/bin/tldr - /usr/bin/tldr.py + - /usr/share/man/man1/tldr* ipykernel: metadata: dirs: @@ -376,6 +377,7 @@ classified: - /usr/share/jupyter/kernels/python3/logo-64x64.png - /usr/share/jupyter/kernels/python3/logo-32x32.png - /usr/share/jupyter/kernels/python3/kernel.json + - /usr/man/man5/ipykernel.5* zope: metadata: dirs: @@ -7421,6 +7423,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 +7444,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 +7491,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 +7519,8 @@ 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 + - /usr/share/man/man1/tldr* + - - tldr - - mistune - mistune - - '%dir /usr/lib64/python3.7/site-packages/mistune-0.8.3.dist-info' @@ -7498,6 +7532,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' @@ -7643,9 +7678,67 @@ dumped: - /usr/lib/python3.7/site-packages/ipykernel/tests/utils.py - /usr/lib/python3.7/site-packages/ipykernel/trio_runner.py - /usr/lib/python3.7/site-packages/ipykernel/zmqshell.py + - /usr/man/man5/ipykernel.5* - /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 +7759,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 +14435,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 +15398,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: @@ -14607,6 +15563,7 @@ records: ../../../bin/__pycache__/tldr.cpython-37.pyc,, ../../../bin/tldr,sha256=6MUiLCWhldmV8OelT2dvPgS7q5GFwuhvd6th0Bb-LH4,12766 ../../../bin/tldr.py,sha256=6MUiLCWhldmV8OelT2dvPgS7q5GFwuhvd6th0Bb-LH4,12766 + ../../../share/man/man1/tldr.bz2,sha256=xp_kqadh3PjDb4OaU8D8RQDcakrwl5AMmCnaOUV7ioo,10957 __pycache__/tldr.cpython-37.pyc,, tldr-0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 tldr-0.5.dist-info/LICENSE,sha256=q7quAfjDWCYKC_WRk_uaP6d2wwVpOpVjUSkv8l6H7xI,1075 @@ -14635,6 +15592,7 @@ records: ../../../share/jupyter/kernels/python3/kernel.json,sha256=7o0-HNZRKjrk7Fqb71O3gptCssqWqfd_sxw5FNFeYO0,143 ../../../share/jupyter/kernels/python3/logo-32x32.png,sha256=4ytcKCBy1xeIe2DacxeP3TWmXcPK6sunoCblpCVcyZc,1084 ../../../share/jupyter/kernels/python3/logo-64x64.png,sha256=XJBjtDbO3wVnSA_kh-zg0UeeqVRfMQy6k_oYTMurKQ0,2180 + ../../../man/man5/ipykernel.5,sha256=xp_kqadh3PjDb4OaU8D8RQDcakrwl5AMmCnaOUV7ioo,10957 __pycache__/ipykernel_launcher.cpython-37.pyc,, ipykernel-5.2.1.dist-info/COPYING.md,sha256=YMWypaSJDUjGk7i5CKSWdbUkuErBWn7ByVY-Bea__ho,2835 ipykernel-5.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 diff --git a/SOURCES/test_pyproject_buildrequires.py b/SOURCES/test_pyproject_buildrequires.py index 9588bba..15075c5 100644 --- a/SOURCES/test_pyproject_buildrequires.py +++ b/SOURCES/test_pyproject_buildrequires.py @@ -40,15 +40,16 @@ def test_data(case_name, capsys, tmp_path, monkeypatch): ) requirement_files = case.get('requirement_files', []) requirement_files = [open(f) for f in requirement_files] + use_build_system = case.get('use_build_system', True) try: generate_requires( get_installed_version=get_installed_version, - include_runtime=case.get('include_runtime', False), + include_runtime=case.get('include_runtime', use_build_system), extras=case.get('extras', []), toxenv=case.get('toxenv', None), generate_extras=case.get('generate_extras', False), requirement_files=requirement_files, - use_build_system=case.get('use_build_system', True), + use_build_system=use_build_system, ) except SystemExit as e: assert e.code == case['result'] diff --git a/SOURCES/test_pyproject_save_files.py b/SOURCES/test_pyproject_save_files.py index df2d05c..5dd0b27 100755 --- a/SOURCES/test_pyproject_save_files.py +++ b/SOURCES/test_pyproject_save_files.py @@ -8,8 +8,10 @@ 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 +PREFIX = Path("/usr") BINDIR = BuildrootPath("/usr/bin") DATADIR = BuildrootPath("/usr/share") SITELIB = BuildrootPath("/usr/lib/python3.7/site-packages") @@ -61,9 +63,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"]) @@ -105,19 +111,27 @@ def test_parse_record_tensorflow(): def remove_others(expected): - return [p for p in expected if not (p.startswith(str(BINDIR)) or p.endswith(".pth") or p.rpartition(' ')[-1].startswith(str(DATADIR)))] + return [ + p for p in expected + if not ( + p.startswith(str(BINDIR)) or + p.endswith(".pth") or + p.endswith("*") or + p.rpartition(' ')[-1].startswith(str(DATADIR)) + ) + ] @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 +144,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", @@ -143,67 +188,75 @@ def default_options(output, mock_root, pyproject_record): "--python-version", "3.7", # test data are for 3.7, "--pyproject-record", - str(pyproject_record) + str(pyproject_record), + "--prefix", + str(PREFIX), ] @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/SPECS/pyproject-rpm-macros.spec b/SPECS/pyproject-rpm-macros.spec index 07979c7..c69e573 100644 --- a/SPECS/pyproject-rpm-macros.spec +++ b/SPECS/pyproject-rpm-macros.spec @@ -6,13 +6,14 @@ License: MIT # Specify --with tests to run the tests e.g. on EPEL %bcond_with tests -# Keep the version at zero and increment only release. -# The release number indicates the feature set so only -# increment it when syncing or rebasing from Fedora. -# In other cases, such as backports, increment the point -# release. -Version: 0 -Release: 46%{?dist} +# The idea is to follow the spirit of semver +# Given version X.Y.Z: +# Increment X and reset Y.Z when there is a *major* incompatibility +# Increment Y and reset Z when new macros or features are added +# Increment Z when this is a bugfix or a cosmetic change +# Dropping support for EOL Fedoras is *not* considered a breaking change +Version: 1.0.0~rc1 +Release: 1%{?dist} # Macro files Source001: macros.pyproject @@ -29,6 +30,7 @@ Source106: pyproject_requirements_txt.py Source201: test_pyproject_buildrequires.py Source202: test_pyproject_save_files.py Source203: test_pyproject_requirements_txt.py +Source204: compare_mandata.py # Test data Source301: pyproject_buildrequires_testcases.yaml @@ -59,15 +61,14 @@ Requires: python-rpm-macros Requires: python-srpm-macros Requires: python3-rpm-macros +# We use the following tools outside of coreutils +Requires: /usr/bin/find +Requires: /usr/bin/sed %description These macros allow projects that follow the Python packaging specifications to be packaged as RPMs. -They are still provisional: we can make non-backwards-compatible changes to -the API. -Please subscribe to Fedora's python-devel list if you use the macros. - They work for: * traditional Setuptools-based projects that use the setup.py file, @@ -103,6 +104,9 @@ install -m 644 pyproject_requirements_txt.py %{buildroot}%{_rpmconfigdir}/redhat %check export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856356 %{python3} -m pytest -vv --doctest-modules + +# brp-compress is provided as an argument to get the right directory macro expansion +%{python3} compare_mandata.py -f %{_rpmconfigdir}/brp-compress %endif @@ -119,6 +123,42 @@ export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856 %license LICENSE %changelog +* Tue Jan 25 2022 Miro Hrončok - 1.0.0~rc1-1 +- Release version 1.0.0, first release candidate + +* Mon Jan 24 2022 Miro Hrončok - 0-55 +- %%pyproject_buildrequires: Generate BuildRequires for this package + This package is already installed, but this way, the resulting SRPM explicitly BuildRequires it + +* Wed Jan 19 2022 Karolina Surma - 0-54 +- Include compressed manpages to the package if flag '+auto' is provided to %%pyproject_save_files + +* Fri Jan 14 2022 Miro Hrončok - 0-53 +- %%pyproject_buildrequires: Make -r (include runtime) the default, use -R to opt-out + +* Sun Dec 19 2021 Gordon Messmer - 0-52 +- Handle legacy version specifiers that would previously raise exceptions. + +* Wed Dec 08 2021 Miro Hrončok - 0-51 +- Define provisional %%pyproject_build_lib + +* Mon Nov 1 2021 Gordon Messmer - 0-50 +- Improve handling of > operator, preventing post-release from satisfying most rpm requirements +- Improve handling of < operator, preventing pre-release from satisfying rpm requirement +- Improve handling of != operator with prefix matching, preventing pre-release from satisfying rpm requirements + +* 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 + +* Thu Sep 09 2021 Miro Hrončok - 0-47 +- %%pyproject_save_files: Expand the namespace error message, also display it with / +- %%pyproject_save_files: Add a workaround error for spaces and [brackets] + * Fri Jul 23 2021 Miro Hrončok - 0-46 - %%pyproject_buildrequires now fails when it encounters an invalid requirement - Rename %%_pyproject_ghost_distinfo and %%_pyproject_record to indicate they are private