mirror of https://github.com/astral-sh/uv
Build `path` sources without build systems by default (#14413)
We currently treat path sources as virtual if they do not specify a build system, which is surprising behavior. This PR updates the behavior to treat path sources as packages unless the path source is explicitly marked as `package = false` or its own `tool.uv.package` is set to `false`. Closes #12015 --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
b98ac8c224
commit
ff30f14d50
|
|
@ -729,12 +729,14 @@ fn path_source(
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Determine whether the project is a package or virtual.
|
// 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 is_package = package.unwrap_or_else(|| {
|
||||||
let pyproject_path = install_path.join("pyproject.toml");
|
let pyproject_path = install_path.join("pyproject.toml");
|
||||||
fs_err::read_to_string(&pyproject_path)
|
fs_err::read_to_string(&pyproject_path)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|contents| PyProjectToml::from_string(contents).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)
|
.unwrap_or(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,7 @@ impl PyProjectToml {
|
||||||
/// non-package ("virtual") project.
|
/// non-package ("virtual") project.
|
||||||
pub fn is_package(&self) -> bool {
|
pub fn is_package(&self) -> bool {
|
||||||
// If `tool.uv.package` is set, defer to that explicit setting.
|
// If `tool.uv.package` is set, defer to that explicit setting.
|
||||||
if let Some(is_package) = self
|
if let Some(is_package) = self.tool_uv_package() {
|
||||||
.tool
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|tool| tool.uv.as_ref())
|
|
||||||
.and_then(|uv| uv.package)
|
|
||||||
{
|
|
||||||
return is_package;
|
return is_package;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,6 +91,14 @@ impl PyProjectToml {
|
||||||
self.build_system.is_some()
|
self.build_system.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the value of `tool.uv.package` if set.
|
||||||
|
pub fn tool_uv_package(&self) -> Option<bool> {
|
||||||
|
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.
|
/// Returns `true` if the project uses a dynamic version.
|
||||||
pub fn is_dynamic(&self) -> bool {
|
pub fn is_dynamic(&self) -> bool {
|
||||||
self.project
|
self.project
|
||||||
|
|
|
||||||
|
|
@ -13381,7 +13381,9 @@ fn add_path_with_no_workspace() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 2 packages in [TIME]
|
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");
|
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]
|
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||||
Creating virtual environment at: .venv
|
Creating virtual environment at: .venv
|
||||||
Resolved 2 packages in [TIME]
|
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)?;
|
let pyproject_toml = fs_err::read_to_string(workspace_toml)?;
|
||||||
|
|
|
||||||
|
|
@ -7205,12 +7205,12 @@ fn lock_exclusion() -> Result<()> {
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "project", virtual = "../" }]
|
requires-dist = [{ name = "project", directory = "../" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "project"
|
name = "project"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "../" }
|
source = { directory = "../" }
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -7793,7 +7793,7 @@ fn lock_dev_transitive() -> Result<()> {
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "baz", editable = "baz" },
|
{ name = "baz", editable = "baz" },
|
||||||
{ name = "foo", virtual = "../foo" },
|
{ name = "foo", directory = "../foo" },
|
||||||
{ name = "iniconfig", specifier = ">1" },
|
{ name = "iniconfig", specifier = ">1" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -7815,7 +7815,7 @@ fn lock_dev_transitive() -> Result<()> {
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foo"
|
name = "foo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "../foo" }
|
source = { directory = "../foo" }
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
|
|
||||||
|
|
@ -13651,7 +13651,7 @@ fn lock_narrowed_python_version_upper() -> Result<()> {
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dependency"
|
name = "dependency"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "dependency" }
|
source = { directory = "dependency" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "iniconfig", marker = "python_full_version >= '3.10'" },
|
{ name = "iniconfig", marker = "python_full_version >= '3.10'" },
|
||||||
]
|
]
|
||||||
|
|
@ -13677,7 +13677,7 @@ fn lock_narrowed_python_version_upper() -> Result<()> {
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lock a project that has a path dependency that is implicitly virtual (by way of omitting
|
/// Lock a project that has a path dependency that is implicitly non-virtual (despite
|
||||||
/// `build-system`).
|
/// omitting `build-system`).
|
||||||
#[test]
|
#[test]
|
||||||
fn lock_implicit_virtual_path() -> Result<()> {
|
fn lock_implicit_package_path() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
|
@ -17243,7 +17243,7 @@ fn lock_implicit_virtual_path() -> Result<()> {
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "child"
|
name = "child"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "child" }
|
source = { directory = "child" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "iniconfig" },
|
{ name = "iniconfig" },
|
||||||
]
|
]
|
||||||
|
|
@ -17281,7 +17281,7 @@ fn lock_implicit_virtual_path() -> Result<()> {
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "anyio", specifier = ">3" },
|
{ name = "anyio", specifier = ">3" },
|
||||||
{ name = "child", virtual = "child" },
|
{ name = "child", directory = "child" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -17317,20 +17317,21 @@ fn lock_implicit_virtual_path() -> Result<()> {
|
||||||
Resolved 6 packages in [TIME]
|
Resolved 6 packages in [TIME]
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// Install from the lockfile. The virtual project should _not_ be installed.
|
// Install from the lockfile. The path dependency should be installed.
|
||||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Prepared 4 packages in [TIME]
|
Prepared 5 packages in [TIME]
|
||||||
Installed 4 packages in [TIME]
|
Installed 5 packages in [TIME]
|
||||||
+ anyio==4.3.0
|
+ anyio==4.3.0
|
||||||
|
+ child==0.1.0 (from file://[TEMP_DIR]/child)
|
||||||
+ idna==3.6
|
+ idna==3.6
|
||||||
+ iniconfig==2.0.0
|
+ iniconfig==2.0.0
|
||||||
+ sniffio==1.3.1
|
+ sniffio==1.3.1
|
||||||
"###);
|
");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5939,6 +5939,91 @@ fn sync_override_package() -> Result<()> {
|
||||||
~ project==0.0.0 (from file://[TEMP_DIR]/)
|
~ 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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
|
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
|
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`,
|
installable. Similarly, if you [add a dependency on a local project](./dependencies.md#path)
|
||||||
uv will always attempt to build and install it.
|
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
|
### Build system options
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -410,33 +410,28 @@ $ uv add ~/projects/bar/
|
||||||
|
|
||||||
!!! important
|
!!! important
|
||||||
|
|
||||||
An [editable installation](#editable-dependencies) is not used for path dependencies by
|
When using a directory as a path dependency, uv will attempt to build and install the target as
|
||||||
default. An editable installation may be requested for project directories:
|
a package by default. See the [virtual dependency](#virtual-dependencies) documentation for
|
||||||
|
details.
|
||||||
|
|
||||||
```console
|
An [editable installation](#editable-dependencies) is not used for path dependencies by default. An
|
||||||
$ uv add --editable ../projects/bar/
|
editable installation may be requested for project directories:
|
||||||
```
|
|
||||||
|
|
||||||
Which will result in a `pyproject.toml` with:
|
```console
|
||||||
|
$ uv add --editable ../projects/bar/
|
||||||
|
```
|
||||||
|
|
||||||
```toml title="pyproject.toml"
|
Which will result in a `pyproject.toml` with:
|
||||||
[project]
|
|
||||||
dependencies = ["bar"]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
```toml title="pyproject.toml"
|
||||||
bar = { path = "../projects/bar", editable = true }
|
[project]
|
||||||
```
|
dependencies = ["bar"]
|
||||||
|
|
||||||
Similarly, if a project is marked as a [non-package](./config.md#build-systems), but you'd
|
[tool.uv.sources]
|
||||||
like to install it in the environment as a package, set `package = true` on the source:
|
bar = { path = "../projects/bar", editable = true }
|
||||||
|
```
|
||||||
|
|
||||||
```toml title="pyproject.toml"
|
!!! tip
|
||||||
[project]
|
|
||||||
dependencies = ["bar"]
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
bar = { path = "../projects/bar", package = true }
|
|
||||||
```
|
|
||||||
|
|
||||||
For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better
|
For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better
|
||||||
fit.
|
fit.
|
||||||
|
|
@ -808,6 +803,39 @@ Or, to opt-out of using an editable dependency in a workspace:
|
||||||
$ uv add --no-editable ./path/foo
|
$ 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
|
## Dependency specifiers
|
||||||
|
|
||||||
uv uses standard
|
uv uses standard
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue