If you do not want to hear me rambling about the shortcomings of Python packaging metadata standards, feel free to skip to section 1.c.
I sometimes praise Python wheels as the best binary packaging format for cross-platform delivery. It truly is, at least to my limited knowledge. Via compatibility tags, the constraints for the target environment can be exclusively specified. Even though PEP 425#compressed-tag-sets was introduced
to allow for compact filenames of bdists that work with more than one compatibility tag triple,
the filenames are not always compact and it is not always possible to specify all environments a module can work on either. For instance, there's no direct answer for one of the FAQ:
What tag do I use if my distribution uses a feature exclusive to the newest version of Python?
I have seen workarounds like
cp35.cp36.cp37.cp38.cp39, which accept CPython from version 3.5, but not 3.10, which is the topic of the next Q&A:
Why isn't there a
.in the Python version number?
CPython has lasted 20+ years without a 3-digit major release. This should continue for some time. Other implementations may use
_as a delimiter, since both
.delimit the surrounding filename.
The platform tag was not carefully standardized either, which gives us inconsistent cyphers such as
A few years later, PEP 508#environment-markers came along after multiple iterations (PEP 345, PEP 426) with a much more human-readable format, which is essentially a Boolean algebra of comparisons. With this, the aforementioned CPython constraint can be correctly expressed as
platform_python_implementation == 'CPython' and python_version >= '3.5'
Unfortunately, they are only standardized for marking dependencies, usually to watch out for environments where a library is either unnecessary or unusable. But who watches the watchers? While it is possible to specify Python and platform tags when building wheels, the feature is undocumented and almost unknown. Therefore, many, if not most, platform-specific pure-Python wheels are tagged as
none platform and all their dependents needs to have the same redundant markers. Furthermore, due to the exclusive nature of compatibility tags, a GNU/Linux-only purelib wheel should be tagged with something like the following abomination:
Now imagine the filename of a wheel that only works on Unix-like systems.
uvloop is one and it is among the 500 most-downloaded on PyPI at the time of writing.
This article is, though, not going to sell you the idea of porting environment markers to top-level
WHEEL metadata. I should sit down with the PyPA people and discuss it one day, but the currently defined markers are lacking CPU and ABI information. The reason for this is that Python's standard library does not implement a way to retrieve such information, and for completeness, the operating system's version is not meaningfully available either. If we want to redo this right, I highly recommend looking into Zig's std.Target, which was designed from the start for cross-compilation. Whatever we will come up with, it shall not be done overnight.
Instead, right now I need to solve a more immediate problem. For the last few months, I have been working on IPWHL, a downstream repository for Python distributions. Since we enforce that all dependencies to be satisfied, packages with too strict or too lax environment declaration will break the automated tests, even though effectively the declared packages integrate well with each other. We came to a conclusion that adding information would improve the checks' accuracy and decided to use the floating cheeses as a testbed for top-level environment specifiers.
In short, we are going to merge the
Requires-Python with other environmental information to a single field
environment. Since other information may or may not be present in the filename, during metadata integrity check we can only check if what declared is included by what found from the original wheel.
extra, a Python environment can be represented as a 11-tuple of
(python_version, python_full_version, os_name, sys_platform, platform_release, platform_system, platform_version, platform_machine, platform_python_implementation, implementation_name, implementation_version)
Denote as the set of all strings, then the set of all environments . Let
the set of all marker expressions can be defined as
and the set of all environment markers can be recursively defined as
where is the top element in Boolean algebra, is a constant function and . With all Boolean expressions converted into one of their standard forms, we have the following alternative definitions of an environment marker
where , , , and an empty Boolean expression is implicitly .
Now it is possible to formalize inclusion for markers as
Thus, checking if a marker includes another is equivalent to evaluating the following expression, for all
where is whether the original marker (defined by , , and ) includes the declared one (defined by , , and ). Let
and assume are orthogonal, we have
where is the Boolean algebra bottom element. By De Morgan's laws,
Essentially, checking if
is unsatisfiable for all is a SAT problem which we can write a solver for. Indeed, the original expression could be formulated as a SAT, but it would be more complex due to the multiple dimensions of environments.
|||Built distributions, as opposed to sdist|
|||Bonus: try to sort |
|||Ignoring relations between two variables or two strings|
|||Or environment specifiers when not marking anything|