packaging icon indicating copy to clipboard operation
packaging copied to clipboard

`_parse_version_many` does not match PEP 508 or documentation

Open matt-phylum opened this issue 9 months ago • 2 comments

PEP 508 says that a version_many looks like version_one (wsp* ',' version_one)* and the documentation comment in _parse_version_many says that a version_many looks like (SPECIFIER (WS? COMMA WS? SPECIFIER)*)? (essentially the same thing, but optional).

However, the implementation of _parse_version_many actually parses something like version_one (wsp* ',' version_one)* wsp* (',' wsp*)? or (SPECIFIER (WS? COMMA WS? SPECIFIER)* WS? (COMMA WS?)?)?. An extra comma, possibly surrounded by whitespace, is accepted.

It's probably too late to fix the implementation. Packages like this one contain malformed requirements that are accepted by the implementation but not the documentation. The documentation should probably be changed to reflect reality instead.

from packaging._tokenizer import DEFAULT_RULES, Tokenizer
from packaging._parser import _parse_version_many

def parse(input: str) -> str:
    tokenizer = Tokenizer(input, rules=DEFAULT_RULES)
    _parse_version_many(tokenizer)
    return (tokenizer.source[:tokenizer.position], tokenizer.source[tokenizer.position:])

print(repr(parse(">=1.0")))
print(repr(parse(">=1.0 ")))
print(repr(parse(">=1.0,")))
print(repr(parse(">=1.0 ,")))
print(repr(parse(">=1.0 , ")))

produces

('>=1.0', '')
('>=1.0 ', '')
('>=1.0,', '')
('>=1.0 ,', '')
('>=1.0 , ', '')

It looks like the extra, empty specifier is accidentally removed by SpecifierSet when it tries to handle converting an empty string into an empty set.

from packaging.specifiers import SpecifierSet

print(SpecifierSet("")._specs)
print(SpecifierSet(">=1.0")._specs)
print(SpecifierSet(">=1.0,")._specs)

produces

frozenset()
frozenset({<Specifier('>=1.0')>})
frozenset({<Specifier('>=1.0')>})

Together, these behaviors cause trailing commas in requirements to be ignored, allowing invalid requirements to be installed by setuptools.

from packaging.requirements import Requirement

print(Requirement("a>=1.0,"))

produces

a>=1.0

matt-phylum avatar May 20 '24 16:05 matt-phylum