feat: make conda-pypi-map additive and add pypi-conda-map build overrides#6333
Draft
tdejager wants to merge 10 commits into
Draft
feat: make conda-pypi-map additive and add pypi-conda-map build overrides#6333tdejager wants to merge 10 commits into
tdejager wants to merge 10 commits into
Conversation
…eplace modes, inline mappings and cache-ttl
- conda-pypi-map now accepts false (global disable), and per-channel
values can be a bare string, false, or a table with location,
inline mapping entries, mode = "extend"|"replace" and cache-ttl.
- BREAKING: bare location strings now use the additive extend mode
(overlay over the prefix.dev chain) instead of the exclusive
replace mode. The old behavior is available via mode = "replace".
- conda-pypi-map = {} is soft-deprecated in favor of false and emits
a deprecation warning.
- pixi_core wires the manifest entries into the per-channel mapping
configuration; inline keys are lowercased to match normalized conda
names, and cache-ttl is validated to require an http(s) location.
…{} disable to false
…tion and breaking-change notes
- Move all mapping/purl tests from solve_group_tests.rs into a new
conda_pypi_map_tests.rs integration module.
- Split CondaPypiMapEntry::Map into CondaPypiMapSpec with a dedicated
MappingLocationSpec { location, cache_ttl } so the TTL is structurally
tied to the location source it applies to.
- Clarify in the Disabled doc comments that the offline conda-forge
verbatim fallback still applies when lookups are disabled.
- Deduplicate the offline help text into a shared MAPPING_OFFLINE_HELP
const used by both the prefix.dev and project-defined fetch errors, and
mention pointing at a custom mapping location (with cache-ttl) as an
escape hatch.
- Document why the TTL cache cannot reuse the http-cache middleware
(header-driven freshness, client-global max_ttl, no stale-on-error).
- Docs: add a parselmouth raw-URL pinning recipe (and a note that blob
URLs serve HTML).
- pixi_toml: add a custom_error(message, span) constructor and use it for the conda-pypi-map validation errors. - pixi_core: extract the conda-pypi-map manifest conversion out of workspace/mod.rs into a workspace::conda_pypi_map module with named, unit-testable functions (incl. the channel-membership validation). - pixi_core: classify mapping locations with rattler_lock::UrlOrPath instead of hand-rolled starts_with checks; file:// urls normalize to paths and non-http(s) remote schemes are rejected with a clear error. - pypi_mapping: make the per-record fallback policy explicit with a Fallback enum (PrefixThenVerbatim | Verbatim | None) instead of a mutable suppression flag. - pixi-build-python: dedupe the requirement version conversion into convert_requirement_version, shared by the user-map and service paths. - test: pin that a mapping for one channel no longer suppresses the verbatim fallback for records from other, unmapped channels (online).
- TTL cache: treat a future mtime (clock skew) as age zero instead of
making the cached copy invisible to the freshness check and the stale
fallback; write cache files atomically via tempfile + persist; unit
tests for the age computation.
- pypi-conda-map: an invalid conda name in an override now falls through
to the mapping service instead of silently dropping the dependency.
- Split the offline help text: failures fetching a user-configured
location now suggest checking the URL / adding cache-ttl instead of
the firewall-framed prefix.dev advice; clearer HTTP status error.
- Warn when a mapping location uses plain http://, since a tampered
mapping influences dependency resolution.
- Encode the manifest-mode to MappingMode conversion in a documented
convert_mode function (a From impl is impossible: neither crate
depends on the other, so the orphan rule forces it into pixi_core).
- Error wording: cache-ttl duration errors show example values;
cache-ttl-without-location message no longer implies location must be
a URL; {} deprecation help reworded; stale Disabled doc hedge fixed;
duplicated doc comment removed.
- Docs: warning box now also covers the verbatim-fallback scope change
for unmapped channels; cache-ttl docs state the no-cache hard-failure;
inline-mapping example no longer reuses 'pytorch' as both channel and
package name.
- New tests: mixed-case inline keys, cache-ttl on a local path rejected,
file:// table-form location works (pins UrlOrPath normalization),
Skip entries with markers, vacuous purls assertion fixed, unit tests
for parse_mapping_location/convert_entry; re-documented what the
fresh-cache TTL test actually pins (cache layout + no network).
- typos: reword 'mis-mapped' in the conda/PyPI concepts page. - basedpyright: no implicit string concatenation in the new schema/model.py field descriptions (schema output unchanged).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
We have a way to map conda packages to PyPI packages (used during the solve to figure out which conda packages already satisfy
pypi-dependencies) and the other way around inpixi-build-python. The problem was that the user could only replace the whole mapping: as soon as you setconda-pypi-mapfor a channel, you lost the entire default prefix.dev mapping for it and had to replicate thousands of entries just to fix one name. The reverse direction had no override at all, only an on/off toggle.The tricky part is that the mapping shapes are different (the default forward mapping is even keyed on repodata sha256 hashes), so this deliberately does not merge any data. Instead the user mapping is just consulted first, and on a miss we fall through to the default chain (hash → compressed → conda-forge verbatim). Additive becomes resolver layering instead of data merging.
Before, fixing one name meant hosting a full mapping file:
After:
Concretely this PR:
conda-pypi-mapentries per-channel configurable: a bare location string,false, or a table withlocation, inlinemappingentries (inline wins over the file),mode = "extend" | "replace"andcache-ttl.conda-pypi-map = falseas the canonical global disable.conda-pypi-map = {}still works but is soft-deprecated with a warning.cache-ttlfor URL locations: the fetched map is cached on disk and only re-fetched once it is older than the TTL. If the re-fetch fails we fall back to the stale copy with a warning, so solves keep working offline. This also makes it practical to pin the full parselmouth mapping from the raw GitHub URL (documented).pypi-conda-maptopixi-build-python: overrides consulted before the mapping service in both passes (project.dependencies→ run andbuild-system.requires→ host),falsedrops a dependency silently, per-key merge for target-specific config.mode = "replace",<channel> = falseorconda-pypi-map = false, so firewalled setups know how to opt out.Breaking behavior
This is breaking on purpose, in two ways:
conda-forge = "mapping.json"(bare string)conda-forge = { location = "mapping.json", mode = "replace" }B = falseif you really want B's lookups offconda-pypi-map = {}conda-pypi-map = falseThe second one I'm fairly convinced was accidental: the suppression was keyed on the global mode instead of the record's channel, so mapping one channel silently degraded purl coverage everywhere else.
How Has This Been Tested?
Automated tests for the whole behavior matrix: extend/replace/disabled per channel, inline-overrides-file, the cache-ttl fresh/expired/stale/no-cache paths (the offline ones are proven network-free with a blocking middleware), parse errors with snapshots, and the reverse-direction override/skip/marker/merge cases. The extend-miss fallthrough and the unmapped-channel-keeps-verbatim cases are online tests that I verified against the live mapping. Schema is regenerated and
pixi run test-schemapasses.I still want to user-test this end-to-end, therefore it's in draft.
User-test checklist
false:pixi lockwith manifest A, checknumpyhaspkg:pypi/my-renamed-numpy,boltonshas no purl, andpythonstill has its normal purl (proves extend falls through).boltonsstill gets a purl (old behavior: none). Switch the entry tomode = "replace"→boltonsloses it.pytablesgets the verbatimpkg:pypi/pytablesinstead of the realpkg:pypi/tables; remove thefalseline →pkg:pypi/tablescomes back. Also tryconda-pypi-map = {}and check the deprecation warning.cache-ttl+ parselmouth pin: manifest D → firstpixi lockfetches (file appears under<cache>/conda-pypi-mapping/project-defined/), deletepixi.lockand lock again with networking off → still works; setcache-ttl = "0s"with networking off → stale-copy warning, still works.conda-mapping.prefix.devand lock a plain manifest → the error suggests the escape hatches.http://location → warning about tampering shows.pixi-build-python: manifest E withPIXI_BUILD_BACKEND_OVERRIDE="pixi-build-python=/path/to/locally/built/pixi-build-python"→pixi build, check the rendered recipe haspytorchin run deps andmy-internal-helperis gone; add the linux-64 target table and check the per-key merge.pypi-conda-mapwithoutignore-pypi-mapping = false→ inert warning shows.Test manifests
A — inline override + explicit false (extend)
B — the breaking bare-string flip vs replace
C — global disable keeps the verbatim heuristic
pytablesis a nice probe because its real PyPI name istables: with lookups disabled the verbatim fallback yieldspkg:pypi/pytables, with the default mapping you getpkg:pypi/tables.D — pin the parselmouth mapping with cache-ttl
E — pixi-build-python overrides
PIXI_BUILD_BACKEND_OVERRIDE="pixi-build-python=$PWD/target/debug/pixi-build-python" pixi buildAI Disclosure
Tools: Claude Code with Fable 5
Checklist:
schema/model.py.