feat(pypi): derive Metaflow @pypi environments from uv.lock - #32
Open
Abhishek Patil (abhishek-pattern) wants to merge 8 commits into
Open
feat(pypi): derive Metaflow @pypi environments from uv.lock#32Abhishek Patil (abhishek-pattern) wants to merge 8 commits into
Abhishek Patil (abhishek-pattern) wants to merge 8 commits into
Conversation
Adds get_packages_from_uv_lock and get_packages_from_pyproject so a flow's
@pypi_base cannot drift from what the project actually installs.
Prefer the uv.lock variant: it emits resolved versions and exact commit SHAs
for git dependencies, so a bake is reproducible instead of tracking whatever
main points at today.
Emits the root project's direct dependencies only, not the transitive closure
-- lock entries are marker-gated per platform, so pinning the whole graph
would break a bake anywhere but the machine that resolved it. A name locked at
two versions behind different resolution markers is left unpinned for the same
reason. Returns {} when the file is not found, which keeps remote tasks working:
they re-import the flow module in a container holding only .py files, by which
point the image is already baked from the client-resolved map.
Project files are located by walking up from the launch directory, since an
installed package cannot resolve the flow repo from __file__; pass project_root
to override.
Declares packaging as a direct dependency -- get_packages_from_pyproject uses
it for PEP 508 parsing and it was only present transitively.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Updated README.md to reflect changes in the decorators used for PyPI package management. - Modified pypi_packages.md to describe the new `uv_pypi_base` and `uv_pypi` decorators, replacing the previous `get_packages_from_pyproject` and `get_packages_from_uv_lock` functions. - Refactored the __init__.py file to import the new decorators. - Updated pypi_packages.py to implement the new decorators and their functionality. - Adjusted unit tests to validate the new decorators and their behavior, ensuring compatibility with existing functionality.
…ironment uv.lock is a universal resolution: it records the answer for every python version and platform in range at once, each tagged with the marker it applies to. @pypi takes a flat name -> version map with nowhere to put a marker, so the markers have to be resolved when the decorator is applied rather than passed on. A dependency no single version covers appears once per marker region, each naming its own version, so the lock itself says which one applies: { name = "pandas", version = "2.3.3", marker = "python_full_version < '3.11'" } { name = "pandas", version = "3.0.5", marker = "python_full_version >= '3.11'" } Previously any name locked more than once was emitted unpinned; now the resolved python version selects one, and unpinned is only the fallback for entries with nothing to tell them apart. The python version is therefore resolved first and handed down, so the two halves of the environment cannot disagree. The same marker evaluation drops a dependency gated to another platform -- a darwin-only package was previously pinned unconditionally and would fail a linux bake. Markers evaluate against linux, which is what metaflow builds for a remote task; the packages helper takes sys_platform for a local-only flow. Renames the groups parameter to dependency_groups. At a decorator call site, among @resources and @step, a bare "groups" reads as something about the flow; dependency_groups is the PEP 735 term and unambiguous where it is typed. Reworks the decorator tests around a spy on metaflow's pypi_base: the contract under test is "call metaflow with this environment", and metaflow 2.19 moved where the decorator is recorded (_flow_decorators -> _flow_state), breaking tests that asserted on that internal. One test still exercises the real decorator end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-locks against python 3.10 and raises the polars constraint to >=1.36.2. Note for reviewers: the interpreter floor is what decides several packages in the universal lock -- pandas resolves to 2.3.3 below 3.11 and 3.0.5 at or above it -- so this commit changes what flows using @uv_pypi_base bake. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot started reviewing on behalf of
Abhishek Patil (abhishek-pattern)
August 12, 2026 07:30
View session
There was a problem hiding this comment.
Pull request overview
Introduces Metaflow decorators that derive @pypi_base / @pypi configuration (Python version + direct dependency pins) from the repository’s uv.lock, aiming to prevent environment drift between local uv sync and Metaflow-baked images.
Changes:
- Added
uv_pypi_baseanduv_pypidecorators that computepython=andpackages=from.python-version/requires-pythonanduv.lock. - Implemented uv.lock parsing + PEP 508 marker evaluation to emit a flat
name -> version/direct-referencemap for Metaflow. - Added docs and unit tests covering version selection, marker handling, git SHA pinning, dependency groups, and project-root discovery; updated runtime deps (
packaging) and bumped default Python pin to 3.10.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/unit_tests/metaflow/test__pypi_packages.py |
New unit tests exercising uv.lock parsing and decorator behavior. |
src/ds_platform_utils/metaflow/pypi_packages.py |
New implementation of uv.lock → Metaflow @pypi(_base) environment derivation and wrappers. |
src/ds_platform_utils/metaflow/__init__.py |
Exports uv_pypi and uv_pypi_base as the public API. |
README.md |
Adds docs link for the new decorators. |
pyproject.toml |
Adds runtime dependency on packaging and bumps the polars lower bound. |
docs/metaflow/pypi_packages.md |
New documentation page for uv_pypi_base / uv_pypi. |
.python-version |
Updates the repo’s pinned Python version to 3.10. |
Suppressed comments (1)
src/ds_platform_utils/metaflow/pypi_packages.py:295
_split_lock_packagesoverwritesrootevery time it sees any local-source package (virtual/editable/directory). If a lock contains additional local directory/editable dependencies, the last one wins and the derived dependency list can come from the wrong project entry. Avoid overwritingrootonce it has been found.
if any(key in source for key in _LOCAL_SOURCE_KEYS):
# the repo itself -- it is the thing depending on everything else, not a dep.
root = package
continue
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ```python | ||
| from metaflow import FlowSpec, pypi_base, step | ||
|
|
||
| from ds_platform_utils.metaflow import _get_pypi_kwargs |
Comment on lines
+47
to
+51
| _PLATFORM_MARKERS = { | ||
| "linux": {"platform_system": "Linux", "os_name": "posix", "platform_machine": "x86_64"}, | ||
| "darwin": {"platform_system": "Darwin", "os_name": "posix", "platform_machine": "arm64"}, | ||
| "win32": {"platform_system": "Windows", "os_name": "nt", "platform_machine": "AMD64"}, | ||
| } |
Comment on lines
+271
to
+273
| f"uv.lock source for {name!r} cannot be installed by @pypi: {source!r}. Only git and url sources " | ||
| "are fetchable from a remote task -- path and workspace sources are local-only." | ||
| ) |
A flow now records the environment it actually built instead of leaving you to
re-derive it from the lockfile:
@uv_pypi_base on MyFlow: python 3.10, 10 package(s) from uv.lock
jinja2 3.1.6
outerbounds 0.12.39
polars (unpinned)
snowflake-connector-python 4.7.2
Uses print rather than a logger, matching the rest of the package -- a module
logger would be silent by default with no logging configured.
Names are sorted and versions column-aligned so two runs compare by eye. Three
things the format states outright that a raw dict would not: "(unpinned)" for a
deliberately unresolved entry, since an empty string reads as a missing value; a
[environment disabled] header flag, so it is clear the listing will not be
installed; and the decorated flow or step by name, which matters once several
steps carry their own @uv_pypi.
Prints nothing when no lockfile is found. That is the remote task re-importing
the flow module inside an already-baked image -- there is nothing resolved to
report, and printing would add noise to every task's logs.
Note this fires at import, so it appears on any command that loads the flow
module, not just run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Flows depending on a private github.com/patterninc repo need Outerbounds to hold git credentials, because Fast Bakery runs pip install inside the bake -- local git credentials never reach it. Without the integration a bake fails at image build time, before any step runs. Records the current state as of 2026-08-12: the private-repo-access integration exists in the default perimeter and covers the org root https://github.com/patterninc, so every repo under it is already matched and a new private dependency needs no integration change. The prod perimeter has no GIT_PYPI_REPOSITORY integration at all, so a flow deployed there will fail to bake until one is created -- perimeters are isolated and grant nothing to each other. Covers setup per perimeter, verification, adding further git hosts (update replaces the URL list rather than appending), and token rotation. Commands read the token from an environment variable so it stays out of shell history; no token values appear in the doc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What
Adds
@uv_pypi_base/@uv_pypi— Metaflow's@pypi_base/@pypiwith the Python version and packages filled in from the repo's ownuv.lock, so a flow's environment cannot drift from whatuv syncinstalls.No
python=, nopackages=, nothing to keep in sync. Both decorators work bare or called:@uv_pypi_base(dependency_groups=["dev"]),python=,project_root=,disabled=.Why versions come from
uv.lockpyproject.tomlholds constraints, not versions. A>=bound, or a git dependency pinned torev = "main", means two bakes a week apart can produce different images.uv.lockrecords what uv actually resolved, including the commit SHA for git dependencies, so a bake is reproducible.Resolving a universal lockfile
uv.lockis a universal resolution — every Python version and platform in range at once, each tagged with a marker.@pypitakes a flat{name: version}map with nowhere to put a marker, so markers are resolved at decoration time:sys_platform == 'darwin'on a Linux bake)"", left for@pypiConcretely, from one unchanged lockfile:
The Python version is resolved first and handed down, so the interpreter and the packages can't disagree. Markers evaluate against Linux, which is what Metaflow builds for a remote task.
Only the root project's direct dependencies are emitted, not the transitive closure —
@pypiresolves transitives itself, and per-platform wheel availability is its job.Where the Python version comes from
.python-version— the interpreter uv pinnedrequires-pythoninuv.lock, elsepyproject.toml— a range, so its floorAn installed package can't find the flow repo from
__file__, so project files are located by walking up from the launch directory;project_root=overrides.Reviewer notes
pandas 2.3.3over3.0.5. Worth a deliberate look —requires-pythonis a published constraint, so raising it later would stop 3.10 consumers installing this library.packagingis now a declared runtime dependency; it was only present transitively and is used for PEP 508 marker and specifier parsing.uv_pypianduv_pypi_baseare exported. The helpers that build the map are private.Optional/Unionstyle rather than mixing. Either declarerequires-pythonand modernize in one dedicated pass, or pintarget-versionin[tool.ruff].Testing
24 unit tests in
tests/unit_tests/metaflow/test__pypi_packages.py; 80 pass overall. Coverage includes version pinning, git SHA references, marker resolution by Python version, platform-gated dependencies being dropped, the unpinned fallback, dependency groups, the upward project-root search, and the empty map that lets remote tasks re-import the flow module.Decorator tests assert against a spy on Metaflow's
pypi_base— the contract is "call Metaflow with this environment", and Metaflow 2.19 already moved where decorators are recorded. One test exercises the real decorator end to end.🤖 Generated with Claude Code