diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 885bb2508992c..37acc18a40777 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2184,6 +2184,14 @@ pub struct InitArgs { #[arg(long)] pub no_readme: 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 + /// the discovered Python interpreter, which will cause subsequent uv commands to use that + /// version. + #[arg(long)] + pub no_pin_python: bool, + /// Avoid discovering a workspace and create a standalone project. /// /// By default, uv searches for workspaces in the current directory or any diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 7e3e3d3b05310..9e51959f53636 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -30,6 +30,7 @@ pub(crate) async fn init( package: bool, project_kind: InitProjectKind, no_readme: bool, + no_pin_python: bool, python: Option, no_workspace: bool, python_preference: PythonPreference, @@ -73,6 +74,7 @@ pub(crate) async fn init( package, project_kind, no_readme, + no_pin_python, python, no_workspace, python_preference, @@ -121,6 +123,7 @@ async fn init_project( package: bool, project_kind: InitProjectKind, no_readme: bool, + no_pin_python: bool, python: Option, no_workspace: bool, python_preference: PythonPreference, @@ -181,27 +184,67 @@ async fn init_project( // Add a `requires-python` field to the `pyproject.toml` and return the corresponding interpreter. let (requires_python, python_request) = if let Some(request) = python.as_deref() { // (1) Explicit request from user - let python_request = PythonRequest::parse(request); - - // Translate to a `requires-python` specifier. - let requires_python = match PythonRequest::parse(request) { + match PythonRequest::parse(request) { PythonRequest::Version(VersionRequest::MajorMinor(major, minor)) => { - RequiresPython::greater_than_equal_version(&Version::new([ + let requires_python = RequiresPython::greater_than_equal_version(&Version::new([ u64::from(major), u64::from(minor), - ])) + ])); + + let python_request = if no_pin_python { + None + } else { + Some(PythonRequest::Version(VersionRequest::MajorMinor( + major, minor, + ))) + }; + + (requires_python, python_request) } PythonRequest::Version(VersionRequest::MajorMinorPatch(major, minor, patch)) => { - RequiresPython::greater_than_equal_version(&Version::new([ + let requires_python = RequiresPython::greater_than_equal_version(&Version::new([ u64::from(major), u64::from(minor), u64::from(patch), - ])) + ])); + + let python_request = if no_pin_python { + None + } else { + Some(PythonRequest::Version(VersionRequest::MajorMinorPatch( + major, minor, patch, + ))) + }; + + (requires_python, python_request) } - PythonRequest::Version(VersionRequest::Range(specifiers)) => { - RequiresPython::from_specifiers(&specifiers)? + ref python_request @ PythonRequest::Version(VersionRequest::Range(ref specifiers)) => { + let requires_python = RequiresPython::from_specifiers(specifiers)?; + + let python_request = if no_pin_python { + None + } else { + let interpreter = PythonInstallation::find_or_download( + Some(python_request), + EnvironmentPreference::Any, + python_preference, + python_downloads, + &client_builder, + cache, + Some(&reporter), + ) + .await? + .into_interpreter(); + + Some(PythonRequest::Version(VersionRequest::MajorMinor( + interpreter.python_major(), + interpreter.python_minor(), + ))) + }; + + (requires_python, python_request) } - _ => { + python_request => { let interpreter = PythonInstallation::find_or_download( Some(&python_request), EnvironmentPreference::Any, @@ -213,11 +256,22 @@ async fn init_project( ) .await? .into_interpreter(); - RequiresPython::greater_than_equal_version(&interpreter.python_minor_version()) - } - }; - (requires_python, python_request) + let requires_python = + RequiresPython::greater_than_equal_version(&interpreter.python_minor_version()); + + let python_request = if no_pin_python { + None + } else { + Some(PythonRequest::Version(VersionRequest::MajorMinor( + interpreter.python_major(), + interpreter.python_minor(), + ))) + }; + + (requires_python, python_request) + } + } } else if let Some(requires_python) = workspace .as_ref() .and_then(|workspace| find_requires_python(workspace).ok().flatten()) @@ -226,6 +280,28 @@ async fn init_project( let python_request = PythonRequest::Version(VersionRequest::Range(requires_python.specifiers().clone())); + // Pin to the minor version. + let python_request = if no_pin_python { + None + } else { + let interpreter = PythonInstallation::find_or_download( + Some(&python_request), + EnvironmentPreference::Any, + python_preference, + python_downloads, + &client_builder, + cache, + Some(&reporter), + ) + .await? + .into_interpreter(); + + Some(PythonRequest::Version(VersionRequest::MajorMinor( + interpreter.python_major(), + interpreter.python_minor(), + ))) + }; + (requires_python, python_request) } else { // (3) Default to the system Python @@ -244,12 +320,15 @@ async fn init_project( let requires_python = RequiresPython::greater_than_equal_version(&interpreter.python_minor_version()); - // If no `--python` is specified, pin to the patch version of the discovered interpreter. - let python_request = PythonRequest::Version(VersionRequest::MajorMinorPatch( - interpreter.python_major(), - interpreter.python_minor(), - interpreter.python_patch(), - )); + // Pin to the minor version. + let python_request = if no_pin_python { + None + } else { + Some(PythonRequest::Version(VersionRequest::MajorMinor( + interpreter.python_major(), + interpreter.python_minor(), + ))) + }; (requires_python, python_request) }; @@ -259,7 +338,7 @@ async fn init_project( name, path, &requires_python, - &python_request, + python_request.as_ref(), no_readme, package, ) @@ -322,7 +401,7 @@ impl InitProjectKind { name: &PackageName, path: &Path, requires_python: &RequiresPython, - python_request: &PythonRequest, + python_request: Option<&PythonRequest>, no_readme: bool, package: bool, ) -> Result<()> { @@ -362,7 +441,7 @@ impl InitProjectKind { name: &PackageName, path: &Path, requires_python: &RequiresPython, - python_request: &PythonRequest, + python_request: Option<&PythonRequest>, no_readme: bool, package: bool, ) -> Result<()> { @@ -418,14 +497,16 @@ impl InitProjectKind { fs_err::write(path.join("pyproject.toml"), pyproject)?; // Write .python-version if it doesn't exist. - if PythonVersionFile::discover(path, false, false) - .await? - .is_none() - { - PythonVersionFile::new(path.join(".python-version")) - .with_versions(vec![python_request.clone()]) - .write() - .await?; + if let Some(python_request) = python_request { + if PythonVersionFile::discover(path, false, false) + .await? + .is_none() + { + PythonVersionFile::new(path.join(".python-version")) + .with_versions(vec![python_request.clone()]) + .write() + .await?; + } } Ok(()) @@ -436,7 +517,7 @@ impl InitProjectKind { name: &PackageName, path: &Path, requires_python: &RequiresPython, - python_request: &PythonRequest, + python_request: Option<&PythonRequest>, no_readme: bool, package: bool, ) -> Result<()> { @@ -469,14 +550,16 @@ impl InitProjectKind { } // Write .python-version if it doesn't exist. - if PythonVersionFile::discover(path, false, false) - .await? - .is_none() - { - PythonVersionFile::new(path.join(".python-version")) - .with_versions(vec![python_request.clone()]) - .write() - .await?; + if let Some(python_request) = python_request { + if PythonVersionFile::discover(path, false, false) + .await? + .is_none() + { + PythonVersionFile::new(path.join(".python-version")) + .with_versions(vec![python_request.clone()]) + .write() + .await?; + } } Ok(()) diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 92d470953d95a..c841433e3294c 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1028,6 +1028,7 @@ async fn run_project( args.package, args.kind, args.no_readme, + args.no_pin_python, args.python, args.no_workspace, globals.python_preference, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 55302c5005312..95ddb3514355c 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -157,6 +157,7 @@ pub(crate) struct InitSettings { pub(crate) package: bool, pub(crate) kind: InitProjectKind, pub(crate) no_readme: bool, + pub(crate) no_pin_python: bool, pub(crate) no_workspace: bool, pub(crate) python: Option, } @@ -174,6 +175,7 @@ impl InitSettings { app, lib, no_readme, + no_pin_python, no_workspace, python, } = args; @@ -193,6 +195,7 @@ impl InitSettings { package, kind, no_readme, + no_pin_python, no_workspace, python, } diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/init.rs index 0abafa8de4b15..a1f1a2b2cf048 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/init.rs @@ -59,7 +59,7 @@ fn init() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - python_version, @"3.12.[X]" + python_version, @"3.12" ); }); @@ -1633,7 +1633,7 @@ fn init_requires_python_workspace() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - python_version, @">=3.10" + python_version, @"3.12" ); }); @@ -1702,7 +1702,7 @@ fn init_requires_python_version() -> Result<()> { /// specifiers verbatim. #[test] fn init_requires_python_specifiers() -> Result<()> { - let context = TestContext::new("3.12"); + let context = TestContext::new_with_versions(&["3.8", "3.12"]); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! { @@ -1750,7 +1750,7 @@ fn init_requires_python_specifiers() -> Result<()> { filters => context.filters(), }, { assert_snapshot!( - python_version, @"==3.8.*" + python_version, @"3.8" ); }); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 254d497c8185c..4ba82d500610b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -453,6 +453,10 @@ uv init [OPTIONS] [PATH]

This is the default behavior when using --app.

+
--no-pin-python

Do not create a .python-version file for the project.

+ +

By default, uv will create a .python-version file containing the minor version of the discovered Python interpreter, which will cause subsequent uv commands to use that version.

+
--no-progress

Hide all progress outputs.

For example, spinners or progress bars.