Commit c5c7624e by Sébastien Eustace

Initial commit (semver)

parents
*.pyc
# Packages
*.egg
*.egg-info
dist
build
_build
.cache
*.so
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
.pytest_cache
.DS_Store
.idea/*
.python-version
poetry.lock
This diff is collapsed. Click to expand it.
[package]
name = "poetry"
version = "0.1.0"
description = ""
# Compatibe Python versions
python = ["~2.7", "^3.5"]
license = "MIT"
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
# The readme file of the package.
#
# The file can be either README.rst or README.md.
#
# If it's a markdown file you have to install the pandoc utility
# so that it can be automatically converted to a RestructuredText file.
#
# You also need to have the pypandoc package installed.
# If you install poet via pip you can use the markdown-readme extra to do so.
readme = "README.md"
# An URL to the homepage of the project.
# homepage = "https://github.com/sdispater/poet"
# An URL to the repository of the project.
# repository = "https://github.com/sdispater/poet"
# An URL to the documentation of the project.
# documentation = "https://github.com/sdispater/poet"
# A list of keywords (max: 5) that the package is related to.
# keywords = ["packaging", "poet"]
# A list of patterns that will be included in the final package.
#
# Python packages, modules and package data will be automatically detected
# include = ['poet/**/*', 'LICENSE']
#
# If you packages lies elsewhere (like in a src directory),
# you can tell poet to find them from there:
# include = { from = 'src', include = '**/*' }
#
# Similarly, you can tell that the src directory represent the foo package:
# include = { from = 'src', include = '**/*', as = 'foo' }
# Features are sets of optional dependencies, which enhance a package,
# but are not required
# [features]
# markdown-readme = ["pypandoc"] # Adds support for markdown readmes
[dependencies]
# Main dependencies of the project
# See https://github.com/sdispater/poet#dependencies-and-dev-dependencies
cleo = "^0.6"
pendulum = "^1.3"
[dev-dependencies]
# Development dependencies of the package
# See https://github.com/sdispater/poet#dependencies-and-dev-dependencies
pytest = "^3.3"
# CLI scripts
#
# They must follow the following convention:
# {script_name} = "{my.package:executable}"
#
# [scripts]
# poet = 'poet:app.run'
# -*- coding: utf-8 -*-
from functools import cmp_to_key
from .comparison import less_than
from .constraints import Constraint
from .helpers import normalize_version
from .version_parser import VersionParser
SORT_ASC = 1
SORT_DESC = -1
_parser = VersionParser()
def statisfies(version, constraints):
"""
Determine if given version satisfies given constraints.
:type version: str
:type constraints: str
:rtype: bool
"""
provider = Constraint('==', normalize_version(version))
constraints = _parser.parse_constraints(constraints)
return constraints.matches(provider)
def satisfied_by(versions, constraints):
"""
Return all versions that satisfy given constraints.
:type versions: List[str]
:type constraints: str
:rtype: List[str]
"""
return [version for version in versions if statisfies(version, constraints)]
def sort(versions):
return _sort(versions, SORT_ASC)
def rsort(versions):
return _sort(versions, SORT_DESC)
def _sort(versions, direction):
normalized = [
(i, normalize_version(version))
for i, version in enumerate(versions)
]
normalized.sort(
key=cmp_to_key(
lambda x, y:
0 if x[1] == y[1]
else -direction * int(less_than(x[1], y[1]) or -1)
)
)
return [versions[i] for i, _ in normalized]
from .constraints.constraint import Constraint
def greater_than(version1, version2):
"""
Evaluates the expression: version1 > version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '>', version2)
def greater_than_or_equal(version1, version2):
"""
Evaluates the expression: version1 >= version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '>=', version2)
def less_than(version1, version2):
"""
Evaluates the expression: version1 < version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '<', version2)
def less_than_or_equal(version1, version2):
"""
Evaluates the expression: version1 <= version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '<=', version2)
def equal(version1, version2):
"""
Evaluates the expression: version1 == version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '==', version2)
def not_equal(version1, version2):
"""
Evaluates the expression: version1 != version2.
:type version1: str
:type version2: str
:rtype: bool
"""
return compare(version1, '!=', version2)
def compare(version1, operator, version2):
"""
Evaluates the expression: $version1 $operator $version2
:type version1: str
:type operator: str
:type version2: str
:rtype: bool
"""
constraint = Constraint(operator, version2)
return constraint.matches(Constraint('==', version1))
from .constraint import Constraint
from .empty_constraint import EmptyConstraint
from .multi_constraint import MultiConstraint
import operator
from pkg_resources import parse_version
from ..helpers import normalize_version
class Constraint:
OP_EQ = operator.eq
OP_LT = operator.lt
OP_LE = operator.le
OP_GT = operator.gt
OP_GE = operator.ge
OP_NE = operator.ne
_trans_op_str = {
'=': OP_EQ,
'==': OP_EQ,
'<': OP_LT,
'<=': OP_LE,
'>': OP_GT,
'>=': OP_GE,
'<>': OP_NE,
'!=': OP_NE
}
_trans_op_int = {
OP_EQ: '==',
OP_LT: '<',
OP_LE: '<=',
OP_GT: '>',
OP_GE: '>=',
OP_NE: '!='
}
def __init__(self, operator: str, version: str):
if operator not in self._trans_op_str:
raise ValueError(
f'Invalid operator "{operator}" given, '
f'expected one of: {", ".join(self.supported_operators)}'
)
self._operator = self._trans_op_str[operator]
self._version = version
@property
def supported_operators(self) -> list:
return list(self._trans_op_str.keys())
@property
def operator(self):
return self._operator
@property
def version(self) -> str:
return self._version
def matches(self, provider):
if isinstance(provider, self.__class__):
return self.match_specific(provider)
# turn matching around to find a match
return provider.matches(self)
def version_compare(self, a: str, b: str, operator: str) -> bool:
if operator not in self._trans_op_str:
raise ValueError(
f'Invalid operator "{operator}" given, '
f'expected one of: {", ".join(self.supported_operators)}'
)
return self._trans_op_str[operator](
parse_version(normalize_version(a)),
parse_version(normalize_version(b))
)
def match_specific(self, provider: 'Constraint') -> bool:
no_equal_op = self._trans_op_int[self._operator].replace('=', '')
provider_no_equal_op = self._trans_op_int[provider.operator].replace('=', '')
is_equal_op = self.OP_EQ is self._operator
is_non_equal_op = self.OP_NE is self._operator
is_provider_equal_op = self.OP_EQ is provider.operator
is_provider_non_equal_op = self.OP_NE is provider.operator
# '!=' operator is match when other operator
# is not '==' operator or version is not match
# these kinds of comparisons always have a solution
if is_non_equal_op or is_provider_non_equal_op:
return (not is_equal_op and not is_provider_equal_op
or self.version_compare(provider.version,
self._version,
'!='))
# An example for the condition is <= 2.0 & < 1.0
# These kinds of comparisons always have a solution
if (self._operator is not self.OP_EQ
and no_equal_op == provider_no_equal_op):
return True
if self.version_compare(
provider.version,
self.version,
self._trans_op_int[self._operator]
):
# special case, e.g. require >= 1.0 and provide < 1.0
# 1.0 >= 1.0 but 1.0 is outside of the provided interval
if (
provider.version == self.version
and self._trans_op_int[provider.operator] == provider_no_equal_op
and self._trans_op_int[self.operator] != no_equal_op
):
return False
return True
return False
def __str__(self):
return '{} {}'.format(
self._trans_op_int[self._operator],
self._version
)
def __repr__(self):
return '<Constraint \'{}\'>'.format(str(self))
class EmptyConstraint:
pretty_string = None
def matches(self, _):
return True
def __str__(self):
return '[]'
class MultiConstraint:
def __init__(self, constraints, conjunctive=True):
self._constraints = tuple(constraints)
self._conjunctive = conjunctive
@property
def constraints(self):
return self._constraints
def is_conjunctive(self):
return self._conjunctive
def is_disjunctive(self):
return not self._conjunctive
def matches(self, provider):
if self.is_disjunctive():
for constraint in self._constraints:
if constraint.matches(provider):
return True
return False
for constraint in self._constraints:
if not constraint.matches(provider):
return False
return True
def __str__(self):
constraints = []
for constraint in self._constraints:
constraints.append(str(constraint))
return '[{}]'.format(
(' ' if self._conjunctive else ' || ').join(constraints)
)
import re
_modifier_regex = (
'[._-]?'
'(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?'
'([.-]?dev)?'
)
def normalize_version(version):
"""
Normalizes a version string to be able to perform comparisons on it.
"""
version = version.strip()
# strip off build metadata
m = re.match('^([^,\s+]+)\+[^\s]+$', version)
if m:
version = m.group(1)
index = None
# Match classic versioning
m = re.match(
'(?i)^v?(\d{{1,5}})(\.\d+)?(\.\d+)?(\.\d+)?{}$'.format(
_modifier_regex
),
version
)
if m:
version = f'{m.group(1)}' \
f'{m.group(2) if m.group(2) else ".0"}' \
f'{m.group(3) if m.group(3) else ".0"}' \
f'{m.group(4) if m.group(4) else ".0"}'
index = 5
else:
# Match date(time) based versioning
m = re.match(
'(?i)^v?(\d{{4}}(?:[.:-]?\d{{2}}){{1,6}}(?:[.:-]?\d{{1,3}})?){}$'.format(
_modifier_regex
),
version
)
if m:
version = re.sub('\D', '.', m.group(1))
index = 2
# add version modifiers if a version was matched
if index is not None:
if len(m.groups()) - 1 >= index and m.group(index):
version = f'{version}' \
f'-{_expand_stability(m.group(index))}'
if m.group(index + 1):
version = f'{version}.{m.group(index + 1).lstrip(".-")}'
return version
raise ValueError(f'Invalid version string "{version}"')
def normalize_stability(stability: str) -> str:
stability = stability.lower()
if stability == 'rc':
return 'RC'
return stability
def _expand_stability(stability: str) -> str:
stability = stability.lower()
if stability == 'a':
return 'alpha'
elif stability == 'b':
return 'beta'
elif stability in ['p', 'pl']:
return 'patch'
return stability
import re
from .constraints.constraint import Constraint
from .constraints.empty_constraint import EmptyConstraint
from .constraints.multi_constraint import MultiConstraint
from .helpers import normalize_version, _expand_stability
class VersionParser:
_modifier_regex = (
'[._-]?'
'(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?'
'([.-]?dev)?'
)
_stabilities = [
'stable', 'RC', 'beta', 'alpha', 'dev'
]
@classmethod
def parse_stability(cls, version: str) -> str:
"""
Returns the stability of a version.
"""
version = re.sub('#.+$i', '', version)
if 'dev-' == version[:4] or '-dev' == version[-4:]:
return 'dev'
m = re.match(f'(?i){cls._modifier_regex}(?:\+.*)?$', version)
if m.group(3):
return 'dev'
if m.group(1):
if m.group(1) in ['beta', 'b']:
return 'beta'
if m.group(1) in ['alpha', 'a']:
return 'alpha'
if m.group(1) == 'rc':
return 'RC'
return 'stable'
def parse_constraints(self, constraints: str):
"""
Parses a constraint string into
MultiConstraint and/or Constraint objects.
"""
pretty_constraint = constraints
m = re.match(
'(?i)([^,\s]*?)@({})$'.format('|'.join(self._stabilities)),
constraints
)
if m:
constraints = m.group(1)
if not constraints:
constraints = '*'
or_constraints = re.split('\s*\|\|?\s*', constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
'(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)',
constraints
)
if len(and_constraints) > 1:
constraint_objects = []
for constraint in and_constraints:
for parsed_constraint in self._parse_constraint(constraint):
constraint_objects.append(parsed_constraint)
else:
constraint_objects = self._parse_constraint(and_constraints[0])
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = MultiConstraint(constraint_objects)
or_groups.append(constraint)
if len(or_groups) == 1:
constraint = or_groups[0]
elif len(or_groups) == 2:
# parse the two OR groups and if they are contiguous we collapse
# them into one constraint
a = str(or_groups[0])
b = str(or_groups[1])
pos_a = a.find('<', 4)
pos_b = a.find('<', 4)
if (
isinstance(or_groups[0], MultiConstraint)
and isinstance(or_groups[1], MultiConstraint)
and len(or_groups[0].constraints)
and len(or_groups[1].constraints)
and a[:3] == '[>=' and pos_a != -1
and b[:3] == '[>=' and pos_b != -1
and a[pos_a + 2:-1] == b[4:pos_b - 5]
):
constraint = MultiConstraint(
Constraint('>=', a[4:pos_a - 5]),
Constraint('<', b[pos_b + 2:-1])
)
else:
constraint = MultiConstraint(or_groups, False)
else:
constraint = MultiConstraint(or_groups, False)
constraint.pretty_string = pretty_constraint
return constraint
def _parse_constraint(self, constraint):
m = re.match('(?i)^v?[xX*](\.[xX*])*$', constraint)
if m:
return EmptyConstraint(),
version_regex = (
'v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?'
).format(self._modifier_regex)
# Tilde range
#
# Like wildcard constraints, unsuffixed tilde constraints
# say that they must be greater than the previous version,
# to ensure that unstable instances of the current version are allowed.
# However, if a stability suffix is added to the constraint,
# then a >= match on the current version is used instead.
m = re.match('(?i)^~{}$'.format(version_regex), constraint)
if m:
# Work out which position in the version we are operating at
if m.group(4):
position = 3
elif m.group(3):
position = 2
elif m.group(2):
position = 1
else:
position = 0
# Calculate the stability suffix
stability_suffix = ''
if m.group(5):
stability_suffix += '-{}{}'.format(
_expand_stability(m.group(5)),
'.' + m.group(6) if m.group(6) else ''
)
low_version = self._manipulate_version_string(
m.groups(), position, 0
) + stability_suffix
lower_bound = Constraint('>=', low_version)
# For upper bound,
# we increment the position of one more significance,
# but high_position = 0 would be illegal
high_position = max(0, position - 1)
high_version = self._manipulate_version_string(
m.groups(), high_position, 1
)
upper_bound = Constraint('<', high_version)
return lower_bound, upper_bound
# Caret range
#
# Allows changes that do not modify
# the left-most non-zero digit in the [major, minor, patch] tuple.
# In other words, this allows:
# - patch and minor updatesfor versions 1.0.0 and above,
# - patch updates for versions 0.X >=0.1.0,
# - and no updates for versions 0.0.X
m = re.match('^\^{}($)'.format(version_regex), constraint)
if m:
if m.group(1) != '0' or not m.group(2):
position = 0
elif m.group(2) != '0' or not m.group(3):
position = 1
else:
position = 2
low_version = normalize_version(constraint[1:])
lower_bound = Constraint('>=', low_version)
# For upper bound,
# we increment the position of one more significance,
# but high_position = 0 would be illegal
high_version = self._manipulate_version_string(
m.groups(), position, 1
)
upper_bound = Constraint('<', high_version)
return lower_bound, upper_bound
# X range
#
# Any of X, x, or * may be used to "stand in"
# for one of the numeric values in the [major, minor, patch] tuple.
# A partial version range is treated as an X-Range,
# so the special character is in fact optional.
m = re.match(
'^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
constraint
)
if m:
if m.group(3):
position = 2
elif m.group(2):
position = 1
else:
position = 0
low_version = self._manipulate_version_string(
m.groups(), position
)
high_version = self._manipulate_version_string(
m.groups(), position, 1
)
if low_version == '0.0.0.0':
return Constraint('<', high_version),
return Constraint('>=', low_version), Constraint('<', high_version)
# Basic Comparators
m = re.match('^(<>|!=|>=?|<=?|==?)?\s*(.*)', constraint)
if m:
try:
version = normalize_version(m.group(2))
return Constraint(m.group(1) or '=', version),
except ValueError:
pass
raise ValueError(
'Could not parse version constraint: {}'.format(constraint)
)
def _manipulate_version_string(self, matches, position,
increment=0, pad='0'):
"""
Increment, decrement, or simply pad a version number.
"""
matches = [matches[i]
if i < len(matches) - 1 and matches[i] is not None else '0'
for i in range(4)]
for i in range(3, -1, -1):
if i > position:
matches[i] = pad
elif i == position and increment:
matches[i] = int(matches[i]) + increment
# If $matches[i] was 0, carry the decrement
if matches[i] < 0:
matches[i] = pad
position -= 1
# Return null on a carry overflow
if i == 1:
return
return '{}.{}.{}.{}'.format(matches[0], matches[1],
matches[2], matches[3])
import pytest
from poetry.semver.constraints.constraint import Constraint
@pytest.mark.parametrize(
'require_op, require_version, provide_op, provide_version',
[
('==', '1', '==', '1'),
('>=', '1', '>=', '2'),
('>=', '2', '>=', '1'),
('>=', '2', '>', '1'),
('<=', '2', '>=', '1'),
('>=', '1', '<=', '2'),
('==', '2', '>=', '2'),
('!=', '1', '!=', '1'),
('!=', '1', '==', '2'),
('!=', '1', '<', '1'),
('!=', '1', '<=', '1'),
('!=', '1', '>', '1'),
('!=', '1', '>=', '1')
]
)
def test_version_match_succeeds(require_op, require_version,
provide_op, provide_version):
require = Constraint(require_op, require_version)
provide = Constraint(provide_op, provide_version)
assert require.matches(provide)
@pytest.mark.parametrize(
'require_op, require_version, provide_op, provide_version',
[
('==', '1', '==', '2'),
('>=', '2', '<=', '1'),
('>=', '2', '<', '2'),
('<=', '2', '>', '2'),
('>', '2', '<=', '2'),
('<=', '1', '>=', '2'),
('>=', '2', '<=', '1'),
('==', '2', '<', '2'),
('!=', '1', '==', '1'),
('==', '1', '!=', '1'),
]
)
def test_version_match_fails(require_op, require_version,
provide_op, provide_version):
require = Constraint(require_op, require_version)
provide = Constraint(provide_op, provide_version)
assert not require.matches(provide)
def test_invalid_operators():
with pytest.raises(ValueError):
Constraint('invalid', '1.2.3')
from poetry.semver.constraints.constraint import Constraint
from poetry.semver.constraints.multi_constraint import MultiConstraint
def test_multi_version_match_succeeds():
require_start = Constraint('>', '1.0')
require_end = Constraint('<', '1.2')
provider = Constraint('==', '1.1')
multi = MultiConstraint((require_start, require_end))
assert multi.matches(provider)
def test_multi_version_provided_match_succeeds():
require_start = Constraint('>', '1.0')
require_end = Constraint('<', '1.2')
provide_start = Constraint('>=', '1.1')
provide_end = Constraint('<', '2.0')
multi_require = MultiConstraint((require_start, require_end))
multi_provide = MultiConstraint((provide_start, provide_end))
assert multi_require.matches(multi_provide)
def test_multi_version_match_fails():
require_start = Constraint('>', '1.0')
require_end = Constraint('<', '1.2')
provider = Constraint('==', '1.2')
multi = MultiConstraint((require_start, require_end))
assert not multi.matches(provider)
import pytest
from poetry.semver.comparison import compare
from poetry.semver.comparison import equal
from poetry.semver.comparison import greater_than
from poetry.semver.comparison import greater_than_or_equal
from poetry.semver.comparison import less_than
from poetry.semver.comparison import less_than_or_equal
from poetry.semver.comparison import not_equal
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', True),
('1.25.0', '1.25.0', False),
('1.25.0', '1.26.0', False),
]
)
def test_greater_than(version1, version2, expected):
if expected is True:
assert greater_than(version1, version2)
else:
assert not greater_than(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', True),
('1.25.0', '1.25.0', True),
('1.25.0', '1.26.0', False),
]
)
def test_greater_than_or_equal(version1, version2, expected):
if expected is True:
assert greater_than_or_equal(version1, version2)
else:
assert not greater_than_or_equal(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', False),
('1.25.0', '1.25.0', False),
('1.25.0', '1.26.0', True),
('1.25.0', '1.26.0-beta', True),
('1.25.0', '1.25.0-beta', False),
]
)
def test_less_than(version1, version2, expected):
if expected is True:
assert less_than(version1, version2)
else:
assert not less_than(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', False),
('1.25.0', '1.25.0', True),
('1.25.0', '1.26.0', True),
]
)
def test_less_than_or_equal(version1, version2, expected):
if expected is True:
assert less_than_or_equal(version1, version2)
else:
assert not less_than_or_equal(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', False),
('1.25.0', '1.25.0', True),
('1.25.0', '1.26.0', False),
]
)
def test_equal(version1, version2, expected):
if expected is True:
assert equal(version1, version2)
else:
assert not equal(version1, version2)
@pytest.mark.parametrize(
'version1, version2, expected',
[
('1.25.0', '1.24.0', True),
('1.25.0', '1.25.0', False),
('1.25.0', '1.26.0', True),
]
)
def test_not_equal(version1, version2, expected):
if expected is True:
assert not_equal(version1, version2)
else:
assert not not_equal(version1, version2)
@pytest.mark.parametrize(
'version1, operator, version2, expected',
[
('1.25.0', '>', '1.24.0', True),
('1.25.0', '>', '1.25.0', False),
('1.25.0', '>', '1.26.0', False),
('1.25.0', '>=', '1.24.0', True),
('1.25.0', '>=', '1.25.0', True),
('1.25.0', '>=', '1.26.0', False),
('1.25.0', '<', '1.24.0', False),
('1.25.0', '<', '1.25.0', False),
('1.25.0', '<', '1.26.0', True),
('1.25.0-beta2.1', '<', '1.25.0-b.3', True),
('1.25.0-b2.1', '<', '1.25.0beta.3', True),
('1.25.0-b-2.1', '<', '1.25.0-rc', True),
('1.25.0', '<=', '1.24.0', False),
('1.25.0', '<=', '1.25.0', True),
('1.25.0', '<=', '1.26.0', True),
('1.25.0', '==', '1.24.0', False),
('1.25.0', '==', '1.25.0', True),
('1.25.0', '==', '1.26.0', False),
('1.25.0-beta2.1', '==', '1.25.0-b.2.1', True),
('1.25.0beta2.1', '==', '1.25.0-b2.1', True),
('1.25.0', '=', '1.24.0', False),
('1.25.0', '=', '1.25.0', True),
('1.25.0', '=', '1.26.0', False),
('1.25.0', '!=', '1.24.0', True),
('1.25.0', '!=', '1.25.0', False),
('1.25.0', '!=', '1.26.0', True),
]
)
def test_compare(version1, operator, version2, expected):
if expected is True:
assert compare(version1, operator, version2)
else:
assert not compare(version1, operator, version2)
import pytest
from poetry.semver.helpers import normalize_version
@pytest.mark.parametrize(
'version,expected',
[
('1.0.0', '1.0.0.0'),
('1.2.3.4', '1.2.3.4'),
('1.0.0RC1', '1.0.0.0-rc.1'),
('1.0.0rC13', '1.0.0.0-rc.13'),
('1.0.0.RC.15-dev', '1.0.0.0-rc.15'),
('1.0.0-rc1', '1.0.0.0-rc.1'),
('1.0.0.pl3', '1.0.0.0-patch.3'),
('1.0', '1.0.0.0'),
('0', '0.0.0.0'),
('10.4.13-b', '10.4.13.0-beta'),
('10.4.13-b5', '10.4.13.0-beta.5'),
('v1.0.0', '1.0.0.0'),
('2010.01', '2010.01.0.0'),
('2010.01.02', '2010.01.02.0'),
('v20100102', '20100102'),
('2010-01-02', '2010.01.02'),
('2010-01-02.5', '2010.01.02.5'),
('20100102-203040', '20100102.203040'),
('20100102203040-10', '20100102203040.10'),
('20100102-203040-p1', '20100102.203040-patch.1'),
('1.0.0-beta.5+foo', '1.0.0.0-beta.5'),
]
)
def test_normalize(version, expected):
assert normalize_version(version) == expected
@pytest.mark.parametrize(
'version',
[
'',
'1.0.0-meh',
'1.0.0.0.0',
'1.0.0+foo bar',
]
)
def test_normalize_fail(version):
with pytest.raises(ValueError):
normalize_version(version)
import pytest
from poetry.semver import sort, rsort, statisfies, satisfied_by
@pytest.mark.parametrize(
'version, constraint',
[
('1.2.3', '^1.2.3+build'),
('1.3.0', '^1.2.3+build'),
('1.3.0-beta', '>1.2'),
('1.2.3-beta', '<=1.2.3'),
('1.0.0', '1.0.0'),
('1.2.3', '*'),
('v1.2.3', '*'),
('1.0.0', '>=1.0.0'),
('1.0.1', '>=1.0.0'),
('1.1.0', '>=1.0.0'),
('1.0.1', '>1.0.0'),
('1.1.0', '>1.0.0'),
('2.0.0', '<=2.0.0'),
('1.9999.9999', '<=2.0.0'),
('0.2.9', '<=2.0.0'),
('1.9999.9999', '<2.0.0'),
('0.2.9', '<2.0.0'),
('1.0.0', '>= 1.0.0'),
('1.0.1', '>= 1.0.0'),
('1.1.0', '>= 1.0.0'),
('1.0.1', '> 1.0.0'),
('1.1.0', '> 1.0.0'),
('2.0.0', '<= 2.0.0'),
('1.9999.9999', '<= 2.0.0'),
('0.2.9', '<= 2.0.0'),
('1.9999.9999', '< 2.0.0'),
('0.2.9', "<\t2.0.0"),
('v0.1.97', '>=0.1.97'),
('0.1.97', '>=0.1.97'),
('1.2.4', '0.1.20 || 1.2.4'),
('0.0.0', '>=0.2.3 || <0.0.1'),
('0.2.3', '>=0.2.3 || <0.0.1'),
('0.2.4', '>=0.2.3 || <0.0.1'),
('2.1.3', '2.x.x'),
('1.2.3', '1.2.x'),
('2.1.3', '1.2.x || 2.x'),
('1.2.3', '1.2.x || 2.x'),
('1.2.3', 'x'),
('2.1.3', '2.*.*'),
('1.2.3', '1.2.*'),
('2.1.3', '1.2.* || 2.*'),
('1.2.3', '1.2.* || 2.*'),
('1.2.3', '*'),
('2.9.0', '~2.4'), # >= 2.4.0 < 3.0.0
('2.4.5', '~2.4'),
('1.2.3', '~1'), # >= 1.0.0 < 2.0.0
('1.4.7', '~1.0'), # >= 1.0.0 < 2.0.0
('1.0.0', '>=1'),
('1.0.0', '>= 1'),
('1.2.8', '>1.2'), # > 1.2.0
('1.1.1', '<1.2'), # < 1.2.0
('1.1.1', '< 1.2'),
('1.2.3', '~1.2.1 >=1.2.3'),
('1.2.3', '~1.2.1 =1.2.3'),
('1.2.3', '~1.2.1 1.2.3'),
('1.2.3', '~1.2.1 >=1.2.3 1.2.3'),
('1.2.3', '~1.2.1 1.2.3 >=1.2.3'),
('1.2.3', '~1.2.1 1.2.3'),
('1.2.3', '>=1.2.1 1.2.3'),
('1.2.3', '1.2.3 >=1.2.1'),
('1.2.3', '>=1.2.3 >=1.2.1'),
('1.2.3', '>=1.2.1 >=1.2.3'),
('1.2.8', '>=1.2'),
('1.8.1', '^1.2.3'),
('0.1.2', '^0.1.2'),
('0.1.2', '^0.1'),
('1.4.2', '^1.2'),
('1.4.2', '^1.2 ^1'),
('0.0.1-beta', '^0.0.1-alpha'),
]
)
def test_statisfies_positive(version, constraint):
assert statisfies(version, constraint)
@pytest.mark.parametrize(
'version, constraint',
[
('2.0.0', '^1.2.3+build'),
('1.2.0', '^1.2.3+build'),
('1.0.0beta', '1'),
('1.0.1', '1.0.0'),
('0.0.0', '>=1.0.0'),
('0.0.1', '>=1.0.0'),
('0.1.0', '>=1.0.0'),
('0.0.1', '>1.0.0'),
('0.1.0', '>1.0.0'),
('3.0.0', '<=2.0.0'),
('2.9999.9999', '<=2.0.0'),
('2.2.9', '<=2.0.0'),
('2.9999.9999', '<2.0.0'),
('2.2.9', '<2.0.0'),
('v0.1.93', '>=0.1.97'),
('0.1.93', '>=0.1.97'),
('1.2.3', '0.1.20 || 1.2.4'),
('0.0.3', '>=0.2.3 || <0.0.1'),
('0.2.2', '>=0.2.3 || <0.0.1'),
('1.1.3', '2.x.x'),
('3.1.3', '2.x.x'),
('1.3.3', '1.2.x'),
('3.1.3', '1.2.x || 2.x'),
('1.1.3', '1.2.x || 2.x'),
('1.1.3', '2.*.*'),
('3.1.3', '2.*.*'),
('1.3.3', '1.2.*'),
('3.1.3', '1.2.* || 2.*'),
('1.1.3', '1.2.* || 2.*'),
('1.1.2', '2'),
('2.4.1', '2.3'),
('3.0.0', '~2.4'), # >= 2.4.0 < 3.0.0
('2.3.9', '~2.4'),
('0.2.3', '~1'), # >= 1.0.0 < 2.0.0
('1.0.0', '<1'),
('1.1.1', '>=1.2'),
('2.0.0beta', '1'),
('0.5.4-alpha', '~v0.5.4-beta'),
('1.2.2', '^1.2.3'),
('1.1.9', '^1.2'),
]
)
def test_statisfies_negative(version, constraint):
assert not statisfies(version, constraint)
@pytest.mark.parametrize(
'constraint, versions, expected',
[
(
'~1.0',
['1.0', '1.2', '1.9999.9999', '2.0', '2.1', '0.9999.9999'],
['1.0', '1.2', '1.9999.9999'],
),
(
'>1.0 <3.0 || >=4.0',
['1.0', '1.1', '2.9999.9999', '3.0', '3.1', '3.9999.9999', '4.0', '4.1'],
['1.1', '2.9999.9999', '4.0', '4.1'],
),
(
'^0.2.0',
['0.1.1', '0.1.9999', '0.2.0', '0.2.1', '0.3.0'],
['0.2.0', '0.2.1'],
),
]
)
def test_satisfied_by(constraint, versions, expected):
assert satisfied_by(versions, constraint) == expected
@pytest.mark.parametrize(
'versions, sorted, rsorted',
[
(
['1.0', '0.1', '0.1', '3.2.1', '2.4.0-alpha', '2.4.0'],
['0.1', '0.1', '1.0', '2.4.0-alpha', '2.4.0', '3.2.1'],
['3.2.1', '2.4.0', '2.4.0-alpha', '1.0', '0.1', '0.1'],
)
]
)
def test_sort(versions, sorted, rsorted):
assert sort(versions) == sorted
assert rsort(versions) == rsorted
import pytest
from poetry.semver.version_parser import VersionParser
from poetry.semver.constraints.constraint import Constraint
from poetry.semver.constraints.empty_constraint import EmptyConstraint
from poetry.semver.constraints.multi_constraint import MultiConstraint
@pytest.fixture
def parser():
return VersionParser()
@pytest.mark.parametrize(
'input,constraint',
[
('*', EmptyConstraint()),
('*.*', EmptyConstraint()),
('v*.*', EmptyConstraint()),
('*.x.*', EmptyConstraint()),
('x.X.x.*', EmptyConstraint()),
('!=1.0.0', Constraint('!=', '1.0.0.0')),
('>1.0.0', Constraint('>', '1.0.0.0')),
('<1.2.3.4', Constraint('<', '1.2.3.4')),
('<=1.2.3', Constraint('<=', '1.2.3.0')),
('>=1.2.3', Constraint('>=', '1.2.3.0')),
('=1.2.3', Constraint('=', '1.2.3.0')),
('1.2.3', Constraint('=', '1.2.3.0')),
('=1.0', Constraint('=', '1.0.0.0')),
('1.2.3b5', Constraint('=', '1.2.3.0-beta.5')),
('>= 1.2.3', Constraint('>=', '1.2.3.0'))
]
)
def test_parse_constraints_simple(parser, input, constraint):
assert str(parser.parse_constraints(input)) == str(constraint)
@pytest.mark.parametrize(
'input,min,max',
[
('v2.*', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('2.*.*', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('20.*', Constraint('>=', '20.0.0.0'), Constraint('<', '21.0.0.0')),
('20.*.*', Constraint('>=', '20.0.0.0'), Constraint('<', '21.0.0.0')),
('2.0.*', Constraint('>=', '2.0.0.0'), Constraint('<', '2.1.0.0')),
('2.x', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('2.x.x', Constraint('>=', '2.0.0.0'), Constraint('<', '3.0.0.0')),
('2.2.X', Constraint('>=', '2.2.0.0'), Constraint('<', '2.3.0.0')),
('0.*', None, Constraint('<', '1.0.0.0')),
('0.*.*', None, Constraint('<', '1.0.0.0')),
('0.x', None, Constraint('<', '1.0.0.0')),
]
)
def test_parse_constraints_wildcard(parser, input, min, max):
if min:
expected = MultiConstraint((min, max))
else:
expected = max
assert str(parser.parse_constraints(input)) == str(expected)
@pytest.mark.parametrize(
'input,min,max',
[
('~v1', Constraint('>=', '1.0.0.0'), Constraint('<', '2.0.0.0')),
('~1.0', Constraint('>=', '1.0.0.0'), Constraint('<', '2.0.0.0')),
('~1.0.0', Constraint('>=', '1.0.0.0'), Constraint('<', '1.1.0.0')),
('~1.2', Constraint('>=', '1.2.0.0'), Constraint('<', '2.0.0.0')),
('~1.2.3', Constraint('>=', '1.2.3.0'), Constraint('<', '1.3.0.0')),
('~1.2.3.4', Constraint('>=', '1.2.3.4'), Constraint('<', '1.2.4.0')),
('~1.2-beta', Constraint('>=', '1.2.0.0-beta'), Constraint('<', '2.0.0.0')),
('~1.2-b2', Constraint('>=', '1.2.0.0-beta.2'), Constraint('<', '2.0.0.0')),
]
)
def test_parse_constraints_tilde(parser, input, min, max):
if min:
expected = MultiConstraint((min, max))
else:
expected = max
assert str(parser.parse_constraints(input)) == str(expected)
@pytest.mark.parametrize(
'input,min,max',
[
('^v1', Constraint('>=', '1.0.0.0'), Constraint('<', '2.0.0.0')),
('^0', Constraint('>=', '0.0.0.0'), Constraint('<', '1.0.0.0')),
('^0.0', Constraint('>=', '0.0.0.0'), Constraint('<', '0.1.0.0')),
('^1.2', Constraint('>=', '1.2.0.0'), Constraint('<', '2.0.0.0')),
('^1.2.3-beta.2', Constraint('>=', '1.2.3.0-beta.2'), Constraint('<', '2.0.0.0')),
('^1.2.3.4', Constraint('>=', '1.2.3.4'), Constraint('<', '2.0.0.0')),
('^1.2.3', Constraint('>=', '1.2.3.0'), Constraint('<', '2.0.0.0')),
('^0.2.3', Constraint('>=', '0.2.3.0'), Constraint('<', '0.3.0.0')),
('^0.2', Constraint('>=', '0.2.0.0'), Constraint('<', '0.3.0.0')),
('^0.2.0', Constraint('>=', '0.2.0.0'), Constraint('<', '0.3.0.0')),
('^0.0.3', Constraint('>=', '0.0.3.0'), Constraint('<', '0.0.4.0')),
]
)
def test_parse_constraints_caret(parser, input, min, max):
if min:
expected = MultiConstraint((min, max))
else:
expected = max
assert str(parser.parse_constraints(input)) == str(expected)
@pytest.mark.parametrize(
'input',
[
'>2.0,<=3.0',
'>2.0 <=3.0',
'>2.0 <=3.0',
'>2.0, <=3.0',
'>2.0 ,<=3.0',
'>2.0 , <=3.0',
'>2.0 , <=3.0',
'> 2.0 <= 3.0',
'> 2.0 , <= 3.0',
' > 2.0 , <= 3.0 ',
]
)
def test_parse_constraints_multi(parser, input):
first = Constraint('>', '2.0.0.0')
second = Constraint('<=', '3.0.0.0')
multi = MultiConstraint((first, second))
assert str(parser.parse_constraints(input)) == str(multi)
@pytest.mark.parametrize(
'input',
[
'>2.0,<2.0.5 | >2.0.6',
'>2.0,<2.0.5 || >2.0.6',
'> 2.0 , <2.0.5 | > 2.0.6',
]
)
def test_parse_constraints_multi2(parser, input):
first = Constraint('>', '2.0.0.0')
second = Constraint('<', '2.0.5.0')
third = Constraint('>', '2.0.6.0')
multi1 = MultiConstraint((first, second))
multi2 = MultiConstraint((multi1, third), False)
assert str(parser.parse_constraints(input)) == str(multi2)
@pytest.mark.parametrize(
'input',
[
'',
'1.0.0-meh',
'>2.0,,<=3.0',
'>2.0 ,, <=3.0',
'>2.0 ||| <=3.0',
]
)
def test_parse_constraints_fail(parser, input):
with pytest.raises(ValueError):
parser.parse_constraints(input)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment