diff --git a/src/python/jw/pkg/lib/version/Dependency.py b/src/python/jw/pkg/lib/version/Dependency.py index f09b4e30..01c710c8 100644 --- a/src/python/jw/pkg/lib/version/Dependency.py +++ b/src/python/jw/pkg/lib/version/Dependency.py @@ -70,18 +70,17 @@ class Dependency: # export specified = self.__parsed_spec[1] if specified is None: return [] - if not expanded or specified.version.is_full: + if not expanded or not specified.version.is_full: return (specified, ) ret: list[Boundary] = [] match specified.op: + case '>' | '>=': + ret.append(specified) + ret.append(Boundary('<', specified.version.next_binary_incompatible)) case '=': - try: - next_version = specified.version.next - except Version.Error: - return (specified, ) ret.append(Boundary('>=', specified.version)) - ret.append(Boundary('<', next_version)) - case '>' | '>=' | '<' | '<=': + ret.append(Boundary('<', specified.version.next_binary_incompatible)) + case '<' | '<=': ret.append(specified) case _: self.__raise( @@ -133,7 +132,7 @@ class Dependency: # export def version_boundaries(self, expanded: bool = False) -> Sequence[Boundary]: """The parsed version boundary, or the range it spans when - expanded is True and it does not pin a full version. + expanded is True and it pins a full version. """ return self.__version_boundaries(expanded) @@ -148,12 +147,11 @@ class Dependency: # export ) -> str: """Render the dependency as a version constraint string. - NAMES_ONLY renders the name alone. With untemplated = False the - VERSION, VERSION-REVISION and REVISION macros are kept as - written instead of resolved. include_revision = False drops the - revision of VERSION specs. as_range expands a = boundary that - does not pin a full version into the range it spans; other - operators pass through unchanged. no_subpackages + NAMES_ONLY renders the name alone. untemplated keeps the + VERSION, VERSION-REVISION and REVISION macros as written instead + of the resolved versions. include_revision = False drops the + revision of VERSION specs. as_range expands a boundary that + pins a full version into the range it spans. no_subpackages renders the base name, quote wraps the result in the given string. """ @@ -163,12 +161,12 @@ class Dependency: # export if syntax is Syntax.NAMES_ONLY: return name 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) version = boundary.version.id(untemplated, include_revision) ret.append(f'{name} {op} {version}') if ret: - return ', '.join(ret) + return ' '.join(ret) return name return __str() if quote is None else f'{quote}{__str()}{quote}' diff --git a/src/python/jw/pkg/lib/version/Version.py b/src/python/jw/pkg/lib/version/Version.py index aee96cc3..ee57b444 100644 --- a/src/python/jw/pkg/lib/version/Version.py +++ b/src/python/jw/pkg/lib/version/Version.py @@ -38,17 +38,11 @@ class Version: # export raise Version.Error( f'Tried to look up version of {self.base_name} without lookup function' ) - resolved = self.__lookup_version(self.base_name) - if self.__spec == 'VERSION-REVISION': - return resolved - parts = resolved.split('-') - match self.__spec: - case 'VERSION': - return parts[0] - case 'REVISION': - return parts[1] if len(parts) == 2 else '' - case _: - raise Version.Error(f'Invalid version specifier "{self.__spec}"') + version = self.__lookup_version(self.base_name) + if self.__spec == 'REVISION': + parts = version.split('-', 1) + return parts[1] if len(parts) == 2 else '' + return version def __id(self, untemplate: bool, throw: bool = True) -> str: if not untemplate: @@ -141,8 +135,9 @@ class Version: # export def is_full(self) -> bool: """True if the spec pins a complete version: the VERSION-REVISION macro, or a literal of the form - MAJOR.MINOR.MICRO-REVISION. Range expansion leaves full - versions exact and expands the non-full specs. + MAJOR.MINOR.MICRO-REVISION. Only full versions have the + exclusive upper bound (next_binary_incompatible) that range + expansion relies on. """ if self.__spec == 'VERSION-REVISION': return True @@ -150,8 +145,8 @@ class Version: # export def id(self, untemplate: bool, include_revision: bool = True) -> str: """Return the version id: the raw spec if untemplate is False, - else the resolved version. A VERSION spec yields the core - version. + else the resolved version. With include_revision = False a + VERSION spec yields the core version. """ if not include_revision and self.__spec == 'VERSION': return self.core(untemplate) @@ -220,28 +215,3 @@ class Version: # export f'{self.major}.{self.minor + 1}.0', 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, - ) diff --git a/test/unit/python/jw/pkg/lib/version/test.py b/test/unit/python/jw/pkg/lib/version/test.py index 72a62ba2..337219a6 100644 --- a/test/unit/python/jw/pkg/lib/version/test.py +++ b/test/unit/python/jw/pkg/lib/version/test.py @@ -62,7 +62,7 @@ except Version.Error: # The lookup resolves the macros d = Dependency('foo = VERSION', app.get_version) -assert d.constraint_str() == 'foo = 1.2.3' +assert d.constraint_str() == 'foo = 1.2.3-45' d = Dependency('foo = VERSION', app.get_version) assert d.constraint_str(include_revision = False) == 'foo = 1.2.3' # 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 d = Dependency('foo-devel = VERSION', app.get_version) -assert d.constraint_str() == 'foo-devel = 1.2.3' +assert d.constraint_str() == 'foo-devel = 1.2.3-45' # Unknown projects fail when untemplating try: Dependency('baz = VERSION', app.get_version).constraint_str() @@ -105,38 +105,34 @@ def mapper(project: str) -> str: return '1.2.3-45' d = Dependency('foo = VERSION', mapper) -assert d.constraint_str() == 'foo = 1.2.3' -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-45' assert calls == ['foo'] -# as_range expands a non-full = boundary into the range it spans +# as_range expands full boundaries into a revision range d = Dependency('foo = VERSION-REVISION', 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-45 foo < 1.2.4' d = Dependency('foo >= VERSION-REVISION', 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-45 foo < 1.2.4' d = Dependency('foo > 1.0.0-259', app.get_version) -assert d.constraint_str(as_range = True) == 'foo > 1.0.0-259' -# A raw render keeps the macro as written +assert d.constraint_str(as_range = True) == 'foo > 1.0.0-259 foo < 1.0.1' +# A raw render keeps the macro, with the computed bound alongside it d = Dependency('foo = VERSION-REVISION', app.get_version) constraint = d.constraint_str(untemplated = False, as_range = True) -assert constraint == 'foo = VERSION-REVISION' -# A full version stays exact; a non-full one expands +assert constraint == 'foo >= VERSION-REVISION foo < 1.2.4' +# VERSION is not full, so a VERSION constraint is not expanded d = Dependency('foo = VERSION', app.get_version) -assert d.constraint_str(as_range = True) == 'foo >= 1.2.3, foo < 1.2.4' -# Operators other than = pass through unchanged +assert d.constraint_str(as_range = True) == 'foo = 1.2.3-45' d = Dependency('foo >= VERSION', 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-45' +# '<' and '<=' are not expanded d = Dependency('foo <= VERSION', app.get_version) -assert d.constraint_str(as_range = True) == 'foo <= 1.2.3' -# The bound steps the last part, whatever it is +assert d.constraint_str(as_range = True) == 'foo <= 1.2.3-45' +# versions without a full major.minor.micro-revision are not expanded d = Dependency('foo = 1.0', app.get_version) -assert d.constraint_str(as_range = True) == 'foo >= 1.0, foo < 1.1' +assert d.constraint_str(as_range = True) == 'foo = 1.0' d = Dependency('foo = 1.2.3', app.get_version) -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 +assert d.constraint_str(as_range = True) == 'foo = 1.2.3' d = Dependency('foo = 1.0-rc1', app.get_version) assert d.constraint_str(as_range = True) == 'foo = 1.0-rc1' # Default syntax is SEM_VER @@ -201,29 +197,6 @@ for spec in ['1.2.3-rc1', '1.2.3']: except Version.Error: 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 d = Dependency('foo-devel = 1.0') assert d.base_name == 'foo' @@ -240,7 +213,7 @@ assert deps[2].constraint_str() == 'baz' # parse_deps_spec passes the lookup to the created packages deps = Dependency.parse_deps_spec('foo = VERSION, bar', app.get_version) -assert deps[0].constraint_str() == 'foo = 1.2.3' +assert deps[0].constraint_str() == 'foo = 1.2.3-45' assert deps[1].constraint_str() == 'bar' # Malformed specs are rejected when parsed