From 4978fc38fb94735883388c359782b7dbaa686133 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 2 Jul 2025 10:58:57 +0200 Subject: [PATCH 1/8] No longer treat path sources as virtual if missing build specification --- .../uv-distribution/src/metadata/lowering.rs | 4 ++- crates/uv-workspace/src/pyproject.rs | 15 +++++---- crates/uv/tests/it/lock.rs | 33 ++++++++++--------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 54782c0832be2..c05ac47796eb9 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -729,12 +729,14 @@ fn path_source( }) } else { // Determine whether the project is a package or virtual. + // If the `package` option is unset, check if `tool.uv.package` is set + // on the path source (otherwise, default to `true`). let is_package = package.unwrap_or_else(|| { let pyproject_path = install_path.join("pyproject.toml"); fs_err::read_to_string(&pyproject_path) .ok() .and_then(|contents| PyProjectToml::from_string(contents).ok()) - .map(|pyproject_toml| pyproject_toml.is_package()) + .and_then(|pyproject_toml| pyproject_toml.tool_uv_package()) .unwrap_or(true) }); diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 124a628810260..aa64c601eee97 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -83,12 +83,7 @@ impl PyProjectToml { /// non-package ("virtual") project. pub fn is_package(&self) -> bool { // If `tool.uv.package` is set, defer to that explicit setting. - if let Some(is_package) = self - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.package) - { + if let Some(is_package) = self.tool_uv_package() { return is_package; } @@ -96,6 +91,14 @@ impl PyProjectToml { self.build_system.is_some() } + /// Returns the value of `tool.uv.package` if set. + pub fn tool_uv_package(&self) -> Option { + self.tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.package) + } + /// Returns `true` if the project uses a dynamic version. pub fn is_dynamic(&self) -> bool { self.project diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index faf37a83a0f27..75d81b4c01f46 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -7205,12 +7205,12 @@ fn lock_exclusion() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "project", virtual = "../" }] + requires-dist = [{ name = "project", directory = "../" }] [[package]] name = "project" version = "0.1.0" - source = { virtual = "../" } + source = { directory = "../" } "# ); }); @@ -7793,7 +7793,7 @@ fn lock_dev_transitive() -> Result<()> { [package.metadata] requires-dist = [ { name = "baz", editable = "baz" }, - { name = "foo", virtual = "../foo" }, + { name = "foo", directory = "../foo" }, { name = "iniconfig", specifier = ">1" }, ] @@ -7815,7 +7815,7 @@ fn lock_dev_transitive() -> Result<()> { [[package]] name = "foo" version = "0.1.0" - source = { virtual = "../foo" } + source = { directory = "../foo" } [package.metadata] @@ -13651,7 +13651,7 @@ fn lock_narrowed_python_version_upper() -> Result<()> { [[package]] name = "dependency" version = "0.1.0" - source = { virtual = "dependency" } + source = { directory = "dependency" } dependencies = [ { name = "iniconfig", marker = "python_full_version >= '3.10'" }, ] @@ -13677,7 +13677,7 @@ fn lock_narrowed_python_version_upper() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "dependency", marker = "python_full_version >= '3.10'", virtual = "dependency" }] + requires-dist = [{ name = "dependency", marker = "python_full_version >= '3.10'", directory = "dependency" }] "# ); }); @@ -17173,10 +17173,10 @@ fn lock_implicit_virtual_project() -> Result<()> { Ok(()) } -/// Lock a project that has a path dependency that is implicitly virtual (by way of omitting -/// `build-system`). +/// Lock a project that has a path dependency that is implicitly non-virtual (despite +/// omitting `build-system`). #[test] -fn lock_implicit_virtual_path() -> Result<()> { +fn lock_implicit_package_path() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -17243,7 +17243,7 @@ fn lock_implicit_virtual_path() -> Result<()> { [[package]] name = "child" version = "0.1.0" - source = { virtual = "child" } + source = { directory = "child" } dependencies = [ { name = "iniconfig" }, ] @@ -17281,7 +17281,7 @@ fn lock_implicit_virtual_path() -> Result<()> { [package.metadata] requires-dist = [ { name = "anyio", specifier = ">3" }, - { name = "child", virtual = "child" }, + { name = "child", directory = "child" }, ] [[package]] @@ -17317,20 +17317,21 @@ fn lock_implicit_virtual_path() -> Result<()> { Resolved 6 packages in [TIME] "###); - // Install from the lockfile. The virtual project should _not_ be installed. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + // Install from the lockfile. The path dependency should be installed. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Prepared 4 packages in [TIME] - Installed 4 packages in [TIME] + Prepared 5 packages in [TIME] + Installed 5 packages in [TIME] + anyio==4.3.0 + + child==0.1.0 (from file://[TEMP_DIR]/child) + idna==3.6 + iniconfig==2.0.0 + sniffio==1.3.1 - "###); + "); Ok(()) } From 00af1b37874437b0ad9a83a1cab120d1073eba26 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 2 Jul 2025 20:33:10 +0200 Subject: [PATCH 2/8] Add `tool.uv.package` is `true` case to test --- crates/uv/tests/it/sync.rs | 85 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 35a06ea57b07d..7f4104e76bcbc 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -5939,6 +5939,91 @@ fn sync_override_package() -> Result<()> { ~ project==0.0.0 (from file://[TEMP_DIR]/) "); + // Update the source `tool.uv` to `package = true` + let pyproject_toml = context.temp_dir.child("core").child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "core" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv] + package = true + "#, + )?; + + // Mark the source as `package = false`. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.0.0" + requires-python = ">=3.12" + dependencies = ["core"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv.sources] + core = { path = "./core", package = false } + "#, + )?; + + // Syncing the project should _not_ install `core`. + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + ~ project==0.0.0 (from file://[TEMP_DIR]/) + "); + + // Remove the `package = false` mark. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.0.0" + requires-python = ">=3.12" + dependencies = ["core"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv.sources] + core = { path = "./core" } + "#, + )?; + + // Syncing the project _should_ install `core`. + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 2 packages in [TIME] + + core==0.1.0 (from file://[TEMP_DIR]/core) + ~ project==0.0.0 (from file://[TEMP_DIR]/) + "); + Ok(()) } From b674c92780e3c45fcd94c3b70069b84c69ef1045 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Thu, 3 Jul 2025 15:39:54 +0200 Subject: [PATCH 3/8] Add docs --- docs/concepts/projects/dependencies.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/concepts/projects/dependencies.md b/docs/concepts/projects/dependencies.md index 022db4d7e1d73..436fda7959b15 100644 --- a/docs/concepts/projects/dependencies.md +++ b/docs/concepts/projects/dependencies.md @@ -438,6 +438,21 @@ $ uv add ~/projects/bar/ bar = { path = "../projects/bar", package = true } ``` + If the project is not marked as a non-package and no value is set for `package` on the source, the default behavior is to install it as a package, even if it lacks a [build + specification](./config.md#build-systems). If you'd like to avoid installing it, set + `package = false` on the source: + + ```toml title="pyproject.toml" + [project] + dependencies = ["bar"] + + [tool.uv.sources] + bar = { path = "../projects/bar", package = false } + ``` + + This will override the default behavior and the path dependency's own `tool.uv.package` + value. + For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better fit. From 0dbf402b72d91061d6b82188a726e2807d7fb972 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Thu, 3 Jul 2025 16:43:30 +0200 Subject: [PATCH 4/8] .. --- docs/concepts/projects/config.md | 5 +- docs/concepts/projects/dependencies.md | 67 +++++++++++++------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/concepts/projects/config.md b/docs/concepts/projects/config.md index 8efb667a10437..4b4b4a5e2dbbf 100644 --- a/docs/concepts/projects/config.md +++ b/docs/concepts/projects/config.md @@ -116,8 +116,9 @@ with the default build system. the presence of a `[build-system]` table is not required in other packages. For legacy reasons, if a build system is not defined, then `setuptools.build_meta:__legacy__` is used to build the package. Packages you depend on may not explicitly declare their build system but are still - installable. Similarly, if you add a dependency on a local package or install it with `uv pip`, - uv will always attempt to build and install it. + installable (e.g., the default behavior for [installing path sources](./dependencies.md#installing-path-sources)). + Similarly, if you add a dependency on a local package or install it with `uv pip`, uv will + always attempt to build and install it. ### Build system options diff --git a/docs/concepts/projects/dependencies.md b/docs/concepts/projects/dependencies.md index 436fda7959b15..8a04a6dc219ba 100644 --- a/docs/concepts/projects/dependencies.md +++ b/docs/concepts/projects/dependencies.md @@ -408,53 +408,52 @@ Or, a path to a project directory: $ uv add ~/projects/bar/ ``` -!!! important +### Path dependency installation - An [editable installation](#editable-dependencies) is not used for path dependencies by - default. An editable installation may be requested for project directories: +An [editable installation](#editable-dependencies) is not used for path dependencies by default. An +editable installation may be requested for project directories: - ```console - $ uv add --editable ../projects/bar/ - ``` +```console +$ uv add --editable ../projects/bar/ +``` - Which will result in a `pyproject.toml` with: +Which will result in a `pyproject.toml` with: - ```toml title="pyproject.toml" - [project] - dependencies = ["bar"] +```toml title="pyproject.toml" +[project] +dependencies = ["bar"] - [tool.uv.sources] - bar = { path = "../projects/bar", editable = true } - ``` +[tool.uv.sources] +bar = { path = "../projects/bar", editable = true } +``` - Similarly, if a project is marked as a [non-package](./config.md#build-systems), but you'd - like to install it in the environment as a package, set `package = true` on the source: +Similarly, if a project is marked as a [non-package](./config.md#build-systems), but you'd like to +install it in the environment as a package, set `package = true` on the source: - ```toml title="pyproject.toml" - [project] - dependencies = ["bar"] +```toml title="pyproject.toml" +[project] +dependencies = ["bar"] - [tool.uv.sources] - bar = { path = "../projects/bar", package = true } - ``` +[tool.uv.sources] +bar = { path = "../projects/bar", package = true } +``` - If the project is not marked as a non-package and no value is set for `package` on the source, the default behavior is to install it as a package, even if it lacks a [build - specification](./config.md#build-systems). If you'd like to avoid installing it, set - `package = false` on the source: +If the dependency project is not marked as a non-package and no value is set for `package` on the +source, the default behavior is to install it as a package, even if it lacks a +[[build-system] table](./config.md#build-systems). If you'd like to avoid installing it, set +`package = false` on the source: - ```toml title="pyproject.toml" - [project] - dependencies = ["bar"] +```toml title="pyproject.toml" +[project] +dependencies = ["bar"] - [tool.uv.sources] - bar = { path = "../projects/bar", package = false } - ``` +[tool.uv.sources] +bar = { path = "../projects/bar", package = false } +``` - This will override the default behavior and the path dependency's own `tool.uv.package` - value. +This will override the default behavior and the path dependency's own `tool.uv.package` value. - For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better - fit. +For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better fit. ### Workspace member From 20bf3e497377839066ae3db8eeb077af1132cfec Mon Sep 17 00:00:00 2001 From: John Mumm Date: Thu, 3 Jul 2025 17:42:40 +0200 Subject: [PATCH 5/8] Update docs --- docs/concepts/projects/dependencies.md | 34 ++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/concepts/projects/dependencies.md b/docs/concepts/projects/dependencies.md index 8a04a6dc219ba..5eef041a07225 100644 --- a/docs/concepts/projects/dependencies.md +++ b/docs/concepts/projects/dependencies.md @@ -410,25 +410,21 @@ $ uv add ~/projects/bar/ ### Path dependency installation -An [editable installation](#editable-dependencies) is not used for path dependencies by default. An -editable installation may be requested for project directories: - -```console -$ uv add --editable ../projects/bar/ -``` - -Which will result in a `pyproject.toml` with: +By default, a path dependency project is installed in the environment as a package, unless it is +explicitly marked as a [non-package](./config.md#build-systems). This is true even if it lacks a +[`[build-system] table`](./config.md#build-systems). If you'd like to override this behavior and +ensure the path dependency is not installed as a package, set `package = false` on the source: ```toml title="pyproject.toml" [project] dependencies = ["bar"] [tool.uv.sources] -bar = { path = "../projects/bar", editable = true } +bar = { path = "../projects/bar", package = false } ``` -Similarly, if a project is marked as a [non-package](./config.md#build-systems), but you'd like to -install it in the environment as a package, set `package = true` on the source: +If the path dependency project is marked as a non-package, but you'd like to install it as a +package, set `package = true` on the source: ```toml title="pyproject.toml" [project] @@ -438,21 +434,23 @@ dependencies = ["bar"] bar = { path = "../projects/bar", package = true } ``` -If the dependency project is not marked as a non-package and no value is set for `package` on the -source, the default behavior is to install it as a package, even if it lacks a -[[build-system] table](./config.md#build-systems). If you'd like to avoid installing it, set -`package = false` on the source: +An [editable installation](#editable-dependencies) is not used for path dependencies by default. An +editable installation may be requested for project directories: + +```console +$ uv add --editable ../projects/bar/ +``` + +Which will result in a `pyproject.toml` with: ```toml title="pyproject.toml" [project] dependencies = ["bar"] [tool.uv.sources] -bar = { path = "../projects/bar", package = false } +bar = { path = "../projects/bar", editable = true } ``` -This will override the default behavior and the path dependency's own `tool.uv.package` value. - For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better fit. ### Workspace member From 5685196b1863bdd17564946678c428787b05332f Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 4 Jul 2025 13:55:06 +0200 Subject: [PATCH 6/8] .. --- docs/concepts/projects/dependencies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/projects/dependencies.md b/docs/concepts/projects/dependencies.md index 5eef041a07225..cc71c692e1a16 100644 --- a/docs/concepts/projects/dependencies.md +++ b/docs/concepts/projects/dependencies.md @@ -412,7 +412,7 @@ $ uv add ~/projects/bar/ By default, a path dependency project is installed in the environment as a package, unless it is explicitly marked as a [non-package](./config.md#build-systems). This is true even if it lacks a -[`[build-system] table`](./config.md#build-systems). If you'd like to override this behavior and +[`[build-system]` table](./config.md#build-systems). If you'd like to override this behavior and ensure the path dependency is not installed as a package, set `package = false` on the source: ```toml title="pyproject.toml" From b117b4b4332ce96fb20072a9f1f1ae93d612313c Mon Sep 17 00:00:00 2001 From: John Mumm Date: Wed, 16 Jul 2025 10:26:35 +0200 Subject: [PATCH 7/8] Update tests --- crates/uv/tests/it/edit.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index ccc0cabf2ff96..70b8d6e500044 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -13381,7 +13381,9 @@ fn add_path_with_no_workspace() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] - Audited in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + dep==0.1.0 (from file://[TEMP_DIR]/dep) "); let pyproject_toml = context.read("pyproject.toml"); @@ -13452,7 +13454,9 @@ fn add_path_outside_workspace_no_default() -> Result<()> { Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 2 packages in [TIME] - Audited in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + dep==0.1.0 (from file://[TEMP_DIR]/external_dep) "); let pyproject_toml = fs_err::read_to_string(workspace_toml)?; From 6224fa2fd395a189c4ff8b872b86aee97b85c565 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 16 Jul 2025 15:52:44 -0500 Subject: [PATCH 8/8] Update documentation for path dependency packaging (#14632) Edits for https://github.com/astral-sh/uv/pull/14413 --- docs/concepts/projects/config.md | 6 +-- docs/concepts/projects/dependencies.md | 66 ++++++++++++++++---------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/docs/concepts/projects/config.md b/docs/concepts/projects/config.md index 4b4b4a5e2dbbf..34b62c01a86fe 100644 --- a/docs/concepts/projects/config.md +++ b/docs/concepts/projects/config.md @@ -116,9 +116,9 @@ with the default build system. the presence of a `[build-system]` table is not required in other packages. For legacy reasons, if a build system is not defined, then `setuptools.build_meta:__legacy__` is used to build the package. Packages you depend on may not explicitly declare their build system but are still - installable (e.g., the default behavior for [installing path sources](./dependencies.md#installing-path-sources)). - Similarly, if you add a dependency on a local package or install it with `uv pip`, uv will - always attempt to build and install it. + installable. Similarly, if you [add a dependency on a local project](./dependencies.md#path) + or install it with `uv pip`, uv will attempt to build and install it regardless of the presence + of a `[build-system]` table. ### Build system options diff --git a/docs/concepts/projects/dependencies.md b/docs/concepts/projects/dependencies.md index cc71c692e1a16..bf11e7174b61b 100644 --- a/docs/concepts/projects/dependencies.md +++ b/docs/concepts/projects/dependencies.md @@ -408,31 +408,11 @@ Or, a path to a project directory: $ uv add ~/projects/bar/ ``` -### Path dependency installation - -By default, a path dependency project is installed in the environment as a package, unless it is -explicitly marked as a [non-package](./config.md#build-systems). This is true even if it lacks a -[`[build-system]` table](./config.md#build-systems). If you'd like to override this behavior and -ensure the path dependency is not installed as a package, set `package = false` on the source: - -```toml title="pyproject.toml" -[project] -dependencies = ["bar"] - -[tool.uv.sources] -bar = { path = "../projects/bar", package = false } -``` - -If the path dependency project is marked as a non-package, but you'd like to install it as a -package, set `package = true` on the source: - -```toml title="pyproject.toml" -[project] -dependencies = ["bar"] +!!! important -[tool.uv.sources] -bar = { path = "../projects/bar", package = true } -``` + When using a directory as a path dependency, uv will attempt to build and install the target as + a package by default. See the [virtual dependency](#virtual-dependencies) documentation for + details. An [editable installation](#editable-dependencies) is not used for path dependencies by default. An editable installation may be requested for project directories: @@ -451,7 +431,10 @@ dependencies = ["bar"] bar = { path = "../projects/bar", editable = true } ``` -For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better fit. +!!! tip + + For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better + fit. ### Workspace member @@ -820,6 +803,39 @@ Or, to opt-out of using an editable dependency in a workspace: $ uv add --no-editable ./path/foo ``` +## Virtual dependencies + +uv allows dependencies to be "virtual", in which the dependency itself is not installed as a +[package](./config.md#project-packaging), but its dependencies are. + +By default, only workspace members without build systems declared are virtual. + +A dependency with a [`path` source](#path) is not virtual unless it explicitly sets +[`tool.uv.package = false`](../../reference/settings.md#package). Unlike working _in_ the dependent +project with uv, the package will be built even if a [build system](./config.md#build-systems) is +not declared. + +To treat a dependency as virtual, set `package = false` on the source: + +```toml title="pyproject.toml" +[project] +dependencies = ["bar"] + +[tool.uv.sources] +bar = { path = "../projects/bar", package = false } +``` + +Similarly, if a dependency sets `tool.uv.package = false`, it can be overridden by declaring +`package = true` on the source: + +```toml title="pyproject.toml" +[project] +dependencies = ["bar"] + +[tool.uv.sources] +bar = { path = "../projects/bar", package = true } +``` + ## Dependency specifiers uv uses standard