From acbbb2b82ad7a50a1bd00a29a09bb25cf237d4b4 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 5 Feb 2025 10:12:27 -0600 Subject: [PATCH] Add `--bare` option to `uv init` (#11192) People are looking for a less opinionated version of `uv init`. The goal here is to create a `pyproject.toml` and nothing else. With the `--lib` or `--package` flags, we'll still configure a build backend but we won't create the source tree. This disables things like the default `description`, author behavior, and VCS. See - https://github.com/astral-sh/uv/issues/8178 - https://github.com/astral-sh/uv/issues/7181 - https://github.com/astral-sh/uv/issues/6750 --- crates/uv-cli/src/lib.rs | 13 +- crates/uv/src/commands/project/init.rs | 51 +++++-- crates/uv/src/lib.rs | 2 + crates/uv/src/settings.rs | 14 +- crates/uv/tests/it/init.rs | 202 +++++++++++++++++++++++++ docs/concepts/projects/init.md | 36 +++++ docs/reference/cli.md | 6 + 7 files changed, 311 insertions(+), 13 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ddbcd4b97..a521026ca 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2518,6 +2518,13 @@ pub struct InitArgs { #[arg(long, conflicts_with = "script")] pub name: Option, + /// Only create a `pyproject.toml`. + /// + /// Disables creating extra files like `README.md`, the `src/` tree, `.python-version` files, + /// etc. + #[arg(long, conflicts_with = "script")] + pub bare: bool, + /// Create a virtual project, rather than a package. /// /// This option is deprecated and will be removed in a future release. @@ -2574,9 +2581,13 @@ pub struct InitArgs { pub r#script: bool, /// Set the project description. - #[arg(long, conflicts_with = "script")] + #[arg(long, conflicts_with = "script", overrides_with = "no_description")] pub description: Option, + /// Disable the description for the project. + #[arg(long, conflicts_with = "script", overrides_with = "description")] + pub no_description: bool, + /// Initialize a version control system for the project. /// /// By default, uv will initialize a Git repository (`git`). Use `--vcs none` to explicitly diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index e95568830..914caaa0e 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -41,7 +41,9 @@ pub(crate) async fn init( name: Option, package: bool, init_kind: InitKind, + bare: bool, description: Option, + no_description: bool, vcs: Option, build_backend: Option, no_readme: bool, @@ -133,7 +135,9 @@ pub(crate) async fn init( &name, package, project_kind, + bare, description, + no_description, vcs, build_backend, no_readme, @@ -275,7 +279,9 @@ async fn init_project( name: &PackageName, package: bool, project_kind: InitProjectKind, + bare: bool, description: Option, + no_description: bool, vcs: Option, build_backend: Option, no_readme: bool, @@ -576,6 +582,8 @@ async fn init_project( path, &requires_python, description.as_deref(), + no_description, + bare, vcs, build_backend, author_from, @@ -694,12 +702,15 @@ impl InitKind { impl InitProjectKind { /// Initialize this project kind at the target path. + #[allow(clippy::fn_params_excessive_bools)] fn init( self, name: &PackageName, path: &Path, requires_python: &RequiresPython, description: Option<&str>, + no_description: bool, + bare: bool, vcs: Option, build_backend: Option, author_from: Option, @@ -712,6 +723,8 @@ impl InitProjectKind { path, requires_python, description, + no_description, + bare, vcs, build_backend, author_from, @@ -723,6 +736,8 @@ impl InitProjectKind { path, requires_python, description, + no_description, + bare, vcs, build_backend, author_from, @@ -733,11 +748,14 @@ impl InitProjectKind { } /// Initialize a Python application at the target path. + #[allow(clippy::fn_params_excessive_bools)] fn init_application( name: &PackageName, path: &Path, requires_python: &RequiresPython, description: Option<&str>, + no_description: bool, + bare: bool, vcs: Option, build_backend: Option, author_from: Option, @@ -762,14 +780,17 @@ impl InitProjectKind { requires_python, author.as_ref(), description, + no_description, 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, name.as_str(), "main")); + if !bare { + pyproject.push('\n'); + pyproject.push_str(&pyproject_project_scripts(name, name.as_str(), "main")); + } // Add a build system let build_backend = build_backend.unwrap_or_default(); @@ -777,13 +798,15 @@ impl InitProjectKind { pyproject.push_str(&pyproject_build_system(name, build_backend)); pyproject_build_backend_prerequisites(name, path, build_backend)?; - // Generate `src` files - generate_package_scripts(name, path, build_backend, false)?; + if !bare { + // Generate `src` files + generate_package_scripts(name, path, build_backend, false)?; + } } 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()? { + if !hello_py.try_exists()? && !bare { fs_err::write( path.join("hello.py"), indoc::formatdoc! {r#" @@ -806,11 +829,14 @@ impl InitProjectKind { } /// Initialize a library project at the target path. + #[allow(clippy::fn_params_excessive_bools)] fn init_library( name: &PackageName, path: &Path, requires_python: &RequiresPython, description: Option<&str>, + no_description: bool, + bare: bool, vcs: Option, build_backend: Option, author_from: Option, @@ -831,6 +857,7 @@ impl InitProjectKind { requires_python, author.as_ref(), description, + no_description, no_readme, ); @@ -843,7 +870,9 @@ impl InitProjectKind { fs_err::write(path.join("pyproject.toml"), pyproject)?; // Generate `src` files - generate_package_scripts(name, path, build_backend, true)?; + if !bare { + generate_package_scripts(name, path, build_backend, true)?; + }; // Initialize the version control system. init_vcs(path, vcs)?; @@ -877,20 +906,24 @@ fn pyproject_project( requires_python: &RequiresPython, author: Option<&Author>, description: Option<&str>, + no_description: bool, no_readme: bool, ) -> String { indoc::formatdoc! {r#" [project] name = "{name}" - version = "0.1.0" - description = "{description}"{readme}{authors} + version = "0.1.0"{description}{readme}{authors} requires-python = "{requires_python}" dependencies = [] "#, readme = if no_readme { "" } else { "\nreadme = \"README.md\"" }, + description = if no_description { + String::new() + } else { + format!("\ndescription = \"{description}\"", description = description.unwrap_or("Add your description here")) + }, authors = author.map_or_else(String::new, |author| format!("\nauthors = [\n {}\n]", author.to_toml_string())), requires_python = requires_python.specifiers(), - description = description.unwrap_or("Add your description here"), } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 3bb5a4b2b..457f68368 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1427,7 +1427,9 @@ async fn run_project( args.name, args.package, args.kind, + args.bare, args.description, + args.no_description, args.vcs, args.build_backend, args.no_readme, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index f02345d6c..26394b7ec 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -195,7 +195,9 @@ pub(crate) struct InitSettings { pub(crate) name: Option, pub(crate) package: bool, pub(crate) kind: InitKind, + pub(crate) bare: bool, pub(crate) description: Option, + pub(crate) no_description: bool, pub(crate) vcs: Option, pub(crate) build_backend: Option, pub(crate) no_readme: bool, @@ -216,10 +218,12 @@ impl InitSettings { r#virtual, package, no_package, + bare, app, lib, script, description, + no_description, vcs, build_backend, no_readme, @@ -245,17 +249,21 @@ impl InitSettings { .map(|fs| fs.install_mirrors.clone()) .unwrap_or_default(); + let no_description = no_description || (bare && description.is_none()); + Self { path, name, package, kind, + bare, description, - vcs, + no_description, + vcs: vcs.or(bare.then_some(VersionControlSystem::None)), build_backend, - no_readme, + no_readme: no_readme || bare, author_from, - no_pin_python, + no_pin_python: no_pin_python || bare, no_workspace, python: python.and_then(Maybe::into_option), install_mirrors, diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 3dc8a4f91..179964ad7 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -64,6 +64,53 @@ fn init() { }); } +#[test] +fn init_bare() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.init().arg("foo").arg("--bare"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` at `[TEMP_DIR]/foo` + "###); + + // No extra files should be created + context + .temp_dir + .child("foo/README.md") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/hello.py") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/.python-version") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/.git") + .assert(predicate::path::missing()); + + let pyproject = context.read("foo/pyproject.toml"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); +} + /// Run `uv init --app` to create an application project #[test] fn init_application() -> Result<()> { @@ -399,6 +446,161 @@ fn init_library() -> Result<()> { Ok(()) } +#[test] +fn init_bare_lib() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.init().arg("foo").arg("--bare").arg("--lib"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` at `[TEMP_DIR]/foo` + "###); + + // No extra files should be created + context + .temp_dir + .child("foo/README.md") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/src") + .assert(predicate::path::missing()); + + context + .temp_dir + .child("foo/.git") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/.python-version") + .assert(predicate::path::missing()); + + let pyproject = context.read("foo/pyproject.toml"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "### + ); + }); +} + +#[test] +fn init_bare_package() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.init().arg("foo").arg("--bare").arg("--package"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` at `[TEMP_DIR]/foo` + "###); + + // No extra files should be created + context + .temp_dir + .child("foo/README.md") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/src") + .assert(predicate::path::missing()); + + context + .temp_dir + .child("foo/.git") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/.python-version") + .assert(predicate::path::missing()); + + let pyproject = context.read("foo/pyproject.toml"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "### + ); + }); +} + +#[test] +fn init_bare_opt_in() { + let context = TestContext::new("3.12"); + + // With `--bare`, you can still opt-in to extras + // TODO(zanieb): Add options for `--pin-python` and `--readme` + uv_snapshot!(context.filters(), context.init().arg("foo").arg("--bare") + .arg("--description").arg("foo") + .arg("--vcs").arg("git"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` at `[TEMP_DIR]/foo` + "###); + + context + .temp_dir + .child("foo/README.md") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/src") + .assert(predicate::path::missing()); + context + .temp_dir + .child("foo/.git") + .assert(predicate::path::is_dir()); + context + .temp_dir + .child("foo/.python-version") + .assert(predicate::path::missing()); + + let pyproject = context.read("foo/pyproject.toml"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "foo" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); +} + // General init --script correctness test #[test] fn init_script() -> Result<()> { diff --git a/docs/concepts/projects/init.md b/docs/concepts/projects/init.md index a237af7f5..a768d7ff2 100644 --- a/docs/concepts/projects/init.md +++ b/docs/concepts/projects/init.md @@ -295,3 +295,39 @@ Hello from example-ext! Changes to the extension code in `lib.rs` or `main.cpp` will require running `--reinstall` to rebuild them. + +## Creating a minimal project + +If you only want to create a `pyproject.toml`, use the `--bare` option: + +```console +$ uv init example --bare +``` + +uv will skip creating a Python version pin file, a README, and any source directories or files. +Additionally, uv will not initialize a version control system (i.e., `git`). + +```console +$ tree example-bare +example-bare +└── pyproject.toml +``` + +uv will also not add extra metadata to the `pyproject.toml`, such as the `description` or `authors`. + +```toml +[project] +name = "example" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [] +``` + +The `--bare` option can be used with other options like `--lib` or `--build-backend` — in these +cases uv will still configure a build system but will not create the expected file structure. + +When `--bare` is used, additional features can still be used opt-in: + +```console +$ uv init example --description "Hello world" --author-from git --vcs git +``` diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 357096efe..ef66de830 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -555,6 +555,10 @@ uv init [OPTIONS] [PATH]
  • none: Do not infer the author information
  • +
    --bare

    Only create a pyproject.toml.

    + +

    Disables creating extra files like README.md, the src/ tree, .python-version files, etc.

    +
    --build-backend build-backend

    Initialize a build-backend of choice for the project.

    Implicitly sets --package.

    @@ -632,6 +636,8 @@ uv init [OPTIONS] [PATH]

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

    May also be set with the UV_NO_CONFIG environment variable.

    +
    --no-description

    Disable the description for the project

    +
    --no-package

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

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