-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Originally reported by: jgehrcke (Bitbucket: jgehrcke, GitHub: jgehrcke)
This is about a possible collision between implementation and PEP440 or possibly a scenario which is not sufficiently specified in PEP440.
Short description of current behavior
Current behavior (as of setuptools 18.0.1):
1.2does not pass the~=1.1b1requirement.- However,
1.2passes the~=1.1brequirement - In other words, specification of
Xin~=M.N.bXremoves flexibility inN.
This is unexpected.
Test code
#!python
Python 3.4.3 (default, Jul 19 2015, 22:30:27)
>>> import setuptools; print(setuptools.__version__)
18.0.1
>>> from pkg_resources import parse_version, parse_requirements
>>> requirement = list(parse_requirements("project~=1.1b1"))[0]
>>> testversion = parse_version("1.2")
>>> testversion in requirement
False
>>> requirement = list(parse_requirements("project~=1.1b"))[0]
>>> testversion in requirement
True
(I came up with this quick test method after digging into the pkg_resources code, and am pretty sure that this is what actually is invoked behind the scenes when using e.g. pip with setuptools; please correct me if I am wrong.)
What PEP440 says about this
The relevant section clearly is https://www.python.org/dev/peps/pep-0440/#compatible-release
Let's go through a couple of statements
The specified version identifier must be in the standard format described in Version scheme
I think 1.1b1 complies with this standard, whereas 1.1b does not. The section of the PEP where the scheme is described: https://www.python.org/dev/peps/pep-0440/#version-scheme
If a pre-release, post-release or developmental release is named in a compatible release clause as V.N.suffix , then the suffix is ignored when determining the required prefix match
Now, that's critical. Step-by-step:
If a pre-release,
1.1b1 is a valid pre-release version number, as explicitly covered by the examples in https://www.python.org/dev/peps/pep-0440/#pre-releases
then the suffix is ignored when determining the required prefix match
b1 is a valid suffix, as specified by https://www.python.org/dev/peps/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering
One example is provided, saying that
~= 1.4.5a4 does translate to >= 1.4.5a4 AND == 1.4.*
This makes sense. So, this example covers the case for three components ("major.minor.micro").
Here, in my example, we have just two components ("major.minor"). That's the only difference. Translating from the given example and from what is described in words, I expect the following behavior:
~= 1.1b1 SHOULD translate to >= 1.1b1 AND == 1.*
There is another example in the relevant section, saying that
~= 2.2.post3 does translate to >= 2.2.post3 AND == 2.*. Most notably this is a three-component notation which allows for flexibility in the second component.
The examples as well as the exact wording ("the suffix is ignored") provide almost 100 % certainty that the current behavior violates the spec. What do you think?
Where this is relevant
Of course a mismatch between implementation and spec is relevant per se, but I also have a use case: gevent has recently released 1.1b1. The newest release of my dependency, gipc, should work with the gevent versions which will be released in the near future, whereas these can be expected: 1.1bX, 1.1, 1.1.X, 1.2, ... -- all of this I want to cover, but I clearly want to declare incompatibility with a putative gevent 2.X.
This was motivation for me to dig into PEP440 (https://www.python.org/dev/peps/pep-0440), and to especially look at the compatible release operator ~=. I did not want to purely trust the specification but actually test things with the most recent setuptools release (18.0.1).
In my specific use case there are for sure very simple workarounds, and one of them is to just use ~=1.1b. Works in my case, but this is not a valid version number and a very simple example of a situation where this does not help is if b1 is incompatible, but b2 is compatible. In this case, specifying ~=1.1b2 would be helpful:
#!python
>>> requirement = list(parse_requirements("project~=1.1b2"))[0]
>>> parse_version("1.1.b1") in requirement
False
>>> parse_version("1.1.b2") in requirement
True
This topic clearly requires some thorough double-checking which is why this text got so lengthy. I hope that it is easy to read nevertheless.
Cheers,
Jan-Philip Gehrcke