mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-31 07:47:27 +00:00
Run interpreter discovery under -I
mode (#2552)
## Summary If you have a file `typing.py` in the current working directory, `python -m` doesn't work in some Python versions: ```sh ❯ python -m foo Could not import runpy module Traceback (most recent call last): File "/Users/crmarsh/.local/share/rtx/installs/python/3.9.18/lib/python3.9/runpy.py", line 15, in <module> import importlib.util File "/Users/crmarsh/.local/share/rtx/installs/python/3.9.18/lib/python3.9/importlib/util.py", line 2, in <module> from . import abc File "/Users/crmarsh/.local/share/rtx/installs/python/3.9.18/lib/python3.9/importlib/abc.py", line 17, in <module> from typing import Protocol, runtime_checkable ImportError: cannot import name 'Protocol' from 'typing' (/Users/crmarsh/workspace/uv/typing.py) ``` This did _not_ cause problems for us on Python 3.11 or later, because we set `PYTHONSAFEPATH`, which avoids adding the current working directory to `sys.path`. However, on earlier versions, we _were_ failing with the above. (It's important that we run interpreter discovery in the current working directory, since doing otherwise breaks pyenv shims.) The fix implemented here uses `-I` to run Python in isolated mode, which is even stricter. The downside of isolated mode is that we currently rely on setting `PYTHONPATH` to find the "fake module" that we create on disk, and `-I` means `PYTHONPATH` is totally ignored. So, instead, we run a script directly, and that _script_ injects the path we care about into `PYTHONSAFEPATH`. Closes https://github.com/astral-sh/uv/issues/2547.
This commit is contained in:
parent
79fbac7af5
commit
c180fedbce
4 changed files with 63 additions and 38 deletions
|
@ -28,7 +28,7 @@ use tracing::{debug, info_span, instrument, Instrument};
|
|||
use distribution_types::Resolution;
|
||||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use pep508_rs::Requirement;
|
||||
use uv_fs::Simplified;
|
||||
use uv_fs::{PythonExt, Simplified};
|
||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||
use uv_traits::{
|
||||
BuildContext, BuildIsolation, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait,
|
||||
|
@ -637,7 +637,7 @@ impl SourceBuild {
|
|||
pep517_backend.backend_import(),
|
||||
escape_path_for_python(&metadata_directory),
|
||||
self.config_settings.escape_for_python(),
|
||||
escape_path_for_python(&outfile),
|
||||
outfile.escape_for_python(),
|
||||
};
|
||||
let span = info_span!(
|
||||
"run_python_script",
|
||||
|
@ -744,7 +744,7 @@ impl SourceBuild {
|
|||
.metadata_directory
|
||||
.as_deref()
|
||||
.map_or("None".to_string(), |path| {
|
||||
format!(r#""{}""#, escape_path_for_python(path))
|
||||
format!(r#""{}""#, path.escape_for_python())
|
||||
});
|
||||
|
||||
// Write the hook output to a file so that we can read it back reliably.
|
||||
|
@ -767,10 +767,10 @@ impl SourceBuild {
|
|||
"#,
|
||||
pep517_backend.backend_import(),
|
||||
self.build_kind,
|
||||
escape_path_for_python(wheel_dir),
|
||||
wheel_dir.escape_for_python(),
|
||||
metadata_directory,
|
||||
self.config_settings.escape_for_python(),
|
||||
escape_path_for_python(&outfile)
|
||||
outfile.escape_for_python()
|
||||
};
|
||||
let span = info_span!(
|
||||
"run_python_script",
|
||||
|
@ -869,7 +869,7 @@ async fn create_pep517_build_environment(
|
|||
pep517_backend.backend_import(),
|
||||
build_kind,
|
||||
config_settings.escape_for_python(),
|
||||
escape_path_for_python(&outfile)
|
||||
outfile.escape_for_python()
|
||||
};
|
||||
let span = info_span!(
|
||||
"run_python_script",
|
||||
|
|
|
@ -24,6 +24,20 @@ impl<T: AsRef<Path>> Simplified for T {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait PythonExt {
|
||||
/// Escape a [`Path`] for use in Python code.
|
||||
fn escape_for_python(&self) -> String;
|
||||
}
|
||||
|
||||
impl<T: AsRef<Path>> PythonExt for T {
|
||||
fn escape_for_python(&self) -> String {
|
||||
self.as_ref()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize the `path` component of a URL for use as a file path.
|
||||
///
|
||||
/// For example, on Windows, transforms `C:\Users\ferris\wheel-0.42.0.tar.gz` to
|
||||
|
|
|
@ -498,30 +498,35 @@ def get_operating_system_and_architecture():
|
|||
return {"os": operating_system, "arch": architecture}
|
||||
|
||||
|
||||
markers = {
|
||||
"implementation_name": implementation_name,
|
||||
"implementation_version": implementation_version,
|
||||
"os_name": os.name,
|
||||
"platform_machine": platform.machine(),
|
||||
"platform_python_implementation": platform.python_implementation(),
|
||||
"platform_release": platform.release(),
|
||||
"platform_system": platform.system(),
|
||||
"platform_version": platform.version(),
|
||||
"python_full_version": python_full_version,
|
||||
"python_version": ".".join(platform.python_version_tuple()[:2]),
|
||||
"sys_platform": sys.platform,
|
||||
}
|
||||
interpreter_info = {
|
||||
"result": "success",
|
||||
"markers": markers,
|
||||
"base_prefix": sys.base_prefix,
|
||||
"base_exec_prefix": sys.base_exec_prefix,
|
||||
"prefix": sys.prefix,
|
||||
"base_executable": getattr(sys, "_base_executable", None),
|
||||
"sys_executable": sys.executable,
|
||||
"stdlib": sysconfig.get_path("stdlib"),
|
||||
"scheme": get_scheme(),
|
||||
"virtualenv": get_virtualenv(),
|
||||
"platform": get_operating_system_and_architecture(),
|
||||
}
|
||||
print(json.dumps(interpreter_info))
|
||||
def main() -> None:
|
||||
markers = {
|
||||
"implementation_name": implementation_name,
|
||||
"implementation_version": implementation_version,
|
||||
"os_name": os.name,
|
||||
"platform_machine": platform.machine(),
|
||||
"platform_python_implementation": platform.python_implementation(),
|
||||
"platform_release": platform.release(),
|
||||
"platform_system": platform.system(),
|
||||
"platform_version": platform.version(),
|
||||
"python_full_version": python_full_version,
|
||||
"python_version": ".".join(platform.python_version_tuple()[:2]),
|
||||
"sys_platform": sys.platform,
|
||||
}
|
||||
interpreter_info = {
|
||||
"result": "success",
|
||||
"markers": markers,
|
||||
"base_prefix": sys.base_prefix,
|
||||
"base_exec_prefix": sys.base_exec_prefix,
|
||||
"prefix": sys.prefix,
|
||||
"base_executable": getattr(sys, "_base_executable", None),
|
||||
"sys_executable": sys.executable,
|
||||
"stdlib": sysconfig.get_path("stdlib"),
|
||||
"scheme": get_scheme(),
|
||||
"virtualenv": get_virtualenv(),
|
||||
"platform": get_operating_system_and_architecture(),
|
||||
}
|
||||
print(json.dumps(interpreter_info))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -15,7 +15,7 @@ use platform_tags::Platform;
|
|||
use platform_tags::{Tags, TagsError};
|
||||
use pypi_types::Scheme;
|
||||
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
|
||||
use uv_fs::{write_atomic_sync, Simplified};
|
||||
use uv_fs::{write_atomic_sync, PythonExt, Simplified};
|
||||
|
||||
use crate::Error;
|
||||
use crate::Virtualenv;
|
||||
|
@ -369,11 +369,17 @@ impl InterpreterInfo {
|
|||
let tempdir = tempfile::tempdir_in(cache.root())?;
|
||||
Self::setup_python_query_files(tempdir.path())?;
|
||||
|
||||
// Sanitize the path by (1) running under isolated mode (`-I`) to ignore any site packages
|
||||
// modifications, and then (2) adding the path containing our query script to the front of
|
||||
// `sys.path` so that we can import it.
|
||||
let script = format!(
|
||||
r#"import sys; sys.path = ["{}"] + sys.path; from python.get_interpreter_info import main; main()"#,
|
||||
tempdir.path().escape_for_python()
|
||||
);
|
||||
let output = Command::new(interpreter)
|
||||
.arg("-m")
|
||||
.arg("python.get_interpreter_info")
|
||||
.env("PYTHONPATH", tempdir.path())
|
||||
.env("PYTHONSAFEPATH", "1")
|
||||
.arg("-I")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.output()
|
||||
.map_err(|err| Error::PythonSubcommandLaunch {
|
||||
interpreter: interpreter.to_path_buf(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue