There were three problems:
- sys.version was not imported
- sys.version[:3] is not reliable on Python 3.10+
- distutils is deprecated on Python 3.10+
We were not hit by the missing import in Fedora because we only run the script
on .dist-info/.egg-info/.egg and not on .py files, so this if-branch never runs.
But when the script was fed with a .py path, it errored:
Traceback (most recent call last):
File "/usr/lib/rpm/pythondistdeps.py", line 344, in <module>
purelib = get_python_lib(standard_lib=0, plat_specific=0).split(version[:3])[0]
NameError: name 'version' is not defined
The sys.version[:3] thing kinda works for Python 3.10+ because *in this
particular case* splitting on '3.1' and taking the prefix yields the same
results as splitting on '3.10', but I consider that mere coincidence.
Finally, since the distutils import happened at module-level,
we got the Deprecation warning in all Fedora's Python packages:
/usr/lib/rpm/pythondistdeps.py:16: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12
Backported from https://github.com/rpm-software-management/python-rpm-packaging/commit/d12e039037
self.name in PathDistribution is a property in Python 3.10+ and thus we
can't redefine it as an instance variable. Instead we explicitly define
it as a property, which works on all supported Python versions.
Upstream change to importlib.metadata: https://github.com/rpm-software-management/rpm/pull/1317
Due to extras packages being hadled slightly differently by importlib,
one test case for this was added. And due to changes in handling
requires.txt files, comments were removed from the pyreq2rpm.tests
testing package.
Also because of the switch, we removed the dependency on setuptools and
added a dependency on packaging.
Note: Some packages with egg-info files might provide a different name
due to this change if there is a conflict between the filename and the
name in the metadata. Previously, the filename was sometimes used to
parse the name, now it is always the content of that file, which is what
packaging does, and thus also pip and other Python tooling. Currently,
this is known to affect only 1 package in Fedora (ntpsec).
The resulting script is different from upstream because of not yet upstreamed changes in Fedora:
- scripts/pythondistdeps: Rework error messages
- scripts/pythondistdeps: Add parameter --package-name
- scripts/pythondistdeps: Implement provides/requires for extras packages
- pythondistdeps.py: When parsing extras name, take the rightmost +
These changes are proposed in this upstream PR: https://github.com/rpm-software-management/rpm/pull/1546
https://fedoraproject.org/wiki/Changes/Disable_Python_2_Dist_RPM_Generators_and_Freeze_Python_2_Macros
The regex previously matched any Python version in a form of <single digit>.<at least one digit>.
Now it matches anything from 3.0 above: <single digit (3 or higher)>.<at least one digit>
It still does not match <multiple digits>.<at least one digit>, e.g. 11.0.
This is a breaking change, hence the version bump.
See https://bugzilla.redhat.com/show_bug.cgi?id=1853597#c11
pkg_resources from setuptools 42+ no longer only use platform.python_version(),
but also platform.python_version_tuple() -- this was updated in packaging 19.1+.
This fix makes it work again with both new and old setuptools,
hopefully for some while.
bf069fe9dd86a443f318
See https://src.fedoraproject.org/rpms/python-setuptools/pull-request/40
Strictly speaking, this is not an RPM generator, but:
- it generates provides
- it is tighly coupled with pythondistdeps.py
Usage:
1. Run `$ /usr/lib/rpm/pythonbundles.py .../vendored.txt`
2. Copy the output into the spec as a macro definition:
%global bundled %{expand:
Provides: bundled(python3dist(appdirs)) = 1.4.3
Provides: bundled(python3dist(packaging)) = 16.8
Provides: bundled(python3dist(pyparsing)) = 2.2.1
Provides: bundled(python3dist(six)) = 1.15
}
3. Use the macro to expand the provides
4. Verify the macro contents in %check:
%check
...
%{_rpmconfigdir}/pythonbundles.py src/_vendor/vendored.txt --compare-with '%{bundled}'
The %__python_magic filter suddenly got actually working with file 5.39:
Before:
file.cpython-38.opt-1.pyc: data
After:
file.cpython-38.opt-1.pyc: python 3.8 byte-compiled
Hence, the filter started to pick all Python files regardless of their location.
Later, in the actual generator, paths like this were considered:
/opt/usr/lib/python3.X/...
And generated requirements on python(abi).
We don't actually need to filter the files by file magic,
so we drop it to get the previously accidentally working behavior.
We could choose if the path and magic filters are applied as OR or AND.
However, we don't want either.
We actually want to mach any files in Python directories regardless of their magic.
We *could* filter by file type (and executable bit) for provides,
but that would require us to split the attr files into two.
Note that the code is completely unchanged except for the indentation
under the new if __name__ == "__main__":
Note that this change is necessary, but not sufficient to use the
RpmVersion class.
The init of the RpmVersion class will fail when called from an outside
script, because the `parse_version()` function is lazily imported from
the code outside the class. However, adding the import of
parse_version() to RpmVersion class is not done right now, because while
we would import it from `pkg_resources`, other scripts might want to
rely instead of the lightweight `packaging` module for the import. Thus
I'm leaving this conondrum to be addressed in the future.
--normalized-names-format FORMAT
FORMAT of normalized names can be `pep503` [default] or `legacy-dots` (dots allowed)
--normalized-names-provide-both
Provede both `pep503` and `legacy-dots` format of normalized names (useful for a transition period)
Notes from an attempted rewrite from pkg_resources to importlib.metadata in 2020:
1. While pkg_resources can open a metadata on a specified path
(Distribution.from_location()), importlib provides access only to
"installed package metadata", i.e. the the dist-info or egg-info directory
must be "discoverable", i.e. on the sys.path.
- Thankfully only the dist/egg-info directory must exist, the
corresponding Python module does not have to be present.
- The problems this causes:
(a) You have to manipulate the sys.path to add the specific location of
the site-packages directory inside the buildroot
(b) If you have package "foo" in this newly added directory on sys.path
and there is some problem and its dist/egg-info metadata are not found,
importlib.metadata continues searching the sys.path and may discover a
package with the same name (possibly same version) outside the
buildroot.
To get around this, you can manipulate the sys.path to remove all
other "site-packages" directories. But you have to leave the
standard library there, because importlib may import other modules
(in my testing: base64, quopri, random, socket, calendar, uu)
(c) I have not tested how well it works if you're ispecting metadata of
different Python versions than the one you run the script with
(especially Python 2 vs Python 3). This might also cause problems with
dependency specifiers (i.e. python_version != "3.4")
2. Handling of dependencies (requires) is problematic in importlib.metadata
- pkg_resources provides a way to separately list standard requires and a
requires for each "extras" category. importlib does not provide this, it
only spits out a list of strings, each string in the format:
- 'packaging>=14',
- 'towncrier>=18.5.0; extra == "docs"', or
- 'psutil<6,>=5.6.1; (python_version != "3.4") and extra == "testing"
you can either parse these with a regex (fragile) or use the external
`packaging` Python module. `packaging`, however, also doesn't have a great
support for figuring out extra dependencies, it provides the marker api:
- <Marker(\'python_version != "3.4" and extra == "testing"\')>
you can use Marker api to evaluate the condition, but not to parse.
For parsing you can access the private api Marker._markers:
- marker._markers=[[(<Variable('python_version')>, <Op('!=')>, \
<Value('3.4')>)], 'and', (<Variable('extra')>, <Op('==')>, \
<Value('testing')>)]
which beyond the problem of being private is also not very useful for
parsing due to its structure.
- pkg_resources also provides version parsing, which importlib does not
and `packaging` needs to be used
- importlib is part of the standard library, but packaging and its
2 runtime dependencies (pyparsing and six) are not, and therefore we
would go from 1 dependency to 3
3. A few minor issues, more in the next section about equivalents.
importlib.metadata.distribution equivalents of pkg_resources.Distribution attributes:
- pkg_resources: dist.py_version
importlib: # not implemented (but can be guessed from the /usr/lib/pythonXX.YY/ path)
- pkg_resources: dist.project_name
importlib: dist.metadata['name']
- pkg_resources: dist.key
importlib: # not implemented
- pkg_resources: dist.version
importlib: dist.version
- pkg_resources: dist.requires()
importlib: dist.requires # but returns strings with almost no parsing done, and also lists extras
- pkg_resources: dist.requires(extras=dist.extras)
importlib: # not implemented, has to be parsed from dist.requires
- pkg_resources: dist.get_entry_map('console_scripts')
importlib: [ep for ep in importlib.metadata.entry_points()['console_scripts'] if ep.name == pkg][0]
# I have not found a better way to get the console_scripts
- pkg_resources: dist.get_entry_map('gui_scripts')
importlib: # Presumably same as console_scripts, but untested