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' # 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')