Conversation
Populate user-requested specs into both lockfile formats so round-trips actually carry "what the user asked for" rather than just the full explicit install set. Source of intent is `conda-meta/history` via a new `conda_lockfiles.history.requested_specs_from_prefix` helper. We re-derive from history rather than trusting `env.requested_packages` because conda populates that with every installed package by default (`conda.models.environment.Environment.from_prefix(from_history=False)`, tracked upstream at conda/conda#15961). Storage per format: - **conda-lock-v1**: under `metadata.custom_metadata.requested_specs` as a JSON-encoded list of MatchSpec strings. The model field is widened to free-form `dict[str, str]` to match upstream conda-lock and CEP 37. Name choice follows CEP 32. - **rattler-lock-v6**: under `environments.<name>.requested-packages` as a per-platform map of MatchSpec strings. rattler silently ignores unknown fields on parse, so pixi `install --frozen` is unaffected. Any rattler-driven re-lock (`pixi add`, `pixi update`) will drop the annotation; this is advisory, not durable. Loaders populate `Environment.requested_packages` from these fields, filtering out specs whose package name is not in `explicit_packages` since `Environment.__post_init__` enforces that subset relationship.
Reverting part of the previous commit: we shouldn't drop the typed custom_metadata wrapper just because the wire format is dict[str, str]. Named Python-side fields for created_by and requested_specs are more useful for readers/writers than a raw dict, and extra="allow" on the BaseModel preserves any unknown keys that upstream consumers may have put there. The JSON-stringification of the requested_specs value is still required for interop with conda-lock (StrictModel on LockMeta.custom_metadata with dict[str, str]), but that constraint is worth revisiting at the CEP level: see conda/ceps#163.
|
@soapy1 Not sure if you have thoughts on this, but I remember talking to you about it.. |
| from conda.common.path import PathType | ||
|
|
||
|
|
||
| def requested_specs_from_prefix(prefix: PathType | None) -> list[str]: |
There was a problem hiding this comment.
What is the motivation for getting information from the history in the plugin? I would expect that the plugin should be as simple as possible, just converting Environments to their target file format and back. Then gathering information from history is something that core conda should do when required.
For example, consider a new lockfile plugin (eg. maybe contributed from the community). It would be nice for this plugin to also get the correct user requested specs without needing to copy this code + plug it into the right places.
| try: | ||
| ms = MatchSpec(spec_str) | ||
| except Exception: | ||
| continue |
There was a problem hiding this comment.
Does this error represent a spec_str that is an invalid matchspec? That seems like a pretty significant error. Probably worth warning the user in some way. Maybe like a log or warning. +1 to not throwing an error and exiting the program.
| explicit_names = {pkg.name for pkg in resolved_explicit} | ||
| for spec_str in platform_specs: | ||
| if not isinstance(spec_str, str): | ||
| continue |
There was a problem hiding this comment.
In which cases are these spec_str's not strings? Should the user be notified that their environment file has something wrong with it?
| # kebab-case convention for multi-word keys. rattler silently ignores | ||
| # unknown fields on read, but WILL drop them on any re-serialize | ||
| # (``pixi add``, ``pixi update``, ...), so treat this as advisory. | ||
| requested_packages: Annotated[ |
There was a problem hiding this comment.
Is it safe to be extending external lockfile specifications like this? I see in this case it is since rattler will silently ignore unknown fields. But will other specifications also do this?
| ), | ||
| explicit_packages=resolved_explicit, | ||
| external_packages=external_packages, | ||
| requested_packages=_requested_packages_from_metadata( |
There was a problem hiding this comment.
Is there extra work on the conda side that needs to be done so that these requested packages get represented in the history?
Closes #8.
Summary
Populates user-requested specs into both supported lockfile formats so exports/round-trips carry "what the user asked for", not just the full explicit install set.
Intent is sourced from
conda-meta/historyvia a newconda_lockfiles.history.requested_specs_from_prefixhelper. We re-derive from history rather than trustenv.requested_packagesbecause conda populates that field with every installed package by default (Environment.from_prefix(from_history=False), tracked upstream at conda/conda#15961).Storage
conda-lock-v1 —
metadata.custom_metadata.requested_specs, JSON-encoded list of MatchSpec strings:custom_metadatamodel to free-formdict[str, str]to match upstream conda-lock and CEP 37.requested_specsfollows CEP 32's terminology for MatchSpec-string lists; no namespacing since conda-lock has no convention and no CEP mandates it.LockMetaisStrictModel(extra fields rejected), butcustom_metadataitself is explicitly a user-extensibledict[str, str].rattler-lock-v6 —
environments.<name>.requested-packagesas a per-platform map:SolveOptionsuses#[serde(rename_all = "kebab-case")]).deny_unknown_fields, sopixi install --frozenkeeps working and ignores the extension.pixi add,pixi update) will drop the annotation. This is advisory, not durable. Documented inline.Loader round-trip
Both
CondaLockV1LoaderandRattlerLockV6Loaderdecode the fields and populateEnvironment.requested_packagesaslist[MatchSpec]. Specs whose package name isn't in the platform'sexplicit_packagesare dropped becauseEnvironment.__post_init__enforces the subset relationship and would otherwise raise.Test coverage
New
tests/test_requested_specs.pywith 7 tests covering the history helper, fixture contents, loader population for both formats, and malformed-payload tolerance. The existingsingle_packagefixtures (conda-lock.yml,pixi.lock) are updated to carry the new field, so the existingtest_export_to_conda_lock_v1[single-package]andtest_export_to_rattler_lock_v6[single-package]round-trip comparisons now also validate serialization.Full suite: 55 passed locally on
test-py313.References
from_history=Falsedefault: ReconsiderEnvironment.from_prefix(from_history=False)as the default conda#15961conda.history.History: https://github.com/conda/conda/blob/main/conda/history.pyEnvironment.from_prefix: https://github.com/conda/conda/blob/main/conda/models/environment.pyrequested_specsterminology): https://github.com/conda/ceps/blob/main/cep-0032.mdcustom_metadata): https://github.com/conda/ceps/blob/main/cep-0037.mdLockMeta: https://github.com/conda/conda-lock/blob/main/conda_lock/lockfile/v1/models.pydeny_unknown_fields): https://github.com/conda/rattler/blob/main/crates/rattler_lock/src/parse/deserialize.rs