rbarrois / python-semanticversion

Semantic version comparison for Python (see http://semver.org/)
BSD 2-Clause "Simplified" License
281 stars 74 forks source link

Invalid NPM block #99

Closed gjedeer closed 4 years ago

gjedeer commented 4 years ago

I'm getting an error when parsing an expression from npmjs.com:

>>> import semantic_version
>>> semantic_version.__version__
'2.8.5'
>>> semantic_version.NpmSpec('>= 2.1.2 < 3')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/gdr/venv3/lib/python3.6/site-packages/semantic_version/base.py", line 618, in __init__
    self.clause = self._parse_to_clause(expression)
  File "/home/gdr/venv3/lib/python3.6/site-packages/semantic_version/base.py", line 1225, in _parse_to_clause
    return cls.Parser.parse(expression)
  File "/home/gdr/venv3/lib/python3.6/site-packages/semantic_version/base.py", line 1265, in parse
    raise ValueError("Invalid NPM block in %r: %r" % (expression, block))
ValueError: Invalid NPM block in '>= 2.1.2 < 3': '>='

The full version spec was:

>= 2.1.2 < 3

I did not make up this spec string - it was found when parsing package.json found on npm.

rbarrois commented 4 years ago

Reading the NPM grammar, the version is invalid:

range      ::= hyphen | simple ( ' ' simple ) * | ''
simple     ::= primitive | partial | tilde | caret

A range can be empty, an hyphen (a.b.c-d.e.f), or a set of simple blocks separated by a single space.

A simple block is defined as:

primitive  ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial

So: a comparator, immediately attached to a partial.

A partial is:

partial    ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
xr         ::= 'x' | 'X' | '*' | nr
nr         ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
qualifier  ::= ( '-' pre )? ( '+' build )?

I suggest raising the issue with the developer of that package ;)

gjedeer commented 4 years ago

It seems what they implement is different from their specification, then: https://github.com/npm/node-semver/blob/master/classes/range.js#L91 - but it looks like I can pre-process the range with the same regex node-semver uses internally.

gjedeer commented 4 years ago

So, just in case this comes useful for someone in the future, the npm logic translated to Python is as following:

comparator_trim_re = re.compile(r'(\s*)((?:<|>)?=?)\s*([v=\s]*([0-9]+)\.([0-9]+)\.([0-9]+)(?:-?((?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:[0-9]+|\d*[a-zA-Z-][a-zA-Z0-9-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?|[v=\s]*(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:\.(0|[1-9]\d*|x|X|\*)(?:(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][a-zA-Z0-9-]*))*)))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?)?)?)')

def clean_up_npm_range(range):
    return comparator_trim_re.sub(r'\1\2\3', range)