Skip to content

feat(python,resolver): package-orderer plugin SDK#100

Open
doubleailes wants to merge 1 commit into
mainfrom
feat/package-orderer-plugin
Open

feat(python,resolver): package-orderer plugin SDK#100
doubleailes wants to merge 1 commit into
mainfrom
feat/package-orderer-plugin

Conversation

@doubleailes
Copy link
Copy Markdown
Owner

Summary

Adds a package-orderer plugin SDK to pyrer, so a host can override
which version of a family the solver prefers. rer previously hardcoded
rez's default SortedOrder(descending=True); a studio running a custom rez
orderer (e.g. Fortiche's PEP 440 Pep440Orderer) therefore diverged from
rer on version selection — the root of issue #96.

The design mirrors rez's own orderer model — an SDK base class + an
explicit registry — not rez's heavyweight plugin_managers discovery (which
orderers don't use either).

Python SDK

pyrer is now a mixed Rust+Python package: the compiled extension moved
to the pyrer._native submodule; a pure-Python pyrer package wraps it and
hosts the SDK. The restructure is transparentimport pyrer and every
existing symbol are unchanged for callers.

```python
import pyrer

class Pep440Orderer(pyrer.PackageOrderer):
name = "pep440"
def order(self, family, versions):
return sorted(versions, key=_pep440_key, reverse=True)

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

package_orderer accepts a registered name, a PackageOrderer instance, or
None (default). An orderer is a preference function — never changes
whether a solve succeeds. Misbehaving orderers are handled defensively;
a raising order()status="error", no exception escapes pyrer.solve.

Rust core

The one solver-visible version-order site, PackageVariantSlice::sort_versions(),
now keys on a per-version order_rank instead of version.cmp descending.
order_rank is computed once per family in PackageVariantList::new — the
no-orderer branch reproduces version-descending bit-identically. New
FamilyOrderer callback type, SolverContext.package_order field,
Solver::new_with_options 5th param.

Verification

Check Result
cargo test --lib 123/123 (rer-resolver 55 incl. 9 new, rer-package 34, rer-version 34)
pytest tests/ 121/121 (111 existing transparent through the restructure + 10 new)
188-case strict rez differential 188/188 — default ordering unchanged
cargo clippy clean (only pre-existing type_complexity warnings in #92 test code)
maturin develop builds + installs the mixed package

Test plan

  • Rust unit tests for build_rank_map + orderer-driven solves
  • Python tests: register/select, instance, error cases, default unchanged
  • All 111 pre-existing tests pass through the mixed-package restructure
  • 188/188 differential with no orderer
  • Port Fortiche's Pep440Orderer to a pyrer.PackageOrderer subclass and confirm resolves match Fortiche's rez via scripts/compare_resolves.py

Files

crates/rer-resolver/FamilyOrderer type, SolverContext field + manual Debug + builder, order_rank + build_rank_map + sort_versions, new_with_options 5th param, 9 unit tests.
crates/rer-python/[lib] name = "_native", #[pymodule] _native, make_orderer, package_order kwarg; new python/pyrer/{__init__.py,orderer.py}; pyproject.toml maturin mixed layout.
tests/test_rich_api.py — 10 new tests. docs/, CHANGELOG.md, .gitignore.

🤖 Generated with Claude Code

`rer` hardcoded rez's default `SortedOrder(descending=True)` — the
solver always preferred the highest version of a family, by rez's
native alphanumeric-token comparison. A studio running a custom rez
orderer (e.g. Fortiche's PEP 440 `Pep440Orderer`) therefore picked
different versions than `rer` — the root of the version-selection
divergence reported in #96.

This adds a plugin SDK so a host can override the version preference,
mirroring rez's own orderer model (an SDK base class + an explicit
registry — not rez's heavyweight plugin-manager discovery, which
orderers don't use either).

## Python SDK

`pyrer` is now a mixed Rust+Python package: the compiled extension
moved to the `pyrer._native` submodule; a pure-Python `pyrer` package
wraps it and hosts the SDK. The restructure is transparent —
`import pyrer` and every existing symbol are unchanged for callers;
`pyrer.__version__` is now available.

- `pyrer.PackageOrderer` — subclass it, set `name`, implement
  `order(family, versions) -> list[str]` (versions reordered
  most-preferred-first).
- `pyrer.register_orderer(MyOrderer)` — register a subclass or
  instance under its `name`.
- `pyrer.solve(..., package_orderer="<name>")` — select by registered
  name, or pass a `PackageOrderer` instance, or `None` for the
  default.

The orderer is a *preference* function: it changes which solution is
found, never whether a solve succeeds. A misbehaving orderer is
handled defensively — a version omitted from the result sinks to the
bottom, a version not in the input is ignored. A raising `order()`
surfaces as `status="error"`; no exception escapes `pyrer.solve`.

## Rust core

The single solver-visible version-order site, `PackageVariantSlice::
sort_versions()`, now keys on a per-version `order_rank` (0 = most
preferred) instead of `version.cmp` descending. `order_rank` is
computed once per family in `PackageVariantList::new`:

- No orderer → rank = version-descending position (bit-identical to
  the previous hardcoded behaviour).
- Orderer set → the `FamilyOrderer` callback is invoked once with the
  family's version strings; `build_rank_map` turns its output into
  ranks, defensively (omitted versions sink, unknowns ignored, never
  panics).

New `FamilyOrderer` callback type and `SolverContext.package_order`
field (with a `with_package_order` builder and a manual `Debug` impl
— `Rc<dyn Fn>` isn't `Debug`). `Solver::new_with_options` gains a 5th
`package_order` parameter; `new` / `new_with_cache` pass `None`.

The orderer is re-invoked on every `PackageVariantList` (re)build,
including the issue #92 widened-range reload path — ranks are always
recomputed wholesale, no stale ranks survive.

## Tests

- Rust: 5 `build_rank_map` tests (permutation, omitted-sink,
  unknown-ignored, duplicate-first, empty-output) + 4 solver tests
  (reverse-preference picks lowest, pin-a-version, partial-output
  no-panic, no-orderer-control picks highest). 55/55 resolver unit
  tests pass.
- Python: 10 new tests — register+select-by-name, select-by-instance,
  unknown name → ValueError, no-`name` → ValueError, non-orderer →
  TypeError, `order()` raises → status="error", partial output,
  `None` is default. 121/121 `pytest tests/` pass.
- Strict 188-case rez differential: 188/188 with no orderer set —
  default ordering unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant