Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fill in authors filed during uv init #7756

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,10 @@ pub struct InitArgs {
#[arg(long)]
pub no_readme: bool,

/// Do not fill in the `authors` field in the `pyproject.toml`.
#[arg(long)]
pub no_authors: bool,

/// Do not create a `.python-version` file for the project.
///
/// By default, uv will create a `.python-version` file containing the minor version of
Expand Down
108 changes: 102 additions & 6 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use anyhow::{anyhow, Context, Result};
use owo_colors::OwoColorize;
Expand Down Expand Up @@ -36,6 +37,7 @@ pub(crate) async fn init(
init_kind: InitKind,
vcs: Option<VersionControlSystem>,
no_readme: bool,
no_authors: bool,
no_pin_python: bool,
python: Option<String>,
no_workspace: bool,
Expand All @@ -62,6 +64,7 @@ pub(crate) async fn init(
printer,
no_workspace,
no_readme,
no_authors,
no_pin_python,
package,
native_tls,
Expand Down Expand Up @@ -111,6 +114,7 @@ pub(crate) async fn init(
project_kind,
vcs,
no_readme,
no_authors,
no_pin_python,
python,
no_workspace,
Expand Down Expand Up @@ -165,6 +169,7 @@ async fn init_script(
printer: Printer,
no_workspace: bool,
no_readme: bool,
no_authors: bool,
no_pin_python: bool,
package: bool,
native_tls: bool,
Expand All @@ -175,6 +180,9 @@ async fn init_script(
if no_readme {
warn_user_once!("`--no_readme` is a no-op for Python scripts, which are standalone");
}
if no_authors {
warn_user_once!("`--no_authors` is a no-op for Python scripts, which are standalone");
}
if package {
warn_user_once!("`--package` is a no-op for Python scripts, which are standalone");
}
Expand Down Expand Up @@ -237,6 +245,7 @@ async fn init_project(
project_kind: InitProjectKind,
vcs: Option<VersionControlSystem>,
no_readme: bool,
no_authors: bool,
no_pin_python: bool,
python: Option<String>,
no_workspace: bool,
Expand Down Expand Up @@ -461,6 +470,7 @@ async fn init_project(
&requires_python,
python_request.as_ref(),
vcs,
no_authors,
no_readme,
package,
)
Expand Down Expand Up @@ -550,6 +560,7 @@ impl InitProjectKind {
requires_python: &RequiresPython,
python_request: Option<&PythonRequest>,
vcs: Option<VersionControlSystem>,
no_authors: bool,
no_readme: bool,
package: bool,
) -> Result<()> {
Expand All @@ -561,6 +572,7 @@ impl InitProjectKind {
requires_python,
python_request,
vcs,
no_authors,
no_readme,
package,
)
Expand All @@ -573,6 +585,7 @@ impl InitProjectKind {
requires_python,
python_request,
vcs,
no_authors,
no_readme,
package,
)
Expand All @@ -589,11 +602,25 @@ impl InitProjectKind {
requires_python: &RequiresPython,
python_request: Option<&PythonRequest>,
vcs: Option<VersionControlSystem>,
no_authors: bool,
no_readme: bool,
package: bool,
) -> Result<()> {
fs_err::create_dir_all(path)?;

// Get the author information from git configuration.
let author = if no_authors {
Author::default()
} else {
get_author_from_git(path)
.inspect_err(|err| {
debug!("Failed to get author information from git: {err}");
})
.unwrap_or_default()
};

// Create the `pyproject.toml`
let mut pyproject = pyproject_project(name, requires_python, no_readme);
let mut pyproject = pyproject_project(name, requires_python, &author, no_readme);

// Include additional project configuration for packaged applications
if package {
Expand All @@ -606,8 +633,6 @@ impl InitProjectKind {
pyproject.push_str(pyproject_build_system());
}

fs_err::create_dir_all(path)?;

// Create the source structure.
if package {
// Create `src/{name}/__init__.py`, if it doesn't exist already.
Expand Down Expand Up @@ -670,21 +695,34 @@ impl InitProjectKind {
requires_python: &RequiresPython,
python_request: Option<&PythonRequest>,
vcs: Option<VersionControlSystem>,
no_authors: bool,
no_readme: bool,
package: bool,
) -> Result<()> {
if !package {
return Err(anyhow!("Library projects must be packaged"));
}

fs_err::create_dir_all(path)?;

// Get the author information from git configuration.
let author = if no_authors {
Author::default()
} else {
get_author_from_git(path)
.inspect_err(|err| {
debug!("Failed to get author from git: {err}");
})
.unwrap_or_default()
};

// Create the `pyproject.toml`
let mut pyproject = pyproject_project(name, requires_python, no_readme);
let mut pyproject = pyproject_project(name, requires_python, &author, no_readme);

// Always include a build system if the project is packaged.
pyproject.push('\n');
pyproject.push_str(pyproject_build_system());

fs_err::create_dir_all(path)?;
fs_err::write(path.join("pyproject.toml"), pyproject)?;

// Create `src/{name}/__init__.py`, if it doesn't exist already.
Expand Down Expand Up @@ -728,21 +766,44 @@ impl InitProjectKind {
}
}

#[derive(Debug, Default)]
struct Author {
name: Option<String>,
email: Option<String>,
}

impl Author {
fn is_empty(&self) -> bool {
self.name.is_none() && self.email.is_none()
}

fn to_toml_string(&self) -> String {
match (self.name.as_deref(), self.email.as_deref()) {
(Some(name), Some(email)) => format!("{{ name = \"{name}\", email = \"{email}\" }}"),
(Some(name), None) => format!("{{ name = \"{name}\" }}"),
(None, Some(email)) => format!("{{ email = \"{email}\" }}"),
(None, None) => String::new(),
}
}
}

/// Generate the `[project]` section of a `pyproject.toml`.
fn pyproject_project(
name: &PackageName,
requires_python: &RequiresPython,
author: &Author,
no_readme: bool,
) -> String {
indoc::formatdoc! {r#"
[project]
name = "{name}"
version = "0.1.0"
description = "Add your description here"{readme}
description = "Add your description here"{readme}{authors}
requires-python = "{requires_python}"
dependencies = []
"#,
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
authors = if author.is_empty() { String::new() } else { format!("\nauthors = [\n {} \n]", author.to_toml_string()) },
requires_python = requires_python.specifiers(),
}
}
Expand Down Expand Up @@ -812,3 +873,38 @@ fn init_vcs(path: &Path, vcs: Option<VersionControlSystem>) -> Result<()> {

Ok(())
}

/// Fetch the default author from git configuration.
fn get_author_from_git(path: &Path) -> Result<Author> {
let Ok(git) = which::which("git") else {
anyhow::bail!("`git` not found in PATH")
};

let mut author = Author::default();

let output = Command::new(&git)
.arg("config")
.arg("get")
.arg("user.name")
.current_dir(path)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()?;
if output.status.success() {
author.name = Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}

let output = Command::new(&git)
.arg("config")
.arg("get")
.arg("user.email")
.current_dir(path)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()?;
if output.status.success() {
author.email = Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
}

Ok(author)
}
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ async fn run_project(
args.kind,
args.vcs,
args.no_readme,
args.no_authors,
args.no_pin_python,
args.python,
args.no_workspace,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ pub(crate) struct InitSettings {
pub(crate) kind: InitKind,
pub(crate) vcs: Option<VersionControlSystem>,
pub(crate) no_readme: bool,
pub(crate) no_authors: bool,
pub(crate) no_pin_python: bool,
pub(crate) no_workspace: bool,
pub(crate) python: Option<String>,
Expand All @@ -184,6 +185,7 @@ impl InitSettings {
script,
vcs,
no_readme,
no_authors,
no_pin_python,
no_workspace,
python,
Expand All @@ -206,6 +208,7 @@ impl InitSettings {
kind,
vcs,
no_readme,
no_authors,
no_pin_python,
no_workspace,
python,
Expand Down
56 changes: 56 additions & 0 deletions crates/uv/tests/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2260,3 +2260,59 @@ fn init_git_not_installed() {
error: Attempted to initialize a Git repository, but `git` was not found in PATH
"###);
}

#[test]
fn init_with_author() -> Result<()> {
let context = TestContext::new("3.12");

// Create a Git repository and set the author.
Command::new("git")
.arg("init")
.current_dir(&context.temp_dir)
.status()?;
Command::new("git")
.arg("config")
.arg("user.name")
.arg("Alice")
.arg("--local")
.current_dir(&context.temp_dir)
.status()?;
Command::new("git")
.arg("config")
.arg("user.email")
.arg("[email protected]")
.arg("--local")
.current_dir(&context.temp_dir)
.status()?;

uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Initialized project `temp`
"###);

let pyproject = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject, @r###"
[project]
name = "temp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Alice", email = "[email protected]" }
]
requires-python = ">=3.12"
dependencies = []
"###
);
});

Ok(())
}
2 changes: 2 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@ uv init [OPTIONS] [PATH]
<p>However, in some cases, you may want to use the platform&#8217;s native certificate store, especially if you&#8217;re relying on a corporate trust root (e.g., for a mandatory proxy) that&#8217;s included in your system&#8217;s certificate store.</p>

<p>May also be set with the <code>UV_NATIVE_TLS</code> environment variable.</p>
</dd><dt><code>--no-authors</code></dt><dd><p>Do not fill in the <code>authors</code> field in the <code>pyproject.toml</code></p>

</dd><dt><code>--no-cache</code>, <code>-n</code></dt><dd><p>Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation</p>

<p>May also be set with the <code>UV_NO_CACHE</code> environment variable.</p>
Expand Down
Loading