lib.version: Fix version range expansion #109
3 changed files with 102 additions and 43 deletions
|
|
@ -70,17 +70,18 @@ class Dependency: # export
|
||||||
specified = self.__parsed_spec[1]
|
specified = self.__parsed_spec[1]
|
||||||
if specified is None:
|
if specified is None:
|
||||||
return []
|
return []
|
||||||
if not expanded or not specified.version.is_full:
|
if not expanded or specified.version.is_full:
|
||||||
return (specified, )
|
return (specified, )
|
||||||
ret: list[Boundary] = []
|
ret: list[Boundary] = []
|
||||||
match specified.op:
|
match specified.op:
|
||||||
case '>' | '>=':
|
|
||||||
ret.append(specified)
|
|
||||||
ret.append(Boundary('<', specified.version.next_binary_incompatible))
|
|
||||||
case '=':
|
case '=':
|
||||||
|
try:
|
||||||
|
next_version = specified.version.next
|
||||||
|
except Version.Error:
|
||||||
|
return (specified, )
|
||||||
ret.append(Boundary('>=', specified.version))
|
ret.append(Boundary('>=', specified.version))
|
||||||
ret.append(Boundary('<', specified.version.next_binary_incompatible))
|
ret.append(Boundary('<', next_version))
|
||||||
case '<' | '<=':
|
case '>' | '>=' | '<' | '<=':
|
||||||
ret.append(specified)
|
ret.append(specified)
|
||||||
case _:
|
case _:
|
||||||
self.__raise(
|
self.__raise(
|
||||||
|
|
@ -132,7 +133,7 @@ class Dependency: # export
|
||||||
|
|
||||||
def version_boundaries(self, expanded: bool = False) -> Sequence[Boundary]:
|
def version_boundaries(self, expanded: bool = False) -> Sequence[Boundary]:
|
||||||
"""The parsed version boundary, or the range it spans when
|
"""The parsed version boundary, or the range it spans when
|
||||||
expanded is True and it pins a full version.
|
expanded is True and it does not pin a full version.
|
||||||
"""
|
"""
|
||||||
return self.__version_boundaries(expanded)
|
return self.__version_boundaries(expanded)
|
||||||
|
|
||||||
|
|
@ -147,11 +148,12 @@ class Dependency: # export
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Render the dependency as a version constraint string.
|
"""Render the dependency as a version constraint string.
|
||||||
|
|
||||||
NAMES_ONLY renders the name alone. untemplated keeps the
|
NAMES_ONLY renders the name alone. With untemplated = False the
|
||||||
VERSION, VERSION-REVISION and REVISION macros as written instead
|
VERSION, VERSION-REVISION and REVISION macros are kept as
|
||||||
of the resolved versions. include_revision = False drops the
|
written instead of resolved. include_revision = False drops the
|
||||||
revision of VERSION specs. as_range expands a boundary that
|
revision of VERSION specs. as_range expands a = boundary that
|
||||||
pins a full version into the range it spans. no_subpackages
|
does not pin a full version into the range it spans; other
|
||||||
|
operators pass through unchanged. no_subpackages
|
||||||
renders the base name, quote wraps the result in the given
|
renders the base name, quote wraps the result in the given
|
||||||
string.
|
string.
|
||||||
"""
|
"""
|
||||||
|
|
@ -161,12 +163,12 @@ class Dependency: # export
|
||||||
if syntax is Syntax.NAMES_ONLY:
|
if syntax is Syntax.NAMES_ONLY:
|
||||||
return name
|
return name
|
||||||
ret: list[str] = []
|
ret: list[str] = []
|
||||||
for boundary in self.version_boundaries(expanded = as_range, ):
|
for boundary in self.version_boundaries(expanded = as_range):
|
||||||
op = boundary.format_op(syntax)
|
op = boundary.format_op(syntax)
|
||||||
version = boundary.version.id(untemplated, include_revision)
|
version = boundary.version.id(untemplated, include_revision)
|
||||||
ret.append(f'{name} {op} {version}')
|
ret.append(f'{name} {op} {version}')
|
||||||
if ret:
|
if ret:
|
||||||
return ' '.join(ret)
|
return ', '.join(ret)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
return __str() if quote is None else f'{quote}{__str()}{quote}'
|
return __str() if quote is None else f'{quote}{__str()}{quote}'
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,17 @@ class Version: # export
|
||||||
raise Version.Error(
|
raise Version.Error(
|
||||||
f'Tried to look up version of {self.base_name} without lookup function'
|
f'Tried to look up version of {self.base_name} without lookup function'
|
||||||
)
|
)
|
||||||
version = self.__lookup_version(self.base_name)
|
resolved = self.__lookup_version(self.base_name)
|
||||||
if self.__spec == 'REVISION':
|
if self.__spec == 'VERSION-REVISION':
|
||||||
parts = version.split('-', 1)
|
return resolved
|
||||||
|
parts = resolved.split('-')
|
||||||
|
match self.__spec:
|
||||||
|
case 'VERSION':
|
||||||
|
return parts[0]
|
||||||
|
case 'REVISION':
|
||||||
return parts[1] if len(parts) == 2 else ''
|
return parts[1] if len(parts) == 2 else ''
|
||||||
return version
|
case _:
|
||||||
|
raise Version.Error(f'Invalid version specifier "{self.__spec}"')
|
||||||
|
|
||||||
def __id(self, untemplate: bool, throw: bool = True) -> str:
|
def __id(self, untemplate: bool, throw: bool = True) -> str:
|
||||||
if not untemplate:
|
if not untemplate:
|
||||||
|
|
@ -135,9 +141,8 @@ class Version: # export
|
||||||
def is_full(self) -> bool:
|
def is_full(self) -> bool:
|
||||||
"""True if the spec pins a complete version: the
|
"""True if the spec pins a complete version: the
|
||||||
VERSION-REVISION macro, or a literal of the form
|
VERSION-REVISION macro, or a literal of the form
|
||||||
MAJOR.MINOR.MICRO-REVISION. Only full versions have the
|
MAJOR.MINOR.MICRO-REVISION. Range expansion leaves full
|
||||||
exclusive upper bound (next_binary_incompatible) that range
|
versions exact and expands the non-full specs.
|
||||||
expansion relies on.
|
|
||||||
"""
|
"""
|
||||||
if self.__spec == 'VERSION-REVISION':
|
if self.__spec == 'VERSION-REVISION':
|
||||||
return True
|
return True
|
||||||
|
|
@ -145,8 +150,8 @@ class Version: # export
|
||||||
|
|
||||||
def id(self, untemplate: bool, include_revision: bool = True) -> str:
|
def id(self, untemplate: bool, include_revision: bool = True) -> str:
|
||||||
"""Return the version id: the raw spec if untemplate is False,
|
"""Return the version id: the raw spec if untemplate is False,
|
||||||
else the resolved version. With include_revision = False a
|
else the resolved version. A VERSION spec yields the core
|
||||||
VERSION spec yields the core version.
|
version.
|
||||||
"""
|
"""
|
||||||
if not include_revision and self.__spec == 'VERSION':
|
if not include_revision and self.__spec == 'VERSION':
|
||||||
return self.core(untemplate)
|
return self.core(untemplate)
|
||||||
|
|
@ -215,3 +220,28 @@ class Version: # export
|
||||||
f'{self.major}.{self.minor + 1}.0',
|
f'{self.major}.{self.minor + 1}.0',
|
||||||
self.__lookup_version,
|
self.__lookup_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next(self) -> Version:
|
||||||
|
"""The next version in the series: the last part
|
||||||
|
incremented. next of '1' is '2', of '1.0' is '1.1', of
|
||||||
|
'1.2.3' is '1.2.4', of '1.2.3-45' is '1.2.3-46'. The
|
||||||
|
separators of the spec are kept. Like in
|
||||||
|
next_binary_compatible(), only the last part's leading
|
||||||
|
digits count: a suffixed part such as '4blah' steps to
|
||||||
|
'5'. A part without leading digits raises Version.Error.
|
||||||
|
"""
|
||||||
|
untemplated = self.__id(untemplate = True, throw = True)
|
||||||
|
parts = re.split('([.-])', untemplated)
|
||||||
|
m = re.match('[0-9]+', parts[-1])
|
||||||
|
if not m:
|
||||||
|
raise Version.Error(
|
||||||
|
f'Cannot step version "{self.__spec}": the last '
|
||||||
|
f'part "{parts[-1]}" has no leading digits'
|
||||||
|
)
|
||||||
|
parts[-1] = str(int(m.group()) + 1)
|
||||||
|
return Version(
|
||||||
|
self.__name,
|
||||||
|
''.join(parts),
|
||||||
|
self.__lookup_version,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ except Version.Error:
|
||||||
|
|
||||||
# The lookup resolves the macros
|
# The lookup resolves the macros
|
||||||
d = Dependency('foo = VERSION', app.get_version)
|
d = Dependency('foo = VERSION', app.get_version)
|
||||||
assert d.constraint_str() == 'foo = 1.2.3-45'
|
assert d.constraint_str() == 'foo = 1.2.3'
|
||||||
d = Dependency('foo = VERSION', app.get_version)
|
d = Dependency('foo = VERSION', app.get_version)
|
||||||
assert d.constraint_str(include_revision = False) == 'foo = 1.2.3'
|
assert d.constraint_str(include_revision = False) == 'foo = 1.2.3'
|
||||||
# Only the VERSION macro loses its revision, VERSION-REVISION and
|
# Only the VERSION macro loses its revision, VERSION-REVISION and
|
||||||
|
|
@ -89,7 +89,7 @@ except Dependency.Error:
|
||||||
|
|
||||||
# The lookup uses the base name, not the subpackage name
|
# The lookup uses the base name, not the subpackage name
|
||||||
d = Dependency('foo-devel = VERSION', app.get_version)
|
d = Dependency('foo-devel = VERSION', app.get_version)
|
||||||
assert d.constraint_str() == 'foo-devel = 1.2.3-45'
|
assert d.constraint_str() == 'foo-devel = 1.2.3'
|
||||||
# Unknown projects fail when untemplating
|
# Unknown projects fail when untemplating
|
||||||
try:
|
try:
|
||||||
Dependency('baz = VERSION', app.get_version).constraint_str()
|
Dependency('baz = VERSION', app.get_version).constraint_str()
|
||||||
|
|
@ -105,34 +105,38 @@ def mapper(project: str) -> str:
|
||||||
return '1.2.3-45'
|
return '1.2.3-45'
|
||||||
|
|
||||||
d = Dependency('foo = VERSION', mapper)
|
d = Dependency('foo = VERSION', mapper)
|
||||||
assert d.constraint_str() == 'foo = 1.2.3-45'
|
assert d.constraint_str() == 'foo = 1.2.3'
|
||||||
assert d.constraint_str() == 'foo = 1.2.3-45'
|
assert d.constraint_str() == 'foo = 1.2.3'
|
||||||
assert calls == ['foo']
|
assert calls == ['foo']
|
||||||
|
|
||||||
# as_range expands full boundaries into a revision range
|
# as_range expands a non-full = boundary into the range it spans
|
||||||
d = Dependency('foo = VERSION-REVISION', app.get_version)
|
d = Dependency('foo = VERSION-REVISION', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3-45 foo < 1.2.4'
|
assert d.constraint_str(as_range = True) == 'foo = 1.2.3-45'
|
||||||
d = Dependency('foo >= VERSION-REVISION', app.get_version)
|
d = Dependency('foo >= VERSION-REVISION', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3-45 foo < 1.2.4'
|
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3-45'
|
||||||
d = Dependency('foo > 1.0.0-259', app.get_version)
|
d = Dependency('foo > 1.0.0-259', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo > 1.0.0-259 foo < 1.0.1'
|
assert d.constraint_str(as_range = True) == 'foo > 1.0.0-259'
|
||||||
# A raw render keeps the macro, with the computed bound alongside it
|
# A raw render keeps the macro as written
|
||||||
d = Dependency('foo = VERSION-REVISION', app.get_version)
|
d = Dependency('foo = VERSION-REVISION', app.get_version)
|
||||||
constraint = d.constraint_str(untemplated = False, as_range = True)
|
constraint = d.constraint_str(untemplated = False, as_range = True)
|
||||||
assert constraint == 'foo >= VERSION-REVISION foo < 1.2.4'
|
assert constraint == 'foo = VERSION-REVISION'
|
||||||
# VERSION is not full, so a VERSION constraint is not expanded
|
# A full version stays exact; a non-full one expands
|
||||||
d = Dependency('foo = VERSION', app.get_version)
|
d = Dependency('foo = VERSION', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo = 1.2.3-45'
|
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3, foo < 1.2.4'
|
||||||
|
# Operators other than = pass through unchanged
|
||||||
d = Dependency('foo >= VERSION', app.get_version)
|
d = Dependency('foo >= VERSION', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3-45'
|
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3'
|
||||||
# '<' and '<=' are not expanded
|
|
||||||
d = Dependency('foo <= VERSION', app.get_version)
|
d = Dependency('foo <= VERSION', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo <= 1.2.3-45'
|
assert d.constraint_str(as_range = True) == 'foo <= 1.2.3'
|
||||||
# versions without a full major.minor.micro-revision are not expanded
|
# The bound steps the last part, whatever it is
|
||||||
d = Dependency('foo = 1.0', app.get_version)
|
d = Dependency('foo = 1.0', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo = 1.0'
|
assert d.constraint_str(as_range = True) == 'foo >= 1.0, foo < 1.1'
|
||||||
d = Dependency('foo = 1.2.3', app.get_version)
|
d = Dependency('foo = 1.2.3', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo = 1.2.3'
|
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3, foo < 1.2.4'
|
||||||
|
d = Dependency('foo = 1.2.3rc1', app.get_version)
|
||||||
|
assert d.constraint_str(as_range = True) == 'foo >= 1.2.3rc1, foo < 1.2.4'
|
||||||
|
# A last part without leading digits cannot be stepped, so the pin
|
||||||
|
# stays exact
|
||||||
d = Dependency('foo = 1.0-rc1', app.get_version)
|
d = Dependency('foo = 1.0-rc1', app.get_version)
|
||||||
assert d.constraint_str(as_range = True) == 'foo = 1.0-rc1'
|
assert d.constraint_str(as_range = True) == 'foo = 1.0-rc1'
|
||||||
# Default syntax is SEM_VER
|
# Default syntax is SEM_VER
|
||||||
|
|
@ -197,6 +201,29 @@ for spec in ['1.2.3-rc1', '1.2.3']:
|
||||||
except Version.Error:
|
except Version.Error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# next steps the last existing part, whatever it is
|
||||||
|
for spec, expected in [
|
||||||
|
('1', '2'),
|
||||||
|
('1.0', '1.1'),
|
||||||
|
('1.2.3', '1.2.4'),
|
||||||
|
('1.2.3-45', '1.2.3-46'),
|
||||||
|
('1.2.3-4blah', '1.2.3-5'),
|
||||||
|
('1.2.3.4', '1.2.3.5'),
|
||||||
|
('1.2.3rc1', '1.2.4'),
|
||||||
|
('VERSION', '1.2.4'),
|
||||||
|
]:
|
||||||
|
v = Version('foo', spec, app.get_version)
|
||||||
|
assert str(v.next) == expected, spec
|
||||||
|
|
||||||
|
# A last part without leading digits cannot be stepped
|
||||||
|
for spec in ['1.0-rc1', '1.2.alpha']:
|
||||||
|
v = Version('foo', spec)
|
||||||
|
try:
|
||||||
|
v.next
|
||||||
|
assert False, f'Should have raised for {spec!r}'
|
||||||
|
except Version.Error:
|
||||||
|
pass
|
||||||
|
|
||||||
# constructor
|
# constructor
|
||||||
d = Dependency('foo-devel = 1.0')
|
d = Dependency('foo-devel = 1.0')
|
||||||
assert d.base_name == 'foo'
|
assert d.base_name == 'foo'
|
||||||
|
|
@ -213,7 +240,7 @@ assert deps[2].constraint_str() == 'baz'
|
||||||
|
|
||||||
# parse_deps_spec passes the lookup to the created packages
|
# parse_deps_spec passes the lookup to the created packages
|
||||||
deps = Dependency.parse_deps_spec('foo = VERSION, bar', app.get_version)
|
deps = Dependency.parse_deps_spec('foo = VERSION, bar', app.get_version)
|
||||||
assert deps[0].constraint_str() == 'foo = 1.2.3-45'
|
assert deps[0].constraint_str() == 'foo = 1.2.3'
|
||||||
assert deps[1].constraint_str() == 'bar'
|
assert deps[1].constraint_str() == 'bar'
|
||||||
|
|
||||||
# Malformed specs are rejected when parsed
|
# Malformed specs are rejected when parsed
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue