lib.version: Add module #91
10 changed files with 734 additions and 113 deletions
|
|
@ -11,10 +11,11 @@ from enum import Enum, auto
|
||||||
from functools import cache
|
from functools import cache
|
||||||
from typing import TYPE_CHECKING, override
|
from typing import TYPE_CHECKING, override
|
||||||
|
|
||||||
from .lib.ExecApp import ExecApp as Base
|
|
||||||
from .lib.Distro import Distro
|
from .lib.Distro import Distro
|
||||||
|
from .lib.ExecApp import ExecApp as Base
|
||||||
from .lib.log import DEBUG, ERR, log
|
from .lib.log import DEBUG, ERR, log
|
||||||
from .lib.ProjectConf import ProjectConf
|
from .lib.ProjectConf import ProjectConf
|
||||||
|
from .lib.version.Dependency import Dependency
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import argparse
|
import argparse
|
||||||
|
|
@ -435,7 +436,7 @@ class App(Base):
|
||||||
return self.find_dir(name, ['/tmpl'], ['/opt/' + name + '/share/tmpl'])
|
return self.find_dir(name, ['/tmpl'], ['/opt/' + name + '/share/tmpl'])
|
||||||
|
|
||||||
def strip_module_from_spec(self, mod: str) -> str:
|
def strip_module_from_spec(self, mod: str) -> str:
|
||||||
return re.sub(r'-dev$|-devel$|-run$', '', re.split('([=><]+)', mod)[0].strip())
|
return Dependency(mod).base_name
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_value(self, project: str, section: str, key: str) -> str | None:
|
def get_value(self, project: str, section: str, key: str) -> str | None:
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import re
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, cast, override
|
from typing import TYPE_CHECKING, cast, override
|
||||||
|
|
||||||
|
from ...lib.version.base import Syntax
|
||||||
from .Cmd import Cmd, Parent
|
from .Cmd import Cmd, Parent
|
||||||
from .lib.pkg_relations import VersionSyntax
|
|
||||||
from .lib.pkg_relations import pkg_relations as pkg_relations_list
|
from .lib.pkg_relations import pkg_relations as pkg_relations_list
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -30,7 +30,11 @@ class BaseCmdPkgRelations(Cmd):
|
||||||
no_subpackages = args.no_subpackages,
|
no_subpackages = args.no_subpackages,
|
||||||
dont_strip_revision = args.dont_strip_revision,
|
dont_strip_revision = args.dont_strip_revision,
|
||||||
expand_semver_revision_range = args.expand_semver_revision_range,
|
expand_semver_revision_range = args.expand_semver_revision_range,
|
||||||
syntax = VersionSyntax[args.syntax.replace('-', '_')],
|
syntax = {
|
||||||
|
'semver': Syntax.SEM_VER,
|
||||||
|
'debian': Syntax.DEBIAN,
|
||||||
|
'names-only': Syntax.NAMES_ONLY,
|
||||||
|
}[args.syntax],
|
||||||
recursive = args.recursive,
|
recursive = args.recursive,
|
||||||
dont_expand_version_macros = args.dont_expand_version_macros,
|
dont_expand_version_macros = args.dont_expand_version_macros,
|
||||||
ignore = set(re.split(self.arg_sep, args.ignore)),
|
ignore = set(re.split(self.arg_sep, args.ignore)),
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@ from enum import Enum, auto
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from ...lib.log import WARNING, log
|
from ...lib.log import WARNING, log
|
||||||
|
from ...lib.version.base import Syntax
|
||||||
from .Cmd import Cmd, Parent
|
from .Cmd import Cmd, Parent
|
||||||
from .lib.pkg_relations import VersionSyntax, pkg_relations
|
from .lib.pkg_relations import pkg_relations
|
||||||
from .lib.templates import ListDict, RenderValues, tmpl_render
|
from .lib.templates import ListDict, RenderValues, tmpl_render
|
||||||
|
|
||||||
def key_value(s: str) -> tuple[str, str]:
|
def key_value(s: str) -> tuple[str, str]:
|
||||||
|
|
@ -36,7 +37,7 @@ class CmdCreateFile(Cmd): # export
|
||||||
flavours = ['run'],
|
flavours = ['run'],
|
||||||
subsections = ['jw'],
|
subsections = ['jw'],
|
||||||
seed_pkgs = [module],
|
seed_pkgs = [module],
|
||||||
syntax = VersionSyntax.names_only,
|
syntax = Syntax.NAMES_ONLY,
|
||||||
no_subpackages = True,
|
no_subpackages = True,
|
||||||
recursive = True,
|
recursive = True,
|
||||||
quote = False,
|
quote = False,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from enum import Enum, auto
|
|
||||||
|
|
||||||
from ....App import App, Scope
|
from ....App import App, Scope
|
||||||
from ....lib.log import DEBUG, log
|
from ....lib.log import DEBUG, log
|
||||||
|
from ....lib.version.base import Syntax
|
||||||
class VersionSyntax(Enum):
|
from ....lib.version.Dependency import Dependency
|
||||||
semver = auto()
|
|
||||||
debian = auto()
|
|
||||||
names_only = auto()
|
|
||||||
|
|
||||||
def pkg_relations(
|
def pkg_relations(
|
||||||
app: App,
|
app: App,
|
||||||
|
|
@ -20,7 +13,7 @@ def pkg_relations(
|
||||||
no_subpackages: bool = False,
|
no_subpackages: bool = False,
|
||||||
dont_strip_revision: bool = False,
|
dont_strip_revision: bool = False,
|
||||||
expand_semver_revision_range: bool = False,
|
expand_semver_revision_range: bool = False,
|
||||||
syntax: VersionSyntax = VersionSyntax.semver,
|
syntax: Syntax = Syntax.SEM_VER,
|
||||||
recursive: bool = False,
|
recursive: bool = False,
|
||||||
dont_expand_version_macros: bool = False,
|
dont_expand_version_macros: bool = False,
|
||||||
ignore: set[str] = set(),
|
ignore: set[str] = set(),
|
||||||
|
|
@ -34,8 +27,7 @@ def pkg_relations(
|
||||||
subsections = app.distro.os_cascade
|
subsections = app.distro.os_cascade
|
||||||
subsections.append('jw')
|
subsections.append('jw')
|
||||||
|
|
||||||
expand_semver_revision_range = expand_semver_revision_range
|
if syntax == Syntax.DEBIAN:
|
||||||
if syntax == VersionSyntax.debian:
|
|
||||||
expand_semver_revision_range = True
|
expand_semver_revision_range = True
|
||||||
|
|
||||||
if skip_excluded:
|
if skip_excluded:
|
||||||
|
|
@ -58,113 +50,48 @@ def pkg_relations(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
version_pattern = re.compile('[0-9-.]*')
|
|
||||||
ret: list[str] = []
|
ret: list[str] = []
|
||||||
for flavour in flavours: # build / release / run / devel
|
for flavour in flavours: # build / release / run / devel
|
||||||
cur_pkgs = seed_pkgs.copy()
|
cur_pkgs = seed_pkgs.copy()
|
||||||
visited = set()
|
visited_pkgs: set[str] = set()
|
||||||
while len(cur_pkgs):
|
while len(cur_pkgs):
|
||||||
cur_pkg = cur_pkgs.pop(0)
|
cur_pkg = cur_pkgs.pop(0)
|
||||||
if cur_pkg in visited or cur_pkg in ignore:
|
if cur_pkg in visited_pkgs or cur_pkg in ignore:
|
||||||
continue
|
continue
|
||||||
for subsec in subsections:
|
for subsec in subsections:
|
||||||
version: str | None = None
|
|
||||||
section = 'pkg.' + rel_type + '.' + subsec
|
section = 'pkg.' + rel_type + '.' + subsec
|
||||||
visited.add(cur_pkg)
|
visited_pkgs.add(cur_pkg)
|
||||||
value = app.get_value(cur_pkg, section, flavour)
|
deps_spec = app.get_value(cur_pkg, section, flavour)
|
||||||
if not value:
|
if not deps_spec:
|
||||||
continue
|
continue
|
||||||
deps = value.split(',')
|
for dep in Dependency.parse_deps_spec(
|
||||||
for spec in deps:
|
deps_spec,
|
||||||
dep = re.split('([=><]+)', spec)
|
lookup_version = app.get_version,
|
||||||
if syntax == VersionSyntax.names_only:
|
):
|
||||||
dep = dep[:1]
|
dep_name = dep.base_name
|
||||||
dep = list(map(str.strip, dep))
|
if dep_name in ignore or dep.full_name in ignore:
|
||||||
dep_name = re.sub('-dev$|-devel$|-run$', '', dep[0])
|
|
||||||
if dep_name in ignore or dep[0] in ignore:
|
|
||||||
continue
|
continue
|
||||||
if no_subpackages:
|
|
||||||
dep[0] = dep_name
|
|
||||||
for i, item in enumerate(dep):
|
|
||||||
dep[i] = item.strip()
|
|
||||||
if subsec == 'jw':
|
if subsec == 'jw':
|
||||||
if (recursive and dep_name not in visited
|
if (recursive and dep_name not in visited_pkgs
|
||||||
and dep_name not in cur_pkgs):
|
and dep_name not in cur_pkgs):
|
||||||
cur_pkgs.append(dep_name)
|
cur_pkgs.append(dep_name)
|
||||||
if hide_jw_pkg:
|
if hide_jw_pkg and dep_name == 'jw-pkg':
|
||||||
continue
|
continue
|
||||||
if len(dep) == 3:
|
expand_version_macros = subsec == 'jw'
|
||||||
if dont_expand_version_macros and dep_name in cur_pkgs:
|
if dont_expand_version_macros and dep_name in cur_pkgs:
|
||||||
version = dep[2]
|
expand_version_macros = False
|
||||||
else:
|
if hide_self and dep_name in seed_pkgs:
|
||||||
version = app.get_version(dep_name)
|
continue
|
||||||
if dep[2] == 'VERSION':
|
dep_str = dep.constraint_str(
|
||||||
if dont_strip_revision:
|
untemplated = expand_version_macros,
|
||||||
dep[2] = version
|
include_revision = dont_strip_revision,
|
||||||
else:
|
as_range = expand_semver_revision_range,
|
||||||
dep[2] = version.split('-')[0]
|
no_subpackages = no_subpackages,
|
||||||
elif dep[2] == 'VERSION-REVISION':
|
syntax = syntax,
|
||||||
dep[2] = version
|
quote = '"' if quote else None,
|
||||||
elif version_pattern.match(dep[2]):
|
)
|
||||||
# dep[2] = dep[2]
|
if dep_str in ret:
|
||||||
pass
|
continue
|
||||||
else:
|
log(DEBUG, f'Appending dependency >{dep_str}<')
|
||||||
raise Exception('Unknown version specifier in ' + spec)
|
ret.append(dep_str)
|
||||||
if len(dep) != 3 or not expand_semver_revision_range:
|
|
||||||
expanded_deps = [dep]
|
|
||||||
else:
|
|
||||||
assert version is not None
|
|
||||||
expanded_deps = []
|
|
||||||
semver = re.split(r'[.-]', version)
|
|
||||||
if len(semver) != 4:
|
|
||||||
expanded_deps = [dep]
|
|
||||||
else:
|
|
||||||
release = int(semver[2])
|
|
||||||
major_minor = f'{semver[0]}.{semver[1]}'
|
|
||||||
match dep[1]:
|
|
||||||
case '>' | '>=':
|
|
||||||
expanded_deps.append([dep[0], dep[1], dep[2]])
|
|
||||||
expanded_deps.append(
|
|
||||||
[dep[0], '<', f'{major_minor}.{release + 1}']
|
|
||||||
)
|
|
||||||
case '<' | '<=':
|
|
||||||
expanded_deps.append([dep[0], dep[1], dep[2]])
|
|
||||||
case '=':
|
|
||||||
expanded_deps.append(
|
|
||||||
[dep[0], '>=', f'{major_minor}.{release}']
|
|
||||||
)
|
|
||||||
expanded_deps.append(
|
|
||||||
[dep[0], '<', f'{major_minor}.{release + 1}']
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
raise NotImplementedError(
|
|
||||||
(
|
|
||||||
'Expanding SemVer range '
|
|
||||||
f'"{dep[0]} {dep[1]} {dep[3]}" '
|
|
||||||
'is not yet implemented'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for expanded_dep in expanded_deps:
|
|
||||||
if hide_self and dep_name in seed_pkgs:
|
|
||||||
continue
|
|
||||||
match syntax:
|
|
||||||
case VersionSyntax.semver:
|
|
||||||
pass
|
|
||||||
case VersionSyntax.names_only:
|
|
||||||
pass
|
|
||||||
case VersionSyntax.debian:
|
|
||||||
if len(expanded_dep) == 3:
|
|
||||||
match expanded_dep[1]:
|
|
||||||
case '<':
|
|
||||||
expanded_dep[1] = '<<'
|
|
||||||
case '>':
|
|
||||||
expanded_dep[1] = '>>'
|
|
||||||
case _:
|
|
||||||
pass
|
|
||||||
dep_str = ' '.join(expanded_dep)
|
|
||||||
if quote:
|
|
||||||
dep_str = '"' + dep_str + '"'
|
|
||||||
if dep_str not in ret:
|
|
||||||
log(DEBUG, f'Appending dependency >{dep_str}<')
|
|
||||||
ret.append(dep_str)
|
|
||||||
return ret
|
return ret
|
||||||
|
|
|
||||||
163
src/python/jw/pkg/lib/version/Dependency.py
Normal file
163
src/python/jw/pkg/lib/version/Dependency.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
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
|
||||||
|
"""A single package dependency: a package name and an optional
|
||||||
|
version boundary, parsed from a specification such as
|
||||||
|
'foo-devel >= 1.2.3-4'. Rendering happens in constraint_str().
|
||||||
|
"""
|
||||||
|
|
||||||
|
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]:
|
||||||
|
"""The parsed version boundary, or the range it spans when
|
||||||
|
expanded is True and it pins a full version.
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""Render the dependency as a version constraint string.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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]:
|
||||||
|
"""Split a comma-separated specification into Dependency objects"""
|
||||||
|
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
|
||||||
217
src/python/jw/pkg/lib/version/Version.py
Normal file
217
src/python/jw/pkg/lib/version/Version.py
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
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
|
||||||
|
"""A version spec: a literal such as '1.2.3-4', or one of the macros
|
||||||
|
VERSION, VERSION-REVISION and REVISION, which resolve against the
|
||||||
|
version of the project the name refers to, through the lookup
|
||||||
|
callback.
|
||||||
|
|
||||||
|
Versions step through compatibility tiers: a different revision is
|
||||||
|
a binary-compatible change, a different micro is source-compatible,
|
||||||
|
a different minor carries no major incompatibilities but downstream
|
||||||
|
packages should expect trivial fixes, and a different major gives
|
||||||
|
no compatibility guarantees at all.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""Remove the -dev, -devel or -run subpackage suffix"""
|
||||||
|
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:
|
||||||
|
"""True if the spec pins a complete version: the
|
||||||
|
VERSION-REVISION macro, or a literal of the form
|
||||||
|
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
|
||||||
|
return bool(self.__FULL_ID_RE.fullmatch(self.__spec))
|
||||||
|
|
||||||
|
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. With include_revision = False a
|
||||||
|
VERSION spec yields the core version.
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""The next version within the binary tier: the same core with
|
||||||
|
the revision incremented. A different revision is a
|
||||||
|
binary-compatible change, so binaries built against the current
|
||||||
|
version keep working. Only the revision's leading digits count:
|
||||||
|
a suffixed revision such as '4blah' steps to '5', which sits
|
||||||
|
strictly above every variant of the current revision. A
|
||||||
|
revision without leading digits raises Version.Error.
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""The next version out of the binary tier: the micro
|
||||||
|
incremented, the revision dropped. A different micro is
|
||||||
|
source-compatible, so this is the first version binaries of the
|
||||||
|
current version are not guaranteed to run against.
|
||||||
|
"""
|
||||||
|
return Version(
|
||||||
|
self.__name,
|
||||||
|
f'{self.major}.{self.minor}.{self.micro + 1}',
|
||||||
|
self.__lookup_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next_source_incompatible(self) -> Version:
|
||||||
|
"""The next version out of the source tier: the minor
|
||||||
|
incremented, micro and revision reset. A different minor carries
|
||||||
|
no major incompatibilities, but downstream packages should
|
||||||
|
expect to need trivial fixes.
|
||||||
|
"""
|
||||||
|
return Version(
|
||||||
|
self.__name,
|
||||||
|
f'{self.major}.{self.minor + 1}.0',
|
||||||
|
self.__lookup_version,
|
||||||
|
)
|
||||||
45
src/python/jw/pkg/lib/version/base.py
Normal file
45
src/python/jw/pkg/lib/version/base.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
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
|
||||||
|
"""Syntaxes for rendered version constraints"""
|
||||||
|
|
||||||
|
DEBIAN = auto()
|
||||||
|
SEM_VER = auto()
|
||||||
|
NAMES_ONLY = auto()
|
||||||
|
|
||||||
|
class Component(Flag): # export
|
||||||
|
"""Version components that parts_str() can extract"""
|
||||||
|
|
||||||
|
ID = auto()
|
||||||
|
MAJOR = auto()
|
||||||
|
MINOR = auto()
|
||||||
|
MICRO = auto()
|
||||||
|
REVISION = auto()
|
||||||
|
CORE = MAJOR | MINOR | MICRO
|
||||||
|
|
||||||
|
class Boundary(NamedTuple): # export
|
||||||
|
"""A comparison operator and the Version it bounds"""
|
||||||
|
|
||||||
|
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