class wrapper(base): __slots__ = ("_raw_pkg", ) klass.inject_richcmp_methods_from_cmp(locals()) def operations(self, domain, **kwds): return self._raw_pkg._operations(domain, self, **kwds) def __init__(self, raw_pkg): object.__setattr__(self, "_raw_pkg", raw_pkg) def __cmp__(self, other): if isinstance(other, wrapper): return cmp(self._raw_pkg, other._raw_pkg) return cmp(self._raw_pkg, other) def __eq__(self, other): if isinstance(other, wrapper): return cmp(self._raw_pkg, other._raw_pkg) == 0 return cmp(self._raw_pkg, other) == 0 def __ne__(self, other): return not self == other __getattr__ = klass.GetAttrProxy("_raw_pkg") built = klass.alias_attr("_raw_pkg.built") versioned_atom = klass.alias_attr("_raw_pkg.versioned_atom") unversioned_atom = klass.alias_attr("_raw_pkg.unversioned_atom") is_supported = klass.alias_attr('_raw_pkg.is_supported') def __hash__(self): return hash(self._raw_pkg)
class CPV(base.base, base_cls): """ base ebuild package class :ivar category: str category :ivar package: str package :ivar key: strkey (cat/pkg) :ivar version: str version :ivar revision: int revision :ivar versioned_atom: atom matching this exact version :ivar unversioned_atom: atom matching all versions of this package :cvar _get_attr: mapping of attr:callable to generate attributes on the fly """ __slots__ = () inject_richcmp_methods_from_cmp(locals()) # __metaclass__ = WeakInstMeta # __inst_caching__ = True def __repr__(self): return '<%s cpvstr=%s @%#8x>' % (self.__class__.__name__, getattr(self, 'cpvstr', None), id(self)) # for some insane reason, py3k doesn't pick up the __hash__... add it # manually. __hash__ = base_cls.__hash__ @property def versioned_atom(self): return atom.atom("=%s" % self.cpvstr) @property def unversioned_atom(self): return atom.atom(self.key) @classmethod def versioned(cls, *args): return cls(versioned=True, *args) @classmethod def unversioned(cls, *args): return cls(versioned=False, *args) def __reduce__(self): return (self.__class__, (self.cpvstr, ), None, None, None)
class FakePkg(base): # XXX why isn't this using existing classes? __slotting_intentionally_disabled__ = True def __init__(self, pkg, ver, data): base.__init__(self) self.pkg = pkg self.ver = ver self._get_attr = {k: partial(passthru, v) for k, v in data.iteritems()} # disable protection. don't want it here __setattr__ = object.__setattr__ __getattr__ = dynamic_getattr_dict def __cmp__(self, other): return cmp(self.ver, other.ver) inject_richcmp_methods_from_cmp(locals())
class atom(boolean.AndRestriction): """Currently implements gentoo ebuild atom parsing. Should be converted into an agnostic dependency base. """ # note we don't need _hash __slots__ = ("blocks", "blocks_strongly", "op", "cpvstr", "negate_vers", "use", "slot_operator", "slot", "subslot", "category", "version", "revision", "fullver", "package", "key", "repo_id", "_hash") type = packages.package_type negate = False _evaluate_collapse = True __attr_comparison__ = ("cpvstr", "op", "blocks", "negate_vers", "use", "slot", "subslot", "slot_operator", "repo_id") inject_richcmp_methods_from_cmp(locals()) # hack; combine these 2 metaclasses at some point... locals().pop("__eq__", None) locals().pop("__ne__", None) __metaclass__ = generic_equality __inst_caching__ = True locals().update(atom_overrides.iteritems()) # overrided in child class if it's supported evaluate_depset = None @property def blocks_temp_ignorable(self): return not self.blocks_strongly weak_blocker = alias_attr("blocks_temp_ignorable") def __repr__(self): if self.op == '=*': atom = "=%s*" % self.cpvstr else: atom = self.op + self.cpvstr if self.blocks: atom = '!' + atom if self.blocks: if self.blocks_strongly: atom = '!!' + atom else: atom = '!' + atom attrs = [atom] if self.use: attrs.append('use=%r' % (self.use, )) if self.slot is not None: attrs.append('slot=%r' % (self.slot, )) if self.subslot is not None: attrs.append('subslot=%r' % (self.subslot, )) if self.repo_id is not None: attrs.append('repo_id=%r' % (self.repo_id, )) return '<%s %s @#%x>' % (self.__class__.__name__, ' '.join(attrs), id(self)) def __reduce__(self): return (atom, (str(self), self.negate_vers)) def iter_dnf_solutions(self, full_solution_expansion=False): if full_solution_expansion: return boolean.AndRestriction.iter_dnf_solutions(self, True) return iter([[self]]) def iter_cnf_solutions(self, full_solution_expansion=False): if full_solution_expansion: return boolean.AndRestriction.iter_cnf_solutions(self, True) return iter([[self]]) def cnf_solutions(self, full_solution_expansion=False): if full_solution_expansion: return boolean.AndRestriction.cnf_solutions(self, True) return [[self]] @property def is_simple(self): return len(self.restrictions) == 2 def __str__(self): if self.op == '=*': s = "=%s*" % self.cpvstr else: s = self.op + self.cpvstr if self.blocks: if self.blocks_strongly: s = '!!' + s else: s = '!' + s if self.slot: s += ":%s" % self.slot if self.subslot and self.slot_operator == "=": s += "/%s=" % self.subslot if self.repo_id: s += "::%s" % self.repo_id if self.use: s += "[%s]" % ",".join(self.use) return s __hash__ = reflective_hash('_hash') def __iter__(self): return iter(self.restrictions) def __getitem__(self, index): return self.restrictions[index] def __cmp__(self, other): if not isinstance(other, self.__class__): raise TypeError("other isn't of %s type, is %s" % (self.__class__, other.__class__)) c = cmp(self.category, other.category) if c: return c c = cmp(self.package, other.package) if c: return c c = cmp(self.op, other.op) if c: return c c = cpv.ver_cmp(self.version, self.revision, other.version, other.revision) if c: return c c = cmp(self.blocks, other.blocks) if c: # invert it; cmp(True, False) == 1 # want non blockers then blockers. return -c c = cmp(self.blocks_strongly, other.blocks_strongly) if c: # want !! prior to ! return c c = cmp(self.negate_vers, other.negate_vers) if c: return c def f(v): return '' if v is None else v c = cmp(f(self.slot), f(other.slot)) if c: return c c = cmp(self.use, other.use) if c: return c return cmp(self.repo_id, other.repo_id) def intersects(self, other): """Check if a passed in atom "intersects" this restriction's atom. Two atoms "intersect" if a package can be constructed that matches both: - if you query for just "dev-lang/python" it "intersects" both "dev-lang/python" and ">=dev-lang/python-2.4" - if you query for "=dev-lang/python-2.4" it "intersects" ">=dev-lang/python-2.4" and "dev-lang/python" but not "<dev-lang/python-2.3" USE and slot deps are also taken into account. The block/nonblock state of the atom is ignored. """ # Our "key" (cat/pkg) must match exactly: if self.key != other.key: return False # Slot dep only matters if we both have one. If we do they # must be identical: if (self.slot is not None and other.slot is not None and self.slot != other.slot): return False if (self.repo_id is not None and other.repo_id is not None and self.repo_id != other.repo_id): return False # Use deps are similar: if one of us forces a flag on and the # other forces it off we do not intersect. If only one of us # cares about a flag it is irrelevant. # Skip the (very common) case of one of us not having use deps: if self.use and other.use: # Set of flags we do not have in common: flags = set(self.use) ^ set(other.use) for flag in flags: # If this is unset and we also have the set version we fail: if flag[0] == '-' and flag[1:] in flags: return False # Remaining thing to check is version restrictions. Get the # ones we can check without actual version comparisons out of # the way first. # If one of us is unversioned we intersect: if not self.op or not other.op: return True # If we are both "unbounded" in the same direction we intersect: if (('<' in self.op and '<' in other.op) or ('>' in self.op and '>' in other.op)): return True # Trick used here: just use the atoms as sufficiently # package-like object to pass to these functions (all that is # needed is a version and revision attr). # If one of us is an exact match we intersect if the other matches it: if self.op == '=': if other.op == '=*': return self.fullver.startswith(other.fullver) return restricts.VersionMatch(other.op, other.version, other.revision).match(self) if other.op == '=': if self.op == '=*': return other.fullver.startswith(self.fullver) return restricts.VersionMatch(self.op, self.version, self.revision).match(other) # If we are both ~ matches we match if we are identical: if self.op == other.op == '~': return (self.version == other.version and self.revision == other.revision) # If we are both glob matches we match if one of us matches the other. if self.op == other.op == '=*': return (self.fullver.startswith(other.fullver) or other.fullver.startswith(self.fullver)) # If one of us is a glob match and the other a ~ we match if the glob # matches the ~ (ignoring a revision on the glob): if self.op == '=*' and other.op == '~': return other.fullver.startswith(self.version) if other.op == '=*' and self.op == '~': return self.fullver.startswith(other.version) # If we get here at least one of us is a <, <=, > or >=: if self.op in ('<', '<=', '>', '>='): ranged, other = self, other else: ranged, other = other, self if '<' in other.op or '>' in other.op: # We are both ranged, and in the opposite "direction" (or # we would have matched above). We intersect if we both # match the other's endpoint (just checking one endpoint # is not enough, it would give a false positive on <=2 vs >2) return (restricts.VersionMatch(other.op, other.version, other.revision).match(ranged) and restricts.VersionMatch(ranged.op, ranged.version, ranged.revision).match(other)) if other.op == '~': # Other definitely matches its own version. If ranged also # does we're done: if restricts.VersionMatch(ranged.op, ranged.version, ranged.revision).match(other): return True # The only other case where we intersect is if ranged is a # > or >= on other's version and a nonzero revision. In # that case other will match ranged. Be careful not to # give a false positive for ~2 vs <2 here: return ranged.op in ('>', '>=') and restricts.VersionMatch( other.op, other.version, other.revision).match(ranged) if other.op == '=*': # The fun one, since glob matches do not correspond to a # single contiguous region of versions. # a glob match definitely matches its own version, so if # ranged does too we're done: if restricts.VersionMatch(ranged.op, ranged.version, ranged.revision).match(other): return True if '<' in ranged.op: # Remaining cases where this intersects: there is a # package smaller than ranged.fullver and # other.fullver that they both match. # If other.revision is not None then other does not # match anything smaller than its own fullver: if other.revision is not None: return False # If other.revision is None then we can always # construct a package smaller than other.fullver by # tagging e.g. an _alpha1 on, since # cat/pkg_beta2_alpha1_alpha1 is a valid version. # (Yes, really. Try it if you don't believe me.) # If and only if other also matches ranged then # ranged will also match one of those smaller packages. # XXX (I think, need to try harder to verify this.) return ranged.fullver.startswith(other.version) else: # Remaining cases where this intersects: there is a # package greater than ranged.fullver and # other.fullver that they both match. # We can always construct a package greater than # other.fullver by adding a digit to it. # If and only if other also matches ranged then # ranged will match such a larger package # XXX (I think, need to try harder to verify this.) return ranged.fullver.startswith(other.version) # Handled all possible ops. raise NotImplementedError( 'Someone added an op to atom without adding it to intersects') def evaluate_conditionals(self, parent_cls, parent_seq, enabled, tristate=None): parent_seq.append(self)
class fsBase: """base class, all extensions must derive from this class""" __slots__ = ("location", "mtime", "mode", "uid", "gid") __attrs__ = __slots__ __default_attrs__ = {} locals().update( (x.replace("is", "is_"), False) for x in __all__ if x.startswith("is") and x.islower() and not x.endswith("fs_obj")) klass.inject_richcmp_methods_from_cmp(locals()) klass.inject_immutable_instance(locals()) def __init__(self, location, strict=True, **d): d["location"] = normpath(location) s = object.__setattr__ if strict: for k in self.__attrs__: s(self, k, d[k]) else: for k, v in d.items(): s(self, k, v) gen_doc_additions(__init__, __attrs__) def change_attributes(self, **kwds): d = {x: getattr(self, x) for x in self.__attrs__ if hasattr(self, x)} d.update(kwds) # split location out location = d.pop("location") if not location.startswith(path_seperator): location = abspath(location) d["strict"] = False return self.__class__(location, **d) def __getattr__(self, attr): # we would only get called if it doesn't exist. if attr not in self.__attrs__: raise AttributeError(self, attr) obj = self.__default_attrs__.get(attr) if not callable(obj): return obj return obj(self) def __hash__(self): return hash(self.location) def __eq__(self, other): if not isinstance(other, self.__class__): return False return self.location == other.location def __ne__(self, other): return not self == other def realpath(self, cache=None): """calculate the abspath/canonicalized path for this entry, returning a new instance if the path differs. :keyword cache: Either None (no cache), or a data object of path-> resolved. Currently unused, but left in for forwards compatibility """ new_path = realpath(self.location) if new_path == self.location: return self return self.change_attributes(location=new_path) @property def basename(self): return basename(self.location) @property def dirname(self): return dirname(self.location) def fnmatch(self, pattern): return fnmatch.fnmatch(self.location, pattern) def __cmp__(self, other): return cmp(self.location, other.location) def __str__(self): return self.location
class atom(boolean.AndRestriction, metaclass=klass.generic_equality): """Currently implements gentoo ebuild atom parsing. Should be converted into an agnostic dependency base. """ # note we don't need _hash __slots__ = ( "blocks", "blocks_strongly", "op", "cpvstr", "negate_vers", "use", "slot_operator", "slot", "subslot", "repo_id", "_hash", "_cpv", "_restrictions", ) type = restriction.package_type negate = False _evaluate_collapse = True __attr_comparison__ = ("cpvstr", "op", "blocks", "negate_vers", "use", "slot", "subslot", "slot_operator", "repo_id") klass.inject_richcmp_methods_from_cmp(locals()) # hack; combine these 2 metaclasses at some point... locals().pop("__eq__", None) locals().pop("__ne__", None) __inst_caching__ = True # overrided in child class if it's supported evaluate_depset = None def __init__(self, atom, negate_vers=False, eapi='-1'): """ :param atom: string, see gentoo ebuild atom syntax :keyword negate_vers: boolean controlling whether the version should be inverted for restriction matching :keyword eapi: string/int controlling what eapi to enforce for this atom """ if not atom: raise errors.MalformedAtom(atom) sf = object.__setattr__ orig_atom = atom override_kls = False use_start = atom.find("[") slot_start = atom.find(":") if use_start != -1: # use dep use_end = atom.find("]", use_start) if use_end == -1: raise errors.MalformedAtom(orig_atom, "use restriction isn't completed") elif use_end != len(atom) - 1: raise errors.MalformedAtom(orig_atom, "trailing garbage after use dep") sf(self, "use", tuple(sorted(atom[use_start + 1:use_end].split(',')))) for x in self.use: # stripped purely for validation reasons try: if x[-1] in "=?": override_kls = True x = x[:-1] if x[0] == '!': x = x[1:] if x[0] == '-': raise errors.MalformedAtom( orig_atom, f"malformed use flag: {x}") elif x[0] == '-': x = x[1:] if x[-1] == ')' and eapi not in ('0', '1', '2', '3'): # use defaults. if x[-3:] in ("(+)", "(-)"): x = x[:-3] if not x: raise errors.MalformedAtom(orig_atom, 'empty use dep detected') if not valid_use_flag.match(x): raise errors.MalformedAtom(orig_atom, f'invalid USE flag: {x!r}') except IndexError: raise errors.MalformedAtom(orig_atom, 'empty use dep detected') if override_kls: sf(self, '__class__', transitive_use_atom) atom = atom[0:use_start] + atom[use_end + 1:] else: sf(self, "use", None) if slot_start != -1: i2 = atom.find("::", slot_start) if i2 != -1: repo_id = atom[i2 + 2:] if not repo_id: raise errors.MalformedAtom(orig_atom, "repo_id must not be empty") elif repo_id[0] in '-': raise errors.MalformedAtom( orig_atom, f"invalid first char of repo_id '{repo_id}' (must not begin with a hyphen)" ) elif not valid_repo_chars.issuperset(repo_id): raise errors.MalformedAtom( orig_atom, f"repo_id may contain only [a-Z0-9_-/], found {repo_id!r}" ) atom = atom[:i2] sf(self, "repo_id", repo_id) else: sf(self, "repo_id", None) # slot dep. slot = atom[slot_start + 1:] slot_operator = subslot = None if not slot: # if the slot char came in only due to repo_id, force slots to None if i2 == -1: raise errors.MalformedAtom( orig_atom, "Empty slot targets aren't allowed") slot = None else: slots = (slot, ) if eapi not in ('0', '1', '2', '3', '4'): if slot[0:1] in ("*", "="): if len(slot) > 1: raise errors.MalformedAtom( orig_atom, "Slot operators '*' and '=' do not take slot targets" ) slot_operator = slot slot, slots = None, () else: if slot.endswith('='): slot_operator = '=' slot = slot[:-1] slots = slot.split('/', 1) elif eapi == '0': raise errors.MalformedAtom( orig_atom, "slot dependencies aren't allowed in EAPI 0") for chunk in slots: if not chunk: raise errors.MalformedAtom( orig_atom, "Empty slot targets aren't allowed") if chunk[0] in '-.': raise errors.MalformedAtom( orig_atom, "Slot targets must not start with a hypen or dot: {chunk!r}" ) elif not valid_slot_chars.issuperset(chunk): invalid_chars = ', '.join( map( repr, sorted( set(chunk).difference(valid_slot_chars)))) raise errors.MalformedAtom( orig_atom, f"Invalid character(s) in slot target: {invalid_chars}" ) if len(slots) == 2: slot, subslot = slots sf(self, "slot_operator", slot_operator) sf(self, "slot", slot) sf(self, "subslot", subslot) atom = atom[:slot_start] else: sf(self, "slot_operator", None) sf(self, "slot", None) sf(self, "subslot", None) sf(self, "repo_id", None) sf(self, "blocks", atom[0] == "!") if self.blocks: atom = atom[1:] # hackish/slow, but lstrip doesn't take a 'prune this many' arg # open to alternatives if eapi not in ('0', '1') and atom.startswith("!"): atom = atom[1:] sf(self, "blocks_strongly", True) else: sf(self, "blocks_strongly", False) else: sf(self, "blocks_strongly", False) if atom[0] in ('<', '>'): if atom[1] == '=': sf(self, 'op', atom[:2]) atom = atom[2:] else: sf(self, 'op', atom[0]) atom = atom[1:] elif atom[0] == '=': if atom[-1] == '*': sf(self, 'op', '=*') atom = atom[1:-1] else: atom = atom[1:] sf(self, 'op', '=') elif atom[0] == '~': sf(self, 'op', '~') atom = atom[1:] else: sf(self, 'op', '') sf(self, 'cpvstr', atom) if eapi == '0': for x in ('use', 'slot'): if getattr(self, x) is not None: raise errors.MalformedAtom( orig_atom, f"{x} atoms aren't supported for EAPI 0") elif eapi == '1': if self.use is not None: raise errors.MalformedAtom( orig_atom, "use atoms aren't supported for EAPI < 2") if eapi != '-1': if self.repo_id is not None: raise errors.MalformedAtom( orig_atom, f"repo_id atoms aren't supported for EAPI {eapi}") if use_start != -1 and slot_start != -1 and use_start < slot_start: raise errors.MalformedAtom(orig_atom, "slot restriction must proceed use") try: sf(self, "_cpv", cpv.CPV(self.cpvstr, versioned=bool(self.op))) except errors.InvalidCPV as e: raise errors.MalformedAtom(orig_atom) from e if self.op: if self.version is None: raise errors.MalformedAtom(orig_atom, "operator requires a version") elif self.op == '~' and self.revision: raise errors.MalformedAtom( orig_atom, "~ revision operater cannot be combined with a revision") elif self.version is not None: raise errors.MalformedAtom(orig_atom, 'versioned atom requires an operator') sf(self, "_hash", hash(orig_atom)) sf(self, "negate_vers", negate_vers) __getattr__ = klass.GetAttrProxy("_cpv") __dir__ = klass.DirProxy("_cpv") @property def blocks_temp_ignorable(self): return not self.blocks_strongly weak_blocker = klass.alias_attr("blocks_temp_ignorable") def __repr__(self): if self.op == '=*': atom = f"={self.cpvstr}*" else: atom = self.op + self.cpvstr if self.blocks: atom = '!' + atom if self.blocks: if self.blocks_strongly: atom = '!!' + atom else: atom = '!' + atom attrs = [atom] if self.use: attrs.append(f'use={self.use!r}') if self.slot is not None: attrs.append(f'slot={self.slot!r}') if self.subslot is not None: attrs.append(f'subslot={self.subslot!r}') if self.repo_id is not None: attrs.append(f'repo_id={self.repo_id!r}') return '<%s %s @#%x>' % (self.__class__.__name__, ' '.join(attrs), id(self)) def __reduce__(self): return (atom, (str(self), self.negate_vers)) def iter_dnf_solutions(self, full_solution_expansion=False): if full_solution_expansion: return boolean.AndRestriction.iter_dnf_solutions(self, True) return iter([[self]]) def iter_cnf_solutions(self, full_solution_expansion=False): if full_solution_expansion: return boolean.AndRestriction.iter_cnf_solutions(self, True) return iter([[self]]) def cnf_solutions(self, full_solution_expansion=False): if full_solution_expansion: return boolean.AndRestriction.cnf_solutions(self, True) return [[self]] @property def is_simple(self): return len(self.restrictions) == 2 @klass.jit_attr def restrictions(self): # ordering here matters; against 24702 ebuilds for # a non matchable atom with package as the first restriction # 10 loops, best of 3: 206 msec per loop # with category as the first(the obvious ordering) # 10 loops, best of 3: 209 msec per loop # why? because category is more likely to collide; # at the time of this profiling, there were 151 categories. # over 11k packages however. r = [ restricts.PackageDep(self.package), restricts.CategoryDep(self.category) ] if self.repo_id is not None: r.insert(0, restricts.RepositoryDep(self.repo_id)) if self.fullver is not None: if self.op == '=*': r.append( packages.PackageRestriction( "fullver", values.StrGlobMatch(self.fullver))) else: r.append( restricts.VersionMatch(self.op, self.version, self.revision, negate=self.negate_vers)) if self.slot is not None: r.append(restricts.SlotDep(self.slot)) if self.subslot is not None: r.append(restricts.SubSlotDep(self.subslot)) if self.use is not None: r.extend(restricts._parse_nontransitive_use(self.use)) return tuple(r) def __str__(self): if self.op == '=*': s = f"={self.cpvstr}*" else: s = self.op + self.cpvstr if self.blocks: if self.blocks_strongly: s = '!!' + s else: s = '!' + s if self.slot: s += f":{self.slot}" if self.subslot: s += f"/{self.subslot}" if self.slot_operator == "=": s += self.slot_operator elif self.slot_operator: s += f":{self.slot_operator}" if self.repo_id: s += f"::{self.repo_id}" if self.use: use = ','.join(self.use) s += f"[{use}]" return s __hash__ = klass.reflective_hash('_hash') def __iter__(self): return iter(self.restrictions) def __getitem__(self, index): return self.restrictions[index] def __cmp__(self, other): if not isinstance(other, atom): raise TypeError( f"other isn't of {atom!r} type, is {other.__class__}") c = cmp(self.category, other.category) if c: return c c = cmp(self.package, other.package) if c: return c c = cmp(self.op, other.op) if c: return c c = cpv.ver_cmp(self.version, self.revision, other.version, other.revision) if c: return c c = cmp(self.blocks, other.blocks) if c: # invert it; cmp(True, False) == 1 # want non blockers then blockers. return -c c = cmp(self.blocks_strongly, other.blocks_strongly) if c: # want !! prior to ! return c c = cmp(self.negate_vers, other.negate_vers) if c: return c def f(v): return '' if v is None else v c = cmp(f(self.slot), f(other.slot)) if c: return c c = cmp(self.use, other.use) if c: return c return cmp(self.repo_id, other.repo_id) no_usedeps = klass.alias_attr("get_atom_without_use_deps") @property def get_atom_without_use_deps(self): """Return atom object stripped of USE dependencies.""" if not self.use: return self if self.op == '=*': s = f'={self.cpvstr}*' else: s = self.op + self.cpvstr if self.blocks: s = '!' + s if not self.blocks_temp_ignorable: s = '!' + s if self.slot: s += f':{self.slot}' return atom(s) def intersects(self, other): """Check if a passed in atom "intersects" this restriction's atom. Two atoms "intersect" if a package can be constructed that matches both: - if you query for just "dev-lang/python" it "intersects" both "dev-lang/python" and ">=dev-lang/python-2.4" - if you query for "=dev-lang/python-2.4" it "intersects" ">=dev-lang/python-2.4" and "dev-lang/python" but not "<dev-lang/python-2.3" USE and slot deps are also taken into account. The block/nonblock state of the atom is ignored. """ # Our "key" (cat/pkg) must match exactly: if self.key != other.key: return False # Slot dep only matters if we both have one. If we do they # must be identical: if (self.slot is not None and other.slot is not None and self.slot != other.slot): return False # Subslot dep only matters if we both have one. If we do they # must be identical: if (self.subslot is not None and other.subslot is not None and self.subslot != other.subslot): return False if (self.repo_id is not None and other.repo_id is not None and self.repo_id != other.repo_id): return False # Use deps are similar: if one of us forces a flag on and the # other forces it off we do not intersect. If only one of us # cares about a flag it is irrelevant. # Skip the (very common) case of one of us not having use deps: if self.use and other.use: # Set of flags we do not have in common: flags = set(self.use) ^ set(other.use) for flag in flags: # If this is unset and we also have the set version we fail: if flag[0] == '-' and flag[1:] in flags: return False # Remaining thing to check is version restrictions. Get the # ones we can check without actual version comparisons out of # the way first. # If one of us is unversioned we intersect: if not self.op or not other.op: return True # If we are both "unbounded" in the same direction we intersect: if (('<' in self.op and '<' in other.op) or ('>' in self.op and '>' in other.op)): return True # Trick used here: just use the atoms as sufficiently # package-like object to pass to these functions (all that is # needed is a version and revision attr). # If one of us is an exact match we intersect if the other matches it: if self.op == '=': if other.op == '=*': return self.fullver.startswith(other.fullver) return restricts.VersionMatch(other.op, other.version, other.revision).match(self) if other.op == '=': if self.op == '=*': return other.fullver.startswith(self.fullver) return restricts.VersionMatch(self.op, self.version, self.revision).match(other) # If we are both ~ matches we match if we are identical: if self.op == other.op == '~': return (self.version == other.version and self.revision == other.revision) # If we are both glob matches we match if one of us matches the other. if self.op == other.op == '=*': return (self.fullver.startswith(other.fullver) or other.fullver.startswith(self.fullver)) # If one of us is a glob match and the other a ~ we match if the glob # matches the ~ (ignoring a revision on the glob): if self.op == '=*' and other.op == '~': return other.fullver.startswith(self.version) if other.op == '=*' and self.op == '~': return self.fullver.startswith(other.version) # If we get here at least one of us is a <, <=, > or >=: if self.op in ('<', '<=', '>', '>='): ranged, other = self, other else: ranged, other = other, self if '<' in other.op or '>' in other.op: # We are both ranged, and in the opposite "direction" (or # we would have matched above). We intersect if we both # match the other's endpoint (just checking one endpoint # is not enough, it would give a false positive on <=2 vs >2) return (restricts.VersionMatch(other.op, other.version, other.revision).match(ranged) and restricts.VersionMatch(ranged.op, ranged.version, ranged.revision).match(other)) if other.op == '~': # Other definitely matches its own version. If ranged also # does we're done: if restricts.VersionMatch(ranged.op, ranged.version, ranged.revision).match(other): return True # The only other case where we intersect is if ranged is a # > or >= on other's version and a nonzero revision. In # that case other will match ranged. Be careful not to # give a false positive for ~2 vs <2 here: return ranged.op in ('>', '>=') and restricts.VersionMatch( other.op, other.version, other.revision).match(ranged) if other.op == '=*': # The fun one, since glob matches do not correspond to a # single contiguous region of versions. # a glob match definitely matches its own version, so if # ranged does too we're done: if restricts.VersionMatch(ranged.op, ranged.version, ranged.revision).match(other): return True if '<' in ranged.op: # Remaining cases where this intersects: there is a # package smaller than ranged.fullver and # other.fullver that they both match. # If other.revision is not None or 0 then other does not match # anything smaller than its own fullver: if other.revision: return False # If other.revision is None or 0 then we can always # construct a package smaller than other.fullver by # tagging e.g. an _alpha1 on, since # cat/pkg_beta2_alpha1_alpha1 is a valid version. # (Yes, really. Try it if you don't believe me.) # If and only if other also matches ranged then # ranged will also match one of those smaller packages. # XXX (I think, need to try harder to verify this.) return ranged.fullver.startswith(other.version) else: # Remaining cases where this intersects: there is a # package greater than ranged.fullver and # other.fullver that they both match. # We can always construct a package greater than # other.fullver by adding a digit to it. # If and only if other also matches ranged then # ranged will match such a larger package # XXX (I think, need to try harder to verify this.) return ranged.fullver.startswith(other.version) # Handled all possible ops. raise NotImplementedError( 'Someone added an op to atom without adding it to intersects') def evaluate_conditionals(self, parent_cls, parent_seq, enabled, tristate=None): parent_seq.append(self)
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 :cvar _get_attr: mapping of attr:callable to generate attributes on the fly """ __slots__ = ("cpvstr", "key", "category", "package", "version", "revision", "fullver") inject_richcmp_methods_from_cmp(locals()) 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 __cmp__(self, other): try: if self.cpvstr == other.cpvstr: return 0 if (self.category and other.category and self.category != other.category): return cmp(self.category, other.category) if self.package and other.package and self.package != other.package: return cmp(self.package, other.package) # note I chucked out valueerror, none checks on versions # passed in. I suck, I know. # ~harring # fails in doing comparison of unversioned atoms against # versioned atoms return ver_cmp(self.version, self.revision, other.version, other.revision) except AttributeError: return 1 @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) @classmethod def versioned(cls, *args): return cls(versioned=True, *args) @classmethod def unversioned(cls, *args): return cls(versioned=False, *args)