diff --git a/.python-versions b/.python-versions index f3d5240aa2911..f79a15eeeff8a 100644 --- a/.python-versions +++ b/.python-versions @@ -10,3 +10,5 @@ 3.9.12 # The following is needed for `==3.13` request tests 3.13.0 +# A pre-release version required for testing +3.14.0rc2 diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 217606ae13b42..aed735a431e5e 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -727,7 +727,7 @@ fn python_interpreters<'a>( cache: &'a Cache, preview: Preview, ) -> impl Iterator> + 'a { - python_interpreters_from_executables( + let interpreters = python_interpreters_from_executables( // Perform filtering on the discovered executables based on their source. This avoids // unnecessary interpreter queries, which are generally expensive. We'll filter again // with `interpreter_satisfies_environment_preference` after querying. @@ -761,7 +761,21 @@ fn python_interpreters<'a>( }) .filter_ok(move |(source, interpreter)| { satisfies_python_preference(*source, interpreter, preference) - }) + }); + + if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() { + Either::Left(interpreters.map_ok(|(source, interpreter)| { + // In test mode, change the source to `Managed` if a version was marked as such via + // `TestContext::with_versions_as_managed`. + if interpreter.is_managed() { + (PythonSource::Managed, interpreter) + } else { + (source, interpreter) + } + })) + } else { + Either::Right(interpreters) + } } /// Lazily convert Python executables into interpreters. diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 8f2a4a767a5ba..85ec65d78eda2 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -2434,7 +2434,7 @@ pub(crate) async fn init_script_python_requirement( ) -> anyhow::Result { let python_request = if let Some(request) = python { // (1) Explicit request from user - PythonRequest::parse(request) + Some(PythonRequest::parse(request)) } else if let (false, Some(request)) = ( no_pin_python, PythonVersionFile::discover( @@ -2445,14 +2445,14 @@ pub(crate) async fn init_script_python_requirement( .and_then(PythonVersionFile::into_version), ) { // (2) Request from `.python-version` - request + Some(request) } else { - // (3) Assume any Python version - PythonRequest::Any + // (3) No explicit request + None }; let interpreter = PythonInstallation::find_or_download( - Some(&python_request), + python_request.as_ref(), EnvironmentPreference::Any, python_preference, python_downloads, diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 3f374eada3ee6..2a8c2509ad047 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -901,6 +901,49 @@ fn init_script_shebang() -> Result<()> { Ok(()) } +// Make sure that `uv init --script` picks the latest non-pre-release version of Python +// for the `requires-python` constraint. +#[cfg(feature = "python-patch")] +#[test] +fn init_script_picks_latest_stable_version() -> Result<()> { + let managed_versions = &["3.14.0rc2", "3.13", "3.12"]; + // If we do not mark these versions as managed, they would have `PythonSource::SearchPath(First)`, which + // would mean that pre-releases would be preferred without opt-in (see `PythonSource::allows_prereleases`). + let context = + TestContext::new_with_versions(managed_versions).with_versions_as_managed(managed_versions); + + let script_path = context.temp_dir.join("main.py"); + + uv_snapshot!(context.filters(), context.init().arg("--script").arg("main.py"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized script at `main.py` + "#); + + let resulting_script = fs_err::read_to_string(&script_path)?; + assert_snapshot!( + resulting_script, @r#" + # /// script + # requires-python = ">=3.13" + # dependencies = [] + # /// + + + def main() -> None: + print("Hello from main.py!") + + + if __name__ == "__main__": + main() + "# + ); + + Ok(()) +} + /// Run `uv init --lib` with an existing py.typed file #[test] fn init_py_typed_exists() -> Result<()> { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 92ddd2a802e45..bdcb1537a49e9 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -12035,7 +12035,7 @@ fn install_python_preference() { ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Using CPython 3.12.[X] Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5e818f14553d1..cd5e5ae434768 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -12644,7 +12644,7 @@ fn sync_python_preference() -> Result<()> { ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Using CPython 3.12.[X] Removed virtual environment at: .venv Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -12698,7 +12698,7 @@ fn sync_python_preference() -> Result<()> { ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Using CPython 3.12.[X] Removed virtual environment at: .venv Creating virtual environment at: .venv Resolved 1 package in [TIME] diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index f5e542e9c3e74..deadad0bd8214 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -1359,7 +1359,7 @@ fn venv_python_preference() { ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Using CPython 3.12.[X] Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); @@ -1394,7 +1394,7 @@ fn venv_python_preference() { ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Using CPython 3.12.[X] Creating virtual environment at: .venv warning: A virtual environment already exists at `.venv`. In the future, uv will require `--clear` to replace it Activate with: source .venv/[BIN]/activate @@ -1406,7 +1406,7 @@ fn venv_python_preference() { ----- stdout ----- ----- stderr ----- - Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Using CPython 3.12.[X] Creating virtual environment at: .venv warning: A virtual environment already exists at `.venv`. In the future, uv will require `--clear` to replace it Activate with: source .venv/[BIN]/activate