mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:18 +00:00
[ty] Track the origin of the environment.python
setting for better error messages (#18483)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
This commit is contained in:
parent
8d24760643
commit
1274521f9f
9 changed files with 313 additions and 75 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -3965,6 +3965,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"camino",
|
"camino",
|
||||||
|
"colored 3.0.0",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"countme",
|
"countme",
|
||||||
"dir-test",
|
"dir-test",
|
||||||
|
@ -3977,6 +3978,7 @@ dependencies = [
|
||||||
"ordermap",
|
"ordermap",
|
||||||
"quickcheck",
|
"quickcheck",
|
||||||
"quickcheck_macros",
|
"quickcheck_macros",
|
||||||
|
"ruff_annotate_snippets",
|
||||||
"ruff_db",
|
"ruff_db",
|
||||||
"ruff_index",
|
"ruff_index",
|
||||||
"ruff_macros",
|
"ruff_macros",
|
||||||
|
|
|
@ -566,7 +566,7 @@ fn venv() -> Result<()> {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
ruff failed
|
ruff failed
|
||||||
Cause: Invalid search path settings
|
Cause: Invalid search path settings
|
||||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `none` does not point to a Python executable or a directory on disk
|
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `none`: does not point to a Python executable or a directory on disk
|
||||||
");
|
");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -974,7 +974,7 @@ fn python_cli_argument_virtual_environment() -> anyhow::Result<()> {
|
||||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
ty failed
|
ty failed
|
||||||
Cause: Invalid search path settings
|
Cause: Invalid search path settings
|
||||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `<temp_dir>/my-venv/foo/some_other_file.txt` does not point to a Python executable or a directory on disk
|
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk
|
||||||
");
|
");
|
||||||
|
|
||||||
// And so are paths that do not exist on disk
|
// And so are paths that do not exist on disk
|
||||||
|
@ -987,7 +987,7 @@ fn python_cli_argument_virtual_environment() -> anyhow::Result<()> {
|
||||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
ty failed
|
ty failed
|
||||||
Cause: Invalid search path settings
|
Cause: Invalid search path settings
|
||||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `<temp_dir>/not-a-directory-or-executable` does not point to a Python executable or a directory on disk
|
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk
|
||||||
");
|
");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1045,6 +1045,14 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> {
|
||||||
(
|
(
|
||||||
"pyproject.toml",
|
"pyproject.toml",
|
||||||
r#"
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "test"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Some description"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
[tool.ty.environment]
|
[tool.ty.environment]
|
||||||
python = "not-a-directory-or-executable"
|
python = "not-a-directory-or-executable"
|
||||||
"#,
|
"#,
|
||||||
|
@ -1052,8 +1060,7 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> {
|
||||||
("test.py", ""),
|
("test.py", ""),
|
||||||
])?;
|
])?;
|
||||||
|
|
||||||
// TODO: this error message should say "invalid `python` configuration setting" rather than "invalid `--python` argument"
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
assert_cmd_snapshot!(case.command(), @r"
|
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
@ -1062,8 +1069,92 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> {
|
||||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
ty failed
|
ty failed
|
||||||
Cause: Invalid search path settings
|
Cause: Invalid search path settings
|
||||||
Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `<temp_dir>/not-a-directory-or-executable` does not point to a Python executable or a directory on disk
|
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
|
||||||
");
|
|
||||||
|
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||||
|
|
|
||||||
|
9 |
|
||||||
|
10 | [tool.ty.environment]
|
||||||
|
11 | python = "not-a-directory-or-executable"
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk
|
||||||
|
|
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> {
|
||||||
|
let case = TestCase::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.environment]
|
||||||
|
python = "directory-but-no-site-packages"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
("directory-but-no-site-packages/lib/foo.py", ""),
|
||||||
|
("test.py", ""),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
ty failed
|
||||||
|
Cause: Invalid search path settings
|
||||||
|
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
|
||||||
|
|
||||||
|
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
2 | [tool.ty.environment]
|
||||||
|
3 | python = "directory-but-no-site-packages"
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable
|
||||||
|
|
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// This error message is never emitted on Windows, because Windows installations have simpler layouts
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[test]
|
||||||
|
fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
|
||||||
|
let case = TestCase::with_files([
|
||||||
|
(
|
||||||
|
"pyproject.toml",
|
||||||
|
r#"
|
||||||
|
[tool.ty.environment]
|
||||||
|
python = "directory-but-no-site-packages"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
("directory-but-no-site-packages/foo.py", ""),
|
||||||
|
("test.py", ""),
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(case.command(), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||||
|
ty failed
|
||||||
|
Cause: Invalid search path settings
|
||||||
|
Cause: Failed to discover the site-packages directory: Failed to iterate over the contents of the `lib` directory of the Python installation
|
||||||
|
|
||||||
|
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
2 | [tool.ty.environment]
|
||||||
|
3 | python = "directory-but-no-site-packages"
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
"#);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,10 +182,14 @@ impl Options {
|
||||||
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
|
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
|
||||||
python_path: python
|
python_path: python
|
||||||
.map(|python_path| {
|
.map(|python_path| {
|
||||||
PythonPath::sys_prefix(
|
let origin = match python_path.source() {
|
||||||
python_path.absolute(project_root, system),
|
ValueSource::Cli => SysPrefixPathOrigin::PythonCliFlag,
|
||||||
SysPrefixPathOrigin::PythonCliFlag,
|
ValueSource::File(path) => SysPrefixPathOrigin::ConfigFileSetting(
|
||||||
)
|
path.clone(),
|
||||||
|
python_path.range(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
PythonPath::sys_prefix(python_path.absolute(project_root, system), origin)
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
std::env::var("VIRTUAL_ENV").ok().map(|virtual_env| {
|
std::env::var("VIRTUAL_ENV").ok().map(|virtual_env| {
|
||||||
|
|
|
@ -32,6 +32,10 @@ impl ValueSource {
|
||||||
ValueSource::Cli => None,
|
ValueSource::Cli => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn is_cli(&self) -> bool {
|
||||||
|
matches!(self, ValueSource::Cli)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
@ -324,6 +328,14 @@ impl RelativePathBuf {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn source(&self) -> &ValueSource {
|
||||||
|
self.0.source()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range(&self) -> Option<TextRange> {
|
||||||
|
self.0.range()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the owned relative path.
|
/// Returns the owned relative path.
|
||||||
pub fn into_path_buf(self) -> SystemPathBuf {
|
pub fn into_path_buf(self) -> SystemPathBuf {
|
||||||
self.0.into_inner()
|
self.0.into_inner()
|
||||||
|
|
|
@ -12,6 +12,7 @@ license = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ruff_db = { workspace = true }
|
ruff_db = { workspace = true }
|
||||||
|
ruff_annotate_snippets = { workspace = true }
|
||||||
ruff_index = { workspace = true, features = ["salsa"] }
|
ruff_index = { workspace = true, features = ["salsa"] }
|
||||||
ruff_macros = { workspace = true }
|
ruff_macros = { workspace = true }
|
||||||
ruff_python_ast = { workspace = true, features = ["salsa"] }
|
ruff_python_ast = { workspace = true, features = ["salsa"] }
|
||||||
|
@ -25,6 +26,7 @@ ruff_python_trivia = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
camino = { workspace = true }
|
camino = { workspace = true }
|
||||||
|
colored = { workspace = true }
|
||||||
compact_str = { workspace = true }
|
compact_str = { workspace = true }
|
||||||
countme = { workspace = true }
|
countme = { workspace = true }
|
||||||
drop_bomb = { workspace = true }
|
drop_bomb = { workspace = true }
|
||||||
|
|
|
@ -235,7 +235,7 @@ impl SearchPaths {
|
||||||
|
|
||||||
let (site_packages_paths, python_version) = match python_path {
|
let (site_packages_paths, python_version) = match python_path {
|
||||||
PythonPath::IntoSysPrefix(path, origin) => {
|
PythonPath::IntoSysPrefix(path, origin) => {
|
||||||
if *origin == SysPrefixPathOrigin::LocalVenv {
|
if origin == &SysPrefixPathOrigin::LocalVenv {
|
||||||
tracing::debug!("Discovering virtual environment in `{path}`");
|
tracing::debug!("Discovering virtual environment in `{path}`");
|
||||||
let virtual_env_directory = path.join(".venv");
|
let virtual_env_directory = path.join(".venv");
|
||||||
|
|
||||||
|
@ -260,7 +260,7 @@ impl SearchPaths {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("Resolving {origin}: {path}");
|
tracing::debug!("Resolving {origin}: {path}");
|
||||||
PythonEnvironment::new(path, *origin, system)?.into_settings(system)?
|
PythonEnvironment::new(path, origin.clone(), system)?.into_settings(system)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,7 +228,6 @@ impl Default for PythonVersionWithSource {
|
||||||
|
|
||||||
/// Configures the search paths for module resolution.
|
/// Configures the search paths for module resolution.
|
||||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
|
||||||
pub struct SearchPathSettings {
|
pub struct SearchPathSettings {
|
||||||
/// List of user-provided paths that should take first priority in the module resolution.
|
/// List of user-provided paths that should take first priority in the module resolution.
|
||||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||||
|
@ -260,7 +259,6 @@ impl SearchPathSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
pub enum PythonPath {
|
pub enum PythonPath {
|
||||||
/// A path that either represents the value of [`sys.prefix`] at runtime in Python
|
/// A path that either represents the value of [`sys.prefix`] at runtime in Python
|
||||||
/// for a given Python executable, or which represents a path relative to `sys.prefix`
|
/// for a given Python executable, or which represents a path relative to `sys.prefix`
|
||||||
|
|
|
@ -8,16 +8,17 @@
|
||||||
//! reasonably ask us to type-check code assuming that the code runs
|
//! reasonably ask us to type-check code assuming that the code runs
|
||||||
//! on Linux.)
|
//! on Linux.)
|
||||||
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::{fmt, sync::Arc};
|
use std::{fmt, sync::Arc};
|
||||||
|
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
|
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
||||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||||
use ruff_python_ast::PythonVersion;
|
use ruff_python_ast::PythonVersion;
|
||||||
use ruff_python_trivia::Cursor;
|
use ruff_python_trivia::Cursor;
|
||||||
|
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||||
use ruff_text_size::{TextLen, TextRange};
|
use ruff_text_size::{TextLen, TextRange};
|
||||||
|
|
||||||
use crate::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource};
|
use crate::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource};
|
||||||
|
@ -102,7 +103,7 @@ impl PythonEnvironment {
|
||||||
Ok(venv) => Ok(Self::Virtual(venv)),
|
Ok(venv) => Ok(Self::Virtual(venv)),
|
||||||
// If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment
|
// If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment
|
||||||
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(path, _))
|
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(path, _))
|
||||||
if !origin.must_be_virtual_env() =>
|
if !path.origin.must_be_virtual_env() =>
|
||||||
{
|
{
|
||||||
Ok(Self::System(SystemEnvironment::new(path)))
|
Ok(Self::System(SystemEnvironment::new(path)))
|
||||||
}
|
}
|
||||||
|
@ -530,33 +531,21 @@ impl SystemEnvironment {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumeration of ways in which `site-packages` discovery can fail.
|
/// Enumeration of ways in which `site-packages` discovery can fail.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum SitePackagesDiscoveryError {
|
pub(crate) enum SitePackagesDiscoveryError {
|
||||||
/// `site-packages` discovery failed because the provided path couldn't be canonicalized.
|
/// `site-packages` discovery failed because the provided path couldn't be canonicalized.
|
||||||
#[error("Invalid {1}: `{0}` could not be canonicalized")]
|
CanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, io::Error),
|
||||||
CanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, #[source] io::Error),
|
|
||||||
|
|
||||||
/// `site-packages` discovery failed because the provided path doesn't appear to point to
|
/// `site-packages` discovery failed because the provided path doesn't appear to point to
|
||||||
/// a Python executable or a `sys.prefix` directory.
|
/// a Python executable or a `sys.prefix` directory.
|
||||||
#[error(
|
PathNotExecutableOrDirectory(SystemPathBuf, SysPrefixPathOrigin, Option<io::Error>),
|
||||||
"Invalid {1}: `{0}` does not point to a {thing}",
|
|
||||||
|
|
||||||
thing = if .1.must_point_directly_to_sys_prefix() {
|
|
||||||
"directory on disk"
|
|
||||||
} else {
|
|
||||||
"Python executable or a directory on disk"
|
|
||||||
}
|
|
||||||
)]
|
|
||||||
PathNotExecutableOrDirectory(SystemPathBuf, SysPrefixPathOrigin),
|
|
||||||
|
|
||||||
/// `site-packages` discovery failed because the [`SysPrefixPathOrigin`] indicated that
|
/// `site-packages` discovery failed because the [`SysPrefixPathOrigin`] indicated that
|
||||||
/// the provided path should point to the `sys.prefix` of a virtual environment,
|
/// the provided path should point to the `sys.prefix` of a virtual environment,
|
||||||
/// but there was no file at `<sys.prefix>/pyvenv.cfg`.
|
/// but there was no file at `<sys.prefix>/pyvenv.cfg`.
|
||||||
#[error("{} points to a broken venv with no pyvenv.cfg file", .0.origin)]
|
NoPyvenvCfgFile(SysPrefixPath, io::Error),
|
||||||
NoPyvenvCfgFile(SysPrefixPath, #[source] io::Error),
|
|
||||||
|
|
||||||
/// `site-packages` discovery failed because the `pyvenv.cfg` file could not be parsed.
|
/// `site-packages` discovery failed because the `pyvenv.cfg` file could not be parsed.
|
||||||
#[error("Failed to parse the pyvenv.cfg file at {0} because {1}")]
|
|
||||||
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
|
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
|
||||||
|
|
||||||
/// `site-packages` discovery failed because we're on a Unix system,
|
/// `site-packages` discovery failed because we're on a Unix system,
|
||||||
|
@ -564,17 +553,149 @@ pub(crate) enum SitePackagesDiscoveryError {
|
||||||
/// would be relative to the `sys.prefix` path, and we tried to fallback to iterating
|
/// would be relative to the `sys.prefix` path, and we tried to fallback to iterating
|
||||||
/// through the `<sys.prefix>/lib` directory looking for a `site-packages` directory,
|
/// through the `<sys.prefix>/lib` directory looking for a `site-packages` directory,
|
||||||
/// but we came across some I/O error while trying to do so.
|
/// but we came across some I/O error while trying to do so.
|
||||||
#[error(
|
CouldNotReadLibDirectory(SysPrefixPath, io::Error),
|
||||||
"Failed to iterate over the contents of the `lib` directory of the Python installation at {1}"
|
|
||||||
)]
|
|
||||||
CouldNotReadLibDirectory(#[source] io::Error, SysPrefixPath),
|
|
||||||
|
|
||||||
/// We looked everywhere we could think of for the `site-packages` directory,
|
/// We looked everywhere we could think of for the `site-packages` directory,
|
||||||
/// but none could be found despite our best endeavours.
|
/// but none could be found despite our best endeavours.
|
||||||
#[error("Could not find the `site-packages` directory for the Python installation at {0}")]
|
|
||||||
NoSitePackagesDirFound(SysPrefixPath),
|
NoSitePackagesDirFound(SysPrefixPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for SitePackagesDiscoveryError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::CanonicalizationError(_, _, io_err) => Some(io_err),
|
||||||
|
Self::PathNotExecutableOrDirectory(_, _, io_err) => {
|
||||||
|
io_err.as_ref().map(|e| e as &dyn std::error::Error)
|
||||||
|
}
|
||||||
|
Self::NoPyvenvCfgFile(_, io_err) => Some(io_err),
|
||||||
|
Self::PyvenvCfgParseError(_, _) => None,
|
||||||
|
Self::CouldNotReadLibDirectory(_, io_err) => Some(io_err),
|
||||||
|
Self::NoSitePackagesDirFound(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SitePackagesDiscoveryError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::CanonicalizationError(given_path, origin, _) => {
|
||||||
|
display_error(f, origin, given_path, "Failed to canonicalize", None)
|
||||||
|
}
|
||||||
|
Self::PathNotExecutableOrDirectory(path, origin, _) => {
|
||||||
|
let thing = if origin.must_point_directly_to_sys_prefix() {
|
||||||
|
"directory on disk"
|
||||||
|
} else {
|
||||||
|
"Python executable or a directory on disk"
|
||||||
|
};
|
||||||
|
display_error(
|
||||||
|
f,
|
||||||
|
origin,
|
||||||
|
path,
|
||||||
|
&format!("Invalid {origin}"),
|
||||||
|
Some(&format!("does not point to a {thing}")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::NoPyvenvCfgFile(SysPrefixPath { inner, origin }, _) => display_error(
|
||||||
|
f,
|
||||||
|
origin,
|
||||||
|
inner,
|
||||||
|
&format!("Invalid {origin}"),
|
||||||
|
Some("points to a broken venv with no pyvenv.cfg file"),
|
||||||
|
),
|
||||||
|
Self::PyvenvCfgParseError(path, kind) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Failed to parse the `pyvenv.cfg` file at `{path}` because {kind}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::CouldNotReadLibDirectory(SysPrefixPath { inner, origin }, _) => display_error(
|
||||||
|
f,
|
||||||
|
origin,
|
||||||
|
inner,
|
||||||
|
"Failed to iterate over the contents of the `lib` directory of the Python installation",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Self::NoSitePackagesDirFound(SysPrefixPath { inner, origin }) => display_error(
|
||||||
|
f,
|
||||||
|
origin,
|
||||||
|
inner,
|
||||||
|
&format!("Invalid {origin}"),
|
||||||
|
Some(
|
||||||
|
"Could not find a `site-packages` directory for this Python installation/executable",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_error(
|
||||||
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
|
sys_prefix_origin: &SysPrefixPathOrigin,
|
||||||
|
given_path: &SystemPath,
|
||||||
|
primary_message: &str,
|
||||||
|
secondary_message: Option<&str>,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
let fallback: &mut dyn FnMut() -> std::fmt::Result = &mut || {
|
||||||
|
f.write_str(primary_message)?;
|
||||||
|
write!(f, " `{given_path}`")?;
|
||||||
|
if let Some(secondary_message) = secondary_message {
|
||||||
|
f.write_str(": ")?;
|
||||||
|
f.write_str(secondary_message)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let SysPrefixPathOrigin::ConfigFileSetting(config_file_path, Some(setting_range)) =
|
||||||
|
sys_prefix_origin
|
||||||
|
else {
|
||||||
|
return fallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(config_file_source) = std::fs::read_to_string((**config_file_path).as_ref()) else {
|
||||||
|
return fallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
let index = LineIndex::from_source_text(&config_file_source);
|
||||||
|
let source = SourceCode::new(&config_file_source, &index);
|
||||||
|
|
||||||
|
let primary_message = format!(
|
||||||
|
"{primary_message}
|
||||||
|
|
||||||
|
--> Invalid setting in configuration file `{config_file_path}`"
|
||||||
|
);
|
||||||
|
|
||||||
|
let start_index = source.line_index(setting_range.start()).saturating_sub(2);
|
||||||
|
let end_index = source
|
||||||
|
.line_index(setting_range.end())
|
||||||
|
.saturating_add(2)
|
||||||
|
.min(OneIndexed::from_zero_indexed(source.line_count()));
|
||||||
|
|
||||||
|
let start_offset = source.line_start(start_index);
|
||||||
|
let end_offset = source.line_end(end_index);
|
||||||
|
|
||||||
|
let mut annotation = Level::Error.span((setting_range - start_offset).into());
|
||||||
|
|
||||||
|
if let Some(secondary_message) = secondary_message {
|
||||||
|
annotation = annotation.label(secondary_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let snippet = Snippet::source(&config_file_source[TextRange::new(start_offset, end_offset)])
|
||||||
|
.annotation(annotation)
|
||||||
|
.line_start(start_index.get())
|
||||||
|
.fold(false);
|
||||||
|
|
||||||
|
let message = Level::None.title(&primary_message).snippet(snippet);
|
||||||
|
|
||||||
|
let renderer = if colored::control::SHOULD_COLORIZE.should_colorize() {
|
||||||
|
Renderer::styled()
|
||||||
|
} else {
|
||||||
|
Renderer::plain()
|
||||||
|
};
|
||||||
|
let renderer = renderer.cut_indicator("…");
|
||||||
|
|
||||||
|
writeln!(f, "{}", renderer.render(message))
|
||||||
|
}
|
||||||
|
|
||||||
/// The various ways in which parsing a `pyvenv.cfg` file could fail
|
/// The various ways in which parsing a `pyvenv.cfg` file could fail
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum PyvenvCfgParseErrorKind {
|
pub(crate) enum PyvenvCfgParseErrorKind {
|
||||||
|
@ -615,7 +736,10 @@ fn site_packages_directory_from_sys_prefix(
|
||||||
implementation: PythonImplementation,
|
implementation: PythonImplementation,
|
||||||
system: &dyn System,
|
system: &dyn System,
|
||||||
) -> SitePackagesDiscoveryResult<SystemPathBuf> {
|
) -> SitePackagesDiscoveryResult<SystemPathBuf> {
|
||||||
tracing::debug!("Searching for site-packages directory in {sys_prefix_path}");
|
tracing::debug!(
|
||||||
|
"Searching for site-packages directory in sys.prefix {}",
|
||||||
|
sys_prefix_path.inner
|
||||||
|
);
|
||||||
|
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
let site_packages = sys_prefix_path.join(r"Lib\site-packages");
|
let site_packages = sys_prefix_path.join(r"Lib\site-packages");
|
||||||
|
@ -684,7 +808,7 @@ fn site_packages_directory_from_sys_prefix(
|
||||||
for entry_result in system
|
for entry_result in system
|
||||||
.read_directory(&sys_prefix_path.join("lib"))
|
.read_directory(&sys_prefix_path.join("lib"))
|
||||||
.map_err(|io_err| {
|
.map_err(|io_err| {
|
||||||
SitePackagesDiscoveryError::CouldNotReadLibDirectory(io_err, sys_prefix_path.to_owned())
|
SitePackagesDiscoveryError::CouldNotReadLibDirectory(sys_prefix_path.to_owned(), io_err)
|
||||||
})?
|
})?
|
||||||
{
|
{
|
||||||
let Ok(entry) = entry_result else {
|
let Ok(entry) = entry_result else {
|
||||||
|
@ -743,14 +867,15 @@ impl SysPrefixPath {
|
||||||
// It's important to resolve symlinks here rather than simply making the path absolute,
|
// 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"
|
// since system Python installations often only put symlinks in the "expected"
|
||||||
// locations for `home` and `site-packages`
|
// locations for `home` and `site-packages`
|
||||||
let canonicalized = system
|
let canonicalized = match system.canonicalize_path(unvalidated_path) {
|
||||||
.canonicalize_path(unvalidated_path)
|
Ok(path) => path,
|
||||||
.map_err(|io_err| {
|
Err(io_err) => {
|
||||||
let unvalidated_path = unvalidated_path.to_path_buf();
|
let unvalidated_path = unvalidated_path.to_path_buf();
|
||||||
if io_err.kind() == io::ErrorKind::NotFound {
|
let err = if io_err.kind() == io::ErrorKind::NotFound {
|
||||||
SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||||
unvalidated_path,
|
unvalidated_path,
|
||||||
origin,
|
origin,
|
||||||
|
Some(io_err),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
SitePackagesDiscoveryError::CanonicalizationError(
|
SitePackagesDiscoveryError::CanonicalizationError(
|
||||||
|
@ -758,22 +883,24 @@ impl SysPrefixPath {
|
||||||
origin,
|
origin,
|
||||||
io_err,
|
io_err,
|
||||||
)
|
)
|
||||||
|
};
|
||||||
|
return Err(err);
|
||||||
}
|
}
|
||||||
})?;
|
};
|
||||||
|
|
||||||
if origin.must_point_directly_to_sys_prefix() {
|
if origin.must_point_directly_to_sys_prefix() {
|
||||||
return system
|
return if system.is_directory(&canonicalized) {
|
||||||
.is_directory(&canonicalized)
|
Ok(Self {
|
||||||
.then_some(Self {
|
|
||||||
inner: canonicalized,
|
inner: canonicalized,
|
||||||
origin,
|
origin,
|
||||||
})
|
})
|
||||||
.ok_or_else(|| {
|
} else {
|
||||||
SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||||
unvalidated_path.to_path_buf(),
|
unvalidated_path.to_path_buf(),
|
||||||
origin,
|
origin,
|
||||||
)
|
None,
|
||||||
});
|
))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let sys_prefix = if system.is_file(&canonicalized)
|
let sys_prefix = if system.is_file(&canonicalized)
|
||||||
|
@ -800,18 +927,21 @@ impl SysPrefixPath {
|
||||||
// regardless of whether it's a virtual environment or a system installation.
|
// regardless of whether it's a virtual environment or a system installation.
|
||||||
canonicalized.ancestors().nth(2)
|
canonicalized.ancestors().nth(2)
|
||||||
};
|
};
|
||||||
sys_prefix.map(SystemPath::to_path_buf).ok_or_else(|| {
|
let Some(sys_prefix) = sys_prefix else {
|
||||||
SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||||
unvalidated_path.to_path_buf(),
|
unvalidated_path.to_path_buf(),
|
||||||
origin,
|
origin,
|
||||||
)
|
None,
|
||||||
})?
|
));
|
||||||
|
};
|
||||||
|
sys_prefix.to_path_buf()
|
||||||
} else if system.is_directory(&canonicalized) {
|
} else if system.is_directory(&canonicalized) {
|
||||||
canonicalized
|
canonicalized
|
||||||
} else {
|
} else {
|
||||||
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||||
unvalidated_path.to_path_buf(),
|
unvalidated_path.to_path_buf(),
|
||||||
origin,
|
origin,
|
||||||
|
None,
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -847,16 +977,11 @@ impl Deref for SysPrefixPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SysPrefixPath {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "`sys.prefix` path `{}`", self.inner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enumeration of sources a `sys.prefix` path can come from.
|
/// Enumeration of sources a `sys.prefix` path can come from.
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
pub enum SysPrefixPathOrigin {
|
pub enum SysPrefixPathOrigin {
|
||||||
|
/// The `sys.prefix` path came from a configuration file setting: `pyproject.toml` or `ty.toml`
|
||||||
|
ConfigFileSetting(Arc<SystemPathBuf>, Option<TextRange>),
|
||||||
/// The `sys.prefix` path came from a `--python` CLI flag
|
/// The `sys.prefix` path came from a `--python` CLI flag
|
||||||
PythonCliFlag,
|
PythonCliFlag,
|
||||||
/// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
|
/// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
|
||||||
|
@ -875,10 +1000,13 @@ pub enum SysPrefixPathOrigin {
|
||||||
impl SysPrefixPathOrigin {
|
impl SysPrefixPathOrigin {
|
||||||
/// Whether the given `sys.prefix` path must be a virtual environment (rather than a system
|
/// Whether the given `sys.prefix` path must be a virtual environment (rather than a system
|
||||||
/// Python environment).
|
/// Python environment).
|
||||||
pub(crate) const fn must_be_virtual_env(self) -> bool {
|
pub(crate) const fn must_be_virtual_env(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::LocalVenv | Self::VirtualEnvVar => true,
|
Self::LocalVenv | Self::VirtualEnvVar => true,
|
||||||
Self::PythonCliFlag | Self::DerivedFromPyvenvCfg | Self::CondaPrefixVar => false,
|
Self::ConfigFileSetting(..)
|
||||||
|
| Self::PythonCliFlag
|
||||||
|
| Self::DerivedFromPyvenvCfg
|
||||||
|
| Self::CondaPrefixVar => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -886,9 +1014,9 @@ impl SysPrefixPathOrigin {
|
||||||
///
|
///
|
||||||
/// Some variants can point either directly to `sys.prefix` or to a Python executable inside
|
/// Some variants can point either directly to `sys.prefix` or to a Python executable inside
|
||||||
/// the `sys.prefix` directory, e.g. the `--python` CLI flag.
|
/// the `sys.prefix` directory, e.g. the `--python` CLI flag.
|
||||||
pub(crate) const fn must_point_directly_to_sys_prefix(self) -> bool {
|
pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::PythonCliFlag => false,
|
Self::PythonCliFlag | Self::ConfigFileSetting(..) => false,
|
||||||
Self::VirtualEnvVar
|
Self::VirtualEnvVar
|
||||||
| Self::CondaPrefixVar
|
| Self::CondaPrefixVar
|
||||||
| Self::DerivedFromPyvenvCfg
|
| Self::DerivedFromPyvenvCfg
|
||||||
|
@ -897,10 +1025,11 @@ impl SysPrefixPathOrigin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for SysPrefixPathOrigin {
|
impl std::fmt::Display for SysPrefixPathOrigin {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::PythonCliFlag => f.write_str("`--python` argument"),
|
Self::PythonCliFlag => f.write_str("`--python` argument"),
|
||||||
|
Self::ConfigFileSetting(_, _) => f.write_str("`environment.python` setting"),
|
||||||
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
|
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
|
||||||
Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"),
|
Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"),
|
||||||
Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
|
Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
|
||||||
|
@ -1107,7 +1236,7 @@ mod tests {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn run(self) -> PythonEnvironment {
|
fn run(self) -> PythonEnvironment {
|
||||||
let env_path = self.build();
|
let env_path = self.build();
|
||||||
let env = PythonEnvironment::new(env_path.clone(), self.origin, &self.system)
|
let env = PythonEnvironment::new(env_path.clone(), self.origin.clone(), &self.system)
|
||||||
.expect("Expected environment construction to succeed");
|
.expect("Expected environment construction to succeed");
|
||||||
|
|
||||||
let expect_virtual_env = self.virtual_env.is_some();
|
let expect_virtual_env = self.virtual_env.is_some();
|
||||||
|
@ -1144,7 +1273,7 @@ mod tests {
|
||||||
venv.root_path,
|
venv.root_path,
|
||||||
SysPrefixPath {
|
SysPrefixPath {
|
||||||
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
||||||
origin: self.origin,
|
origin: self.origin.clone(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -1216,7 +1345,7 @@ mod tests {
|
||||||
env.root_path,
|
env.root_path,
|
||||||
SysPrefixPath {
|
SysPrefixPath {
|
||||||
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
||||||
origin: self.origin,
|
origin: self.origin.clone(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue