mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 02:48:17 +00:00
Discover and respect .python-version
files in parent directories (#6370)
Uses #6369 for test coverage. Updates version file discovery to search up into parent directories. Also refactors Python request determination to avoid duplicating the user request / version file / workspace lookup logic in every command (this supersedes the work started in https://github.com/astral-sh/uv/pull/6372). There is a bit of remaining work here, mostly around documentation. There are some edge-cases where we don't use the refactored request utility, like `uv build` — I'm not sure how I'm going to handle that yet as it needs a separate root directory.
This commit is contained in:
parent
fb89b64acf
commit
8ef5949294
23 changed files with 807 additions and 252 deletions
|
@ -17,6 +17,7 @@ pub use crate::prefix::Prefix;
|
|||
pub use crate::python_version::PythonVersion;
|
||||
pub use crate::target::Target;
|
||||
pub use crate::version_files::{
|
||||
DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
|
||||
PythonVersionFile, PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME,
|
||||
};
|
||||
pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
|||
use fs_err as fs;
|
||||
use itertools::Itertools;
|
||||
use tracing::debug;
|
||||
use uv_fs::Simplified;
|
||||
|
||||
use crate::PythonRequest;
|
||||
|
||||
|
@ -22,38 +23,91 @@ pub struct PythonVersionFile {
|
|||
versions: Vec<PythonRequest>,
|
||||
}
|
||||
|
||||
/// Whether to prefer the `.python-version` or `.python-versions` file.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum FilePreference {
|
||||
#[default]
|
||||
Version,
|
||||
Versions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DiscoveryOptions<'a> {
|
||||
/// The path to stop discovery at.
|
||||
stop_discovery_at: Option<&'a Path>,
|
||||
/// When `no_config` is set, Python version files will be ignored.
|
||||
///
|
||||
/// Discovery will still run in order to display a log about the ignored file.
|
||||
no_config: bool,
|
||||
preference: FilePreference,
|
||||
}
|
||||
|
||||
impl<'a> DiscoveryOptions<'a> {
|
||||
#[must_use]
|
||||
pub fn with_no_config(self, no_config: bool) -> Self {
|
||||
Self { no_config, ..self }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_preference(self, preference: FilePreference) -> Self {
|
||||
Self { preference, ..self }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_stop_discovery_at(self, stop_discovery_at: Option<&'a Path>) -> Self {
|
||||
Self {
|
||||
stop_discovery_at,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PythonVersionFile {
|
||||
/// Find a Python version file in the given directory.
|
||||
/// Find a Python version file in the given directory or any of its parents.
|
||||
pub async fn discover(
|
||||
working_directory: impl AsRef<Path>,
|
||||
// TODO(zanieb): Create a `DiscoverySettings` struct for these options
|
||||
no_config: bool,
|
||||
prefer_versions: bool,
|
||||
options: &DiscoveryOptions<'_>,
|
||||
) -> Result<Option<Self>, std::io::Error> {
|
||||
let versions_path = working_directory.as_ref().join(PYTHON_VERSIONS_FILENAME);
|
||||
let version_path = working_directory.as_ref().join(PYTHON_VERSION_FILENAME);
|
||||
let Some(path) = Self::find_nearest(working_directory, options) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if no_config {
|
||||
if version_path.exists() {
|
||||
debug!("Ignoring `.python-version` file due to `--no-config`");
|
||||
} else if versions_path.exists() {
|
||||
debug!("Ignoring `.python-versions` file due to `--no-config`");
|
||||
};
|
||||
if options.no_config {
|
||||
debug!(
|
||||
"Ignoring Python version file at `{}` due to `--no-config`",
|
||||
path.user_display()
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let paths = if prefer_versions {
|
||||
[versions_path, version_path]
|
||||
} else {
|
||||
[version_path, versions_path]
|
||||
};
|
||||
for path in paths {
|
||||
if let Some(result) = Self::try_from_path(path).await? {
|
||||
return Ok(Some(result));
|
||||
};
|
||||
}
|
||||
// Uses `try_from_path` instead of `from_path` to avoid TOCTOU failures.
|
||||
Self::try_from_path(path).await
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
fn find_nearest(path: impl AsRef<Path>, options: &DiscoveryOptions<'_>) -> Option<PathBuf> {
|
||||
path.as_ref()
|
||||
.ancestors()
|
||||
.take_while(|path| {
|
||||
// Only walk up the given directory, if any.
|
||||
options
|
||||
.stop_discovery_at
|
||||
.and_then(Path::parent)
|
||||
.map(|stop_discovery_at| stop_discovery_at != *path)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.find_map(|path| Self::find_in_directory(path, options))
|
||||
}
|
||||
|
||||
fn find_in_directory(path: &Path, options: &DiscoveryOptions<'_>) -> Option<PathBuf> {
|
||||
let version_path = path.join(PYTHON_VERSION_FILENAME);
|
||||
let versions_path = path.join(PYTHON_VERSIONS_FILENAME);
|
||||
|
||||
let paths = match options.preference {
|
||||
FilePreference::Versions => [versions_path, version_path],
|
||||
FilePreference::Version => [version_path, versions_path],
|
||||
};
|
||||
|
||||
paths.into_iter().find(|path| path.is_file())
|
||||
}
|
||||
|
||||
/// Try to read a Python version file at the given path.
|
||||
|
@ -62,7 +116,10 @@ impl PythonVersionFile {
|
|||
pub async fn try_from_path(path: PathBuf) -> Result<Option<Self>, std::io::Error> {
|
||||
match fs::tokio::read_to_string(&path).await {
|
||||
Ok(content) => {
|
||||
debug!("Reading requests from `{}`", path.display());
|
||||
debug!(
|
||||
"Reading Python requests from version file at `{}`",
|
||||
path.display()
|
||||
);
|
||||
let versions = content
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
|
@ -104,7 +161,7 @@ impl PythonVersionFile {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the first version declared in the file, if any.
|
||||
/// Return the first request declared in the file, if any.
|
||||
pub fn version(&self) -> Option<&PythonRequest> {
|
||||
self.versions.first()
|
||||
}
|
||||
|
|
|
@ -44,10 +44,19 @@ impl Pep723Item {
|
|||
Self::Remote(metadata) => metadata,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the path of the PEP 723 item, if any.
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::Script(script) => Some(&script.path),
|
||||
Self::Stdin(_) => None,
|
||||
Self::Remote(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A PEP 723 script, including its [`Pep723Metadata`].
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pep723Script {
|
||||
/// The path to the Python script.
|
||||
pub path: PathBuf,
|
||||
|
@ -188,7 +197,7 @@ impl Pep723Script {
|
|||
/// PEP 723 metadata as parsed from a `script` comment block.
|
||||
///
|
||||
/// See: <https://peps.python.org/pep-0723/>
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Pep723Metadata {
|
||||
pub dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
|
||||
|
@ -248,13 +257,13 @@ impl FromStr for Pep723Metadata {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Tool {
|
||||
pub uv: Option<ToolUv>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ToolUv {
|
||||
#[serde(flatten)]
|
||||
|
|
|
@ -925,6 +925,7 @@ impl ProjectWorkspace {
|
|||
// Only walk up the given directory, if any.
|
||||
options
|
||||
.stop_discovery_at
|
||||
.and_then(Path::parent)
|
||||
.map(|stop_discovery_at| stop_discovery_at != *path)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
|
@ -1127,6 +1128,7 @@ async fn find_workspace(
|
|||
// Only walk up the given directory, if any.
|
||||
options
|
||||
.stop_discovery_at
|
||||
.and_then(Path::parent)
|
||||
.map(|stop_discovery_at| stop_discovery_at != *path)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
|
@ -1219,6 +1221,7 @@ pub fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryO
|
|||
// Only walk up the given directory, if any.
|
||||
options
|
||||
.stop_discovery_at
|
||||
.and_then(Path::parent)
|
||||
.map(|stop_discovery_at| stop_discovery_at != *path)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
|
@ -1385,6 +1388,7 @@ impl VirtualProject {
|
|||
// Only walk up the given directory, if any.
|
||||
options
|
||||
.stop_discovery_at
|
||||
.and_then(Path::parent)
|
||||
.map(|stop_discovery_at| stop_discovery_at != *path)
|
||||
.unwrap_or(true)
|
||||
})
|
||||
|
|
|
@ -23,7 +23,8 @@ use uv_fs::Simplified;
|
|||
use uv_normalize::PackageName;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionRequest,
|
||||
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions,
|
||||
VersionRequest,
|
||||
};
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython};
|
||||
|
@ -391,9 +392,12 @@ async fn build_package(
|
|||
|
||||
// (2) Request from `.python-version`
|
||||
if interpreter_request.is_none() {
|
||||
interpreter_request = PythonVersionFile::discover(source.directory(), no_config, false)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version);
|
||||
interpreter_request = PythonVersionFile::discover(
|
||||
source.directory(),
|
||||
&VersionFileDiscoveryOptions::default().with_no_config(no_config),
|
||||
)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version);
|
||||
}
|
||||
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
|
|
|
@ -27,11 +27,11 @@ use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl};
|
|||
use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl};
|
||||
use uv_python::{
|
||||
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionRequest,
|
||||
PythonPreference, PythonRequest,
|
||||
};
|
||||
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
|
||||
use uv_resolver::{FlatIndex, InstallTarget};
|
||||
use uv_scripts::Pep723Script;
|
||||
use uv_scripts::{Pep723Item, Pep723Script};
|
||||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::pyproject::{DependencyType, Source, SourceError};
|
||||
|
@ -44,7 +44,9 @@ use crate::commands::pip::loggers::{
|
|||
use crate::commands::pip::operations::Modifications;
|
||||
use crate::commands::pip::resolution_environment;
|
||||
use crate::commands::project::lock::LockMode;
|
||||
use crate::commands::project::{script_python_requirement, ProjectError};
|
||||
use crate::commands::project::{
|
||||
init_script_python_requirement, validate_script_requires_python, ProjectError, ScriptPython,
|
||||
};
|
||||
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
|
||||
use crate::commands::{diagnostics, pip, project, ExitStatus, SharedState};
|
||||
use crate::printer::Printer;
|
||||
|
@ -76,6 +78,7 @@ pub(crate) async fn add(
|
|||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
@ -134,12 +137,13 @@ pub(crate) async fn add(
|
|||
let script = if let Some(script) = Pep723Script::read(&script).await? {
|
||||
script
|
||||
} else {
|
||||
let requires_python = script_python_requirement(
|
||||
let requires_python = init_script_python_requirement(
|
||||
python.as_deref(),
|
||||
project_dir,
|
||||
false,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
no_config,
|
||||
&client_builder,
|
||||
cache,
|
||||
&reporter,
|
||||
|
@ -148,28 +152,17 @@ pub(crate) async fn add(
|
|||
Pep723Script::init(&script, requires_python.specifiers()).await?
|
||||
};
|
||||
|
||||
let python_request = if let Some(request) = python.as_deref() {
|
||||
// (1) Explicit request from user
|
||||
Some(PythonRequest::parse(request))
|
||||
} else if let Some(request) = PythonVersionFile::discover(project_dir, false, false)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version)
|
||||
{
|
||||
// (2) Request from `.python-version`
|
||||
Some(request)
|
||||
} else {
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
script
|
||||
.metadata
|
||||
.requires_python
|
||||
.clone()
|
||||
.map(|requires_python| {
|
||||
PythonRequest::Version(VersionRequest::Range(
|
||||
requires_python,
|
||||
PythonVariant::Default,
|
||||
))
|
||||
})
|
||||
};
|
||||
let ScriptPython {
|
||||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = ScriptPython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
None,
|
||||
&Pep723Item::Script(script.clone()),
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let interpreter = PythonInstallation::find_or_download(
|
||||
python_request.as_ref(),
|
||||
|
@ -183,6 +176,16 @@ pub(crate) async fn add(
|
|||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
if let Some((requires_python, requires_python_source)) = requires_python {
|
||||
validate_script_requires_python(
|
||||
&interpreter,
|
||||
None,
|
||||
&requires_python,
|
||||
&requires_python_source,
|
||||
&source,
|
||||
)?;
|
||||
}
|
||||
|
||||
Target::Script(script, Box::new(interpreter))
|
||||
} else {
|
||||
// Find the project in the workspace.
|
||||
|
@ -221,6 +224,7 @@ pub(crate) async fn add(
|
|||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -49,6 +49,7 @@ pub(crate) async fn export(
|
|||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
quiet: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
|
@ -99,12 +100,14 @@ pub(crate) async fn export(
|
|||
// Find an interpreter for the project
|
||||
interpreter = ProjectInterpreter::discover(
|
||||
project.workspace(),
|
||||
project_dir,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@ use uv_pep440::Version;
|
|||
use uv_pep508::PackageName;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
|
||||
PythonVariant, PythonVersionFile, VersionRequest,
|
||||
PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions, VersionRequest,
|
||||
};
|
||||
use uv_resolver::RequiresPython;
|
||||
use uv_scripts::{Pep723Script, ScriptTag};
|
||||
|
@ -26,7 +26,7 @@ use uv_warnings::warn_user_once;
|
|||
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
|
||||
use uv_workspace::{DiscoveryOptions, MemberDiscovery, Workspace, WorkspaceError};
|
||||
|
||||
use crate::commands::project::{find_requires_python, script_python_requirement};
|
||||
use crate::commands::project::{find_requires_python, init_script_python_requirement};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
@ -51,6 +51,7 @@ pub(crate) async fn init(
|
|||
connectivity: Connectivity,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
@ -75,6 +76,7 @@ pub(crate) async fn init(
|
|||
package,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -131,6 +133,7 @@ pub(crate) async fn init(
|
|||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -183,6 +186,7 @@ async fn init_script(
|
|||
package: bool,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
) -> Result<()> {
|
||||
if no_workspace {
|
||||
warn_user_once!("`--no-workspace` is a no-op for Python scripts, which are standalone");
|
||||
|
@ -226,12 +230,13 @@ async fn init_script(
|
|||
}
|
||||
};
|
||||
|
||||
let requires_python = script_python_requirement(
|
||||
let requires_python = init_script_python_requirement(
|
||||
python.as_deref(),
|
||||
&CWD,
|
||||
no_pin_python,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
no_config,
|
||||
&client_builder,
|
||||
cache,
|
||||
&reporter,
|
||||
|
@ -266,6 +271,7 @@ async fn init_project(
|
|||
connectivity: Connectivity,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
|
@ -318,10 +324,35 @@ async fn init_project(
|
|||
.native_tls(native_tls)
|
||||
.allow_insecure_host(allow_insecure_host.to_vec());
|
||||
|
||||
// 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() {
|
||||
// First, determine if there is an request for Python
|
||||
let python_request = if let Some(request) = python {
|
||||
// (1) Explicit request from user
|
||||
match PythonRequest::parse(request) {
|
||||
Some(PythonRequest::parse(&request))
|
||||
} else if let Some(file) = PythonVersionFile::discover(
|
||||
path,
|
||||
&VersionFileDiscoveryOptions::default()
|
||||
.with_stop_discovery_at(
|
||||
workspace
|
||||
.as_ref()
|
||||
.map(Workspace::install_path)
|
||||
.map(PathBuf::as_ref),
|
||||
)
|
||||
.with_no_config(no_config),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
// (2) Request from `.python-version`
|
||||
file.into_version()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Add a `requires-python` field to the `pyproject.toml` and return the corresponding interpreter.
|
||||
let (requires_python, python_request) = if let Some(python_request) = python_request {
|
||||
// (1) A request from the user or `.python-version` file
|
||||
// This can be arbitrary, i.e., not a version — in which case we may need to resolve the
|
||||
// interpreter
|
||||
match python_request {
|
||||
PythonRequest::Version(VersionRequest::MajorMinor(
|
||||
major,
|
||||
minor,
|
||||
|
@ -427,7 +458,7 @@ async fn init_project(
|
|||
}
|
||||
}
|
||||
} else if let Some(requires_python) = workspace.as_ref().and_then(find_requires_python) {
|
||||
// (2) `Requires-Python` from the workspace
|
||||
// (2) `requires-python` from the workspace
|
||||
let python_request = PythonRequest::Version(VersionRequest::Range(
|
||||
requires_python.specifiers().clone(),
|
||||
PythonVariant::Default,
|
||||
|
@ -687,7 +718,7 @@ impl InitProjectKind {
|
|||
|
||||
// Write .python-version if it doesn't exist.
|
||||
if let Some(python_request) = python_request {
|
||||
if PythonVersionFile::discover(path, false, false)
|
||||
if PythonVersionFile::discover(path, &VersionFileDiscoveryOptions::default())
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
|
@ -741,7 +772,7 @@ impl InitProjectKind {
|
|||
|
||||
// Write .python-version if it doesn't exist.
|
||||
if let Some(python_request) = python_request {
|
||||
if PythonVersionFile::discover(path, false, false)
|
||||
if PythonVersionFile::discover(path, &VersionFileDiscoveryOptions::default())
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
|
|
|
@ -84,6 +84,7 @@ pub(crate) async fn lock(
|
|||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<ExitStatus> {
|
||||
|
@ -98,12 +99,14 @@ pub(crate) async fn lock(
|
|||
// Find an interpreter for the project
|
||||
interpreter = ProjectInterpreter::discover(
|
||||
&workspace,
|
||||
project_dir,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@ use uv_distribution::DistributionDatabase;
|
|||
use uv_distribution_types::{
|
||||
Index, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_fs::Simplified;
|
||||
use uv_fs::{Simplified, CWD};
|
||||
use uv_git::ResolvedRepositoryReference;
|
||||
use uv_installer::{SatisfiesResult, SitePackages};
|
||||
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
|
||||
|
@ -26,7 +26,7 @@ use uv_pypi_types::Requirement;
|
|||
use uv_python::{
|
||||
EnvironmentPreference, Interpreter, InvalidEnvironmentKind, PythonDownloads, PythonEnvironment,
|
||||
PythonInstallation, PythonPreference, PythonRequest, PythonVariant, PythonVersionFile,
|
||||
VersionRequest,
|
||||
VersionFileDiscoveryOptions, VersionRequest,
|
||||
};
|
||||
use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements};
|
||||
use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification};
|
||||
|
@ -34,6 +34,7 @@ use uv_resolver::{
|
|||
FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython, ResolutionGraph,
|
||||
ResolverEnvironment,
|
||||
};
|
||||
use uv_scripts::Pep723Item;
|
||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
|
@ -89,13 +90,13 @@ pub(crate) enum ProjectError {
|
|||
RequiresPythonProjectIncompatibility(Version, RequiresPython),
|
||||
|
||||
#[error("The requested interpreter resolved to Python {0}, which is incompatible with the script's Python requirement: `{1}`")]
|
||||
RequestedPythonScriptIncompatibility(Version, VersionSpecifiers),
|
||||
RequestedPythonScriptIncompatibility(Version, RequiresPython),
|
||||
|
||||
#[error("The Python request from `{0}` resolved to Python {1}, which is incompatible with the script's Python requirement: `{2}`")]
|
||||
DotPythonVersionScriptIncompatibility(String, Version, VersionSpecifiers),
|
||||
DotPythonVersionScriptIncompatibility(String, Version, RequiresPython),
|
||||
|
||||
#[error("The resolved Python interpreter (Python {0}) is incompatible with the script's Python requirement: `{1}`")]
|
||||
RequiresPythonScriptIncompatibility(Version, VersionSpecifiers),
|
||||
RequiresPythonScriptIncompatibility(Version, RequiresPython),
|
||||
|
||||
#[error("The requested interpreter resolved to Python {0}, which is incompatible with the project's Python requirement: `{1}`. However, a workspace member (`{member}`) supports Python {3}. To install the workspace member on its own, navigate to `{path}`, then run `{venv}` followed by `{install}`.", member = _2.cyan(), venv = format!("uv venv --python {_0}").green(), install = "uv pip install -e .".green(), path = _4.user_display().cyan() )]
|
||||
RequestedMemberIncompatibility(
|
||||
|
@ -222,7 +223,7 @@ pub(crate) fn find_requires_python(workspace: &Workspace) -> Option<RequiresPyth
|
|||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn validate_requires_python(
|
||||
interpreter: &Interpreter,
|
||||
workspace: &Workspace,
|
||||
workspace: Option<&Workspace>,
|
||||
requires_python: &RequiresPython,
|
||||
source: &PythonRequestSource,
|
||||
) -> Result<(), ProjectError> {
|
||||
|
@ -235,7 +236,7 @@ pub(crate) fn validate_requires_python(
|
|||
// a library in the workspace is compatible with Python >=3.8, the user may attempt
|
||||
// to sync on Python 3.8. This will fail, but we should provide a more helpful error
|
||||
// message.
|
||||
for (name, member) in workspace.packages() {
|
||||
for (name, member) in workspace.into_iter().flat_map(Workspace::packages) {
|
||||
let Some(project) = member.pyproject_toml().project.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
@ -255,7 +256,7 @@ pub(crate) fn validate_requires_python(
|
|||
}
|
||||
PythonRequestSource::DotPythonVersion(file) => {
|
||||
Err(ProjectError::DotPythonVersionMemberIncompatibility(
|
||||
file.to_string(),
|
||||
file.path().user_display().to_string(),
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
name.clone(),
|
||||
|
@ -285,7 +286,7 @@ pub(crate) fn validate_requires_python(
|
|||
}
|
||||
PythonRequestSource::DotPythonVersion(file) => {
|
||||
Err(ProjectError::DotPythonVersionProjectIncompatibility(
|
||||
file.to_string(),
|
||||
file.path().user_display().to_string(),
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
))
|
||||
|
@ -299,6 +300,49 @@ pub(crate) fn validate_requires_python(
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns an error if the [`Interpreter`] does not satisfy script or workspace `requires-python`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn validate_script_requires_python(
|
||||
interpreter: &Interpreter,
|
||||
workspace: Option<&Workspace>,
|
||||
requires_python: &RequiresPython,
|
||||
requires_python_source: &RequiresPythonSource,
|
||||
request_source: &PythonRequestSource,
|
||||
) -> Result<(), ProjectError> {
|
||||
match requires_python_source {
|
||||
RequiresPythonSource::Project => {
|
||||
validate_requires_python(interpreter, workspace, requires_python, request_source)?;
|
||||
}
|
||||
RequiresPythonSource::Script => {}
|
||||
};
|
||||
|
||||
if requires_python.contains(interpreter.python_version()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match request_source {
|
||||
PythonRequestSource::UserRequest => {
|
||||
Err(ProjectError::RequestedPythonScriptIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
))
|
||||
}
|
||||
PythonRequestSource::DotPythonVersion(file) => {
|
||||
Err(ProjectError::DotPythonVersionScriptIncompatibility(
|
||||
file.file_name().to_string(),
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
))
|
||||
}
|
||||
PythonRequestSource::RequiresPython => {
|
||||
Err(ProjectError::RequiresPythonScriptIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An interpreter suitable for the project.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
@ -314,47 +358,68 @@ pub(crate) enum PythonRequestSource {
|
|||
/// The request was provided by the user.
|
||||
UserRequest,
|
||||
/// The request was inferred from a `.python-version` or `.python-versions` file.
|
||||
DotPythonVersion(String),
|
||||
DotPythonVersion(PythonVersionFile),
|
||||
/// The request was inferred from a `pyproject.toml` file.
|
||||
RequiresPython,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PythonRequestSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PythonRequestSource::UserRequest => write!(f, "explicit request"),
|
||||
PythonRequestSource::DotPythonVersion(file) => {
|
||||
write!(f, "version file at `{}`", file.path().user_display())
|
||||
}
|
||||
PythonRequestSource::RequiresPython => write!(f, "`requires-python` metadata"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved Python request and requirement for a [`Workspace`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WorkspacePython {
|
||||
/// The source of the Python request.
|
||||
source: PythonRequestSource,
|
||||
pub(crate) source: PythonRequestSource,
|
||||
/// The resolved Python request, computed by considering (1) any explicit request from the user
|
||||
/// via `--python`, (2) any implicit request from the user via `.python-version`, and (3) any
|
||||
/// `Requires-Python` specifier in the `pyproject.toml`.
|
||||
python_request: Option<PythonRequest>,
|
||||
pub(crate) python_request: Option<PythonRequest>,
|
||||
/// The resolved Python requirement for the project, computed by taking the intersection of all
|
||||
/// `Requires-Python` specifiers in the workspace.
|
||||
requires_python: Option<RequiresPython>,
|
||||
pub(crate) requires_python: Option<RequiresPython>,
|
||||
}
|
||||
|
||||
impl WorkspacePython {
|
||||
/// Determine the [`WorkspacePython`] for the current [`Workspace`].
|
||||
pub(crate) async fn from_request(
|
||||
python_request: Option<PythonRequest>,
|
||||
workspace: &Workspace,
|
||||
workspace: Option<&Workspace>,
|
||||
project_dir: &Path,
|
||||
no_config: bool,
|
||||
) -> Result<Self, ProjectError> {
|
||||
let requires_python = find_requires_python(workspace);
|
||||
let requires_python = workspace.and_then(find_requires_python);
|
||||
|
||||
let workspace_root = workspace.map(Workspace::install_path);
|
||||
|
||||
let (source, python_request) = if let Some(request) = python_request {
|
||||
// (1) Explicit request from user
|
||||
let source = PythonRequestSource::UserRequest;
|
||||
let request = Some(request);
|
||||
(source, request)
|
||||
} else if let Some(file) =
|
||||
PythonVersionFile::discover(workspace.install_path(), false, false).await?
|
||||
} else if let Some(file) = PythonVersionFile::discover(
|
||||
project_dir,
|
||||
&VersionFileDiscoveryOptions::default()
|
||||
.with_stop_discovery_at(workspace_root.map(PathBuf::as_ref))
|
||||
.with_no_config(no_config),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
// (2) Request from `.python-version`
|
||||
let source = PythonRequestSource::DotPythonVersion(file.file_name().to_string());
|
||||
let source = PythonRequestSource::DotPythonVersion(file.clone());
|
||||
let request = file.into_version();
|
||||
(source, request)
|
||||
} else {
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
// (3) `requires-python` in `pyproject.toml`
|
||||
let request = requires_python
|
||||
.as_ref()
|
||||
.map(RequiresPython::specifiers)
|
||||
|
@ -368,6 +433,87 @@ impl WorkspacePython {
|
|||
(source, request)
|
||||
};
|
||||
|
||||
if let Some(python_request) = python_request.as_ref() {
|
||||
debug!(
|
||||
"Using Python request `{}` from {source}",
|
||||
python_request.to_canonical_string()
|
||||
);
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The source of a `Requires-Python` specifier.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum RequiresPythonSource {
|
||||
/// From the PEP 723 inline script metadata.
|
||||
Script,
|
||||
/// From a `pyproject.toml` in a workspace.
|
||||
Project,
|
||||
}
|
||||
|
||||
/// The resolved Python request and requirement for a [`Pep723Script`]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ScriptPython {
|
||||
/// The source of the Python request.
|
||||
pub(crate) source: PythonRequestSource,
|
||||
/// The resolved Python request, computed by considering (1) any explicit request from the user
|
||||
/// via `--python`, (2) any implicit request from the user via `.python-version`, (3) any
|
||||
/// `Requires-Python` specifier in the script metadata, and (4) any `Requires-Python` specifier
|
||||
/// in the `pyproject.toml`.
|
||||
pub(crate) python_request: Option<PythonRequest>,
|
||||
/// The resolved Python requirement for the script and its source.
|
||||
pub(crate) requires_python: Option<(RequiresPython, RequiresPythonSource)>,
|
||||
}
|
||||
|
||||
impl ScriptPython {
|
||||
/// Determine the [`ScriptPython`] for the current [`Workspace`].
|
||||
pub(crate) async fn from_request(
|
||||
python_request: Option<PythonRequest>,
|
||||
workspace: Option<&Workspace>,
|
||||
script: &Pep723Item,
|
||||
no_config: bool,
|
||||
) -> Result<Self, ProjectError> {
|
||||
// First, discover a requirement from the workspace
|
||||
let WorkspacePython {
|
||||
mut source,
|
||||
mut python_request,
|
||||
requires_python,
|
||||
} = WorkspacePython::from_request(
|
||||
python_request,
|
||||
workspace,
|
||||
script.path().and_then(Path::parent).unwrap_or(&**CWD),
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If the script has a `requires-python` specifier, prefer that over one from the workspace.
|
||||
let requires_python =
|
||||
if let Some(requires_python_specifiers) = script.metadata().requires_python.as_ref() {
|
||||
if python_request.is_none() {
|
||||
python_request = Some(PythonRequest::Version(VersionRequest::Range(
|
||||
requires_python_specifiers.clone(),
|
||||
PythonVariant::Default,
|
||||
)));
|
||||
source = PythonRequestSource::RequiresPython;
|
||||
}
|
||||
Some((
|
||||
RequiresPython::from_specifiers(requires_python_specifiers),
|
||||
RequiresPythonSource::Script,
|
||||
))
|
||||
} else {
|
||||
requires_python.map(|requirement| (requirement, RequiresPythonSource::Project))
|
||||
};
|
||||
|
||||
if let Some(python_request) = python_request.as_ref() {
|
||||
debug!("Using Python request {python_request} from {source}");
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
source,
|
||||
python_request,
|
||||
|
@ -380,12 +526,14 @@ impl ProjectInterpreter {
|
|||
/// Discover the interpreter to use in the current [`Workspace`].
|
||||
pub(crate) async fn discover(
|
||||
workspace: &Workspace,
|
||||
project_dir: &Path,
|
||||
python_request: Option<PythonRequest>,
|
||||
python_preference: PythonPreference,
|
||||
python_downloads: PythonDownloads,
|
||||
connectivity: Connectivity,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<Self, ProjectError> {
|
||||
|
@ -394,7 +542,8 @@ impl ProjectInterpreter {
|
|||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = WorkspacePython::from_request(python_request, workspace).await?;
|
||||
} = WorkspacePython::from_request(python_request, Some(workspace), project_dir, no_config)
|
||||
.await?;
|
||||
|
||||
// Read from the virtual environment first.
|
||||
let venv = workspace.venv();
|
||||
|
@ -402,11 +551,15 @@ impl ProjectInterpreter {
|
|||
Ok(venv) => {
|
||||
if python_request.as_ref().map_or(true, |request| {
|
||||
if request.satisfied(venv.interpreter(), cache) {
|
||||
debug!("The virtual environment's Python version satisfies `{request}`");
|
||||
debug!(
|
||||
"The virtual environment's Python version satisfies `{}`",
|
||||
request.to_canonical_string()
|
||||
);
|
||||
true
|
||||
} else {
|
||||
debug!(
|
||||
"The virtual environment's Python version does not satisfy `{request}`"
|
||||
"The virtual environment's Python version does not satisfy `{}`",
|
||||
request.to_canonical_string()
|
||||
);
|
||||
false
|
||||
}
|
||||
|
@ -499,7 +652,7 @@ impl ProjectInterpreter {
|
|||
}
|
||||
|
||||
if let Some(requires_python) = requires_python.as_ref() {
|
||||
validate_requires_python(&interpreter, workspace, requires_python, &source)?;
|
||||
validate_requires_python(&interpreter, Some(workspace), requires_python, &source)?;
|
||||
}
|
||||
|
||||
Ok(Self::Interpreter(interpreter))
|
||||
|
@ -523,17 +676,20 @@ pub(crate) async fn get_or_init_environment(
|
|||
connectivity: Connectivity,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<PythonEnvironment, ProjectError> {
|
||||
match ProjectInterpreter::discover(
|
||||
workspace,
|
||||
workspace.install_path().as_ref(),
|
||||
python,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -1326,13 +1482,14 @@ pub(crate) async fn update_environment(
|
|||
})
|
||||
}
|
||||
|
||||
/// Determine the [`RequiresPython`] requirement for a PEP 723 script.
|
||||
pub(crate) async fn script_python_requirement(
|
||||
/// Determine the [`RequiresPython`] requirement for a new PEP 723 script.
|
||||
pub(crate) async fn init_script_python_requirement(
|
||||
python: Option<&str>,
|
||||
directory: &Path,
|
||||
no_pin_python: bool,
|
||||
python_preference: PythonPreference,
|
||||
python_downloads: PythonDownloads,
|
||||
no_config: bool,
|
||||
client_builder: &BaseClientBuilder<'_>,
|
||||
cache: &Cache,
|
||||
reporter: &PythonDownloadReporter,
|
||||
|
@ -1342,9 +1499,12 @@ pub(crate) async fn script_python_requirement(
|
|||
PythonRequest::parse(request)
|
||||
} else if let (false, Some(request)) = (
|
||||
no_pin_python,
|
||||
PythonVersionFile::discover(directory, false, false)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version),
|
||||
PythonVersionFile::discover(
|
||||
directory,
|
||||
&VersionFileDiscoveryOptions::default().with_no_config(no_config),
|
||||
)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version),
|
||||
) {
|
||||
// (2) Request from `.python-version`
|
||||
request
|
||||
|
|
|
@ -47,6 +47,7 @@ pub(crate) async fn remove(
|
|||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
@ -193,6 +194,7 @@ pub(crate) async fn remove(
|
|||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -28,7 +28,7 @@ use uv_normalize::PackageName;
|
|||
|
||||
use uv_python::{
|
||||
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionRequest,
|
||||
PythonPreference, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions,
|
||||
};
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use uv_resolver::{InstallTarget, Lock};
|
||||
|
@ -45,8 +45,8 @@ use crate::commands::pip::operations::Modifications;
|
|||
use crate::commands::project::environment::CachedEnvironment;
|
||||
use crate::commands::project::lock::LockMode;
|
||||
use crate::commands::project::{
|
||||
default_dependency_groups, validate_requires_python, DependencyGroupsTarget,
|
||||
EnvironmentSpecification, ProjectError, PythonRequestSource, WorkspacePython,
|
||||
default_dependency_groups, validate_requires_python, validate_script_requires_python,
|
||||
DependencyGroupsTarget, EnvironmentSpecification, ProjectError, ScriptPython, WorkspacePython,
|
||||
};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::{diagnostics, project, ExitStatus, SharedState};
|
||||
|
@ -178,31 +178,17 @@ pub(crate) async fn run(
|
|||
}
|
||||
}
|
||||
|
||||
let (source, python_request) = if let Some(request) = python.as_deref() {
|
||||
// (1) Explicit request from user
|
||||
let source = PythonRequestSource::UserRequest;
|
||||
let request = Some(PythonRequest::parse(request));
|
||||
(source, request)
|
||||
} else if let Some(file) = PythonVersionFile::discover(&project_dir, false, false).await? {
|
||||
// (2) Request from `.python-version`
|
||||
let source = PythonRequestSource::DotPythonVersion(file.file_name().to_string());
|
||||
let request = file.into_version();
|
||||
(source, request)
|
||||
} else {
|
||||
// (3) `Requires-Python` in the script
|
||||
let request = script
|
||||
.metadata()
|
||||
.requires_python
|
||||
.as_ref()
|
||||
.map(|requires_python| {
|
||||
PythonRequest::Version(VersionRequest::Range(
|
||||
requires_python.clone(),
|
||||
PythonVariant::Default,
|
||||
))
|
||||
});
|
||||
let source = PythonRequestSource::RequiresPython;
|
||||
(source, request)
|
||||
};
|
||||
let ScriptPython {
|
||||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = ScriptPython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
None,
|
||||
&script,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client_builder = BaseClientBuilder::new()
|
||||
.connectivity(connectivity)
|
||||
|
@ -221,30 +207,18 @@ pub(crate) async fn run(
|
|||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
if let Some(requires_python) = script.metadata().requires_python.as_ref() {
|
||||
if !requires_python.contains(interpreter.python_version()) {
|
||||
let err = match source {
|
||||
PythonRequestSource::UserRequest => {
|
||||
ProjectError::RequestedPythonScriptIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
)
|
||||
}
|
||||
PythonRequestSource::DotPythonVersion(file) => {
|
||||
ProjectError::DotPythonVersionScriptIncompatibility(
|
||||
file,
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
)
|
||||
}
|
||||
PythonRequestSource::RequiresPython => {
|
||||
ProjectError::RequiresPythonScriptIncompatibility(
|
||||
interpreter.python_version().clone(),
|
||||
requires_python.clone(),
|
||||
)
|
||||
}
|
||||
};
|
||||
warn_user!("{err}");
|
||||
if let Some((requires_python, requires_python_source)) = requires_python {
|
||||
match validate_script_requires_python(
|
||||
&interpreter,
|
||||
None,
|
||||
&requires_python,
|
||||
&requires_python_source,
|
||||
&source,
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
warn_user!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -536,7 +510,9 @@ pub(crate) async fn run(
|
|||
requires_python,
|
||||
} = WorkspacePython::from_request(
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
project.workspace(),
|
||||
Some(project.workspace()),
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -555,7 +531,7 @@ pub(crate) async fn run(
|
|||
if let Some(requires_python) = requires_python.as_ref() {
|
||||
validate_requires_python(
|
||||
&interpreter,
|
||||
project.workspace(),
|
||||
Some(project.workspace()),
|
||||
requires_python,
|
||||
&source,
|
||||
)?;
|
||||
|
@ -583,6 +559,7 @@ pub(crate) async fn run(
|
|||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -737,9 +714,12 @@ pub(crate) async fn run(
|
|||
Some(PythonRequest::parse(request))
|
||||
// (2) Request from `.python-version`
|
||||
} else {
|
||||
PythonVersionFile::discover(&project_dir, no_config, false)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version)
|
||||
PythonVersionFile::discover(
|
||||
&project_dir,
|
||||
&VersionFileDiscoveryOptions::default().with_no_config(no_config),
|
||||
)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version)
|
||||
};
|
||||
|
||||
let python = PythonInstallation::find_or_download(
|
||||
|
|
|
@ -59,6 +59,7 @@ pub(crate) async fn sync(
|
|||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
@ -118,6 +119,7 @@ pub(crate) async fn sync(
|
|||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -52,6 +52,7 @@ pub(crate) async fn tree(
|
|||
concurrency: Concurrency,
|
||||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
|
@ -74,12 +75,14 @@ pub(crate) async fn tree(
|
|||
Some(
|
||||
ProjectInterpreter::discover(
|
||||
&workspace,
|
||||
project_dir,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
|
|
|
@ -4,15 +4,14 @@ use std::path::Path;
|
|||
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVariant,
|
||||
PythonVersionFile, VersionRequest,
|
||||
};
|
||||
use uv_resolver::RequiresPython;
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
|
||||
|
||||
use crate::commands::{project::find_requires_python, ExitStatus};
|
||||
use crate::commands::{
|
||||
project::{validate_requires_python, WorkspacePython},
|
||||
ExitStatus,
|
||||
};
|
||||
|
||||
/// Find a Python interpreter.
|
||||
pub(crate) async fn find(
|
||||
|
@ -30,50 +29,55 @@ pub(crate) async fn find(
|
|||
EnvironmentPreference::Any
|
||||
};
|
||||
|
||||
// (1) Explicit request from user
|
||||
let mut request = request.map(|request| PythonRequest::parse(&request));
|
||||
|
||||
// (2) Request from `.python-version`
|
||||
if request.is_none() {
|
||||
request = PythonVersionFile::discover(project_dir, no_config, false)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version);
|
||||
}
|
||||
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
if request.is_none() && !no_project {
|
||||
let project =
|
||||
match VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await {
|
||||
Ok(project) => Some(project),
|
||||
Err(WorkspaceError::MissingProject(_)) => None,
|
||||
Err(WorkspaceError::MissingPyprojectToml) => None,
|
||||
Err(WorkspaceError::NonWorkspace(_)) => None,
|
||||
Err(err) => {
|
||||
warn_user_once!("{err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project) = project {
|
||||
request = find_requires_python(project.workspace())
|
||||
.as_ref()
|
||||
.map(RequiresPython::specifiers)
|
||||
.map(|specifiers| {
|
||||
PythonRequest::Version(VersionRequest::Range(
|
||||
specifiers.clone(),
|
||||
PythonVariant::Default,
|
||||
))
|
||||
});
|
||||
let project = if no_project {
|
||||
None
|
||||
} else {
|
||||
match VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await {
|
||||
Ok(project) => Some(project),
|
||||
Err(WorkspaceError::MissingProject(_)) => None,
|
||||
Err(WorkspaceError::MissingPyprojectToml) => None,
|
||||
Err(WorkspaceError::NonWorkspace(_)) => None,
|
||||
Err(err) => {
|
||||
warn_user_once!("{err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let WorkspacePython {
|
||||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = WorkspacePython::from_request(
|
||||
request.map(|request| PythonRequest::parse(&request)),
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let python = PythonInstallation::find(
|
||||
&request.unwrap_or_default(),
|
||||
&python_request.unwrap_or_default(),
|
||||
environment_preference,
|
||||
python_preference,
|
||||
cache,
|
||||
)?;
|
||||
|
||||
// Warn if the discovered Python version is incompatible with the current workspace
|
||||
if let Some(requires_python) = requires_python {
|
||||
match validate_requires_python(
|
||||
python.interpreter(),
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
&requires_python,
|
||||
&source,
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
warn_user!("{err}");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
std::path::absolute(python.interpreter().sys_executable())?.simplified_display()
|
||||
|
|
|
@ -18,7 +18,10 @@ use uv_python::downloads::{DownloadResult, ManagedPythonDownload, PythonDownload
|
|||
use uv_python::managed::{
|
||||
python_executable_dir, ManagedPythonInstallation, ManagedPythonInstallations,
|
||||
};
|
||||
use uv_python::{PythonDownloads, PythonInstallationKey, PythonRequest, PythonVersionFile};
|
||||
use uv_python::{
|
||||
PythonDownloads, PythonInstallationKey, PythonRequest, PythonVersionFile,
|
||||
VersionFileDiscoveryOptions, VersionFilePreference,
|
||||
};
|
||||
use uv_shell::Shell;
|
||||
use uv_trampoline_builder::{Launcher, LauncherKind};
|
||||
use uv_warnings::warn_user;
|
||||
|
@ -123,17 +126,22 @@ pub(crate) async fn install(
|
|||
// Resolve the requests
|
||||
let mut is_default_install = false;
|
||||
let requests: Vec<_> = if targets.is_empty() {
|
||||
PythonVersionFile::discover(project_dir, no_config, true)
|
||||
.await?
|
||||
.map(PythonVersionFile::into_versions)
|
||||
.unwrap_or_else(|| {
|
||||
// If no version file is found and no requests were made
|
||||
is_default_install = true;
|
||||
vec![PythonRequest::Default]
|
||||
})
|
||||
.into_iter()
|
||||
.map(InstallRequest::new)
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
PythonVersionFile::discover(
|
||||
project_dir,
|
||||
&VersionFileDiscoveryOptions::default()
|
||||
.with_no_config(no_config)
|
||||
.with_preference(VersionFilePreference::Versions),
|
||||
)
|
||||
.await?
|
||||
.map(PythonVersionFile::into_versions)
|
||||
.unwrap_or_else(|| {
|
||||
// If no version file is found and no requests were made
|
||||
is_default_install = true;
|
||||
vec![PythonRequest::Default]
|
||||
})
|
||||
.into_iter()
|
||||
.map(InstallRequest::new)
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
} else {
|
||||
targets
|
||||
.iter()
|
||||
|
|
|
@ -10,7 +10,7 @@ use uv_cache::Cache;
|
|||
use uv_fs::Simplified;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile,
|
||||
PYTHON_VERSION_FILENAME,
|
||||
VersionFileDiscoveryOptions, PYTHON_VERSION_FILENAME,
|
||||
};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject};
|
||||
|
@ -40,7 +40,8 @@ pub(crate) async fn pin(
|
|||
}
|
||||
};
|
||||
|
||||
let version_file = PythonVersionFile::discover(project_dir, false, false).await;
|
||||
let version_file =
|
||||
PythonVersionFile::discover(project_dir, &VersionFileDiscoveryOptions::default()).await;
|
||||
|
||||
let Some(request) = request else {
|
||||
// Display the current pinned Python version
|
||||
|
|
|
@ -22,17 +22,16 @@ use uv_install_wheel::linker::LinkMode;
|
|||
use uv_pypi_types::Requirement;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
|
||||
PythonVariant, PythonVersionFile, VersionRequest,
|
||||
};
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython};
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex};
|
||||
use uv_shell::Shell;
|
||||
use uv_types::{BuildContext, BuildIsolation, HashStrategy};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
|
||||
|
||||
use crate::commands::pip::loggers::{DefaultInstallLogger, InstallLogger};
|
||||
use crate::commands::pip::operations::Changelog;
|
||||
use crate::commands::project::find_requires_python;
|
||||
use crate::commands::project::{validate_requires_python, WorkspacePython};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::{ExitStatus, SharedState};
|
||||
use crate::printer::Printer;
|
||||
|
@ -184,35 +183,22 @@ async fn venv_impl(
|
|||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
// (1) Explicit request from user
|
||||
let mut interpreter_request = python_request.map(PythonRequest::parse);
|
||||
|
||||
// (2) Request from `.python-version`
|
||||
if interpreter_request.is_none() {
|
||||
interpreter_request = PythonVersionFile::discover(project_dir, no_config, false)
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
.and_then(PythonVersionFile::into_version);
|
||||
}
|
||||
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
if interpreter_request.is_none() {
|
||||
if let Some(project) = project {
|
||||
interpreter_request = find_requires_python(project.workspace())
|
||||
.as_ref()
|
||||
.map(RequiresPython::specifiers)
|
||||
.map(|specifiers| {
|
||||
PythonRequest::Version(VersionRequest::Range(
|
||||
specifiers.clone(),
|
||||
PythonVariant::Default,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
let WorkspacePython {
|
||||
source,
|
||||
python_request,
|
||||
requires_python,
|
||||
} = WorkspacePython::from_request(
|
||||
python_request.map(PythonRequest::parse),
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
project_dir,
|
||||
no_config,
|
||||
)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
// Locate the Python interpreter to use in the environment
|
||||
let python = PythonInstallation::find_or_download(
|
||||
interpreter_request.as_ref(),
|
||||
python_request.as_ref(),
|
||||
EnvironmentPreference::OnlySystem,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
|
@ -253,6 +239,21 @@ async fn venv_impl(
|
|||
.into_diagnostic()?;
|
||||
}
|
||||
|
||||
// Check if the discovered Python version is incompatible with the current workspace
|
||||
if let Some(requires_python) = requires_python {
|
||||
match validate_requires_python(
|
||||
&interpreter,
|
||||
project.as_ref().map(VirtualProject::workspace),
|
||||
&requires_python,
|
||||
&source,
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
warn_user!("{err}");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Creating virtual environment {}at: {}",
|
||||
|
|
|
@ -1278,6 +1278,7 @@ async fn run_project(
|
|||
globals.connectivity,
|
||||
globals.native_tls,
|
||||
&globals.allow_insecure_host,
|
||||
no_config,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -1374,6 +1375,7 @@ async fn run_project(
|
|||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
&globals.allow_insecure_host,
|
||||
no_config,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -1403,6 +1405,7 @@ async fn run_project(
|
|||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
&globals.allow_insecure_host,
|
||||
no_config,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -1455,6 +1458,7 @@ async fn run_project(
|
|||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
&globals.allow_insecure_host,
|
||||
no_config,
|
||||
&cache,
|
||||
printer,
|
||||
))
|
||||
|
@ -1496,6 +1500,7 @@ async fn run_project(
|
|||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
&globals.allow_insecure_host,
|
||||
no_config,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -1531,6 +1536,7 @@ async fn run_project(
|
|||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
&globals.allow_insecure_host,
|
||||
no_config,
|
||||
&cache,
|
||||
printer,
|
||||
)
|
||||
|
@ -1566,6 +1572,7 @@ async fn run_project(
|
|||
globals.concurrency,
|
||||
globals.native_tls,
|
||||
&globals.allow_insecure_host,
|
||||
no_config,
|
||||
globals.quiet,
|
||||
&cache,
|
||||
printer,
|
||||
|
|
|
@ -2005,6 +2005,43 @@ fn init_requires_python_specifiers() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `uv init`, inferring the `requires-python` from the `.python-version` file.
|
||||
#[test]
|
||||
fn init_requires_python_version_file() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.8", "3.12"]);
|
||||
|
||||
context.temp_dir.child(".python-version").write_str("3.8")?;
|
||||
|
||||
let child = context.temp_dir.join("foo");
|
||||
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Initialized project `foo` at `[TEMP_DIR]/foo`
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?;
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = []
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `uv init` from within an unmanaged project.
|
||||
#[test]
|
||||
fn init_unmanaged() -> Result<()> {
|
||||
|
|
|
@ -195,11 +195,43 @@ fn python_find_pin() {
|
|||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
let child_dir = context.temp_dir.child("child");
|
||||
child_dir.create_dir_all().unwrap();
|
||||
|
||||
// We should also find pinned versions in the parent directory
|
||||
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.12]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.11").current_dir(&child_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Updated `.python-version` from `3.12` -> `3.11`
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// Unless the child directory also has a pin
|
||||
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.11]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_find_project() {
|
||||
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
|
||||
let context: TestContext = TestContext::new_with_versions(&["3.10", "3.11", "3.12"]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml
|
||||
|
@ -207,7 +239,7 @@ fn python_find_project() {
|
|||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["anyio==3.7.0"]
|
||||
"#})
|
||||
.unwrap();
|
||||
|
@ -217,26 +249,90 @@ fn python_find_project() {
|
|||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.12]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// Unless explicitly requested
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.11]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// Unless explicitly requested
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("3.10"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.10]
|
||||
|
||||
----- stderr -----
|
||||
warning: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`
|
||||
"###);
|
||||
|
||||
// Or `--no-project` is used
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("--no-project"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.10]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// But a pin should take precedence
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Pinned `.python-version` to `3.12`
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
uv_snapshot!(context.filters(), context.python_find(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.12]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// Create a pin that's incompatible with the project
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.10").arg("--no-workspace"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Updated `.python-version` from `3.12` -> `3.10`
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// We should warn on subsequent uses, but respect the pinned version?
|
||||
uv_snapshot!(context.filters(), context.python_find(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.10]
|
||||
|
||||
----- stderr -----
|
||||
warning: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`
|
||||
"###);
|
||||
|
||||
// Unless the pin file is outside the project, in which case we should just ignore it
|
||||
let child_dir = context.temp_dir.child("child");
|
||||
child_dir.create_dir_all().unwrap();
|
||||
|
||||
let pyproject_toml = child_dir.child("pyproject.toml");
|
||||
pyproject_toml
|
||||
.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["anyio==3.7.0"]
|
||||
"#})
|
||||
.unwrap();
|
||||
|
||||
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.11]
|
||||
|
||||
----- stderr -----
|
||||
|
|
|
@ -2434,6 +2434,7 @@ fn sync_custom_environment_path() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
Creating virtual environment at: foo
|
||||
Activate with: source foo/[BIN]/activate
|
||||
"###);
|
||||
|
@ -3603,6 +3604,7 @@ fn sync_invalid_environment() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"###);
|
||||
|
@ -3669,6 +3671,7 @@ fn sync_invalid_environment() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"###);
|
||||
|
@ -3755,6 +3758,127 @@ fn sync_no_sources_missing_member() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_python_version() -> Result<()> {
|
||||
let context: TestContext = TestContext::new_with_versions(&["3.10", "3.11", "3.12"]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc::indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["anyio==3.7.0"]
|
||||
"#})?;
|
||||
|
||||
// We should respect the project's required version, not the first on the path
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
// Unless explicitly requested...
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--python").arg("3.10"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
|
||||
error: The requested interpreter resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`
|
||||
"###);
|
||||
|
||||
// But a pin should take precedence
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Pinned `.python-version` to `3.12`
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 4 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
// Create a pin that's incompatible with the project
|
||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.10").arg("--no-workspace"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Updated `.python-version` from `3.12` -> `3.10`
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
// We should warn on subsequent uses, but respect the pinned version?
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.10.[X] interpreter at: [PYTHON-3.10]
|
||||
error: The Python request from `.python-version` resolved to Python 3.10.[X], which is incompatible with the project's Python requirement: `>=3.11`
|
||||
"###);
|
||||
|
||||
// Unless the pin file is outside the project, in which case we should just ignore it entirely
|
||||
let child_dir = context.temp_dir.child("child");
|
||||
child_dir.create_dir_all().unwrap();
|
||||
|
||||
let pyproject_toml = child_dir.child("pyproject.toml");
|
||||
pyproject_toml
|
||||
.write_str(indoc::indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["anyio==3.7.0"]
|
||||
"#})
|
||||
.unwrap();
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().current_dir(&child_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
Creating virtual environment at: .venv
|
||||
Resolved 4 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==3.7.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_explicit() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
@ -475,6 +475,20 @@ fn create_venv_respects_pyproject_requires_python() -> Result<()> {
|
|||
|
||||
context.venv.assert(predicates::path::is_dir());
|
||||
|
||||
// We warn if we receive an incompatible version
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: The requested interpreter resolved to Python 3.11.[X], which is incompatible with the project's Python requirement: `>=3.12`
|
||||
Creating virtual environment at: .venv
|
||||
Activate with: source .venv/[BIN]/activate
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue