Skip to content

Commit 8e7f3b9

Browse files
committed
Package by default
This change is more complex than just toggling the switch, as we want to give users something runnable to start with, but also packaged setup. I haven't updated the tests yet. **Draft** | kind | package | template | |-----------|----------------|----------------------------------------------------------| | (default) | (default) | `main.py` calling `hello()` + package exposing `hello()` | | (default) | `--package` | `main.py` calling `hello()` + package exposing `hello()` | | (default) | `--no-package` | `main.py` with `main()` | | `--app` | (default) | `main.py` with `main()` | | `--app` | `--package` | `main.py` calling `hello()` + package exposing `hello()` | | `--app` | `--no-package` | `main.py` with `main()` | | `--lib` | (default) | package exposing `hello()` | | `--lib` | `--package` | package exposing `hello()` | | `--lib` | `--no-package` | Error: libraries are always packaged | The default is a mixed layout: There's a package, but also a `main.py`. `--lib` creates only the package, no `main.py`, while `--app` creates only `main.py`. `--package` has no effect - it's the default now - while `--no-package` goes back to the previous default.
1 parent cd8d9ba commit 8e7f3b9

6 files changed

Lines changed: 259 additions & 38 deletions

File tree

crates/uv-cli/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3372,7 +3372,7 @@ pub struct InitArgs {
33723372
///
33733373
/// Defines a `[build-system]` for the project.
33743374
///
3375-
/// This is the default behavior when using `--lib` or `--build-backend`.
3375+
/// This is the default behavior, the option exists for backwards compatibility.
33763376
///
33773377
/// When using `--app`, this will include a `[project.scripts]` entrypoint and use a `src/`
33783378
/// project structure.
@@ -3414,7 +3414,7 @@ pub struct InitArgs {
34143414
///
34153415
/// By default, adds a requirement on the system Python version; use `--python` to specify an
34163416
/// alternative Python version requirement.
3417-
#[arg(long, conflicts_with_all=["app", "lib", "package", "build_backend", "description"])]
3417+
#[arg(long, conflicts_with_all=["app", "lib", "package", "no_package", "virtual", "build_backend", "description"])]
34183418
pub r#script: bool,
34193419

34203420
/// Set the project description.

crates/uv-preview/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ pub enum PreviewFeature {
256256
Audit = 1 << 26,
257257
ProjectDirectoryMustExist = 1 << 27,
258258
IndexExcludeNewer = 1 << 28,
259+
PackagedInit = 1 << 29,
259260
}
260261

261262
impl PreviewFeature {
@@ -291,6 +292,7 @@ impl PreviewFeature {
291292
Self::Audit => "audit",
292293
Self::ProjectDirectoryMustExist => "project-directory-must-exist",
293294
Self::IndexExcludeNewer => "index-exclude-newer",
295+
Self::PackagedInit => "packaged-init",
294296
}
295297
}
296298
}
@@ -339,6 +341,7 @@ impl FromStr for PreviewFeature {
339341
"audit" => Self::Audit,
340342
"project-directory-must-exist" => Self::ProjectDirectoryMustExist,
341343
"index-exclude-newer" => Self::IndexExcludeNewer,
344+
"packaged-init" => Self::PackagedInit,
342345
_ => return Err(PreviewFeatureParseError),
343346
})
344347
}

crates/uv/src/commands/project/init.rs

