lib.version: Add module #91
6 changed files with 624 additions and 0 deletions
lib.version: Add module to parse dependency specs
pkg_relations() parses dependency specifications such as 'foo-devel >=
1.2.3-4' with inline regular expressions and mixes the parsing with version
macro expansion and rendering in three different syntaxes.
This commit adds the lib.version module that owns the parsing of a single
specification: the base and full name and the version boundary with its
operator. Rendering happens in constraint_str() in SEM_VER, DEBIAN or
NAMES_ONLY syntax, with untemplated, include_revision, as_range,
no_subpackages and quote options. The VERSION, VERSION-REVISION and
REVISION macros are resolved through a version lookup callback,
lib.version.Version.parse_deps_spec() splits comma-separated specification
strings into Version objects, and Version breaks a version down into its
major, minor, micro and revision parts.
Unit tests cover parsing, macro resolution, rendering in all syntaxes and
the range expansion.
The module is designed to be integrated into the status quo and
intentionally does not address a couple of further TODOs. Notably
version.Syntax.SEM_VER is intended to replace
pkg_relations.VersionSyntax.SemVer, but neither are really semantic
versioning according to spec. They are close but more targeted toward RPM
and Debian package versioning. And the naming and semantics differ from
SemVer.
Naming of the four components (major and minor version) is identical,
semantic meaning aside, there's no disagreement between real SemVer,
Debian, RPM and jw-pkg. The third and fourth component deviate:
SemVer Debian RPM jw-pkg lib.version
3 PATCH RELEASE MICRO
4 PRERELEASE Revision Release REVISION REVISION
TODOs: RELEASE over the rest of jw-pkg needs to be adjusted by a later
commit. Syntax.SEM_VER should also be renamed, to Syntax.JW_PKG maybe,
because, as said, it's not SemVer and will probably never be - SemVer
doesn't provide the compatibility guarantees that the jw-pkg versioning
scheme offers. To be documented.
Signed-off-by: Jan Lindemann <jan@janware.com>
commit
40a12571e2
145
src/python/jw/pkg/lib/version/Dependency.py
Normal file
145
src/python/jw/pkg/lib/version/Dependency.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from ..log import WARNING, log
|
||||
from .base import Boundary, Lookup, Syntax
|
||||
from .Version import Version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from typing import ClassVar
|
||||
|
||||
class Dependency: # export
|
||||
|
||||
class Error(ValueError):
|
||||
pass
|
||||
|
||||
__SPLIT_RE: ClassVar[re.Pattern[str]] = re.compile('([=><]+)')
|
||||
|
||||
@cached_property
|
||||
def __parsed_spec(self) -> tuple[str, Boundary | None]:
|
||||
parts = [part.strip() for part in self.__SPLIT_RE.split(self.__spec)]
|
||||
match len(parts):
|
||||
case 1:
|
||||
if not parts[0]:
|
||||
raise Dependency.Error(f'Invalid dependency spec "{self.__spec}"')
|
||||
return parts[0], None
|
||||
case 3:
|
||||
if not parts[0] or not parts[2]:
|
||||
raise Dependency.Error(f'Invalid dependency spec "{self.__spec}"')
|
||||
if parts[2] == 'REVISION':
|
||||
log(
|
||||
WARNING,
|
||||
f'Spec "{self.__spec}": a bare REVISION renders as a bare '
|
||||
'number, which RPM reads as a version constraint, not a '
|
||||
'release constraint'
|
||||
)
|
||||
return parts[0], Boundary(
|
||||
op = parts[1],
|
||||
version = Version(parts[0], parts[2], self.__lookup_version),
|
||||
)
|
||||
case _:
|
||||
raise Dependency.Error(f'Invalid dependency spec "{self.__spec}"')
|
||||
|
||||
def __version_boundaries(
|
||||
self,
|
||||
expanded: bool,
|
||||
) -> Sequence[Boundary]:
|
||||
specified = self.__parsed_spec[1]
|
||||
if specified is None:
|
||||
return []
|
||||
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 '=':
|
||||
ret.append(Boundary('>=', specified.version))
|
||||
ret.append(Boundary('<', specified.version.next_binary_incompatible))
|
||||
case '<' | '<=':
|
||||
ret.append(specified)
|
||||
case _:
|
||||
raise NotImplementedError(
|
||||
(
|
||||
'Expanding version boundary '
|
||||
f'"{self.full_name} {specified.op} {specified.version}" '
|
||||
'is not yet implemented'
|
||||
)
|
||||
)
|
||||
return ret
|
||||
|
||||
# -- Public API
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
spec: str,
|
||||
lookup_version: Lookup | None = None,
|
||||
) -> None:
|
||||
self.__spec = spec
|
||||
self.__lookup_version = lookup_version
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return self.__spec
|
||||
|
||||
@cached_property
|
||||
def current_version(self) -> str:
|
||||
if not self.__lookup_version:
|
||||
raise Dependency.Error(
|
||||
f'Tried to look up "{self.__spec}" for package '
|
||||
f'"{self.base_name}" without lookup function'
|
||||
)
|
||||
return self.__lookup_version(self.base_name)
|
||||
|
||||
@cached_property
|
||||
def full_name(self) -> str:
|
||||
return self.__parsed_spec[0]
|
||||
|
||||
@cached_property
|
||||
def base_name(self) -> str:
|
||||
return Version.strip_package_suffix(self.full_name)
|
||||
|
||||
def version_boundaries(self, expanded: bool = False) -> Sequence[Boundary]:
|
||||
return self.__version_boundaries(expanded)
|
||||
|
||||
def constraint_str(
|
||||
self,
|
||||
syntax: Syntax = Syntax.SEM_VER,
|
||||
untemplated: bool = True,
|
||||
include_revision: bool = True,
|
||||
as_range: bool = False,
|
||||
no_subpackages: bool = False,
|
||||
quote: str | None = None,
|
||||
) -> str:
|
||||
|
||||
def __str() -> str:
|
||||
name = self.base_name if no_subpackages else self.full_name
|
||||
if syntax is Syntax.NAMES_ONLY:
|
||||
return name
|
||||
ret: list[str] = []
|
||||
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 name
|
||||
|
||||
return __str() if quote is None else f'{quote}{__str()}{quote}'
|
||||
|
||||
@classmethod
|
||||
def parse_deps_spec(
|
||||
cls,
|
||||
spec: str,
|
||||
lookup_version: Lookup | None = None,
|
||||
) -> Sequence[Dependency]:
|
||||
return [
|
||||
Dependency(spec = spec.strip(), lookup_version = lookup_version)
|
||||
for spec in spec.split(',')
|
||||
]
|
||||
4
src/python/jw/pkg/lib/version/Makefile
Normal file
4
src/python/jw/pkg/lib/version/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
TOPDIR = ../../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-mod.mk
|
||||
177
src/python/jw/pkg/lib/version/Version.py
Normal file
177
src/python/jw/pkg/lib/version/Version.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, ClassVar, override
|
||||
|
||||
from .base import Component, Lookup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from enum import Flag
|
||||
|
||||
class Version: # export
|
||||
|
||||
class Error(ValueError):
|
||||
pass
|
||||
|
||||
__FULL_ID_RE: ClassVar[
|
||||
re.Pattern[str],
|
||||
] = re.compile(r'[0-9]+\.[0-9]+\.[0-9]+-[0-9]+')
|
||||
|
||||
__SUFFIX_RE: ClassVar[re.Pattern[str]] = re.compile('-dev$|-devel$|-run$')
|
||||
|
||||
@cached_property
|
||||
def __resolved_id(self) -> str:
|
||||
if self.__lookup_version is None:
|
||||
raise Version.Error(
|
||||
f'Tried to look up version of {self.base_name} without lookup function'
|
||||
)
|
||||
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:
|
||||
return self.__spec
|
||||
if self.__spec not in ['VERSION', 'REVISION', 'VERSION-REVISION']:
|
||||
return self.__spec
|
||||
try:
|
||||
return self.__resolved_id
|
||||
except Exception:
|
||||
if throw:
|
||||
raise
|
||||
return self.__spec
|
||||
|
||||
def __split_spec(self, untemplate: bool) -> tuple[str, str]:
|
||||
parts = self.__id(untemplate).split('-')
|
||||
if len(parts) == 1:
|
||||
return parts[0], ''
|
||||
return parts[0], parts[1]
|
||||
|
||||
def __split_core(self, untemplate: bool) -> tuple[str, ...]:
|
||||
return tuple(self.__split_spec(untemplate)[0].split('.'))
|
||||
|
||||
def __parts_str(self, parts: Component, untemplate: bool) -> str:
|
||||
# Without the widening, mypy will decry unreachable code below match block
|
||||
match_subject: Flag = parts
|
||||
match match_subject:
|
||||
case Component.ID:
|
||||
return self.__id(untemplate)
|
||||
case Component.CORE:
|
||||
return self.__split_spec(untemplate)[0]
|
||||
case Component.MAJOR:
|
||||
return self.__split_core(untemplate)[0]
|
||||
case Component.MINOR:
|
||||
return self.__split_core(untemplate)[1]
|
||||
case Component.MICRO:
|
||||
return self.__split_core(untemplate)[2]
|
||||
case Component.REVISION:
|
||||
return self.__split_spec(untemplate)[1]
|
||||
case _:
|
||||
pass
|
||||
ret: list[str] = []
|
||||
for part in [
|
||||
Component.MAJOR,
|
||||
Component.MINOR,
|
||||
Component.MICRO,
|
||||
Component.REVISION,
|
||||
]:
|
||||
if part not in parts:
|
||||
break
|
||||
ret.append(self.__parts_str(part, untemplate))
|
||||
parts &= ~part
|
||||
if parts:
|
||||
raise Version.Error(f'Invalid version part combination {parts}')
|
||||
if len(ret) < 4:
|
||||
return '.'.join(ret)
|
||||
return f'{".".join(ret[:3])}-{ret[3]}'
|
||||
|
||||
def __init__(
|
||||
self, name: str, spec: str, lookup_version: Lookup | None = None
|
||||
) -> None:
|
||||
self.__name = name
|
||||
self.__spec = spec
|
||||
self.__lookup_version = lookup_version
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return self.__spec
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return self.__id(untemplate = True, throw = False)
|
||||
|
||||
@classmethod
|
||||
def strip_package_suffix(cls, name: str) -> str:
|
||||
return cls.__SUFFIX_RE.sub('', name)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.__name
|
||||
|
||||
@property
|
||||
def base_name(self) -> str:
|
||||
return self.strip_package_suffix(self.__name)
|
||||
|
||||
def parts_str(self, parts: Component, untemplate: bool) -> str:
|
||||
return self.__parts_str(parts, untemplate)
|
||||
|
||||
@property
|
||||
def is_full(self) -> bool:
|
||||
if self.__spec == 'VERSION-REVISION':
|
||||
return True
|
||||
return bool(self.__FULL_ID_RE.fullmatch(self.__spec))
|
||||
|
||||
def id(self, untemplate: bool, include_revision: bool = True) -> str:
|
||||
if not include_revision and self.__spec == 'VERSION':
|
||||
return self.core(untemplate)
|
||||
return self.__id(untemplate)
|
||||
|
||||
def core(self, untemplate: bool) -> str:
|
||||
return self.__split_spec(untemplate)[0]
|
||||
|
||||
def revision(self, untemplate: bool = True) -> str:
|
||||
return self.__split_spec(untemplate)[1]
|
||||
|
||||
@property
|
||||
def major(self) -> int:
|
||||
return int(self.__parts_str(Component.MAJOR, True))
|
||||
|
||||
@property
|
||||
def minor(self) -> int:
|
||||
return int(self.__parts_str(Component.MINOR, True))
|
||||
|
||||
@property
|
||||
def micro(self) -> int:
|
||||
return int(self.__parts_str(Component.MICRO, True))
|
||||
|
||||
@property
|
||||
def next_binary_compatible(self) -> Version:
|
||||
rev = self.revision(True)
|
||||
m = re.match('[0-9]+', rev)
|
||||
if not m:
|
||||
raise Version.Error(f'Cannot increment non-numeric revision "{rev}"')
|
||||
return Version(
|
||||
self.__name,
|
||||
f'{self.major}.{self.minor}.{self.micro}-{int(m.group()) + 1}',
|
||||
self.__lookup_version,
|
||||
)
|
||||
|
||||
@property
|
||||
def next_binary_incompatible(self) -> Version:
|
||||
return Version(
|
||||
self.__name,
|
||||
f'{self.major}.{self.minor}.{self.micro + 1}',
|
||||
self.__lookup_version,
|
||||
)
|
||||
|
||||
@property
|
||||
def next_source_incompatible(self) -> Version:
|
||||
return Version(
|
||||
self.__name,
|
||||
f'{self.major}.{self.minor + 1}.0',
|
||||
self.__lookup_version,
|
||||
)
|
||||
39
src/python/jw/pkg/lib/version/base.py
Normal file
39
src/python/jw/pkg/lib/version/base.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from enum import Enum, Flag, auto
|
||||
from typing import TYPE_CHECKING, NamedTuple, TypeAlias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Version import Version
|
||||
|
||||
class Syntax(Enum): # export
|
||||
DEBIAN = auto()
|
||||
SEM_VER = auto()
|
||||
NAMES_ONLY = auto()
|
||||
|
||||
class Component(Flag): # export
|
||||
ID = auto()
|
||||
MAJOR = auto()
|
||||
MINOR = auto()
|
||||
MICRO = auto()
|
||||
REVISION = auto()
|
||||
CORE = MAJOR | MINOR | MICRO
|
||||
|
||||
class Boundary(NamedTuple): # export
|
||||
op: str
|
||||
version: Version
|
||||
|
||||
"""Syntax-aware formatted version comparion operator"""
|
||||
def format_op(self, syntax: Syntax) -> str:
|
||||
if syntax is Syntax.DEBIAN:
|
||||
match self.op:
|
||||
case '<':
|
||||
return '<<'
|
||||
case '>':
|
||||
return '>>'
|
||||
case _:
|
||||
return self.op
|
||||
return self.op
|
||||
|
||||
Lookup: TypeAlias = Callable[[str], str] # export
|
||||
7
test/unit/python/jw/pkg/lib/version/Makefile
Normal file
7
test/unit/python/jw/pkg/lib/version/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
TOPDIR = ../../../../../../..
|
||||
|
||||
include $(TOPDIR)/make/proj.mk
|
||||
include $(JWBDIR)/make/py-run.mk
|
||||
|
||||
all:
|
||||
test: run
|
||||
252
test/unit/python/jw/pkg/lib/version/test.py
Normal file
252
test/unit/python/jw/pkg/lib/version/test.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import io
|
||||
|
||||
from jw.pkg.lib.log import add_capture_stream, rm_capture_stream
|
||||
from jw.pkg.lib.version import Component, Dependency, Syntax, Version
|
||||
|
||||
class FakeApp:
|
||||
"""Minimal stand-in for jw.pkg.App providing get_version()"""
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, versions: dict[str, str]) -> None:
|
||||
self.__versions = versions
|
||||
|
||||
def get_version(self, project: str) -> str:
|
||||
if project not in self.__versions:
|
||||
raise self.Error(f"Can't get version of project {project}")
|
||||
return self.__versions[project]
|
||||
|
||||
app = FakeApp({'foo': '1.2.3-45', 'bar': '0.9.1-2'})
|
||||
|
||||
# Name only
|
||||
d = Dependency('foo')
|
||||
assert d.base_name == 'foo'
|
||||
assert d.full_name == 'foo'
|
||||
assert d.version_boundaries() == []
|
||||
assert d.constraint_str() == 'foo'
|
||||
|
||||
# Subpackage suffixes are stripped from base_name
|
||||
d = Dependency('foo-dev')
|
||||
assert d.base_name == 'foo'
|
||||
assert d.full_name == 'foo-dev'
|
||||
d = Dependency('foo-devel')
|
||||
assert d.base_name == 'foo'
|
||||
assert d.full_name == 'foo-devel'
|
||||
d = Dependency('foo-run')
|
||||
assert d.base_name == 'foo'
|
||||
assert d.full_name == 'foo-run'
|
||||
|
||||
# A package literally named 'dev' keeps its name
|
||||
d = Dependency('dev')
|
||||
assert d.base_name == 'dev'
|
||||
assert d.full_name == 'dev'
|
||||
|
||||
# Operators with and without whitespace
|
||||
for op in ['=', '==', '<', '<=', '>', '>=']:
|
||||
d = Dependency(f'foo {op} 1.0')
|
||||
assert d.base_name == 'foo'
|
||||
assert d.full_name == 'foo'
|
||||
b = d.version_boundaries()
|
||||
assert len(b) == 1
|
||||
assert b[0].op == op
|
||||
assert b[0].version.id(False) == '1.0'
|
||||
d = Dependency(f'foo{op}1.0')
|
||||
assert d.constraint_str(untemplated = False) == f'foo {op} 1.0'
|
||||
|
||||
# Without a lookup, literals render as written and macros fail
|
||||
d = Dependency('foo-devel >= 2.0')
|
||||
assert d.constraint_str() == 'foo-devel >= 2.0'
|
||||
try:
|
||||
Dependency('foo = VERSION').constraint_str()
|
||||
assert False, 'Should have raised'
|
||||
except Version.Error:
|
||||
pass
|
||||
|
||||
# The lookup resolves the macros
|
||||
d = Dependency('foo = VERSION', app.get_version)
|
||||
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
|
||||
# literals keep it
|
||||
d = Dependency('foo = VERSION-REVISION', app.get_version)
|
||||
assert d.constraint_str() == 'foo = 1.2.3-45'
|
||||
assert d.constraint_str(include_revision = False) == 'foo = 1.2.3-45'
|
||||
d = Dependency('foo = 1.2.3-45')
|
||||
assert d.constraint_str(include_revision = False) == 'foo = 1.2.3-45'
|
||||
# untemplated, the macro has no revision to strip
|
||||
d = Dependency('foo = VERSION', app.get_version)
|
||||
assert d.constraint_str(untemplated=False, include_revision=False) == \
|
||||
'foo = VERSION'
|
||||
# untemplated=False keeps the specifiers as written
|
||||
d = Dependency('foo = VERSION', app.get_version)
|
||||
assert d.constraint_str(untemplated = False) == 'foo = VERSION'
|
||||
# REVISION resolves to the revision part
|
||||
d = Dependency('foo = REVISION', app.get_version)
|
||||
assert d.constraint_str() == 'foo = 45'
|
||||
assert d.constraint_str(untemplated = False) == 'foo = REVISION'
|
||||
# A bare REVISION warns: RPM reads a bare number as a version
|
||||
buf = io.StringIO()
|
||||
sd = add_capture_stream(buf)
|
||||
d = Dependency('foo = REVISION', app.get_version)
|
||||
assert d.constraint_str() == 'foo = 45'
|
||||
assert 'not a release constraint' in buf.getvalue()
|
||||
rm_capture_stream(sd)
|
||||
|
||||
# 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-45'
|
||||
# Unknown projects fail when untemplating
|
||||
try:
|
||||
Dependency('baz = VERSION', app.get_version).constraint_str()
|
||||
assert False, 'Should have raised'
|
||||
except FakeApp.Error:
|
||||
pass
|
||||
|
||||
# The lookup is cached: the mapper is called once
|
||||
calls: list[str] = []
|
||||
|
||||
def mapper(project: str) -> str:
|
||||
calls.append(project)
|
||||
return '1.2.3-45'
|
||||
|
||||
d = Dependency('foo = VERSION', mapper)
|
||||
assert d.constraint_str() == 'foo = 1.2.3-45'
|
||||
assert d.constraint_str() == 'foo = 1.2.3-45'
|
||||
assert calls == ['foo']
|
||||
|
||||
# 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 foo < 1.2.4'
|
||||
d = Dependency('foo >= VERSION-REVISION', app.get_version)
|
||||
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 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 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-45'
|
||||
d = Dependency('foo >= VERSION', app.get_version)
|
||||
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-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'
|
||||
d = Dependency('foo = 1.2.3', app.get_version)
|
||||
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'
|
||||
# unimplemented operators raise
|
||||
d = Dependency('foo == 1.2.3-45', app.get_version)
|
||||
try:
|
||||
d.constraint_str(as_range = True)
|
||||
assert False, 'Should have raised'
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
# Default syntax is SEM_VER
|
||||
d = Dependency('foo < 2.0')
|
||||
assert d.constraint_str(untemplated = False) == 'foo < 2.0'
|
||||
|
||||
# DEBIAN converts strict inequalities, other operators pass through
|
||||
d = Dependency('foo < 2.0')
|
||||
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo << 2.0'
|
||||
d = Dependency('foo > 1.0')
|
||||
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo >> 1.0'
|
||||
d = Dependency('foo <= 1.0')
|
||||
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo <= 1.0'
|
||||
d = Dependency('foo = 1.0')
|
||||
assert d.constraint_str(Syntax.DEBIAN, untemplated = False) == 'foo = 1.0'
|
||||
|
||||
# NAMES_ONLY drops the version
|
||||
d = Dependency('foo-devel >= 1.0')
|
||||
assert d.constraint_str(Syntax.NAMES_ONLY) == 'foo-devel'
|
||||
|
||||
# no_subpackages strips the suffix from the name
|
||||
d = Dependency('foo-devel >= 1.0')
|
||||
assert d.constraint_str(no_subpackages = True, untemplated = False) == 'foo >= 1.0'
|
||||
|
||||
# quote wraps the result
|
||||
d = Dependency('foo-devel >= 1.0')
|
||||
assert d.constraint_str(quote = '"', untemplated = False) == '"foo-devel >= 1.0"'
|
||||
|
||||
# Version parts
|
||||
v = Version('foo', '1.2.3-45', app.get_version)
|
||||
assert v.major == 1
|
||||
assert v.minor == 2
|
||||
assert v.micro == 3
|
||||
assert v.core(True) == '1.2.3'
|
||||
assert v.revision(True) == '45'
|
||||
assert str(v.next_binary_compatible) == '1.2.3-46'
|
||||
assert str(v.next_binary_incompatible) == '1.2.4'
|
||||
assert str(v.next_source_incompatible) == '1.3.0'
|
||||
assert v.parts_str(Component.ID, True) == '1.2.3-45'
|
||||
assert v.parts_str(Component.CORE, True) == '1.2.3'
|
||||
assert v.parts_str(Component.MAJOR | Component.MINOR, True) == '1.2'
|
||||
|
||||
# next_binary_compatible steps to the next revision, dropping any suffix
|
||||
for spec, expected in [
|
||||
('1.2.3-4', '1.2.3-5'),
|
||||
('1.2.3-4blah', '1.2.3-5'),
|
||||
('1.2.3-4.myvariant', '1.2.3-5'),
|
||||
('1.2.3-4-broken', '1.2.3-5'),
|
||||
('1.0.0-259', '1.0.0-260'),
|
||||
]:
|
||||
v = Version('foo', spec)
|
||||
assert str(v.next_binary_compatible) == expected, spec
|
||||
# A macro resolves before the increment
|
||||
v = Version('foo', 'VERSION-REVISION', app.get_version)
|
||||
assert str(v.next_binary_compatible) == '1.2.3-46'
|
||||
# A revision without a leading digit cannot be incremented
|
||||
for spec in ['1.2.3-rc1', '1.2.3']:
|
||||
v = Version('foo', spec)
|
||||
try:
|
||||
v.next_binary_compatible
|
||||
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'
|
||||
assert d.full_name == 'foo-devel'
|
||||
assert d.constraint_str(untemplated = False) == 'foo-devel = 1.0'
|
||||
|
||||
# parse_deps_spec with a comma-separated string
|
||||
deps = Dependency.parse_deps_spec('foo = 1.0, bar-devel >= 2.0, baz')
|
||||
assert len(deps) == 3
|
||||
assert [d.full_name for d in deps] == ['foo', 'bar-devel', 'baz']
|
||||
assert deps[1].base_name == 'bar'
|
||||
assert deps[1].constraint_str(untemplated = False) == 'bar-devel >= 2.0'
|
||||
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-45'
|
||||
assert deps[1].constraint_str() == 'bar'
|
||||
|
||||
# Malformed specs are rejected when parsed
|
||||
try:
|
||||
Dependency('foo = 1.0 > 2.0').full_name
|
||||
assert False, 'Should have raised'
|
||||
except Dependency.Error:
|
||||
pass
|
||||
# Empty names and versions are rejected
|
||||
for bad in ['', 'foo =', ' = 1.0']:
|
||||
try:
|
||||
Dependency(bad).full_name
|
||||
assert False, f'Should have raised for {bad!r}'
|
||||
except Dependency.Error:
|
||||
pass
|
||||
|
||||
# str
|
||||
assert str(Dependency('foo-devel >= 1.0')) == 'foo-devel >= 1.0'
|
||||
assert str(Dependency('foo')) == 'foo'
|
||||
|
||||
print('All lib.version tests passed')
|
||||
Loading…
Reference in a new issue