Skip to content
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 @@ -3391,6 +3391,8 @@ pub struct InitArgs {
/// Disables creating extra files like `README.md`, the `src/` tree, `.python-version` files,
/// etc.
///
/// A `[build-system]` table is only created with `--package` or `--build-backend`.
///
/// When combined with `--script`, the script will only contain the inline metadata header.
#[arg(long)]
pub bare: bool,
Expand All @@ -3405,7 +3407,9 @@ pub struct InitArgs {
///
/// Defines a `[build-system]` for the project.
///
/// This is the default behavior when using `--lib` or `--build-backend`.
/// This is the default behavior when using `--lib` or `--build-backend`, or when the
/// `packaged-init` preview feature is enabled. It will become the default unconditionally in
/// the future.
///
/// When using `--app`, this will include a `[project.scripts]` entrypoint and use a `src/`
/// project structure.
Expand Down
3 changes: 3 additions & 0 deletions crates/uv-preview/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ pub enum PreviewFeature {
MalwareCheck = 1 << 31,
VenvSafeClear = 1 << 32,
Check = 1 << 33,
PackagedInit = 1 << 34,
}

impl PreviewFeature {
Expand Down Expand Up @@ -296,6 +297,7 @@ impl PreviewFeature {
Self::MalwareCheck => "malware-check",
Self::VenvSafeClear => "venv-safe-clear",
Self::Check => "check-command",
Self::PackagedInit => "packaged-init",
}
}
}
Expand Down Expand Up @@ -349,6 +351,7 @@ impl FromStr for PreviewFeature {
"malware-check" => Self::MalwareCheck,
"venv-safe-clear" => Self::VenvSafeClear,
"check" | "check-command" => Self::Check,
"packaged-init" => Self::PackagedInit,
_ => return Err(PreviewFeatureParseError),
})
}
Expand Down
165 changes: 143 additions & 22 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ async fn init_script(
async fn init_project(
path: &Path,
name: &PackageName,
// TODO(konsti): Remove when stabilizing.
package: bool,
project_kind: InitProjectKind,
bare: bool,
Expand Down Expand Up @@ -725,33 +726,35 @@ pub(crate) enum InitKind {
Script,
}

impl Default for InitKind {
fn default() -> Self {
Self::Project(InitProjectKind::default())
}
}

/// The kind of Python project to initialize (either an application or a library).
#[derive(Debug, Copy, Clone, Default)]
pub(crate) enum InitProjectKind {
/// Initialize a Python application.
/// A python package with a `main` function in a `__init__.py` and a script entrypoint pointing
/// to that.
#[default]
ApplicationWithLibrary,
/// A flat application with a `main.py`.
Application,
/// Initialize a Python library.
/// A python package, no entrypoint.
Library,
}

impl InitKind {
/// Returns `true` if the project should be packaged by default.
pub(crate) fn packaged_by_default(self) -> bool {
matches!(self, Self::Project(InitProjectKind::Library))
}
/// Initialize only a `pyproject.toml`
Bare,
/// Initialize only a `pyproject.toml` with `[build-system]` table (but without associated
/// source files).
BareWithBuildSystem,
// TODO(konsti): Remove when stabilizing.
/// Initialize a Python application.
ApplicationOld,
// TODO(konsti): Remove when stabilizing.
/// Initialize a Python library.
LibraryOld,
Comment on lines +747 to +750

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: don't use Old/New, they quickly become confusing :) I'd go with ApplicationWithPackageInitOff and LibraryWithPackageInitOff

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This should only be temporary, https://github.com/astral-sh/uv/pull/19197/changes removes the nomenclature entirely, the idea is more like <style>ToBeRemoved and <style>ToBecomeTheDefault

}

impl InitProjectKind {
/// Initialize this project kind at the target path.
// TODO(konsti): Remove when stabilizing packaged-init.
#[expect(clippy::fn_params_excessive_bools)]
fn init(
fn init_old(
self,
name: &PackageName,
path: &Path,
Expand All @@ -766,7 +769,7 @@ impl InitProjectKind {
package: bool,
) -> Result<()> {
match self {
Self::Application => Self::init_application(
Self::ApplicationOld => Self::init_application_old(
name,
path,
requires_python,
Expand All @@ -779,7 +782,7 @@ impl InitProjectKind {
no_readme,
package,
),
Self::Library => Self::init_library(
Self::LibraryOld => Self::init_library_old(
name,
path,
requires_python,
Expand All @@ -792,12 +795,14 @@ impl InitProjectKind {
no_readme,
package,
),
_ => unreachable!(),
}
}

/// Initialize a Python application at the target path.
// TODO(konsti): Remove when stabilizing packaged-init.
#[expect(clippy::fn_params_excessive_bools)]
fn init_application(
fn init_application_old(
name: &PackageName,
path: &Path,
requires_python: &RequiresPython,
Expand All @@ -816,7 +821,7 @@ impl InitProjectKind {
// read conditional includes that depend on the repository path.
init_vcs(path, vcs)?;

// Do no fill in `authors` for non-packaged applications unless explicitly requested.
// Do not fill in `authors` for non-packaged applications unless explicitly requested.
let author_from = author_from.unwrap_or_else(|| {
if package {
AuthorFrom::default()
Expand Down Expand Up @@ -879,8 +884,9 @@ impl InitProjectKind {
}

/// Initialize a library project at the target path.
// TODO(konsti): Remove when stabilizing packaged-init.
#[expect(clippy::fn_params_excessive_bools)]
fn init_library(
fn init_library_old(
name: &PackageName,
path: &Path,
requires_python: &RequiresPython,
Expand Down Expand Up @@ -930,6 +936,122 @@ impl InitProjectKind {

Ok(())
}

/// Initialize this project kind at the target path.
#[expect(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<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>,
author_from: Option<AuthorFrom>,
no_readme: bool,
package: bool,
) -> Result<()> {
// TODO(konsti): Remove when stabilizing.
if matches!(self, Self::ApplicationOld | Self::LibraryOld) {
return self.init_old(
name,
path,
requires_python,
description,
no_description,
bare,
vcs,
build_backend,
author_from,
no_readme,
package,
);
}

fs_err::create_dir_all(path)?;

// Initialize the version control system first so that Git configuration can properly
// read conditional includes that depend on the repository path.
init_vcs(path, vcs)?;

// Do not fill in `authors` for non-packaged applications unless explicitly requested.
let author_from = author_from.unwrap_or_else(|| match self {
Self::ApplicationWithLibrary | Self::Library | Self::BareWithBuildSystem => {
AuthorFrom::default()
}
Self::Application | Self::Bare => AuthorFrom::None,
Self::ApplicationOld | Self::LibraryOld => unreachable!(),
});
let author = get_author_info(path, author_from);

// Create the `pyproject.toml`
let mut pyproject = pyproject_project(
name,
requires_python,
author.as_ref(),
description,
no_description,
no_readme || bare,
);

match self {
// Create only the most barebones `pyproject.toml`, no build system
Self::Bare => {}
// Create only a barebones `pyproject.toml`, but with a build system table
Self::BareWithBuildSystem => {
// Add a build system
let build_backend = build_backend.unwrap_or(ProjectBuildBackend::Uv);
pyproject.push('\n');
pyproject.push_str(&pyproject_build_system(name, build_backend));
Comment thread
konstin marked this conversation as resolved.
}
Self::ApplicationWithLibrary => {
// 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"));

// Add a build system
let build_backend = build_backend.unwrap_or(ProjectBuildBackend::Uv);
pyproject.push('\n');
pyproject.push_str(&pyproject_build_system(name, build_backend));
pyproject_build_backend_prerequisites(name, path, build_backend)?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we don't call this in BareWithBuildSystem, which will probably result in a broken configuration with e.g. maturin or scikit. Is that intentional? If so, maybe capture that somewhere?

Edit: I now realize this is another thing you call out, so my opinion is that we shouldn't generate broken configuration if we can help it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Does this mean we should error for uv init --lib --bare or uv init --package --bare? (Given that we shouldn't ignore --lib/--package)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you think it's possible to generate something that's not broken? If not, then erroring out would be fine

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There's two aspects here:

Stable already allows uv init --bare --package, which creates a [build-system] table without the associated files. Not supporting this anymore would be a regression.

It seems fine to not create a build system with --bare by default, as --bare doesn't create most things we usually create.


// Generate `src` files with app-style `main()` in `__init__.py`
generate_package_scripts(name, path, build_backend, false)?;
}
Self::Application => {
let main_contents = indoc::formatdoc! {r#"
def main():
print("Hello from {name}!")


if __name__ == "__main__":
main()
"#};

// Create `main.py` if it doesn't exist
// (This isn't intended to be a particularly special or magical filename, just nice)
// TODO(zanieb): Only create `main.py` if there are no other Python files?
let main_py = path.join("main.py");
if !main_py.try_exists()? && !bare {
fs_err::write(path.join("main.py"), main_contents)?;
}
}
Self::Library => {
let build_backend = build_backend.unwrap_or(ProjectBuildBackend::Uv);
pyproject.push('\n');
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, true)?;
}
_ => unreachable!(),
}
fs_err::write(path.join("pyproject.toml"), pyproject)?;
Ok(())
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -1151,7 +1273,6 @@ fn generate_package_scripts(
let pkg_dir = src_dir.join(&*module_name);
fs_err::create_dir_all(&pkg_dir)?;

// Python script for pure-python packaged apps or libs
let pure_python_script = if is_lib {
indoc::formatdoc! {r#"
def hello() -> str:
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2122,7 +2122,8 @@ async fn run_project(
match *project_command {
ProjectCommand::Init(args) => {
// Resolve the settings from the command-line arguments and workspace configuration.
let args = settings::InitSettings::resolve(args, filesystem, environment);
let args =
settings::InitSettings::resolve(args, filesystem, environment, globals.preview)?;
show_settings!(args);

// The `--project` arg is being deprecated for `init` with a warning now and an error in preview.
Expand Down
Loading
Loading