Skip to content

Commit

Permalink
Add include_prerelease and loose option to version range expressi…
Browse files Browse the repository at this point in the history
…on (conan-io#3898)

* Add `include_prerelease` and `loose` option to version range expression (New to node-semver 0.5.0)
Usage example: [~1.2.2,include_prerelease=True,loose=False]

Change-Id: I056ac89bf37949701f67abe57455a519d14310ee

* Change keywords to "strict" (loose=False) and "prerelease" (instead of include_prerelease)
Reactive test for invalid semver expression.

Change-Id: I2f75293f9c0b498eab6ff8d8a2be381f15cf406f

* Move parsing of version expression into separate function

Change-Id: Ib5209ef611295ce71c4b94f2b93e09a99c56c636

* Change back to "include_prerelease=True" and "loose=False" option.

Change-Id: I388e9eba183e640fcf462546bb502e137997111f

* Use python 2.7 compatible regex syntax

Change-Id: I21765c9086ca192f90e5ed7e46a608e8d765a922

* Update 'node-semver' version to 0.6.1

Change-Id: I182bb1190f78d9648d30a5fe5dbc2a0a441334f6

* Add deprecated message for comma usage in version range

Change-Id: Ifc2872ad1c4f9d40328e360777015727fdf20357

* Add unit tests for '_parse_versionexpr' function

Change-Id: I85804296068d2252553e1c84ba4b753b957cfb34

* Check version range in 'satisfying' function

Added some tests for invalid version ranges
Updated warning message, when using commas in version range.

Change-Id: Ie76dfa0769f434e710fc21ddcaab48841ec5b54e

* work on version ranges

* Updated version range regex and corrected test to fail correctly with ConanException

Change-Id: If4f3123ef2ae3061b6556d89417d0cd4c898e197

* Reverted some tests, because "" is a valid range.
Add asterix and caret symbol to version regex

Change-Id: I3fa9cae1a6b8801e252e9bca2efe860a0ec38d68

* Add round brackets to version range regex

Change-Id: I9024e63a8387a388cd8f486b05051fc5197228c8

* minor improvements

* missing init.py

* Add deprecated message for comma usage in version range.
Change assertion to raise ConanException in '_parse_versionexpr'.
Used named groups in regex matching.

Change-Id: Ife07c32062eb192997b5ca1700392111519b93b9
  • Loading branch information
climblinne authored and lasote committed Dec 4, 2018
1 parent 523e8b8 commit e3e7b35
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 7 deletions.
73 changes: 67 additions & 6 deletions conans/client/graph/range_resolver.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,84 @@
from conans.model.ref import ConanFileReference
import re

from conans.errors import ConanException
from conans.model.ref import ConanFileReference
from conans.search.search import search_recipes


re_param = re.compile(r"^(?P<function>include_prerelease|loose)\s*=\s*(?P<value>True|False)$")
re_version = re.compile(r"^((?!(include_prerelease|loose))[a-zA-Z0-9_+.\-~<>=|*^\(\)\s])*$")


def _parse_versionexpr(versionexpr, output):
expression = [it.strip() for it in versionexpr.split(",")]
if len(expression) > 4:
raise ConanException("Invalid expression for version_range '{}'".format(versionexpr))

include_prerelease = False
loose = True
version_range = []

for i, expr in enumerate(expression):
match_param = re_param.match(expr)
match_version = re_version.match(expr)

if match_param == match_version:
raise ConanException("Invalid version range '{}', failed in "
"chunk '{}'".format(versionexpr, expr))

if match_version and i not in [0, 1]:
raise ConanException("Invalid version range '{}'".format(versionexpr))

if match_param and i not in [1, 2, 3]:
raise ConanException("Invalid version range '{}'".format(versionexpr))

if match_version:
version_range.append(expr)

if match_param:
if match_param.group('function') == 'loose':
loose = match_param.group('value') == "True"
elif match_param.group('function') == 'include_prerelease':
include_prerelease = match_param.group('value') == "True"
else:
raise ConanException("Unexpected version range "
"parameter '{}'".format(match_param.group(1)))

if len(version_range) > 1:
output.warn("Commas as separator in version '%s' range will are deprecated and will be removed in Conan 2.0" %
str(versionexpr))

version_range = " ".join(map(str, version_range))
return version_range, loose, include_prerelease


def satisfying(list_versions, versionexpr, output):
""" returns the maximum version that satisfies the expression
if some version cannot be converted to loose SemVer, it is discarded with a msg
This provides some woraround for failing comparisons like "2.1" not matching "<=2.1"
This provides some workaround for failing comparisons like "2.1" not matching "<=2.1"
"""
from semver import SemVer, max_satisfying
version_range = versionexpr.replace(",", " ")
from semver import SemVer, Range, max_satisfying

version_range, loose, include_prerelease = _parse_versionexpr(versionexpr, output)

# Check version range expression
try:
act_range = Range(version_range, loose)
except ValueError:
raise ConanException("version range expression '%s' is not valid" % version_range)

# Validate all versions
candidates = {}
for v in list_versions:
try:
ver = SemVer(v, loose=True)
ver = SemVer(v, loose=loose)
candidates[ver] = v
except (ValueError, AttributeError):
output.warn("Version '%s' is not semver, cannot be compared with a range" % str(v))
result = max_satisfying(candidates, version_range, loose=True)

# Search best matching version in range
result = max_satisfying(candidates, act_range, loose=loose,
include_prerelease=include_prerelease)
return candidates.get(result)


Expand Down
2 changes: 1 addition & 1 deletion conans/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ PyYAML>=3.11, <3.14.0
patch==1.16
fasteners>=0.14.1
six>=1.10.0
node-semver==0.2.0
node-semver==0.6.1
distro>=1.0.2, <1.2.0
pylint>=1.9.3
future==0.16.0
Expand Down
26 changes: 26 additions & 0 deletions conans/test/model/version_ranges_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ def prereleases_versions_test(self):
result = satisfying(["4.2.2", "4.2.3-pre", "4.2.3"], "~4.2.3-", output)
self.assertEqual(result, "4.2.3")

def loose_versions_test(self):
output = TestBufferConanOutput()
result = satisfying(["4.2.2", "4.2.3-pre"], "~4.2.1,loose=False", output)
self.assertEqual(result, "4.2.2")
result = satisfying(["1.1.1", "1.1.2", "1.2", "1.2.1", "1.3", "2.1"], "1.8||1.3,loose=False", output)
self.assertEqual(result, None)
result = satisfying(["1.1.1", "1.1.2", "1.2", "1.2.1", "1.3", "2.1"], "1.8||1.3, loose = False ", output)
self.assertEqual(result, None)
result = satisfying(["1.1.1", "1.1.2", "1.2", "1.2.1", "1.3", "2.1"], "1.8||1.3", output)
self.assertEqual(result, "1.3")

def include_prerelease_versions_test(self):
output = TestBufferConanOutput()
result = satisfying(["4.2.2", "4.2.3-pre"], "~4.2.1,include_prerelease = True", output)
self.assertEqual(result, "4.2.3-pre")
result = satisfying(["4.2.2", "4.2.3-pre"], "~4.2.1", output)
self.assertEqual(result, "4.2.2")

def basic_test(self):
output = TestBufferConanOutput()
result = satisfying(["1.1", "1.2", "1.3", "2.1"], "", output)
Expand Down Expand Up @@ -92,6 +110,14 @@ def basic_test(self):
result = satisfying(["2.1.1"], ">2.1.0", output)
self.assertEqual(result, "2.1.1")

# Invalid ranges
with self.assertRaises(ConanException):
satisfying(["2.1.1"], "2.3 3.2; include_prerelease=True, loose=False", output)
with self.assertRaises(ConanException):
satisfying(["2.1.1"], "2.3 3.2, include_prerelease=Ture, loose=False", output)
with self.assertRaises(ConanException):
satisfying(["2.1.1"], "~2.3, abc, loose=False", output)


hello_content = """
from conans import ConanFile
Expand Down
Empty file.
Empty file.
Empty file.
65 changes: 65 additions & 0 deletions conans/test/unittests/client/graph/test_range_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import unittest

from conans.client.graph.range_resolver import _parse_versionexpr
from conans.errors import ConanException
from conans.test.utils.tools import TestBufferConanOutput


class ParseVersionExprTest(unittest.TestCase):
def test_backwards_compatibility(self):
output = TestBufferConanOutput()
self.assertEqual(_parse_versionexpr("2.3, 3.2", output), ("2.3 3.2", True, False))
self.assertTrue(str(output).startswith("WARN: Commas as separator"))
output = TestBufferConanOutput()
self.assertEqual(_parse_versionexpr("2.3, <=3.2", output), ("2.3 <=3.2", True, False))
self.assertTrue(str(output).startswith("WARN: Commas as separator"))

def test_only_spaces_without_warning(self):
output = TestBufferConanOutput()
self.assertEqual(_parse_versionexpr("2.3 3.2", output), ("2.3 3.2", True, False))
self.assertEqual(str(output), "")

def test_standard_semver(self):
output = TestBufferConanOutput()
self.assertEqual(_parse_versionexpr("*", output), ("*", True, False))
self.assertEqual(_parse_versionexpr("", output), ("", True, False)) # Defaults to '*'
self.assertEqual(_parse_versionexpr("~1", output), ("~1", True, False))
self.assertEqual(_parse_versionexpr("~1.2.3-beta.2", output), ("~1.2.3-beta.2", True, False))
self.assertEqual(_parse_versionexpr("^0.0", output), ("^0.0", True, False))
self.assertEqual(_parse_versionexpr("1.2.3 - 2.3.4", output), ("1.2.3 - 2.3.4", True, False))
self.assertEqual(_parse_versionexpr(">=1.2.3 <1.(2+1).0", output),
(">=1.2.3 <1.(2+1).0", True, False))

def test_only_loose(self):
output = TestBufferConanOutput()
self.assertEqual(_parse_versionexpr("2.3 ,3.2, loose=True", output), ("2.3 3.2", True, False))
self.assertEqual(_parse_versionexpr("2.3 3.2, loose=False", output), ("2.3 3.2", False, False))
self.assertEqual(_parse_versionexpr("2.3 3.2, loose = False", output), ("2.3 3.2", False, False))
self.assertEqual(_parse_versionexpr("2.3 3.2, loose = True", output), ("2.3 3.2", True, False))

def test_only_prerelease(self):
output = TestBufferConanOutput()
self.assertEqual(_parse_versionexpr("2.3, 3.2, include_prerelease=False", output),
("2.3 3.2", True, False))
self.assertEqual(_parse_versionexpr("2.3, 3.2, include_prerelease=True", output),
("2.3 3.2", True, True))

def test_both(self):
output = TestBufferConanOutput()
self.assertEqual(_parse_versionexpr("2.3, 3.2, loose=False, include_prerelease=True", output),
("2.3 3.2", False, True))
self.assertEqual(_parse_versionexpr("2.3, 3.2, include_prerelease=True, loose=False", output),
("2.3 3.2", False, True))

def test_invalid(self):
output = TestBufferConanOutput()
self.assertRaises(ConanException, _parse_versionexpr, "loose=False, include_prerelease=True", output)
self.assertRaises(ConanException, _parse_versionexpr, "2.3, 3.2, unexpected=True", output)
self.assertRaises(ConanException, _parse_versionexpr, "2.3, 3.2, loose=Other", output)
self.assertRaises(ConanException, _parse_versionexpr, "2.3, 3.2, ", output)
self.assertRaises(ConanException, _parse_versionexpr, "2.3, 3.2, 1.2.3", output)
self.assertRaises(ConanException, _parse_versionexpr,
"2.3 3.2; loose=True, include_prerelease=True", output)
self.assertRaises(ConanException, _parse_versionexpr, "loose=True, 2.3 3.3", output)
self.assertRaises(ConanException, _parse_versionexpr,
"2.3, 3.2, 1.4, loose=False, include_prerelease=True", output)

0 comments on commit e3e7b35

Please sign in to comment.