Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ page.

### Added

- **`PackageData.from_strings(name, version, requires=None, variants=None)`** —
classmethod constructor for raw-string callers, symmetric with
`from_rez(pkg)`. Skips rez's `AttributeForwardMeta` chain, the
`Requirement` parse, and the `str(Requirement)` round-trip — the
latter being a measurable fraction of integration overhead on
rez-shim hot paths (per-package, every package, every resolve).
Functionally equivalent to the four-arg constructor; the
classmethod form exists so callers wiring `pkg.resource.data`
through pyrer have a named, documented contract to reach for.
Falls back to `from_rez` for `@early` / `@late`-bound attributes.
Closes #88.
- **`load_family` callback** on `pyrer.solve()` — opt-in lazy package
discovery: pass `load_family: Callable[[str], list[PackageData]]` and the
solver calls it on demand the first time it needs a family it hasn't seen.
Expand Down
62 changes: 62 additions & 0 deletions crates/rer-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ impl PackageData {
/// `pyrer` so every integration site doesn't have to write the same
/// extraction loop. `pyrer` itself does not import rez; this method is
/// duck-typed and works against any object with the four attributes.
///
/// **Faster alternative for raw-data callers:** if you already have the
/// raw strings (typically from `pkg.resource.data` on a rez `Package`,
/// which stores `requires` / `variants` as raw `list[str]` /
/// `list[list[str]]` in the common non-late-bound case), prefer
/// [`Self::from_strings`]. It skips the per-attribute
/// `AttributeForwardMeta` lookup, the late-bound wrapping, the
/// `Requirement` parse, and the `str(Requirement)` round-trip — none
/// of which produce a different `PackageData` for the common case,
/// but all of which take real time per package.
#[classmethod]
fn from_rez(_cls: &Bound<'_, PyType>, pkg: &Bound<'_, PyAny>) -> PyResult<Self> {
let name: String = pkg.getattr("name")?.extract()?;
Expand All @@ -98,6 +108,58 @@ impl PackageData {
variants,
})
}

/// Build a [`PackageData`] from raw strings, skipping any rez
/// wrapper-object resolution. Use this when you already have raw
/// `(name, version, requires, variants)` data — typically pulled from
/// `pkg.resource.data` on a rez `Package`:
///
/// ```python
/// data = pkg.resource.data
/// pd = pyrer.PackageData.from_strings(
/// data["name"],
/// data["version"],
/// data.get("requires"), # may be None / list[str]
/// data.get("variants"), # may be None / list[list[str]]
/// )
/// ```
///
/// Faster than [`Self::from_rez`] on rez-integration hot paths because
/// it does not trigger rez's `AttributeForwardMeta` per attribute, does
/// not parse each requirement string into a `Requirement` object, and
/// does not round-trip each `Requirement` back through `__str__`.
///
/// `requires` and `variants` accept `None` (interpreted as empty),
/// matching `dict.get(...)` ergonomics.
///
/// Functionally equivalent to the four-arg constructor
/// `PackageData(name, version, requires, variants)` — both take the
/// same fast PyO3 extraction path. The classmethod form exists to make
/// the fast path discoverable alongside [`Self::from_rez`] and to give
/// the contract a name in callers' code. Closes #88.
///
/// **Caveat — late-bound requirements:** for packages where rez stores
/// `requires` or `variants` as a `SourceCode` instance (`@early` /
/// `@late` binding), `pkg.resource.data["requires"]` is *not* a
/// `list[str]` and this method will raise. Fall back to
/// [`Self::from_rez`] for those packages — it walks rez's lazy
/// attribute path which evaluates the source code.
#[classmethod]
#[pyo3(signature = (name, version, requires=None, variants=None))]
fn from_strings(
_cls: &Bound<'_, PyType>,
name: String,
version: String,
requires: Option<Vec<String>>,
variants: Option<Vec<Vec<String>>>,
) -> Self {
PackageData {
name,
version,
requires: requires.unwrap_or_default(),
variants: variants.unwrap_or_default(),
}
}
}

/// Pull a flat list of requirement strings from a Python object that is
Expand Down
44 changes: 44 additions & 0 deletions docs/content/docs/getting-started/rez-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,50 @@ is duck-typed — `pyrer` itself does not import rez — so you can also
pass any object exposing the same four attributes (e.g. a test
fixture).

### Faster construction with `from_strings`

`from_rez(pkg)` triggers rez's `AttributeForwardMeta` chain on every
attribute and parses each requirement string into a `Requirement`
object only to immediately turn it back into a string. When you
already have the raw strings, prefer
`PackageData.from_strings(name, version, requires, variants)` —
it skips the wrapper round-trip entirely:

```python
def build_pyrer_packages_fast(package_paths):
for family in iter_package_families(paths=package_paths):
for pkg in family.iter_packages():
data = pkg.resource.data
# `data["requires"]` is a raw list[str] in the common
# (non-late-bound) case; fall back to from_rez otherwise.
if isinstance(data.get("requires", []), list) and \
isinstance(data.get("variants", []), list):
yield pyrer.PackageData.from_strings(
data["name"],
data["version"],
data.get("requires"),
data.get("variants"),
)
else:
# @early / @late bindings — let rez evaluate them.
yield pyrer.PackageData.from_rez(pkg)
```

The `from_strings` method:

- Skips the per-attribute `AttributeForwardMeta` lookup.
- Skips the `Requirement` parse (no `Version` / `VersionRange` AST
is built then discarded).
- Skips the `str(Requirement)` round-trip per requirement.
- Accepts `None` for `requires` / `variants` (matches
`dict.get(...)` ergonomics — no `or ()` boilerplate needed).

Functionally equivalent to the four-arg constructor; the
classmethod form exists so the contract has a name. **Always fall
back to `from_rez` for packages with `@early` or `@late` binding** —
in those cases `resource.data["requires"]` is a `SourceCode`
instance, not a `list[str]`, and `from_strings` will raise.

Two notes on this step:

- It is **eager** — every package on every path is loaded before the
Expand Down
Loading
Loading