mirror of https://github.com/astral-sh/uv
Respect global Python version pins in `uv tool run` and `uv tool install`
This commit is contained in:
parent
39e2e3e74b
commit
bc87b2151d
|
|
@ -41,7 +41,7 @@ use crate::{BrokenSymlink, Interpreter, PythonInstallationKey, PythonVersion};
|
||||||
/// A request to find a Python installation.
|
/// A request to find a Python installation.
|
||||||
///
|
///
|
||||||
/// See [`PythonRequest::from_str`].
|
/// See [`PythonRequest::from_str`].
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
|
#[derive(Debug, Clone, Eq, Default)]
|
||||||
pub enum PythonRequest {
|
pub enum PythonRequest {
|
||||||
/// An appropriate default Python installation
|
/// An appropriate default Python installation
|
||||||
///
|
///
|
||||||
|
|
@ -68,6 +68,18 @@ pub enum PythonRequest {
|
||||||
Key(PythonDownloadRequest),
|
Key(PythonDownloadRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for PythonRequest {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.to_canonical_string() == other.to_canonical_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for PythonRequest {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.to_canonical_string().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> serde::Deserialize<'a> for PythonRequest {
|
impl<'a> serde::Deserialize<'a> for PythonRequest {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,15 @@ use uv_distribution_types::{
|
||||||
ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource,
|
ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource,
|
||||||
UnresolvedRequirementSpecification,
|
UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
|
use uv_fs::CWD;
|
||||||
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||||
use uv_pep508::MarkerTree;
|
use uv_pep508::MarkerTree;
|
||||||
use uv_preview::Preview;
|
use uv_preview::Preview;
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
|
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||||
|
PythonPreference, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions,
|
||||||
};
|
};
|
||||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||||
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
|
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
|
||||||
|
|
@ -66,13 +68,31 @@ pub(crate) async fn install(
|
||||||
python_downloads: PythonDownloads,
|
python_downloads: PythonDownloads,
|
||||||
installer_metadata: bool,
|
installer_metadata: bool,
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
|
no_config: bool,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
preview: Preview,
|
preview: Preview,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
let reporter = PythonDownloadReporter::single(printer);
|
let reporter = PythonDownloadReporter::single(printer);
|
||||||
|
|
||||||
let python_request = python.as_deref().map(PythonRequest::parse);
|
let (python_request, explicit_python_request) = if let Some(request) = python.as_deref() {
|
||||||
|
(Some(PythonRequest::parse(request)), true)
|
||||||
|
} else {
|
||||||
|
// Discover a global Python version pin, if no request was made
|
||||||
|
(
|
||||||
|
PythonVersionFile::discover(
|
||||||
|
// TODO(zanieb): We don't use the directory, should we expose another interface?
|
||||||
|
// Should `no_local` be implied by `None` here?
|
||||||
|
&*CWD,
|
||||||
|
&VersionFileDiscoveryOptions::default()
|
||||||
|
.with_no_config(no_config)
|
||||||
|
.with_no_local(true),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.and_then(PythonVersionFile::into_version),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Pre-emptively identify a Python interpreter. We need an interpreter to resolve any unnamed
|
// Pre-emptively identify a Python interpreter. We need an interpreter to resolve any unnamed
|
||||||
// requirements, even if we end up using a different interpreter for the tool install itself.
|
// requirements, even if we end up using a different interpreter for the tool install itself.
|
||||||
|
|
@ -344,26 +364,20 @@ pub(crate) async fn install(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let existing_environment =
|
let existing_environment = installed_tools
|
||||||
installed_tools
|
.get_environment(package_name, &cache)?
|
||||||
.get_environment(package_name, &cache)?
|
.filter(|environment| {
|
||||||
.filter(|environment| {
|
existing_environment_usable(
|
||||||
if environment.uses(&interpreter) {
|
environment,
|
||||||
trace!(
|
&interpreter,
|
||||||
"Existing interpreter matches the requested interpreter for `{}`: {}",
|
package_name,
|
||||||
package_name,
|
python_request.as_ref(),
|
||||||
environment.interpreter().sys_executable().display()
|
explicit_python_request,
|
||||||
);
|
&settings,
|
||||||
true
|
existing_tool_receipt.as_ref(),
|
||||||
} else {
|
printer,
|
||||||
let _ = writeln!(
|
)
|
||||||
printer.stderr(),
|
});
|
||||||
"Ignoring existing environment for `{}`: the requested Python interpreter does not match the environment interpreter",
|
|
||||||
package_name.cyan(),
|
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the requested and receipt requirements are the same...
|
// If the requested and receipt requirements are the same...
|
||||||
if let Some(environment) = existing_environment.as_ref().filter(|_| {
|
if let Some(environment) = existing_environment.as_ref().filter(|_| {
|
||||||
|
|
@ -394,9 +408,13 @@ pub(crate) async fn install(
|
||||||
)
|
)
|
||||||
.into_inner();
|
.into_inner();
|
||||||
|
|
||||||
// Determine the markers and tags to use for the resolution.
|
// Determine the markers and tags to use for the resolution. We use the existing
|
||||||
let markers = resolution_markers(None, python_platform.as_ref(), &interpreter);
|
// environment for markers here — above we filter the environment to `None` if
|
||||||
let tags = resolution_tags(None, python_platform.as_ref(), &interpreter)?;
|
// `existing_environment_usable` is `false`, so we've determined it's valid.
|
||||||
|
let markers =
|
||||||
|
resolution_markers(None, python_platform.as_ref(), environment.interpreter());
|
||||||
|
let tags =
|
||||||
|
resolution_tags(None, python_platform.as_ref(), environment.interpreter())?;
|
||||||
|
|
||||||
// Check if the installed packages meet the requirements.
|
// Check if the installed packages meet the requirements.
|
||||||
let site_packages = SitePackages::from_environment(environment)?;
|
let site_packages = SitePackages::from_environment(environment)?;
|
||||||
|
|
@ -640,7 +658,12 @@ pub(crate) async fn install(
|
||||||
&installed_tools,
|
&installed_tools,
|
||||||
&options,
|
&options,
|
||||||
force || invalid_tool_receipt,
|
force || invalid_tool_receipt,
|
||||||
python_request,
|
// Only persist the Python request if it was explicitly provided
|
||||||
|
if explicit_python_request {
|
||||||
|
python_request
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
requirements,
|
requirements,
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
|
|
@ -650,3 +673,54 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn existing_environment_usable(
|
||||||
|
environment: &PythonEnvironment,
|
||||||
|
interpreter: &Interpreter,
|
||||||
|
package_name: &PackageName,
|
||||||
|
python_request: Option<&PythonRequest>,
|
||||||
|
explicit_python_request: bool,
|
||||||
|
settings: &ResolverInstallerSettings,
|
||||||
|
existing_tool_receipt: Option<&uv_tool::Tool>,
|
||||||
|
printer: Printer,
|
||||||
|
) -> bool {
|
||||||
|
// If the environment matches the interpreter, it's usable
|
||||||
|
if environment.uses(interpreter) {
|
||||||
|
trace!(
|
||||||
|
"Existing interpreter matches the requested interpreter for `{}`: {}",
|
||||||
|
package_name,
|
||||||
|
environment.interpreter().sys_executable().display()
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was an explicit Python request that does not match, we'll invalidate the
|
||||||
|
// environment.
|
||||||
|
if explicit_python_request {
|
||||||
|
let _ = writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Ignoring existing environment for `{}`: the requested Python interpreter does not match the environment interpreter",
|
||||||
|
package_name.cyan(),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll invalidate the environment if all of the following are true:
|
||||||
|
// - The user requested a reinstall
|
||||||
|
// - The tool was not previously pinned to a Python version
|
||||||
|
// - There is _some_ alternative Python request
|
||||||
|
if let Some(tool_receipt) = existing_tool_receipt
|
||||||
|
&& settings.reinstall.is_all()
|
||||||
|
&& tool_receipt.python().is_none()
|
||||||
|
&& python_request.is_some()
|
||||||
|
{
|
||||||
|
let _ = writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Ignoring existing environment for `{from}`: the Python interpreter does not match the environment interpreter",
|
||||||
|
from = package_name.cyan(),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,15 @@ use uv_distribution_types::{
|
||||||
IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource,
|
IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource,
|
||||||
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
|
use uv_fs::CWD;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||||
use uv_pep508::MarkerTree;
|
use uv_pep508::MarkerTree;
|
||||||
use uv_preview::Preview;
|
use uv_preview::Preview;
|
||||||
|
use uv_python::PythonVersionFile;
|
||||||
|
use uv_python::VersionFileDiscoveryOptions;
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
|
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||||
PythonPreference, PythonRequest,
|
PythonPreference, PythonRequest,
|
||||||
|
|
@ -699,43 +702,38 @@ async fn get_or_create_environment(
|
||||||
) -> Result<(ToolRequirement, PythonEnvironment), ProjectError> {
|
) -> Result<(ToolRequirement, PythonEnvironment), ProjectError> {
|
||||||
let reporter = PythonDownloadReporter::single(printer);
|
let reporter = PythonDownloadReporter::single(printer);
|
||||||
|
|
||||||
// Figure out what Python we're targeting, either explicitly like `uvx python@3`, or via the
|
// Determine explicit Python version requests
|
||||||
// -p/--python flag.
|
let explicit_python_request = python.map(PythonRequest::parse);
|
||||||
let python_request = match request {
|
let tool_python_request = match request {
|
||||||
ToolRequest::Python {
|
ToolRequest::Python { request, .. } => Some(request.clone()),
|
||||||
request: tool_python_request,
|
ToolRequest::Package { .. } => None,
|
||||||
..
|
};
|
||||||
} => {
|
|
||||||
match python {
|
|
||||||
None => Some(tool_python_request.clone()),
|
|
||||||
|
|
||||||
// The user is both invoking a python interpreter directly and also supplying the
|
// Resolve Python request with version file lookup when no explicit request
|
||||||
// -p/--python flag. Cases like `uvx -p pypy python` are allowed, for two reasons:
|
let python_request = match (explicit_python_request, tool_python_request) {
|
||||||
// 1) Previously this was the only way to invoke e.g. PyPy via `uvx`, and it's nice
|
// e.g., `uvx --python 3.10 python3.12`
|
||||||
// to remain compatible with that. 2) A script might define an alias like `uvx
|
(Some(explicit), Some(tool_request)) if tool_request != PythonRequest::Default => {
|
||||||
// --python $MY_PYTHON ...`, and it's nice to be able to run the interpreter
|
// Conflict: both --python flag and versioned tool name
|
||||||
// directly while sticking to that alias.
|
return Err(anyhow::anyhow!(
|
||||||
//
|
"Received multiple Python version requests: `{}` and `{}`",
|
||||||
// However, we want to error out if we see conflicting or redundant versions like
|
explicit.to_canonical_string().cyan(),
|
||||||
// `uvx -p python38 python39`.
|
tool_request.to_canonical_string().cyan()
|
||||||
//
|
)
|
||||||
// Note that a command like `uvx default` doesn't bring us here. ToolRequest::parse
|
.into());
|
||||||
// returns ToolRequest::Package rather than ToolRequest::Python in that case. See
|
|
||||||
// PythonRequest::try_from_tool_name.
|
|
||||||
Some(python_flag) => {
|
|
||||||
if tool_python_request != &PythonRequest::Default {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Received multiple Python version requests: `{}` and `{}`",
|
|
||||||
python_flag.to_string().cyan(),
|
|
||||||
tool_python_request.to_canonical_string().cyan()
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
Some(PythonRequest::parse(python_flag))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ToolRequest::Package { .. } => python.map(PythonRequest::parse),
|
// e.g, `uvx --python 3.10 ...`
|
||||||
|
(Some(explicit), _) => Some(explicit),
|
||||||
|
// e.g., `uvx python` or `uvx <tool>`
|
||||||
|
(None, Some(PythonRequest::Default) | None) => PythonVersionFile::discover(
|
||||||
|
&*CWD,
|
||||||
|
&VersionFileDiscoveryOptions::default()
|
||||||
|
.with_no_config(false)
|
||||||
|
.with_no_local(true),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.and_then(PythonVersionFile::into_version),
|
||||||
|
// e.g., `uvx python3.12`
|
||||||
|
(None, Some(tool_request)) => Some(tool_request),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Discover an interpreter.
|
// Discover an interpreter.
|
||||||
|
|
|
||||||
|
|
@ -1387,6 +1387,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
globals.python_downloads,
|
globals.python_downloads,
|
||||||
globals.installer_metadata,
|
globals.installer_metadata,
|
||||||
globals.concurrency,
|
globals.concurrency,
|
||||||
|
cli.top_level.no_config,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use assert_cmd::assert::OutputAssertExt;
|
||||||
use assert_fs::{
|
use assert_fs::{
|
||||||
assert::PathAssert,
|
assert::PathAssert,
|
||||||
fixture::{FileTouch, FileWriteStr, PathChild},
|
fixture::{FileTouch, FileWriteStr, PathChild},
|
||||||
|
|
@ -178,15 +179,20 @@ fn tool_install() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_with_global_python() -> Result<()> {
|
fn tool_install_python_from_global_version_file() {
|
||||||
let context = TestContext::new_with_versions(&["3.11", "3.12"])
|
let context = TestContext::new_with_versions(&["3.11", "3.12", "3.13"])
|
||||||
.with_filtered_counts()
|
.with_filtered_counts()
|
||||||
.with_filtered_exe_suffix();
|
.with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
let bin_dir = context.temp_dir.child("bin");
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
let uv = context.user_config_dir.child("uv");
|
|
||||||
let versions = uv.child(".python-version");
|
// Pin to 3.12
|
||||||
versions.write_str("3.11")?;
|
context
|
||||||
|
.python_pin()
|
||||||
|
.arg("3.12")
|
||||||
|
.arg("--global")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
// Install a tool
|
// Install a tool
|
||||||
uv_snapshot!(context.filters(), context.tool_install()
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
|
@ -212,14 +218,147 @@ fn tool_install_with_global_python() -> Result<()> {
|
||||||
Installed 1 executable: flask
|
Installed 1 executable: flask
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
tool_dir.child("flask").assert(predicate::path::is_dir());
|
// It should use the version from the global file
|
||||||
assert!(
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
bin_dir
|
success: true
|
||||||
.child(format!("flask{}", std::env::consts::EXE_SUFFIX))
|
exit_code: 0
|
||||||
.exists()
|
----- stdout -----
|
||||||
);
|
Python 3.12.[X]
|
||||||
|
Flask 3.0.2
|
||||||
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// Change global version
|
||||||
|
context
|
||||||
|
.python_pin()
|
||||||
|
.arg("3.13")
|
||||||
|
.arg("--global")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
// Installing flask again should be a no-op, even though the global pin changed
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("flask")
|
||||||
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||||
|
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
`flask` is already installed
|
||||||
|
");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
Flask 3.0.2
|
||||||
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// Using `--upgrade` forces us to check the environment
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("flask")
|
||||||
|
.arg("--upgrade")
|
||||||
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||||
|
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Audited [N] packages in [TIME]
|
||||||
|
Installed 1 executable: flask
|
||||||
|
");
|
||||||
|
|
||||||
|
// This will not change to the new global pin, since there was not a reinstall request
|
||||||
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
Flask 3.0.2
|
||||||
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// Using `--reinstall` forces us to install flask again
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("flask")
|
||||||
|
.arg("--reinstall")
|
||||||
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||||
|
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Ignoring existing environment for `flask`: the Python interpreter does not match the environment interpreter
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ blinker==1.7.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ flask==3.0.2
|
||||||
|
+ itsdangerous==2.1.2
|
||||||
|
+ jinja2==3.1.3
|
||||||
|
+ markupsafe==2.1.5
|
||||||
|
+ werkzeug==3.0.1
|
||||||
|
Installed 1 executable: flask
|
||||||
|
");
|
||||||
|
|
||||||
|
// This will change to the new global pin, since there was not an explicit request recorded in
|
||||||
|
// the receipt
|
||||||
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.13.[X]
|
||||||
|
Flask 3.0.2
|
||||||
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// If we request a specific Python version, it takes precedence over the pin
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("flask")
|
||||||
|
.arg("--python")
|
||||||
|
.arg("3.11")
|
||||||
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||||
|
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Ignoring existing environment for `flask`: the requested Python interpreter does not match the environment interpreter
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ blinker==1.7.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ flask==3.0.2
|
||||||
|
+ itsdangerous==2.1.2
|
||||||
|
+ jinja2==3.1.3
|
||||||
|
+ markupsafe==2.1.5
|
||||||
|
+ werkzeug==3.0.1
|
||||||
|
Installed 1 executable: flask
|
||||||
|
");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -228,21 +367,9 @@ fn tool_install_with_global_python() -> Result<()> {
|
||||||
Werkzeug 3.0.1
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Change global version
|
// Use `--reinstall` to install flask again
|
||||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12").arg("--global"),
|
|
||||||
@r"
|
|
||||||
success: true
|
|
||||||
exit_code: 0
|
|
||||||
----- stdout -----
|
|
||||||
Updated `[UV_USER_CONFIG_DIR]/.python-version` from `3.11` -> `3.12`
|
|
||||||
|
|
||||||
----- stderr -----
|
|
||||||
"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Install flask again
|
|
||||||
uv_snapshot!(context.filters(), context.tool_install()
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
.arg("flask")
|
.arg("flask")
|
||||||
.arg("--reinstall")
|
.arg("--reinstall")
|
||||||
|
|
@ -268,9 +395,8 @@ fn tool_install_with_global_python() -> Result<()> {
|
||||||
Installed 1 executable: flask
|
Installed 1 executable: flask
|
||||||
");
|
");
|
||||||
|
|
||||||
// Currently, when reinstalling a tool we use the original version the tool
|
// We should continue to use the version from the install, not the global pin
|
||||||
// was installed with, not the most up-to-date global version
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -279,9 +405,7 @@ fn tool_install_with_global_python() -> Result<()> {
|
||||||
Werkzeug 3.0.1
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -2145,6 +2145,93 @@ fn tool_run_hint_version_not_available() {
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_python_from_global_version_file() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_python_sources();
|
||||||
|
|
||||||
|
context
|
||||||
|
.python_pin()
|
||||||
|
.arg("3.11")
|
||||||
|
.arg("--global")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.11.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_python_version_overrides_global_pin() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_python_sources();
|
||||||
|
|
||||||
|
// Set global pin to 3.11
|
||||||
|
context
|
||||||
|
.python_pin()
|
||||||
|
.arg("3.11")
|
||||||
|
.arg("--global")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
// Explicitly request python3.12, should override global pin
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("python3.12")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_run_python_with_explicit_default_bypasses_global_pin() {
|
||||||
|
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_python_sources();
|
||||||
|
|
||||||
|
// Set global pin to 3.11
|
||||||
|
context
|
||||||
|
.python_pin()
|
||||||
|
.arg("3.11")
|
||||||
|
.arg("--global")
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
// Explicitly request --python default, should bypass global pin and use system default (3.12)
|
||||||
|
uv_snapshot!(context.filters(), context.tool_run()
|
||||||
|
.arg("--python")
|
||||||
|
.arg("default")
|
||||||
|
.arg("python")
|
||||||
|
.arg("--version"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved in [TIME]
|
||||||
|
Audited in [TIME]
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_run_python_from() {
|
fn tool_run_python_from() {
|
||||||
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
let context = TestContext::new_with_versions(&["3.12", "3.11"])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue