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
This commit is contained in:
Zanie Blue 2025-02-05 10:12:27 -06:00 committed by GitHub
parent 989b103171
commit acbbb2b82a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 311 additions and 13 deletions

View File

@ -2518,6 +2518,13 @@ pub struct InitArgs {
#[arg(long, conflicts_with = "script")] #[arg(long, conflicts_with = "script")]
pub name: Option<PackageName>, pub name: Option<PackageName>,
/// 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. /// Create a virtual project, rather than a package.
/// ///
/// This option is deprecated and will be removed in a future release. /// This option is deprecated and will be removed in a future release.
@ -2574,9 +2581,13 @@ pub struct InitArgs {
pub r#script: bool, pub r#script: bool,
/// Set the project description. /// Set the project description.
#[arg(long, conflicts_with = "script")] #[arg(long, conflicts_with = "script", overrides_with = "no_description")]
pub description: Option<String>, pub description: Option<String>,
/// 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. /// Initialize a version control system for the project.
/// ///
/// By default, uv will initialize a Git repository (`git`). Use `--vcs none` to explicitly /// By default, uv will initialize a Git repository (`git`). Use `--vcs none` to explicitly

View File

@ -41,7 +41,9 @@ pub(crate) async fn init(
name: Option<PackageName>, name: Option<PackageName>,
package: bool, package: bool,
init_kind: InitKind, init_kind: InitKind,
bare: bool,
description: Option<String>, description: Option<String>,
no_description: bool,
vcs: Option<VersionControlSystem>, vcs: Option<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>, build_backend: Option<ProjectBuildBackend>,
no_readme: bool, no_readme: bool,
@ -133,7 +135,9 @@ pub(crate) async fn init(
&name, &name,
package, package,
project_kind, project_kind,
bare,
description, description,
no_description,
vcs, vcs,
build_backend, build_backend,
no_readme, no_readme,
@ -275,7 +279,9 @@ async fn init_project(
name: &PackageName, name: &PackageName,
package: bool, package: bool,
project_kind: InitProjectKind, project_kind: InitProjectKind,
bare: bool,
description: Option<String>, description: Option<String>,
no_description: bool,
vcs: Option<VersionControlSystem>, vcs: Option<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>, build_backend: Option<ProjectBuildBackend>,
no_readme: bool, no_readme: bool,
@ -576,6 +582,8 @@ async fn init_project(
path, path,
&requires_python, &requires_python,
description.as_deref(), description.as_deref(),
no_description,
bare,
vcs, vcs,
build_backend, build_backend,
author_from, author_from,
@ -694,12 +702,15 @@ impl InitKind {
impl InitProjectKind { impl InitProjectKind {
/// Initialize this project kind at the target path. /// Initialize this project kind at the target path.
#[allow(clippy::fn_params_excessive_bools)]
fn init( fn init(
self, self,
name: &PackageName, name: &PackageName,
path: &Path, path: &Path,
requires_python: &RequiresPython, requires_python: &RequiresPython,
description: Option<&str>, description: Option<&str>,
no_description: bool,
bare: bool,
vcs: Option<VersionControlSystem>, vcs: Option<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>, build_backend: Option<ProjectBuildBackend>,
author_from: Option<AuthorFrom>, author_from: Option<AuthorFrom>,
@ -712,6 +723,8 @@ impl InitProjectKind {
path, path,
requires_python, requires_python,
description, description,
no_description,
bare,
vcs, vcs,
build_backend, build_backend,
author_from, author_from,
@ -723,6 +736,8 @@ impl InitProjectKind {
path, path,
requires_python, requires_python,
description, description,
no_description,
bare,
vcs, vcs,
build_backend, build_backend,
author_from, author_from,
@ -733,11 +748,14 @@ impl InitProjectKind {
} }
/// Initialize a Python application at the target path. /// Initialize a Python application at the target path.
#[allow(clippy::fn_params_excessive_bools)]
fn init_application( fn init_application(
name: &PackageName, name: &PackageName,
path: &Path, path: &Path,
requires_python: &RequiresPython, requires_python: &RequiresPython,
description: Option<&str>, description: Option<&str>,
no_description: bool,
bare: bool,
vcs: Option<VersionControlSystem>, vcs: Option<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>, build_backend: Option<ProjectBuildBackend>,
author_from: Option<AuthorFrom>, author_from: Option<AuthorFrom>,
@ -762,14 +780,17 @@ impl InitProjectKind {
requires_python, requires_python,
author.as_ref(), author.as_ref(),
description, description,
no_description,
no_readme, no_readme,
); );
// Include additional project configuration for packaged applications // Include additional project configuration for packaged applications
if package { if package {
// Since it'll be packaged, we can add a `[project.scripts]` entry // Since it'll be packaged, we can add a `[project.scripts]` entry
if !bare {
pyproject.push('\n'); pyproject.push('\n');
pyproject.push_str(&pyproject_project_scripts(name, name.as_str(), "main")); pyproject.push_str(&pyproject_project_scripts(name, name.as_str(), "main"));
}
// Add a build system // Add a build system
let build_backend = build_backend.unwrap_or_default(); let build_backend = build_backend.unwrap_or_default();
@ -777,13 +798,15 @@ impl InitProjectKind {
pyproject.push_str(&pyproject_build_system(name, build_backend)); pyproject.push_str(&pyproject_build_system(name, build_backend));
pyproject_build_backend_prerequisites(name, path, build_backend)?; pyproject_build_backend_prerequisites(name, path, build_backend)?;
if !bare {
// Generate `src` files // Generate `src` files
generate_package_scripts(name, path, build_backend, false)?; generate_package_scripts(name, path, build_backend, false)?;
}
} else { } else {
// Create `hello.py` if it doesn't exist // Create `hello.py` if it doesn't exist
// TODO(zanieb): Only create `hello.py` if there are no other Python files? // TODO(zanieb): Only create `hello.py` if there are no other Python files?
let hello_py = path.join("hello.py"); let hello_py = path.join("hello.py");
if !hello_py.try_exists()? { if !hello_py.try_exists()? && !bare {
fs_err::write( fs_err::write(
path.join("hello.py"), path.join("hello.py"),
indoc::formatdoc! {r#" indoc::formatdoc! {r#"
@ -806,11 +829,14 @@ impl InitProjectKind {
} }
/// Initialize a library project at the target path. /// Initialize a library project at the target path.
#[allow(clippy::fn_params_excessive_bools)]
fn init_library( fn init_library(
name: &PackageName, name: &PackageName,
path: &Path, path: &Path,
requires_python: &RequiresPython, requires_python: &RequiresPython,
description: Option<&str>, description: Option<&str>,
no_description: bool,
bare: bool,
vcs: Option<VersionControlSystem>, vcs: Option<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>, build_backend: Option<ProjectBuildBackend>,
author_from: Option<AuthorFrom>, author_from: Option<AuthorFrom>,
@ -831,6 +857,7 @@ impl InitProjectKind {
requires_python, requires_python,
author.as_ref(), author.as_ref(),
description, description,
no_description,
no_readme, no_readme,
); );
@ -843,7 +870,9 @@ impl InitProjectKind {
fs_err::write(path.join("pyproject.toml"), pyproject)?; fs_err::write(path.join("pyproject.toml"), pyproject)?;
// Generate `src` files // Generate `src` files
if !bare {
generate_package_scripts(name, path, build_backend, true)?; generate_package_scripts(name, path, build_backend, true)?;
};
// Initialize the version control system. // Initialize the version control system.
init_vcs(path, vcs)?; init_vcs(path, vcs)?;
@ -877,20 +906,24 @@ fn pyproject_project(
requires_python: &RequiresPython, requires_python: &RequiresPython,
author: Option<&Author>, author: Option<&Author>,
description: Option<&str>, description: Option<&str>,
no_description: bool,
no_readme: bool, no_readme: bool,
) -> String { ) -> String {
indoc::formatdoc! {r#" indoc::formatdoc! {r#"
[project] [project]
name = "{name}" name = "{name}"
version = "0.1.0" version = "0.1.0"{description}{readme}{authors}
description = "{description}"{readme}{authors}
requires-python = "{requires_python}" requires-python = "{requires_python}"
dependencies = [] dependencies = []
"#, "#,
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" }, 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())), authors = author.map_or_else(String::new, |author| format!("\nauthors = [\n {}\n]", author.to_toml_string())),
requires_python = requires_python.specifiers(), requires_python = requires_python.specifiers(),
description = description.unwrap_or("Add your description here"),
} }
} }

View File

@ -1427,7 +1427,9 @@ async fn run_project(
args.name, args.name,
args.package, args.package,
args.kind, args.kind,
args.bare,
args.description, args.description,
args.no_description,
args.vcs, args.vcs,
args.build_backend, args.build_backend,
args.no_readme, args.no_readme,

View File

@ -195,7 +195,9 @@ pub(crate) struct InitSettings {
pub(crate) name: Option<PackageName>, pub(crate) name: Option<PackageName>,
pub(crate) package: bool, pub(crate) package: bool,
pub(crate) kind: InitKind, pub(crate) kind: InitKind,
pub(crate) bare: bool,
pub(crate) description: Option<String>, pub(crate) description: Option<String>,
pub(crate) no_description: bool,
pub(crate) vcs: Option<VersionControlSystem>, pub(crate) vcs: Option<VersionControlSystem>,
pub(crate) build_backend: Option<ProjectBuildBackend>, pub(crate) build_backend: Option<ProjectBuildBackend>,
pub(crate) no_readme: bool, pub(crate) no_readme: bool,
@ -216,10 +218,12 @@ impl InitSettings {
r#virtual, r#virtual,
package, package,
no_package, no_package,
bare,
app, app,
lib, lib,
script, script,
description, description,
no_description,
vcs, vcs,
build_backend, build_backend,
no_readme, no_readme,
@ -245,17 +249,21 @@ impl InitSettings {
.map(|fs| fs.install_mirrors.clone()) .map(|fs| fs.install_mirrors.clone())
.unwrap_or_default(); .unwrap_or_default();
let no_description = no_description || (bare && description.is_none());
Self { Self {
path, path,
name, name,
package, package,
kind, kind,
bare,
description, description,
vcs, no_description,
vcs: vcs.or(bare.then_some(VersionControlSystem::None)),
build_backend, build_backend,
no_readme, no_readme: no_readme || bare,
author_from, author_from,
no_pin_python, no_pin_python: no_pin_python || bare,
no_workspace, no_workspace,
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
install_mirrors, install_mirrors,

View File

@ -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 /// Run `uv init --app` to create an application project
#[test] #[test]
fn init_application() -> Result<()> { fn init_application() -> Result<()> {
@ -399,6 +446,161 @@ fn init_library() -> Result<()> {
Ok(()) 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 // General init --script correctness test
#[test] #[test]
fn init_script() -> Result<()> { fn init_script() -> Result<()> {

View File

@ -295,3 +295,39 @@ Hello from example-ext!
Changes to the extension code in `lib.rs` or `main.cpp` will require running `--reinstall` to Changes to the extension code in `lib.rs` or `main.cpp` will require running `--reinstall` to
rebuild them. 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
```

View File

@ -555,6 +555,10 @@ uv init [OPTIONS] [PATH]
<li><code>none</code>: Do not infer the author information</li> <li><code>none</code>: Do not infer the author information</li>
</ul> </ul>
</dd><dt><code>--bare</code></dt><dd><p>Only create a <code>pyproject.toml</code>.</p>
<p>Disables creating extra files like <code>README.md</code>, the <code>src/</code> tree, <code>.python-version</code> files, etc.</p>
</dd><dt><code>--build-backend</code> <i>build-backend</i></dt><dd><p>Initialize a build-backend of choice for the project.</p> </dd><dt><code>--build-backend</code> <i>build-backend</i></dt><dd><p>Initialize a build-backend of choice for the project.</p>
<p>Implicitly sets <code>--package</code>.</p> <p>Implicitly sets <code>--package</code>.</p>
@ -632,6 +636,8 @@ uv init [OPTIONS] [PATH]
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p> <p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p> <p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p>
</dd><dt><code>--no-description</code></dt><dd><p>Disable the description for the project</p>
</dd><dt><code>--no-package</code></dt><dd><p>Do not set up the project to be built as a Python package.</p> </dd><dt><code>--no-package</code></dt><dd><p>Do not set up the project to be built as a Python package.</p>
<p>Does not include a <code>[build-system]</code> for the project.</p> <p>Does not include a <code>[build-system]</code> for the project.</p>