Reputation: 123
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
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
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
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