FutureGadget
FutureGadget

Reputation: 123

Is there a way to programmatically confirm that a python package version satisfies a requirements specifier?

I am trying to find whether there is a way to take an installed package and version and check whether it satisfies a requirements spec.

For example, if I have the package pip==20.0.2, I want the program to do the following:

CheckReqSpec("pip==20.0.2", "pip>=19.0.0")  -> True
CheckReqSpec("pip==20.0.2", "pip<=20.1")    -> True
CheckReqSpec("pip==20.0.2", "pip~=20.0.0")  -> True
CheckReqSpec("pip==20.0.2", "pip>20.0.2")   -> False

I found that pkg_resources.extern.packaging has version.parse, which is useful for comparing different versions greater than or less than, but requirement specs can be very complex, and there are operators like ~= that are not standard mathematical operators.

The setuptools docs has this example:

PickyThing<1.6,>1.9,!=1.9.6,<2.0a0,==2.4c1

Is there an existing way to do this check, or an easy way to make my own?

edit: The ~= in particular is difficult, especially if the specs are input as a variable. * in the version requirement is also hard to figure out, since

 version.parse("20.0.*") == version.parse("20.0.1") # False
 version.parse("20.0.*") < version.parse("20.0.0")  # True 
 version.parse("20.0.*") < version.parse("20.1.1")  # True 
 version.parse("20.0.*") >= version.parse("20.0.0") # False

Upvotes: 11

Views: 1191

Answers (3)

wim
wim

Reputation: 362847

Using pkg_resources (from setuptools) as an API is now deprecated, and will cause warnings at import time:

$ python3 -W always -c 'from pkg_resources import Requirement'
<string>:1: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html

Instead, we can parse a requirement using packaging (which pkg_resources was using internally anyway). Check requirement name is matching with an equality comparison, and check requirement version is within a specifier set using in:

>>> from packaging.requirements import Requirement
>>> req = Requirement("pip~=20.0.0")
>>> pin = "pip==20.0.2"
>>> name, version = pin.split("==")
>>> name == req.name and version in req.specifier
True

Post releases work. Pre-releases have to be opted in for explicitly.

>>> "20.0.0post1" in req.specifier
True
>>> req.specifier.contains("20.0.1b3")
False
>>> req.specifier.contains("20.0.1b3", prereleases=True)
True

Note: a top-level packaging installation may be at a different version than the packaging version which pip vendors and uses internally. If you need to guarantee the packaging APIs are matching pip's behavior exactly, you could import the Requirement type from pip's vendored subpackage directly:

from pip._vendor.packaging.requirements import Requirement

Or, if importing from a private submodule scares you, then install packaging at top-level to the exact same version which your pip version is currently vendoring. Check your pip version (with pip --version) and then check the corresponding packaging version which pip vendors. For example, if your pip version is 23.2.1 you may check in:

https://github.com/pypa/pip/blob/23.2.1/src/pip/_vendor/vendor.txt

Here you will see that pip==23.2.1 vendors an older version at packaging==21.3.

Upvotes: 10

sinoroc
sinoroc

Reputation: 22370

I would recommend packaging, which could be used like the following:

>>> import packaging.requirements
>>> import packaging.version
>>> packaging.version.parse('20.0.2') in packaging.requirements.Requirement('pip>=19.0.0').specifier
True
>>> packaging.version.parse('20.0.2') in packaging.requirements.Requirement('pip~=20.0').specifier
True
>>> packaging.requirements.Requirement('pip==20.0.*').specifier.contains('20.0.2')
True
>>> packaging.requirements.Requirement('pip==20.0.*').specifier.contains('21')
False
>>> packaging.requirements.Requirement('PickyThing<1.6,>1.9,!=1.9.6,<2.0a0,==2.4c1').specifier
<SpecifierSet('!=1.9.6,<1.6,<2.0a0,==2.4c1,>1.9')>

Upvotes: 2

felipe
felipe

Reputation: 8035

Perhaps packaging?

from packaging import version

version.parse("20.0.2") > version.parse("19.0.0")   # True
version.parse("20.0.2") <= version.parse("20.1")    # True
version.parse("20.0.2") >= version.parse("20.0.0")  # True
version.parse("20.0.2") > version.parse("20.0.2")   # False

Upvotes: 1

Related Questions