The unit test files below test/unit/python/jw/pkg/lib/ define Cmd and App subclasses with unannotated methods: __init__(), add_arguments(), _run() and close() lack signatures, the made class attribute lacks a type, and the _cleanup() and exit_context() helpers are untyped, so a strict mypy run over the test tree fails on them. Likewise, the ExecApp and version tests carry list formatting that yapf rejects. Annotate the test classes following the library conventions: parent is App | Cmd, the parser is ArgumentParser, and args is Namespace, and mark every overridden method with @override. Guard the imports that are only used for annotations behind TYPE_CHECKING, and add the future annotations import so they stay out of the runtime path. Then reformat the test tree with yapf. That folds the opts list of the ExecApp test and re-indents the rejected operator list of the version test. Signed-off-by: Jan Lindemann <jan@janware.com> Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1
257 lines
9.1 KiB
Python
257 lines
9.1 KiB
Python
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'
|
|
# A bare REVISION is rejected on the grounds that is assumed that
|
|
# the user wanted VERSION instead
|
|
try:
|
|
Dependency('foo = REVISION', app.get_version).constraint_str()
|
|
assert False, 'Should have raised'
|
|
except Dependency.Error:
|
|
pass
|
|
|
|
# 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'
|
|
# 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
|
|
|
|
# Operators outside the supported set are rejected when parsed,
|
|
# not folded into the package name or deferred to expansion
|
|
for bad in [
|
|
'foo ~= 1.0',
|
|
'foo~=1.0',
|
|
'foo ~ 1.0',
|
|
'foo != 1.0',
|
|
'foo!=1.0',
|
|
'foo == 1.0',
|
|
'foo==1.0',
|
|
'foo === 1.0',
|
|
'foo << 1.0',
|
|
'foo >> 1.0',
|
|
]:
|
|
try:
|
|
Dependency(bad).full_name
|
|
assert False, f'Should have raised for {bad!r}'
|
|
except Dependency.Error as e:
|
|
assert 'unsupported operator' in str(e)
|
|
|
|
# str
|
|
assert str(Dependency('foo-devel >= 1.0')) == 'foo-devel >= 1.0'
|
|
assert str(Dependency('foo')) == 'foo'
|
|
|
|
print('All lib.version tests passed')
|