mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-20 11:56:03 +00:00
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:
parent
c8ac8ee57a
commit
0f8186d9ad
16 changed files with 461 additions and 103 deletions
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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)?;
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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())?;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue