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.
#[arg(long)]
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)]

View file

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

View file

@ -42,6 +42,14 @@ pub enum WorkspaceError {
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`].
#[derive(Debug, Clone)]
#[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.
pub async fn discover(
path: &Path,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Workspace, WorkspaceError> {
let path = absolutize_path(path)
.map_err(WorkspaceError::Normalize)?
@ -133,8 +141,7 @@ impl Workspace {
} else if pyproject_toml.project.is_none() {
// Without a project, it can't be an implicit root
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.
workspace
} else {
@ -168,7 +175,7 @@ impl Workspace {
workspace_definition,
workspace_pyproject_toml,
current_project,
stop_discovery_at,
options,
)
.await
}
@ -345,11 +352,19 @@ impl Workspace {
}
}
/// Returns `true` if the path is a workspace member.
pub fn includes(&self, project_path: &Path) -> bool {
self.packages
.values()
.any(|member| project_path == member.root())
/// Returns `true` if the path is included by the workspace.
pub fn includes(&self, project_path: &Path) -> Result<bool, WorkspaceError> {
if let Some(workspace) = self
.pyproject_toml
.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.
@ -359,7 +374,7 @@ impl Workspace {
workspace_definition: ToolUvWorkspace,
workspace_pyproject_toml: PyProjectToml,
current_project: Option<WorkspaceMember>,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Workspace, WorkspaceError> {
let mut workspace_members = BTreeMap::new();
// Avoid reading a `pyproject.toml` more than once.
@ -421,6 +436,13 @@ impl Workspace {
if !seen.insert(member_root.clone()) {
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)
.map_err(WorkspaceError::Normalize)?
.to_path_buf();
@ -474,7 +496,7 @@ impl Workspace {
.and_then(|uv| uv.sources)
.unwrap_or_default();
check_nested_workspaces(&workspace_root, stop_discovery_at);
check_nested_workspaces(&workspace_root, options);
Ok(Workspace {
install_path: workspace_root,
@ -612,13 +634,14 @@ impl ProjectWorkspace {
/// only directories between the current path and `stop_discovery_at` are considered.
pub async fn discover(
path: &Path,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> {
let project_root = path
.ancestors()
.take_while(|path| {
// Only walk up the given directory, if any.
stop_discovery_at
options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true)
})
@ -630,13 +653,13 @@ impl ProjectWorkspace {
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`.
async fn from_project_root(
project_root: &Path,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> {
// Read the current `pyproject.toml`.
let pyproject_path = project_root.join("pyproject.toml");
@ -655,7 +678,7 @@ impl ProjectWorkspace {
Path::new(""),
&project,
&pyproject_toml,
stop_discovery_at,
options,
)
.await
}
@ -665,7 +688,7 @@ impl ProjectWorkspace {
pub async fn from_maybe_project_root(
install_path: &Path,
lock_path: &Path,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Option<Self>, WorkspaceError> {
// Read the `pyproject.toml`.
let pyproject_path = install_path.join("pyproject.toml");
@ -683,14 +706,7 @@ impl ProjectWorkspace {
};
Ok(Some(
Self::from_project(
install_path,
lock_path,
&project,
&pyproject_toml,
stop_discovery_at,
)
.await?,
Self::from_project(install_path, lock_path, &project, &pyproject_toml, options).await?,
))
}
@ -721,7 +737,7 @@ impl ProjectWorkspace {
lock_path: &Path,
project: &Project,
project_pyproject_toml: &PyProjectToml,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> {
let project_path = absolutize_path(install_path)
.map_err(WorkspaceError::Normalize)?
@ -756,7 +772,7 @@ impl ProjectWorkspace {
if workspace.is_none() {
// The project isn't an explicit workspace root, check if we're a regular workspace
// 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 {
@ -819,7 +835,7 @@ impl ProjectWorkspace {
workspace_definition,
workspace_pyproject_toml,
Some(current_project),
stop_discovery_at,
options,
)
.await?;
@ -834,14 +850,15 @@ impl ProjectWorkspace {
/// Find the workspace root above the current project, if any.
async fn find_workspace(
project_root: &Path,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Option<(PathBuf, ToolUvWorkspace, PyProjectToml)>, WorkspaceError> {
// Skip 1 to ignore the current project itself.
for workspace_root in project_root
.ancestors()
.take_while(|path| {
// Only walk up the given directory, if any.
stop_discovery_at
options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true)
})
@ -919,12 +936,13 @@ async fn find_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
.ancestors()
.take_while(|path| {
// Only walk up the given directory, if any.
stop_discovery_at
options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true)
})
@ -1013,6 +1031,31 @@ fn is_excluded_from_workspace(
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.
///
/// 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.
pub async fn discover(
path: &Path,
stop_discovery_at: Option<&Path>,
options: &DiscoveryOptions<'_>,
) -> Result<Self, WorkspaceError> {
assert!(
path.is_absolute(),
@ -1045,7 +1088,8 @@ impl VirtualProject {
.ancestors()
.take_while(|path| {
// Only walk up the given directory, if any.
stop_discovery_at
options
.stop_discovery_at
.map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true)
})
@ -1070,7 +1114,7 @@ impl VirtualProject {
Path::new(""),
project,
&pyproject_toml,
stop_discovery_at,
options,
)
.await?;
Ok(Self::Project(project))
@ -1091,7 +1135,7 @@ impl VirtualProject {
workspace.clone(),
pyproject_toml,
None,
stop_discovery_at,
options,
)
.await?;
@ -1135,7 +1179,7 @@ mod tests {
use insta::assert_json_snapshot;
use crate::workspace::ProjectWorkspace;
use crate::workspace::{DiscoveryOptions, ProjectWorkspace};
async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) {
let root_dir = env::current_dir()
@ -1146,7 +1190,8 @@ mod tests {
.unwrap()
.join("scripts")
.join("workspaces");
let project = ProjectWorkspace::discover(&root_dir.join(folder), None)
let project =
ProjectWorkspace::discover(&root_dir.join(folder), &DiscoveryOptions::default())
.await
.unwrap();
let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref());

View file

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

View file

@ -3,14 +3,23 @@ use std::path::PathBuf;
use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use pep440_rs::Version;
use pep508_rs::PackageName;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::PreviewMode;
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_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::printer::Printer;
@ -20,8 +29,14 @@ pub(crate) async fn init(
explicit_path: Option<String>,
name: Option<PackageName>,
no_readme: bool,
python: Option<String>,
isolated: bool,
preview: PreviewMode,
python_preference: PythonPreference,
python_fetch: PythonFetch,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
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.
let workspace = if isolated {
None
} else {
// Attempt to find a workspace root.
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),
Err(WorkspaceError::MissingPyprojectToml) => None,
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.
let src_dir = path.join("src").join(&*name.as_dist_info_name());
let init_py = src_dir.join("__init__.py");
@ -123,7 +213,7 @@ pub(crate) async fn init(
name.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.
writeln!(
printer.stderr(),

View file

@ -24,7 +24,7 @@ use uv_resolver::{
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
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::{pip, ExitStatus};
@ -51,7 +51,8 @@ pub(crate) async fn lock(
}
// 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
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_workspace::pyproject::DependencyType;
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::{project, ExitStatus, SharedState};
@ -39,12 +39,12 @@ pub(crate) async fn remove(
// Find the project in the workspace.
let project = if let Some(package) = package {
Workspace::discover(&std::env::current_dir()?, None)
Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?
} 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())?;

View file

@ -23,7 +23,7 @@ use uv_python::{
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
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::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
// the root of a virtual workspace and then switch into the selected package.
Some(VirtualProject::Project(
Workspace::discover(&std::env::current_dir()?, None)
Workspace::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
))
} else {
match VirtualProject::discover(&std::env::current_dir()?, None).await {
match VirtualProject::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await
{
Ok(project) => Some(project),
Err(WorkspaceError::MissingPyprojectToml) => 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_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once;
use uv_workspace::VirtualProject;
use uv_workspace::{DiscoveryOptions, VirtualProject};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::lock::do_safe_lock;
@ -45,7 +45,8 @@ pub(crate) async fn sync(
}
// 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.
let venv = project::get_or_init_environment(

View file

@ -10,7 +10,7 @@ use uv_client::Connectivity;
use uv_configuration::{Concurrency, PreviewMode};
use uv_python::{PythonFetch, PythonPreference, PythonRequest};
use uv_warnings::warn_user_once;
use uv_workspace::Workspace;
use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::pip::tree::DisplayDependencyGraph;
use crate::commands::project::FoundInterpreter;
@ -47,7 +47,8 @@ pub(crate) async fn tree(
}
// 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
let interpreter = FoundInterpreter::discover(

View file

@ -14,7 +14,7 @@ use uv_python::{
PYTHON_VERSION_FILENAME,
};
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::printer::Printer;
@ -33,7 +33,10 @@ pub(crate) async fn pin(
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 =
match VirtualProject::discover(&std::env::current_dir()?, &DiscoveryOptions::default())
.await
{
Ok(virtual_project) if !isolated => Some(virtual_project),
Ok(_) => None,
Err(err) => {

View file

@ -1,4 +1,3 @@
use std::env;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
@ -24,7 +23,7 @@ use uv_cli::{SelfCommand, SelfNamespace};
use uv_configuration::Concurrency;
use uv_requirements::RequirementsSource;
use uv_settings::{Combine, FilesystemOptions};
use uv_workspace::Workspace;
use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::{ExitStatus, ToolRunCommand};
use crate::printer::Printer;
@ -74,12 +73,14 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
Some(FilesystemOptions::from_file(config_file)?)
} else if cli.global_args.isolated {
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 user = FilesystemOptions::user()?;
project.combine(user)
} else {
let project = FilesystemOptions::find(env::current_dir()?)?;
let project = FilesystemOptions::find(std::env::current_dir()?)?;
let user = FilesystemOptions::user()?;
project.combine(user)
};
@ -130,7 +131,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.break_words(false)
.word_separator(textwrap::WordSeparator::AsciiSpace)
.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(),
)
}))?;
@ -834,8 +835,14 @@ async fn run_project(
args.path,
args.name,
args.no_readme,
args.python,
globals.isolated,
globals.preview,
globals.python_preference,
globals.python_fetch,
globals.connectivity,
globals.native_tls,
&cache,
printer,
)
.await
@ -1100,7 +1107,7 @@ where
// 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())`
// 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 tokio_main = move || {
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) name: Option<PackageName>,
pub(crate) no_readme: bool,
pub(crate) python: Option<String>,
}
impl InitSettings {
@ -164,12 +165,14 @@ impl InitSettings {
path,
name,
no_readme,
python,
} = args;
Self {
path,
name,
no_readme,
python,
}
}
}

View file

@ -37,6 +37,7 @@ fn init() -> Result<()> {
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -65,7 +66,6 @@ fn init() -> Result<()> {
----- stderr -----
warning: `uv lock` is experimental and may change without warning
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]
"###);
@ -98,6 +98,7 @@ fn init_no_readme() -> Result<()> {
name = "foo"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -140,6 +141,7 @@ fn current_dir() -> Result<()> {
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -168,7 +170,6 @@ fn current_dir() -> Result<()> {
----- stderr -----
warning: `uv lock` is experimental and may change without warning
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]
"###);
@ -206,6 +207,7 @@ fn init_dot_args() -> Result<()> {
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -234,7 +236,6 @@ fn init_dot_args() -> Result<()> {
----- stderr -----
warning: `uv lock` is experimental and may change without warning
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]
"###);
@ -285,6 +286,7 @@ fn init_workspace() -> Result<()> {
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -352,7 +354,6 @@ fn init_workspace_relative_sub_package() -> Result<()> {
})?;
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###"
success: true
@ -380,6 +381,7 @@ fn init_workspace_relative_sub_package() -> Result<()> {
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -447,7 +449,6 @@ fn init_workspace_outside() -> Result<()> {
})?;
let child = context.temp_dir.join("foo");
fs_err::create_dir(&child)?;
// Run `uv init <path>` outside the workspace.
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"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -556,6 +558,7 @@ fn init_invalid_names() -> Result<()> {
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
@ -690,6 +693,7 @@ fn init_nested_workspace() -> Result<()> {
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[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");
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###"
success: true
@ -876,3 +883,163 @@ fn init_matches_exclude() -> Result<()> {
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(())
}