Expand uv workspace metadata with dependency information from the lock#18356
Expand uv workspace metadata with dependency information from the lock#18356
uv workspace metadata with dependency information from the lock#18356Conversation
|
Note that if the desugared "resolve" thing is too weird it's really easy for me to inline the virtual nodes back into the package nodes -- they are after all basically just an array of dependencies. My motivation for desugarring is because similar sugaring of nodes in |
|
Can you add what kinds of queries we users to be able to run against this graph? Such as "Give me all the wheels that would be installed on linux (to audit them)", or "What packages are outdated", etc. |
| kind: MetadataNodeKind, | ||
| } | ||
|
|
||
| type MetadataNodeIdFlat = String; |
There was a problem hiding this comment.
Can you add user-facing documentation that the format of this id is internal and not for parsing?
| "members": [ | ||
| { | ||
| "name": "albatross", | ||
| "path": "[TEMP_DIR]/workspace" | ||
| "path": "[TEMP_DIR]/workspace", |
There was a problem hiding this comment.
How do I know what the entrypoint is, just path equals workspace root?
| .await | ||
| { | ||
| Ok(lock) => print_lock_as_metadata(&workspace, &lock.into_lock(), printer), | ||
| Err(err @ ProjectError::LockMismatch(..)) => { |
There was a problem hiding this comment.
How is that different from forwarding the error?
| args.python, | ||
| args.install_mirrors, | ||
| args.settings, | ||
| client_builder.subcommand(vec!["lock".to_owned()]), |
There was a problem hiding this comment.
I mean it does a lock op, but is that the right string here?
| @@ -319,3 +839,2610 @@ fn workspace_metadata_no_project() { | |||
| " | |||
| ); | |||
| } | |||
|
|
|||
| /// Test packse (has optional-dependencies, dev-dependencies, and build-system) | |||
There was a problem hiding this comment.
That snapshot is over 2000 lines long, can we use something shorter that's easier to check?
|
|
||
| #[derive(Clone, Debug, serde::Serialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| enum MetadataWheelWireSource { |
There was a problem hiding this comment.
The "url": { "url": "url": … } } looks strange, should make this enum untagged, similar to tool.uv.sources?
| fn from_wheel(wheel: &Wheel) -> Self { | ||
| Self { | ||
| url: MetadataWheelWireSource::from_wheel(&wheel.url), | ||
| hash: wheel.hash.as_ref().map(ToString::to_string), |
There was a problem hiding this comment.
I know it's what we do in uv.lock, but I think we should rather follow PEP 691 here (https://peps.python.org/pep-0691/#project-detail), this avoids string parsing in the client and allows multiple hashes, should we need them in the future.
| /// This is because `mypackage[cli]` is fundamentally an augmentation of `mypackage` while `mypackage:dev` | ||
| /// is just a list of packages that happens to be defined by `mypackage`'s pyproject.toml. | ||
| #[derive(Debug, Clone, serde::Serialize)] | ||
| struct MetadataNode { |
There was a problem hiding this comment.
Do we have a description or schema available somewhere?
| struct MetadataNodeId { | ||
| /// The name of the package | ||
| name: PackageName, | ||
| /// The version of the package, if any could be found (workspace packages may have no version) |
There was a problem hiding this comment.
| /// The version of the package, if any could be found (workspace packages may have no version) | |
| /// The version of the package, if any could be found (source trees may have no version) |
or "directory dependencies"
| registry: MetadataRegistrySource::Path(PortablePathBuf::from(path)), | ||
| }, | ||
| }, | ||
| Source::Git(url, _) => Self::Git { git: url }, |
There was a problem hiding this comment.
Shouldn't we record the resolved git info too?
Summary
This expands
uv workspace metadatawith many of the fields that are found inuv.lockso that we have a format with information about the dependency graph/resolution that we're willing to call stable and have people rely upon (rather thanuv.lockwhich we'd rather you don't try to interpret).To a first approximation you can think of this as "uv.lock but serialized to json" but with the fields a bit more limited for now (easy to add later).
The biggest intentional divergence with uv.lock is that we favour encoding the dependency graph in a form that looks more like our internal "resolve" graph, in that hopes that it will simplify the work of anyone doing analysis on the graph (we structure our internal graph like this for a reason).
Specifically, the
resolvefield contains the entire dependency graph, with packages desugarred into several different nodes. There are 4 kinds of nodes (really 3, the build nodes will only be introduced when we establish build-dependency locking):mypackage==1.0.0 @ registry+https://pypi.org/simplemypackage[myextra]==1.0.0 @ registry+https://pypi.org/simplemypackage:mygroup==1.0.0 @ registry+https://pypi.org/simplemypackage(build)==1.0.0 @ registry+https://pypi.org/simplepackage nodes hold additional metadata about the package itself, and ids of the associated extra/group/build nodes.
A package like this:
will get 4 nodes with the following edges (Version and Source omitted here for brevity):
mypackagehttpxmypackage(build)hatchlingmypackage[cli]mypackagerichmypackage:devtyping-extensionsNote that
mypackage[cli]has a dependency edge onmypackagewhilemypackage:devdoes not. This is becausemypackage[cli]is fundamentally an augmentation ofmypackagewhilemypackage:devis just a list of packages that happens to be defined bymypackage's pyproject.toml.The resulting nodes for
mypackagewill look something like:json blob
{ "resolve": { "mypackage==1.0.0 @ editable+.": { "name": "mypackage", "version": "1.0.0", "source": { "editable": "." }, "kind": "package", "dependencies": [ { "id": "httpx==3.6 @ registry+https://pypi.org/simple" "marker": "sys_platform == 'linux'" }, ], "optional_dependencies": [ { "name": "cli", "id": "mypackage[cli]==1.0.0 @ editable+." }, ], "dependency_groups": [ { "name": "dev", "id": "mypackage:dev==1.0.0 @ editable+." } ] "build_system": { "build_backend": "hatchling.build", "id": "mypackage(build)==1.0.0 @ editable+." } "sdist": { ... }, "wheels": [ ... ] }, "mypackage:dev==1.0.0 @ editable+.": { "name": "mypackage", "version": "1.0.0", "source": { "editable": "." }, "kind": { "group": "dev" }, "dependencies": [ { "id": "typing-extensions==1.2.3 @ registry+https://pypi.org/simple" }, ] }, } "mypackage[cli]==1.0.0 @ editable+.": { "name": "mypackage", "version": "1.0.0", "source": { "editable": "." }, "kind": { "extra": "cli" }, "dependencies": [ { "id": "rich==2.2.3 @ registry+https://pypi.org/simple" }, { "id": "mypackage==1.0.0 @ editable+." }, ] }, "mypackage(build)==1.0.0 @ editable+.": { "name": "mypackage", "version": "1.0.0", "source": { "editable": "." }, "kind": "build", "dependencies": [ { "id": "hatchling==3.2.3 @ registry+https://pypi.org/simple" }, ] } } }Test Plan
Snapshots