Add requires-python to uv init (#5322)

## Summary

Prefers, in order:

- The major-minor version of an interpreter discovered via `--python`.
- The `requires-python` from the workspace.
- The major-minor version of the default interpreter.

If the `--python` request is a version or a version range, we use that
without fetching an interpreter.

Closes https://github.com/astral-sh/uv/issues/5299.
This commit is contained in:
Charlie Marsh 2024-07-23 12:02:40 -04:00 committed by GitHub
parent c8ac8ee57a
commit 0f8186d9ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 461 additions and 103 deletions

View file

@ -1797,6 +1797,20 @@ pub struct InitArgs {
/// Do not create a readme file. /// Do not create a readme file.
#[arg(long)] #[arg(long)]
pub no_readme: bool, pub no_readme: bool,
/// The Python interpreter to use to determine the minimum supported Python version.
///
/// By default, uv uses the virtual environment in the current working directory or any parent
/// directory, falling back to searching for a Python executable in `PATH`. The `--python`
/// option allows you to specify a different interpreter.
///
/// Supported formats:
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
/// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)]
pub python: Option<String>,
} }
#[derive(Args)] #[derive(Args)]

View file

@ -5,7 +5,7 @@ use once_cell::sync::Lazy;
use uv_configuration::PreviewMode; use uv_configuration::PreviewMode;
use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_workspace::ProjectWorkspace; use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
use crate::metadata::lowering::lower_requirement; use crate::metadata::lowering::lower_requirement;
use crate::metadata::MetadataError; use crate::metadata::MetadataError;
@ -52,8 +52,12 @@ impl RequiresDist {
) -> Result<Self, MetadataError> { ) -> Result<Self, MetadataError> {
// TODO(konsti): Limit discovery for Git checkouts to Git root. // TODO(konsti): Limit discovery for Git checkouts to Git root.
// TODO(konsti): Cache workspace discovery. // TODO(konsti): Cache workspace discovery.
let Some(project_workspace) = let Some(project_workspace) = ProjectWorkspace::from_maybe_project_root(
ProjectWorkspace::from_maybe_project_root(install_path, lock_path, None).await? install_path,
lock_path,
&DiscoveryOptions::default(),
)
.await?
else { else {
return Ok(Self::from_metadata23(metadata)); return Ok(Self::from_metadata23(metadata));
}; };
@ -155,7 +159,7 @@ mod test {
use uv_configuration::PreviewMode; use uv_configuration::PreviewMode;
use uv_workspace::pyproject::PyProjectToml; use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::ProjectWorkspace; use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
use crate::RequiresDist; use crate::RequiresDist;
@ -170,7 +174,10 @@ mod test {
.as_ref() .as_ref()
.context("metadata field project not found")?, .context("metadata field project not found")?,
&pyproject_toml, &pyproject_toml,
Some(path), &DiscoveryOptions {
stop_discovery_at: Some(path),
..DiscoveryOptions::default()
},
) )
.await?; .await?;
let requires_dist = pypi_types::RequiresDist::parse_pyproject_toml(contents)?; let requires_dist = pypi_types::RequiresDist::parse_pyproject_toml(contents)?;

View file

@ -49,6 +49,21 @@ impl RequiresPython {
} }
} }
/// Returns a [`RequiresPython`] from a version specifier.
pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Result<Self, RequiresPythonError> {
let bound = RequiresPythonBound(
crate::pubgrub::PubGrubSpecifier::from_release_specifiers(specifiers)?
.iter()
.next()
.map(|(lower, _)| lower.clone())
.unwrap_or(Bound::Unbounded),
);
Ok(Self {
specifiers: specifiers.clone(),
bound,
})
}
/// Returns a [`RequiresPython`] to express the union of the given version specifiers. /// Returns a [`RequiresPython`] to express the union of the given version specifiers.
/// ///
/// For example, given `>=3.8` and `>=3.9`, this would return `>=3.8`. /// For example, given `>=3.8` and `>=3.9`, this would return `>=3.8`.

View file

@ -1,4 +1,6 @@
pub use workspace::{ProjectWorkspace, VirtualProject, Workspace, WorkspaceError, WorkspaceMember}; pub use workspace::{
DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace, WorkspaceError, WorkspaceMember,
};
pub mod pyproject; pub mod pyproject;
pub mod pyproject_mut; pub mod pyproject_mut;

View file

@ -42,6 +42,14 @@ pub enum WorkspaceError {
Normalize(#[source] std::io::Error), Normalize(#[source] std::io::Error),
} }
#[derive(Debug, Default, Clone)]
pub struct DiscoveryOptions<'a> {
/// The path to stop discovery at.
pub stop_discovery_at: Option<&'a Path>,
/// The set of member paths to ignore.
pub ignore: FxHashSet<&'a Path>,
}
/// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`]. /// A workspace, consisting of a root directory and members. See [`ProjectWorkspace`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(test, derive(serde::Serialize))] #[cfg_attr(test, derive(serde::Serialize))]
@ -80,7 +88,7 @@ impl Workspace {
/// * If there is no explicit workspace: We have a single project workspace, we're done. /// * If there is no explicit workspace: We have a single project workspace, we're done.
pub async fn discover( pub async fn discover(
path: &Path, path: &Path,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Workspace, WorkspaceError> { ) -> Result<Workspace, WorkspaceError> {
let path = absolutize_path(path) let path = absolutize_path(path)
.map_err(WorkspaceError::Normalize)? .map_err(WorkspaceError::Normalize)?
@ -133,8 +141,7 @@ impl Workspace {
} else if pyproject_toml.project.is_none() { } else if pyproject_toml.project.is_none() {
// Without a project, it can't be an implicit root // Without a project, it can't be an implicit root
return Err(WorkspaceError::MissingProject(project_path)); return Err(WorkspaceError::MissingProject(project_path));
} else if let Some(workspace) = find_workspace(&project_path, stop_discovery_at).await? } else if let Some(workspace) = find_workspace(&project_path, options).await? {
{
// We have found an explicit root above. // We have found an explicit root above.
workspace workspace
} else { } else {
@ -168,7 +175,7 @@ impl Workspace {
workspace_definition, workspace_definition,
workspace_pyproject_toml, workspace_pyproject_toml,
current_project, current_project,
stop_discovery_at, options,
) )
.await .await
} }
@ -345,11 +352,19 @@ impl Workspace {
} }
} }
/// Returns `true` if the path is a workspace member. /// Returns `true` if the path is included by the workspace.
pub fn includes(&self, project_path: &Path) -> bool { pub fn includes(&self, project_path: &Path) -> Result<bool, WorkspaceError> {
self.packages if let Some(workspace) = self
.values() .pyproject_toml
.any(|member| project_path == member.root()) .tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.workspace.as_ref())
{
is_included_in_workspace(project_path, &self.install_path, workspace)
} else {
Ok(false)
}
} }
/// Collect the workspace member projects from the `members` and `excludes` entries. /// Collect the workspace member projects from the `members` and `excludes` entries.
@ -359,7 +374,7 @@ impl Workspace {
workspace_definition: ToolUvWorkspace, workspace_definition: ToolUvWorkspace,
workspace_pyproject_toml: PyProjectToml, workspace_pyproject_toml: PyProjectToml,
current_project: Option<WorkspaceMember>, current_project: Option<WorkspaceMember>,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Workspace, WorkspaceError> { ) -> Result<Workspace, WorkspaceError> {
let mut workspace_members = BTreeMap::new(); let mut workspace_members = BTreeMap::new();
// Avoid reading a `pyproject.toml` more than once. // Avoid reading a `pyproject.toml` more than once.
@ -421,6 +436,13 @@ impl Workspace {
if !seen.insert(member_root.clone()) { if !seen.insert(member_root.clone()) {
continue; continue;
} }
if options.ignore.contains(member_root.as_path()) {
debug!(
"Ignoring workspace member: `{}`",
member_root.simplified_display()
);
continue;
}
let member_root = absolutize_path(&member_root) let member_root = absolutize_path(&member_root)
.map_err(WorkspaceError::Normalize)? .map_err(WorkspaceError::Normalize)?
.to_path_buf(); .to_path_buf();
@ -474,7 +496,7 @@ impl Workspace {
.and_then(|uv| uv.sources) .and_then(|uv| uv.sources)
.unwrap_or_default(); .unwrap_or_default();
check_nested_workspaces(&workspace_root, stop_discovery_at); check_nested_workspaces(&workspace_root, options);
Ok(Workspace { Ok(Workspace {
install_path: workspace_root, install_path: workspace_root,
@ -612,13 +634,14 @@ impl ProjectWorkspace {
/// only directories between the current path and `stop_discovery_at` are considered. /// only directories between the current path and `stop_discovery_at` are considered.
pub async fn discover( pub async fn discover(
path: &Path, path: &Path,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
let project_root = path let project_root = path
.ancestors() .ancestors()
.take_while(|path| { .take_while(|path| {
// Only walk up the given directory, if any. // Only walk up the given directory, if any.
stop_discovery_at options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path) .map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true) .unwrap_or(true)
}) })
@ -630,13 +653,13 @@ impl ProjectWorkspace {
project_root.simplified_display() project_root.simplified_display()
); );
Self::from_project_root(project_root, stop_discovery_at).await Self::from_project_root(project_root, options).await
} }
/// Discover the workspace starting from the directory containing the `pyproject.toml`. /// Discover the workspace starting from the directory containing the `pyproject.toml`.
async fn from_project_root( async fn from_project_root(
project_root: &Path, project_root: &Path,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
// Read the current `pyproject.toml`. // Read the current `pyproject.toml`.
let pyproject_path = project_root.join("pyproject.toml"); let pyproject_path = project_root.join("pyproject.toml");
@ -655,7 +678,7 @@ impl ProjectWorkspace {
Path::new(""), Path::new(""),
&project, &project,
&pyproject_toml, &pyproject_toml,
stop_discovery_at, options,
) )
.await .await
} }
@ -665,7 +688,7 @@ impl ProjectWorkspace {
pub async fn from_maybe_project_root( pub async fn from_maybe_project_root(
install_path: &Path, install_path: &Path,
lock_path: &Path, lock_path: &Path,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Option<Self>, WorkspaceError> { ) -> Result<Option<Self>, WorkspaceError> {
// Read the `pyproject.toml`. // Read the `pyproject.toml`.
let pyproject_path = install_path.join("pyproject.toml"); let pyproject_path = install_path.join("pyproject.toml");
@ -683,14 +706,7 @@ impl ProjectWorkspace {
}; };
Ok(Some( Ok(Some(
Self::from_project( Self::from_project(install_path, lock_path, &project, &pyproject_toml, options).await?,
install_path,
lock_path,
&project,
&pyproject_toml,
stop_discovery_at,
)
.await?,
)) ))
} }
@ -721,7 +737,7 @@ impl ProjectWorkspace {
lock_path: &Path, lock_path: &Path,
project: &Project, project: &Project,
project_pyproject_toml: &PyProjectToml, project_pyproject_toml: &PyProjectToml,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
let project_path = absolutize_path(install_path) let project_path = absolutize_path(install_path)
.map_err(WorkspaceError::Normalize)? .map_err(WorkspaceError::Normalize)?
@ -756,7 +772,7 @@ impl ProjectWorkspace {
if workspace.is_none() { if workspace.is_none() {
// The project isn't an explicit workspace root, check if we're a regular workspace // The project isn't an explicit workspace root, check if we're a regular workspace
// member by looking for an explicit workspace root above. // member by looking for an explicit workspace root above.
workspace = find_workspace(&project_path, stop_discovery_at).await?; workspace = find_workspace(&project_path, options).await?;
} }
let current_project = WorkspaceMember { let current_project = WorkspaceMember {
@ -819,7 +835,7 @@ impl ProjectWorkspace {
workspace_definition, workspace_definition,
workspace_pyproject_toml, workspace_pyproject_toml,
Some(current_project), Some(current_project),
stop_discovery_at, options,
) )
.await?; .await?;
@ -834,14 +850,15 @@ impl ProjectWorkspace {
/// Find the workspace root above the current project, if any. /// Find the workspace root above the current project, if any.
async fn find_workspace( async fn find_workspace(
project_root: &Path, project_root: &Path,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Option<(PathBuf, ToolUvWorkspace, PyProjectToml)>, WorkspaceError> { ) -> Result<Option<(PathBuf, ToolUvWorkspace, PyProjectToml)>, WorkspaceError> {
// Skip 1 to ignore the current project itself. // Skip 1 to ignore the current project itself.
for workspace_root in project_root for workspace_root in project_root
.ancestors() .ancestors()
.take_while(|path| { .take_while(|path| {
// Only walk up the given directory, if any. // Only walk up the given directory, if any.
stop_discovery_at options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path) .map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true) .unwrap_or(true)
}) })
@ -919,12 +936,13 @@ async fn find_workspace(
} }
/// Warn when the valid workspace is included in another workspace. /// Warn when the valid workspace is included in another workspace.
fn check_nested_workspaces(inner_workspace_root: &Path, stop_discovery_at: Option<&Path>) { fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptions) {
for outer_workspace_root in inner_workspace_root for outer_workspace_root in inner_workspace_root
.ancestors() .ancestors()
.take_while(|path| { .take_while(|path| {
// Only walk up the given directory, if any. // Only walk up the given directory, if any.
stop_discovery_at options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path) .map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true) .unwrap_or(true)
}) })
@ -1013,6 +1031,31 @@ fn is_excluded_from_workspace(
Ok(false) Ok(false)
} }
/// Check if we're in the `tool.uv.workspace.members` of a workspace.
fn is_included_in_workspace(
project_path: &Path,
workspace_root: &Path,
workspace: &ToolUvWorkspace,
) -> Result<bool, WorkspaceError> {
for member_glob in workspace.members.iter().flatten() {
let absolute_glob = workspace_root
.simplified()
.join(member_glob.as_str())
.to_string_lossy()
.to_string();
for member_root in glob(&absolute_glob)
.map_err(|err| WorkspaceError::Pattern(absolute_glob.to_string(), err))?
{
let member_root =
member_root.map_err(|err| WorkspaceError::Glob(absolute_glob.to_string(), err))?;
if member_root == project_path {
return Ok(true);
}
}
}
Ok(false)
}
/// A project that can be synced. /// A project that can be synced.
/// ///
/// The project could be a package within a workspace, a real workspace root, or even a virtual /// The project could be a package within a workspace, a real workspace root, or even a virtual
@ -1035,7 +1078,7 @@ impl VirtualProject {
/// discovering the main workspace. /// discovering the main workspace.
pub async fn discover( pub async fn discover(
path: &Path, path: &Path,
stop_discovery_at: Option<&Path>, options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> { ) -> Result<Self, WorkspaceError> {
assert!( assert!(
path.is_absolute(), path.is_absolute(),
@ -1045,7 +1088,8 @@ impl VirtualProject {
.ancestors() .ancestors()
.take_while(|path| { .take_while(|path| {
// Only walk up the given directory, if any. // Only walk up the given directory, if any.
stop_discovery_at options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path) .map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true) .unwrap_or(true)
}) })
@ -1070,7 +1114,7 @@ impl VirtualProject {
Path::new(""), Path::new(""),
project, project,
&pyproject_toml, &pyproject_toml,
stop_discovery_at, options,
) )
.await?; .await?;
Ok(Self::Project(project)) Ok(Self::Project(project))
@ -1091,7 +1135,7 @@ impl VirtualProject {
workspace.clone(), workspace.clone(),
pyproject_toml, pyproject_toml,
None, None,
stop_discovery_at, options,
) )
.await?; .await?;
@ -1135,7 +1179,7 @@ mod tests {
use insta::assert_json_snapshot; use insta::assert_json_snapshot;
use crate::workspace::ProjectWorkspace; use crate::workspace::{DiscoveryOptions, ProjectWorkspace};
async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) { async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) {
let root_dir = env::current_dir() let root_dir = env::current_dir()
@ -1146,9 +1190,10 @@ mod tests {
.unwrap() .unwrap()
.join("scripts") .join("scripts")
.join("workspaces"); .join("workspaces");
let project = ProjectWorkspace::discover(&root_dir.join(folder), None) let project =
.await ProjectWorkspace::discover(&root_dir.join(folder), &DiscoveryOptions::default())
.unwrap(); .await
.unwrap();
let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref()); let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref());
(project, root_escaped) (project, root_escaped)
} }

View file

@ -14,7 +14,7 @@ use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::pyproject::{DependencyType, Source, SourceError}; use uv_workspace::pyproject::{DependencyType, Source, SourceError};
use uv_workspace::pyproject_mut::PyProjectTomlMut; use uv_workspace::pyproject_mut::PyProjectTomlMut;
use uv_workspace::{ProjectWorkspace, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::pip::resolution_environment; use crate::commands::pip::resolution_environment;
@ -54,12 +54,12 @@ pub(crate) async fn add(
// Find the project in the workspace. // Find the project in the workspace.
let project = if let Some(package) = package { let project = if let Some(package) = package {
Workspace::discover(&std::env::current_dir()?, None) Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await? .await?
.with_current_project(package.clone()) .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))? .with_context(|| format!("Package `{package}` not found in workspace"))?
} else { } else {
ProjectWorkspace::discover(&std::env::current_dir()?, None).await? ProjectWorkspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default()).await?
}; };
// Discover or create the virtual environment. // Discover or create the virtual environment.

View file

@ -3,14 +3,23 @@ use std::path::PathBuf;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use pep440_rs::Version;
use pep508_rs::PackageName; use pep508_rs::PackageName;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::PreviewMode; use uv_configuration::PreviewMode;
use uv_fs::{absolutize_path, Simplified}; use uv_fs::{absolutize_path, Simplified};
use uv_python::{
EnvironmentPreference, PythonFetch, PythonInstallation, PythonPreference, PythonRequest,
VersionRequest,
};
use uv_resolver::RequiresPython;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::pyproject_mut::PyProjectTomlMut; use uv_workspace::pyproject_mut::PyProjectTomlMut;
use uv_workspace::{Workspace, WorkspaceError}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError};
use crate::commands::project::find_requires_python;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
use crate::printer::Printer; use crate::printer::Printer;
@ -20,8 +29,14 @@ pub(crate) async fn init(
explicit_path: Option<String>, explicit_path: Option<String>,
name: Option<PackageName>, name: Option<PackageName>,
no_readme: bool, no_readme: bool,
python: Option<String>,
isolated: bool, isolated: bool,
preview: PreviewMode, preview: PreviewMode,
python_preference: PythonPreference,
python_fetch: PythonFetch,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
if preview.is_disabled() { if preview.is_disabled() {
@ -62,36 +77,111 @@ pub(crate) async fn init(
} }
}; };
// Create the `pyproject.toml`.
let pyproject = indoc::formatdoc! {r#"
[project]
name = "{name}"
version = "0.1.0"
description = "Add your description here"{readme}
dependencies = []
[tool.uv]
dev-dependencies = []
"#,
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
};
fs_err::create_dir_all(&path)?;
fs_err::write(path.join("pyproject.toml"), pyproject)?;
// Discover the current workspace, if it exists. // Discover the current workspace, if it exists.
let workspace = if isolated { let workspace = if isolated {
None None
} else { } else {
// Attempt to find a workspace root. // Attempt to find a workspace root.
let parent = path.parent().expect("Project path has no parent"); let parent = path.parent().expect("Project path has no parent");
match Workspace::discover(parent, None).await { match Workspace::discover(
parent,
&DiscoveryOptions {
ignore: std::iter::once(path.as_ref()).collect(),
..DiscoveryOptions::default()
},
)
.await
{
Ok(workspace) => Some(workspace), Ok(workspace) => Some(workspace),
Err(WorkspaceError::MissingPyprojectToml) => None, Err(WorkspaceError::MissingPyprojectToml) => None,
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
} }
}; };
// Add a `requires-python` field to the `pyproject.toml`.
let requires_python = if let Some(request) = python.as_deref() {
// (1) Explicit request from user
match PythonRequest::parse(request) {
PythonRequest::Version(VersionRequest::MajorMinor(major, minor)) => {
RequiresPython::greater_than_equal_version(&Version::new([
u64::from(major),
u64::from(minor),
]))
}
PythonRequest::Version(VersionRequest::MajorMinorPatch(major, minor, patch)) => {
RequiresPython::greater_than_equal_version(&Version::new([
u64::from(major),
u64::from(minor),
u64::from(patch),
]))
}
PythonRequest::Version(VersionRequest::Range(specifiers)) => {
RequiresPython::from_specifiers(&specifiers)?
}
request => {
let reporter = PythonDownloadReporter::single(printer);
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);
let interpreter = PythonInstallation::find_or_fetch(
Some(request),
EnvironmentPreference::Any,
python_preference,
python_fetch,
&client_builder,
cache,
Some(&reporter),
)
.await?
.into_interpreter();
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version())
}
}
} else if let Some(requires_python) = workspace
.as_ref()
.and_then(|workspace| find_requires_python(workspace).ok().flatten())
{
// (2) `Requires-Python` from the workspace
requires_python
} else {
// (3) Default to the system Python
let request = PythonRequest::Any;
let reporter = PythonDownloadReporter::single(printer);
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);
let interpreter = PythonInstallation::find_or_fetch(
Some(request),
EnvironmentPreference::Any,
python_preference,
python_fetch,
&client_builder,
cache,
Some(&reporter),
)
.await?
.into_interpreter();
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version())
};
// Create the `pyproject.toml`.
let pyproject = indoc::formatdoc! {r#"
[project]
name = "{name}"
version = "0.1.0"
description = "Add your description here"{readme}
requires-python = "{requires_python}"
dependencies = []
[tool.uv]
dev-dependencies = []
"#,
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
requires_python = requires_python.specifiers(),
};
fs_err::create_dir_all(&path)?;
fs_err::write(path.join("pyproject.toml"), pyproject)?;
// Create `src/{name}/__init__.py` if it does not already exist. // Create `src/{name}/__init__.py` if it does not already exist.
let src_dir = path.join("src").join(&*name.as_dist_info_name()); let src_dir = path.join("src").join(&*name.as_dist_info_name());
let init_py = src_dir.join("__init__.py"); let init_py = src_dir.join("__init__.py");
@ -123,7 +213,7 @@ pub(crate) async fn init(
name.cyan(), name.cyan(),
workspace.install_path().simplified_display().cyan() workspace.install_path().simplified_display().cyan()
)?; )?;
} else if workspace.includes(&path) { } else if workspace.includes(&path)? {
// If the member is already included in the workspace, skip the `members` addition. // If the member is already included in the workspace, skip the `members` addition.
writeln!( writeln!(
printer.stderr(), printer.stderr(),

View file

@ -24,7 +24,7 @@ use uv_resolver::{
}; };
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::Workspace; use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::project::{find_requires_python, FoundInterpreter, ProjectError, SharedState}; use crate::commands::project::{find_requires_python, FoundInterpreter, ProjectError, SharedState};
use crate::commands::{pip, ExitStatus}; use crate::commands::{pip, ExitStatus};
@ -51,7 +51,8 @@ pub(crate) async fn lock(
} }
// Find the project requirements. // Find the project requirements.
let workspace = Workspace::discover(&std::env::current_dir()?, None).await?; let workspace =
Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default()).await?;
// Find an interpreter for the project // Find an interpreter for the project
let interpreter = FoundInterpreter::discover( let interpreter = FoundInterpreter::discover(

View file

@ -8,7 +8,7 @@ use uv_python::{PythonFetch, PythonPreference, PythonRequest};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::pyproject::DependencyType; use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject_mut::PyProjectTomlMut; use uv_workspace::pyproject_mut::PyProjectTomlMut;
use uv_workspace::{ProjectWorkspace, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::{project, ExitStatus, SharedState}; use crate::commands::{project, ExitStatus, SharedState};
@ -39,12 +39,12 @@ pub(crate) async fn remove(
// Find the project in the workspace. // Find the project in the workspace.
let project = if let Some(package) = package { let project = if let Some(package) = package {
Workspace::discover(&std::env::current_dir()?, None) Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await? .await?
.with_current_project(package.clone()) .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))? .with_context(|| format!("Package `{package}` not found in workspace"))?
} else { } else {
ProjectWorkspace::discover(&std::env::current_dir()?, None).await? ProjectWorkspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default()).await?
}; };
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?; let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;

View file

@ -23,7 +23,7 @@ use uv_python::{
}; };
use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::{VirtualProject, Workspace, WorkspaceError}; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::environment::CachedEnvironment;
@ -144,13 +144,15 @@ pub(crate) async fn run(
// We need a workspace, but we don't need to have a current package, we can be e.g. in // We need a workspace, but we don't need to have a current package, we can be e.g. in
// the root of a virtual workspace and then switch into the selected package. // the root of a virtual workspace and then switch into the selected package.
Some(VirtualProject::Project( Some(VirtualProject::Project(
Workspace::discover(&std::env::current_dir()?, None) Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await? .await?
.with_current_project(package.clone()) .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?, .with_context(|| format!("Package `{package}` not found in workspace"))?,
)) ))
} else { } else {
match VirtualProject::discover(&std::env::current_dir()?, None).await { match VirtualProject::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await
{
Ok(project) => Some(project), Ok(project) => Some(project),
Err(WorkspaceError::MissingPyprojectToml) => None, Err(WorkspaceError::MissingPyprojectToml) => None,
Err(WorkspaceError::NonWorkspace(_)) => None, Err(WorkspaceError::NonWorkspace(_)) => None,

View file

@ -12,7 +12,7 @@ use uv_python::{PythonEnvironment, PythonFetch, PythonPreference, PythonRequest}
use uv_resolver::{FlatIndex, Lock}; use uv_resolver::{FlatIndex, Lock};
use uv_types::{BuildIsolation, HashStrategy}; use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::VirtualProject; use uv_workspace::{DiscoveryOptions, VirtualProject};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
use crate::commands::project::lock::do_safe_lock; use crate::commands::project::lock::do_safe_lock;
@ -45,7 +45,8 @@ pub(crate) async fn sync(
} }
// Identify the project // Identify the project
let project = VirtualProject::discover(&std::env::current_dir()?, None).await?; let project =
VirtualProject::discover(&std::env::current_dir()?, &DiscoveryOptions::default()).await?;
// Discover or create the virtual environment. // Discover or create the virtual environment.
let venv = project::get_or_init_environment( let venv = project::get_or_init_environment(

View file

@ -10,7 +10,7 @@ use uv_client::Connectivity;
use uv_configuration::{Concurrency, PreviewMode}; use uv_configuration::{Concurrency, PreviewMode};
use uv_python::{PythonFetch, PythonPreference, PythonRequest}; use uv_python::{PythonFetch, PythonPreference, PythonRequest};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::Workspace; use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::pip::tree::DisplayDependencyGraph; use crate::commands::pip::tree::DisplayDependencyGraph;
use crate::commands::project::FoundInterpreter; use crate::commands::project::FoundInterpreter;
@ -47,7 +47,8 @@ pub(crate) async fn tree(
} }
// Find the project requirements. // Find the project requirements.
let workspace = Workspace::discover(&std::env::current_dir()?, None).await?; let workspace =
Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default()).await?;
// Find an interpreter for the project // Find an interpreter for the project
let interpreter = FoundInterpreter::discover( let interpreter = FoundInterpreter::discover(

View file

@ -14,7 +14,7 @@ use uv_python::{
PYTHON_VERSION_FILENAME, PYTHON_VERSION_FILENAME,
}; };
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::VirtualProject; use uv_workspace::{DiscoveryOptions, VirtualProject};
use crate::commands::{project::find_requires_python, ExitStatus}; use crate::commands::{project::find_requires_python, ExitStatus};
use crate::printer::Printer; use crate::printer::Printer;
@ -33,14 +33,17 @@ pub(crate) async fn pin(
warn_user_once!("`uv python pin` is experimental and may change without warning"); warn_user_once!("`uv python pin` is experimental and may change without warning");
} }
let virtual_project = match VirtualProject::discover(&std::env::current_dir()?, None).await { let virtual_project =
Ok(virtual_project) if !isolated => Some(virtual_project), match VirtualProject::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
Ok(_) => None, .await
Err(err) => { {
debug!("Failed to discover virtual project: {err}"); Ok(virtual_project) if !isolated => Some(virtual_project),
None Ok(_) => None,
} Err(err) => {
}; debug!("Failed to discover virtual project: {err}");
None
}
};
let Some(request) = request else { let Some(request) = request else {
// Display the current pinned Python version // Display the current pinned Python version

View file

@ -1,4 +1,3 @@
use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::fmt::Write; use std::fmt::Write;
use std::io::stdout; use std::io::stdout;
@ -24,7 +23,7 @@ use uv_cli::{SelfCommand, SelfNamespace};
use uv_configuration::Concurrency; use uv_configuration::Concurrency;
use uv_requirements::RequirementsSource; use uv_requirements::RequirementsSource;
use uv_settings::{Combine, FilesystemOptions}; use uv_settings::{Combine, FilesystemOptions};
use uv_workspace::Workspace; use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::{ExitStatus, ToolRunCommand}; use crate::commands::{ExitStatus, ToolRunCommand};
use crate::printer::Printer; use crate::printer::Printer;
@ -74,12 +73,14 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
Some(FilesystemOptions::from_file(config_file)?) Some(FilesystemOptions::from_file(config_file)?)
} else if cli.global_args.isolated { } else if cli.global_args.isolated {
None None
} else if let Ok(project) = Workspace::discover(&env::current_dir()?, None).await { } else if let Ok(project) =
Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default()).await
{
let project = FilesystemOptions::from_directory(project.install_path())?; let project = FilesystemOptions::from_directory(project.install_path())?;
let user = FilesystemOptions::user()?; let user = FilesystemOptions::user()?;
project.combine(user) project.combine(user)
} else { } else {
let project = FilesystemOptions::find(env::current_dir()?)?; let project = FilesystemOptions::find(std::env::current_dir()?)?;
let user = FilesystemOptions::user()?; let user = FilesystemOptions::user()?;
project.combine(user) project.combine(user)
}; };
@ -130,7 +131,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.break_words(false) .break_words(false)
.word_separator(textwrap::WordSeparator::AsciiSpace) .word_separator(textwrap::WordSeparator::AsciiSpace)
.word_splitter(textwrap::WordSplitter::NoHyphenation) .word_splitter(textwrap::WordSplitter::NoHyphenation)
.wrap_lines(env::var("UV_NO_WRAP").map(|_| false).unwrap_or(true)) .wrap_lines(std::env::var("UV_NO_WRAP").map(|_| false).unwrap_or(true))
.build(), .build(),
) )
}))?; }))?;
@ -834,8 +835,14 @@ async fn run_project(
args.path, args.path,
args.name, args.name,
args.no_readme, args.no_readme,
args.python,
globals.isolated, globals.isolated,
globals.preview, globals.preview,
globals.python_preference,
globals.python_fetch,
globals.connectivity,
globals.native_tls,
&cache,
printer, printer,
) )
.await .await
@ -1100,7 +1107,7 @@ where
// We support increasing the stack size to avoid stack overflows in debug mode on Windows. In // We support increasing the stack size to avoid stack overflows in debug mode on Windows. In
// addition, we box types and futures in various places. This includes the `Box::pin(run())` // addition, we box types and futures in various places. This includes the `Box::pin(run())`
// here, which prevents the large (non-send) main future alone from overflowing the stack. // here, which prevents the large (non-send) main future alone from overflowing the stack.
let result = if let Ok(stack_size) = env::var("UV_STACK_SIZE") { let result = if let Ok(stack_size) = std::env::var("UV_STACK_SIZE") {
let stack_size = stack_size.parse().expect("Invalid stack size"); let stack_size = stack_size.parse().expect("Invalid stack size");
let tokio_main = move || { let tokio_main = move || {
let runtime = tokio::runtime::Builder::new_current_thread() let runtime = tokio::runtime::Builder::new_current_thread()

View file

@ -154,6 +154,7 @@ pub(crate) struct InitSettings {
pub(crate) path: Option<String>, pub(crate) path: Option<String>,
pub(crate) name: Option<PackageName>, pub(crate) name: Option<PackageName>,
pub(crate) no_readme: bool, pub(crate) no_readme: bool,
pub(crate) python: Option<String>,
} }
impl InitSettings { impl InitSettings {
@ -164,12 +165,14 @@ impl InitSettings {
path, path,
name, name,
no_readme, no_readme,
python,
} = args; } = args;
Self { Self {
path, path,
name, name,
no_readme, no_readme,
python,
} }
} }
} }

