mirror of https://github.com/astral-sh/uv
Add `--workspace` flag to `uv add` (#14496)
## Summary You can now pass `--workspace` to `uv add` to add a path dependency as a workspace member. Closes https://github.com/astral-sh/uv/issues/14464.
This commit is contained in:
parent
b1dc2b71a3
commit
4d061a6fc3
|
|
@ -3632,7 +3632,8 @@ pub struct AddArgs {
|
||||||
long,
|
long,
|
||||||
conflicts_with = "dev",
|
conflicts_with = "dev",
|
||||||
conflicts_with = "optional",
|
conflicts_with = "optional",
|
||||||
conflicts_with = "package"
|
conflicts_with = "package",
|
||||||
|
conflicts_with = "workspace"
|
||||||
)]
|
)]
|
||||||
pub script: Option<PathBuf>,
|
pub script: Option<PathBuf>,
|
||||||
|
|
||||||
|
|
@ -3648,6 +3649,13 @@ pub struct AddArgs {
|
||||||
value_parser = parse_maybe_string,
|
value_parser = parse_maybe_string,
|
||||||
)]
|
)]
|
||||||
pub python: Option<Maybe<String>>,
|
pub python: Option<Maybe<String>>,
|
||||||
|
|
||||||
|
/// Add the dependency as a workspace member.
|
||||||
|
///
|
||||||
|
/// When used with a path dependency, the package will be added to the workspace's `members`
|
||||||
|
/// list in the root `pyproject.toml` file.
|
||||||
|
#[arg(long)]
|
||||||
|
pub workspace: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ pub(crate) async fn add(
|
||||||
extras_of_dependency: Vec<ExtraName>,
|
extras_of_dependency: Vec<ExtraName>,
|
||||||
package: Option<PackageName>,
|
package: Option<PackageName>,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
|
workspace: bool,
|
||||||
install_mirrors: PythonInstallMirrors,
|
install_mirrors: PythonInstallMirrors,
|
||||||
settings: ResolverInstallerSettings,
|
settings: ResolverInstallerSettings,
|
||||||
network_settings: NetworkSettings,
|
network_settings: NetworkSettings,
|
||||||
|
|
@ -151,7 +152,7 @@ pub(crate) async fn add(
|
||||||
// Default groups we need the actual project for, interpreter discovery will use this!
|
// Default groups we need the actual project for, interpreter discovery will use this!
|
||||||
let defaulted_groups;
|
let defaulted_groups;
|
||||||
|
|
||||||
let target = if let Some(script) = script {
|
let mut target = if let Some(script) = script {
|
||||||
// If we found a PEP 723 script and the user provided a project-only setting, warn.
|
// If we found a PEP 723 script and the user provided a project-only setting, warn.
|
||||||
if package.is_some() {
|
if package.is_some() {
|
||||||
warn_user_once!(
|
warn_user_once!(
|
||||||
|
|
@ -478,6 +479,9 @@ pub(crate) async fn add(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the content prior to any modifications.
|
||||||
|
let snapshot = target.snapshot().await?;
|
||||||
|
|
||||||
// If the user provides a single, named index, pin all requirements to that index.
|
// If the user provides a single, named index, pin all requirements to that index.
|
||||||
let index = indexes
|
let index = indexes
|
||||||
.first()
|
.first()
|
||||||
|
|
@ -488,7 +492,72 @@ pub(crate) async fn add(
|
||||||
debug!("Pinning all requirements to index: `{index}`");
|
debug!("Pinning all requirements to index: `{index}`");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the requirements to the `pyproject.toml` or script.
|
// Track modification status, for reverts.
|
||||||
|
let mut modified = false;
|
||||||
|
|
||||||
|
// If `--workspace` is provided, add any members to the `workspace` section of the
|
||||||
|
// `pyproject.toml` file.
|
||||||
|
if workspace {
|
||||||
|
let AddTarget::Project(project, python_target) = target else {
|
||||||
|
unreachable!("`--workspace` and `--script` are conflicting options");
|
||||||
|
};
|
||||||
|
|
||||||
|
let workspace = project.workspace();
|
||||||
|
let mut toml = PyProjectTomlMut::from_toml(
|
||||||
|
&workspace.pyproject_toml().raw,
|
||||||
|
DependencyTarget::PyProjectToml,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Check each requirement to see if it's a path dependency
|
||||||
|
for requirement in &requirements {
|
||||||
|
if let RequirementSource::Directory { install_path, .. } = &requirement.source {
|
||||||
|
let absolute_path = if install_path.is_absolute() {
|
||||||
|
install_path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
project.root().join(install_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the path is not already included in the workspace.
|
||||||
|
if !workspace.includes(&absolute_path)? {
|
||||||
|
let relative_path = absolute_path
|
||||||
|
.strip_prefix(workspace.install_path())
|
||||||
|
.unwrap_or(&absolute_path);
|
||||||
|
|
||||||
|
toml.add_workspace(relative_path)?;
|
||||||
|
modified |= true;
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Added `{}` to workspace members",
|
||||||
|
relative_path.user_display().cyan()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we modified the workspace root, we need to reload it entirely, since this can impact
|
||||||
|
// the discovered members, etc.
|
||||||
|
target = if modified {
|
||||||
|
let workspace_content = toml.to_string();
|
||||||
|
fs_err::write(
|
||||||
|
workspace.install_path().join("pyproject.toml"),
|
||||||
|
&workspace_content,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
AddTarget::Project(
|
||||||
|
VirtualProject::discover(
|
||||||
|
project.root(),
|
||||||
|
&DiscoveryOptions::default(),
|
||||||
|
&WorkspaceCache::default(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
python_target,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AddTarget::Project(project, python_target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut toml = match &target {
|
let mut toml = match &target {
|
||||||
AddTarget::Script(script, _) => {
|
AddTarget::Script(script, _) => {
|
||||||
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
|
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
|
||||||
|
|
@ -498,6 +567,7 @@ pub(crate) async fn add(
|
||||||
DependencyTarget::PyProjectToml,
|
DependencyTarget::PyProjectToml,
|
||||||
),
|
),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let edits = edits(
|
let edits = edits(
|
||||||
requirements,
|
requirements,
|
||||||
&target,
|
&target,
|
||||||
|
|
@ -543,7 +613,7 @@ pub(crate) async fn add(
|
||||||
let content = toml.to_string();
|
let content = toml.to_string();
|
||||||
|
|
||||||
// Save the modified `pyproject.toml` or script.
|
// Save the modified `pyproject.toml` or script.
|
||||||
let modified = target.write(&content)?;
|
modified |= target.write(&content)?;
|
||||||
|
|
||||||
// If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock`
|
// If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock`
|
||||||
// to exist at all.
|
// to exist at all.
|
||||||
|
|
@ -563,9 +633,6 @@ pub(crate) async fn add(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the content prior to any modifications.
|
|
||||||
let snapshot = target.snapshot().await?;
|
|
||||||
|
|
||||||
// Update the `pypackage.toml` in-memory.
|
// Update the `pypackage.toml` in-memory.
|
||||||
let target = target.update(&content)?;
|
let target = target.update(&content)?;
|
||||||
|
|
||||||
|
|
@ -1296,6 +1363,16 @@ impl AddTargetSnapshot {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Self::Project(project, lock) => {
|
Self::Project(project, lock) => {
|
||||||
|
// Write the workspace `pyproject.toml` back to disk.
|
||||||
|
let workspace = project.workspace();
|
||||||
|
if workspace.install_path() != project.root() {
|
||||||
|
debug!("Reverting changes to workspace `pyproject.toml`");
|
||||||
|
fs_err::write(
|
||||||
|
workspace.install_path().join("pyproject.toml"),
|
||||||
|
workspace.pyproject_toml().as_ref(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Write the `pyproject.toml` back to disk.
|
// Write the `pyproject.toml` back to disk.
|
||||||
debug!("Reverting changes to `pyproject.toml`");
|
debug!("Reverting changes to `pyproject.toml`");
|
||||||
fs_err::write(
|
fs_err::write(
|
||||||
|
|
|
||||||
|
|
@ -1965,6 +1965,7 @@ async fn run_project(
|
||||||
args.extras,
|
args.extras,
|
||||||
args.package,
|
args.package,
|
||||||
args.python,
|
args.python,
|
||||||
|
args.workspace,
|
||||||
args.install_mirrors,
|
args.install_mirrors,
|
||||||
args.settings,
|
args.settings,
|
||||||
globals.network_settings,
|
globals.network_settings,
|
||||||
|
|
|
||||||
|
|
@ -1326,6 +1326,7 @@ pub(crate) struct AddSettings {
|
||||||
pub(crate) package: Option<PackageName>,
|
pub(crate) package: Option<PackageName>,
|
||||||
pub(crate) script: Option<PathBuf>,
|
pub(crate) script: Option<PathBuf>,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
|
pub(crate) workspace: bool,
|
||||||
pub(crate) install_mirrors: PythonInstallMirrors,
|
pub(crate) install_mirrors: PythonInstallMirrors,
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) indexes: Vec<Index>,
|
pub(crate) indexes: Vec<Index>,
|
||||||
|
|
@ -1363,6 +1364,7 @@ impl AddSettings {
|
||||||
package,
|
package,
|
||||||
script,
|
script,
|
||||||
python,
|
python,
|
||||||
|
workspace,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
let dependency_type = if let Some(extra) = optional {
|
let dependency_type = if let Some(extra) = optional {
|
||||||
|
|
@ -1463,6 +1465,7 @@ impl AddSettings {
|
||||||
package,
|
package,
|
||||||
script,
|
script,
|
||||||
python: python.and_then(Maybe::into_option),
|
python: python.and_then(Maybe::into_option),
|
||||||
|
workspace,
|
||||||
editable: flag(editable, no_editable, "editable"),
|
editable: flag(editable, no_editable, "editable"),
|
||||||
extras: extra.unwrap_or_default(),
|
extras: extra.unwrap_or_default(),
|
||||||
refresh: Refresh::from(refresh),
|
refresh: Refresh::from(refresh),
|
||||||
|
|
|
||||||
|
|
@ -7210,6 +7210,7 @@ fn remove_include_default_groups() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Revert changes to the `pyproject.toml` and `uv.lock` when the `add` operation fails.
|
/// Revert changes to the `pyproject.toml` and `uv.lock` when the `add` operation fails.
|
||||||
#[test]
|
#[test]
|
||||||
fn fail_to_add_revert_project() -> Result<()> {
|
fn fail_to_add_revert_project() -> Result<()> {
|
||||||
|
|
@ -7401,6 +7402,256 @@ fn fail_to_edit_revert_project() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Revert changes to the root `pyproject.toml` and `uv.lock` when the `add` operation fails.
|
||||||
|
#[test]
|
||||||
|
fn fail_to_add_revert_workspace_root() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("pyproject.toml")
|
||||||
|
.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Add a dependency on a package that declares static metadata (so can always resolve), but
|
||||||
|
// can't be installed.
|
||||||
|
let pyproject_toml = context.temp_dir.child("child/pyproject.toml");
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "child"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
"#})?;
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("child")
|
||||||
|
.child("setup.py")
|
||||||
|
.write_str("1/0")?;
|
||||||
|
|
||||||
|
// Add a dependency on a package that declares static metadata (so can always resolve), but
|
||||||
|
// can't be installed.
|
||||||
|
let pyproject_toml = context.temp_dir.child("broken").child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "broken"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
"#})?;
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("broken")
|
||||||
|
.child("setup.py")
|
||||||
|
.write_str("1/0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add().arg("--workspace").arg("./broken"), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Added `broken` to workspace members
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
× Failed to build `broken @ file://[TEMP_DIR]/broken`
|
||||||
|
├─▶ The build backend returned an error
|
||||||
|
╰─▶ Call to `setuptools.build_meta.build_editable` failed (exit status: 1)
|
||||||
|
|
||||||
|
[stderr]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<string>", line 14, in <module>
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 448, in get_requires_for_build_editable
|
||||||
|
return self.get_requires_for_build_wheel(config_settings)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
|
||||||
|
return self._get_build_requires(config_settings, requirements=['wheel'])
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
|
||||||
|
self.run_setup()
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
|
||||||
|
exec(code, locals())
|
||||||
|
File "<string>", line 1, in <module>
|
||||||
|
ZeroDivisionError: division by zero
|
||||||
|
|
||||||
|
hint: This usually indicates a problem with the package or the build environment.
|
||||||
|
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
|
||||||
|
"#);
|
||||||
|
|
||||||
|
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The lockfile should not exist, even though resolution succeeded.
|
||||||
|
assert!(!context.temp_dir.join("uv.lock").exists());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Revert changes to the root `pyproject.toml` and `uv.lock` when the `add` operation fails.
|
||||||
|
#[test]
|
||||||
|
fn fail_to_add_revert_workspace_member() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("pyproject.toml")
|
||||||
|
.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["child"]
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["child"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
child = { workspace = true }
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Add a workspace dependency.
|
||||||
|
let project = context.temp_dir.child("child");
|
||||||
|
project.child("pyproject.toml").write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "child"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
"#})?;
|
||||||
|
project
|
||||||
|
.child("src")
|
||||||
|
.child("child")
|
||||||
|
.child("__init__.py")
|
||||||
|
.touch()?;
|
||||||
|
|
||||||
|
// Add a dependency on a package that declares static metadata (so can always resolve), but
|
||||||
|
// can't be installed.
|
||||||
|
let pyproject_toml = context.temp_dir.child("broken/pyproject.toml");
|
||||||
|
pyproject_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "broken"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
"#})?;
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("broken")
|
||||||
|
.child("setup.py")
|
||||||
|
.write_str("1/0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add().current_dir(&project).arg("--workspace").arg("../broken"), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Added `broken` to workspace members
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
× Failed to build `broken @ file://[TEMP_DIR]/broken`
|
||||||
|
├─▶ The build backend returned an error
|
||||||
|
╰─▶ Call to `setuptools.build_meta.build_editable` failed (exit status: 1)
|
||||||
|
|
||||||
|
[stderr]
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<string>", line 14, in <module>
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 448, in get_requires_for_build_editable
|
||||||
|
return self.get_requires_for_build_wheel(config_settings)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
|
||||||
|
return self._get_build_requires(config_settings, requirements=['wheel'])
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
|
||||||
|
self.run_setup()
|
||||||
|
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
|
||||||
|
exec(code, locals())
|
||||||
|
File "<string>", line 1, in <module>
|
||||||
|
ZeroDivisionError: division by zero
|
||||||
|
|
||||||
|
hint: This usually indicates a problem with the package or the build environment.
|
||||||
|
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
|
||||||
|
"#);
|
||||||
|
|
||||||
|
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["child"]
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["child"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
child = { workspace = true }
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let pyproject_toml =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join("child").join("pyproject.toml"))?;
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r#"
|
||||||
|
[project]
|
||||||
|
name = "child"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The lockfile should not exist, even though resolution succeeded.
|
||||||
|
assert!(!context.temp_dir.join("uv.lock").exists());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure that the added dependencies are sorted if the dependency list was already sorted prior
|
/// Ensure that the added dependencies are sorted if the dependency list was already sorted prior
|
||||||
/// to the operation.
|
/// to the operation.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -12629,3 +12880,163 @@ fn add_bounds_requirement_over_bounds_kind() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a path dependency with `--workspace` flag to add it to workspace members. The root already
|
||||||
|
/// contains a workspace definition, so the package should be added to the workspace members.
|
||||||
|
#[test]
|
||||||
|
fn add_path_with_existing_workspace() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let workspace_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
workspace_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["project"]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Create a project within the workspace.
|
||||||
|
let project_dir = context.temp_dir.child("project");
|
||||||
|
project_dir.create_dir_all()?;
|
||||||
|
|
||||||
|
let project_toml = project_dir.child("pyproject.toml");
|
||||||
|
project_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Create a dependency package outside the workspace members.
|
||||||
|
let dep_dir = context.temp_dir.child("dep");
|
||||||
|
dep_dir.create_dir_all()?;
|
||||||
|
|
||||||
|
let dep_toml = dep_dir.child("pyproject.toml");
|
||||||
|
dep_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "dep"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Add the dependency with `--workspace` flag from the project directory.
|
||||||
|
uv_snapshot!(context.filters(), context
|
||||||
|
.add()
|
||||||
|
.current_dir(&project_dir)
|
||||||
|
.arg("../dep")
|
||||||
|
.arg("--workspace"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Added `dep` to workspace members
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
");
|
||||||
|
|
||||||
|
let pyproject_toml = context.read("pyproject.toml");
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = [
|
||||||
|
"project",
|
||||||
|
"dep",
|
||||||
|
]
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
let pyproject_toml = context.read("project/pyproject.toml");
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r#"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"dep",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
dep = { workspace = true }
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a path dependency with `--workspace` flag to add it to workspace members. The root doesn't
|
||||||
|
/// contain a workspace definition, so `uv add` should create one.
|
||||||
|
#[test]
|
||||||
|
fn add_path_with_workspace() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let workspace_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
workspace_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Create a dependency package outside the workspace members.
|
||||||
|
let dep_dir = context.temp_dir.child("dep");
|
||||||
|
dep_dir.create_dir_all()?;
|
||||||
|
|
||||||
|
let dep_toml = dep_dir.child("pyproject.toml");
|
||||||
|
dep_toml.write_str(indoc! {r#"
|
||||||
|
[project]
|
||||||
|
name = "dep"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// Add the dependency with `--workspace` flag from the project directory.
|
||||||
|
uv_snapshot!(context.filters(), context
|
||||||
|
.add()
|
||||||
|
.arg("./dep")
|
||||||
|
.arg("--workspace"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Added `dep` to workspace members
|
||||||
|
Resolved 2 packages in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
");
|
||||||
|
|
||||||
|
let pyproject_toml = context.read("pyproject.toml");
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r#"
|
||||||
|
[project]
|
||||||
|
name = "parent"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"dep",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = [
|
||||||
|
"dep",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
dep = { workspace = true }
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -582,6 +582,8 @@ uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
</dd><dt id="uv-add--upgrade-package"><a href="#uv-add--upgrade-package"><code>--upgrade-package</code></a>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
|
</dd><dt id="uv-add--upgrade-package"><a href="#uv-add--upgrade-package"><code>--upgrade-package</code></a>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
|
||||||
</dd><dt id="uv-add--verbose"><a href="#uv-add--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
|
</dd><dt id="uv-add--verbose"><a href="#uv-add--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
|
||||||
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
|
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
|
||||||
|
</dd><dt id="uv-add--workspace"><a href="#uv-add--workspace"><code>--workspace</code></a></dt><dd><p>Add the dependency as a workspace member.</p>
|
||||||
|
<p>When used with a path dependency, the package will be added to the workspace's <code>members</code> list in the root <code>pyproject.toml</code> file.</p>
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
## uv remove
|
## uv remove
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue