Support `extras` and `dependency_groups` markers on `uv pip install` and `uv pip sync` (#14755)

## Summary

We don't yet support writing these, but we can at least read them
(which, e.g., allows you to install PDM-exported `pylock.toml` files
with uv, since PDM _always_ writes a default group).

Closes #14740.
This commit is contained in:
Charlie Marsh 2025-07-21 08:48:47 -04:00 committed by GitHub
parent ab48dfd0cb
commit b81cce9152
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 492 additions and 82 deletions

View File

@ -1202,6 +1202,14 @@ pub struct PipCompileArgs {
#[arg(long, overrides_with("all_extras"), hide = true)]
pub no_all_extras: bool,
/// Install the specified dependency group from a `pyproject.toml`.
///
/// If no path is provided, the `pyproject.toml` in the working directory is used.
///
/// May be provided multiple times.
#[arg(long, group = "sources")]
pub group: Vec<PipGroupName>,
#[command(flatten)]
pub resolver: ResolverArgs,
@ -1216,14 +1224,6 @@ pub struct PipCompileArgs {
#[arg(long, overrides_with("no_deps"), hide = true)]
pub deps: bool,
/// Install the specified dependency group from a `pyproject.toml`.
///
/// If no path is provided, the `pyproject.toml` in the working directory is used.
///
/// May be provided multiple times.
#[arg(long, group = "sources")]
pub group: Vec<PipGroupName>,
/// Write the compiled requirements to the given `requirements.txt` or `pylock.toml` file.
///
/// If the file already exists, the existing versions will be preferred when resolving
@ -1518,6 +1518,30 @@ pub struct PipSyncArgs {
#[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub build_constraints: Vec<Maybe<PathBuf>>,
/// Include optional dependencies from the specified extra name; may be provided more than once.
///
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
pub extra: Option<Vec<ExtraName>>,
/// Include all optional dependencies.
///
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
#[arg(long, conflicts_with = "extra", overrides_with = "no_all_extras")]
pub all_extras: bool,
#[arg(long, overrides_with("all_extras"), hide = true)]
pub no_all_extras: bool,
/// Install the specified dependency group from a `pylock.toml` or `pyproject.toml`.
///
/// If no path is provided, the `pylock.toml` or `pyproject.toml` in the working directory is
/// used.
///
/// May be provided multiple times.
#[arg(long, group = "sources")]
pub group: Vec<PipGroupName>,
#[command(flatten)]
pub installer: InstallerArgs,
@ -1798,19 +1822,28 @@ pub struct PipInstallArgs {
/// Include optional dependencies from the specified extra name; may be provided more than once.
///
/// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
pub extra: Option<Vec<ExtraName>>,
/// Include all optional dependencies.
///
/// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
#[arg(long, conflicts_with = "extra", overrides_with = "no_all_extras")]
pub all_extras: bool,
#[arg(long, overrides_with("all_extras"), hide = true)]
pub no_all_extras: bool,
/// Install the specified dependency group from a `pylock.toml` or `pyproject.toml`.
///
/// If no path is provided, the `pylock.toml` or `pyproject.toml` in the working directory is
/// used.
///
/// May be provided multiple times.
#[arg(long, group = "sources")]
pub group: Vec<PipGroupName>,
#[command(flatten)]
pub installer: ResolverInstallerArgs,
@ -1825,14 +1858,6 @@ pub struct PipInstallArgs {
#[arg(long, overrides_with("no_deps"), hide = true)]
pub deps: bool,
/// Install the specified dependency group from a `pyproject.toml`.
///
/// If no path is provided, the `pyproject.toml` in the working directory is used.
///
/// May be provided multiple times.
#[arg(long, group = "sources")]
pub group: Vec<PipGroupName>,
/// Require a matching hash for each requirement.
///
/// By default, uv will verify any available hashes in the requirements file, but will not

View File

@ -186,6 +186,18 @@ impl DependencyGroupsInner {
self.include.names().chain(&self.exclude)
}
/// Returns an iterator over all groups that are included in the specification,
/// assuming `all_names` is an iterator over all groups.
pub fn group_names<'a, Names>(
&'a self,
all_names: Names,
) -> impl Iterator<Item = &'a GroupName> + 'a
where
Names: Iterator<Item = &'a GroupName> + 'a,
{
all_names.filter(move |name| self.contains(name))
}
/// Iterate over all groups the user explicitly asked for on the CLI
pub fn explicit_names(&self) -> impl Iterator<Item = &GroupName> {
let DependencyGroupsHistory {

View File

@ -754,6 +754,51 @@ impl Display for MarkerExpression {
}
}
/// The extra and dependency group names to use when evaluating a marker tree.
#[derive(Debug, Copy, Clone)]
enum ExtrasEnvironment<'a> {
/// E.g., `extra == '...'`
Extras(&'a [ExtraName]),
/// E.g., `'...' in extras` or `'...' in dependency_groups`
Pep751(&'a [ExtraName], &'a [GroupName]),
}
impl<'a> ExtrasEnvironment<'a> {
/// Creates a new [`ExtrasEnvironment`] for the given `extra` names.
fn from_extras(extras: &'a [ExtraName]) -> Self {
Self::Extras(extras)
}
/// Creates a new [`ExtrasEnvironment`] for the given PEP 751 `extras` and `dependency_groups`.
fn from_pep751(extras: &'a [ExtraName], dependency_groups: &'a [GroupName]) -> Self {
Self::Pep751(extras, dependency_groups)
}
/// Returns the `extra` names in this environment.
fn extra(&self) -> &[ExtraName] {
match self {
ExtrasEnvironment::Extras(extra) => extra,
ExtrasEnvironment::Pep751(..) => &[],
}
}
/// Returns the `extras` names in this environment, as in a PEP 751 lockfile.
fn extras(&self) -> &[ExtraName] {
match self {
ExtrasEnvironment::Extras(..) => &[],
ExtrasEnvironment::Pep751(extras, ..) => extras,
}
}
/// Returns the `dependency_group` group names in this environment, as in a PEP 751 lockfile.
fn dependency_groups(&self) -> &[GroupName] {
match self {
ExtrasEnvironment::Extras(..) => &[],
ExtrasEnvironment::Pep751(.., groups) => groups,
}
}
}
/// Represents one or more nested marker expressions with and/or/parentheses.
///
/// Marker trees are canonical, meaning any two functionally equivalent markers
@ -1001,7 +1046,27 @@ impl MarkerTree {
/// Does this marker apply in the given environment?
pub fn evaluate(self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
self.evaluate_reporter_impl(env, extras, &mut TracingReporter)
self.evaluate_reporter_impl(
env,
ExtrasEnvironment::from_extras(extras),
&mut TracingReporter,
)
}
/// Evaluate a marker in the context of a PEP 751 lockfile, which exposes several additional
/// markers (`extras` and `dependency_groups`) that are not available in any other context,
/// per the spec.
pub fn evaluate_pep751(
self,
env: &MarkerEnvironment,
extras: &[ExtraName],
groups: &[GroupName],
) -> bool {
self.evaluate_reporter_impl(
env,
ExtrasEnvironment::from_pep751(extras, groups),
&mut TracingReporter,
)
}
/// Evaluates this marker tree against an optional environment and a
@ -1018,7 +1083,11 @@ impl MarkerTree {
) -> bool {
match env {
None => self.evaluate_extras(extras),
Some(env) => self.evaluate_reporter_impl(env, extras, &mut TracingReporter),
Some(env) => self.evaluate_reporter_impl(
env,
ExtrasEnvironment::from_extras(extras),
&mut TracingReporter,
),
}
}
@ -1030,13 +1099,13 @@ impl MarkerTree {
extras: &[ExtraName],
reporter: &mut impl Reporter,
) -> bool {
self.evaluate_reporter_impl(env, extras, reporter)
self.evaluate_reporter_impl(env, ExtrasEnvironment::from_extras(extras), reporter)
}
fn evaluate_reporter_impl(
self,
env: &MarkerEnvironment,
extras: &[ExtraName],
extras: ExtrasEnvironment,
reporter: &mut impl Reporter,
) -> bool {
match self.kind() {
@ -1088,12 +1157,18 @@ impl MarkerTree {
}
MarkerTreeKind::Extra(marker) => {
return marker
.edge(extras.contains(marker.name().extra()))
.edge(extras.extra().contains(marker.name().extra()))
.evaluate_reporter_impl(env, extras, reporter);
}
// TODO(charlie): Add support for evaluating container extras in PEP 751 lockfiles.
MarkerTreeKind::Extras(..) | MarkerTreeKind::DependencyGroups(..) => {
return false;
MarkerTreeKind::Extras(marker) => {
return marker
.edge(extras.extras().contains(marker.name().extra()))
.evaluate_reporter_impl(env, extras, reporter);
}
MarkerTreeKind::DependencyGroups(marker) => {
return marker
.edge(extras.dependency_groups().contains(marker.name().group()))
.evaluate_reporter_impl(env, extras, reporter);
}
}

View File

@ -273,13 +273,13 @@ impl RequirementsSource {
pub fn allows_extras(&self) -> bool {
matches!(
self,
Self::PyprojectToml(_) | Self::SetupPy(_) | Self::SetupCfg(_)
Self::PylockToml(_) | Self::PyprojectToml(_) | Self::SetupPy(_) | Self::SetupCfg(_)
)
}
/// Returns `true` if the source allows groups to be specified.
pub fn allows_groups(&self) -> bool {
matches!(self, Self::PyprojectToml(_))
matches!(self, Self::PylockToml(_) | Self::PyprojectToml(_))
}
}

View File

@ -250,10 +250,13 @@ impl RequirementsSpecification {
// If we have a `pylock.toml`, don't allow additional requirements, constraints, or
// overrides.
if requirements
.iter()
.any(|source| matches!(source, RequirementsSource::PylockToml(..)))
{
if let Some(pylock_toml) = requirements.iter().find_map(|source| {
if let RequirementsSource::PylockToml(path) = source {
Some(path)
} else {
None
}
}) {
if requirements
.iter()
.any(|source| !matches!(source, RequirementsSource::PylockToml(..)))
@ -272,22 +275,38 @@ impl RequirementsSpecification {
"Cannot specify constraints with a `pylock.toml` file"
));
}
if groups.is_some_and(|groups| !groups.groups.is_empty()) {
// If we have a `pylock.toml`, disallow specifying paths for groups; instead, require
// that all groups refer to the `pylock.toml` file.
if let Some(groups) = groups {
let mut names = Vec::new();
for group in &groups.groups {
if group.path.is_some() {
return Err(anyhow::anyhow!(
"Cannot specify groups with a `pylock.toml` file"
"Cannot specify paths for groups with a `pylock.toml` file; all groups must refer to the `pylock.toml` file"
));
}
names.push(group.name.clone());
}
// Resolve sources into specifications so we know their `source_tree`.
let mut requirement_sources = Vec::new();
for source in requirements {
let source = Self::from_source(source, client_builder).await?;
requirement_sources.push(source);
if !names.is_empty() {
spec.groups.insert(
pylock_toml.clone(),
DependencyGroups::from_args(
false,
false,
false,
Vec::new(),
Vec::new(),
false,
names,
false,
),
);
}
// pip `--group` flags specify their own sources, which we need to process here
if let Some(groups) = groups {
}
} else if let Some(groups) = groups {
// pip `--group` flags specify their own sources, which we need to process here.
// First, we collect all groups by their path.
let mut groups_by_path = BTreeMap::new();
for group in &groups.groups {
@ -320,6 +339,13 @@ impl RequirementsSpecification {
spec.groups = group_specs;
}
// Resolve sources into specifications so we know their `source_tree`.
let mut requirement_sources = Vec::new();
for source in requirements {
let source = Self::from_source(source, client_builder).await?;
requirement_sources.push(source);
}
// Read all requirements, and keep track of all requirements _and_ constraints.
// A `requirements.txt` can contain a `-c constraints.txt` directive within it, so reading
// a requirements file can also add constraints.

View File

@ -188,11 +188,11 @@ pub struct PylockToml {
#[serde(skip_serializing_if = "Option::is_none")]
requires_python: Option<RequiresPython>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
extras: Vec<ExtraName>,
pub extras: Vec<ExtraName>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
dependency_groups: Vec<GroupName>,
pub dependency_groups: Vec<GroupName>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
default_groups: Vec<GroupName>,
pub default_groups: Vec<GroupName>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub packages: Vec<PylockTomlPackage>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
@ -966,9 +966,12 @@ impl<'lock> PylockToml {
self,
install_path: &Path,
markers: &MarkerEnvironment,
extras: &[ExtraName],
groups: &[GroupName],
tags: &Tags,
build_options: &BuildOptions,
) -> Result<Resolution, PylockTomlError> {
// Convert the extras and dependency groups specifications to a concrete environment.
let mut graph =
petgraph::graph::DiGraph::with_capacity(self.packages.len(), self.packages.len());
@ -977,7 +980,7 @@ impl<'lock> PylockToml {
for package in self.packages {
// Omit packages that aren't relevant to the current environment.
if !package.marker.evaluate(markers, &[]) {
if !package.marker.evaluate_pep751(markers, extras, groups) {
continue;
}

View File

@ -22,6 +22,7 @@ use uv_distribution_types::{
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_normalize::{DefaultExtras, DefaultGroups};
use uv_pep508::PackageName;
use uv_pypi_types::Conflicts;
use uv_python::{
@ -439,11 +440,35 @@ pub(crate) async fn pip_install(
let install_path = std::path::absolute(&pylock)?;
let install_path = install_path.parent().unwrap();
let content = fs_err::tokio::read_to_string(&pylock).await?;
let lock = toml::from_str::<PylockToml>(&content)
.with_context(|| format!("Not a valid pylock.toml file: {}", pylock.user_display()))?;
let lock = toml::from_str::<PylockToml>(&content).with_context(|| {
format!("Not a valid `pylock.toml` file: {}", pylock.user_display())
})?;
let resolution =
lock.to_resolution(install_path, marker_env.markers(), &tags, &build_options)?;
// Convert the extras and groups specifications into a concrete form.
let extras = extras.with_defaults(DefaultExtras::default());
let extras = extras
.extra_names(lock.extras.iter())
.cloned()
.collect::<Vec<_>>();
let groups = groups
.get(&pylock)
.cloned()
.unwrap_or_default()
.with_defaults(DefaultGroups::List(lock.default_groups.clone()));
let groups = groups
.group_names(lock.dependency_groups.iter())
.cloned()
.collect::<Vec<_>>();
let resolution = lock.to_resolution(
install_path,
marker_env.markers(),
&extras,
&groups,
&tags,
&build_options,
)?;
let hasher = HashStrategy::from_resolution(&resolution, HashCheckingMode::Verify)?;
(resolution, hasher)

View File

@ -70,7 +70,7 @@ pub(crate) async fn read_requirements(
"Use `package[extra]` syntax instead."
};
return Err(anyhow!(
"Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. {hint}"
"Requesting extras requires a `pylock.toml`, `pyproject.toml`, `setup.cfg`, or `setup.py` file. {hint}"
)
.into());
}

View File

@ -18,13 +18,14 @@ use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, Origin, R
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_installer::SitePackages;
use uv_normalize::{DefaultExtras, DefaultGroups};
use uv_pep508::PackageName;
use uv_pypi_types::Conflicts;
use uv_python::{
EnvironmentPreference, Prefix, PythonEnvironment, PythonInstallation, PythonPreference,
PythonRequest, PythonVersion, Target,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_requirements::{GroupsSpecification, RequirementsSource, RequirementsSpecification};
use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PrereleaseMode, PylockToml,
PythonRequirement, ResolutionMode, ResolverEnvironment,
@ -48,6 +49,8 @@ pub(crate) async fn pip_sync(
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
build_constraints: &[RequirementsSource],
extras: &ExtrasSpecification,
groups: &GroupsSpecification,
reinstall: Reinstall,
link_mode: LinkMode,
compile: bool,
@ -91,8 +94,6 @@ pub(crate) async fn pip_sync(
// Initialize a few defaults.
let overrides = &[];
let extras = ExtrasSpecification::default();
let groups = None;
let upgrade = Upgrade::default();
let resolution_mode = ResolutionMode::default();
let prerelease_mode = PrereleaseMode::default();
@ -118,8 +119,8 @@ pub(crate) async fn pip_sync(
requirements,
constraints,
overrides,
&extras,
groups,
extras,
Some(groups),
&client_builder,
)
.await?;
@ -377,11 +378,35 @@ pub(crate) async fn pip_sync(
let install_path = std::path::absolute(&pylock)?;
let install_path = install_path.parent().unwrap();
let content = fs_err::tokio::read_to_string(&pylock).await?;
let lock = toml::from_str::<PylockToml>(&content)
.with_context(|| format!("Not a valid pylock.toml file: {}", pylock.user_display()))?;
let lock = toml::from_str::<PylockToml>(&content).with_context(|| {
format!("Not a valid `pylock.toml` file: {}", pylock.user_display())
})?;
let resolution =
lock.to_resolution(install_path, marker_env.markers(), &tags, &build_options)?;
// Convert the extras and groups specifications into a concrete form.
let extras = extras.with_defaults(DefaultExtras::default());
let extras = extras
.extra_names(lock.extras.iter())
.cloned()
.collect::<Vec<_>>();
let groups = groups
.get(&pylock)
.cloned()
.unwrap_or_default()
.with_defaults(DefaultGroups::List(lock.default_groups.clone()));
let groups = groups
.group_names(lock.dependency_groups.iter())
.cloned()
.collect::<Vec<_>>();
let resolution = lock.to_resolution(
install_path,
marker_env.markers(),
&extras,
&groups,
&tags,
&build_options,
)?;
let hasher = HashStrategy::from_resolution(&resolution, HashCheckingMode::Verify)?;
(resolution, hasher)
@ -406,7 +431,7 @@ pub(crate) async fn pip_sync(
source_trees,
project,
BTreeSet::default(),
&extras,
extras,
&groups,
preferences,
site_packages.clone(),

View File

@ -566,11 +566,17 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Result<Vec<_>, _>>()?;
let groups = GroupsSpecification {
root: project_dir.to_path_buf(),
groups: args.settings.groups,
};
commands::pip_sync(
&requirements,
&constraints,
&build_constraints,
&args.settings.extras,
&groups,
args.settings.reinstall,
args.settings.link_mode,
args.settings.compile_bytecode,

View File

@ -2058,6 +2058,10 @@ impl PipSyncSettings {
src_file,
constraints,
build_constraints,
extra,
all_extras,
no_all_extras,
group,
installer,
refresh,
require_hashes,
@ -2122,6 +2126,9 @@ impl PipSyncSettings {
python_version,
python_platform,
strict: flag(strict, no_strict, "strict"),
extra,
all_extras: flag(all_extras, no_all_extras, "all-extras"),
group: Some(group),
torch_backend,
..PipOptions::from(installer)
},

View File

@ -1298,27 +1298,27 @@ fn install_extras() -> Result<()> {
uv_snapshot!(context.filters(), context.pip_install()
.arg("--all-extras")
.arg("-e")
.arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###"
.arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `<dir>[extra]` syntax or `-r <file>` instead.
"###
error: Requesting extras requires a `pylock.toml`, `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `<dir>[extra]` syntax or `-r <file>` instead.
"
);
// Request extras for a source tree
uv_snapshot!(context.filters(), context.pip_install()
.arg("--all-extras")
.arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###"
.arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `package[extra]` syntax instead.
"###
error: Requesting extras requires a `pylock.toml`, `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `package[extra]` syntax instead.
"
);
let requirements_txt = context.temp_dir.child("requirements.txt");
@ -1327,14 +1327,14 @@ fn install_extras() -> Result<()> {
// Request extras for a requirements file
uv_snapshot!(context.filters(), context.pip_install()
.arg("--all-extras")
.arg("-r").arg("requirements.txt"), @r###"
.arg("-r").arg("requirements.txt"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `package[extra]` syntax instead.
"###
error: Requesting extras requires a `pylock.toml`, `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `package[extra]` syntax instead.
"
);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
@ -11392,6 +11392,205 @@ fn pep_751_multiple_sources() -> Result<()> {
Ok(())
}
#[test]
fn pep_751_groups() -> Result<()> {
let context = TestContext::new("3.13");
let pylock_toml = context.temp_dir.child("pylock.toml");
pylock_toml.write_str(
r#"
lock-version = "1.0"
requires-python = "==3.13.*"
environments = [
"python_version == \"3.13\"",
]
extras = ["async", "dev"]
dependency-groups = ["default", "test"]
default-groups = ["default"]
created-by = "pdm"
[[packages]]
name = "anyio"
version = "4.9.0"
requires-python = ">=3.9"
sdist = {name = "anyio-4.9.0.tar.gz", url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hashes = {sha256 = "673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}}
wheels = [
{name = "anyio-4.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl",hashes = {sha256 = "9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}},
]
marker = "\"async\" in extras"
[packages.tool.pdm]
dependencies = [
"exceptiongroup>=1.0.2; python_version < \"3.11\"",
"idna>=2.8",
"sniffio>=1.1",
"typing-extensions>=4.5; python_version < \"3.13\"",
]
[[packages]]
name = "blinker"
version = "1.9.0"
requires-python = ">=3.9"
sdist = {name = "blinker-1.9.0.tar.gz", url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hashes = {sha256 = "b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}}
wheels = [
{name = "blinker-1.9.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl",hashes = {sha256 = "ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}},
]
marker = "\"dev\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "idna"
version = "3.10"
requires-python = ">=3.6"
sdist = {name = "idna-3.10.tar.gz", url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hashes = {sha256 = "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}}
wheels = [
{name = "idna-3.10-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl",hashes = {sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}},
]
marker = "\"async\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "iniconfig"
version = "2.1.0"
requires-python = ">=3.8"
sdist = {name = "iniconfig-2.1.0.tar.gz", url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hashes = {sha256 = "3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}}
wheels = [
{name = "iniconfig-2.1.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl",hashes = {sha256 = "9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}},
]
marker = "\"default\" in dependency_groups"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "pygments"
version = "2.19.2"
requires-python = ">=3.8"
sdist = {name = "pygments-2.19.2.tar.gz", url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hashes = {sha256 = "636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}}
wheels = [
{name = "pygments-2.19.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl",hashes = {sha256 = "86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}},
]
marker = "\"test\" in dependency_groups"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "sniffio"
version = "1.3.1"
requires-python = ">=3.7"
sdist = {name = "sniffio-1.3.1.tar.gz", url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hashes = {sha256 = "f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}}
wheels = [
{name = "sniffio-1.3.1-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl",hashes = {sha256 = "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}},
]
marker = "\"async\" in extras"
[packages.tool.pdm]
dependencies = []
[tool.pdm]
hashes = {sha256 = "51795362d337720c28bd6c3a26eb33751f2b69590261f599ffb4172ee2c441c6"}
[[tool.pdm.targets]]
requires_python = "==3.13.*"
"#,
)?;
// By default, only `iniconfig` should be installed, since it's in the default group.
uv_snapshot!(context.filters(), context.pip_install()
.arg("--preview")
.arg("-r")
.arg("pylock.toml"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.1.0
"
);
// With `--extra async`, `anyio` should be installed.
uv_snapshot!(context.filters(), context.pip_install()
.arg("--preview")
.arg("-r")
.arg("pylock.toml")
.arg("--extra")
.arg("async"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.9.0
+ idna==3.10
+ sniffio==1.3.1
"
);
// With `--group test`, `pygments` should be installed.
uv_snapshot!(context.filters(), context.pip_install()
.arg("--preview")
.arg("-r")
.arg("pylock.toml")
.arg("--group")
.arg("test"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ pygments==2.19.2
"
);
// With `--all-extras`, `blinker` should be installed.
uv_snapshot!(context.filters(), context.pip_install()
.arg("--preview")
.arg("-r")
.arg("pylock.toml")
.arg("--all-extras"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ blinker==1.9.0
"
);
// `--group pylock.toml:test` should be rejeceted.
uv_snapshot!(context.filters(), context.pip_install()
.arg("--preview")
.arg("-r")
.arg("pylock.toml")
.arg("--group")
.arg("pylock.toml:test"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'pylock.toml:test' for '--group <GROUP>': The `--group` path is required to end in 'pyproject.toml' for compatibility with pip; got: pylock.toml
For more information, try '--help'.
"
);
Ok(())
}
/// Test that uv doesn't hang if an index returns a distribution for the wrong package.
#[tokio::test]
async fn bogus_redirect() -> Result<()> {

View File

@ -3637,7 +3637,9 @@ uv pip sync [OPTIONS] <SRC_FILE>...
<h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt id="uv-pip-sync--allow-empty-requirements"><a href="#uv-pip-sync--allow-empty-requirements"><code>--allow-empty-requirements</code></a></dt><dd><p>Allow sync of empty requirements, which will clear the environment of all packages</p>
<dl class="cli-reference"><dt id="uv-pip-sync--all-extras"><a href="#uv-pip-sync--all-extras"><code>--all-extras</code></a></dt><dd><p>Include all optional dependencies.</p>
<p>Only applies to <code>pylock.toml</code>, <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>
</dd><dt id="uv-pip-sync--allow-empty-requirements"><a href="#uv-pip-sync--allow-empty-requirements"><code>--allow-empty-requirements</code></a></dt><dd><p>Allow sync of empty requirements, which will clear the environment of all packages</p>
</dd><dt id="uv-pip-sync--allow-insecure-host"><a href="#uv-pip-sync--allow-insecure-host"><code>--allow-insecure-host</code></a>, <code>--trusted-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
<p>Can be provided multiple times.</p>
<p>Expects to receive either a hostname (e.g., <code>localhost</code>), a host-port pair (e.g., <code>localhost:8080</code>), or a URL (e.g., <code>https://localhost</code>).</p>
@ -3675,13 +3677,18 @@ uv pip sync [OPTIONS] <SRC_FILE>...
</dd><dt id="uv-pip-sync--dry-run"><a href="#uv-pip-sync--dry-run"><code>--dry-run</code></a></dt><dd><p>Perform a dry run, i.e., don't actually install anything but resolve the dependencies and print the resulting plan</p>
</dd><dt id="uv-pip-sync--exclude-newer"><a href="#uv-pip-sync--exclude-newer"><code>--exclude-newer</code></a> <i>exclude-newer</i></dt><dd><p>Limit candidate packages to those that were uploaded prior to the given date.</p>
<p>Accepts both RFC 3339 timestamps (e.g., <code>2006-12-02T02:07:43Z</code>) and local dates in the same format (e.g., <code>2006-12-02</code>) in your system's configured time zone.</p>
<p>May also be set with the <code>UV_EXCLUDE_NEWER</code> environment variable.</p></dd><dt id="uv-pip-sync--extra-index-url"><a href="#uv-pip-sync--extra-index-url"><code>--extra-index-url</code></a> <i>extra-index-url</i></dt><dd><p>(Deprecated: use <code>--index</code> instead) Extra URLs of package indexes to use, in addition to <code>--index-url</code>.</p>
<p>May also be set with the <code>UV_EXCLUDE_NEWER</code> environment variable.</p></dd><dt id="uv-pip-sync--extra"><a href="#uv-pip-sync--extra"><code>--extra</code></a> <i>extra</i></dt><dd><p>Include optional dependencies from the specified extra name; may be provided more than once.</p>
<p>Only applies to <code>pylock.toml</code>, <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>
</dd><dt id="uv-pip-sync--extra-index-url"><a href="#uv-pip-sync--extra-index-url"><code>--extra-index-url</code></a> <i>extra-index-url</i></dt><dd><p>(Deprecated: use <code>--index</code> instead) Extra URLs of package indexes to use, in addition to <code>--index-url</code>.</p>
<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>
<p>All indexes provided via this flag take priority over the index specified by <code>--index-url</code> (which defaults to PyPI). When multiple <code>--extra-index-url</code> flags are provided, earlier values take priority.</p>
<p>May also be set with the <code>UV_EXTRA_INDEX_URL</code> environment variable.</p></dd><dt id="uv-pip-sync--find-links"><a href="#uv-pip-sync--find-links"><code>--find-links</code></a>, <code>-f</code> <i>find-links</i></dt><dd><p>Locations to search for candidate distributions, in addition to those found in the registry indexes.</p>
<p>If a path, the target must be a directory that contains packages as wheel files (<code>.whl</code>) or source distributions (e.g., <code>.tar.gz</code> or <code>.zip</code>) at the top level.</p>
<p>If a URL, the page must contain a flat list of links to package files adhering to the formats described above.</p>
<p>May also be set with the <code>UV_FIND_LINKS</code> environment variable.</p></dd><dt id="uv-pip-sync--help"><a href="#uv-pip-sync--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
<p>May also be set with the <code>UV_FIND_LINKS</code> environment variable.</p></dd><dt id="uv-pip-sync--group"><a href="#uv-pip-sync--group"><code>--group</code></a> <i>group</i></dt><dd><p>Install the specified dependency group from a <code>pylock.toml</code> or <code>pyproject.toml</code>.</p>
<p>If no path is provided, the <code>pylock.toml</code> or <code>pyproject.toml</code> in the working directory is used.</p>
<p>May be provided multiple times.</p>
</dd><dt id="uv-pip-sync--help"><a href="#uv-pip-sync--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
</dd><dt id="uv-pip-sync--index"><a href="#uv-pip-sync--index"><code>--index</code></a> <i>index</i></dt><dd><p>The URLs to use when resolving dependencies, in addition to the default index.</p>
<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>
<p>All indexes provided via this flag take priority over the index specified by <code>--default-index</code> (which defaults to PyPI). When multiple <code>--index</code> flags are provided, earlier values take priority.</p>
@ -3888,7 +3895,7 @@ uv pip install [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>|--editable <EDIT
<h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt id="uv-pip-install--all-extras"><a href="#uv-pip-install--all-extras"><code>--all-extras</code></a></dt><dd><p>Include all optional dependencies.</p>
<p>Only applies to <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>
<p>Only applies to <code>pylock.toml</code>, <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>
</dd><dt id="uv-pip-install--allow-insecure-host"><a href="#uv-pip-install--allow-insecure-host"><code>--allow-insecure-host</code></a>, <code>--trusted-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
<p>Can be provided multiple times.</p>
<p>Expects to receive either a hostname (e.g., <code>localhost</code>), a host-port pair (e.g., <code>localhost:8080</code>), or a URL (e.g., <code>https://localhost</code>).</p>
@ -3930,7 +3937,7 @@ uv pip install [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>|--editable <EDIT
</dd><dt id="uv-pip-install--exclude-newer"><a href="#uv-pip-install--exclude-newer"><code>--exclude-newer</code></a> <i>exclude-newer</i></dt><dd><p>Limit candidate packages to those that were uploaded prior to the given date.</p>
<p>Accepts both RFC 3339 timestamps (e.g., <code>2006-12-02T02:07:43Z</code>) and local dates in the same format (e.g., <code>2006-12-02</code>) in your system's configured time zone.</p>
<p>May also be set with the <code>UV_EXCLUDE_NEWER</code> environment variable.</p></dd><dt id="uv-pip-install--extra"><a href="#uv-pip-install--extra"><code>--extra</code></a> <i>extra</i></dt><dd><p>Include optional dependencies from the specified extra name; may be provided more than once.</p>
<p>Only applies to <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>
<p>Only applies to <code>pylock.toml</code>, <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>
</dd><dt id="uv-pip-install--extra-index-url"><a href="#uv-pip-install--extra-index-url"><code>--extra-index-url</code></a> <i>extra-index-url</i></dt><dd><p>(Deprecated: use <code>--index</code> instead) Extra URLs of package indexes to use, in addition to <code>--index-url</code>.</p>
<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>
<p>All indexes provided via this flag take priority over the index specified by <code>--index-url</code> (which defaults to PyPI). When multiple <code>--extra-index-url</code> flags are provided, earlier values take priority.</p>
@ -3944,8 +3951,8 @@ uv pip install [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>|--editable <EDIT
<ul>
<li><code>fewest</code>: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms</li>
<li><code>requires-python</code>: Optimize for selecting latest supported version of each package, for each supported Python version</li>
</ul></dd><dt id="uv-pip-install--group"><a href="#uv-pip-install--group"><code>--group</code></a> <i>group</i></dt><dd><p>Install the specified dependency group from a <code>pyproject.toml</code>.</p>
<p>If no path is provided, the <code>pyproject.toml</code> in the working directory is used.</p>
</ul></dd><dt id="uv-pip-install--group"><a href="#uv-pip-install--group"><code>--group</code></a> <i>group</i></dt><dd><p>Install the specified dependency group from a <code>pylock.toml</code> or <code>pyproject.toml</code>.</p>
<p>If no path is provided, the <code>pylock.toml</code> or <code>pyproject.toml</code> in the working directory is used.</p>
<p>May be provided multiple times.</p>
</dd><dt id="uv-pip-install--help"><a href="#uv-pip-install--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
</dd><dt id="uv-pip-install--index"><a href="#uv-pip-install--index"><code>--index</code></a> <i>index</i></dt><dd><p>The URLs to use when resolving dependencies, in addition to the default index.</p>