Merge pull request #613 from betatim/ka-ka-coverage

[MRG] Add tests for Julia semver matching
pull/619/head
Carol Willing 2019-03-08 13:28:20 -08:00 zatwierdzone przez GitHub
commit c75d40fc58
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 376 dodań i 120 usunięć

Wyświetl plik

@ -2,7 +2,7 @@
import os
import toml
from ..python import PythonBuildPack
from .julia_semver import find_semver_match
from .semver import find_semver_match
class JuliaProjectTomlBuildPack(PythonBuildPack):
"""

Wyświetl plik

@ -1,119 +0,0 @@
# This file implements the julia-specific logic for handling SemVer (Semantic
# Versioning) strings in .toml files.
#
# It uses the python "semver" package to do most version string comparisons, but
# the places where julia's SemVer handling differs from the semver package have
# been implemented directly.
#
# Here, we use tuples to represent a Version, and functors as "matchers". The matcher
# functors take a version string and return true if it passes its constraints.
import re
import semver
# Main algorithm:
# Create an AbstractMatcher instance from the constraint string, and check it
# against each version in the versions_list, returning the first success.
def find_semver_match(constraint, versions_list):
matcher = create_semver_matcher(constraint)
for vstr in reversed(versions_list):
if matcher.match(str_to_version(vstr)):
return vstr
return None
def str_to_version(vstr):
return tuple([int(n) for n in vstr.split('.')])
# --- Matcher interface -------------------------------------------
class AbstractMatcher:
def match(self, v):
pass
class SemverMatcher(AbstractMatcher):
""" Match a version tuple to a given constraint_str using the `semver` package. """
def __init__(self, constraint_str):
self.constraint_str = constraint_str
def match(self, v):
while len(v) < 3:
v = v+(0,)
v_str = '.'.join(map(str, v))
return semver.match(v_str, self.constraint_str)
def __repr__(self):
return self.constraint_str
# --- Custom matcher for julia-specific SemVer handling: ---------
from enum import Enum
class Exclusivity(Enum):
EXCLUSIVE = 1
INCLUSIVE = 2
class VersionRange(AbstractMatcher):
""" Match a version tuple between lower and upper bounds. """
def __init__(self, lower, upper, upper_exclusivity):
self.lower = lower
self.upper = upper
self.upper_exclusivity = upper_exclusivity
def match(self, v):
if self.upper_exclusivity == Exclusivity.EXCLUSIVE:
return self.lower <= v < self.upper
else:
return self.lower <= v <= self.upper
def __repr__(self):
return ("["+".".join(map(str, self.lower)) +"-"+ ".".join(map(str, self.upper)) +
(")" if self.upper_exclusivity == Exclusivity.EXCLUSIVE else "]"))
# Helpers
def major(v): return v[0]
def minor(v): return v[1] if len(v) >= 2 else 0
def patch(v): return v[2] if len(v) >= 3 else 0
# --- main constraint parser function ------------------------------------
def create_semver_matcher(constraint_str):
"""
Returns a derived-class instance of AbstractMatcher that matches version
tuples against the provided constraint_str.
"""
constraint_str = constraint_str.strip()
first_digit = re.search(r"\d", constraint_str)
if not first_digit:
# Invalid version string (no numbers in it)
return ""
constraint = str_to_version(constraint_str[first_digit.start():])
comparison_symbol = constraint_str[0:first_digit.start()].strip()
# Default to "^" search if no matching mode specified (up to next major version)
if (first_digit.start() == 0) or (comparison_symbol == "^"):
if major(constraint) == 0:
# Also, julia treats pre-1.0 releases specially, as if the first
# non-zero number is actually a major number:
# https://docs.julialang.org/en/latest/stdlib/Pkg/#Caret-specifiers-1
# So we need to handle it separately by bumping the first non-zero number.
for i,n in enumerate(constraint):
if n != 0 or i == len(constraint)-1: # (using the last existing number handles situations like "^0.0" or "^0")
upper = constraint[0:i] + (n+1,)
break
return VersionRange(constraint, upper, Exclusivity.EXCLUSIVE)
else:
return VersionRange(constraint, (major(constraint)+1,), Exclusivity.EXCLUSIVE)
# '~' matching (only allowed to bump the last present number by one)
if (comparison_symbol == "~"):
return VersionRange(constraint, constraint[:-1] +(constraint[-1]+1,), Exclusivity.INCLUSIVE)
# Use semver package's comparisons for everything else:
# semver requires three version numbers
if len(constraint) < 3:
while len(constraint) < 3:
constraint = constraint+(0,)
constraint_str = constraint_str[0:first_digit.start()] + ".".join(map(str, constraint))
# Convert special comparison strings to format accepted by `semver` library.
constraint_str = constraint_str.replace("", ">=").replace("", "<=")
constraint_str = re.sub(r"(^|\b)=\b", "==", constraint_str)
return SemverMatcher(constraint_str)

Wyświetl plik

@ -0,0 +1,165 @@
"""
Julia specific handling of SemVer strings
It uses the python "semver" package to do most version string comparisons, but
the places where julia's SemVer handling differs from the semver package have
been implemented directly.
We use tuples to represent a Version, and functors as "matchers". The
matcher functors take a version string and return True if it passes its
constraints.
"""
import re
import semver
def find_semver_match(constraint, versions_list):
matcher = create_semver_matcher(constraint)
for vstr in reversed(versions_list):
if matcher.match(str_to_version(vstr)):
return vstr
return None
def str_to_version(vstr):
return tuple([int(n) for n in vstr.split(".")])
# Helpers
def major(v):
return v[0]
def minor(v):
return v[1] if len(v) >= 2 else 0
def patch(v):
return v[2] if len(v) >= 3 else 0
def create_semver_matcher(constraint_str):
"""Create a matcher that can be used to match version tuples
Version tuples are matched against the provided regex `constraint_str`.
"""
constraint_str = constraint_str.strip()
first_digit = re.search(r"\d", constraint_str)
if not first_digit:
# Invalid version string (no numbers in it)
return ""
constraint = str_to_version(constraint_str[first_digit.start() :])
comparison_symbol = constraint_str[0 : first_digit.start()].strip()
# Default to "^" search if no matching mode specified (up to next major version)
if (first_digit.start() == 0) or (comparison_symbol == "^"):
if major(constraint) == 0:
# Also, julia treats pre-1.0 releases specially, as if the first
# non-zero number is actually a major number:
# https://docs.julialang.org/en/latest/stdlib/Pkg/#Caret-specifiers-1
# So we need to handle it separately by bumping the first non-zero
# enumber.
for i, n in enumerate(constraint):
if (
n != 0 or i == len(constraint) - 1
): # (using the last existing number handles situations like "^0.0" or "^0")
upper = constraint[0:i] + (n + 1,)
break
return VersionRange(constraint, upper, True)
else:
return VersionRange(
constraint, (major(constraint) + 1,), True
)
# '~' matching (only allowed to bump the last present number by one)
if comparison_symbol == "~":
return VersionRange(
constraint,
constraint[:-1] + (constraint[-1] + 1,),
exclusive=False
)
# Use semver package's comparisons for everything else:
# semver requires three version numbers
if len(constraint) < 3:
while len(constraint) < 3:
constraint = constraint + (0,)
constraint_str = constraint_str[0 : first_digit.start()] + ".".join(
map(str, constraint)
)
# Convert special comparison strings to format accepted by `semver` library.
constraint_str = constraint_str.replace("", ">=").replace("", "<=")
constraint_str = re.sub(r"(^|\b)=\b", "==", constraint_str)
return SemverMatcher(constraint_str)
class SemverMatcher:
"""Provides a utility for using `semver` package to do version matching.
The `SemverMatcher` takes a `constraint_str` to represent a regex to
determine if a version tuple matches the constraint.
The matching is handled via the `semver` package.
"""
def __init__(self, constraint_str):
self.constraint_str = constraint_str
def match(self, v):
"""Check if `v` matches the constraint"""
while len(v) < 3:
v = v + (0,)
v_str = ".".join(map(str, v))
return semver.match(v_str, self.constraint_str)
def __eq__(self, rhs):
return self.constraint_str == rhs.constraint_str
def __repr__(self):
return self.constraint_str
class VersionRange:
"""Represents a range of release versions.
A `VersionRange` contains versions from a `lower` to `upper` bound
which may be inclusive (default: `exclusive=False`) or exclusive (`exclusive=True`).
A release version (represented by a tuple) can be checked to see if it
falls within a `VersionRange`
"""
def __init__(self, lower, upper, exclusive=False):
self.lower = lower
self.upper = upper
self.exclusive = exclusive
def match(self, v):
"""Check if `v` falls into the version range"""
if self.exclusive:
return self.lower <= v < self.upper
else:
return self.lower <= v <= self.upper
def __eq__(self, rhs):
return (
self.lower == rhs.lower
and self.upper == rhs.upper
and self.exclusive == rhs.exclusive
)
def __repr__(self):
return (
"["
+ ".".join(map(str, self.lower))
+ "-"
+ ".".join(map(str, self.upper))
+ (")" if self.exclusive else "]")
)

Wyświetl plik

@ -0,0 +1,210 @@
from repo2docker.buildpacks.julia import semver
def test_str_to_version():
assert semver.str_to_version("1.5.2") == (1, 5, 2)
assert semver.str_to_version("1") == (1,)
def test_major_minor_patch():
V = (1, 2, 3)
assert (semver.major(V), semver.minor(V), semver.patch(V)) == (1, 2, 3)
assert semver.major((1,)) == 1
assert semver.minor((1,)) == 0
assert semver.patch((1,)) == 0
def test_simple_matches():
assert repr(semver.create_semver_matcher("1.2.5")) == "[1.2.5-2)"
assert repr(semver.create_semver_matcher("1.2.5")) == "[1.2.5-2)"
assert repr(semver.create_semver_matcher("^1.2.3")) == "[1.2.3-2)"
assert repr(semver.create_semver_matcher("^1.2")) == "[1.2-2)"
assert repr(semver.create_semver_matcher("^1")) == "[1-2)"
assert repr(semver.create_semver_matcher("^0.2.3")) == "[0.2.3-0.3)"
assert repr(semver.create_semver_matcher("^0.0.3")) == "[0.0.3-0.0.4)"
assert repr(semver.create_semver_matcher("^0.0")) == "[0.0-0.1)"
assert repr(semver.create_semver_matcher("^0")) == "[0-1)"
# This one seems wrong: `~1.2.3 = [1.2.3, 1.2.4)` but ~ is special in Julia
# from https://docs.julialang.org/en/latest/stdlib/Pkg/#Tilde-specifiers-1
assert repr(semver.create_semver_matcher("~1.2.3")) == "[1.2.3-1.2.4]"
assert repr(semver.create_semver_matcher("~1.3.5")) == "[1.3.5-1.3.6]"
assert repr(semver.create_semver_matcher("~1.2")) == "[1.2-1.3]"
assert repr(semver.create_semver_matcher("~1")) == "[1-2]"
def test_range_matches():
assert semver.create_semver_matcher(
"1.2.3"
) == semver.create_semver_matcher("^1.2.3")
assert semver.create_semver_matcher(
"1.2.3"
) == semver.create_semver_matcher("^1.2.3")
assert semver.create_semver_matcher("1.2") == semver.create_semver_matcher(
"^1.2"
)
assert semver.create_semver_matcher("1") == semver.create_semver_matcher(
"^1"
)
assert semver.create_semver_matcher(
"0.0.3"
) == semver.create_semver_matcher("^0.0.3")
assert semver.create_semver_matcher("0") == semver.create_semver_matcher(
"^0"
)
def test_match_particular_version():
assert semver.create_semver_matcher("1.2.3").match(
semver.str_to_version("1.5.2")
)
assert semver.create_semver_matcher("1.2.3").match(
semver.str_to_version("1.2.3")
)
assert (
semver.create_semver_matcher("1.2.3").match(
semver.str_to_version("2.0.0")
)
== False
)
assert (
semver.create_semver_matcher("1.2.3").match(
semver.str_to_version("1.2.2")
)
== False
)
assert semver.create_semver_matcher("~1.2.3").match(
semver.str_to_version("1.2.4")
)
assert semver.create_semver_matcher("~1.2.3").match(
semver.str_to_version("1.2.3")
)
assert (
semver.create_semver_matcher("~1.2.3").match(
semver.str_to_version("1.3")
)
== False
)
assert semver.create_semver_matcher("1.2").match(
semver.str_to_version("1.2.0")
)
assert semver.create_semver_matcher("1.2").match(
semver.str_to_version("1.9.9")
)
assert (
semver.create_semver_matcher("1.2").match(
semver.str_to_version("2.0.0")
)
== False
)
assert (
semver.create_semver_matcher("1.2").match(
semver.str_to_version("1.1.9")
)
== False
)
assert semver.create_semver_matcher("0.2.3").match(
semver.str_to_version("0.2.3")
)
assert (
semver.create_semver_matcher("0.2.3").match(
semver.str_to_version("0.3.0")
)
== False
)
assert (
semver.create_semver_matcher("0.2.3").match(
semver.str_to_version("0.2.2")
)
== False
)
assert semver.create_semver_matcher("0").match(
semver.str_to_version("0.0.0")
)
assert semver.create_semver_matcher("0").match(
semver.str_to_version("0.99.0")
)
assert (
semver.create_semver_matcher("0").match(semver.str_to_version("1.0.0"))
== False
)
assert semver.create_semver_matcher("0.0").match(
semver.str_to_version("0.0.0")
)
assert semver.create_semver_matcher("0.0").match(
semver.str_to_version("0.0.99")
)
assert (
semver.create_semver_matcher("0.0").match(
semver.str_to_version("0.1.0")
)
== False
)
def test_less_than_prefix():
assert repr(semver.create_semver_matcher("<1.2.3")) == "<1.2.3"
assert repr(semver.create_semver_matcher("<1")) == "<1.0.0"
assert repr(semver.create_semver_matcher("<0.2.3")) == "<0.2.3"
assert semver.create_semver_matcher("<2.0.3").match(
semver.str_to_version("2.0.2")
)
assert semver.create_semver_matcher("<2").match(
semver.str_to_version("0.0.1")
)
assert semver.create_semver_matcher("<2.0.3").match(
semver.str_to_version("0.2.3")
)
assert (
semver.create_semver_matcher("<0.2.4").match(
semver.str_to_version("0.2.4")
)
== False
)
def test_equal_prefix():
assert repr(semver.create_semver_matcher("=1.2.3")) == "==1.2.3"
assert repr(semver.create_semver_matcher("=1.2")) == "==1.2.0"
assert repr(semver.create_semver_matcher(" =1")) == "==1.0.0"
assert semver.create_semver_matcher("=1.2.3").match(
semver.str_to_version("1.2.3")
)
assert (
semver.create_semver_matcher("=1.2.3").match(
semver.str_to_version("1.2.4")
)
== False
)
assert (
semver.create_semver_matcher("=1.2.3").match(
semver.str_to_version("1.2.2")
)
== False
)
def test_fancy_unicode():
assert semver.create_semver_matcher(
"≥1.3.0"
) == semver.create_semver_matcher(">=1.3.0")
def test_largerthan_equal():
assert repr(semver.create_semver_matcher(">= 1.2.3")) == ">= 1.2.3"
assert repr(semver.create_semver_matcher(" >= 1")) == ">= 1.0.0"
assert semver.create_semver_matcher(">=1").match(
semver.str_to_version("1.0.0")
)
assert semver.create_semver_matcher(">=0").match(
semver.str_to_version("0.0.1")
)
assert semver.create_semver_matcher(">=1.2.3").match(
semver.str_to_version("1.2.3")
)
assert (
semver.create_semver_matcher(">=1.2.3").match(
semver.str_to_version("1.2.2")
)
== False
)