Requirements and versions
A dependency line such as requests>=2.30,<3 is a requirement: a project name, an optional version specifier, and sometimes extras in brackets and an environment marker after ;. The packaging library parses them with the same rules pip uses.
from packaging import requirements
from packaging.version import Version
print("1.10" > "1.9")
print(Version("1.10") > Version("1.9"))
req = requirements.Requirement(
"rich[jupyter]>=13,<15")
print(req.name, req.extras, req.specifier)
print(req.specifier.contains("14.2"))==2.*allows any 2.x release,~=2.1means at least 2.1 but below 3, and!=excludes one version.- Versions compare by their numbers. As text,
"1.10"sorts before"1.9". - Pre-releases such as
2.0rc1are left out of ranges unless you ask for them. - A marker such as
sys_platform == "win32"limits a requirement to some machines, andreq.marker.evaluate()tests it on this one. - Names compare after
canonicalize_namefrompackaging.utils, soTyping_Extensionsandtyping-extensionsare the same project.
Your task
Write unmet(requirements, installed) taking requirement strings and a dict of installed names to versions, and returning a sorted list of the requirement names, as written, that are missing or out of range (an installed pre-release counts when in range). Compare names with canonicalize_name, and skip requirements whose marker does not apply here.
The explanation opens here once every check passes.