Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uv init --virtual #5396

Merged
merged 3 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1794,7 +1794,11 @@ pub struct InitArgs {
#[arg(long)]
pub name: Option<PackageName>,

/// Do not create a readme file.
/// Create a virtual workspace instead of a project.
#[arg(long)]
pub r#virtual: bool,

/// Do not create a `README.md` file.
#[arg(long)]
pub no_readme: bool,

Expand Down
3 changes: 2 additions & 1 deletion crates/uv-workspace/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub use workspace::{
DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace, WorkspaceError, WorkspaceMember,
check_nested_workspaces, DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace,
WorkspaceError, WorkspaceMember,
};

pub mod pyproject;
Expand Down
14 changes: 9 additions & 5 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ impl Workspace {
workspace_root.simplified_display()
);

check_nested_workspaces(&workspace_root, options);

// Unlike in `ProjectWorkspace` discovery, we might be in a virtual workspace root without
// being in any specific project.
let current_project = pyproject_toml
Expand All @@ -170,6 +172,7 @@ impl Workspace {
project,
pyproject_toml,
});

Self::collect_members(
workspace_root.clone(),
// This method supports only absolute paths.
Expand Down Expand Up @@ -526,8 +529,6 @@ impl Workspace {
.and_then(|uv| uv.sources)
.unwrap_or_default();

check_nested_workspaces(&workspace_root, options);

Ok(Workspace {
install_path: workspace_root,
lock_path,
Expand Down Expand Up @@ -966,7 +967,7 @@ async fn find_workspace(
}

/// Warn when the valid workspace is included in another workspace.
fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptions) {
pub fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptions) {
for outer_workspace_root in inner_workspace_root
.ancestors()
.take_while(|path| {
Expand Down Expand Up @@ -1025,8 +1026,9 @@ fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptio
};
if !is_excluded {
warn_user!(
"Nested workspaces are not supported, but outer workspace includes existing workspace: `{}`",
pyproject_toml_path.user_display().cyan(),
"Nested workspaces are not supported, but outer workspace (`{}`) includes `{}`",
outer_workspace_root.simplified_display().cyan(),
inner_workspace_root.simplified_display().cyan()
);
}
}
Expand Down Expand Up @@ -1159,6 +1161,8 @@ impl VirtualProject {
.map_err(WorkspaceError::Normalize)?
.to_path_buf();

check_nested_workspaces(&project_path, options);

let workspace = Workspace::collect_members(
project_path,
PathBuf::new(),
Expand Down
142 changes: 101 additions & 41 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fmt::Write;
use std::path::PathBuf;
use std::path::Path;

use anyhow::{Context, Result};
use owo_colors::OwoColorize;
Expand All @@ -16,18 +16,19 @@ use uv_python::{
use uv_resolver::RequiresPython;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject_mut::PyProjectTomlMut;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError};
use uv_workspace::{check_nested_workspaces, DiscoveryOptions, Workspace, WorkspaceError};

use crate::commands::project::find_requires_python;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::ExitStatus;
use crate::printer::Printer;

/// Add one or more packages to the project requirements.
#[allow(clippy::single_match_else)]
#[allow(clippy::single_match_else, clippy::fn_params_excessive_bools)]
pub(crate) async fn init(
explicit_path: Option<String>,
name: Option<PackageName>,
r#virtual: bool,
no_readme: bool,
python: Option<String>,
isolated: bool,
Expand All @@ -46,7 +47,7 @@ pub(crate) async fn init(
// Default to the current directory if a path was not provided.
let path = match explicit_path {
None => std::env::current_dir()?.canonicalize()?,
Some(ref path) => PathBuf::from(path),
Some(ref path) => absolutize_path(Path::new(path))?.to_path_buf(),
};

// Make sure a project does not already exist in the given directory.
Expand All @@ -61,9 +62,6 @@ pub(crate) async fn init(
);
}

// Canonicalize the path to the project.
let path = absolutize_path(&path)?;

// Default to the directory name if a name was not provided.
let name = match name {
Some(name) => name,
Expand All @@ -77,6 +75,96 @@ pub(crate) async fn init(
}
};

if r#virtual {
init_virtual_workspace(&path, isolated)?;
} else {
init_project(
&path,
&name,
no_readme,
python,
isolated,
python_preference,
python_fetch,
connectivity,
native_tls,
cache,
printer,
)
.await?;
}

// Create the `README.md` if it does not already exist.
if !no_readme {
let readme = path.join("README.md");
if !readme.exists() {
fs_err::write(readme, String::new())?;
}
}

let project = if r#virtual { "workspace" } else { "project" };
match explicit_path {
// Initialized a project in the current directory.
None => {
writeln!(
printer.stderr(),
"Initialized {} `{}`",
project,
name.cyan()
)?;
}
// Initialized a project in the given directory.
Some(path) => {
let path = path
.simple_canonicalize()
.unwrap_or_else(|_| path.simplified().to_path_buf());

writeln!(
printer.stderr(),
"Initialized {} `{}` at `{}`",
project,
name.cyan(),
path.display().cyan()
)?;
}
}

Ok(ExitStatus::Success)
}

/// Initialize a virtual workspace at the given path.
fn init_virtual_workspace(path: &Path, isolated: bool) -> Result<()> {
// Ensure that we aren't creating a nested workspace.
if !isolated {
check_nested_workspaces(path, &DiscoveryOptions::default());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we warn or abort if nested workspace detected?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think warn is ok.

}

// Create the `pyproject.toml`.
let pyproject = indoc::indoc! {r"
[tool.uv.workspace]
members = []
"};

fs_err::create_dir_all(path)?;
fs_err::write(path.join("pyproject.toml"), pyproject)?;

Ok(())
}

/// Initialize a project (and, implicitly, a workspace root) at the given path.
async fn init_project(
path: &Path,
name: &PackageName,
no_readme: bool,
python: Option<String>,
isolated: bool,
python_preference: PythonPreference,
python_fetch: PythonFetch,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<()> {
// Discover the current workspace, if it exists.
let workspace = if isolated {
None
Expand All @@ -86,7 +174,7 @@ pub(crate) async fn init(
match Workspace::discover(
parent,
&DiscoveryOptions {
ignore: std::iter::once(path.as_ref()).collect(),
ignore: std::iter::once(path).collect(),
..DiscoveryOptions::default()
},
)
Expand Down Expand Up @@ -177,7 +265,8 @@ pub(crate) async fn init(
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
requires_python = requires_python.specifiers(),
};
fs_err::create_dir_all(&path)?;

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.
Expand All @@ -194,24 +283,16 @@ pub(crate) async fn init(
)?;
}

// Create the `README.md` if it does not already exist.
if !no_readme {
let readme = path.join("README.md");
if !readme.exists() {
fs_err::write(readme, String::new())?;
}
}

if let Some(workspace) = workspace {
if workspace.excludes(&path)? {
if workspace.excludes(path)? {
// If the member is excluded by the workspace, ignore it.
writeln!(
printer.stderr(),
"Project `{}` is excluded by workspace `{}`",
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(),
Expand Down Expand Up @@ -239,26 +320,5 @@ pub(crate) async fn init(
}
}

match explicit_path {
// Initialized a project in the current directory.
None => {
writeln!(printer.stderr(), "Initialized project `{}`", name.cyan())?;
}

// Initialized a project in the given directory.
Some(path) => {
let path = path
.simple_canonicalize()
.unwrap_or_else(|_| path.simplified().to_path_buf());

writeln!(
printer.stderr(),
"Initialized project `{}` at `{}`",
name.cyan(),
path.display().cyan()
)?;
}
}

Ok(ExitStatus::Success)
Ok(())
}
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,7 @@ async fn run_project(
commands::init(
args.path,
args.name,
args.r#virtual,
args.no_readme,
args.python,
globals.isolated,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ impl CacheSettings {
pub(crate) struct InitSettings {
pub(crate) path: Option<String>,
pub(crate) name: Option<PackageName>,
pub(crate) r#virtual: bool,
pub(crate) no_readme: bool,
pub(crate) python: Option<String>,
}
Expand All @@ -164,13 +165,15 @@ impl InitSettings {
let InitArgs {
path,
name,
r#virtual,
no_readme,
python,
} = args;

Self {
path,
name,
r#virtual,
no_readme,
python,
}
Expand Down
Loading
Loading