add pip-compatible --group flag to uv pip install and uv pip compile (#11686)

This is a minimal redux of #10861 to be compatible with `uv pip`.

This implements the interface described in:
https://github.com/pypa/pip/pull/13065#issuecomment-2544000876 for `uv
pip install` and `uv pip compile`. Namely `--group <[path:]name>`, where
`path` when not defined defaults to `pyproject.toml`.

In that interface they add `--group` to `pip install`, `pip download`,
and `pip wheel`. Notably we do not define `uv pip download` and `uv pip
wheel`, so for parity we only need to implement `uv pip install`.
However, we also support `uv pip compile` which is not part of pip
itself, and `--group` makes sense there too.

----

The behaviour of `--group` for `uv pip` commands makes sense for the
cases upstream pip supports, but has confusing meanings in cases that
only we support (because reading pyproject.tomls is New Tech to them but
heavily supported by us). **Specifically case (h) below is a concerning
footgun, and case (e) below may get complaints from people who aren't
well-versed in dependency-groups-as-they-pertain-to-wheels.**


## Only Group Flags

Group flags on their own work reasonably and uncontroversially, except
perhaps that they don't do very clever automatic project discovery.

a) `uv pip install --group path/to/pyproject.toml:mygroup` pulls up
`path/to/project.toml` and installs all the packages listed by its
`mygroup` dependency-group (essentially treating it like another kind of
requirements.txt). In this regard it functions similarly to
`--only-group` in the rest of uv's interface.

b) `uv pip install --group mygroup` is just sugar for `uv pip install
--group pyproject.toml:mygroup` (**note that no project discovery
occurs**, upstream pip simply hardcodes the path "pyproject.toml" here
and we reproduce that.)

c) `uv pip install --group a/pyproject.toml:groupx --group
b/pyproject.toml:groupy`, and any other instance of multiple `--group`
flags, can be understood as completely independent requests for the
given groups at the given files.


## Groups With Named Packages

Groups being mixed with named packages also work in a fairly
unsurprising way, especially if you understand that things like
dependency-groups are not really supposed to exist on pypi, they're just
for local development.

d) `uv pip install mypackage --group path/to/pyproject.toml:mygroup`
much like multiple instances of `--group` the two requests here are
essentially completely independent: pleases install `mypackage`, and
please also install `path/to/pyproject.toml:mygroup`.

e) `uv pip install mypackage --group mygroup` is exactly the same, but
this is where it becomes possible for someone to be a little confused,
as you might think `mygroup` is supposed to refer to `mypackage` in some
way (it can't). But no, it's sourcing `pyproject.toml:mygroup` from the
current working directory.


## Groups With Requirements/Sourcetrees/Editables

Requirements and sourcetrees are where I expect users to get confused.
It behaves *exactly* the same as it does in the previous sections but
you would absolutely be forgiven for expecting a different behaviour.
*Especially* because `--group` with the rest of uv *does* do something
different.

f) `uv pip install -r a/pyproject.toml --group b/pyproject.toml:mygroup`
is again just two independent requests (install `a/pyproject.toml`'s
dependencies, and `b/pyproject.toml`'s `mygroup`).

g) `uv pip install -r pyproject.toml --group mygroup` is exactly like
the previous case but *incidentally* the two requests refer to the same
file. What the user wanted to happen is almost certainly happening, but
they are likely getting "lucky" here that they're requesting something
simple.

h) `uv pip install -r a/pyproject.toml --group mygroup` is again exactly
the same but the user is likely to get surprised and upset as this
invocation actually sources two different files (install
`a/pyproject.toml`'s dependencies, and `pyproject.toml`'s `mygroup`)! I
would expect most people to assume the `--group` flag here is covering
all applicable requirements/sourcetrees/editables, but no, it continues
to be a totally independent reference to a file with a hardcoded
relative path.

------

Fixes https://github.com/astral-sh/uv/issues/8590
Fixes https://github.com/astral-sh/uv/issues/8969
This commit is contained in:
Aria Desires
2025-03-17 14:44:11 -04:00
committed by GitHub
parent 3c20ffe9ef
commit ba73231164
26 changed files with 1999 additions and 707 deletions
+19 -2
View File
@@ -15,7 +15,7 @@ use uv_configuration::{
ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
};
use uv_distribution_types::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName};
use uv_pep508::{MarkerTree, Requirement};
use uv_pypi_types::VerbatimParsedUrl;
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
@@ -949,6 +949,7 @@ fn parse_maybe_string(input: &str) -> Result<Maybe<String>, String> {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
pub struct PipCompileArgs {
/// Include all packages listed in the given `requirements.in` files.
///
@@ -959,7 +960,7 @@ pub struct PipCompileArgs {
///
/// The order of the requirements files and the requirements in them is used to determine
/// priority during resolution.
#[arg(required(true), value_parser = parse_file_path)]
#[arg(group = "sources", value_parser = parse_file_path)]
pub src_file: Vec<PathBuf>,
/// Constrain versions using the given requirements files.
@@ -1022,6 +1023,14 @@ 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` file.
///
/// If the file already exists, the existing versions will be preferred when resolving
@@ -1586,6 +1595,14 @@ 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
@@ -25,6 +25,9 @@ impl std::fmt::Display for SourceAnnotation {
RequirementOrigin::Project(path, project_name) => {
write!(f, "{project_name} ({})", path.portable_display())
}
RequirementOrigin::Group(path, project_name, group) => {
write!(f, "{project_name} ({}:{group})", path.portable_display())
}
RequirementOrigin::Workspace => {
write!(f, "(workspace)")
}
@@ -40,6 +43,14 @@ impl std::fmt::Display for SourceAnnotation {
// Project is not used for override
write!(f, "--override {project_name} ({})", path.portable_display())
}
RequirementOrigin::Group(path, project_name, group) => {
// Group is not used for override
write!(
f,
"--override {project_name} ({}:{group})",
path.portable_display()
)
}
RequirementOrigin::Workspace => {
write!(f, "--override (workspace)")
}
+83 -1
View File
@@ -1,3 +1,5 @@
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock;
@@ -5,7 +7,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use uv_small_str::SmallString;
use crate::{validate_and_normalize_ref, InvalidNameError};
use crate::{
validate_and_normalize_ref, InvalidNameError, InvalidPipGroupError, InvalidPipGroupPathError,
};
/// The normalized name of a dependency group.
///
@@ -82,6 +86,84 @@ impl AsRef<str> for GroupName {
}
}
/// The pip-compatible variant of a [`GroupName`].
///
/// Either <groupname> or <path>:<groupname>.
/// If <path> is omitted it defaults to "pyproject.toml".
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PipGroupName {
pub path: Option<PathBuf>,
pub name: GroupName,
}
impl PipGroupName {
/// Gets the path to use, applying the default if it's missing
pub fn path(&self) -> &Path {
if let Some(path) = &self.path {
path
} else {
Path::new("pyproject.toml")
}
}
}
impl FromStr for PipGroupName {
type Err = InvalidPipGroupError;
fn from_str(path_and_name: &str) -> Result<Self, Self::Err> {
// The syntax is `<path>:<name>`.
//
// `:` isn't valid as part of a dependency-group name, but it can appear in a path.
// Therefore we look for the first `:` starting from the end to find the delimiter.
// If there is no `:` then there's no path and we use the default one.
if let Some((path, name)) = path_and_name.rsplit_once(':') {
// pip hard errors if the path does not end with pyproject.toml
if !path.ends_with("pyproject.toml") {
Err(InvalidPipGroupPathError(path.to_owned()))?;
}
let name = GroupName::from_str(name)?;
let path = Some(PathBuf::from(path));
Ok(Self { path, name })
} else {
let name = GroupName::from_str(path_and_name)?;
let path = None;
Ok(Self { path, name })
}
}
}
impl<'de> Deserialize<'de> for PipGroupName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl Serialize for PipGroupName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let string = self.to_string();
string.serialize(serializer)
}
}
impl Display for PipGroupName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(path) = &self.path {
write!(f, "{}:{}", path.display(), self.name)
} else {
self.name.fmt(f)
}
}
}
/// The name of the global `dev-dependencies` group.
///
/// Internally, we model dependency groups as a generic concept; but externally, we only expose the
+50 -1
View File
@@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter};
pub use dist_info_name::DistInfoName;
pub use extra_name::ExtraName;
pub use group_name::{GroupName, DEV_DEPENDENCIES};
pub use group_name::{GroupName, PipGroupName, DEV_DEPENDENCIES};
pub use package_name::PackageName;
use uv_small_str::SmallString;
@@ -121,6 +121,55 @@ impl Display for InvalidNameError {
impl Error for InvalidNameError {}
/// Path didn't end with `pyproject.toml`
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvalidPipGroupPathError(String);
impl InvalidPipGroupPathError {
/// Returns the invalid path.
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for InvalidPipGroupPathError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"The `--group` path is required to end in 'pyproject.toml' for compatibility with pip; got: {}",
self.0,
)
}
}
impl Error for InvalidPipGroupPathError {}
/// Possible errors from reading a [`PipGroupName`].
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InvalidPipGroupError {
Name(InvalidNameError),
Path(InvalidPipGroupPathError),
}
impl Display for InvalidPipGroupError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
InvalidPipGroupError::Name(e) => e.fmt(f),
InvalidPipGroupError::Path(e) => e.fmt(f),
}
}
}
impl Error for InvalidPipGroupError {}
impl From<InvalidNameError> for InvalidPipGroupError {
fn from(value: InvalidNameError) -> Self {
Self::Name(value)
}
}
impl From<InvalidPipGroupPathError> for InvalidPipGroupError {
fn from(value: InvalidPipGroupPathError) -> Self {
Self::Path(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
+4 -1
View File
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use uv_normalize::PackageName;
use uv_normalize::{GroupName, PackageName};
/// The origin of a dependency, e.g., a `-r requirements.txt` file.
#[derive(
@@ -12,6 +12,8 @@ pub enum RequirementOrigin {
File(PathBuf),
/// The requirement was provided via a local project (e.g., a `pyproject.toml` file).
Project(PathBuf, PackageName),
/// The requirement was provided via a local project (e.g., a `pyproject.toml` file).
Group(PathBuf, PackageName, GroupName),
/// The requirement was provided via a workspace.
Workspace,
}
@@ -22,6 +24,7 @@ impl RequirementOrigin {
match self {
RequirementOrigin::File(path) => path.as_path(),
RequirementOrigin::Project(path, _) => path.as_path(),
RequirementOrigin::Group(path, _, _) => path.as_path(),
// Multiple toml are merged and difficult to track files where Requirement is defined. Returns a dummy path instead.
RequirementOrigin::Workspace => Path::new("(workspace)"),
}
+24 -16
View File
@@ -1,6 +1,6 @@
use std::borrow::Cow;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{Context, Result};
use futures::stream::FuturesOrdered;
@@ -18,7 +18,7 @@ use uv_pep508::RequirementOrigin;
use uv_pypi_types::Requirement;
use uv_resolver::{InMemoryIndex, MetadataResponse};
use uv_types::{BuildContext, HashStrategy};
use uv_warnings::warn_user_once;
#[derive(Debug, Clone)]
pub struct SourceTreeResolution {
/// The requirements sourced from the source trees.
@@ -37,7 +37,7 @@ pub struct SourceTreeResolver<'a, Context: BuildContext> {
/// The extras to include when resolving requirements.
extras: &'a ExtrasSpecification,
/// The groups to include when resolving requirements.
groups: &'a DependencyGroups,
groups: &'a BTreeMap<PathBuf, DependencyGroups>,
/// The hash policy to enforce.
hasher: &'a HashStrategy,
/// The in-memory index for resolving dependencies.
@@ -50,7 +50,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
/// Instantiate a new [`SourceTreeResolver`] for a given set of `source_trees`.
pub fn new(
extras: &'a ExtrasSpecification,
groups: &'a DependencyGroups,
groups: &'a BTreeMap<PathBuf, DependencyGroups>,
hasher: &'a HashStrategy,
index: &'a InMemoryIndex,
database: DistributionDatabase<'a, Context>,
@@ -100,9 +100,13 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
let mut requirements = Vec::new();
// Resolve any groups associated with this path
let default_groups = DependencyGroups::default();
let groups = self.groups.get(path).unwrap_or(&default_groups);
// Flatten any transitive extras and include dependencies
// (unless something like --only-group was passed)
if self.groups.prod() {
if groups.prod() {
requirements.extend(
FlatRequiresDist::from_requirements(metadata.requires_dist, &metadata.name)
.into_iter()
@@ -116,20 +120,24 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
// Apply dependency-groups
for (group_name, group) in &metadata.dependency_groups {
if self.groups.contains(group_name) {
requirements.extend(group.iter().cloned());
if groups.contains(group_name) {
requirements.extend(group.iter().cloned().map(|group| Requirement {
origin: Some(RequirementOrigin::Group(
path.to_path_buf(),
metadata.name.clone(),
group_name.clone(),
)),
..group
}));
}
}
// Complain if dependency groups are named that don't appear.
// This is only a warning because *technically* we support passing in
// multiple pyproject.tomls, but at this level of abstraction we can't see them all,
// so hard erroring on "no pyproject.toml mentions this" is a bit difficult.
for name in self.groups.explicit_names() {
for name in groups.explicit_names() {
if !metadata.dependency_groups.contains_key(name) {
warn_user_once!(
"The dependency-group '{name}' is not defined in {}",
path.display()
);
return Err(anyhow::anyhow!(
"The dependency group '{name}' was not found in the project: {}",
path.user_display()
));
}
}
+68 -5
View File
@@ -27,6 +27,7 @@
//! * `setup.py` or `setup.cfg` instead of `pyproject.toml`: Directory is an entry in
//! `source_trees`.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
@@ -34,13 +35,13 @@ use rustc_hash::FxHashSet;
use tracing::instrument;
use uv_cache_key::CanonicalUrl;
use uv_client::BaseClientBuilder;
use uv_configuration::{NoBinary, NoBuild};
use uv_configuration::{DependencyGroups, NoBinary, NoBuild};
use uv_distribution_types::{
IndexUrl, NameRequirementSpecification, UnresolvedRequirement,
UnresolvedRequirementSpecification,
};
use uv_fs::{Simplified, CWD};
use uv_normalize::{ExtraName, PackageName};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep508::{MarkerTree, UnnamedRequirement, UnnamedRequirementUrl};
use uv_pypi_types::Requirement;
use uv_pypi_types::VerbatimParsedUrl;
@@ -62,6 +63,8 @@ pub struct RequirementsSpecification {
pub overrides: Vec<UnresolvedRequirementSpecification>,
/// The source trees from which to extract requirements.
pub source_trees: Vec<PathBuf>,
/// The groups to use for `source_trees`
pub groups: BTreeMap<PathBuf, DependencyGroups>,
/// The extras used to collect requirements.
pub extras: FxHashSet<ExtraName>,
/// The index URL to use for fetching packages.
@@ -223,15 +226,75 @@ impl RequirementsSpecification {
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
groups: BTreeMap<PathBuf, Vec<GroupName>>,
client_builder: &BaseClientBuilder<'_>,
) -> Result<Self> {
let mut spec = Self::default();
// Resolve sources into specifications so we know their `source_tree`s∂
let mut requirement_sources = Vec::new();
for source in requirements {
let source = Self::from_source(source, client_builder).await?;
requirement_sources.push(source);
}
// pip `--group` flags specify their own sources, which we need to process here
if !groups.is_empty() {
let mut group_specs = BTreeMap::new();
for (path, groups) in groups {
// Conceptually pip `--group` flags just add the group referred to by the file.
// In uv semantics this would be like `--only-group`, however if you do this:
//
// uv pip install -r pyproject.toml --group pyproject.toml:foo
//
// We don't want to discard the package listed by `-r` in the way `--only-group`
// would. So we check to see if any other source wants to add this path, and use
// that to determine if we're doing `--group` or `--only-group` semantics.
//
// Note that it's fine if a file gets referred to multiple times by
// different-looking paths (like `./pyproject.toml` vs `pyproject.toml`). We're
// specifically trying to disambiguate in situations where the `--group` *happens*
// to match with an unrelated argument, and `--only-group` would be overzealous!
let source_exists_without_group = requirement_sources
.iter()
.any(|source| source.source_trees.contains(&path));
let (group, only_group) = if source_exists_without_group {
(groups, Vec::new())
} else {
(Vec::new(), groups)
};
let group_spec = DependencyGroups::from_args(
false,
false,
false,
group,
Vec::new(),
false,
only_group,
false,
);
// If we're doing `--only-group` semantics it's because only `--group` flags referred
// to this file, and so we need to make sure to add it to the list of sources!
if !source_exists_without_group {
let source = Self::from_source(
&RequirementsSource::PyprojectToml(path.clone()),
client_builder,
)
.await?;
requirement_sources.push(source);
}
group_specs.insert(path, group_spec);
}
spec.groups = group_specs;
}
// 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.
for source in requirements {
let source = Self::from_source(source, client_builder).await?;
for source in requirement_sources {
spec.requirements.extend(source.requirements);
spec.constraints.extend(source.constraints);
spec.overrides.extend(source.overrides);
@@ -337,7 +400,7 @@ impl RequirementsSpecification {
requirements: &[RequirementsSource],
client_builder: &BaseClientBuilder<'_>,
) -> Result<Self> {
Self::from_sources(requirements, &[], &[], client_builder).await
Self::from_sources(requirements, &[], &[], BTreeMap::default(), client_builder).await
}
/// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`].
+10 -1
View File
@@ -13,7 +13,7 @@ use uv_distribution_types::{
};
use uv_install_wheel::LinkMode;
use uv_macros::{CombineOptions, OptionsMetadata};
use uv_normalize::{ExtraName, PackageName};
use uv_normalize::{ExtraName, PackageName, PipGroupName};
use uv_pep508::Requirement;
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
@@ -1127,6 +1127,15 @@ pub struct PipOptions {
"#
)]
pub no_deps: Option<bool>,
/// Include the following dependency groups.
#[option(
default = "None",
value_type = "list[str]",
example = r#"
group = ["dev", "docs"]
"#
)]
pub group: Option<Vec<PipGroupName>>,
/// Allow `uv pip sync` with empty requirements, which will clear the environment of all
/// packages.
#[option(
+8 -6
View File
@@ -1,6 +1,6 @@
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
@@ -13,8 +13,8 @@ use tracing::debug;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, ExtrasSpecification,
IndexStrategy, NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, Upgrade,
BuildOptions, Concurrency, ConfigSettings, Constraints, ExtrasSpecification, IndexStrategy,
NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
@@ -24,7 +24,7 @@ use uv_distribution_types::{
};
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_normalize::{GroupName, PackageName};
use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments};
use uv_python::{
EnvironmentPreference, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest,
@@ -60,7 +60,7 @@ pub(crate) async fn pip_compile(
build_constraints_from_workspace: Vec<Requirement>,
environments: SupportedEnvironments,
extras: ExtrasSpecification,
groups: DependencyGroups,
groups: BTreeMap<PathBuf, Vec<GroupName>>,
output_file: Option<&Path>,
resolution_mode: ResolutionMode,
prerelease_mode: PrereleaseMode,
@@ -148,6 +148,7 @@ pub(crate) async fn pip_compile(
constraints,
overrides,
source_trees,
groups,
extras: used_extras,
index_url,
extra_index_urls,
@@ -159,6 +160,7 @@ pub(crate) async fn pip_compile(
requirements,
constraints,
overrides,
groups,
&client_builder,
)
.await?;
+8 -6
View File
@@ -1,5 +1,6 @@
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use std::path::PathBuf;
use std::sync::Arc;
use itertools::Itertools;
@@ -9,9 +10,8 @@ use tracing::{debug, enabled, Level};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, DryRun,
ExtrasSpecification, HashCheckingMode, IndexStrategy, PreviewMode, Reinstall, SourceStrategy,
Upgrade,
BuildOptions, Concurrency, ConfigSettings, Constraints, DryRun, ExtrasSpecification,
HashCheckingMode, IndexStrategy, PreviewMode, Reinstall, SourceStrategy, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
@@ -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::GroupName;
use uv_pep508::PackageName;
use uv_pypi_types::{Conflicts, Requirement};
use uv_python::{
@@ -55,7 +56,7 @@ pub(crate) async fn pip_install(
overrides_from_workspace: Vec<Requirement>,
build_constraints_from_workspace: Vec<Requirement>,
extras: &ExtrasSpecification,
groups: &DependencyGroups,
groups: BTreeMap<PathBuf, Vec<GroupName>>,
resolution_mode: ResolutionMode,
prerelease_mode: PrereleaseMode,
dependency_mode: DependencyMode,
@@ -107,6 +108,7 @@ pub(crate) async fn pip_install(
constraints,
overrides,
source_trees,
groups,
index_url,
extra_index_urls,
no_index,
@@ -424,7 +426,7 @@ pub(crate) async fn pip_install(
project,
BTreeSet::default(),
extras,
groups,
&groups,
preferences,
site_packages.clone(),
&hasher,
+13 -15
View File
@@ -3,7 +3,7 @@
use anyhow::{anyhow, Context};
use itertools::Itertools;
use owo_colors::OwoColorize;
use std::collections::{BTreeSet, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fmt::Write;
use std::path::PathBuf;
use std::sync::Arc;
@@ -28,7 +28,7 @@ use uv_distribution_types::{
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_installer::{Plan, Planner, Preparer, SitePackages};
use uv_normalize::PackageName;
use uv_normalize::{GroupName, PackageName};
use uv_platform_tags::Tags;
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
use uv_python::{PythonEnvironment, PythonInstallation};
@@ -54,7 +54,7 @@ pub(crate) async fn read_requirements(
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
extras: &ExtrasSpecification,
groups: &DependencyGroups,
groups: BTreeMap<PathBuf, Vec<GroupName>>,
client_builder: &BaseClientBuilder<'_>,
) -> Result<RequirementsSpecification, Error> {
// If the user requests `extras` but does not provide a valid source (e.g., a `pyproject.toml`),
@@ -75,19 +75,13 @@ pub(crate) async fn read_requirements(
)
.into());
}
if !groups.is_empty() && !requirements.iter().any(RequirementsSource::allows_groups) {
let flags = groups.history().as_flags_pretty().join(" ");
return Err(anyhow!(
"Requesting groups requires a `pyproject.toml`. Requested via: {flags}"
)
.into());
}
// Read all requirements from the provided sources.
Ok(RequirementsSpecification::from_sources(
requirements,
constraints,
overrides,
groups,
client_builder,
)
.await?)
@@ -98,11 +92,15 @@ pub(crate) async fn read_constraints(
constraints: &[RequirementsSource],
client_builder: &BaseClientBuilder<'_>,
) -> Result<Vec<NameRequirementSpecification>, Error> {
Ok(
RequirementsSpecification::from_sources(&[], constraints, &[], client_builder)
.await?
.constraints,
Ok(RequirementsSpecification::from_sources(
&[],
constraints,
&[],
BTreeMap::default(),
client_builder,
)
.await?
.constraints)
}
/// Resolve a set of requirements, similar to running `pip compile`.
@@ -114,7 +112,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
mut project: Option<PackageName>,
workspace_members: BTreeSet<PackageName>,
extras: &ExtrasSpecification,
groups: &DependencyGroups,
groups: &BTreeMap<PathBuf, DependencyGroups>,
preferences: Vec<Preference>,
installed_packages: InstalledPackages,
hasher: &HashStrategy,
+6 -6
View File
@@ -1,4 +1,4 @@
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use std::sync::Arc;
@@ -9,9 +9,8 @@ use tracing::debug;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, DryRun,
ExtrasSpecification, HashCheckingMode, IndexStrategy, PreviewMode, Reinstall, SourceStrategy,
Upgrade,
BuildOptions, Concurrency, ConfigSettings, Constraints, DryRun, ExtrasSpecification,
HashCheckingMode, IndexStrategy, PreviewMode, Reinstall, SourceStrategy, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
@@ -88,7 +87,7 @@ pub(crate) async fn pip_sync(
// Initialize a few defaults.
let overrides = &[];
let extras = ExtrasSpecification::default();
let groups = DependencyGroups::default();
let groups = BTreeMap::default();
let upgrade = Upgrade::default();
let resolution_mode = ResolutionMode::default();
let prerelease_mode = PrereleaseMode::default();
@@ -101,6 +100,7 @@ pub(crate) async fn pip_sync(
constraints,
overrides,
source_trees,
groups,
index_url,
extra_index_urls,
no_index,
@@ -113,7 +113,7 @@ pub(crate) async fn pip_sync(
constraints,
overrides,
&extras,
&groups,
groups,
&client_builder,
)
.await?;
+2 -3
View File
@@ -13,8 +13,7 @@ use uv_auth::UrlAuthPolicies;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroups, DryRun, ExtrasSpecification, PreviewMode,
Reinstall, Upgrade,
Concurrency, Constraints, DryRun, ExtrasSpecification, PreviewMode, Reinstall, Upgrade,
};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
@@ -588,7 +587,7 @@ async fn do_lock(
// optional on the downstream APIs.
let build_hasher = HashStrategy::default();
let extras = ExtrasSpecification::default();
let groups = DependencyGroups::default();
let groups = BTreeMap::new();
// Resolve the flat indexes from `--find-links`.
let flat_index = {
+4 -4
View File
@@ -13,8 +13,8 @@ use uv_cache::{Cache, CacheBucket};
use uv_cache_key::cache_digest;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun,
ExtrasSpecification, PreviewMode, Reinstall, SourceStrategy, Upgrade,
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification,
PreviewMode, Reinstall, SourceStrategy, Upgrade,
};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::{DistributionDatabase, LoweredRequirement};
@@ -1727,7 +1727,7 @@ pub(crate) async fn resolve_environment(
// TODO(charlie): These are all default values. We should consider whether we want to make them
// optional on the downstream APIs.
let extras = ExtrasSpecification::default();
let groups = DependencyGroups::default();
let groups = BTreeMap::new();
let hasher = HashStrategy::default();
let build_constraints = Constraints::default();
let build_hasher = HashStrategy::default();
@@ -2112,7 +2112,7 @@ pub(crate) async fn update_environment(
let build_constraints = Constraints::default();
let build_hasher = HashStrategy::default();
let extras = ExtrasSpecification::default();
let groups = DependencyGroups::default();
let groups = BTreeMap::new();
let hasher = HashStrategy::default();
let preferences = Vec::default();
+9 -3
View File
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::fmt::Write;
use std::str::FromStr;
@@ -234,9 +235,14 @@ pub(crate) async fn install(
};
// Read the `--with` requirements.
let spec =
RequirementsSpecification::from_sources(with, constraints, overrides, &client_builder)
.await?;
let spec = RequirementsSpecification::from_sources(
with,
constraints,
overrides,
BTreeMap::default(),
&client_builder,
)
.await?;
// Resolve the `--from` and `--with` requirements.
let requirements = {
+9 -3
View File
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Write;
use std::path::Path;
@@ -750,9 +751,14 @@ async fn get_or_create_environment(
};
// Read the `--with` requirements.
let spec =
RequirementsSpecification::from_sources(with, constraints, overrides, &client_builder)
.await?;
let spec = RequirementsSpecification::from_sources(
with,
constraints,
overrides,
BTreeMap::default(),
&client_builder,
)
.await?;
// Resolve the `--from` and `--with` requirements.
let requirements = {
+31 -2
View File
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
@@ -391,6 +392,20 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let mut groups = BTreeMap::new();
for group in args.settings.groups {
// If there's no path provided, expect a pyproject.toml in the project-dir
// (Which is typically the current working directory, matching pip's behaviour)
let pyproject_path = group
.path
.clone()
.unwrap_or_else(|| project_dir.join("pyproject.toml"));
groups
.entry(pyproject_path)
.or_insert_with(Vec::new)
.push(group.name.clone());
}
commands::pip_compile(
&requirements,
&constraints,
@@ -401,7 +416,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.build_constraints_from_workspace,
args.environments,
args.settings.extras,
args.settings.groups,
groups,
args.settings.output_file.as_deref(),
args.settings.resolution,
args.settings.prerelease,
@@ -561,6 +576,20 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let mut groups = BTreeMap::new();
for group in args.settings.groups {
// If there's no path provided, expect a pyproject.toml in the project-dir
// (Which is typically the current working directory, matching pip's behaviour)
let pyproject_path = group
.path
.clone()
.unwrap_or_else(|| project_dir.join("pyproject.toml"));
groups
.entry(pyproject_path)
.or_insert_with(Vec::new)
.push(group.name.clone());
}
commands::pip_install(
&requirements,
&constraints,
@@ -570,7 +599,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.overrides_from_workspace,
args.build_constraints_from_workspace,
&args.settings.extras,
&args.settings.groups,
groups,
args.settings.resolution,
args.settings.prerelease,
args.settings.dependency_mode,
+8 -12
View File
@@ -30,7 +30,7 @@ use uv_configuration::{
};
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl};
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_normalize::{PackageName, PipGroupName};
use uv_pep508::{ExtraName, MarkerTree, RequirementOrigin};
use uv_pypi_types::{Requirement, SupportedEnvironments};
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
@@ -1605,6 +1605,7 @@ impl PipCompileSettings {
refresh,
no_deps,
deps,
group,
output_file,
no_strip_extras,
strip_extras,
@@ -1721,6 +1722,7 @@ impl PipCompileSettings {
extra,
all_extras: flag(all_extras, no_all_extras),
no_deps: flag(no_deps, deps),
group: Some(group),
output_file,
no_strip_extras: flag(no_strip_extras, strip_extras),
no_strip_markers: flag(no_strip_markers, strip_markers),
@@ -1867,6 +1869,7 @@ impl PipInstallSettings {
refresh,
no_deps,
deps,
group,
require_hashes,
no_require_hashes,
verify_hashes,
@@ -1973,6 +1976,7 @@ impl PipInstallSettings {
strict: flag(strict, no_strict),
extra,
all_extras: flag(all_extras, no_all_extras),
group: Some(group),
no_deps: flag(no_deps, deps),
python_version,
python_platform,
@@ -2592,7 +2596,7 @@ pub(crate) struct PipSettings {
pub(crate) install_mirrors: PythonInstallMirrors,
pub(crate) system: bool,
pub(crate) extras: ExtrasSpecification,
pub(crate) groups: DependencyGroups,
pub(crate) groups: Vec<PipGroupName>,
pub(crate) break_system_packages: bool,
pub(crate) target: Option<Target>,
pub(crate) prefix: Option<Prefix>,
@@ -2669,6 +2673,7 @@ impl PipSettings {
extra,
all_extras,
no_extra,
group,
no_deps,
allow_empty_requirements,
resolution,
@@ -2786,16 +2791,7 @@ impl PipSettings {
args.no_extra.combine(no_extra).unwrap_or_default(),
args.extra.combine(extra).unwrap_or_default(),
),
groups: DependencyGroups::from_args(
false,
false,
false,
Vec::new(),
Vec::new(),
false,
Vec::new(),
false,
),
groups: args.group.combine(group).unwrap_or_default(),
dependency_mode: if args.no_deps.combine(no_deps).unwrap_or_default() {
DependencyMode::Direct
} else {
+733 -6
View File
@@ -1591,7 +1591,7 @@ fn compile_python_conflicts() -> Result<()> {
.arg("--python")
.arg("3.12")
.arg("-p")
.arg("3.12"), @r###"
.arg("3.12"), @r"
success: false
exit_code: 2
----- stdout -----
@@ -1599,10 +1599,10 @@ fn compile_python_conflicts() -> Result<()> {
----- stderr -----
error: the argument '--python <PYTHON>' cannot be used multiple times
Usage: uv pip compile [OPTIONS] <SRC_FILE>...
Usage: uv pip compile [OPTIONS] <SRC_FILE|--group <GROUP>>
For more information, try '--help'.
"###
"
);
// `UV_PYTHON` should be usable with `-p`
@@ -3263,7 +3263,7 @@ optional-dependencies.bar = [
.arg("--all-extras")
.arg("--extra")
.arg("foo"),
@r###"
@r"
success: false
exit_code: 2
----- stdout -----
@@ -3271,10 +3271,10 @@ optional-dependencies.bar = [
----- stderr -----
error: the argument '--all-extras' cannot be used with '--extra <EXTRA>'
Usage: uv pip compile --cache-dir [CACHE_DIR] --all-extras --exclude-newer <EXCLUDE_NEWER> <SRC_FILE>...
Usage: uv pip compile --cache-dir [CACHE_DIR] --all-extras --exclude-newer <EXCLUDE_NEWER> <SRC_FILE|--group <GROUP>>
For more information, try '--help'.
"###
"
);
Ok(())
@@ -15261,6 +15261,733 @@ fn respect_index_preference() -> Result<()> {
Ok(())
}
#[test]
fn dependency_group() -> Result<()> {
// uv pip compile --group tests, with a single pyproject.toml
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
bar = ["iniconfig"]
dev = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// Passing --group should add just that group's contents from ./pyproject.toml
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group bar
iniconfig==2.0.0
# via myproject (pyproject.toml:bar)
----- stderr -----
Resolved 1 package in [TIME]
");
// Passing a pyproject.toml and --group should include both its deps and the group
// (This is a "try to confuse the internals" test, as this file is logically
// imported twice, but with two different semantics.)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("pyproject.toml")
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] pyproject.toml --group bar
iniconfig==2.0.0
# via myproject (pyproject.toml:bar)
typing-extensions==4.10.0
# via myproject (pyproject.toml)
----- stderr -----
Resolved 2 packages in [TIME]
");
// Another "try to confuse the internals" test with an absolute path
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg(context.temp_dir.child("pyproject.toml").path())
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] [TEMP_DIR]/pyproject.toml --group bar
iniconfig==2.0.0
# via myproject (pyproject.toml:bar)
typing-extensions==4.10.0
# via myproject (pyproject.toml)
----- stderr -----
Resolved 2 packages in [TIME]
");
// An explicit use of `<path>:<group>` syntax, here using what the default is anyway
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("pyproject.toml:bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group pyproject.toml:bar
iniconfig==2.0.0
# via myproject (pyproject.toml:bar)
----- stderr -----
Resolved 1 package in [TIME]
");
// "try to confuse the internals" with an explicit path for the group
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("pyproject.toml")
.arg("--group").arg("pyproject.toml:bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] pyproject.toml --group pyproject.toml:bar
iniconfig==2.0.0
# via myproject (pyproject.toml:bar)
typing-extensions==4.10.0
# via myproject (pyproject.toml)
----- stderr -----
Resolved 2 packages in [TIME]
");
// Let's check that the other group works fine individually
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group foo
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 1 package in [TIME]
");
// Now let's do both of the groups together
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("foo")
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group foo --group bar
iniconfig==2.0.0
# via myproject (pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
// And finally put it all together
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("pyproject.toml")
.arg("--group").arg("foo")
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] pyproject.toml --group foo --group bar
iniconfig==2.0.0
# via myproject (pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
typing-extensions==4.10.0
# via myproject (pyproject.toml)
----- stderr -----
Resolved 3 packages in [TIME]
");
Ok(())
}
#[test]
fn many_pyproject_group() -> Result<()> {
// uv pip compile --group tests, with multiple pyproject.tomls at once
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// Use the 'foo' group from the main toml
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group pyproject.toml:foo
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 1 package in [TIME]
");
// Use the 'foo' group from the subtoml
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group subdir/pyproject.toml:foo
iniconfig==2.0.0
# via mysubproject (subdir/pyproject.toml:foo)
----- stderr -----
Resolved 1 package in [TIME]
");
// Now try both together, where they happen to define a group with the same name
// (This does nothing special but the code shouldn't get confused.)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("pyproject.toml:foo")
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group pyproject.toml:foo --group subdir/pyproject.toml:foo
iniconfig==2.0.0
# via mysubproject (subdir/pyproject.toml:foo)
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
Ok(())
}
#[test]
fn suspicious_group() -> Result<()> {
// uv pip compile --group tests, where the invocations are suspicious
// and we might want to add warnings
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// Another variant of "both" but with the path sugar applied to the one in cwd
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("foo")
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group foo --group subdir/pyproject.toml:foo
iniconfig==2.0.0
# via mysubproject (subdir/pyproject.toml:foo)
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
// Using the path sugar for "foo" but requesting "bar" for the subtoml
// Although you would be forgiven for thinking "foo" should be used from
// the subtoml, that's not what should happen.
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("foo")
.arg("--group").arg("subdir/pyproject.toml:bar"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --group foo --group subdir/pyproject.toml:bar
sniffio==1.3.1
# via mysubproject (subdir/pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
// Using the path sugar to request pyproject.toml:foo
// while also importing subdir/pyproject.toml's dependencies
// Although you would be forgiven for thinking "foo" should be used from
// the subtoml, that's not what should happen.
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("subdir/pyproject.toml")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] subdir/pyproject.toml --group foo
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
typing-extensions==4.10.0
# via mysubproject (subdir/pyproject.toml)
----- stderr -----
Resolved 2 packages in [TIME]
");
// An inversion of the previous -- this one isn't terribly ambiguous
// but we should have it in the suite too in case it should be distinguished!
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("pyproject.toml")
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] pyproject.toml --group subdir/pyproject.toml:foo
iniconfig==2.0.0
# via mysubproject (subdir/pyproject.toml:foo)
typing-extensions==4.10.0
# via myproject (pyproject.toml)
----- stderr -----
Resolved 2 packages in [TIME]
");
Ok(())
}
#[test]
fn invalid_group() -> Result<()> {
// uv pip compile --group tests, where the invocations should fail
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let context = new_context()?;
// Hey you passed a path and not a group!
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("subdir/"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'subdir/' for '--group <GROUP>': Not a valid package or extra name: "subdir/". Names must start and end with a letter or digit and may only contain -, _, ., and alphanumeric characters.
For more information, try '--help'.
"#);
// Hey this path needs to end with "pyproject.toml"!
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("./:foo"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value './:foo' for '--group <GROUP>': The `--group` path is required to end in 'pyproject.toml' for compatibility with pip; got: ./
For more information, try '--help'.
");
// Hey this path needs to end with "pyproject.toml"!
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("subdir/:foo"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'subdir/:foo' for '--group <GROUP>': The `--group` path is required to end in 'pyproject.toml' for compatibility with pip; got: subdir/
For more information, try '--help'.
");
// Another invocation that Looks Weird but is asking for bar from two
// different tomls. In this case the main one doesn't define it and
// we should error!
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--group").arg("bar")
.arg("--group").arg("subdir/pyproject.toml:bar"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The dependency group 'bar' was not found in the project: pyproject.toml
");
Ok(())
}
#[test]
fn project_and_group() -> Result<()> {
// Checking that --project is handled properly with --group
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// 'foo' from subtoml, by implicit-sugar + --project
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--project").arg("subdir")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --project subdir --group foo
iniconfig==2.0.0
# via mysubproject (subdir/pyproject.toml:foo)
----- stderr -----
Resolved 1 package in [TIME]
");
// 'foo' from subtoml, by implicit-sugar + --project
// 'bar' from subtoml, by explicit relpath from cwd
// (explicit relpaths are not affected by --project)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--project").arg("subdir")
.arg("--group").arg("subdir/pyproject.toml:bar")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --project subdir --group subdir/pyproject.toml:bar --group foo
iniconfig==2.0.0
# via mysubproject (subdir/pyproject.toml:foo)
sniffio==1.3.1
# via mysubproject (subdir/pyproject.toml:bar)
----- stderr -----
Resolved 2 packages in [TIME]
");
// 'bar' from subtoml, by implicit-sugar + --project
// 'foo' from main toml, by explicit relpath from cwd
// (explicit relpaths are not affected by --project)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--project").arg("subdir")
.arg("--group").arg("bar")
.arg("--group").arg("pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --project subdir --group bar --group pyproject.toml:foo
sniffio==1.3.1
# via mysubproject (subdir/pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
// 'bar' from subtoml, by explicit relpath from cwd
// 'foo' from main toml, by explicit relpath from cwd
// (explicit relpaths are not affected by --project)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--project").arg("subdir")
.arg("--group").arg("subdir/pyproject.toml:bar")
.arg("--group").arg("pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --project subdir --group subdir/pyproject.toml:bar --group pyproject.toml:foo
sniffio==1.3.1
# via mysubproject (subdir/pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject (pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
Ok(())
}
#[test]
fn directory_and_group() -> Result<()> {
// Checking that --directory is handled properly with --group
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// 'bar' from subtoml, by implicit-sugar + --directory
// 'foo' from main toml, by explicit relpath from --directory
// (explicit relpaths ARE affected by --directory)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--directory").arg("subdir")
.arg("--group").arg("bar")
.arg("--group").arg("../pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --directory subdir --group bar --group ../pyproject.toml:foo
sniffio==1.3.1
# via mysubproject (pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject (../pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
// 'bar' from subtoml, by explicit relpath from --directory
// 'foo' from main toml, by explicit relpath from --directory
// (explicit relpaths ARE affected by --directory)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--directory").arg("subdir")
.arg("--group").arg("pyproject.toml:bar")
.arg("--group").arg("../pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --directory subdir --group pyproject.toml:bar --group ../pyproject.toml:foo
sniffio==1.3.1
# via mysubproject (pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject (../pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
// 'bar' from subtoml, by explicit relpath from --directory
// 'foo' from main toml, by implicit path + --project + --directory
// (explicit relpaths ARE affected by --directory)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_compile()
.arg("--directory").arg("subdir")
.arg("--project").arg("../")
.arg("--group").arg("pyproject.toml:bar")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --directory subdir --project ../ --group pyproject.toml:bar --group foo
sniffio==1.3.1
# via mysubproject (pyproject.toml:bar)
sortedcontainers==2.4.0
# via myproject ([TEMP_DIR]/pyproject.toml:foo)
----- stderr -----
Resolved 2 packages in [TIME]
");
Ok(())
}
/// See: <https://github.com/astral-sh/uv/issues/10957>
#[test]
fn compile_preserve_requires_python_split() -> Result<()> {
+743
View File
@@ -9060,6 +9060,749 @@ fn direct_url_json_direct_url() -> Result<()> {
Ok(())
}
#[test]
fn dependency_group() -> Result<()> {
// testing basic `uv pip install --group` functionality
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
bar = ["iniconfig"]
dev = ["sniffio"]
"#,
)?;
context.lock().assert().success();
Ok(context)
}
let mut context;
// 'bar' using path sugar
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
// 'bar' using path sugar
// and also pulling in the same pyproject.toml with -r
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("-r").arg("pyproject.toml")
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ typing-extensions==4.10.0
");
// 'bar' with an explicit path
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("pyproject.toml:bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
// 'bar' using explicit path
// and also pulling in the same pyproject.toml with -r
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("-r").arg("pyproject.toml")
.arg("--group").arg("pyproject.toml:bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ typing-extensions==4.10.0
");
// 'bar' using path sugar
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ sortedcontainers==2.4.0
");
// 'foo' using path sugar
// 'bar' using path sugar
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("foo")
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ sortedcontainers==2.4.0
");
// all together now!
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("-r").arg("pyproject.toml")
.arg("--group").arg("foo")
.arg("--group").arg("bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ iniconfig==2.0.0
+ sortedcontainers==2.4.0
+ typing-extensions==4.10.0
");
Ok(())
}
#[test]
fn many_pyproject_group() -> Result<()> {
// `uv pip install --group` tests with multiple projects
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
context.lock().assert().success();
Ok(context)
}
let mut context;
// 'foo' from main toml
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ sortedcontainers==2.4.0
");
// 'foo' from subtoml
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
// 'foo' from main toml
// 'foo' from sub toml
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("pyproject.toml:foo")
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ sortedcontainers==2.4.0
");
Ok(())
}
#[test]
fn other_sources_group() -> Result<()> {
// `uv pip install --group` tests just slamming random other sources like -e and .
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
Ok(context)
}
let mut context;
// 'foo' from main toml
// and install '.'
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg(".")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ myproject==0.1.0 (from file://[TEMP_DIR]/)
+ sortedcontainers==2.4.0
+ typing-extensions==4.10.0
");
// 'foo' from main toml
// and install an editable
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("-e").arg(context.workspace_root.join("scripts/packages/poetry_editable"))
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ poetry-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/poetry_editable)
+ sniffio==1.3.1
+ sortedcontainers==2.4.0
");
Ok(())
}
#[test]
fn suspicious_group() -> Result<()> {
// uv pip compile --group tests, where the invocations are suspicious
// and we might want to add warnings
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// Another variant of "both" but with the path sugar applied to the one in cwd
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("foo")
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ sortedcontainers==2.4.0
");
// Using the path sugar for "foo" but requesting "bar" for the subtoml
// Although you would be forgiven for thinking "foo" should be used from
// the subtoml, that's not what should happen.
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("foo")
.arg("--group").arg("subdir/pyproject.toml:bar"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ sortedcontainers==2.4.0
");
// Using the path sugar to request pyproject.toml:foo
// while also importing subdir/pyproject.toml's dependencies
// Although you would be forgiven for thinking "foo" should be used from
// the subtoml, that's not what should happen.
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("-r").arg("subdir/pyproject.toml")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sortedcontainers==2.4.0
+ typing-extensions==4.10.0
");
// An inversion of the previous -- this one isn't terribly ambiguous
// but we should have it in the suite too in case it should be distinguished!
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("-r").arg("pyproject.toml")
.arg("--group").arg("subdir/pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ typing-extensions==4.10.0
");
Ok(())
}
#[test]
fn invalid_group() -> Result<()> {
// uv pip compile --group tests, where the invocations should fail
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["typing-extensions"]
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let context = new_context()?;
// Hey you passed a path and not a group!
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("subdir/"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'subdir/' for '--group <GROUP>': Not a valid package or extra name: "subdir/". Names must start and end with a letter or digit and may only contain -, _, ., and alphanumeric characters.
For more information, try '--help'.
"#);
// Hey this path needs to end with "pyproject.toml"!
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("./:foo"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value './:foo' for '--group <GROUP>': The `--group` path is required to end in 'pyproject.toml' for compatibility with pip; got: ./
For more information, try '--help'.
");
// Hey this path needs to end with "pyproject.toml"!
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("subdir/:foo"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'subdir/:foo' for '--group <GROUP>': The `--group` path is required to end in 'pyproject.toml' for compatibility with pip; got: subdir/
For more information, try '--help'.
");
// Another invocation that Looks Weird but is asking for bar from two
// different tomls. In this case the main one doesn't define it and
// we should error!
uv_snapshot!(context.filters(), context.pip_install()
.arg("--group").arg("bar")
.arg("--group").arg("subdir/pyproject.toml:bar"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The dependency group 'bar' was not found in the project: pyproject.toml
");
Ok(())
}
#[test]
fn project_and_group() -> Result<()> {
// Checking that --project is handled properly with --group
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// 'foo' from subtoml, by implicit-sugar + --project
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--project").arg("subdir")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");
// 'foo' from subtoml, by implicit-sugar + --project
// 'bar' from subtoml, by explicit relpath from cwd
// (explicit relpaths are not affected by --project)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--project").arg("subdir")
.arg("--group").arg("subdir/pyproject.toml:bar")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ iniconfig==2.0.0
+ sniffio==1.3.1
");
// 'bar' from subtoml, by implicit-sugar + --project
// 'foo' from main toml, by explicit relpath from cwd
// (explicit relpaths are not affected by --project)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--project").arg("subdir")
.arg("--group").arg("bar")
.arg("--group").arg("pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ sortedcontainers==2.4.0
");
// 'bar' from subtoml, by explicit relpath from cwd
// 'foo' from main toml, by explicit relpath from cwd
// (explicit relpaths are not affected by --project)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--project").arg("subdir")
.arg("--group").arg("subdir/pyproject.toml:bar")
.arg("--group").arg("pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ sortedcontainers==2.4.0
");
Ok(())
}
#[test]
fn directory_and_group() -> Result<()> {
// Checking that --directory is handled properly with --group
fn new_context() -> Result<TestContext> {
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"]
[dependency-groups]
foo = ["sortedcontainers"]
"#,
)?;
let subdir = context.temp_dir.child("subdir");
subdir.create_dir_all()?;
let pyproject_toml2 = subdir.child("pyproject.toml");
pyproject_toml2.write_str(
r#"
[project]
name = "mysubproject"
version = "0.1.0"
requires-python = ">=3.12"
[dependency-groups]
foo = ["iniconfig"]
bar = ["sniffio"]
"#,
)?;
Ok(context)
}
let mut context;
// 'bar' from subtoml, by implicit-sugar + --directory
// 'foo' from main toml, by explicit relpath from --directory
// (explicit relpaths ARE affected by --directory)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--directory").arg("subdir")
.arg("--group").arg("bar")
.arg("--group").arg("../pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] environment at: [VENV]/
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ sortedcontainers==2.4.0
");
// 'bar' from subtoml, by explicit relpath from --directory
// 'foo' from main toml, by explicit relpath from --directory
// (explicit relpaths ARE affected by --directory)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--directory").arg("subdir")
.arg("--group").arg("pyproject.toml:bar")
.arg("--group").arg("../pyproject.toml:foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] environment at: [VENV]/
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ sortedcontainers==2.4.0
");
// 'bar' from subtoml, by explicit relpath from --directory
// 'foo' from main toml, by implicit path + --project + --directory
// (explicit relpaths ARE affected by --directory)
context = new_context()?;
uv_snapshot!(context.filters(), context.pip_install()
.arg("--directory").arg("subdir")
.arg("--project").arg("../")
.arg("--group").arg("pyproject.toml:bar")
.arg("--group").arg("foo"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] environment at: [VENV]/
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ sortedcontainers==2.4.0
");
Ok(())
}
/// Regression test that we don't discover workspaces with `--no-sources`.
///
/// We have a workspace dependency shadowing a PyPI package and using this package's version to
+34 -612
View File
@@ -148,24 +148,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -323,24 +306,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -499,24 +465,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -707,24 +656,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -853,24 +785,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -1039,24 +954,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -1269,24 +1167,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -1508,24 +1389,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -1708,24 +1572,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -1876,24 +1723,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -2098,24 +1928,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -2303,24 +2116,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -2470,24 +2266,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -2621,24 +2400,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -2772,24 +2534,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -2925,24 +2670,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -3260,24 +2988,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -3470,24 +3181,7 @@ fn resolve_both() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -3770,24 +3464,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -4015,24 +3692,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -4169,24 +3829,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -4342,24 +3985,7 @@ fn allow_insecure_host() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -4570,24 +4196,7 @@ fn index_priority() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -4777,24 +4386,7 @@ fn index_priority() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -4990,24 +4582,7 @@ fn index_priority() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -5198,24 +4773,7 @@ fn index_priority() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -5413,24 +4971,7 @@ fn index_priority() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -5621,24 +5162,7 @@ fn index_priority() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -5780,24 +5304,7 @@ fn verify_hashes() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -5925,24 +5432,7 @@ fn verify_hashes() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -6068,24 +5558,7 @@ fn verify_hashes() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -6213,24 +5686,7 @@ fn verify_hashes() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -6356,24 +5812,7 @@ fn verify_hashes() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
@@ -6500,24 +5939,7 @@ fn verify_hashes() -> anyhow::Result<()> {
},
system: false,
extras: None,
groups: DependencyGroups(
DependencyGroupsInner {
include: Some(
[],
),
exclude: [],
only_groups: false,
history: DependencyGroupsHistory {
dev_mode: None,
group: [],
only_group: [],
no_group: [],
all_groups: false,
no_default_groups: false,
defaults: [],
},
},
),
groups: [],
break_system_packages: false,
target: None,
prefix: None,
+29
View File
@@ -60,6 +60,35 @@ $ uv pip compile pyproject.toml --all-extras
Note extras are not supported with the `requirements.in` format.
To lock a dependency group in the current project directory's `pyproject.toml`, for example the
group `foo`:
```console
$ uv pip compile --group foo
```
!!! important
A `--group` flag has to be added to pip-tools' `pip compile`, [although they're considering it](https://github.com/jazzband/pip-tools/issues/2062). We expect to support whatever syntax and semantics they adopt.
To specify the project directory where groups should be sourced from:
```console
$ uv pip compile --project some/path/ --group foo --group bar
```
Alternatively, you can specify a path to a `pyproject.toml` for each group:
```console
$ uv pip compile --group some/path/pyproject.toml:foo --group other/pyproject.toml:bar
```
!!! note
`--group` flags do not apply to other specified sources. For instance,
`uv pip compile some/path/pyproject.toml --group foo` sources `foo`
from `./pyproject.toml` and **not** `some/path/pyproject.toml`.
## Upgrading requirements
When using an output file, uv will consider the versions pinned in an existing output file. If a
+25
View File
@@ -107,6 +107,31 @@ Install from a `pyproject.toml` file with all optional dependencies enabled:
$ uv pip install -r pyproject.toml --all-extras
```
To install dependency groups in the current project directory's `pyproject.toml`, for example the
group `foo`:
```console
$ uv pip install --group foo
```
To specify the project directory where groups should be sourced from:
```console
$ uv pip install --project some/path/ --group foo --group bar
```
Alternatively, you can specify a path to a `pyproject.toml` for each group:
```console
$ uv pip install --group some/path/pyproject.toml:foo --group other/pyproject.toml:bar
```
!!! note
As in pip, `--group` flags do not apply to other sources specified with flags like `-r` or -e`.
For instance, `uv pip install -r some/path/pyproject.toml --group foo` sources `foo`
from `./pyproject.toml` and **not** `some/path/pyproject.toml`.
## Uninstalling a package
To uninstall a package, e.g., Flask:
+14 -2
View File
@@ -5577,7 +5577,7 @@ Compile a `requirements.in` file to a `requirements.txt` file
<h3 class="cli-reference">Usage</h3>
```
uv pip compile [OPTIONS] <SRC_FILE>...
uv pip compile [OPTIONS] <SRC_FILE|--group <GROUP>>
```
<h3 class="cli-reference">Arguments</h3>
@@ -5722,6 +5722,12 @@ uv pip compile [OPTIONS] <SRC_FILE>...
</ul>
</dd><dt id="uv-pip-compile--generate-hashes"><a href="#uv-pip-compile--generate-hashes"><code>--generate-hashes</code></a></dt><dd><p>Include distribution hashes in the output file</p>
</dd><dt id="uv-pip-compile--group"><a href="#uv-pip-compile--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>
<p>May be provided multiple times.</p>
</dd><dt id="uv-pip-compile--help"><a href="#uv-pip-compile--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
</dd><dt id="uv-pip-compile--index"><a href="#uv-pip-compile--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>
@@ -6461,7 +6467,7 @@ Install packages into an environment
<h3 class="cli-reference">Usage</h3>
```
uv pip install [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>|--editable <EDITABLE>>
uv pip install [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>|--editable <EDITABLE>|--group <GROUP>>
```
<h3 class="cli-reference">Arguments</h3>
@@ -6596,6 +6602,12 @@ uv pip install [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>|--editable <EDIT
<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>
<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>
+26
View File
@@ -2308,6 +2308,32 @@ Include distribution hashes in the output file.
---
#### [`group`](#pip_group) {: #pip_group }
<span id="group"></span>
Include the following dependency groups.
**Default value**: `None`
**Type**: `list[str]`
**Example usage**:
=== "pyproject.toml"
```toml
[tool.uv.pip]
group = ["dev", "docs"]
```
=== "uv.toml"
```toml
[pip]
group = ["dev", "docs"]
```
---
#### [`index-strategy`](#pip_index-strategy) {: #pip_index-strategy }
<span id="index-strategy"></span>
+28
View File
@@ -880,6 +880,24 @@
"type": "string",
"pattern": "^(:none:|:all:|([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]))$"
},
"PipGroupName": {
"description": "The pip-compatible variant of a [`GroupName`].\n\nEither <groupname> or <path>:<groupname>. If <path> is omitted it defaults to \"pyproject.toml\".",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"$ref": "#/definitions/GroupName"
},
"path": {
"type": [
"string",
"null"
]
}
}
},
"PipOptions": {
"description": "Settings that are specific to the `uv pip` command-line interface.\n\nThese values will be ignored when running commands outside the `uv pip` namespace (e.g., `uv lock`, `uvx`).",
"type": "object",
@@ -1045,6 +1063,16 @@
"null"
]
},
"group": {
"description": "Include the following dependency groups.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/PipGroupName"
}
},
"index-strategy": {
"description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-index`). This prevents \"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.",
"anyOf": [