Add a dependent_package parameter to Dependency's constructor, and use it in exceptions the class raises. Signed-off-by: Jan Lindemann <jan@janware.com>
177 lines
5.9 KiB
Python
177 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from functools import cached_property
|
|
from typing import TYPE_CHECKING, override
|
|
|
|
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, Never
|
|
|
|
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('([=><]+)')
|
|
|
|
@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]:
|
|
self.__raise(f'Invalid dependency spec "{self.__spec}"')
|
|
return parts[0], None
|
|
case 3:
|
|
if not parts[0] or not parts[2]:
|
|
self.__raise(f'Invalid dependency spec "{self.__spec}"')
|
|
return parts[0], Boundary(
|
|
op=parts[1],
|
|
version=Version(parts[0], parts[2], self.__lookup_version),
|
|
)
|
|
case _:
|
|
self.__raise(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 _:
|
|
self.__raise(
|
|
(
|
|
'Expanding version boundary '
|
|
f'"{self.full_name} {specified.op} {specified.version}" '
|
|
'is not yet implemented'
|
|
),
|
|
cls = NotImplementedError,
|
|
)
|
|
return ret
|
|
|
|
# -- Public API
|
|
|
|
def __init__(
|
|
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:
|
|
self.__raise(
|
|
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,
|
|
dependent_package: str | None = None,
|
|
) -> Sequence[Dependency]:
|
|
"""Split a comma-separated specification into Dependency objects"""
|
|
return [
|
|
Dependency(
|
|
spec = spec.strip(),
|
|
lookup_version = lookup_version,
|
|
dependent_package = dependent_package,
|
|
) for spec in spec.split(',')
|
|
]
|