Skip to content

Commit e74821e

Browse files
committed
Support extras and dependency_groups markers on uv pip install and uv pip sync
1 parent 87af49d commit e74821e

File tree

20 files changed

+5390
-159
lines changed

20 files changed

+5390
-159
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,14 @@ pub struct PipCompileArgs {
12021202
#[arg(long, overrides_with("all_extras"), hide = true)]
12031203
pub no_all_extras: bool,
12041204

1205+
/// Install the specified dependency group from a `pyproject.toml`.
1206+
///
1207+
/// If no path is provided, the `pyproject.toml` in the working directory is used.
1208+
///
1209+
/// May be provided multiple times.
1210+
#[arg(long, group = "sources")]
1211+
pub group: Vec<PipGroupName>,
1212+
12051213
#[command(flatten)]
12061214
pub resolver: ResolverArgs,
12071215

@@ -1216,14 +1224,6 @@ pub struct PipCompileArgs {
12161224
#[arg(long, overrides_with("no_deps"), hide = true)]
12171225
pub deps: bool,
12181226

1219-
/// Install the specified dependency group from a `pyproject.toml`.
1220-
///
1221-
/// If no path is provided, the `pyproject.toml` in the working directory is used.
1222-
///
1223-
/// May be provided multiple times.
1224-
#[arg(long, group = "sources")]
1225-
pub group: Vec<PipGroupName>,
1226-
12271227
/// Write the compiled requirements to the given `requirements.txt` or `pylock.toml` file.
12281228
///
12291229
/// If the file already exists, the existing versions will be preferred when resolving
@@ -1518,6 +1518,30 @@ pub struct PipSyncArgs {
15181518
#[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
15191519
pub build_constraints: Vec<Maybe<PathBuf>>,
15201520

1521+
/// Include optional dependencies from the specified extra name; may be provided more than once.
1522+
///
1523+
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
1524+
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
1525+
pub extra: Option<Vec<ExtraName>>,
1526+
1527+
/// Include all optional dependencies.
1528+
///
1529+
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
1530+
#[arg(long, conflicts_with = "extra", overrides_with = "no_all_extras")]
1531+
pub all_extras: bool,
1532+
1533+
#[arg(long, overrides_with("all_extras"), hide = true)]
1534+
pub no_all_extras: bool,
1535+
1536+
/// Install the specified dependency group from a `pylock.toml` or `pyproject.toml`.
1537+
///
1538+
/// If no path is provided, the `pylock.toml` or `pyproject.toml` in the working directory is
1539+
/// used.
1540+
///
1541+
/// May be provided multiple times.
1542+
#[arg(long, group = "sources")]
1543+
pub group: Vec<PipGroupName>,
1544+
15211545
#[command(flatten)]
15221546
pub installer: InstallerArgs,
15231547

@@ -1798,19 +1822,28 @@ pub struct PipInstallArgs {
17981822

17991823
/// Include optional dependencies from the specified extra name; may be provided more than once.
18001824
///
1801-
/// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
1825+
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
18021826
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
18031827
pub extra: Option<Vec<ExtraName>>,
18041828

18051829
/// Include all optional dependencies.
18061830
///
1807-
/// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
1831+
/// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
18081832
#[arg(long, conflicts_with = "extra", overrides_with = "no_all_extras")]
18091833
pub all_extras: bool,
18101834

18111835
#[arg(long, overrides_with("all_extras"), hide = true)]
18121836
pub no_all_extras: bool,
18131837

1838+
/// Install the specified dependency group from a `pylock.toml` or `pyproject.toml`.
1839+
///
1840+
/// If no path is provided, the `pylock.toml` or `pyproject.toml` in the working directory is
1841+
/// used.
1842+
///
1843+
/// May be provided multiple times.
1844+
#[arg(long, group = "sources")]
1845+
pub group: Vec<PipGroupName>,
1846+
18141847
#[command(flatten)]
18151848
pub installer: ResolverInstallerArgs,
18161849

@@ -1825,14 +1858,6 @@ pub struct PipInstallArgs {
18251858
#[arg(long, overrides_with("no_deps"), hide = true)]
18261859
pub deps: bool,
18271860

1828-
/// Install the specified dependency group from a `pyproject.toml`.
1829-
///
1830-
/// If no path is provided, the `pyproject.toml` in the working directory is used.
1831-
///
1832-
/// May be provided multiple times.
1833-
#[arg(long, group = "sources")]
1834-
pub group: Vec<PipGroupName>,
1835-
18361861
/// Require a matching hash for each requirement.
18371862
///
18381863
/// By default, uv will verify any available hashes in the requirements file, but will not

crates/uv-configuration/src/dependency_groups.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ impl DependencyGroupsInner {
186186
self.include.names().chain(&self.exclude)
187187
}
188188

189+
/// Returns an iterator over all groups that are included in the specification,
190+
/// assuming `all_names` is an iterator over all groups.
191+
pub fn group_names<'a, Names>(
192+
&'a self,
193+
all_names: Names,
194+
) -> impl Iterator<Item = &'a GroupName> + 'a
195+
where
196+
Names: Iterator<Item = &'a GroupName> + 'a,
197+
{
198+
all_names.filter(move |name| self.contains(name))
199+
}
200+
189201
/// Iterate over all groups the user explicitly asked for on the CLI
190202
pub fn explicit_names(&self) -> impl Iterator<Item = &GroupName> {
191203
let DependencyGroupsHistory {

crates/uv-configuration/src/extras.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ impl ExtrasSpecificationInner {
155155
self.include.names().chain(&self.exclude)
156156
}
157157

158-
/// Returns `true` if the specification includes the given extra.
158+
/// Returns an iterator over all extras that are included in the specification,
159+
/// assuming `all_names` is an iterator over all extras.
159160
pub fn extra_names<'a, Names>(
160161
&'a self,
161162
all_names: Names,

crates/uv-normalize/src/group_name.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::fmt::{Display, Formatter};
2-
use std::path::{Path, PathBuf};
2+
use std::path::PathBuf;
33
use std::str::FromStr;
44
use std::sync::LazyLock;
55

@@ -98,17 +98,6 @@ pub struct PipGroupName {
9898
pub name: GroupName,
9999
}
100100

101-
impl PipGroupName {
102-
/// Gets the path to use, applying the default if it's missing
103-
pub fn path(&self) -> &Path {
104-
if let Some(path) = &self.path {
105-
path
106-
} else {
107-
Path::new("pyproject.toml")
108-
}
109-
}
110-
}
111-
112101
impl FromStr for PipGroupName {
113102
type Err = InvalidPipGroupError;
114103

crates/uv-pep508/src/marker/tree.rs

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,34 @@ impl MarkerTree {
986986

987987
/// Does this marker apply in the given environment?
988988
pub fn evaluate(self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {
989-
self.evaluate_reporter_impl(env, extras, &mut TracingReporter)
989+
let pep751_extras = &[];
990+
let pep751_groups = &[];
991+
self.evaluate_reporter_impl(
992+
env,
993+
extras,
994+
pep751_extras,
995+
pep751_groups,
996+
&mut TracingReporter,
997+
)
998+
}
999+
1000+
/// Evaluate a marker in the context of a PEP 751 lockfile, which exposes several additional
1001+
/// markers (`extras` and `dependency_groups`) that are not available in any other context,
1002+
/// per the spec.
1003+
pub fn evaluate_pep751(
1004+
self,
1005+
env: &MarkerEnvironment,
1006+
pep751_extras: &[ExtraName],
1007+
pep751_groups: &[GroupName],
1008+
) -> bool {
1009+
let extras = &[];
1010+
self.evaluate_reporter_impl(
1011+
env,
1012+
extras,
1013+
pep751_extras,
1014+
pep751_groups,
1015+
&mut TracingReporter,
1016+
)
9901017
}
9911018

9921019
/// Evaluates this marker tree against an optional environment and a
@@ -1003,7 +1030,17 @@ impl MarkerTree {
10031030
) -> bool {
10041031
match env {
10051032
None => self.evaluate_extras(extras),
1006-
Some(env) => self.evaluate_reporter_impl(env, extras, &mut TracingReporter),
1033+
Some(env) => {
1034+
let pep751_extras = &[];
1035+
let pep751_groups = &[];
1036+
self.evaluate_reporter_impl(
1037+
env,
1038+
extras,
1039+
pep751_extras,
1040+
pep751_groups,
1041+
&mut TracingReporter,
1042+
)
1043+
}
10071044
}
10081045
}
10091046

@@ -1015,13 +1052,17 @@ impl MarkerTree {
10151052
extras: &[ExtraName],
10161053
reporter: &mut impl Reporter,
10171054
) -> bool {
1018-
self.evaluate_reporter_impl(env, extras, reporter)
1055+
let pep751_extras = &[];
1056+
let pep751_groups = &[];
1057+
self.evaluate_reporter_impl(env, extras, pep751_extras, pep751_groups, reporter)
10191058
}
10201059

10211060
fn evaluate_reporter_impl(
10221061
self,
10231062
env: &MarkerEnvironment,
10241063
extras: &[ExtraName],
1064+
pep751_extras: &[ExtraName],
1065+
pep751_groups: &[GroupName],
10251066
reporter: &mut impl Reporter,
10261067
) -> bool {
10271068
match self.kind() {
@@ -1030,7 +1071,13 @@ impl MarkerTree {
10301071
MarkerTreeKind::Version(marker) => {
10311072
for (range, tree) in marker.edges() {
10321073
if range.contains(env.get_version(marker.key())) {
1033-
return tree.evaluate_reporter_impl(env, extras, reporter);
1074+
return tree.evaluate_reporter_impl(
1075+
env,
1076+
extras,
1077+
pep751_extras,
1078+
pep751_groups,
1079+
reporter,
1080+
);
10341081
}
10351082
}
10361083
}
@@ -1057,28 +1104,40 @@ impl MarkerTree {
10571104
}
10581105

10591106
if range.contains(l_string) {
1060-
return tree.evaluate_reporter_impl(env, extras, reporter);
1107+
return tree.evaluate_reporter_impl(
1108+
env,
1109+
extras,
1110+
pep751_extras,
1111+
pep751_groups,
1112+
reporter,
1113+
);
10611114
}
10621115
}
10631116
}
10641117
MarkerTreeKind::In(marker) => {
10651118
return marker
10661119
.edge(marker.value().contains(env.get_string(marker.key())))
1067-
.evaluate_reporter_impl(env, extras, reporter);
1120+
.evaluate_reporter_impl(env, extras, pep751_extras, pep751_groups, reporter);
10681121
}
10691122
MarkerTreeKind::Contains(marker) => {
10701123
return marker
10711124
.edge(env.get_string(marker.key()).contains(marker.value()))
1072-
.evaluate_reporter_impl(env, extras, reporter);
1125+
.evaluate_reporter_impl(env, extras, pep751_extras, pep751_groups, reporter);
10731126
}
10741127
MarkerTreeKind::Extra(marker) => {
10751128
return marker
10761129
.edge(extras.contains(marker.name().extra()))
1077-
.evaluate_reporter_impl(env, extras, reporter);
1130+
.evaluate_reporter_impl(env, extras, pep751_extras, pep751_groups, reporter);
10781131
}
1079-
// TODO(charlie): Add support for evaluating container extras in PEP 751 lockfiles.
1080-
MarkerTreeKind::Extras(..) | MarkerTreeKind::DependencyGroups(..) => {
1081-
return false;
1132+
MarkerTreeKind::Extras(marker) => {
1133+
return marker
1134+
.edge(pep751_extras.contains(marker.name().extra()))
1135+
.evaluate_reporter_impl(env, extras, pep751_extras, pep751_groups, reporter);
1136+
}
1137+
MarkerTreeKind::DependencyGroups(marker) => {
1138+
return marker
1139+
.edge(pep751_groups.contains(marker.name().group()))
1140+
.evaluate_reporter_impl(env, extras, pep751_extras, pep751_groups, reporter);
10821141
}
10831142
}
10841143

crates/uv-requirements/src/sources.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,13 @@ impl RequirementsSource {
273273
pub fn allows_extras(&self) -> bool {
274274
matches!(
275275
self,
276-
Self::PyprojectToml(_) | Self::SetupPy(_) | Self::SetupCfg(_)
276+
Self::PylockToml(_) | Self::PyprojectToml(_) | Self::SetupPy(_) | Self::SetupCfg(_)
277277
)
278278
}
279279

280280
/// Returns `true` if the source allows groups to be specified.
281281
pub fn allows_groups(&self) -> bool {
282-
matches!(self, Self::PyprojectToml(_))
282+
matches!(self, Self::PylockToml(_) | Self::PyprojectToml(_))
283283
}
284284
}
285285

0 commit comments

Comments
 (0)