From bc5b069a61dca8689342d9ee84ec8b5ace85d59d Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 27 Aug 2024 13:08:09 -0500 Subject: [PATCH] Add `--app` and `--lib` options to `uv init` (#6689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes the `uv init` experience with a focus on working for more use-cases out of the box. - Adds `--app` and `--lib` options to control the created project style - Changes the default from a library with `src/` and a build backend (`--lib`) to an application that is not packaged (`--app`) - Hides the `--virtual` option and replaces it with `--package` and `--no-package` - `--no-package` is not allowed with `--lib` right now, but it could be in the future once we understand a use-case - Creates a runnable project - Applications have a `hello.py` file which you can run with `uv run hello.py` - Packaged applications, e.g., `uv init --app --package` create a package and script entrypoint, which you can run with `uv run hello` - Libraries provide a demo API function, e.g., `uv run python -c "import name; print(name.hello())"` — this is unchanged Closes #6471 --- crates/uv-cli/src/lib.rs | 44 ++- crates/uv/src/commands/mod.rs | 2 +- crates/uv/src/commands/project/init.rs | 242 ++++++++---- crates/uv/src/lib.rs | 3 +- crates/uv/src/settings.rs | 21 +- crates/uv/tests/init.rs | 500 +++++++++++++++++++++++-- docs/reference/cli.md | 34 +- 7 files changed, 726 insertions(+), 120 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ea6783374..8f9e1efca 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2101,13 +2101,47 @@ pub struct InitArgs { /// Create a virtual project, rather than a package. /// - /// A virtual project is a project that is not intended to be built as a Python package, - /// such as a project that only contains scripts or other application code. - /// - /// Virtual projects themselves are not installed into the Python environment. - #[arg(long)] + /// This option is deprecated and will be removed in a future release. + #[arg(long, hide = true, conflicts_with = "package")] pub r#virtual: bool, + /// Set up the project to be built as a Python package. + /// + /// Defines a `[build-system]` for the project. + /// + /// This is the default behavior when using `--lib`. + /// + /// When using `--app`, this will include a `[project.scripts]` entrypoint and use a `src/` + /// project structure. + #[arg(long, overrides_with = "no_package")] + pub r#package: bool, + + /// Do not set up the project to be built as a Python package. + /// + /// Does not include a `[build-system]` for the project. + /// + /// This is the default behavior when using `--app`. + #[arg(long, overrides_with = "package", conflicts_with = "lib")] + pub r#no_package: bool, + + /// Create a project for an application. + /// + /// This is the default behavior if `--lib` is not requested. + /// + /// This project kind is for web servers, scripts, and command-line interfaces. + /// + /// By default, an application is not intended to be built and distributed as a Python package. + /// The `--package` option can be used to create an application that is distributable, e.g., if + /// you want to distribute a command-line interface via PyPI. + #[arg(long, alias = "application", conflicts_with = "lib")] + pub r#app: bool, + + /// Create a project for a library. + /// + /// A library is a project that is intended to be built and distributed as a Python package. + #[arg(long, alias = "library", conflicts_with = "app")] + pub r#lib: bool, + /// Do not create a `README.md` file. #[arg(long)] pub no_readme: bool, diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 181f5ebd3..3da2330ab 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -19,7 +19,7 @@ pub(crate) use pip::sync::pip_sync; pub(crate) use pip::tree::pip_tree; pub(crate) use pip::uninstall::pip_uninstall; pub(crate) use project::add::add; -pub(crate) use project::init::init; +pub(crate) use project::init::{init, InitProjectKind}; pub(crate) use project::lock::lock; pub(crate) use project::remove::remove; pub(crate) use project::run::{run, RunCommand}; diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 914abc1ac..4acd6380b 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -1,7 +1,7 @@ use std::fmt::Write; use std::path::Path; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use owo_colors::OwoColorize; use pep440_rs::Version; use pep508_rs::PackageName; @@ -27,7 +27,8 @@ use crate::printer::Printer; pub(crate) async fn init( explicit_path: Option, name: Option, - r#virtual: bool, + package: bool, + project_kind: InitProjectKind, no_readme: bool, python: Option, no_workspace: bool, @@ -72,7 +73,8 @@ pub(crate) async fn init( init_project( &path, &name, - r#virtual, + package, + project_kind, no_readme, python, no_workspace, @@ -93,16 +95,10 @@ pub(crate) async fn init( } } - 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() - )?; + writeln!(printer.stderr(), "Initialized project `{}`", name.cyan())?; } // Initialized a project in the given directory. Some(path) => { @@ -112,8 +108,7 @@ pub(crate) async fn init( writeln!( printer.stderr(), - "Initialized {} `{}` at `{}`", - project, + "Initialized project `{}` at `{}`", name.cyan(), path.display().cyan() )?; @@ -128,7 +123,8 @@ pub(crate) async fn init( async fn init_project( path: &Path, name: &PackageName, - r#virtual: bool, + package: bool, + project_kind: InitProjectKind, no_readme: bool, python: Option, no_workspace: bool, @@ -245,57 +241,7 @@ async fn init_project( RequiresPython::greater_than_equal_version(&interpreter.python_minor_version()) }; - if r#virtual { - // Create the `pyproject.toml`, but omit `[build-system]`. - let pyproject = indoc::formatdoc! {r#" - [project] - name = "{name}" - version = "0.1.0" - description = "Add your description here"{readme} - requires-python = "{requires_python}" - 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)?; - } else { - // 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 = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#, - 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 doesn't exist already. - let src_dir = path.join("src").join(&*name.as_dist_info_name()); - let init_py = src_dir.join("__init__.py"); - if !init_py.try_exists()? { - fs_err::create_dir_all(&src_dir)?; - fs_err::write( - init_py, - indoc::formatdoc! {r#" - def hello() -> str: - return "Hello from {name}!" - "#}, - )?; - } - } + project_kind.init(name, path, &requires_python, no_readme, package)?; if let Some(workspace) = workspace { if workspace.excludes(path)? { @@ -339,3 +285,171 @@ async fn init_project( Ok(()) } + +#[derive(Debug, Clone, Default)] +pub(crate) enum InitProjectKind { + #[default] + Application, + Library, +} + +impl InitProjectKind { + /// Initialize this project kind at the target path. + fn init( + &self, + name: &PackageName, + path: &Path, + requires_python: &RequiresPython, + no_readme: bool, + package: bool, + ) -> Result<()> { + match self { + InitProjectKind::Application => { + init_application(name, path, requires_python, no_readme, package) + } + InitProjectKind::Library => { + init_library(name, path, requires_python, no_readme, package) + } + } + } + + /// Whether or not this project kind is packaged by default. + pub(crate) fn packaged_by_default(&self) -> bool { + matches!(self, InitProjectKind::Library) + } +} + +fn init_application( + name: &PackageName, + path: &Path, + requires_python: &RequiresPython, + no_readme: bool, + package: bool, +) -> Result<()> { + // Create the `pyproject.toml` + let mut pyproject = pyproject_project(name, requires_python, no_readme); + + // Include additional project configuration for packaged applications + if package { + // Since it'll be packaged, we can add a `[project.scripts]` entry + pyproject.push('\n'); + pyproject.push_str(&pyproject_project_scripts(name, "hello", "hello")); + + // Add a build system + pyproject.push('\n'); + pyproject.push_str(pyproject_build_system()); + } + + fs_err::create_dir_all(path)?; + + // Create the source structure. + if package { + // Create `src/{name}/__init__.py`, if it doesn't exist already. + let src_dir = path.join("src").join(&*name.as_dist_info_name()); + let init_py = src_dir.join("__init__.py"); + if !init_py.try_exists()? { + fs_err::create_dir_all(&src_dir)?; + fs_err::write( + init_py, + indoc::formatdoc! {r#" + def hello(): + print("Hello from {name}!") + "#}, + )?; + } + } else { + // Create `hello.py` if it doesn't exist + // TODO(zanieb): Only create `hello.py` if there are no other Python files? + let hello_py = path.join("hello.py"); + if !hello_py.try_exists()? { + fs_err::write( + path.join("hello.py"), + indoc::formatdoc! {r#" + def main(): + print("Hello from {name}!") + + if __name__ == "__main__": + main() + "#}, + )?; + } + } + fs_err::write(path.join("pyproject.toml"), pyproject)?; + + Ok(()) +} + +fn init_library( + name: &PackageName, + path: &Path, + requires_python: &RequiresPython, + no_readme: bool, + package: bool, +) -> Result<()> { + if !package { + return Err(anyhow!("Library projects must be packaged")); + } + + // Create the `pyproject.toml` + let mut pyproject = pyproject_project(name, requires_python, no_readme); + + // Always include a build system if the project is packaged. + pyproject.push('\n'); + pyproject.push_str(pyproject_build_system()); + + fs_err::create_dir_all(path)?; + fs_err::write(path.join("pyproject.toml"), pyproject)?; + + // Create `src/{name}/__init__.py`, if it doesn't exist already. + let src_dir = path.join("src").join(&*name.as_dist_info_name()); + let init_py = src_dir.join("__init__.py"); + if !init_py.try_exists()? { + fs_err::create_dir_all(&src_dir)?; + fs_err::write( + init_py, + indoc::formatdoc! {r#" + def hello() -> str: + return "Hello from {name}!" + "#}, + )?; + } + + Ok(()) +} + +/// Generate the `[project]` section of a `pyproject.toml`. +fn pyproject_project( + name: &PackageName, + requires_python: &RequiresPython, + no_readme: bool, +) -> String { + indoc::formatdoc! {r#" + [project] + name = "{name}" + version = "0.1.0" + description = "Add your description here"{readme} + requires-python = "{requires_python}" + dependencies = [] + "#, + readme = if no_readme { "" } else { "\nreadme = \"README.md\"" }, + requires_python = requires_python.specifiers(), + } +} + +/// Generate the `[build-system]` section of a `pyproject.toml`. +fn pyproject_build_system() -> &'static str { + indoc::indoc! {r#" + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#} +} + +/// Generate the `[project.scripts]` section of a `pyproject.toml`. +fn pyproject_project_scripts(package: &PackageName, executable_name: &str, target: &str) -> String { + let module_name = package.as_dist_info_name(); + indoc::formatdoc! {r#" + [project.scripts] + {executable_name} = "{module_name}:{target}" + "#} +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 991fdd9fe..99250e561 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1025,7 +1025,8 @@ async fn run_project( commands::init( args.path, args.name, - args.r#virtual, + args.package, + args.kind, args.no_readme, args.python, args.no_workspace, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index dc4c7b904..8e981ce5e 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -35,8 +35,8 @@ use uv_settings::{ use uv_warnings::warn_user_once; use uv_workspace::pyproject::DependencyType; -use crate::commands::pip::operations::Modifications; use crate::commands::ToolRunCommand; +use crate::commands::{pip::operations::Modifications, InitProjectKind}; /// The resolved global settings to use for any invocation of the CLI. #[allow(clippy::struct_excessive_bools)] @@ -154,7 +154,8 @@ impl CacheSettings { pub(crate) struct InitSettings { pub(crate) path: Option, pub(crate) name: Option, - pub(crate) r#virtual: bool, + pub(crate) package: bool, + pub(crate) kind: InitProjectKind, pub(crate) no_readme: bool, pub(crate) no_workspace: bool, pub(crate) python: Option, @@ -168,15 +169,29 @@ impl InitSettings { path, name, r#virtual, + package, + no_package, + app, + lib, no_readme, no_workspace, python, } = args; + let kind = match (app, lib) { + (true, false) => InitProjectKind::Application, + (false, true) => InitProjectKind::Library, + (false, false) => InitProjectKind::default(), + (true, true) => unreachable!("`app` and `lib` are mutually exclusive"), + }; + + let package = flag(package || r#virtual, no_package).unwrap_or(kind.packaged_by_default()); + Self { path, name, - r#virtual, + package, + kind, no_readme, no_workspace, python, diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/init.rs index bb5cb58aa..aef72a66c 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/init.rs @@ -9,6 +9,7 @@ use common::{uv_snapshot, TestContext}; mod common; +/// See [`init_application`] and [`init_library`] for more coverage. #[test] fn init() -> Result<()> { let context = TestContext::new("3.12"); @@ -23,9 +24,308 @@ fn init() -> Result<()> { "###); let pyproject = fs_err::read_to_string(context.temp_dir.join("foo/pyproject.toml"))?; - let init_py = fs_err::read_to_string(context.temp_dir.join("foo/src/foo/__init__.py"))?; let _ = fs_err::read_to_string(context.temp_dir.join("foo/README.md")).unwrap(); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + + // Run `uv lock` in the new project. + uv_snapshot!(context.filters(), context.lock().current_dir(context.temp_dir.join("foo")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 1 package in [TIME] + "###); + + Ok(()) +} + +/// Run `uv init --app` to create an application project +#[test] +fn init_application() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let hello_py = child.join("hello.py"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + + let hello = fs_err::read_to_string(hello_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + hello, @r###" + def main(): + print("Hello from foo!") + + if __name__ == "__main__": + main() + "### + ); + }); + + uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("hello.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello from foo! + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: .venv + Resolved 1 package in [TIME] + Audited in [TIME] + "###); + + Ok(()) +} + +/// When `hello.py` already exists, we don't create it again +#[test] +fn init_application_hello_exists() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let hello_py = child.child("hello.py"); + hello_py.touch()?; + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + + let hello = fs_err::read_to_string(hello_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + hello, @"" + ); + }); + + Ok(()) +} + +/// When other Python files already exists, we still create `hello.py` +#[test] +fn init_application_other_python_exists() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let hello_py = child.join("hello.py"); + let other_py = child.child("foo.py"); + other_py.touch()?; + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + + let hello = fs_err::read_to_string(hello_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + hello, @r###" + def main(): + print("Hello from foo!") + + if __name__ == "__main__": + main() + "### + ); + }); + + Ok(()) +} + +/// Run `uv init --app --package` to create a packaged application project +#[test] +fn init_application_package() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app").arg("--package"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + hello = "foo:hello" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + def hello(): + print("Hello from foo!") + "### + ); + }); + + uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("hello"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello from foo! + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: .venv + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo) + "###); + + Ok(()) +} + +/// Run `uv init --lib` to create an library project +#[test] +fn init_library() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--lib"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; insta::with_settings!({ filters => context.filters(), }, { @@ -46,26 +346,55 @@ fn init() -> Result<()> { ); }); + let init = fs_err::read_to_string(init_py)?; insta::with_settings!({ filters => context.filters(), }, { assert_snapshot!( - init_py, @r###" + init, @r###" def hello() -> str: return "Hello from foo!" "### ); }); - // Run `uv lock` in the new project. - uv_snapshot!(context.filters(), context.lock().current_dir(context.temp_dir.join("foo")), @r###" + uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("python").arg("-c").arg("import foo; print(foo.hello())"), @r###" success: true exit_code: 0 ----- stdout ----- + Hello from foo! ----- stderr ----- Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: .venv Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo) + "###); + + Ok(()) +} + +/// Using `uv init --lib --no-package` isn't allowed +#[test] +fn init_library_no_package() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--lib").arg("--no-package"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--lib' cannot be used with '--no-package' + + Usage: uv init --cache-dir [CACHE_DIR] --lib [PATH] + + For more information, try '--help'. "###); Ok(()) @@ -117,10 +446,6 @@ fn init_no_readme() -> Result<()> { description = "Add your description here" requires-python = ">=3.12" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); @@ -129,13 +454,13 @@ fn init_no_readme() -> Result<()> { } #[test] -fn init_current_dir() -> Result<()> { +fn init_library_current_dir() -> Result<()> { let context = TestContext::new("3.12"); let dir = context.temp_dir.join("foo"); fs_err::create_dir(&dir)?; - uv_snapshot!(context.filters(), context.init().current_dir(&dir), @r###" + uv_snapshot!(context.filters(), context.init().arg("--lib").current_dir(&dir), @r###" success: true exit_code: 0 ----- stdout ----- @@ -193,6 +518,69 @@ fn init_current_dir() -> Result<()> { Ok(()) } +#[test] +fn init_application_current_dir() -> Result<()> { + let context = TestContext::new("3.12"); + + let dir = context.temp_dir.join("foo"); + fs_err::create_dir(&dir)?; + + uv_snapshot!(context.filters(), context.init().arg("--app").current_dir(&dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(dir.join("pyproject.toml"))?; + let hello_py = fs_err::read_to_string(dir.join("hello.py"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + hello_py, @r###" + def main(): + print("Hello from foo!") + + if __name__ == "__main__": + main() + "### + ); + }); + + // Run `uv lock` in the new project. + uv_snapshot!(context.filters(), context.lock().current_dir(&dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 1 package in [TIME] + "###); + + Ok(()) +} + #[test] fn init_dot_args() -> Result<()> { let context = TestContext::new("3.12"); @@ -200,7 +588,7 @@ fn init_dot_args() -> Result<()> { let dir = context.temp_dir.join("foo"); fs_err::create_dir(&dir)?; - uv_snapshot!(context.filters(), context.init().current_dir(&dir).arg("."), @r###" + uv_snapshot!(context.filters(), context.init().current_dir(&dir).arg(".").arg("--lib"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -276,7 +664,7 @@ fn init_workspace() -> Result<()> { let child = context.temp_dir.join("foo"); fs_err::create_dir(&child)?; - uv_snapshot!(context.filters(), context.init().current_dir(&child), @r###" + uv_snapshot!(context.filters(), context.init().arg("--lib").current_dir(&child), @r###" success: true exit_code: 0 ----- stdout ----- @@ -370,7 +758,7 @@ fn init_workspace_relative_sub_package() -> Result<()> { let child = context.temp_dir.join("foo"); - uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo"), @r###" + uv_snapshot!(context.filters(), context.init().arg("--lib").current_dir(&context.temp_dir).arg("foo"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -465,7 +853,7 @@ fn init_workspace_outside() -> Result<()> { let child = context.temp_dir.join("foo"); // Run `uv init ` outside the workspace. - uv_snapshot!(context.filters(), context.init().current_dir(&context.home_dir).arg(&child), @r###" + uv_snapshot!(context.filters(), context.init().arg("--lib").current_dir(&context.home_dir).arg(&child), @r###" success: true exit_code: 0 ----- stdout ----- @@ -543,11 +931,11 @@ fn init_workspace_outside() -> Result<()> { } #[test] -fn init_invalid_names() -> Result<()> { +fn init_normalized_names() -> Result<()> { let context = TestContext::new("3.12"); - // `foo-bar` normalized to `foo_bar`. - uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo-bar"), @r###" + // `foo-bar` module is normalized to `foo_bar`. + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo-bar").arg("--lib"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -580,6 +968,39 @@ fn init_invalid_names() -> Result<()> { ); }); + // `foo-bar` module is normalized to `foo_bar`. + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo-bar").arg("--app"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Project is already initialized in `[TEMP_DIR]/foo-bar` + "###); + + let child = context.temp_dir.child("foo-bar"); + let pyproject = fs_err::read_to_string(child.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo-bar" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "### + ); + }); + // "bar baz" is not allowed. uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("bar baz"), @r###" success: false @@ -744,10 +1165,6 @@ fn init_no_workspace_warning() -> Result<()> { readme = "README.md" requires-python = ">=3.12" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); @@ -822,10 +1239,6 @@ fn init_project_inside_project() -> Result<()> { readme = "README.md" requires-python = ">=3.12" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); @@ -898,7 +1311,7 @@ fn init_virtual_project() -> Result<()> { ----- stdout ----- ----- stderr ----- - Initialized workspace `foo` + Initialized project `foo` "###); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -914,6 +1327,13 @@ fn init_virtual_project() -> Result<()> { readme = "README.md" requires-python = ">=3.12" dependencies = [] + + [project.scripts] + hello = "foo:hello" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" "### ); }); @@ -942,6 +1362,13 @@ fn init_virtual_project() -> Result<()> { requires-python = ">=3.12" dependencies = [] + [project.scripts] + hello = "foo:hello" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + [tool.uv.workspace] members = ["bar"] "### @@ -1013,7 +1440,7 @@ fn init_nested_virtual_workspace() -> Result<()> { ----- stderr ----- Adding `foo` as member of workspace `[TEMP_DIR]/` - Initialized workspace `foo` at `[TEMP_DIR]/foo` + Initialized project `foo` at `[TEMP_DIR]/foo` "###); let pyproject = fs_err::read_to_string(context.temp_dir.join("foo").join("pyproject.toml"))?; @@ -1029,6 +1456,13 @@ fn init_nested_virtual_workspace() -> Result<()> { readme = "README.md" requires-python = ">=3.12" dependencies = [] + + [project.scripts] + hello = "foo:hello" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" "### ); }); @@ -1177,10 +1611,6 @@ fn init_requires_python_workspace() -> Result<()> { readme = "README.md" requires-python = ">=3.10" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); @@ -1230,10 +1660,6 @@ fn init_requires_python_version() -> Result<()> { readme = "README.md" requires-python = ">=3.8" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); @@ -1284,10 +1710,6 @@ fn init_requires_python_specifiers() -> Result<()> { readme = "README.md" requires-python = "==3.8.*" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index f05390cbe..8465bbf9d 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -372,7 +372,15 @@ uv init [OPTIONS] [PATH]

Options

-
--cache-dir cache-dir

Path to the cache directory.

+
--app

Create a project for an application.

+ +

This is the default behavior if --lib is not requested.

+ +

This project kind is for web servers, scripts, and command-line interfaces.

+ +

By default, an application is not intended to be built and distributed as a Python package. The --package option can be used to create an application that is distributable, e.g., if you want to distribute a command-line interface via PyPI.

+ +
--cache-dir cache-dir

Path to the cache directory.

Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

@@ -394,6 +402,10 @@ uv init [OPTIONS] [PATH]
--help, -h

Display the concise help for this command

+
--lib

Create a project for a library.

+ +

A library is a project that is intended to be built and distributed as a Python package.

+
--name name

The name of the project.

Defaults to the name of the directory.

@@ -410,6 +422,12 @@ uv init [OPTIONS] [PATH]

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

+
--no-package

Do not set up the project to be built as a Python package.

+ +

Does not include a [build-system] for the project.

+ +

This is the default behavior when using --app.

+
--no-progress

Hide all progress outputs.

For example, spinners or progress bars.

@@ -428,6 +446,14 @@ uv init [OPTIONS] [PATH]

When disabled, uv will only use locally cached data and locally available files.

+
--package

Set up the project to be built as a Python package.

+ +

Defines a [build-system] for the project.

+ +

This is the default behavior when using --lib.

+ +

When using --app, this will include a [project.scripts] entrypoint and use a src/ project structure.

+
--python, -p python

The Python interpreter to use to determine the minimum supported Python version.

See uv python to view supported request formats.

@@ -455,12 +481,6 @@ uv init [OPTIONS] [PATH]
--version, -V

Display the uv version

-
--virtual

Create a virtual project, rather than a package.

- -

A virtual project is a project that is not intended to be built as a Python package, such as a project that only contains scripts or other application code.

- -

Virtual projects themselves are not installed into the Python environment.

-
## uv add