Skip to content

Commit 1aec5d5

Browse files
committed
Add --app and --lib options to uv init
Changes the default from a library with `src/` to an application that is not packaged
1 parent 588cb6c commit 1aec5d5

File tree

9 files changed

+713
-125
lines changed

9 files changed

+713
-125
lines changed

crates/uv-cli/src/lib.rs

+32-5
Original file line numberDiff line numberDiff line change
@@ -2101,13 +2101,40 @@ pub struct InitArgs {
21012101

21022102
/// Create a virtual project, rather than a package.
21032103
///
2104-
/// A virtual project is a project that is not intended to be built as a Python package,
2105-
/// such as a project that only contains scripts or other application code.
2106-
///
2107-
/// Virtual projects themselves are not installed into the Python environment.
2108-
#[arg(long)]
2104+
/// This option is deprecated and will be removed in a future release.
2105+
#[arg(long, hide = true, conflicts_with = "package")]
21092106
pub r#virtual: bool,
21102107

2108+
/// Set up the project to be packaged.
2109+
///
2110+
/// This is the default behavior when using `--lib`.
2111+
#[arg(long, conflicts_with = "no_package")]
2112+
pub r#package: bool,
2113+
2114+
/// Do not set up the project to be packaged.
2115+
///
2116+
/// This is the default behavior when using `--app`.
2117+
#[arg(long, conflicts_with = "package", conflicts_with = "lib")]
2118+
pub r#no_package: bool,
2119+
2120+
/// Create a project for an application.
2121+
///
2122+
/// This is the default behavior if `--lib` is not requested.
2123+
///
2124+
/// This project kind is for web servers, scripts, and command-line interfaces.
2125+
///
2126+
/// By default, an application is not intended to be built and distributed as a Python package.
2127+
/// The `--package` option can be used to create an application that is distributable, e.g., if
2128+
/// you want to distribute a command-line interface via PyPI.
2129+
#[arg(long, alias = "application", conflicts_with = "lib")]
2130+
pub r#app: bool,
2131+
2132+
/// Create a project for a library.
2133+
///
2134+
/// A library is a project that is intended to be built and distributed as a Python package.
2135+
#[arg(long, alias = "library", conflicts_with = "app")]
2136+
pub r#lib: bool,
2137+
21112138
/// Do not create a `README.md` file.
21122139
#[arg(long)]
21132140
pub no_readme: bool,

crates/uv-workspace/src/workspace.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1608,7 +1608,7 @@ mod tests {
16081608
"exclude": null
16091609
},
16101610
"managed": null,
1611-
"virtual": null,
1611+
"package": null,
16121612
"dev-dependencies": null,
16131613
"environments": null,
16141614
"override-dependencies": null,

crates/uv/src/commands/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub(crate) use pip::sync::pip_sync;
1919
pub(crate) use pip::tree::pip_tree;
2020
pub(crate) use pip::uninstall::pip_uninstall;
2121
pub(crate) use project::add::add;
22-
pub(crate) use project::init::init;
22+
pub(crate) use project::init::{init, InitProjectKind};
2323
pub(crate) use project::lock::lock;
2424
pub(crate) use project::remove::remove;
2525
pub(crate) use project::run::{run, RunCommand};

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

+178-64
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::fmt::Write;
22
use std::path::Path;
33

4-
use anyhow::{Context, Result};
4+
use anyhow::{anyhow, Context, Result};
55
use owo_colors::OwoColorize;
66
use pep440_rs::Version;
77
use pep508_rs::PackageName;
@@ -27,7 +27,8 @@ use crate::printer::Printer;
2727
pub(crate) async fn init(
2828
explicit_path: Option<String>,
2929
name: Option<PackageName>,
30-
r#virtual: bool,
30+
package: bool,
31+
project_kind: InitProjectKind,
3132
no_readme: bool,
3233
python: Option<String>,
3334
no_workspace: bool,
@@ -72,7 +73,8 @@ pub(crate) async fn init(
7273
init_project(
7374
&path,
7475
&name,
75-
r#virtual,
76+
package,
77+
project_kind,
7678
no_readme,
7779
python,
7880
no_workspace,
@@ -93,16 +95,10 @@ pub(crate) async fn init(
9395
}
9496
}
9597

96-
let project = if r#virtual { "workspace" } else { "project" };
9798
match explicit_path {
9899
// Initialized a project in the current directory.
99100
None => {
100-
writeln!(
101-
printer.stderr(),
102-
"Initialized {} `{}`",
103-
project,
104-
name.cyan()
105-
)?;
101+
writeln!(printer.stderr(), "Initialized project `{}`", name.cyan())?;
106102
}
107103
// Initialized a project in the given directory.
108104
Some(path) => {
@@ -112,8 +108,7 @@ pub(crate) async fn init(
112108

113109
writeln!(
114110
printer.stderr(),
115-
"Initialized {} `{}` at `{}`",
116-
project,
111+
"Initialized project `{}` at `{}`",
117112
name.cyan(),
118113
path.display().cyan()
119114
)?;
@@ -128,7 +123,8 @@ pub(crate) async fn init(
128123
async fn init_project(
129124
path: &Path,
130125
name: &PackageName,
131-
r#virtual: bool,
126+
package: bool,
127+
project_kind: InitProjectKind,
132128
no_readme: bool,
133129
python: Option<String>,
134130
no_workspace: bool,
@@ -245,57 +241,7 @@ async fn init_project(
245241
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version())
246242
};
247243

248-
if r#virtual {
249-
// Create the `pyproject.toml`, but omit `[build-system]`.
250-
let pyproject = indoc::formatdoc! {r#"
251-
[project]
252-
name = "{name}"
253-
version = "0.1.0"
254-
description = "Add your description here"{readme}
255-
requires-python = "{requires_python}"
256-
dependencies = []
257-
"#,
258-
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
259-
requires_python = requires_python.specifiers(),
260-
};
261-
262-
fs_err::create_dir_all(path)?;
263-
fs_err::write(path.join("pyproject.toml"), pyproject)?;
264-
} else {
265-
// Create the `pyproject.toml`.
266-
let pyproject = indoc::formatdoc! {r#"
267-
[project]
268-
name = "{name}"
269-
version = "0.1.0"
270-
description = "Add your description here"{readme}
271-
requires-python = "{requires_python}"
272-
dependencies = []
273-
274-
[build-system]
275-
requires = ["hatchling"]
276-
build-backend = "hatchling.build"
277-
"#,
278-
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
279-
requires_python = requires_python.specifiers(),
280-
};
281-
282-
fs_err::create_dir_all(path)?;
283-
fs_err::write(path.join("pyproject.toml"), pyproject)?;
284-
285-
// Create `src/{name}/__init__.py`, if it doesn't exist already.
286-
let src_dir = path.join("src").join(&*name.as_dist_info_name());
287-
let init_py = src_dir.join("__init__.py");
288-
if !init_py.try_exists()? {
289-
fs_err::create_dir_all(&src_dir)?;
290-
fs_err::write(
291-
init_py,
292-
indoc::formatdoc! {r#"
293-
def hello() -> str:
294-
return "Hello from {name}!"
295-
"#},
296-
)?;
297-
}
298-
}
244+
project_kind.init(name, path, &requires_python, no_readme, package)?;
299245

300246
if let Some(workspace) = workspace {
301247
if workspace.excludes(path)? {
@@ -339,3 +285,171 @@ async fn init_project(
339285

340286
Ok(())
341287
}
288+
289+
#[derive(Debug, Clone, Default)]
290+
pub(crate) enum InitProjectKind {
291+
#[default]
292+
Application,
293+
Library,
294+
}
295+
296+
impl InitProjectKind {
297+
/// Initialize this project kind at the target path.
298+
fn init(
299+
&self,
300+
name: &PackageName,
301+
path: &Path,
302+
requires_python: &RequiresPython,
303+
no_readme: bool,
304+
package: bool,
305+
) -> Result<()> {
306+
match self {
307+
InitProjectKind::Application => {
308+
init_application(name, path, requires_python, no_readme, package)
309+
}
310+
InitProjectKind::Library => {
311+
init_library(name, path, requires_python, no_readme, package)
312+
}
313+
}
314+
}
315+
316+
/// Whether or not this project kind is packaged by default.
317+
pub(crate) fn packaged_by_default(&self) -> bool {
318+
matches!(self, InitProjectKind::Library)
319+
}
320+
}
321+
322+
fn init_application(
323+
name: &PackageName,
324+
path: &Path,
325+
requires_python: &RequiresPython,
326+
no_readme: bool,
327+
package: bool,
328+
) -> Result<()> {
329+
// Create the `pyproject.toml`
330+
let mut pyproject = pyproject_project(name, requires_python, no_readme);
331+
332+
// Include additional project configuration for packaged applications
333+
if package {
334+
// Since it'll be packaged, we can add a `[project.scripts]` entry
335+
pyproject.push('\n');
336+
pyproject.push_str(&pyproject_project_scripts(name, "hello", "hello"));
337+
338+
// Add a build system
339+
pyproject.push('\n');
340+
pyproject.push_str(pyproject_build_system());
341+
}
342+
343+
fs_err::create_dir_all(path)?;
344+
345+
// Create the source structure.
346+
if package {
347+
// Create `src/{name}/__init__.py`, if it doesn't exist already.
348+
let src_dir = path.join("src").join(&*name.as_dist_info_name());
349+
let init_py = src_dir.join("__init__.py");
350+
if !init_py.try_exists()? {
351+
fs_err::create_dir_all(&src_dir)?;
352+
fs_err::write(
353+
init_py,
354+
indoc::formatdoc! {r#"
355+
def hello():
356+
print("Hello from {name}!")
357+
"#},
358+
)?;
359+
}
360+
} else {
361+
// Create `hello.py` if it doesn't exist
362+
// TODO(zanieb): Only create `hello.py` if there are no other Python files?
363+
let hello_py = path.join("hello.py");
364+
if !hello_py.try_exists()? {
365+
fs_err::write(
366+
path.join("hello.py"),
367+
indoc::formatdoc! {r#"
368+
def main():
369+
print("Hello from {name}!")
370+
371+
if __name__ == "__main__":
372+
main()
373+
"#},
374+
)?;
375+
}
376+
}
377+
fs_err::write(path.join("pyproject.toml"), pyproject)?;
378+
379+
Ok(())
380+
}
381+
382+
fn init_library(
383+
name: &PackageName,
384+
path: &Path,
385+
requires_python: &RequiresPython,
386+
no_readme: bool,
387+
package: bool,
388+
) -> Result<()> {
389+
if !package {
390+
return Err(anyhow!("Library projects must be packaged"));
391+
}
392+
393+
// Create the `pyproject.toml`
394+
let mut pyproject = pyproject_project(name, requires_python, no_readme);
395+
396+
// Always include a build system if the project is packaged.
397+
pyproject.push('\n');
398+
pyproject.push_str(pyproject_build_system());
399+
400+
fs_err::create_dir_all(path)?;
401+
fs_err::write(path.join("pyproject.toml"), pyproject)?;
402+
403+
// Create `src/{name}/__init__.py`, if it doesn't exist already.
404+
let src_dir = path.join("src").join(&*name.as_dist_info_name());
405+
let init_py = src_dir.join("__init__.py");
406+
if !init_py.try_exists()? {
407+
fs_err::create_dir_all(&src_dir)?;
408+
fs_err::write(
409+
init_py,
410+
indoc::formatdoc! {r#"
411+
def hello() -> str:
412+
return "Hello from {name}!"
413+
"#},
414+
)?;
415+
}
416+
417+
Ok(())
418+
}
419+
420+
/// Generate the `[project]` section of a `pyproject.toml`.
421+
fn pyproject_project(
422+
name: &PackageName,
423+
requires_python: &RequiresPython,
424+
no_readme: bool,
425+
) -> String {
426+
indoc::formatdoc! {r#"
427+
[project]
428+
name = "{name}"
429+
version = "0.1.0"
430+
description = "Add your description here"{readme}
431+
requires-python = "{requires_python}"
432+
dependencies = []
433+
"#,
434+
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
435+
requires_python = requires_python.specifiers(),
436+
}
437+
}
438+
439+
/// Generate the `[build-system]` section of a `pyproject.toml`.
440+
fn pyproject_build_system() -> &'static str {
441+
indoc::indoc! {r#"
442+
[build-system]
443+
requires = ["hatchling"]
444+
build-backend = "hatchling.build"
445+
"#}
446+
}
447+
448+
/// Generate the `[project.scripts]` section of a `pyproject.toml`.
449+
fn pyproject_project_scripts(package: &PackageName, executable_name: &str, target: &str) -> String {
450+
let module_name = package.as_dist_info_name();
451+
indoc::formatdoc! {r#"
452+
[project.scripts]
453+
{executable_name} = "{module_name}:{target}"
454+
"#}
455+
}

crates/uv/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,8 @@ async fn run_project(
10251025
commands::init(
10261026
args.path,
10271027
args.name,
1028-
args.r#virtual,
1028+
args.package,
1029+
args.kind,
10291030
args.no_readme,
10301031
args.python,
10311032
args.no_workspace,

0 commit comments

Comments
 (0)