View file

@ -37,6 +37,7 @@ fn init() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -65,7 +66,6 @@ fn init() -> Result<()> {
----- stderr ----- ----- stderr -----
warning: `uv lock` is experimental and may change without warning warning: `uv lock` is experimental and may change without warning
Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
warning: No `requires-python` field found in the workspace. Defaulting to `>=3.12`.
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
"###); "###);
@ -98,6 +98,7 @@ fn init_no_readme() -> Result<()> {
name = "foo" name = "foo"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -140,6 +141,7 @@ fn current_dir() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -168,7 +170,6 @@ fn current_dir() -> Result<()> {
----- stderr ----- ----- stderr -----
warning: `uv lock` is experimental and may change without warning warning: `uv lock` is experimental and may change without warning
Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
warning: No `requires-python` field found in the workspace. Defaulting to `>=3.12`.
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
"###); "###);
@ -206,6 +207,7 @@ fn init_dot_args() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -234,7 +236,6 @@ fn init_dot_args() -> Result<()> {
----- stderr ----- ----- stderr -----
warning: `uv lock` is experimental and may change without warning warning: `uv lock` is experimental and may change without warning
Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
warning: No `requires-python` field found in the workspace. Defaulting to `>=3.12`.
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
"###); "###);
@ -285,6 +286,7 @@ fn init_workspace() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -352,7 +354,6 @@ fn init_workspace_relative_sub_package() -> Result<()> {
})?; })?;
let child = context.temp_dir.join("foo"); let child = context.temp_dir.join("foo");
fs_err::create_dir(&child)?;
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo"), @r###" uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo"), @r###"
success: true success: true
@ -380,6 +381,7 @@ fn init_workspace_relative_sub_package() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -447,7 +449,6 @@ fn init_workspace_outside() -> Result<()> {
})?; })?;
let child = context.temp_dir.join("foo"); let child = context.temp_dir.join("foo");
fs_err::create_dir(&child)?;
// Run `uv init <path>` outside the workspace. // Run `uv init <path>` outside the workspace.
uv_snapshot!(context.filters(), context.init().current_dir(&context.home_dir).arg(&child), @r###" uv_snapshot!(context.filters(), context.init().current_dir(&context.home_dir).arg(&child), @r###"
@ -476,6 +477,7 @@ fn init_workspace_outside() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -556,6 +558,7 @@ fn init_invalid_names() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -690,6 +693,7 @@ fn init_nested_workspace() -> Result<()> {
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12"
dependencies = [] dependencies = []
[tool.uv] [tool.uv]
@ -804,8 +808,11 @@ fn init_matches_members() -> Result<()> {
", ",
})?; })?;
// Create the parent directory (`packages`) and the child directory (`foo`), to ensure that
// the empty child directory does _not_ trigger a workspace discovery error despite being a
// valid member.
let packages = context.temp_dir.join("packages"); let packages = context.temp_dir.join("packages");
fs_err::create_dir_all(packages)?; fs_err::create_dir_all(packages.join("foo"))?;
uv_snapshot!(context.filters(), context.init().current_dir(context.temp_dir.join("packages")).arg("foo"), @r###" uv_snapshot!(context.filters(), context.init().current_dir(context.temp_dir.join("packages")).arg("foo"), @r###"
success: true success: true
@ -876,3 +883,163 @@ fn init_matches_exclude() -> Result<()> {
Ok(()) Ok(())
} }
/// Run `uv init`, inheriting the `requires-python` from the workspace.
#[test]
fn init_requires_python_workspace() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.10"
[tool.uv.workspace]
members = []
"#,
})?;
let child = context.temp_dir.join("foo");
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv init` is experimental and may change without warning
Adding `foo` as member of workspace `[TEMP_DIR]/`
Initialized project `foo` at `[TEMP_DIR]/foo`
"###);
let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?;
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
[project]
name = "foo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
[tool.uv]
dev-dependencies = []
"###
);
});
Ok(())
}
/// Run `uv init`, inferring the `requires-python` from the `--python` flag.
#[test]
fn init_requires_python_version() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv.workspace]
members = []
"#,
})?;
let child = context.temp_dir.join("foo");
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child).arg("--python").arg("3.8"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv init` is experimental and may change without warning
Adding `foo` as member of workspace `[TEMP_DIR]/`
Initialized project `foo` at `[TEMP_DIR]/foo`
"###);
let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?;
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
[project]
name = "foo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.8"
dependencies = []
[tool.uv]
dev-dependencies = []
"###
);
});
Ok(())
}
/// Run `uv init`, inferring the `requires-python` from the `--python` flag, and preserving the
/// specifiers verbatim.
#[test]
fn init_requires_python_specifiers() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv.workspace]
members = []
"#,
})?;
let child = context.temp_dir.join("foo");
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child).arg("--python").arg("==3.8.*"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv init` is experimental and may change without warning
Adding `foo` as member of workspace `[TEMP_DIR]/`
Initialized project `foo` at `[TEMP_DIR]/foo`
"###);
let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?;
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
[project]
name = "foo"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = "==3.8.*"
dependencies = []
[tool.uv]
dev-dependencies = []
"###
);
});
Ok(())
}