diff --git a/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py b/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py index 9a14fbe9..8156ad7e 100644 --- a/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py +++ b/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py @@ -67,6 +67,7 @@ def pkg_relations( for dep in Dependency.parse_deps_spec( deps_spec, lookup_version = app.get_version, + dependent_package = cur_pkg, ): dep_name = dep.base_name if dep_name in ignore or dep.full_name in ignore: diff --git a/src/python/jw/pkg/lib/version/Dependency.py b/src/python/jw/pkg/lib/version/Dependency.py index 2ef02c42..8ed8ee3b 100644 --- a/src/python/jw/pkg/lib/version/Dependency.py +++ b/src/python/jw/pkg/lib/version/Dependency.py @@ -5,13 +5,13 @@ import re from functools import cached_property from typing import TYPE_CHECKING, override -from ..log import WARNING, log +from ..log import get_caller_pos from .base import Boundary, Lookup, Syntax from .Version import Version if TYPE_CHECKING: from collections.abc import Sequence - from typing import ClassVar + from typing import ClassVar, Never class Dependency: # export """A single package dependency: a package name and an optional @@ -24,30 +24,38 @@ class Dependency: # export __SPLIT_RE: ClassVar[re.Pattern[str]] = re.compile('([=><]+)') + @property + def __target_prefix(self) -> str: + if self.__dependent_package is None: + return '' + return f' Package "{self.__dependent_package}"' + + def __raise(self, msg: str, cls: type[Exception] = Error) -> Never: + mod, file, line = get_caller_pos() + raise cls(f'[{file}:{line}]{self.__target_prefix}: {msg}') + @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}"') + self.__raise(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}"') + self.__raise(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' + self.__raise( + f'Spec "{self.__spec}": a bare REVISION renders as a ' + 'bare number, which is likely not what the user intended' ) return parts[0], Boundary( - op = parts[1], - version = Version(parts[0], parts[2], self.__lookup_version), + op=parts[1], + version=Version(parts[0], parts[2], self.__lookup_version), ) case _: - raise Dependency.Error(f'Invalid dependency spec "{self.__spec}"') + self.__raise(f'Invalid dependency spec "{self.__spec}"') def __version_boundaries( self, @@ -69,12 +77,13 @@ class Dependency: # export case '<' | '<=': ret.append(specified) case _: - raise NotImplementedError( + self.__raise( ( 'Expanding version boundary ' f'"{self.full_name} {specified.op} {specified.version}" ' 'is not yet implemented' - ) + ), + cls = NotImplementedError, ) return ret @@ -84,18 +93,24 @@ class Dependency: # export self, spec: str, lookup_version: Lookup | None = None, + dependent_package: str | None = None, ) -> None: self.__spec = spec self.__lookup_version = lookup_version + self.__dependent_package = dependent_package @override def __str__(self) -> str: return self.__spec + @property + def dependent_package(self) -> str | None: + return self.__dependent_package + @cached_property def current_version(self) -> str: if not self.__lookup_version: - raise Dependency.Error( + self.__raise( f'Tried to look up "{self.__spec}" for package ' f'"{self.base_name}" without lookup function' ) @@ -155,9 +170,13 @@ class Dependency: # export cls, spec: str, lookup_version: Lookup | None = None, + dependent_package: str | 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(',') + Dependency( + spec = spec.strip(), + lookup_version = lookup_version, + dependent_package = dependent_package, + ) for spec in spec.split(',') ] diff --git a/test/unit/python/jw/pkg/lib/version/test.py b/test/unit/python/jw/pkg/lib/version/test.py index 84be31d9..91783ae3 100644 --- a/test/unit/python/jw/pkg/lib/version/test.py +++ b/test/unit/python/jw/pkg/lib/version/test.py @@ -1,6 +1,3 @@ -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: @@ -82,17 +79,13 @@ assert d.constraint_str(untemplated=False, include_revision=False) == \ # 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) +# 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)