Source code for pkgcore.ebuild.cpv

"""gentoo ebuild specific base package class"""

from collections import UserString

from snakeoil.compatibility import cmp
from snakeoil.demandload import demand_compile_regexp

from ..package import base
from . import atom
from .errors import InvalidCPV

demand_compile_regexp("suffix_regexp", "^(alpha|beta|rc|pre|p)(\\d*)$")

suffix_value = {"pre": -2, "p": 1, "alpha": -4, "beta": -3, "rc": -1}

# while the package section looks fugly, there is a reason for it-
# to prevent version chunks from showing up in the package

demand_compile_regexp(
    "isvalid_version_re",
    r"^(?:\d+)(?:\.\d+)*[a-zA-Z]?(?:_(p(?:re)?|beta|alpha|rc)\d*)*$",
)

demand_compile_regexp(
    "isvalid_cat_re", r"^(?:[a-zA-Z0-9][-a-zA-Z0-9+._]*(?:/(?!$))?)+$"
)

# empty string is fine, means a -- was encounter.
demand_compile_regexp("_pkg_re", r"^[a-zA-Z0-9+_]+$")


[docs] def isvalid_pkg_name(chunks): if not chunks[0] or chunks[0][0] == "+": # this means a leading -; additionally, '+asdf' is disallowed return False # all remaining chunks can either be empty (meaning multiple # hyphens) or must be valid chars if not all(not s or _pkg_re.match(s) for s in chunks): return False # the package name must not end with a hyphen followed by anything that # looks like a version or revision -- need to ensure that we've gotten more than one # chunk, i.e. at least one hyphen if len(chunks) == 1: return True if isvalid_version_re.match(chunks[-1]): return False if len(chunks) >= 3 and isvalid_rev(chunks[-1]): # if the last chunk is a revision, the proceeding *must not* be version like. return not isvalid_version_re.match(chunks[-2]) return True
[docs] def isvalid_rev(s: str): return s and s[0] == "r" and s[1:].isdigit()
[docs] class Revision(UserString): """Internal revision class storing revisions as strings and comparing as integers.""" # parent __hash__() isn't inherited when __eq__() is defined in the child class # https://docs.python.org/3/reference/datamodel.html#object.__hash__ __hash__ = UserString.__hash__ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.data: try: self._revint = int(self.data) except ValueError: raise InvalidCPV(self.data, "invalid revision") else: self._revint = 0 def __str__(self): if not self.data: return "0" else: return self.data def __eq__(self, other): if isinstance(other, Revision): return self._revint == other._revint elif isinstance(other, int): return self._revint == other elif other is None: return self._revint == 0 return self.data == other def __lt__(self, other): if isinstance(other, Revision): return self._revint < other._revint elif isinstance(other, int): return self._revint < other elif other is None: return self._revint < 0 return self.data < other def __le__(self, other): if isinstance(other, Revision): return self._revint <= other._revint elif isinstance(other, int): return self._revint <= other elif other is None: return self._revint <= 0 return self.data <= other def __gt__(self, other): if isinstance(other, Revision): return self._revint > other._revint elif isinstance(other, int): return self._revint > other elif other is None: return self._revint > 0 return self.data > other def __ge__(self, other): if isinstance(other, Revision): return self._revint >= other._revint elif isinstance(other, int): return self._revint >= other elif other is None: return self._revint >= 0 return self.data >= other
[docs] def ver_cmp(ver1: str, rev1: str, ver2: str, rev2: str) -> int: # If the versions are the same, comparing revisions will suffice. if ver1 == ver2: # revisions are equal if 0 or None (versionless cpv) if not rev1 and not rev2: return 0 return cmp(rev1, rev2) # Split up the versions into dotted strings and lists of suffixes. parts1 = ver1.split("_") parts2 = ver2.split("_") # If the dotted strings are equal, we can skip doing a detailed comparison. if parts1[0] != parts2[0]: # First split up the dotted strings into their components. ver_parts1 = parts1[0].split(".") ver_parts2 = parts2[0].split(".") # Pull out any letter suffix on the final components and keep # them for later. letters = [] for ver_parts in (ver_parts1, ver_parts2): if ver_parts[-1][-1].isalpha(): letters.append(ord(ver_parts[-1][-1])) ver_parts[-1] = ver_parts[-1][:-1] else: # Using -1 simplifies comparisons later letters.append(-1) # OPT: Pull length calculation out of the loop ver_parts1_len = len(ver_parts1) ver_parts2_len = len(ver_parts2) # Iterate through the components for v1, v2 in zip(ver_parts1, ver_parts2): # If the string components are equal, the numerical # components will be equal too. if v1 == v2: continue # If one of the components begins with a "0" then they # are compared as floats so that 1.1 > 1.02; else ints. if v1[0] != "0" and v2[0] != "0": v1 = int(v1) v2 = int(v2) else: # handle the 0.060 == 0.060 case. v1 = v1.rstrip("0") v2 = v2.rstrip("0") # If they are not equal, the higher value wins. c = cmp(v1, v2) if c: return c if ver_parts1_len > ver_parts2_len: return 1 elif ver_parts2_len > ver_parts1_len: return -1 # The dotted components were equal. Let's compare any single # letter suffixes. if letters[0] != letters[1]: return cmp(letters[0], letters[1]) # The dotted components were equal, so remove them from our lists # leaving only suffixes. del parts1[0] del parts2[0] # OPT: Pull length calculation out of the loop parts1_len = len(parts1) parts2_len = len(parts2) # Iterate through the suffixes for x in range(max(parts1_len, parts2_len)): # If we're at the end of one of our lists, we need to use # the next suffix from the other list to decide who wins. if x == parts1_len: match = suffix_regexp.match(parts2[x]) val = suffix_value[match.group(1)] if val: return cmp(0, val) return cmp(0, int("0" + match.group(2))) if x == parts2_len: match = suffix_regexp.match(parts1[x]) val = suffix_value[match.group(1)] if val: return cmp(val, 0) return cmp(int("0" + match.group(2)), 0) # If the string values are equal, no need to parse them. # Continue on to the next. if parts1[x] == parts2[x]: continue # Match against our regular expression to make a split between # "beta" and "1" in "beta1" match1 = suffix_regexp.match(parts1[x]) match2 = suffix_regexp.match(parts2[x]) # If our int'ified suffix names are different, use that as the basis # for comparison. c = cmp(suffix_value[match1.group(1)], suffix_value[match2.group(1)]) if c: return c # Otherwise use the digit as the basis for comparison. c = cmp(int("0" + match1.group(2)), int("0" + match2.group(2))) if c: return c # Our versions had different strings but ended up being equal. # The revision holds the final difference. return cmp(rev1, rev2)
[docs] class CPV(base.base): """base ebuild package class :ivar category: str category :ivar package: str package :ivar key: strkey (cat/pkg) :ivar version: str version :ivar revision: str revision :ivar versioned_atom: atom matching this exact version :ivar unversioned_atom: atom matching all versions of this package """ __slots__ = ( "cpvstr", "key", "category", "package", "version", "revision", "fullver", ) def __init__(self, *args, versioned=None): """ Can be called with one string or with three string args. If called with one arg that is the cpv string. (See :obj:`parser` for allowed syntax). If called with three args they are the category, package and version components of the cpv string respectively. """ for x in args: if not isinstance(x, str): raise TypeError(f"all args must be strings, got {args!r}") l = len(args) if l == 1: cpvstr = args[0] if versioned is None: raise TypeError( f"single argument invocation requires versioned kwarg; {cpvstr!r}" ) elif l == 2: cpvstr = f"{args[0]}/{args[1]}" versioned = False elif l == 3: cpvstr = f"{args[0]}/{args[1]}-{args[2]}" versioned = True else: raise TypeError( f"CPV takes 1 arg (cpvstr), 2 (cat, pkg), or 3 (cat, pkg, ver): got {args!r}" ) try: category, pkgver = cpvstr.rsplit("/", 1) except ValueError: # occurs if the rsplit yields only one item raise InvalidCPV(cpvstr, "no package or version components") if not isvalid_cat_re.match(category): raise InvalidCPV(cpvstr, "invalid category name") sf = object.__setattr__ sf(self, "category", category) sf(self, "cpvstr", cpvstr) pkg_chunks = pkgver.split("-") lpkg_chunks = len(pkg_chunks) if versioned: if lpkg_chunks == 1: raise InvalidCPV(cpvstr, "missing package version") if isvalid_rev(pkg_chunks[-1]): if lpkg_chunks < 3: # needs at least ('pkg', 'ver', 'rev') raise InvalidCPV( cpvstr, "missing package name, version, and/or revision" ) rev = Revision(pkg_chunks.pop(-1)[1:]) if rev == 0: # reset stored cpvstr to drop -r0+ sf(self, "cpvstr", f"{category}/{'-'.join(pkg_chunks)}") elif rev[0] == "0": # reset stored cpvstr to drop leading zeroes from revision sf(self, "cpvstr", f"{category}/{'-'.join(pkg_chunks)}-r{int(rev)}") sf(self, "revision", rev) else: sf(self, "revision", Revision("")) if not isvalid_version_re.match(pkg_chunks[-1]): raise InvalidCPV(cpvstr, f"invalid version '{pkg_chunks[-1]}'") sf(self, "version", pkg_chunks.pop(-1)) if self.revision: sf(self, "fullver", f"{self.version}-r{self.revision}") else: sf(self, "fullver", self.version) if not isvalid_pkg_name(pkg_chunks): raise InvalidCPV(cpvstr, "invalid package name") sf(self, "package", "-".join(pkg_chunks)) sf(self, "key", f"{category}/{self.package}") else: if not isvalid_pkg_name(pkg_chunks): raise InvalidCPV(cpvstr, "invalid package name") sf(self, "revision", None) sf(self, "fullver", None) sf(self, "version", None) sf(self, "key", cpvstr) sf(self, "package", "-".join(pkg_chunks)) def __hash__(self): return hash(self.cpvstr) def __repr__(self): return "<%s cpvstr=%s @%#8x>" % ( self.__class__.__name__, getattr(self, "cpvstr", None), id(self), ) def __str__(self): return getattr(self, "cpvstr", "None") def __eq__(self, other): try: if self.cpvstr == other.cpvstr: return True if self.category == other.category and self.package == other.package: return ( ver_cmp(self.version, self.revision, other.version, other.revision) == 0 ) except AttributeError: pass return False def __ne__(self, other): return not self.__eq__(other) def __lt__(self, other): try: if self.category == other.category: if self.package == other.package: return ( ver_cmp( self.version, self.revision, other.version, other.revision ) < 0 ) return self.package < other.package return self.category < other.category except AttributeError: raise TypeError( "'<' not supported between instances of " f"{self.__class__.__name__!r} and {other.__class__.__name__!r}" ) def __le__(self, other): try: if self.category == other.category: if self.package == other.package: return ( ver_cmp( self.version, self.revision, other.version, other.revision ) <= 0 ) return self.package < other.package return self.category < other.category except AttributeError: raise TypeError( "'<=' not supported between instances of " f"{self.__class__.__name__!r} and {other.__class__.__name__!r}" ) def __gt__(self, other): try: if self.category == other.category: if self.package == other.package: return ( ver_cmp( self.version, self.revision, other.version, other.revision ) > 0 ) return self.package > other.package return self.category > other.category except AttributeError: raise TypeError( "'>' not supported between instances of " f"{self.__class__.__name__!r} and {other.__class__.__name__!r}" ) def __ge__(self, other): try: if self.category == other.category: if self.package == other.package: return ( ver_cmp( self.version, self.revision, other.version, other.revision ) >= 0 ) return self.package > other.package return self.category > other.category except AttributeError: raise TypeError( "'>=' not supported between instances of " f"{self.__class__.__name__!r} and {other.__class__.__name__!r}" ) @property def versioned_atom(self): if self.version is not None: return atom.atom(f"={self.cpvstr}") return self.unversioned_atom @property def unversioned_atom(self): return atom.atom(self.key)
[docs] @classmethod def versioned(cls, *args): return cls(versioned=True, *args)
[docs] @classmethod def unversioned(cls, *args): return cls(versioned=False, *args)
[docs] class VersionedCPV(CPV): __slots__ = () def __init__(self, *args): super().__init__(*args, versioned=True)
[docs] class UnversionedCPV(CPV): __slots__ = () def __init__(self, *args): super().__init__(*args, versioned=False)