mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-31 07:37:38 +00:00
[ty] Support --python=<symlink to executable>
(#18827)
## Summary Fixes https://github.com/astral-sh/ty/issues/640. If a user passes `--python=<some-virtual-environment>/bin/python`, we must avoid canonicalizing the path until we've traversed upwards to find the `sys.prefix` directory (`<some-virtual-environment>`). On Unix systems, `<sys.prefix>/bin/python` is often a symlink to a system interpreter; if we resolve the symlink too easily then we'll add the system interpreter's `site-packages` directory as a search path rather than the virtual environment's directory. ## Test Plan I added an integration test to `crates/ty/tests/cli/python_environment.rs` which fails on `main`. I also manually tested locally that running `cargo run -p ty check foo.py --python=.venv/bin/python -vv` now prints this log to the terminal ``` 2025-06-20 18:35:24.57702 DEBUG Resolved site-packages directories for this virtual environment are: SitePackagesPaths({"/Users/alexw/dev/ruff/.venv/lib/python3.13/site-packages"}) ``` Whereas it previously resolved `site-packages` to my system intallation's `site-packages` directory
This commit is contained in:
parent
f32ae94bc3
commit
f24e650dfd
3 changed files with 123 additions and 57 deletions
|
@ -864,10 +864,47 @@ impl SysPrefixPath {
|
|||
origin: SysPrefixPathOrigin,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Self> {
|
||||
let sys_prefix = if !origin.must_point_directly_to_sys_prefix()
|
||||
&& system.is_file(unvalidated_path)
|
||||
&& unvalidated_path
|
||||
.file_name()
|
||||
.is_some_and(|name| name.starts_with("python"))
|
||||
{
|
||||
// It looks like they passed us a path to a Python executable, e.g. `.venv/bin/python3`.
|
||||
// Try to figure out the `sys.prefix` value from the Python executable.
|
||||
let sys_prefix = if cfg!(windows) {
|
||||
// On Windows, the relative path to the Python executable from `sys.prefix`
|
||||
// is different depending on whether it's a virtual environment or a system installation.
|
||||
// System installations have their executable at `<sys.prefix>/python.exe`,
|
||||
// whereas virtual environments have their executable at `<sys.prefix>/Scripts/python.exe`.
|
||||
unvalidated_path.parent().and_then(|parent| {
|
||||
if parent.file_name() == Some("Scripts") {
|
||||
parent.parent()
|
||||
} else {
|
||||
Some(parent)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// On Unix, `sys.prefix` is always the grandparent directory of the Python executable,
|
||||
// regardless of whether it's a virtual environment or a system installation.
|
||||
unvalidated_path.ancestors().nth(2)
|
||||
};
|
||||
let Some(sys_prefix) = sys_prefix else {
|
||||
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
unvalidated_path.to_path_buf(),
|
||||
origin,
|
||||
None,
|
||||
));
|
||||
};
|
||||
sys_prefix
|
||||
} else {
|
||||
unvalidated_path
|
||||
};
|
||||
|
||||
// It's important to resolve symlinks here rather than simply making the path absolute,
|
||||
// since system Python installations often only put symlinks in the "expected"
|
||||
// locations for `home` and `site-packages`
|
||||
let canonicalized = match system.canonicalize_path(unvalidated_path) {
|
||||
let sys_prefix = match system.canonicalize_path(sys_prefix) {
|
||||
Ok(path) => path,
|
||||
Err(io_err) => {
|
||||
let unvalidated_path = unvalidated_path.to_path_buf();
|
||||
|
@ -888,69 +925,19 @@ impl SysPrefixPath {
|
|||
}
|
||||
};
|
||||
|
||||
if origin.must_point_directly_to_sys_prefix() {
|
||||
return if system.is_directory(&canonicalized) {
|
||||
Ok(Self {
|
||||
inner: canonicalized,
|
||||
origin,
|
||||
})
|
||||
} else {
|
||||
Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
unvalidated_path.to_path_buf(),
|
||||
origin,
|
||||
None,
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
let sys_prefix = if system.is_file(&canonicalized)
|
||||
&& canonicalized
|
||||
.file_name()
|
||||
.is_some_and(|name| name.starts_with("python"))
|
||||
{
|
||||
// It looks like they passed us a path to a Python executable, e.g. `.venv/bin/python3`.
|
||||
// Try to figure out the `sys.prefix` value from the Python executable.
|
||||
let sys_prefix = if cfg!(windows) {
|
||||
// On Windows, the relative path to the Python executable from `sys.prefix`
|
||||
// is different depending on whether it's a virtual environment or a system installation.
|
||||
// System installations have their executable at `<sys.prefix>/python.exe`,
|
||||
// whereas virtual environments have their executable at `<sys.prefix>/Scripts/python.exe`.
|
||||
canonicalized.parent().and_then(|parent| {
|
||||
if parent.file_name() == Some("Scripts") {
|
||||
parent.parent()
|
||||
} else {
|
||||
Some(parent)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// On Unix, `sys.prefix` is always the grandparent directory of the Python executable,
|
||||
// regardless of whether it's a virtual environment or a system installation.
|
||||
canonicalized.ancestors().nth(2)
|
||||
};
|
||||
let Some(sys_prefix) = sys_prefix else {
|
||||
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
unvalidated_path.to_path_buf(),
|
||||
origin,
|
||||
None,
|
||||
));
|
||||
};
|
||||
sys_prefix.to_path_buf()
|
||||
} else if system.is_directory(&canonicalized) {
|
||||
canonicalized
|
||||
} else {
|
||||
if !system.is_directory(&sys_prefix) {
|
||||
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
unvalidated_path.to_path_buf(),
|
||||
origin,
|
||||
None,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: sys_prefix,
|
||||
origin,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_executable_home_path(path: &PythonHomePath) -> Option<Self> {
|
||||
// No need to check whether `path.parent()` is a directory:
|
||||
// the parent of a canonicalised path that is known to exist
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue