Source code for rez.package_order

# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project


from inspect import isclass
from hashlib import sha1

from rez.config import config
from rez.utils.data_utils import cached_class_property
from rez.vendor.version.version import Version


[docs]class PackageOrder(object): """Package reorderer base class.""" name = None def __init__(self): pass
[docs] def reorder(self, iterable, key=None): """Put packages into some order for consumption. You can safely assume that the packages referred to by `iterable` are all versions of the same package family. Note: Returning None, and an unchanged `iterable` list, are not the same thing. Returning None may cause rez to pass the package list to the next orderer; whereas a package list that has been reordered (even if the unchanged list is returned) is not passed onto another orderer. Args: iterable: Iterable list of packages, or objects that contain packages. key (callable): Callable, where key(iterable) gives a `Package`. If None, iterable is assumed to be a list of `Package` objects. Returns: List of `iterable` type, reordered. """ raise NotImplementedError
[docs] def to_pod(self): raise NotImplementedError
@property def sha1(self): return sha1(repr(self).encode('utf-8')).hexdigest() def __str__(self): raise NotImplementedError def __eq__(self, other): raise NotImplementedError def __ne__(self, other): return not self == other def __repr__(self): return "%s(%s)" % (self.__class__.__name__, str(self))
[docs]class NullPackageOrder(PackageOrder): """An orderer that does not change the order - a no op. This orderer is useful in cases where you want to apply some default orderer to a set of packages, but may want to explicitly NOT reorder a particular package. You would use a `NullPackageOrder` in a `PerFamilyOrder` to do this. """ name = "no_order"
[docs] def reorder(self, iterable, key=None): return list(iterable)
def __str__(self): return "{}" def __eq__(self, other): return type(self) == type(other)
[docs] def to_pod(self): """ Example (in yaml): type: no_order """ return {}
[docs] @classmethod def from_pod(cls, data): return cls()
[docs]class SortedOrder(PackageOrder): """An orderer that sorts wrt version. """ name = "sorted" def __init__(self, descending): self.descending = descending
[docs] def reorder(self, iterable, key=None): key = key or (lambda x: x) return sorted(iterable, key=lambda x: key(x).version, reverse=self.descending)
def __str__(self): return str(self.descending) def __eq__(self, other): return ( type(self) == type(other) and self.descending == other.descending )
[docs] def to_pod(self): """ Example (in yaml): type: sorted descending: true """ return {"descending": self.descending}
[docs] @classmethod def from_pod(cls, data): return cls(descending=data["descending"])
[docs]class PerFamilyOrder(PackageOrder): """An orderer that applies different orderers to different package families. """ name = "per_family" def __init__(self, order_dict, default_order=None): """Create a reorderer. Args: order_dict (dict of (str, `PackageOrder`): Orderers to apply to each package family. default_order (`PackageOrder`): Orderer to apply to any packages not specified in `order_dict`. """ self.order_dict = order_dict.copy() self.default_order = default_order
[docs] def reorder(self, iterable, key=None): try: item = next(iter(iterable)) except: return None key = key or (lambda x: x) package = key(item) orderer = self.order_dict.get(package.name) if orderer is None: orderer = self.default_order if orderer is None: return None return orderer.reorder(iterable, key)
def __str__(self): items = sorted((x[0], str(x[1])) for x in self.order_dict.items()) return str((items, str(self.default_order))) def __eq__(self, other): return ( type(other) == type(self) and self.order_dict == other.order_dict and self.default_order == other.default_order )
[docs] def to_pod(self): """ Example (in yaml): type: per_family orderers: - packages: ['foo', 'bah'] type: version_split first_version: '4.0.5' - packages: ['python'] type: sorted descending: false default_order: type: sorted descending: true """ orderers = {} packages = {} # group package fams by orderer they use for fam, orderer in self.order_dict.items(): k = id(orderer) orderers[k] = orderer packages.setdefault(k, set()).add(fam) orderlist = [] for k, fams in packages.items(): orderer = orderers[k] data = to_pod(orderer) data["packages"] = sorted(fams) orderlist.append(data) result = {"orderers": orderlist} if self.default_order is not None: result["default_order"] = to_pod(self.default_order) return result
[docs] @classmethod def from_pod(cls, data): order_dict = {} default_order = None for d in data["orderers"]: d = d.copy() fams = d.pop("packages") orderer = from_pod(d) for fam in fams: order_dict[fam] = orderer d = data.get("default_order") if d: default_order = from_pod(d) return cls(order_dict, default_order)
[docs]class VersionSplitPackageOrder(PackageOrder): """Orders package versions <= a given version first. For example, given the versions [5, 4, 3, 2, 1], an orderer initialized with version=3 would give the order [3, 2, 1, 5, 4]. """ name = "version_split" def __init__(self, first_version): """Create a reorderer. Args: first_version (`Version`): Start with versions <= this value. """ self.first_version = first_version
[docs] def reorder(self, iterable, key=None): key = key or (lambda x: x) # sort by version descending descending = sorted(iterable, key=lambda x: key(x).version, reverse=True) above = [] below = [] is_above = True for item in descending: if is_above: package = key(item) is_above = (package.version > self.first_version) if is_above: above.append(item) else: below.append(item) return below + above
def __str__(self): return str(self.first_version) def __eq__(self, other): return ( type(other) == type(self) and self.first_version == other.first_version )
[docs] def to_pod(self): """ Example (in yaml): type: version_split first_version: "3.0.0" """ return dict(first_version=str(self.first_version))
[docs] @classmethod def from_pod(cls, data): return cls(Version(data["first_version"]))
[docs]class TimestampPackageOrder(PackageOrder): """A timestamp order function. Given a time T, this orderer returns packages released before T, in descending order, followed by those released after. If `rank` is non-zero, version changes at that rank and above are allowed over the timestamp. For example, consider the common case where we want to prioritize packages released before T, except for newer patches. Consider the following package versions, and time T: 2.2.1 2.2.0 2.1.1 2.1.0 2.0.6 2.0.5 <-- T 2.0.0 1.9.0 A timestamp orderer set to rank=3 (patch versions) will attempt to consume the packages in the following order: 2.0.6 2.0.5 2.0.0 1.9.0 2.1.1 2.1.0 2.2.1 2.2.0 Notice that packages before T are preferred, followed by newer versions. Newer versions are consumed in ascending order, except within rank (this is why 2.1.1 is consumed before 2.1.0). """ name = "soft_timestamp" def __init__(self, timestamp, rank=0): """Create a reorderer. Args: timestamp (int): Epoch time of timestamp. Packages before this time are preferred. rank (int): If non-zero, allow version changes at this rank or above past the timestamp. """ self.timestamp = timestamp self.rank = rank
[docs] def reorder(self, iterable, key=None): first_after = None key = key or (lambda x: x) # sort by version descending descending = sorted(iterable, key=lambda x: key(x).version, reverse=True) for i, o in enumerate(descending): package = key(o) if package.timestamp: if package.timestamp > self.timestamp: first_after = i else: break if first_after is None: # all packages are before T return None before = descending[first_after + 1:] after = list(reversed(descending[:first_after + 1])) if not self.rank: # simple case return before + after # include packages after timestamp but within rank if before and after: package = key(before[0]) first_prerank = package.version.trim(self.rank - 1) found = False for i, o in enumerate(after): package = key(o) prerank = package.version.trim(self.rank - 1) if prerank != first_prerank: found = True break if not found: # highest version is also within rank, so result is just # simple descending list return descending if i: before = list(reversed(after[:i])) + before after = after[i:] # ascend below rank, but descend within after_ = [] postrank = [] prerank = None for o in after: package = key(o) prerank_ = package.version.trim(self.rank - 1) if prerank_ == prerank: postrank.append(o) else: after_.extend(reversed(postrank)) postrank = [o] prerank = prerank_ after_.extend(reversed(postrank)) return before + after_
def __str__(self): return str((self.timestamp, self.rank)) def __eq__(self, other): return ( type(other) == type(self) and self.timestamp == other.timestamp and self.rank == other.rank )
[docs] def to_pod(self): """ Example (in yaml): type: soft_timestamp timestamp: 1234567 rank: 3 """ return dict(timestamp=self.timestamp, rank=self.rank)
[docs] @classmethod def from_pod(cls, data): return cls(timestamp=data["timestamp"], rank=data.get("rank", 0))
[docs]class PackageOrderList(list): """A list of package orderer. """
[docs] def to_pod(self): data = [] for f in self: data.append(to_pod(f)) return data
[docs] @classmethod def from_pod(cls, data): flist = PackageOrderList() for dict_ in data: f = from_pod(dict_) flist.append(f) return flist
[docs] @cached_class_property def singleton(cls): """Filter list as configured by rezconfig.package_filter.""" return cls.from_pod(config.package_orderers)
[docs]def to_pod(orderer): data = {"type": orderer.name} data.update(orderer.to_pod()) return data
[docs]def from_pod(data): if isinstance(data, dict): cls_name = data["type"] data = data.copy() data.pop("type") cls = _orderers[cls_name] return cls.from_pod(data) else: # old-style, kept for backwards compatibility cls_name, data_ = data cls = _orderers[cls_name] return cls.from_pod(data_)
[docs]def register_orderer(cls): if isclass(cls) and issubclass(cls, PackageOrder) and \ hasattr(cls, "name") and cls.name: _orderers[cls.name] = cls return True else: return False
# registration of builtin orderers _orderers = {} for o in list(globals().values()): register_orderer(o)