145 lines
4.7 KiB
Python
145 lines
4.7 KiB
Python
|
|
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
|
||
|
|
|
||
|
|
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]:
|
||
|
|
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:
|
||
|
|
|
||
|
|
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]:
|
||
|
|
return [
|
||
|
|
Dependency(spec = spec.strip(), lookup_version = lookup_version)
|
||
|
|
for spec in spec.split(',')
|
||
|
|
]
|