From fe4a3841ef83c0a75dd5125fba577c319370a8dd Mon Sep 17 00:00:00 2001 From: Robert Minsk Date: Tue, 2 Jul 2024 11:05:42 -0700 Subject: [PATCH 1/4] refactor: move package_order to rezplugin system Signed-off-by: Robert Minsk --- src/rez/package_order.py | 488 ++---------------- src/rez/plugin_managers.py | 7 + src/rez/tests/test_completion.py | 1 + src/rez/tests/test_packages_order.py | 8 +- src/rezplugins/package_order/__init__.py | 6 + src/rezplugins/package_order/no_order.py | 46 ++ src/rezplugins/package_order/per_family.py | 124 +++++ .../package_order/soft_timestamp.py | 177 +++++++ src/rezplugins/package_order/sorted.py | 63 +++ src/rezplugins/package_order/version_split.py | 62 +++ 10 files changed, 537 insertions(+), 445 deletions(-) create mode 100644 src/rezplugins/package_order/__init__.py create mode 100644 src/rezplugins/package_order/no_order.py create mode 100644 src/rezplugins/package_order/per_family.py create mode 100644 src/rezplugins/package_order/soft_timestamp.py create mode 100644 src/rezplugins/package_order/sorted.py create mode 100644 src/rezplugins/package_order/version_split.py diff --git a/src/rez/package_order.py b/src/rez/package_order.py index e60662d77..1580b889c 100644 --- a/src/rez/package_order.py +++ b/src/rez/package_order.py @@ -7,10 +7,10 @@ from typing import Dict, Iterable, List, Optional, Union from rez.config import config +from rez.exceptions import RezPluginError from rez.utils.data_utils import cached_class_property from rez.version import Version, VersionRange -from rez.version._version import _Comparable, _ReversedComparable, _LowerBound, _UpperBound, _Bound -from rez.packages import iter_packages +from rez.version._version import _Comparable, _LowerBound, _UpperBound, _Bound ALL_PACKAGES = "*" @@ -177,7 +177,7 @@ def __str__(self): raise NotImplementedError def __eq__(self, other): - return type(self) == type(other) and str(self) == str(other) + return type(self) is type(other) and str(self) == str(other) def __ne__(self, other): return not self == other @@ -186,434 +186,6 @@ def __repr__(self): return "%s(%s)" % (self.__class__.__name__, str(self)) -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 :class:`NullPackageOrder` in a :class:`PerFamilyOrder` to do this. - """ - name = "no_order" - - def sort_key_implementation(self, package_name, version): - # python's sort will preserve the order of items that compare equal, so - # to not change anything, we just return the same object for all... - return 0 - - def __str__(self): - return "{}" - - def __eq__(self, other): - return type(self) == type(other) - - def to_pod(self): - """ - Example (in yaml): - - .. code-block:: yaml - - type: no_order - packages: ["foo"] - """ - return { - "packages": self.packages, - } - - @classmethod - def from_pod(cls, data): - return cls(packages=data.get("packages")) - - -class SortedOrder(PackageOrder): - """An orderer that sorts based on :attr:`Package.version `. - """ - name = "sorted" - - def __init__(self, descending, packages=None): - super().__init__(packages) - self.descending = descending - - def sort_key_implementation(self, package_name, version): - # Note that the name "descending" can be slightly confusing - it - # indicates that the final ordering this Order gives should be - # version descending (ie, the default) - however, the sort_key itself - # returns its results in "normal" ascending order (because it needs to - # be used "alongside" normally-sorted objects like versions). - # when the key is passed to sort(), though, it is always invoked with - # reverse=True... - if self.descending: - return version - else: - return _ReversedComparable(version) - - def __str__(self): - return str(self.descending) - - def __eq__(self, other): - return ( - type(self) == type(other) - and self.descending == other.descending - ) - - def to_pod(self): - """ - Example (in yaml): - - .. code-block:: yaml - - type: sorted - descending: true - packages: ["foo"] - """ - return { - "descending": self.descending, - "packages": self.packages, - } - - @classmethod - def from_pod(cls, data): - return cls( - data["descending"], - packages=data.get("packages"), - ) - - -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[str, PackageOrder]): Orderers to apply to - each package family. - default_order (PackageOrder): Orderer to apply to any packages - not specified in ``order_dict``. - """ - super().__init__(list(order_dict)) - self.order_dict = order_dict.copy() - self.default_order = default_order - - def reorder(self, iterable, key=None): - package_name = self._get_package_name_from_iterable(iterable, key) - if package_name is None: - return None - - 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 sort_key_implementation(self, package_name, version): - orderer = self.order_dict.get(package_name) - if orderer is None: - if self.default_order is None: - # shouldn't get here, because applies_to should protect us... - raise RuntimeError( - "package family orderer %r does not apply to package family %r", - (self, package_name)) - - orderer = self.default_order - - return orderer.sort_key_implementation(package_name, version) - - 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 - ) - - def to_pod(self): - """ - Example (in yaml): - - .. code-block:: 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 - - @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) - - -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, packages=None): - """Create a reorderer. - - Args: - first_version (Version): Start with versions <= this value. - """ - super().__init__(packages) - self.first_version = first_version - - def sort_key_implementation(self, package_name, version): - priority_key = 1 if version <= self.first_version else 0 - return priority_key, version - - def __str__(self): - return str(self.first_version) - - def __eq__(self, other): - return ( - type(other) == type(self) - and self.first_version == other.first_version - ) - - def to_pod(self): - """ - Example (in yaml): - - .. code-block:: yaml - - type: version_split - first_version: "3.0.0" - packages: ["foo"] - """ - return dict( - first_version=str(self.first_version), - packages=self.packages, - ) - - @classmethod - def from_pod(cls, data): - return cls( - Version(data["first_version"]), - packages=data.get("packages"), - ) - - -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``: - - .. code-block:: text - - 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: - - .. code-block:: text - - 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, packages=None): - """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. - """ - super().__init__(packages) - self.timestamp = timestamp - self.rank = rank - - # dictionary mapping from package family to the first-version-after - # the given timestamp - self._cached_first_after = {} - self._cached_sort_key = {} - - def _get_first_after(self, package_family): - """Get the first package version that is after the timestamp""" - try: - first_after = self._cached_first_after[package_family] - except KeyError: - first_after = self._calc_first_after(package_family) - self._cached_first_after[package_family] = first_after - return first_after - - def _calc_first_after(self, package_family): - descending = sorted(iter_packages(package_family), - key=lambda p: p.version, - reverse=True) - first_after = None - for i, package in enumerate(descending): - if not package.timestamp: - continue - if package.timestamp > self.timestamp: - first_after = package.version - else: - break - - if not self.rank: - return first_after - - # if we have rank, then we need to then go back UP the - # versions, until we find one whose trimmed version doesn't - # match. - # Note that we COULD do this by simply iterating through - # an ascending sequence, in which case we wouldn't have to - # "switch direction" after finding the first result after - # by timestamp... but we're making the assumption that the - # timestamp break will be closer to the higher end of the - # version, and that we'll therefore have to check fewer - # timestamps this way... - trimmed_version = package.version.trim(self.rank - 1) - first_after = None - for after_package in reversed(descending[:i]): - if after_package.version.trim(self.rank - 1) != trimmed_version: - return after_package.version - - return first_after - - def _calc_sort_key(self, package_name, version): - first_after = self._get_first_after(package_name) - if first_after is None: - # all packages are before T - is_before = True - else: - is_before = int(version < first_after) - - if is_before: - return is_before, version - - if self.rank: - return (is_before, - _ReversedComparable(version.trim(self.rank - 1)), - version.tokens[self.rank - 1:]) - - return is_before, _ReversedComparable(version) - - def sort_key_implementation(self, package_name, version): - cache_key = (package_name, str(version)) - result = self._cached_sort_key.get(cache_key) - if result is None: - result = self._calc_sort_key(package_name, version) - self._cached_sort_key[cache_key] = result - - return result - - 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 - ) - - def to_pod(self): - """ - Example (in yaml): - - .. code-block:: yaml - - type: soft_timestamp - timestamp: 1234567 - rank: 3 - packages: ["foo"] - """ - return dict( - timestamp=self.timestamp, - rank=self.rank, - packages=self.packages, - ) - - @classmethod - def from_pod(cls, data): - return cls( - data["timestamp"], - rank=data.get("rank", 0), - packages=data.get("packages"), - ) - - class PackageOrderList(list): """A list of package orderer. """ @@ -675,7 +247,7 @@ def remove(self, *args, **kwargs): def clear(self, *args, **kwargs): self.dirty = True - return super().clear(*args, **kwargs) + return super().clear() def insert(self, *args, **kwargs): self.dirty = True @@ -704,12 +276,12 @@ def from_pod(data): data = data.copy() data.pop("type") - cls = _orderers[cls_name] + cls = _find_orderer(cls_name) return cls.from_pod(data) else: # old-style, kept for backwards compatibility cls_name, data_ = data - cls = _orderers[cls_name] + cls = _find_orderer(cls_name) return cls.from_pod(data_) @@ -721,15 +293,21 @@ def get_orderer(package_name, orderers=None): orderer = orderers.get(ALL_PACKAGES) if orderer is None: # default ordering is version descending - orderer = SortedOrder(descending=True) + sorted_order = _find_orderer("sorted") + orderer = sorted_order(descending=True) return orderer +_orderers = {} + + def register_orderer(cls): - """Register an orderer + """Register an orderer. + + Kept for backwards compatability. New orderers should be a plugin. Args: - cls (type[PackageOrder]): Package orderer class to register. + cls (type[PackageOrder]): Package order class to register. returns: bool: True if successfully registered, else False. @@ -742,7 +320,35 @@ def register_orderer(cls): return False -# registration of builtin orderers -_orderers = {} -for o in list(globals().values()): - register_orderer(o) +def _find_orderer(name): + from rez.plugin_managers import plugin_manager + + try: + return plugin_manager.get_plugin_class('package_order', name) + except RezPluginError: + # Fallback to old "register_orderer" method + if name not in _orderers: + raise + return _orderers[name] + + +# For backwards compatibility create "construction functions" to replace +# existing classes in case a user is importing the class. +def NullPackageOrder(*args, **kwargs): + return _find_orderer('no_order')(*args, **kwargs) + + +def PerFamilyOrder(*args, **kwargs): + return _find_orderer('per_family')(*args, **kwargs) + + +def SortedOrder(*args, **kwargs): + return _find_orderer('sorted')(*args, **kwargs) + + +def TimestampPackageOrder(*args, **kwargs): + return _find_orderer('soft_timestamp')(*args, **kwargs) + + +def VersionSplitPackageOrder(*args, **kwargs): + return _find_orderer("version_split")(*args, **kwargs) diff --git a/src/rez/plugin_managers.py b/src/rez/plugin_managers.py index 64b1d8f09..a03ad9975 100644 --- a/src/rez/plugin_managers.py +++ b/src/rez/plugin_managers.py @@ -452,6 +452,12 @@ class CommandPluginType(RezPluginType): type_name = "command" +class PackageOrderPluginType(RezPluginType): + """Support for different ordering of packages. + """ + type_name = "package_order" + + plugin_manager = RezPluginManager() @@ -462,3 +468,4 @@ class CommandPluginType(RezPluginType): plugin_manager.register_plugin_type(PackageRepositoryPluginType) plugin_manager.register_plugin_type(BuildProcessPluginType) plugin_manager.register_plugin_type(CommandPluginType) +plugin_manager.register_plugin_type(PackageOrderPluginType) diff --git a/src/rez/tests/test_completion.py b/src/rez/tests/test_completion.py index f87629903..a0f618741 100644 --- a/src/rez/tests/test_completion.py +++ b/src/rez/tests/test_completion.py @@ -33,6 +33,7 @@ def _eq(prefix, expected_completions): "plugin_path"]) _eq("plugins", ["plugins", "plugins.command", + "plugins.package_order", "plugins.package_repository", "plugins.build_process", "plugins.build_system", diff --git a/src/rez/tests/test_packages_order.py b/src/rez/tests/test_packages_order.py index 1138b86fe..2f1fa3827 100644 --- a/src/rez/tests/test_packages_order.py +++ b/src/rez/tests/test_packages_order.py @@ -85,7 +85,7 @@ def test_comparison(self): self.assertFalse(inst1 != inst2) # __ne__ negative def test_pod(self): - """Validate we can save and load a VersionSplitPackageOrder to it's pod representation.""" + """Validate we can save and load a VersionSplitPackageOrder to its pod representation.""" self._test_pod(NullPackageOrder()) def test_sha1(self): @@ -124,7 +124,7 @@ def test_repr(self): self.assertEqual("SortedOrder(True)", repr(SortedOrder(descending=True))) def test_pod(self): - """Validate we can save and load a SortedOrder to it's pod representation.""" + """Validate we can save and load a SortedOrder to its pod representation.""" self._test_pod(SortedOrder(descending=True)) @@ -180,7 +180,7 @@ def test_repr(self): self.assertEqual("PerFamilyOrder(([('family1', '2.6.0')], 'None'))", repr(inst)) def test_pod(self): - """Validate we can save and load a PerFamilyOrder to it's pod representation.""" + """Validate we can save and load a PerFamilyOrder to its pod representation.""" self._test_pod( PerFamilyOrder(order_dict={'foo': NullPackageOrder()}, default_order=NullPackageOrder()) ) @@ -218,7 +218,7 @@ def test_repr(self): self.assertEqual("VersionSplitPackageOrder(1,2,3)", repr(inst)) def test_pod(self): - """Validate we can save and load a VersionSplitPackageOrder to it's pod representation.""" + """Validate we can save and load a VersionSplitPackageOrder to its pod representation.""" self._test_pod(VersionSplitPackageOrder(first_version=Version("1.2.3"))) diff --git a/src/rezplugins/package_order/__init__.py b/src/rezplugins/package_order/__init__.py new file mode 100644 index 000000000..dcd210ecf --- /dev/null +++ b/src/rezplugins/package_order/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + + +from rez.plugin_managers import extend_path +__path__ = extend_path(__path__, __name__) diff --git a/src/rezplugins/package_order/no_order.py b/src/rezplugins/package_order/no_order.py new file mode 100644 index 000000000..01b755b0a --- /dev/null +++ b/src/rezplugins/package_order/no_order.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + +from rez.package_order import PackageOrder + + +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 :class:`NullPackageOrder` in a :class:`PerFamilyOrder` to do this. + """ + name = "no_order" + + def sort_key_implementation(self, package_name, version): + # python's sort will preserve the order of items that compare equal, so + # to not change anything, we just return the same object for all... + return 0 + + def __str__(self): + return "{}" + + def __eq__(self, other): + return type(self) is type(other) + + def to_pod(self): + """ + Example (in yaml): + + .. code-block:: yaml + + type: no_order + packages: ["foo"] + """ + return { + "packages": self.packages, + } + + @classmethod + def from_pod(cls, data): + return cls(packages=data.get("packages")) + + +def register_plugin(): + return NullPackageOrder diff --git a/src/rezplugins/package_order/per_family.py b/src/rezplugins/package_order/per_family.py new file mode 100644 index 000000000..404705081 --- /dev/null +++ b/src/rezplugins/package_order/per_family.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + +from rez.package_order import PackageOrder, to_pod, from_pod + + +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[str, PackageOrder]): Orderers to apply to + each package family. + default_order (PackageOrder): Orderer to apply to any packages + not specified in ``order_dict``. + """ + super().__init__(list(order_dict)) + self.order_dict = order_dict.copy() + self.default_order = default_order + + def reorder(self, iterable, key=None): + package_name = self._get_package_name_from_iterable(iterable, key) + if package_name is None: + return None + + 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 sort_key_implementation(self, package_name, version): + orderer = self.order_dict.get(package_name) + if orderer is None: + if self.default_order is None: + # shouldn't get here, because applies_to should protect us... + raise RuntimeError( + "package family orderer %r does not apply to package family %r", + (self, package_name)) + + orderer = self.default_order + + return orderer.sort_key_implementation(package_name, version) + + 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(self) is type(other) + and self.order_dict == other.order_dict + and self.default_order == other.default_order + ) + + def to_pod(self): + """ + Example (in yaml): + + .. code-block:: 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 + + @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) + + +def register_plugin(): + return PerFamilyOrder \ No newline at end of file diff --git a/src/rezplugins/package_order/soft_timestamp.py b/src/rezplugins/package_order/soft_timestamp.py new file mode 100644 index 000000000..5d37da0d2 --- /dev/null +++ b/src/rezplugins/package_order/soft_timestamp.py @@ -0,0 +1,177 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + +from rez.package_order import PackageOrder +from rez.packages import iter_packages +from rez.version._version import _ReversedComparable + + +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``: + + .. code-block:: text + + 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: + + .. code-block:: text + + 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, packages=None): + """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. + """ + super().__init__(packages) + self.timestamp = timestamp + self.rank = rank + + # dictionary mapping from package family to the first-version-after + # the given timestamp + self._cached_first_after = {} + self._cached_sort_key = {} + + def _get_first_after(self, package_family): + """Get the first package version that is after the timestamp""" + try: + first_after = self._cached_first_after[package_family] + except KeyError: + first_after = self._calc_first_after(package_family) + self._cached_first_after[package_family] = first_after + return first_after + + def _calc_first_after(self, package_family): + descending = sorted(iter_packages(package_family), + key=lambda p: p.version, + reverse=True) + first_after = None + for i, package in enumerate(descending): + if not package.timestamp: + continue + if package.timestamp > self.timestamp: + first_after = package.version + else: + break + + if not self.rank: + return first_after + + # if we have rank, then we need to then go back UP the + # versions, until we find one whose trimmed version doesn't + # match. + # Note that we COULD do this by simply iterating through + # an ascending sequence, in which case we wouldn't have to + # "switch direction" after finding the first result after + # by timestamp... but we're making the assumption that the + # timestamp break will be closer to the higher end of the + # version, and that we'll therefore have to check fewer + # timestamps this way... + trimmed_version = package.version.trim(self.rank - 1) + first_after = None + for after_package in reversed(descending[:i]): + if after_package.version.trim(self.rank - 1) != trimmed_version: + return after_package.version + + return first_after + + def _calc_sort_key(self, package_name, version): + first_after = self._get_first_after(package_name) + if first_after is None: + # all packages are before T + is_before = True + else: + is_before = int(version < first_after) + + if is_before: + return is_before, version + + if self.rank: + return (is_before, + _ReversedComparable(version.trim(self.rank - 1)), + version.tokens[self.rank - 1:]) + + return is_before, _ReversedComparable(version) + + def sort_key_implementation(self, package_name, version): + cache_key = (package_name, str(version)) + result = self._cached_sort_key.get(cache_key) + if result is None: + result = self._calc_sort_key(package_name, version) + self._cached_sort_key[cache_key] = result + + return result + + def __str__(self): + return str((self.timestamp, self.rank)) + + def __eq__(self, other): + return ( + type(self) is type(other) + and self.timestamp == other.timestamp + and self.rank == other.rank + ) + + def to_pod(self): + """ + Example (in yaml): + + .. code-block:: yaml + + type: soft_timestamp + timestamp: 1234567 + rank: 3 + packages: ["foo"] + """ + return dict( + timestamp=self.timestamp, + rank=self.rank, + packages=self.packages, + ) + + @classmethod + def from_pod(cls, data): + return cls( + data["timestamp"], + rank=data.get("rank", 0), + packages=data.get("packages"), + ) + + +def register_plugin(): + return TimestampPackageOrder diff --git a/src/rezplugins/package_order/sorted.py b/src/rezplugins/package_order/sorted.py new file mode 100644 index 000000000..0ae9ab8f1 --- /dev/null +++ b/src/rezplugins/package_order/sorted.py @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + +from rez.package_order import PackageOrder +from rez.version._version import _ReversedComparable + + +class SortedOrder(PackageOrder): + """An orderer that sorts based on :attr:`Package.version `. + """ + name = "sorted" + + def __init__(self, descending, packages=None): + super().__init__(packages) + self.descending = descending + + def sort_key_implementation(self, package_name, version): + # Note that the name "descending" can be slightly confusing - it + # indicates that the final ordering this Order gives should be + # version descending (ie, the default) - however, the sort_key itself + # returns its results in "normal" ascending order (because it needs to + # be used "alongside" normally-sorted objects like versions). + # when the key is passed to sort(), though, it is always invoked with + # reverse=True... + if self.descending: + return version + else: + return _ReversedComparable(version) + + def __str__(self): + return str(self.descending) + + def __eq__(self, other): + return ( + type(self) is type(other) + and self.descending == other.descending + ) + + def to_pod(self): + """ + Example (in yaml): + + .. code-block:: yaml + + type: sorted + descending: true + packages: ["foo"] + """ + return { + "descending": self.descending, + "packages": self.packages, + } + + @classmethod + def from_pod(cls, data): + return cls( + data["descending"], + packages=data.get("packages"), + ) + + +def register_plugin(): + return SortedOrder diff --git a/src/rezplugins/package_order/version_split.py b/src/rezplugins/package_order/version_split.py new file mode 100644 index 000000000..c465a9c12 --- /dev/null +++ b/src/rezplugins/package_order/version_split.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + +from rez.package_order import PackageOrder +from rez.version import Version + + +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, packages=None): + """Create a reorderer. + + Args: + first_version (Version): Start with versions <= this value. + """ + super().__init__(packages) + self.first_version = first_version + + def sort_key_implementation(self, package_name, version): + priority_key = 1 if version <= self.first_version else 0 + return priority_key, version + + def __str__(self): + return str(self.first_version) + + def __eq__(self, other): + return ( + type(self) is type(other) + and self.first_version == other.first_version + ) + + def to_pod(self): + """ + Example (in yaml): + + .. code-block:: yaml + + type: version_split + first_version: "3.0.0" + packages: ["foo"] + """ + return dict( + first_version=str(self.first_version), + packages=self.packages, + ) + + @classmethod + def from_pod(cls, data): + return cls( + Version(data["first_version"]), + packages=data.get("packages"), + ) + + +def register_plugin(): + return VersionSplitPackageOrder From 6eee027dcfbc957cdfe340e7342b8a7204335fee Mon Sep 17 00:00:00 2001 From: Robert Minsk Date: Tue, 2 Jul 2024 11:20:49 -0700 Subject: [PATCH 2/4] format: Add newline at end of file to fix lint error Signed-off-by: Robert Minsk --- src/rezplugins/package_order/per_family.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rezplugins/package_order/per_family.py b/src/rezplugins/package_order/per_family.py index 404705081..fee885fd3 100644 --- a/src/rezplugins/package_order/per_family.py +++ b/src/rezplugins/package_order/per_family.py @@ -121,4 +121,5 @@ def from_pod(cls, data): def register_plugin(): - return PerFamilyOrder \ No newline at end of file + return PerFamilyOrder + From 150a3b14bd8b0594cae97e0f3128beaa3e93b38f Mon Sep 17 00:00:00 2001 From: Robert Minsk Date: Tue, 2 Jul 2024 11:23:09 -0700 Subject: [PATCH 3/4] format: fix blank lines at end of files Signed-off-by: Robert Minsk --- src/rezplugins/package_order/per_family.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rezplugins/package_order/per_family.py b/src/rezplugins/package_order/per_family.py index fee885fd3..75c2550be 100644 --- a/src/rezplugins/package_order/per_family.py +++ b/src/rezplugins/package_order/per_family.py @@ -122,4 +122,3 @@ def from_pod(cls, data): def register_plugin(): return PerFamilyOrder - From 09c12c9d1384ad3b835d278cd655c8d1a4e6e0d6 Mon Sep 17 00:00:00 2001 From: Robert Minsk Date: Tue, 2 Jul 2024 11:40:21 -0700 Subject: [PATCH 4/4] Signed-off-by: Robert Minsk format: add blank line after license comment --- src/rezplugins/package_order/no_order.py | 1 + src/rezplugins/package_order/per_family.py | 1 + src/rezplugins/package_order/soft_timestamp.py | 1 + src/rezplugins/package_order/sorted.py | 1 + src/rezplugins/package_order/version_split.py | 1 + 5 files changed, 5 insertions(+) diff --git a/src/rezplugins/package_order/no_order.py b/src/rezplugins/package_order/no_order.py index 01b755b0a..517c608dd 100644 --- a/src/rezplugins/package_order/no_order.py +++ b/src/rezplugins/package_order/no_order.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the Rez Project + from rez.package_order import PackageOrder diff --git a/src/rezplugins/package_order/per_family.py b/src/rezplugins/package_order/per_family.py index 75c2550be..2bfd8dd3e 100644 --- a/src/rezplugins/package_order/per_family.py +++ b/src/rezplugins/package_order/per_family.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the Rez Project + from rez.package_order import PackageOrder, to_pod, from_pod diff --git a/src/rezplugins/package_order/soft_timestamp.py b/src/rezplugins/package_order/soft_timestamp.py index 5d37da0d2..5e1fa4a51 100644 --- a/src/rezplugins/package_order/soft_timestamp.py +++ b/src/rezplugins/package_order/soft_timestamp.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the Rez Project + from rez.package_order import PackageOrder from rez.packages import iter_packages from rez.version._version import _ReversedComparable diff --git a/src/rezplugins/package_order/sorted.py b/src/rezplugins/package_order/sorted.py index 0ae9ab8f1..105b41568 100644 --- a/src/rezplugins/package_order/sorted.py +++ b/src/rezplugins/package_order/sorted.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the Rez Project + from rez.package_order import PackageOrder from rez.version._version import _ReversedComparable diff --git a/src/rezplugins/package_order/version_split.py b/src/rezplugins/package_order/version_split.py index c465a9c12..bb9a58cae 100644 --- a/src/rezplugins/package_order/version_split.py +++ b/src/rezplugins/package_order/version_split.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright Contributors to the Rez Project + from rez.package_order import PackageOrder from rez.version import Version