2

Following on from the question in Exclude certain dependency version ranges in setuptools/pip, I'd like to know if there is a means to use multiple ranges to define supported versions in a version specifier (defined in PEP440).

Take the following usecase:

I have a dependency for which I support versions 1.x, provided x is <3, or 2.x provided x is greater than 1 and less than or equal to 4. The v1.x series is still active, and new versions will continue to be released.

To provide a test case:

import packaging.specifiers as s


def test_specifier_multiple_ranges():
    spec = s.SpecifierSet('THE ANSWER GOES HERE')

    # A dependency is supported for:
    #       versions 1.x, provided x is <3
    #       OR
    #       versions 2.x provided x is greater than 1 and less than or equal to 3
    # The v1.x series is still active, and new versions will continue to
    # be released.
    possible_versions = [
        '1.1', '1.2', '1.3', '1.4', '2.0', '2.1', '2.2', '2.3', '2.4', '3.0'
    ]
    expected =  ['1.1', '1.2', '2.2', '2.3']

    assert list(spec.filter(possible_versions)) == expected

Note that I am not looking to exclude specific versions, they must be excluded via an upper range - the clause "The v1.x series is still active" rules that out, since at any moment a v1.5 (in the test case above) can appear, which it is already known to be not supported.

Note that in reading the bug report at https://github.com/pypa/pip/issues/2744, as well as PEP440's grammar definition (which lacks any OR capability from what I can see), it is my expectation that this is not possible. I will accept such an answer if it can explicitly reference this limitation (e.g. in PEP440), or it comes from an authoritative source.

pelson
  • 21,252
  • 4
  • 92
  • 99

1 Answers1

1

You're correct about the lack of OR capability, commas in a version specifier indicate logical AND:

The comma (",") is equivalent to a logical and operator: a candidate version must match all given version clauses in order to match the specifier as a whole.

It follows that the only mechanism from PEP440 that can be used to exclude multiple "inner" ranges is the prefix matching. See the version matching section of the spec:

By default, the version matching operator is based on a strict equality comparison: the specified version must be exactly the same as the requested version.

...

Prefix matching may be requested instead of strict comparison, by appending a trailing .* to the version identifier in the version matching clause. This means that additional trailing segments will be ignored when determining whether or not a version identifier matches the clause.

This doesn't entirely solve the problem, unless you have some additional information about when the 1.x series and 2.x series will be end-of-life. For example, if you know the final minor release of 1.x is 1.13 and the 2.x will be EOL after 2.7, then this prefix exclusion works:

specifiers  = [f"!= 1.{n}.*" for n in range(3, 14)]
specifiers += [f"!= 2.{n}.*" for n in [0, 1, *range(4, 8)]]
specifiers.append("< 3")
spec = s.SpecifierSet(", ".join(specifiers))

In practice, you could probably just guess some sufficiently high number for upper bounds, because software projects don't just go on releasing new features into older versions indefinitely.

wim
  • 338,267
  • 99
  • 616
  • 750