diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 63e87367c2497..bdd3c180ddcaa 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2903,12 +2903,28 @@ pub struct RunArgs { #[arg(long, conflicts_with = "extra")] pub all_extras: bool, - /// Exclude the specified optional dependencies, if `--all-extras` is supplied. + /// Disable the specified extra. + /// + /// This options always takes precedence over default extras, + /// `--all-extras`, and `--extra`. /// /// May be provided multiple times. #[arg(long)] pub no_extra: Vec, + /// Ignore the default extras. + /// + /// uv includes the extras defined in `tool.uv.default-extras` by default. + /// This disables that option, however, specific extras can still be included with `--extra`. + #[arg(long)] + pub no_default_extras: bool, + + /// Only include dependencies from the specified extra. + /// + /// May be provided multiple times. Implies `--no-default-extras`. + #[arg(long, conflicts_with_all = ["extra", "all_extras"])] + pub only_extra: Vec, + #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, @@ -3194,12 +3210,28 @@ pub struct SyncArgs { #[arg(long, conflicts_with = "extra")] pub all_extras: bool, - /// Exclude the specified optional dependencies, if `--all-extras` is supplied. + /// Disable the specified extra. + /// + /// This options always takes precedence over default extras, + /// `--all-extras`, and `--extra`. /// /// May be provided multiple times. #[arg(long)] pub no_extra: Vec, + /// Ignore the default extras. + /// + /// uv includes the extras defined in `tool.uv.default-extras` by default. + /// This disables that option, however, specific extras can still be included with `--extra`. + #[arg(long)] + pub no_default_extras: bool, + + /// Only include dependencies from the specified extra. + /// + /// May be provided multiple times. Implies `--no-default-extras`. + #[arg(long, conflicts_with_all = ["extra", "all_extras"])] + pub only_extra: Vec, + #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, @@ -3933,12 +3965,28 @@ pub struct ExportArgs { #[arg(long, conflicts_with = "extra")] pub all_extras: bool, - /// Exclude the specified optional dependencies, if `--all-extras` is supplied. + /// Disable the specified extra. + /// + /// This options always takes precedence over default extras, + /// `--all-extras`, and `--extra`. /// /// May be provided multiple times. #[arg(long)] pub no_extra: Vec, + /// Ignore the default extras. + /// + /// uv includes the extras defined in `tool.uv.default-extras` by default. + /// This disables that option, however, specific extras can still be included with `--extra`. + #[arg(long)] + pub no_default_extras: bool, + + /// Only include dependencies from the specified extra. + /// + /// May be provided multiple times. Implies `--no-default-extras`. + #[arg(long, conflicts_with_all = ["extra", "all_extras"])] + pub only_extra: Vec, + #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 54ae4e261b651..225a6753cc612 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -216,6 +216,12 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { "default-groups", )); } + if options.default_extras.is_some() { + return Err(Error::PyprojectOnlyField( + path.to_path_buf(), + "default-extras", + )); + } if options.managed.is_some() { return Err(Error::PyprojectOnlyField(path.to_path_buf(), "managed")); } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index ff6d3999575da..4a43ae3a3c293 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -140,6 +140,9 @@ pub struct Options { #[cfg_attr(feature = "schemars", schemars(skip))] pub default_groups: Option, + #[cfg_attr(feature = "schemars", schemars(skip))] + pub default_extras: Option, + #[cfg_attr(feature = "schemars", schemars(skip))] pub managed: Option, @@ -1870,6 +1873,7 @@ pub struct OptionsWire { managed: Option, r#package: Option, default_groups: Option, + default_extras: Option, dev_dependencies: Option, // Build backend @@ -1934,6 +1938,7 @@ impl From for Options { workspace, sources, default_groups, + default_extras, dev_dependencies, managed, package, @@ -2010,6 +2015,7 @@ impl From for Options { sources, dev_dependencies, default_groups, + default_extras, managed, package, } diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 2b0e44c162d12..19d15236d98e4 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -22,7 +22,7 @@ use uv_distribution_types::{Index, IndexName, RequirementSource}; use uv_fs::{PortablePathBuf, relative_to}; use uv_git_types::GitReference; use uv_macros::OptionsMetadata; -use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName}; +use uv_normalize::{DefaultExtras, DefaultGroups, ExtraName, GroupName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTree; use uv_pypi_types::{ @@ -341,6 +341,18 @@ pub struct ToolUv { )] pub package: Option, + /// The list of `optional-dependencies` extras to install by default. + /// + /// Can also be the literal "all" to default enable all extras. + #[option( + default = r#"[""]"#, + value_type = r#"str | list[str]"#, + example = r#" + default-extras = ["docs", "dev"] + "# + )] + pub default_extras: Option, + /// The list of `dependency-groups` to install by default. /// /// Can also be the literal `"all"` to default enable all groups. diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 3caaa8f8cb7a1..467ec7bd29cd5 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1817,6 +1817,7 @@ mod tests { }, "managed": null, "package": null, + "default-extras": null, "default-groups": null, "dev-dependencies": null, "override-dependencies": null, @@ -1912,6 +1913,7 @@ mod tests { }, "managed": null, "package": null, + "default-extras": null, "default-groups": null, "dev-dependencies": null, "override-dependencies": null, @@ -2122,6 +2124,7 @@ mod tests { }, "managed": null, "package": null, + "default-extras": null, "default-groups": null, "dev-dependencies": null, "override-dependencies": null, @@ -2229,6 +2232,7 @@ mod tests { }, "managed": null, "package": null, + "default-extras": null, "default-groups": null, "dev-dependencies": null, "override-dependencies": null, @@ -2349,6 +2353,7 @@ mod tests { }, "managed": null, "package": null, + "default-extras": null, "default-groups": null, "dev-dependencies": null, "override-dependencies": null, @@ -2443,6 +2448,7 @@ mod tests { }, "managed": null, "package": null, + "default-extras": null, "default-groups": null, "dev-dependencies": null, "override-dependencies": null, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index a4091504db5e1..3ae3944d4dbf7 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -29,7 +29,7 @@ use uv_distribution_types::{ use uv_fs::{LockedFile, Simplified}; use uv_git::GIT_STORE; use uv_git_types::GitReference; -use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, PackageName}; +use uv_normalize::{DEV_DEPENDENCIES, PackageName}; use uv_pep508::{ExtraName, MarkerTree, UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; @@ -53,7 +53,7 @@ use crate::commands::project::lock::LockMode; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ PlatformState, ProjectEnvironment, ProjectError, ProjectInterpreter, ScriptInterpreter, - UniversalState, default_dependency_groups, init_script_python_requirement, + UniversalState, default_dependency_groups, default_extras, init_script_python_requirement, }; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{ExitStatus, ScriptPath, diagnostics, project}; @@ -970,7 +970,7 @@ async fn lock_and_sync( let default_groups = default_dependency_groups(project.pyproject_toml())?; // Determine the default extras to include. - let default_extras = DefaultExtras::default(); + let default_extras = default_extras(project.pyproject_toml())?; // Identify the installation target. let target = match &project { diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 566f4af411cba..f25888705002a 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -25,7 +25,7 @@ use crate::commands::project::lock::{LockMode, LockOperation}; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, default_dependency_groups, - detect_conflicts, + default_extras, detect_conflicts, }; use crate::commands::{ExitStatus, OutputWriter, diagnostics}; use crate::printer::Printer; @@ -118,10 +118,9 @@ pub(crate) async fn export( // Determine the default extras to include. let default_extras = match &target { - ExportTarget::Project(_project) => DefaultExtras::default(), + ExportTarget::Project(project) => default_extras(project.pyproject_toml())?, ExportTarget::Script(_) => DefaultExtras::default(), }; - let dev = dev.with_defaults(default_groups); let extras = extras.with_defaults(default_extras); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index d2efc3ccde808..6054c257a1a79 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -13,7 +13,7 @@ use uv_cache_key::cache_digest; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, - PreviewMode, Reinstall, SourceStrategy, Upgrade, + ExtrasSpecificationWithDefaults, PreviewMode, Reinstall, SourceStrategy, Upgrade, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::{DistributionDatabase, LoweredRequirement}; @@ -23,7 +23,9 @@ use uv_distribution_types::{ use uv_fs::{CWD, LockedFile, Simplified}; use uv_git::ResolvedRepositoryReference; use uv_installer::{SatisfiesResult, SitePackages}; -use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName}; +use uv_normalize::{ + DEV_DEPENDENCIES, DefaultExtras, DefaultGroups, ExtraName, GroupName, PackageName, +}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTreeContents; use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts}; @@ -174,6 +176,11 @@ pub(crate) enum ProjectError { #[error("PEP 723 scripts do not support dependency groups, but group `{0}` was specified")] MissingGroupScript(GroupName), + #[error( + "Default extra `{0}` (from `tool.uv.default-extras`) is not defined in the project's `optional-dependencies` table" + )] + MissingDefaultExtra(ExtraName), + #[error( "Default group `{0}` (from `tool.uv.default-groups`) is not defined in the project's `dependency-groups` table" )] @@ -2428,12 +2435,40 @@ pub(crate) fn default_dependency_groups( } } +/// Returns the default extras from the [`PyProjectToml`]. +#[allow(clippy::result_large_err)] +pub(crate) fn default_extras( + pyproject_toml: &PyProjectToml, +) -> Result { + if let Some(defaults) = pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref().and_then(|uv| uv.default_extras.as_ref())) + { + if let DefaultExtras::List(defaults) = defaults { + for extra in defaults { + if pyproject_toml + .project + .as_ref() + .and_then(|project| project.optional_dependencies.as_ref()) + .is_none_or(|extras| !extras.contains_key(extra)) + { + return Err(ProjectError::MissingDefaultExtra(extra.clone())); + } + } + } + Ok(defaults.clone()) + } else { + Ok(DefaultExtras::default()) + } +} + /// Validate that we aren't trying to install extras or groups that /// are declared as conflicting. #[allow(clippy::result_large_err)] pub(crate) fn detect_conflicts( lock: &Lock, - extras: &ExtrasSpecification, + extras: &ExtrasSpecificationWithDefaults, dev: &DependencyGroupsWithDefaults, ) -> Result<(), ProjectError> { // Note that we need to collect all extras and groups that match in diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 6dab60012d8cf..89094cc3e6c63 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -13,7 +13,7 @@ use uv_configuration::{ PreviewMode, }; use uv_fs::Simplified; -use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras}; +use uv_normalize::DEV_DEPENDENCIES; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; @@ -31,7 +31,7 @@ use crate::commands::project::lock::LockMode; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ ProjectEnvironment, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, - default_dependency_groups, + default_dependency_groups, default_extras, }; use crate::commands::{ExitStatus, diagnostics, project}; use crate::printer::Printer; @@ -318,7 +318,7 @@ pub(crate) async fn remove( let default_groups = default_dependency_groups(project.pyproject_toml())?; // Determine the default extras to include. - let default_extras = DefaultExtras::default(); + let default_extras = default_extras(project.pyproject_toml())?; // Identify the installation target. let target = match &project { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 035528b1ccfb2..e4596d86f677e 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -51,7 +51,7 @@ use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ EnvironmentSpecification, PreferenceSource, ProjectEnvironment, ProjectError, ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython, - default_dependency_groups, script_specification, update_environment, + default_dependency_groups, default_extras, script_specification, update_environment, validate_project_requires_python, }; use crate::commands::reporters::PythonDownloadReporter; @@ -683,7 +683,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl let default_groups = default_dependency_groups(project.pyproject_toml())?; // Determine the default extras to include. - let default_extras = DefaultExtras::default(); + let default_extras = default_extras(project.pyproject_toml())?; // Determine the lock mode. let mode = if frozen { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index ed96795e571ee..7f3341fa2a6ef 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -40,7 +40,8 @@ use crate::commands::project::lock::{LockMode, LockOperation, LockResult}; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment, UniversalState, - default_dependency_groups, detect_conflicts, script_specification, update_environment, + default_dependency_groups, default_extras, detect_conflicts, script_specification, + update_environment, }; use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; @@ -124,7 +125,7 @@ pub(crate) async fn sync( // Determine the default extras to include. let default_extras = match &target { - SyncTarget::Project(_project) => DefaultExtras::default(), + SyncTarget::Project(project) => default_extras(project.pyproject_toml())?, SyncTarget::Script(..) => DefaultExtras::default(), }; diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index b5eb2f5d07b3c..d46201bb33dcb 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -349,6 +349,8 @@ impl RunSettings { extra, all_extras, no_extra, + no_default_extras, + only_extra, no_all_extras, dev, no_dev, @@ -398,10 +400,8 @@ impl RunSettings { extras: ExtrasSpecification::from_args( extra.unwrap_or_default(), no_extra, - // TODO(blueraft): support no_default_extras - false, - // TODO(blueraft): support only_extra - vec![], + no_default_extras, + only_extra, flag(all_extras, no_all_extras).unwrap_or_default(), ), dev: DependencyGroups::from_args( @@ -1117,6 +1117,8 @@ impl SyncSettings { let SyncArgs { extra, all_extras, + no_default_extras, + only_extra, no_extra, no_all_extras, dev, @@ -1174,10 +1176,8 @@ impl SyncSettings { extras: ExtrasSpecification::from_args( extra.unwrap_or_default(), no_extra, - // TODO(blueraft): support no_default_extras - false, - // TODO(blueraft): support only_extra - vec![], + no_default_extras, + only_extra, flag(all_extras, no_all_extras).unwrap_or_default(), ), dev: DependencyGroups::from_args( @@ -1691,6 +1691,8 @@ impl ExportSettings { prune, extra, all_extras, + no_default_extras, + only_extra, no_extra, no_all_extras, dev, @@ -1733,10 +1735,8 @@ impl ExportSettings { extras: ExtrasSpecification::from_args( extra.unwrap_or_default(), no_extra, - // TODO(blueraft): support no_default_extras - false, - // TODO(blueraft): support only_extra - vec![], + no_default_extras, + only_extra, flag(all_extras, no_all_extras).unwrap_or_default(), ), dev: DependencyGroups::from_args( diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index b48536f2e44fc..4513f4f7a07e3 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -342,6 +342,221 @@ fn requirements_txt_project_extra() -> Result<()> { Ok(()) } +#[test] +fn requirements_txt_project_default_extras() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + async = ["anyio==3.7.0"] + pytest = ["iniconfig"] + + [tool.uv] + default-extras = ["async"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export(), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] + -e . + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + # via project + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.export().arg("--extra").arg("pytest").arg("--extra").arg("async").arg("--no-extra").arg("pytest"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --extra pytest --extra async --no-extra pytest + -e . + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + # via project + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--extra").arg("pytest"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --extra pytest + -e . + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + # via project + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via project + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.export().arg("--all-extras"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --all-extras + -e . + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + # via project + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via project + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--all-extras").arg("--no-extra").arg("pytest"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --all-extras --no-extra pytest + -e . + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + # via project + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--no-default-extras"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-default-extras + -e . + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.export().arg("--no-default-extras").arg("--only-extra").arg("pytest"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-default-extras --only-extra pytest + -e . + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via project + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "); + + Ok(()) +} + #[test] fn requirements_txt_prune() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 121915ef0b65b..7b484627a128c 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1043,6 +1043,269 @@ fn run_pep723_script_lock() -> Result<()> { Ok(()) } +#[test] +fn run_default_extras() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + "#, + )?; + + context.lock().assert().success(); + + // Only the main dependencies should be installed. + uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import typing_extensions"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "###); + + // If we set a different default extras, it should be synced instead. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-extras = ["foo"] + "#, + )?; + + uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import typing_extensions"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + // `--no-extra` should remove from the defaults. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-extras = ["foo"] + "#, + )?; + + uv_snapshot!(context.filters(), context.run() + .arg("--no-extra") + .arg("foo") + .arg("--exact") + .arg("python") + .arg("-c") + .arg("import typing_extensions"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 3 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - sniffio==1.3.1 + "###); + + // Using `--extra` should include the defaults + uv_snapshot!(context.filters(), context.run() + .arg("--extra") + .arg("dev") + .arg("python") + .arg("-c") + .arg("import iniconfig"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 1 package in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + // Using `--all-extras` should include the defaults + uv_snapshot!(context.filters(), context.run() + .arg("--all-extras") + .arg("python") + .arg("-c") + .arg("import iniconfig"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + requests==2.31.0 + + urllib3==2.2.1 + "###); + + // Using `--only-extra` should exclude the defaults + uv_snapshot!(context.filters(), context.run() + .arg("--exact") + .arg("--only-extra") + .arg("dev") + .arg("python") + .arg("-c") + .arg("import typing_extensions"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 7 packages in [TIME] + - anyio==4.3.0 + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - idna==3.6 + - requests==2.31.0 + - sniffio==1.3.1 + - urllib3==2.2.1 + "###); + + uv_snapshot!(context.filters(), context.run() + .arg("--all-extras") + .arg("python") + .arg("-c") + .arg("import iniconfig"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 7 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + // Using `--no-default-extras` should exclude all extras. + uv_snapshot!(context.filters(), context.run() + .arg("--no-default-extras") + .arg("--exact") + .arg("python") + .arg("-c") + .arg("import typing_extensions"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 8 packages in [TIME] + - anyio==4.3.0 + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - idna==3.6 + - iniconfig==2.0.0 + - requests==2.31.0 + - sniffio==1.3.1 + - urllib3==2.2.1 + "###); + + uv_snapshot!(context.filters(), context.run() + .arg("--all-extras") + .arg("python") + .arg("-c") + .arg("import iniconfig"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + // Using `--no-default-extras` with `--extra foo` and `--extras bar` should include those + // extras. + uv_snapshot!(context.filters(), context.run() + .arg("--exact") + .arg("--no-default-extras") + .arg("--extra") + .arg("foo") + .arg("--extra") + .arg("bar") + .arg("python") + .arg("-c") + .arg("import typing_extensions"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 1 package in [TIME] + - iniconfig==2.0.0 + "###); + + Ok(()) +} + /// With `managed = false`, we should avoid installing the project itself. #[test] fn run_managed_false() -> Result<()> { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 87453090c2c01..8958750b7c2d2 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3987,7 +3987,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `default-extras`, `dev-dependencies`, `build-backend` " ); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index c2026d51c0fb8..0ac4817b6cc62 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -2144,6 +2144,382 @@ fn sync_non_existent_default_group() -> Result<()> { Ok(()) } +#[test] +fn sync_default_extras() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + "#, + )?; + + context.lock().assert().success(); + + // Only the main dependencies should be installed. + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "); + + // If we set a different default extras, it should be synced instead. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-extras = ["foo"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + // `--no-extra` should remove from the defaults. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-extras = ["foo"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync().arg("--no-extra").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 3 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - sniffio==1.3.1 + "###); + + // Using `--extra` should include the defaults + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("dev"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 1 package in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "); + + // Using `--all-extras` should include the defaults + uv_snapshot!(context.filters(), context.sync().arg("--all-extras"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + requests==2.31.0 + + urllib3==2.2.1 + "###); + + // Using `--only-extra` should exclude the defaults + uv_snapshot!(context.filters(), context.sync().arg("--only-extra").arg("dev"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 7 packages in [TIME] + - anyio==4.3.0 + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - idna==3.6 + - requests==2.31.0 + - sniffio==1.3.1 + - urllib3==2.2.1 + "); + + uv_snapshot!(context.filters(), context.sync().arg("--all-extras"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 7 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "); + + // Using `--no-default-extras` should exclude all extras. + uv_snapshot!(context.filters(), context.sync().arg("--no-default-extras"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 8 packages in [TIME] + - anyio==4.3.0 + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - idna==3.6 + - iniconfig==2.0.0 + - requests==2.31.0 + - sniffio==1.3.1 + - urllib3==2.2.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--all-extras"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + // Using `--no-default-extras` with `--extra foo` and `--extras bar` should include those + // extras. + uv_snapshot!(context.filters(), context.sync().arg("--no-default-extras").arg("--extra").arg("foo").arg("--extra").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 1 package in [TIME] + - iniconfig==2.0.0 + "###); + + Ok(()) +} + +/// default-extras = "all" sugar works +#[test] +fn sync_default_extras_all() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-extras = "all" + "#, + )?; + + context.lock().assert().success(); + + // groups = "all" should behave like --all-extras in contexts where defaults exist + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 9 packages in [TIME] + Installed 9 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + + urllib3==2.2.1 + "); + + // Using `--no-default-extras` should still work + uv_snapshot!(context.filters(), context.sync().arg("--no-default-extras"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 8 packages in [TIME] + - anyio==4.3.0 + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - idna==3.6 + - iniconfig==2.0.0 + - requests==2.31.0 + - sniffio==1.3.1 + - urllib3==2.2.1 + "); + + // Using `--all-extras` should be redundant and work fine + uv_snapshot!(context.filters(), context.sync().arg("--all-extras"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + // Using `--extra` should be redundant and still work fine + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("foo"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Audited 9 packages in [TIME] + "); + + // Using `--only-extra` should still disable defaults + uv_snapshot!(context.filters(), context.sync().arg("--only-extra").arg("foo"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 5 packages in [TIME] + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - iniconfig==2.0.0 + - requests==2.31.0 + - urllib3==2.2.1 + "); + + Ok(()) +} + +/// default-extras = "gibberish" error +#[test] +fn sync_default_extras_gibberish() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + dev = ["iniconfig"] + foo = ["anyio"] + bar = ["requests"] + + [tool.uv] + default-extras = "gibberish" + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 14, column 26 + | + 14 | default-extras = "gibberish" + | ^^^^^^^^^^^ + default-extras must be "all" or a ["list", "of", "extras"] + "#); + + Ok(()) +} + #[test] fn sync_default_groups() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index aef89436838c4..f4c4549031d27 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -174,14 +174,17 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_NO_BUILD_PACKAGE environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

-

May also be set with the UV_NO_CONFIG environment variable.

--no-default-groups

Ignore the default dependency groups.

+

May also be set with the UV_NO_CONFIG environment variable.

--no-default-extras

Ignore the default extras.

+

uv includes the extras defined in tool.uv.default-extras by default. This disables that option, however, specific extras can still be included with --extra.

+
--no-default-groups

Ignore the default dependency groups.

uv includes the groups defined in tool.uv.default-groups by default. This disables that option, however, specific groups can still be included with --group.

--no-dev

Disable the development dependency group.

This option is an alias of --no-group dev. See --no-default-groups to disable all default groups instead.

This option is only available when running in a project.

--no-editable

Install any editable dependencies, including the project and any workspace members, as non-editable

May also be set with the UV_NO_EDITABLE environment variable.

--no-env-file

Avoid reading environment variables from a .env file

-

May also be set with the UV_NO_ENV_FILE environment variable.

--no-extra no-extra

Exclude the specified optional dependencies, if --all-extras is supplied.

+

May also be set with the UV_NO_ENV_FILE environment variable.

--no-extra no-extra

Disable the specified extra.

+

This options always takes precedence over default extras, --all-extras, and --extra.

May be provided multiple times.

--no-group no-group

Disable the specified dependency group.

This option always takes precedence over default groups, --all-groups, and --group.

@@ -203,6 +206,8 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_OFFLINE environment variable.

--only-dev

Only include the development dependency group.

The project and its dependencies will be omitted.

This option is an alias for --only-group dev. Implies --no-default-groups.

+
--only-extra only-extra

Only include dependencies from the specified extra.

+

May be provided multiple times. Implies --no-default-extras.

--only-group only-group

Only include dependencies from the specified dependency group.

The project and its dependencies will be omitted.

May be provided multiple times. Implies --no-default-groups.

@@ -1069,12 +1074,15 @@ uv sync [OPTIONS]

May also be set with the UV_NO_BUILD_PACKAGE environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

-

May also be set with the UV_NO_CONFIG environment variable.

--no-default-groups

Ignore the default dependency groups.

+

May also be set with the UV_NO_CONFIG environment variable.

--no-default-extras

Ignore the default extras.

+

uv includes the extras defined in tool.uv.default-extras by default. This disables that option, however, specific extras can still be included with --extra.

+
--no-default-groups

Ignore the default dependency groups.

uv includes the groups defined in tool.uv.default-groups by default. This disables that option, however, specific groups can still be included with --group.

--no-dev

Disable the development dependency group.

This option is an alias of --no-group dev. See --no-default-groups to disable all default groups instead.

--no-editable

Install any editable dependencies, including the project and any workspace members, as non-editable

-

May also be set with the UV_NO_EDITABLE environment variable.

--no-extra no-extra

Exclude the specified optional dependencies, if --all-extras is supplied.

+

May also be set with the UV_NO_EDITABLE environment variable.

--no-extra no-extra

Disable the specified extra.

+

This options always takes precedence over default extras, --all-extras, and --extra.

May be provided multiple times.

--no-group no-group

Disable the specified dependency group.

This option always takes precedence over default groups, --all-groups, and --group.

@@ -1097,6 +1105,8 @@ uv sync [OPTIONS]

May also be set with the UV_OFFLINE environment variable.

--only-dev

Only include the development dependency group.

The project and its dependencies will be omitted.

This option is an alias for --only-group dev. Implies --no-default-groups.

+
--only-extra only-extra

Only include dependencies from the specified extra.

+

May be provided multiple times. Implies --no-default-extras.

--only-group only-group

Only include dependencies from the specified dependency group.

The project and its dependencies will be omitted.

May be provided multiple times. Implies --no-default-groups.

@@ -1430,7 +1440,9 @@ uv export [OPTIONS]

May also be set with the UV_NO_BUILD_PACKAGE environment variable.

--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

May also be set with the UV_NO_CACHE environment variable.

--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

-

May also be set with the UV_NO_CONFIG environment variable.

--no-default-groups

Ignore the default dependency groups.

+

May also be set with the UV_NO_CONFIG environment variable.

--no-default-extras

Ignore the default extras.

+

uv includes the extras defined in tool.uv.default-extras by default. This disables that option, however, specific extras can still be included with --extra.

+
--no-default-groups

Ignore the default dependency groups.

uv includes the groups defined in tool.uv.default-groups by default. This disables that option, however, specific groups can still be included with --group.

--no-dev

Disable the development dependency group.

This option is an alias of --no-group dev. See --no-default-groups to disable all default groups instead.

@@ -1441,7 +1453,8 @@ uv export [OPTIONS]

By default, the current project is included in the exported requirements file with all of its dependencies. The --no-emit-project option allows the project to be excluded, but all of its dependencies to remain included.

--no-emit-workspace, --no-install-workspace

Do not emit any workspace members, including the root project.

By default, all workspace members and their dependencies are included in the exported requirements file, with all of their dependencies. The --no-emit-workspace option allows exclusion of all the workspace members while retaining their dependencies.

-
--no-extra no-extra

Exclude the specified optional dependencies, if --all-extras is supplied.

+
--no-extra no-extra

Disable the specified extra.

+

This options always takes precedence over default extras, --all-extras, and --extra.

May be provided multiple times.

--no-group no-group

Disable the specified dependency group.

This option always takes precedence over default groups, --all-groups, and --group.

@@ -1460,6 +1473,8 @@ uv export [OPTIONS]

May also be set with the UV_OFFLINE environment variable.

--only-dev

Only include the development dependency group.

The project and its dependencies will be omitted.

This option is an alias for --only-group dev. Implies --no-default-groups.

+
--only-extra only-extra

Only include dependencies from the specified extra.

+

May be provided multiple times. Implies --no-default-extras.

--only-group only-group

Only include dependencies from the specified dependency group.

The project and its dependencies will be omitted.

May be provided multiple times. Implies --no-default-groups.

diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 54fabe83bb246..30144d27ae3f6 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -108,6 +108,25 @@ constraint-dependencies = ["grpcio<1.65"] --- +### [`default-extras`](#default-extras) {: #default-extras } + +The list of `optional-dependencies` extras to install by default. + +Can also be the literal "all" to default enable all extras. + +**Default value**: `[""]` + +**Type**: `str | list[str]` + +**Example usage**: + +```toml title="pyproject.toml" +[tool.uv] +default-extras = ["docs", "dev"] +``` + +--- + ### [`default-groups`](#default-groups) {: #default-groups } The list of `dependency-groups` to install by default. diff --git a/uv.schema.json b/uv.schema.json index d2ca69f20998d..8a1c9470425b7 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -140,6 +140,17 @@ "type": "string" } }, + "default-extras": { + "description": "The list of `optional-dependencies` extras to install by default.\n\nCan also be the literal \"all\" to default enable all extras.", + "anyOf": [ + { + "$ref": "#/definitions/DefaultExtras" + }, + { + "type": "null" + } + ] + }, "default-groups": { "description": "The list of `dependency-groups` to install by default.\n\nCan also be the literal `\"all\"` to default enable all groups.", "anyOf": [ @@ -795,6 +806,34 @@ "$ref": "#/definitions/ConfigSettingValue" } }, + "DefaultExtras": { + "description": "Either the literal \"all\" or a list of extras", + "oneOf": [ + { + "description": "All extras are defaulted", + "type": "string", + "enum": [ + "All" + ] + }, + { + "description": "A list of extras", + "type": "object", + "required": [ + "List" + ], + "properties": { + "List": { + "type": "array", + "items": { + "$ref": "#/definitions/ExtraName" + } + } + }, + "additionalProperties": false + } + ] + }, "DefaultGroups": { "description": "Either the literal \"all\" or a list of groups", "oneOf": [