From f4ce03c140b655f2c9a58709b25e020ce910f087 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sun, 4 Jul 2021 15:09:21 +1000 Subject: [PATCH 01/10] Add tests to handle weird whitespace --- tests/test_npm.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_npm.py b/tests/test_npm.py index 2102cd8..e9c26e5 100644 --- a/tests/test_npm.py +++ b/tests/test_npm.py @@ -71,6 +71,7 @@ def test_spec(self): '1.2 - 2.3.4': '>=1.2.0 <=2.3.4', '1.2.3 - 2.3': '>=1.2.3 <2.4.0', '1.2.3 - 2': '>=1.2.3 <3', + '1.2.3 - 2': '>=1.2.3 <3', # X-Ranges '*': '>=0.0.0', @@ -89,6 +90,8 @@ def test_spec(self): '~0.2': '>=0.2.0 <0.3.0', '~0': '>=0.0.0 <1.0.0', '~1.2.3-beta.2': '>=1.2.3-beta.2 <1.3.0', + '~ 1.2.3': '>=1.2.3 <1.3.0', + '~ 1.2.3': '>=1.2.3 <1.3.0', # Caret ranges '^1.2.3': '>=1.2.3 <2.0.0', @@ -101,6 +104,20 @@ def test_spec(self): '^0.0': '>=0.0.0 <0.1.0', '^1.x': '>=1.0.0 <2.0.0', '^0.x': '>=0.0.0 <1.0.0', + '^ 1.2.3': '>=1.2.3 <2.0.0', + '^ 1.2.3': '>=1.2.3 <2.0.0', + + # Weird whitespace ranges + '>= 1.2.3': '>=1.2.3', + '>= 1.2.3': '>=1.2.3', + ' >=1.2.3 <2.0.0 ': '>=1.2.3 <2.0.0', + '>= 1.2.3 < 2.0.0': '>=1.2.3 <2.0.0', + '>=1.2.3 < 2.0.0': '>=1.2.3 <2.0.0', + '>= 1.2.3 < 2.0.0': '>=1.2.3 <2.0.0', + '>= 1.2.3 < 2.0.0': '>=1.2.3 <2.0.0', + ' >=1.2.3 <2.0.0 ': '>=1.2.3 <2.0.0', + '1.2.7 || >=1.2.9 <2.0.0': '1.2.7 || >=1.2.9 <2.0.0', + '1.2.7 || >= 1.2.9 < 2.0.0': '1.2.7 || >=1.2.9 <2.0.0', } def test_expand(self): From 7b0092617a2ecbd1b4511190c3baf7c8c77fdca7 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 12:51:06 +1000 Subject: [PATCH 02/10] Test for spaces between simple spec clauses. --- tests/test_npm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_npm.py b/tests/test_npm.py index e9c26e5..41fa15a 100644 --- a/tests/test_npm.py +++ b/tests/test_npm.py @@ -110,6 +110,7 @@ def test_spec(self): # Weird whitespace ranges '>= 1.2.3': '>=1.2.3', '>= 1.2.3': '>=1.2.3', + '>=1.2.3 <2.0.0': '>=1.2.3 <2.0.0', ' >=1.2.3 <2.0.0 ': '>=1.2.3 <2.0.0', '>= 1.2.3 < 2.0.0': '>=1.2.3 <2.0.0', '>=1.2.3 < 2.0.0': '>=1.2.3 <2.0.0', From 77ef19a8dbdb88b38c8e3d8c9a4689006c360e84 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 12:51:30 +1000 Subject: [PATCH 03/10] NpmSpec parsing to allow spaces between simple clauses. --- semantic_version/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/semantic_version/base.py b/semantic_version/base.py index 871ccb0..530d5f3 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -1256,10 +1256,10 @@ def parse(cls, expression): subclauses = [] if cls.HYPHEN in group: low, high = group.split(cls.HYPHEN, 2) - subclauses = cls.parse_simple('>=' + low) + cls.parse_simple('<=' + high) + subclauses = cls.parse_simple('>=' + low.strip()) + cls.parse_simple('<=' + high.strip()) else: - blocks = group.split(' ') + blocks = group.split() for block in blocks: if not cls.NPM_SPEC_BLOCK.match(block): raise ValueError("Invalid NPM block in %r: %r" % (expression, block)) From 2fc126d93f6438ffdaabb5d0805bfee458528251 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 14:30:32 +1000 Subject: [PATCH 04/10] NpmSpec to allow whitespace after operator. --- semantic_version/base.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/semantic_version/base.py b/semantic_version/base.py index 530d5f3..5672bd4 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -1230,15 +1230,21 @@ class Parser: NUMBER = r'x|X|\*|0|[1-9][0-9]*' PART = r'[a-zA-Z0-9.-]*' + OP = r'<|<=|=|>=|>|\^|~' NPM_SPEC_BLOCK = re.compile(r""" ^(?:v)? # Strip optional initial v - (?P<|<=|>=|>|=|\^|~|) # Operator, can be empty + (?P{op}|) # Operator, can be empty (?P{nb})(?:\.(?P{nb})(?:\.(?P{nb}))?)? (?:-(?P{part}))? # Optional re-release (?:\+(?P{part}))? # Optional build - $""".format(nb=NUMBER, part=PART), + $""".format(nb=NUMBER, part=PART, op=OP), re.VERBOSE, ) + OP_RE = re.compile(r""" + ^{op}$ # A standalone operator, cannot be empty + """.format(op=OP), + re.VERBOSE, + ) @classmethod def range(cls, operator, target): @@ -1260,11 +1266,17 @@ def parse(cls, expression): else: blocks = group.split() + maybe_prepend_op = None for block in blocks: - if not cls.NPM_SPEC_BLOCK.match(block): + block = maybe_prepend_op + block if maybe_prepend_op else block + if cls.NPM_SPEC_BLOCK.match(block): + subclauses.extend(cls.parse_simple(block)) + maybe_prepend_op = None + elif maybe_prepend_op is None and cls.OP_RE.match(block): + maybe_prepend_op = block + else: raise ValueError("Invalid NPM block in %r: %r" % (expression, block)) - subclauses.extend(cls.parse_simple(block)) prerelease_clauses = [] non_prerel_clauses = [] From 4f72d2fc8f3b599c78a518eb06aa37fe2d56dbf9 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 15:24:03 +1000 Subject: [PATCH 05/10] Add more tests for invalid, weird npm expansions. --- tests/test_npm.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_npm.py b/tests/test_npm.py index 41fa15a..a4bdda0 100644 --- a/tests/test_npm.py +++ b/tests/test_npm.py @@ -66,22 +66,36 @@ def test_spec(self): self.assertNotIn(base.Version(version), base.NpmSpec(spec)) expansions = { + # Basic + '>1.2.3': '>1.2.3', + '<1.2.3': '<1.2.3', + '<=1.2.3': '<=1.2.3', + # Hyphen ranges '1.2.3 - 2.3.4': '>=1.2.3 <=2.3.4', '1.2 - 2.3.4': '>=1.2.0 <=2.3.4', + '1.2 - 2.3.5': '>=1.2.0 <2.3.6', '1.2.3 - 2.3': '>=1.2.3 <2.4.0', '1.2.3 - 2': '>=1.2.3 <3', '1.2.3 - 2': '>=1.2.3 <3', # X-Ranges '*': '>=0.0.0', + '>=*': '>=0.0.0', '1.x': '>=1.0.0 <2.0.0', '1.2.x': '>=1.2.0 <1.3.0', '': '*', + '||': '*', + 'x': '*', '1': '1.x.x', '1.x.x': '>=1.0.0 <2.0.0', '1.2': '1.2.x', + # Partial GT LT Ranges + '>1': '>=2.0.0', + '>1.2': '>=1.3.0', + '<1': '<1.0.0', + # Tilde ranges '~1.2.3': '>=1.2.3 <1.3.0', '~1.2': '>=1.2.0 <1.3.0', @@ -104,11 +118,13 @@ def test_spec(self): '^0.0': '>=0.0.0 <0.1.0', '^1.x': '>=1.0.0 <2.0.0', '^0.x': '>=0.0.0 <1.0.0', + '^0': '>=0.0.0 <1.0.0', '^ 1.2.3': '>=1.2.3 <2.0.0', '^ 1.2.3': '>=1.2.3 <2.0.0', # Weird whitespace ranges '>= 1.2.3': '>=1.2.3', + '>=\t1.2.3': '>=1.2.3', '>= 1.2.3': '>=1.2.3', '>=1.2.3 <2.0.0': '>=1.2.3 <2.0.0', ' >=1.2.3 <2.0.0 ': '>=1.2.3 <2.0.0', @@ -128,3 +144,21 @@ def test_expand(self): base.NpmSpec(source).clause, base.NpmSpec(expanded).clause, ) + + invalid_npmspecs = [ + '==0.1.2', + '>>0.1.2', + '> = 0.1.2', + '<=>0.1.2', + '~1.2.3beta', + '~=1.2.3', + '>01.02.03', + '!0.1.2', + '!=0.1.2', + ] + + def test_invalid(self): + for invalid in self.invalid_npmspecs: + with self.subTest(spec=invalid): + with self.assertRaises(ValueError, msg="NpmSpec(%r) should be invalid" % invalid): + base.NpmSpec(invalid) From 3afef0da531852247494e0e76a7f85f97f88d648 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 15:37:14 +1000 Subject: [PATCH 06/10] Fix mistake with op matching part of the string. --- semantic_version/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semantic_version/base.py b/semantic_version/base.py index 5672bd4..f6fcc12 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -1241,7 +1241,7 @@ class Parser: re.VERBOSE, ) OP_RE = re.compile(r""" - ^{op}$ # A standalone operator, cannot be empty + ^(?:{op})$ # A standalone operator, cannot be empty """.format(op=OP), re.VERBOSE, ) From 912cc8a0c5e79732eee55294299671bf20d1b6e1 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 16:13:24 +1000 Subject: [PATCH 07/10] Add more tests, separate runner for ones requiring simplify --- tests/test_npm.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/test_npm.py b/tests/test_npm.py index a4bdda0..3391034 100644 --- a/tests/test_npm.py +++ b/tests/test_npm.py @@ -74,7 +74,6 @@ def test_spec(self): # Hyphen ranges '1.2.3 - 2.3.4': '>=1.2.3 <=2.3.4', '1.2 - 2.3.4': '>=1.2.0 <=2.3.4', - '1.2 - 2.3.5': '>=1.2.0 <2.3.6', '1.2.3 - 2.3': '>=1.2.3 <2.4.0', '1.2.3 - 2': '>=1.2.3 <3', '1.2.3 - 2': '>=1.2.3 <3', @@ -85,13 +84,13 @@ def test_spec(self): '1.x': '>=1.0.0 <2.0.0', '1.2.x': '>=1.2.0 <1.3.0', '': '*', - '||': '*', 'x': '*', '1': '1.x.x', '1.x.x': '>=1.0.0 <2.0.0', '1.2': '1.2.x', # Partial GT LT Ranges + '>=1': '>=1.0.0', '>1': '>=2.0.0', '>1.2': '>=1.3.0', '<1': '<1.0.0', @@ -145,6 +144,23 @@ def test_expand(self): base.NpmSpec(expanded).clause, ) + equivalent_npmspecs = { + '||': '*', + #'>1.2.3 || *': '*', + #'>=1.2.1 <=2.3.4': '>1.2.0 <2.3.5', + } + + def test_equivalent(self): + # like expand, but we also simplify both sides + # NOTE: some specs can be equivalent but don't + # currently simplify to the same clauses + for left, right in self.equivalent_npmspecs.items(): + with self.subTest(l=left, r=right): + self.assertEqual( + base.NpmSpec(left).clause.simplify(), + base.NpmSpec(right).clause.simplify(), + ) + invalid_npmspecs = [ '==0.1.2', '>>0.1.2', From 2e21f5ab638f77f6bc0468b55a97fe49012b9c9d Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 16:34:49 +1000 Subject: [PATCH 08/10] Add '~>' as an alias for '~'. --- semantic_version/base.py | 3 ++- tests/test_npm.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/semantic_version/base.py b/semantic_version/base.py index f6fcc12..39fc912 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -1230,7 +1230,7 @@ class Parser: NUMBER = r'x|X|\*|0|[1-9][0-9]*' PART = r'[a-zA-Z0-9.-]*' - OP = r'<|<=|=|>=|>|\^|~' + OP = r'<|<=|=|>=|>|\^|~|~>' NPM_SPEC_BLOCK = re.compile(r""" ^(?:v)? # Strip optional initial v (?P{op}|) # Operator, can be empty @@ -1326,6 +1326,7 @@ def parse(cls, expression): PREFIX_ALIASES = { '': PREFIX_EQ, + '~>': PREFIX_TILDE, } PREFIX_TO_OPERATOR = { diff --git a/tests/test_npm.py b/tests/test_npm.py index 3391034..8dc8fff 100644 --- a/tests/test_npm.py +++ b/tests/test_npm.py @@ -105,6 +105,8 @@ def test_spec(self): '~1.2.3-beta.2': '>=1.2.3-beta.2 <1.3.0', '~ 1.2.3': '>=1.2.3 <1.3.0', '~ 1.2.3': '>=1.2.3 <1.3.0', + '~>1.2.3': '>=1.2.3 <1.3.0', + '~> 1.2.3': '>=1.2.3 <1.3.0', # Caret ranges '^1.2.3': '>=1.2.3 <2.0.0', From 12d67c808c420ec58ff6ecd3e8ed077eb9eff670 Mon Sep 17 00:00:00 2001 From: Nathaniel Jensen Date: Sat, 10 Jul 2021 17:07:26 +1000 Subject: [PATCH 09/10] Update changelog. --- ChangeLog | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index c8453e5..5ad5c04 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,8 @@ ChangeLog * Add support for Django 3.1 * Add support for Python 3.7 / 3.8 / 3.9 + * `#115 `_: + Improve ``NpmSpec`` compatibility with official ``node-semver`` implementation by allowing some whitespace before the version (e.g. ``> 1.2.3``, ``1.2.3 - 4.5.6``) and adding ``~>`` as an alias for ``~``. 2.8.5 (2020-04-29) @@ -15,7 +17,7 @@ ChangeLog *Bugfix:* - * `98 `_: + * `#98 `_: Properly handle wildcards in ``SimpleSpec`` (e.g. ``==1.2.*``). From 34320c70e14b9f1eb9e33ab5127d82d9637caa33 Mon Sep 17 00:00:00 2001 From: Nat Date: Thu, 19 May 2022 20:29:47 +1000 Subject: [PATCH 10/10] Update semantic_version/base.py Reorder OP to check for longer match options first. Co-authored-by: Philippe Ombredanne --- semantic_version/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semantic_version/base.py b/semantic_version/base.py index 39fc912..3692576 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -1230,7 +1230,7 @@ class Parser: NUMBER = r'x|X|\*|0|[1-9][0-9]*' PART = r'[a-zA-Z0-9.-]*' - OP = r'<|<=|=|>=|>|\^|~|~>' + OP = r'~>|<=|>=|>|<|\^|~|=' NPM_SPEC_BLOCK = re.compile(r""" ^(?:v)? # Strip optional initial v (?P{op}|) # Operator, can be empty