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 {
|
||||
// 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)
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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.
|
||||
pub fn is_dynamic(&self) -> bool {
|
||||
self.project
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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. 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
|
||||
|
||||
|
|
|
|||
|
|
@ -410,33 +410,28 @@ $ uv add ~/projects/bar/
|
|||
|
||||
!!! important
|
||||
|
||||
An [editable installation](#editable-dependencies) is not used for path dependencies by
|
||||
default. An editable installation may be requested for project directories:
|
||||
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.
|
||||
|
||||
```console
|
||||
$ uv add --editable ../projects/bar/
|
||||
```
|
||||
An [editable installation](#editable-dependencies) is not used for path dependencies by default. An
|
||||
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"
|
||||
[project]
|
||||
dependencies = ["bar"]
|
||||
Which will result in a `pyproject.toml` with:
|
||||
|
||||
[tool.uv.sources]
|
||||
bar = { path = "../projects/bar", editable = true }
|
||||
```
|
||||
```toml title="pyproject.toml"
|
||||
[project]
|
||||
dependencies = ["bar"]
|
||||
|
||||
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:
|
||||
[tool.uv.sources]
|
||||
bar = { path = "../projects/bar", editable = true }
|
||||
```
|
||||
|
||||
```toml title="pyproject.toml"
|
||||
[project]
|
||||
dependencies = ["bar"]
|
||||
|
||||
[tool.uv.sources]
|
||||
bar = { path = "../projects/bar", package = true }
|
||||
```
|
||||
!!! tip
|
||||
|
||||
For multiple packages in the same repository, [_workspaces_](./workspaces.md) may be a better
|
||||
fit.
|
||||
|
|
@ -808,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
|
||||
|
|
|
|||
Loading…
Reference in New Issue