Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ __pycache__
/data_set/benchmark_packages.json
/data_set/benchmark_requests.json
/data_set/benchmark_expected.json

# maturin develop drops the compiled extension into the python source tree
crates/rer-python/python/pyrer/_native*.so
crates/rer-python/python/pyrer/_native*.pyd
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,35 @@ page.

## [Unreleased]

### Added

- **Package-orderer plugin SDK.** `pyrer` now exposes
`pyrer.PackageOrderer` (an SDK base class) and
`pyrer.register_orderer()` (an explicit registry), mirroring rez's
own orderer model. A studio subclasses `PackageOrderer`, implements
`order(family, versions) -> list[str]` (versions reordered
most-preferred-first), registers it, and selects it via
`pyrer.solve(..., package_orderer="<name>")` — by registered name or
by passing an instance. This lets a host override `rer`'s default
highest-version preference to match a custom rez orderer (e.g. a
PEP 440 orderer), the root cause of the version-selection divergence
in #96. The orderer is a preference function — it never changes
whether a solve succeeds; a misbehaving orderer (omitted/extra
versions) is handled defensively; a raising `order()` surfaces as
`status="error"`. On the Rust side: new `FamilyOrderer` callback
type, `SolverContext::with_package_order` builder,
`SolverContext.package_order` field, and a 5th `package_order`
parameter on `Solver::new_with_options`.

### Changed

- **`pyrer` is now a mixed Rust+Python package.** The compiled PyO3
extension moved to the `pyrer._native` submodule; a pure-Python
`pyrer` package wraps it and hosts the plugin SDK. `import pyrer`
and every existing symbol (`solve`, `PackageData`, `SolveResult`,
`parse_static_package_py`, …) are unchanged for callers — the
restructure is transparent. `pyrer.__version__` is now available.

## [1.0.0-rc.3] — 2026-05-19

First release candidate of the 1.0 line. Closes the integration
Expand Down
8 changes: 5 additions & 3 deletions crates/rer-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ license = "MIT"
publish = false

[lib]
# The Python import name — `rer-python` ships to PyPI as `pyrer`, so
# `import pyrer` in Python loads this cdylib.
name = "pyrer"
# `pyrer` is a mixed Rust+Python package: this cdylib is the compiled
# extension `pyrer._native`, and the pure-Python `python/pyrer/` package
# wraps it and hosts the plugin SDK. `import pyrer` loads the Python
# package; it re-exports from `pyrer._native`.
name = "_native"
crate-type = ["cdylib", "lib"]

# abi3-py39 builds a single stable-ABI wheel that works on Python 3.9+,
Expand Down
6 changes: 6 additions & 0 deletions crates/rer-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ Repository = "https://github.com/doubleailes/rer"

[tool.maturin]
features = ["pyo3/extension-module"]
# Mixed Rust+Python layout: the compiled extension is built as the
# `pyrer._native` submodule, and the pure-Python package under
# `python/pyrer/` (which re-exports `_native` and hosts the plugin SDK)
# is the `pyrer` users import.
python-source = "python"
module-name = "pyrer._native"
105 changes: 105 additions & 0 deletions crates/rer-python/python/pyrer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""pyrer — rer ("Rez En Rust"), a rez-compatible package resolver.

``pyrer`` is a mixed Rust+Python package: the compiled solver core lives in
the :mod:`pyrer._native` extension; this module re-exports it and adds the
package-orderer plugin SDK (:class:`~pyrer.orderer.PackageOrderer`,
:func:`~pyrer.orderer.register_orderer`).

The public surface is everything in ``__all__``. Do not import
``pyrer._native`` directly — it is an implementation detail.
"""
from __future__ import annotations

from pyrer._native import (
PackageData,
ResolvedVariant,
SolveResult,
parse_static_package_py,
parse_static_packages_py,
)
from pyrer._native import solve as _native_solve
from pyrer.orderer import PackageOrderer, _orderers, register_orderer

__all__ = [
"solve",
"PackageData",
"ResolvedVariant",
"SolveResult",
"parse_static_package_py",
"parse_static_packages_py",
"PackageOrderer",
"register_orderer",
]

try:
from importlib.metadata import version as _pkg_version

__version__ = _pkg_version("pyrer")
except Exception: # pragma: no cover - version metadata is best-effort
__version__ = "unknown"


def solve(
package_requests,
packages=None,
/,
*,
load_family=None,
variant_select_mode="version_priority",
package_orderer=None,
):
"""Resolve ``package_requests`` against a package repository.

Args:
package_requests: rez-style requirement strings, e.g.
``["python-3", "maya-2024"]``.
packages: a ``list[PackageData]`` (the eager repository), or
``None`` when discovery is fully driven by ``load_family``.
load_family: optional ``Callable`` invoked on demand the first
time the solver needs a family it has not seen. See the rez
integration docs for the callback contract.
variant_select_mode: ``"version_priority"`` (rez's default) or
``"intersection_priority"``.
package_orderer: overrides the per-family version preference. Pass
the registered **name** of a :class:`PackageOrderer` (a
``str``), a :class:`PackageOrderer` **instance** directly, or
``None`` for the default (highest version first). Register an
orderer with :func:`register_orderer` before selecting it by
name.

Returns:
A :class:`SolveResult`. Failures and bad input are reported via
``result.status``, never as a Python exception.

Raises:
ValueError: if ``package_orderer`` is a name with no registered
orderer.
TypeError: if ``package_orderer`` is not a str / PackageOrderer /
None, or ``packages`` is the wrong type.
"""
order_fn = None
if package_orderer is not None:
if isinstance(package_orderer, str):
inst = _orderers.get(package_orderer)
if inst is None:
raise ValueError(
f"no package orderer registered as {package_orderer!r} — "
f"register one with pyrer.register_orderer()"
)
elif isinstance(package_orderer, PackageOrderer):
inst = package_orderer
else:
raise TypeError(
"package_orderer must be a str, a PackageOrderer, or None"
)
# Bound method (family, versions) -> reordered versions; this is
# the plain callable the Rust core consumes.
order_fn = inst.order

return _native_solve(
package_requests,
packages,
load_family=load_family,
variant_select_mode=variant_select_mode,
package_order=order_fn,
)
92 changes: 92 additions & 0 deletions crates/rer-python/python/pyrer/orderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Package-orderer plugin SDK for ``pyrer``.

A *package orderer* decides, per family, which version the solver should
prefer. ``pyrer``'s default is rez's ``SortedOrder(descending=True)`` —
highest version first, using rez's native alphanumeric-token comparison.

To override that, subclass :class:`PackageOrderer`, implement
:meth:`PackageOrderer.order`, register the class with
:func:`register_orderer`, and select it on
``pyrer.solve(..., package_orderer="<name>")``::

import pyrer

class Pep440Orderer(pyrer.PackageOrderer):
name = "pep440"

def order(self, family, versions):
# sort `versions` however you like, most-preferred first
return sorted(versions, key=_pep440_key, reverse=True)

pyrer.register_orderer(Pep440Orderer)
result = pyrer.solve(requests, packages, package_orderer="pep440")

This mirrors rez's own orderer model — an SDK base class plus an explicit
registry — without rez's heavyweight plugin-manager discovery.
"""
from __future__ import annotations

from typing import Dict, List, Union

__all__ = ["PackageOrderer", "register_orderer"]


class PackageOrderer:
"""Base class for a ``pyrer`` package-orderer plugin.

Subclass it, set the class attribute :attr:`name`, and implement
:meth:`order`. Register the subclass (or an instance) with
:func:`register_orderer`, then select it by name on
``pyrer.solve(..., package_orderer="<name>")``.
"""

#: Registry key. A subclass **must** set this to a non-empty string.
name: str = ""

def order(self, family: str, versions: List[str]) -> List[str]:
"""Return ``versions`` reordered, most-preferred-first.

``family`` is the package family name; ``versions`` is every
candidate version string the solver currently has for that family.

The return value should be a permutation of ``versions``. ``pyrer``
is defensive about a misbehaving orderer: a version omitted from the
result sinks to the bottom (least preferred); a version in the
result that was not in ``versions`` is ignored. The orderer is a
*preference* function — it never changes whether a solve succeeds,
only which solution is found first.

Raising from this method propagates as a ``solve()`` result with
``status == "error"`` — no exception escapes ``pyrer.solve``.
"""
raise NotImplementedError(
f"{type(self).__name__} must implement order()"
)


# Registry: orderer name -> PackageOrderer instance.
_orderers: Dict[str, "PackageOrderer"] = {}


def register_orderer(orderer: Union["PackageOrderer", type]) -> None:
"""Register a :class:`PackageOrderer` so it can be selected by name.

Accepts either an instance or a :class:`PackageOrderer` *subclass*
(instantiated with no arguments). The orderer's :attr:`~PackageOrderer.name`
is the registry key; registering a second orderer under the same name
replaces the first.

Raises:
TypeError: if `orderer` is not a `PackageOrderer` subclass/instance.
ValueError: if the orderer's `name` is empty.
"""
inst = orderer() if isinstance(orderer, type) else orderer
if not isinstance(inst, PackageOrderer):
raise TypeError(
"register_orderer expects a PackageOrderer subclass or instance"
)
if not getattr(inst, "name", ""):
raise ValueError(
"a PackageOrderer must set a non-empty class attribute `name`"
)
_orderers[inst.name] = inst
Loading
Loading