Skip to content

"compatible release" operator w/ M.NbX requirement: possible collision between PEP440 and implementation #413

@ghost

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.2 does not pass the ~=1.1b1 requirement.
  • However, 1.2 passes the ~=1.1b requirement
  • In other words, specification of X in ~=M.N.bX removes flexibility in N.

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


Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions