`twine check` should guard against `python_requires` conflicting with the wheel tags
The Issue
With the sunset of Python 2, distribution maintainers should stop producing universal wheels with (ones with py2.py3 tag).
The solution is to drop universal = 1 or turn that into 0 (https://github.com/pypa/packaging.python.org/issues/726) but it's easy to miss this bit. So this results in people adding things like python_requires = >= 3.6 but still having that py2 tag in the wheels which is rather useless (and, well, incorrect).
The same could happen to platform-specific wheels specifying ABI3 with tags like abi3-py36 while having python_requires = >= 3.7, for example.
I propose improving the twine check command to detect such inconsistencies.
I'd like to voice my -1 -- universal just means "pure python and works on any version" and presumably will hold the same meaning when C-incompatible python 4.x rolls around. For the same reason that python_requires = <4 is bad I don't think this should be implemented.
@asottile that's what I was thinking until @gaborbernat pointed out that it's not exactly true. It is currently py2.py3 in the wheel names. How do you suggest we should lint the conflicting metadata?
It is true, or at at least what the documented point of --universal was: https://github.com/pypa/wheel/commit/4a1b292b73a20e71f3939decbbb771507da79ce7#diff-8d068e8797e88947c320f79e856c3e16a72b730124a8f9d7031e2c4680dfa534R110-R111
the metadata isn't conflicting, pip properly handles python_requires and will only consider the wheel on appropriate python 3 versions
equates to the tag "py2.py3".
This is hinting pip to attempt downloading the wheel under Python 2. @pradyunsg seems to think that this could cause problems with the old resolver (combined with Requires-Python: >=3.6, for example) because it doesn't know how to do backtracking.
Also, I think that it's not the config for the tool producing wheels that should be linted but the resulting metadata in the wheel.
I'd like to voice my -1 -- universal just means "pure python and works on any version" and presumably will hold the same meaning when C-incompatible python 4.x rolls around. For the same reason that
python_requires = <4is bad I don't think this should be implemented.
To achieve that intent the wheel tag this should map would need to be just py, not py2.py3 . because py2.py3 is not python 4 compatible tag.
It would only cause problems for pip versions that don't understand python_requires (<9) -- which at this point they have much much harder problems installing valid packages
what I'd rather see is if python2 is truly dead then wheel / pip / setuptools should stop producing py2 in the name instead of requiring me to remove valid metadata from my configuration (which I use to signal that my package is pure python) I'll have to annoyingly add back later
To achieve that intent the wheel tag this should map would need to be just
py, notpy2.py3. becausepy2.py3is not python 4 compatible tag.
I like this idea! let's push to make that happen
I mean, either universal should start meaning py or python 3+ packages should stop using it. The current universal translation to py2.py3 means generated packages end up in an undesirable state because they are incompatible with python 2. At the same time, they contain code that works only with Python 3+.
Can we get some opinion from at least:
- @pfmoore - as BDFL for packaging
- @pradyunsg - as pip maintainer (where this tag is consumed)
- @agronholm - as wheel maintainer (where this setting is used)
Thanks!
I think https://github.com/pypa/wheel/issues/396 is on the right track, except that I would rather abort the operation if such a discrepancy is detected because otherwise this could go unnoticed by the users.
As packaging BDFL, the wheel spec doesn't mention "universal" at all, so I'd consider that an implementation detail of the wheel package.
From pip's POV, we consume the tags, not "universal" directly. I'd be happy if pip were to add a check that the tags and Python-Requires were consistent, but I'd see that as a quality of implementation matter. For correctness, what matters is that pip only considers wheels with supported tags, and it rejects any where Python-Requires isn't satisfied. That's well-defined behaviour even if the tags and python-requires conflict.
Yep, universal is an implementation detail in the wheel package. This feature will be removed in the eventual 1.0.0 release. In the meantime, raising an error when a conflict is detected between python_requires and universal is my current game plan.
Yep,
universalis an implementation detail in thewheelpackage. This feature will be removed in the eventual 1.0.0 release. In the meantime, raising an error when a conflict is detected betweenpython_requiresanduniversalis my current game plan.
This will break a lot of CIs, rightfully though.
This will break a lot of CIs, rightfully though.
As a precaution we could check some of the most popular packages to make sure they have their settings right. I could probably write a script for this.
What information are you looking out for here? I mean, almost all of @asottile packages will fail essentially, and I'm sure even I have a few lying around (though I don't mind if the CI starts failing). You could in theory get https://hugovk.github.io/top-pypi-packages/, and download each wheel, check if the wheel tag is conflicting with python-reqires. But say 60% fail, what would you do then?
Pre-built wheels are not a problem, since wheel won't touch those. The problem lies with those who, for whatever reason, do their builds from source packages.
I would lessen the impact by submitting PRs to projects that have misconfigured their packaging setup.
I should also say that right now, there's nothing in any spec that disallows publishing a wheel with (for example) a Python tag of .py37 and a Python-Requires of >=3.8. Such a wheel is uninstallable, but it's not invalid. I see no reason to object if someone were to propose a spec change that tightened this up, but there isn't anything right now, so it's arguably not standards-compliant to reject such wheels. But it seems like a sensible thing to do, if you want to. It's certainly not mandated that you do so, but I'd have little sympathy for anyone who demanded that tools allow uninstallable wheels to be published 🙂
Wheels where the tags and requires-python describe different but overlapping sets of versions is a much harder problem, though (for a start, there's no set of tags that's equivalent to an open-ended requires-python like >= 3.7). I'd say that it's unlikely to be worth doing anything on that in a standards context. But if someone wants to try, I'm happy to listen to the debate and see where it goes 🙂
And that's about all that I think needs to be said from a standards perspective.
To achieve that intent the wheel tag this should map would need to be just
py
That would mean you'd need a brand new pip version (and brand new packaging version), which would never work on Python 2 since that's been dropped by both packages. Not against a "py" tag, but it's come too late for this. If it was implemented, it probably would need a new setting; [bdist_wheel] universal=1 is pretty arcane; why do you have to tell bdist_wheel something for a normal, pure Python package?
universal just means "pure python and works on any version"
It's very clearly documented that this means Python 2 + Python 3, from the first release note (wheel 0.10, "Define a new setup.cfg section [wheel]. universal=1 will apply the py2.py3-none-any tag for pure python wheels."), two years before the change linked above (which also still mentions py2.py3). It never was integrated with the Requires-Python, so it always meant "py2.py3" because that's what "universal" meant. Wheel already can detect pure-python; as far as I know, it always has (no Extension present); there's no reason to duplicate this information. It has been clearly documented that a Pure Python wheel is a pure Python wheel that does not support Python 2.
almost all of @asottile packages will fail essentially
He can change one package (setup-cfg-fmt) and they all will be instantly fixed, I expect ;) . And that will help clean up mine too if I have any laying around. :) (And I did have to fix a package about a month ago, so it does happen). That's one reason I really wanted to convince him that this should not be set. I miserably failed, he said it's about pure Python, not about Python 2.
I would lessen the impact by submitting PRs to projects that have misconfigured their packaging setup.
I did. Didn't get a happy response. https://github.com/pre-commit/pre-commit/pull/1832. "general word of advice: don't touch infrastructure or tooling for a project you don't maintain unless prompted by an issue asking explicitly to do so" and "the crusade to remove this flag is not correct and I closed this change because it is not welcome here".
a Python tag of .py37 and a Python-Requires of >=3.8. Such a wheel is uninstallable,
technically that could be installable since py37-none-any means purelib 3.7+ -- if instead that's cp37-cp37m then yes it is uninstallable
All older (not c) python3 tags install:
python3.9 -m pip debug -v
...
Compatible tags: 1776
...
py30-none-macosx_10_4_universal
cp39-none-any
py39-none-any
py3-none-any
py38-none-any
py37-none-any
py36-none-any
py35-none-any
py34-none-any
py33-none-any
py32-none-any
py31-none-any
py30-none-any
I would also like to deprecate the universal setting already, but what would the proper replacement be? Should wheels be universal by default if there is no python_requires? Probably not. Perhaps then a python_requires that includes Python 2.x?
There's an idea to set the lowest Python supported as the tag via Requires-Python here: https://github.com/pypa/wheel/issues/336 , by the way.
I would also like to deprecate the
universalsetting already, but what would the proper replacement be?
I don't see a strong point for producing py2.py3. It was a very special situation that hopefully won't be repeated, and most packages are now py3. I would actually rather hope py does get added and turned on in 3-5 years so we don't end up with py3.py4 if there's a Python 4; this should be handled by Requires-Python in the future, rather than tags.
I'm not against getting more packages using the python_requires setting. It's surprisingly rare in cibuildwheel's known users, about 50%.
technically that could be installable since py37-none-any means purelib 3.7+ -- if instead that's cp37-cp37m then yes it is uninstallable
Drat, that'll teach me to try to be clever and invent a "better" example (I was originally going with py2 rather than py37, which would also be uninstallable, I believe - although now I'm nervous even about that 🙂) And actually, there's no specification that mandates what compatibility tags a given distribution must say it supports, so from a purely theoretical standpoint, it's even more complex than that...
Let's just go with "it's possible to create wheels that are uninstallable" 😉
@asottile Do you still object to the removal of universal=1 in your projects?
@asottile Do you still object to the removal of
universal=1in your projects?
correct
Why? When it's clearly wrong to have this in a py3 only project?
I don't see why a user should be expected to make or understand some random universal=1 call to bdist_wheel in setup.cfg. It doesn't translate to PEP 621 configuration, and it is and was never needed to indicate "this is pure python" - the absence of Extensions does that. It was intended from day one (wheel 0.10.0 in 2012, when it was called [wheel] universal=1) to provide a way to go against the original design that Python 2 and Python 3 code could not be run without changes (read the Python 3.0 changelog for the sad statements like that). If we want a "pure_python = true" flag, then it could be added as such, and not as a misused bdist_wheel option; but it's not needed, we don't need a flag that says this is pure Python, and we already have a lot of cruft to just make a simple package (see flit and poetry for attempts to make things shorter/simpler).
For Python 4, we have Requires-Python and the knowledge that people are going to want to write code that runs in both Python 3 and 4 at the same time.
From the 3.0 release notes:
It is not recommended to try to write source code that runs unchanged under both Python 2.6 and 3.0; you’d have to use a very contorted coding style, e.g. avoiding print statements, metaclasses, and much more. If you are maintaining a library that needs to support both Python 2.6 and Python 3.0, the best approach is to modify step 3 above by editing the 2.6 version of the source code and running the 2to3 translator again, rather than editing the 3.0 version of the source code.
The universal=True option was a stopgap measure which was introduced long before requires_python became a thing.
bdist_wheel is also becoming an implementation detail, and not something users are expected to call by hand; it's called by pypa/pip and now by pypa/build. Having it in the config seems counter productive to that work.
Yes, and setuptools is likely to adopt the bdist_wheel command into itself in the future.