diff --git a/crates/pep440-rs/src/version_specifier.rs b/crates/pep440-rs/src/version_specifier.rs index 7faf9f68ad55..fcb783bca813 100644 --- a/crates/pep440-rs/src/version_specifier.rs +++ b/crates/pep440-rs/src/version_specifier.rs @@ -392,6 +392,14 @@ impl VersionSpecifier { } } + /// `>=` + pub fn greater_than_equal_version(version: Version) -> Self { + Self { + operator: Operator::GreaterThanEqual, + version, + } + } + /// Get the operator, e.g. `>=` in `>= 2.0.0` pub fn operator(&self) -> &Operator { &self.operator diff --git a/crates/uv-distribution/src/workspace.rs b/crates/uv-distribution/src/workspace.rs index 46f8a4a63eb5..c09b84feb163 100644 --- a/crates/uv-distribution/src/workspace.rs +++ b/crates/uv-distribution/src/workspace.rs @@ -826,7 +826,7 @@ mod tests { "root": "[ROOT]/albatross-in-example/examples/bird-feeder", "project": { "name": "bird-feeder", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -861,7 +861,7 @@ mod tests { "root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", "project": { "name": "bird-feeder", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -895,7 +895,7 @@ mod tests { "root": "[ROOT]/albatross-root-workspace", "project": { "name": "albatross", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -904,7 +904,7 @@ mod tests { "root": "[ROOT]/albatross-root-workspace/packages/bird-feeder", "project": { "name": "bird-feeder", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -913,7 +913,7 @@ mod tests { "root": "[ROOT]/albatross-root-workspace/packages/seeds", "project": { "name": "seeds", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -953,7 +953,7 @@ mod tests { "root": "[ROOT]/albatross-virtual-workspace/packages/albatross", "project": { "name": "albatross", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -962,7 +962,7 @@ mod tests { "root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder", "project": { "name": "bird-feeder", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -971,7 +971,7 @@ mod tests { "root": "[ROOT]/albatross-virtual-workspace/packages/seeds", "project": { "name": "seeds", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" @@ -1005,7 +1005,7 @@ mod tests { "root": "[ROOT]/albatross-just-project", "project": { "name": "albatross", - "requires-python": null, + "requires-python": ">=3.12", "optional-dependencies": null }, "pyproject_toml": "[PYPROJECT_TOML]" diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index 551b1721cdb2..1ed5654d3d6e 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -209,16 +209,22 @@ impl Interpreter { Some(ExternallyManaged { error }) } - /// Returns the Python version. + /// Returns the `python_full_version` marker corresponding to this Python version. + #[inline] + pub fn python_full_version(&self) -> &StringVersion { + self.markers.python_full_version() + } + + /// Returns the full Python version. #[inline] pub fn python_version(&self) -> &Version { &self.markers.python_full_version().version } - /// Returns the `python_full_version` marker corresponding to this Python version. + /// Returns the full minor Python version. #[inline] - pub fn python_full_version(&self) -> &StringVersion { - self.markers.python_full_version() + pub fn python_minor_version(&self) -> Version { + Version::new(self.python_version().release().iter().take(2).copied()) } /// Return the major version of this Python version. diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 33a1dde145bb..908e19c0db12 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -1,8 +1,10 @@ use anstream::eprint; use anyhow::Result; +use std::borrow::Cow; use distribution_types::{IndexLocations, UnresolvedRequirementSpecification}; use install_wheel_rs::linker::LinkMode; +use pep440_rs::{VersionSpecifier, VersionSpecifiers}; use uv_cache::Cache; use uv_client::RegistryClientBuilder; use uv_configuration::{ @@ -91,11 +93,27 @@ pub(super) async fn do_lock( let source_trees = vec![]; let project_name = project.project_name().clone(); + // Determine the supported Python range. If no range is defined, and warn and default to the + // current minor version. + let requires_python = if let Some(requires_python) = + project.current_project().project().requires_python.as_ref() + { + Cow::Borrowed(requires_python) + } else { + let requires_python = VersionSpecifiers::from( + VersionSpecifier::greater_than_equal_version(venv.interpreter().python_minor_version()), + ); + warn_user!( + "No `requires-python` field found in `{}`. Defaulting to `{requires_python}`.", + project.current_project().project().name, + ); + Cow::Owned(requires_python) + }; + // Determine the tags, markers, and interpreter to use for resolution. let interpreter = venv.interpreter(); let tags = venv.interpreter().tags()?; let markers = venv.interpreter().markers(); - let requires_python = project.current_project().project().requires_python.as_ref(); // Initialize the registry client. // TODO(zanieb): Support client options e.g. offline, tls, etc. @@ -164,7 +182,7 @@ pub(super) async fn do_lock( interpreter, tags, None, - requires_python, + Some(&requires_python), &client, &flat_index, &index, diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 9d20d142eb27..43f64139801e 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -19,6 +19,7 @@ fn lock_wheel_registry() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["anyio==3.7.0"] "#, )?; @@ -41,6 +42,7 @@ fn lock_wheel_registry() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "anyio" @@ -143,6 +145,7 @@ fn lock_sdist_registry() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["source-distribution==0.0.1"] "#, )?; @@ -165,6 +168,7 @@ fn lock_sdist_registry() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "project" @@ -200,6 +204,7 @@ fn lock_sdist_git() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0.0.1"] "#, )?; @@ -222,6 +227,7 @@ fn lock_sdist_git() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "project" @@ -271,6 +277,7 @@ fn lock_wheel_url() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl"] "#, )?; @@ -293,6 +300,7 @@ fn lock_wheel_url() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "anyio" @@ -394,6 +402,7 @@ fn lock_sdist_url() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz"] "#, )?; @@ -416,6 +425,7 @@ fn lock_sdist_url() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "anyio" @@ -517,6 +527,7 @@ fn lock_extra() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["anyio==3.7.0"] [project.optional-dependencies] @@ -542,6 +553,7 @@ fn lock_extra() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "anyio" @@ -671,6 +683,7 @@ fn lock_preference() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["iniconfig<2"] "#, )?; @@ -693,6 +706,7 @@ fn lock_preference() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "iniconfig" @@ -721,6 +735,7 @@ fn lock_preference() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["iniconfig"] "#, )?; @@ -744,6 +759,7 @@ fn lock_preference() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "iniconfig" @@ -785,6 +801,7 @@ fn lock_preference() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "iniconfig" @@ -822,6 +839,7 @@ fn lock_git_sha() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979"] "#, )?; @@ -844,6 +862,7 @@ fn lock_git_sha() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "project" @@ -876,6 +895,7 @@ fn lock_git_sha() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.12" dependencies = ["uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@main"] "#, )?; @@ -900,6 +920,7 @@ fn lock_git_sha() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "project" @@ -942,6 +963,7 @@ fn lock_git_sha() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.12" [[distribution]] name = "project" diff --git a/crates/uv/tests/lock_scenarios.rs b/crates/uv/tests/lock_scenarios.rs index 9fea023564ab..2aaecd43b6bd 100644 --- a/crates/uv/tests/lock_scenarios.rs +++ b/crates/uv/tests/lock_scenarios.rs @@ -44,6 +44,7 @@ fn fork_basic() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.8" dependencies = [ '''fork-basic-a>=2; sys_platform == "linux"''', '''fork-basic-a<2; sys_platform == "darwin"''', @@ -72,6 +73,7 @@ fn fork_basic() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.8" [[distribution]] name = "package-a" @@ -151,6 +153,7 @@ fn fork_marker_accrue() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.8" dependencies = [ '''fork-marker-accrue-a==1.0.0; implementation_name == "cpython"''', '''fork-marker-accrue-b==1.0.0; implementation_name == "pypy"''', @@ -179,6 +182,7 @@ fn fork_marker_accrue() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.8" [[distribution]] name = "package-a" @@ -277,6 +281,7 @@ fn fork_marker_selection() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.8" dependencies = [ '''fork-marker-selection-a''', '''fork-marker-selection-b>=2; sys_platform == "linux"''', @@ -306,6 +311,7 @@ fn fork_marker_selection() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.8" [[distribution]] name = "package-a" @@ -426,6 +432,7 @@ fn fork_marker_track() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.8" dependencies = [ '''fork-marker-track-a''', '''fork-marker-track-b>=2.8; sys_platform == "linux"''', @@ -455,6 +462,7 @@ fn fork_marker_track() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.8" [[distribution]] name = "package-a" @@ -575,6 +583,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.8" dependencies = [ '''fork-non-fork-marker-transitive-a==1.0.0''', '''fork-non-fork-marker-transitive-b==1.0.0''', @@ -603,6 +612,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.8" [[distribution]] name = "package-a" @@ -698,6 +708,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.8" dependencies = [ '''fork-non-local-fork-marker-direct-a==1.0.0; sys_platform == "linux"''', '''fork-non-local-fork-marker-direct-b==1.0.0; sys_platform == "darwin"''', @@ -771,6 +782,7 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { [project] name = "project" version = "0.1.0" + requires-python = ">=3.8" dependencies = [ '''fork-non-local-fork-marker-transitive-a==1.0.0''', '''fork-non-local-fork-marker-transitive-b==1.0.0''', diff --git a/scripts/workspaces/albatross-in-example/examples/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-in-example/examples/bird-feeder/pyproject.toml index 0bb570bb3fb9..3caf10f0877d 100644 --- a/scripts/workspaces/albatross-in-example/examples/bird-feeder/pyproject.toml +++ b/scripts/workspaces/albatross-in-example/examples/bird-feeder/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "bird-feeder" version = "1.0.0" +requires-python = ">=3.12" dependencies = ["anyio>=4.3.0,<5"] [build-system] diff --git a/scripts/workspaces/albatross-in-example/pyproject.toml b/scripts/workspaces/albatross-in-example/pyproject.toml index 9a61c9de0c1a..5780cc868203 100644 --- a/scripts/workspaces/albatross-in-example/pyproject.toml +++ b/scripts/workspaces/albatross-in-example/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "albatross" version = "0.1.0" +requires-python = ">=3.12" dependencies = ["tqdm>=4,<5"] [build-system] diff --git a/scripts/workspaces/albatross-just-project/pyproject.toml b/scripts/workspaces/albatross-just-project/pyproject.toml index 9a61c9de0c1a..5780cc868203 100644 --- a/scripts/workspaces/albatross-just-project/pyproject.toml +++ b/scripts/workspaces/albatross-just-project/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "albatross" version = "0.1.0" +requires-python = ">=3.12" dependencies = ["tqdm>=4,<5"] [build-system] diff --git a/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/pyproject.toml index 0bb570bb3fb9..3caf10f0877d 100644 --- a/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/pyproject.toml +++ b/scripts/workspaces/albatross-project-in-excluded/excluded/bird-feeder/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "bird-feeder" version = "1.0.0" +requires-python = ">=3.12" dependencies = ["anyio>=4.3.0,<5"] [build-system] diff --git a/scripts/workspaces/albatross-project-in-excluded/pyproject.toml b/scripts/workspaces/albatross-project-in-excluded/pyproject.toml index de024d923c2f..8a0702e25fd5 100644 --- a/scripts/workspaces/albatross-project-in-excluded/pyproject.toml +++ b/scripts/workspaces/albatross-project-in-excluded/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "albatross" version = "0.1.0" +requires-python = ">=3.12" dependencies = ["tqdm>=4,<5"] [tool.uv.workspace] diff --git a/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/pyproject.toml index dc16cf9cb4f3..0d42072bea1d 100644 --- a/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/pyproject.toml +++ b/scripts/workspaces/albatross-root-workspace/packages/bird-feeder/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "bird-feeder" version = "1.0.0" +requires-python = ">=3.12" dependencies = ["anyio>=4.3.0,<5", "seeds"] [tool.uv.sources] diff --git a/scripts/workspaces/albatross-root-workspace/packages/seeds/pyproject.toml b/scripts/workspaces/albatross-root-workspace/packages/seeds/pyproject.toml index 4851f107879b..f63b97736abc 100644 --- a/scripts/workspaces/albatross-root-workspace/packages/seeds/pyproject.toml +++ b/scripts/workspaces/albatross-root-workspace/packages/seeds/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "seeds" version = "1.0.0" +requires-python = ">=3.12" dependencies = ["idna==3.6"] [build-system] diff --git a/scripts/workspaces/albatross-root-workspace/pyproject.toml b/scripts/workspaces/albatross-root-workspace/pyproject.toml index 8dab9ce0a945..3075d1bdc710 100644 --- a/scripts/workspaces/albatross-root-workspace/pyproject.toml +++ b/scripts/workspaces/albatross-root-workspace/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "albatross" version = "0.1.0" +requires-python = ">=3.12" dependencies = ["bird-feeder", "tqdm>=4,<5"] [tool.uv.sources] diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/albatross/pyproject.toml b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/pyproject.toml index 74fd3a1ebdaf..757a4bf5c56c 100644 --- a/scripts/workspaces/albatross-virtual-workspace/packages/albatross/pyproject.toml +++ b/scripts/workspaces/albatross-virtual-workspace/packages/albatross/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "albatross" version = "0.1.0" +requires-python = ">=3.12" dependencies = ["bird-feeder", "tqdm>=4,<5"] [tool.uv.sources] diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/pyproject.toml b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/pyproject.toml index dc16cf9cb4f3..0d42072bea1d 100644 --- a/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/pyproject.toml +++ b/scripts/workspaces/albatross-virtual-workspace/packages/bird-feeder/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "bird-feeder" version = "1.0.0" +requires-python = ">=3.12" dependencies = ["anyio>=4.3.0,<5", "seeds"] [tool.uv.sources] diff --git a/scripts/workspaces/albatross-virtual-workspace/packages/seeds/pyproject.toml b/scripts/workspaces/albatross-virtual-workspace/packages/seeds/pyproject.toml index 4851f107879b..f63b97736abc 100644 --- a/scripts/workspaces/albatross-virtual-workspace/packages/seeds/pyproject.toml +++ b/scripts/workspaces/albatross-virtual-workspace/packages/seeds/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "seeds" version = "1.0.0" +requires-python = ">=3.12" dependencies = ["idna==3.6"] [build-system]