Lines changed: 165 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ async fn init_script(
287287
async fn init_project(
288288
path: &Path,
289289
name: &PackageName,
290+
// TODO(konsti): Remove when stabilizing.
290291
package: bool,
291292
project_kind: InitProjectKind,
292293
bare: bool,
@@ -734,33 +735,31 @@ pub(crate) enum InitKind {
734735
Script,
735736
}
736737

737-
impl Default for InitKind {
738-
fn default() -> Self {
739-
Self::Project(InitProjectKind::default())
740-
}
741-
}
742-
743738
/// The kind of Python project to initialize (either an application or a library).
744739
#[derive(Debug, Copy, Clone, Default)]
745740
pub(crate) enum InitProjectKind {
746-
/// Initialize a Python application.
741+
/// A python package with a `main.py`.
747742
#[default]
743+
ApplicationWithLibrary,
744+
/// A flat application with a `main.py`.
748745
Application,
749-
/// Initialize a Python library.
746+
/// A python package, no entrypoint.
750747
Library,
751-
}
752-
753-
impl InitKind {
754-
/// Returns `true` if the project should be packaged by default.
755-
pub(crate) fn packaged_by_default(self) -> bool {
756-
matches!(self, Self::Project(InitProjectKind::Library))
757-
}
748+
/// Initialize only a `pyproject.toml`
749+
Bare,
750+
// TODO(konsti): Remove when stabilizing.
751+
/// Initialize a Python application.
752+
ApplicationOld,
753+
// TODO(konsti): Remove when stabilizing.
754+
/// Initialize a Python library.
755+
LibraryOld,
758756
}
759757

760758
impl InitProjectKind {
761759
/// Initialize this project kind at the target path.
760+
// TODO(konsti): Remove when stabilizing packaged-init.
762761
#[expect(clippy::fn_params_excessive_bools)]
763-
fn init(
762+
fn init_old(
764763
self,
765764
name: &PackageName,
766765
path: &Path,
@@ -775,7 +774,7 @@ impl InitProjectKind {
775774
package: bool,
776775
) -> Result<()> {
777776
match self {
778-
Self::Application => Self::init_application(
777+
Self::ApplicationOld => Self::init_application_old(
779778
name,
780779
path,
781780
requires_python,
@@ -788,7 +787,7 @@ impl InitProjectKind {
788787
no_readme,
789788
package,
790789
),
791-
Self::Library => Self::init_library(
790+
Self::LibraryOld => Self::init_library_old(
792791
name,
793792
path,
794793
requires_python,
@@ -801,12 +800,14 @@ impl InitProjectKind {
801800
no_readme,
802801
package,
803802
),
803+
_ => unreachable!(),
804804
}
805805
}
806806

807807
/// Initialize a Python application at the target path.
808+
// TODO(konsti): Remove when stabilizing packaged-init.
808809
#[expect(clippy::fn_params_excessive_bools)]
809-
fn init_application(
810+
fn init_application_old(
810811
name: &PackageName,
811812
path: &Path,
812813
requires_python: &RequiresPython,
@@ -888,8 +889,9 @@ impl InitProjectKind {
888889
}
889890

890891
/// Initialize a library project at the target path.
892+
// TODO(konsti): Remove when stabilizing packaged-init.
891893
#[expect(clippy::fn_params_excessive_bools)]
892-
fn init_library(
894+
fn init_library_old(
893895
name: &PackageName,
894896
path: &Path,
895897
requires_python: &RequiresPython,
@@ -939,6 +941,149 @@ impl InitProjectKind {
939941

940942
Ok(())
941943
}
944+
945+
fn is_packaged(self) -> bool {
946+
match self {
947+
Self::ApplicationWithLibrary => true,
948+
Self::Application => false,
949+
Self::Library => false,
950+
Self::Bare => false,
951+
Self::ApplicationOld | Self::LibraryOld => unreachable!(),
952+
}
953+
}
954+
955+
/// Initialize this project kind at the target path.
956+
#[expect(clippy::fn_params_excessive_bools)]
957+
fn init(
958+
self,
959+
name: &PackageName,
960+
path: &Path,
961+
requires_python: &RequiresPython,
962+
description: Option<&str>,
963+
no_description: bool,
964+
bare: bool,
965+
vcs: Option<VersionControlSystem>,
966+
build_backend: Option<ProjectBuildBackend>,
967+
author_from: Option<AuthorFrom>,
968+
no_readme: bool,
969+
package: bool,
970+
) -> Result<()> {
971+
// TODO(konsti): Remove when stabilizing.
972+
if matches!(self, Self::ApplicationOld | Self::LibraryOld) {
973+
return self.init_old(
974+
name,
975+
path,
976+
requires_python,
977+
description,
978+
no_description,
979+
bare,
980+
vcs,
981+
build_backend,
982+
author_from,
983+
no_readme,
984+
package,
985+
);
986+
}
987+
988+
fs_err::create_dir_all(path)?;
989+
990+
// Initialize the version control system first so that Git configuration can properly
991+
// read conditional includes that depend on the repository path.
992+
init_vcs(path, vcs)?;
993+
994+
// Do no fill in `authors` for non-packaged applications unless explicitly requested.
995+
let author_from = author_from.unwrap_or_else(|| {
996+
if self.is_packaged() {
997+
AuthorFrom::default()
998+
} else {
999+
AuthorFrom::None
1000+
}
1001+
});
1002+
let author = get_author_info(path, author_from);
1003+
1004+
// Create the `pyproject.toml`
1005+
let mut pyproject = pyproject_project(
1006+
name,
1007+
requires_python,
1008+
author.as_ref(),
1009+
description,
1010+
no_description,
1011+
no_readme || bare,
1012+
);
1013+
1014+
match self {
1015+
// Create only the most barebones `pyproject.toml`, no build system.
1016+
Self::Bare => {}
1017+
Self::ApplicationWithLibrary => {
1018+
// Since it'll be packaged, we can add a `[project.scripts]` entry
1019+
pyproject.push('\n');
1020+
pyproject.push_str(&pyproject_project_scripts(name, name.as_str(), "main"));
1021+
1022+
// Add a build system
1023+
let build_backend = build_backend.unwrap_or(ProjectBuildBackend::Uv);
1024+
pyproject.push('\n');
1025+
pyproject.push_str(&pyproject_build_system(name, build_backend));
1026+
pyproject_build_backend_prerequisites(name, path, build_backend)?;
1027+
1028+
// Generate `src` files (lib-style `hello()` since `main.py` calls into the package)
1029+
generate_package_scripts(name, path, build_backend, true)?;
1030+
1031+
// For packages applications, create an entrypoint that can be run with `uv run main.py`
1032+
// and pulls in the logic implemented in the package.
1033+
let main_contents = indoc::formatdoc! {r#"
1034+
from {module_name} import hello
1035+
1036+
1037+
def main():
1038+
print(hello())
1039+
1040+
1041+
if __name__ == "__main__":
1042+
main()
1043+
"#,
1044+
module_name = name.as_dist_info_name()
1045+
};
1046+
1047+
// Create `main.py` if it doesn't exist
1048+
// (This isn't intended to be a particularly special or magical filename, just nice)
1049+
// TODO(zanieb): Only create `main.py` if there are no other Python files?
1050+
let main_py = path.join("main.py");
1051+
if !main_py.try_exists()? && !bare {
1052+
fs_err::write(path.join("main.py"), main_contents)?;
1053+
}
1054+
}
1055+
Self::Application => {
1056+
let main_contents = indoc::formatdoc! {r#"
1057+
def main():
1058+
print("Hello from {name}!")
1059+
1060+
1061+
if __name__ == "__main__":
1062+
main()
1063+
"#};
1064+
1065+
// Create `main.py` if it doesn't exist
1066+
// (This isn't intended to be a particularly special or magical filename, just nice)
1067+
// TODO(zanieb): Only create `main.py` if there are no other Python files?
1068+
let main_py = path.join("main.py");
1069+
if !main_py.try_exists()? && !bare {
1070+
fs_err::write(path.join("main.py"), main_contents)?;
1071+
}
1072+
}
1073+
Self::Library => {
1074+
let build_backend = build_backend.unwrap_or(ProjectBuildBackend::Uv);
1075+
pyproject.push('\n');
1076+
pyproject.push_str(&pyproject_build_system(name, build_backend));
1077+
pyproject_build_backend_prerequisites(name, path, build_backend)?;
1078+
1079+
// Generate `src` files
1080+
generate_package_scripts(name, path, build_backend, true)?;
1081+
}
1082+
_ => unreachable!(),
1083+
}
1084+
fs_err::write(path.join("pyproject.toml"), pyproject)?;
1085+
Ok(())
1086+
}
9421087
}
9431088

9441089
#[derive(Debug)]
@@ -1160,7 +1305,6 @@ fn generate_package_scripts(
11601305
let pkg_dir = src_dir.join(&*module_name);
11611306
fs_err::create_dir_all(&pkg_dir)?;
11621307

1163-
// Python script for pure-python packaged apps or libs
11641308
let pure_python_script = if is_lib {
11651309
indoc::formatdoc! {r#"
11661310
def hello() -> str:

crates/uv/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2082,7 +2082,8 @@ async fn run_project(
20822082
match *project_command {
20832083
ProjectCommand::Init(args) => {
20842084
// Resolve the settings from the command-line arguments and workspace configuration.
2085-
let args = settings::InitSettings::resolve(args, filesystem, environment);
2085+
let args =
2086+
settings::InitSettings::resolve(args, filesystem, environment, globals.preview);
20862087
show_settings!(args);
20872088

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

0 commit comments

Comments
 (0)