Compare commits
9 Commits
0.11.13
...
dylan/stab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bda1837d9 | ||
|
|
e667d53828 | ||
|
|
a071114638 | ||
|
|
86ae9fbc08 | ||
|
|
11966beeec | ||
|
|
1274521f9f | ||
|
|
8d24760643 | ||
|
|
db8db536f8 | ||
|
|
cb8246bc5f |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3965,6 +3965,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"compact_str",
|
||||
"countme",
|
||||
"dir-test",
|
||||
@@ -3977,6 +3978,7 @@ dependencies = [
|
||||
"ordermap",
|
||||
"quickcheck",
|
||||
"quickcheck_macros",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
|
||||
@@ -566,7 +566,7 @@ fn venv() -> Result<()> {
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
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
|
||||
");
|
||||
});
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ use crate::docstrings::extraction::ExtractionTarget;
|
||||
use crate::importer::{ImportRequest, Importer, ResolutionError};
|
||||
use crate::noqa::NoqaMapping;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::{is_semantic_errors_enabled, is_undefined_export_in_dunder_init_enabled};
|
||||
use crate::preview::is_semantic_errors_enabled;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pyflakes::rules::{
|
||||
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
|
||||
@@ -2900,17 +2900,13 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
} else {
|
||||
if self.enabled(Rule::UndefinedExport) {
|
||||
if is_undefined_export_in_dunder_init_enabled(self.settings)
|
||||
|| !self.path.ends_with("__init__.py")
|
||||
{
|
||||
self.report_diagnostic(
|
||||
pyflakes::rules::UndefinedExport {
|
||||
name: name.to_string(),
|
||||
},
|
||||
range,
|
||||
)
|
||||
.set_parent(definition.start());
|
||||
}
|
||||
self.report_diagnostic(
|
||||
pyflakes::rules::UndefinedExport {
|
||||
name: name.to_string(),
|
||||
},
|
||||
range,
|
||||
)
|
||||
.set_parent(definition.start());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,11 +111,6 @@ pub(crate) const fn is_support_slices_in_literal_concatenation_enabled(
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/11370
|
||||
pub(crate) const fn is_undefined_export_in_dunder_init_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/14236
|
||||
pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
|
||||
@@ -233,7 +233,6 @@ mod tests {
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_27__all_mistyped/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_28__all_multiple/__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_29__all_conditional/__init__.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("__init__.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::Violation;
|
||||
/// Including an undefined name in `__all__` is likely to raise `NameError` at
|
||||
/// runtime, when the module is imported.
|
||||
///
|
||||
/// In [preview], this rule will flag undefined names in `__init__.py` file,
|
||||
/// This rule will flag undefined names in `__init__.py` file
|
||||
/// even if those names implicitly refer to other modules in the package. Users
|
||||
/// that rely on implicit exports should disable this rule in `__init__.py`
|
||||
/// files via [`lint.per-file-ignores`].
|
||||
@@ -37,8 +37,6 @@ use crate::Violation;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `__all__`](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UndefinedExport {
|
||||
pub name: String,
|
||||
|
||||
@@ -9,3 +9,27 @@ __init__.py:1:8: F401 `os` imported but unused
|
||||
3 | print(__path__)
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
|
||||
__init__.py:5:12: F822 Undefined name `a` in `__all__`
|
||||
|
|
||||
3 | print(__path__)
|
||||
4 |
|
||||
5 | __all__ = ["a", "b", "c"]
|
||||
| ^^^ F822
|
||||
|
|
||||
|
||||
__init__.py:5:17: F822 Undefined name `b` in `__all__`
|
||||
|
|
||||
3 | print(__path__)
|
||||
4 |
|
||||
5 | __all__ = ["a", "b", "c"]
|
||||
| ^^^ F822
|
||||
|
|
||||
|
||||
__init__.py:5:22: F822 Undefined name `c` in `__all__`
|
||||
|
|
||||
3 | print(__path__)
|
||||
4 |
|
||||
5 | __all__ = ["a", "b", "c"]
|
||||
| ^^^ F822
|
||||
|
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
__init__.py:5:12: F822 Undefined name `a` in `__all__`
|
||||
|
|
||||
3 | print(__path__)
|
||||
4 |
|
||||
5 | __all__ = ["a", "b", "c"]
|
||||
| ^^^ F822
|
||||
|
|
||||
|
||||
__init__.py:5:17: F822 Undefined name `b` in `__all__`
|
||||
|
|
||||
3 | print(__path__)
|
||||
4 |
|
||||
5 | __all__ = ["a", "b", "c"]
|
||||
| ^^^ F822
|
||||
|
|
||||
|
||||
__init__.py:5:22: F822 Undefined name `c` in `__all__`
|
||||
|
|
||||
3 | print(__path__)
|
||||
4 |
|
||||
5 | __all__ = ["a", "b", "c"]
|
||||
| ^^^ F822
|
||||
|
|
||||
@@ -7,6 +7,7 @@ use ruff_python_semantic::analyze::function_type::is_stub;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::fastapi::rules::is_fastapi_route;
|
||||
|
||||
/// ## What it does
|
||||
|
||||
@@ -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.
|
||||
ty failed
|
||||
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
|
||||
@@ -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.
|
||||
ty failed
|
||||
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(())
|
||||
@@ -1045,6 +1045,14 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> {
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
description = "Some description"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
|
||||
[tool.ty.environment]
|
||||
python = "not-a-directory-or-executable"
|
||||
"#,
|
||||
@@ -1052,8 +1060,7 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> {
|
||||
("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
|
||||
exit_code: 2
|
||||
----- 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.
|
||||
ty failed
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -182,10 +182,14 @@ impl Options {
|
||||
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
|
||||
python_path: python
|
||||
.map(|python_path| {
|
||||
PythonPath::sys_prefix(
|
||||
python_path.absolute(project_root, system),
|
||||
SysPrefixPathOrigin::PythonCliFlag,
|
||||
)
|
||||
let origin = match python_path.source() {
|
||||
ValueSource::Cli => SysPrefixPathOrigin::PythonCliFlag,
|
||||
ValueSource::File(path) => SysPrefixPathOrigin::ConfigFileSetting(
|
||||
path.clone(),
|
||||
python_path.range(),
|
||||
),
|
||||
};
|
||||
PythonPath::sys_prefix(python_path.absolute(project_root, system), origin)
|
||||
})
|
||||
.or_else(|| {
|
||||
std::env::var("VIRTUAL_ENV").ok().map(|virtual_env| {
|
||||
|
||||
@@ -32,6 +32,10 @@ impl ValueSource {
|
||||
ValueSource::Cli => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_cli(&self) -> bool {
|
||||
matches!(self, ValueSource::Cli)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -324,6 +328,14 @@ impl RelativePathBuf {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &ValueSource {
|
||||
self.0.source()
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Option<TextRange> {
|
||||
self.0.range()
|
||||
}
|
||||
|
||||
/// Returns the owned relative path.
|
||||
pub fn into_path_buf(self) -> SystemPathBuf {
|
||||
self.0.into_inner()
|
||||
|
||||
@@ -12,6 +12,7 @@ license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_annotate_snippets = { workspace = true }
|
||||
ruff_index = { workspace = true, features = ["salsa"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["salsa"] }
|
||||
@@ -25,6 +26,7 @@ ruff_python_trivia = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
drop_bomb = { workspace = true }
|
||||
|
||||
@@ -235,7 +235,7 @@ impl SearchPaths {
|
||||
|
||||
let (site_packages_paths, python_version) = match python_path {
|
||||
PythonPath::IntoSysPrefix(path, origin) => {
|
||||
if *origin == SysPrefixPathOrigin::LocalVenv {
|
||||
if origin == &SysPrefixPathOrigin::LocalVenv {
|
||||
tracing::debug!("Discovering virtual environment in `{path}`");
|
||||
let virtual_env_directory = path.join(".venv");
|
||||
|
||||
@@ -260,7 +260,7 @@ impl SearchPaths {
|
||||
})
|
||||
} else {
|
||||
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.
|
||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct SearchPathSettings {
|
||||
/// 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,
|
||||
@@ -260,7 +259,6 @@ impl SearchPathSettings {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum PythonPath {
|
||||
/// 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`
|
||||
|
||||
@@ -8,16 +8,17 @@
|
||||
//! reasonably ask us to type-check code assuming that the code runs
|
||||
//! on Linux.)
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_trivia::Cursor;
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{TextLen, TextRange};
|
||||
|
||||
use crate::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource};
|
||||
@@ -102,7 +103,7 @@ impl PythonEnvironment {
|
||||
Ok(venv) => Ok(Self::Virtual(venv)),
|
||||
// If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment
|
||||
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(path, _))
|
||||
if !origin.must_be_virtual_env() =>
|
||||
if !path.origin.must_be_virtual_env() =>
|
||||
{
|
||||
Ok(Self::System(SystemEnvironment::new(path)))
|
||||
}
|
||||
@@ -530,33 +531,21 @@ impl SystemEnvironment {
|
||||
}
|
||||
|
||||
/// Enumeration of ways in which `site-packages` discovery can fail.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SitePackagesDiscoveryError {
|
||||
/// `site-packages` discovery failed because the provided path couldn't be canonicalized.
|
||||
#[error("Invalid {1}: `{0}` could not be canonicalized")]
|
||||
CanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, #[source] io::Error),
|
||||
CanonicalizationError(SystemPathBuf, SysPrefixPathOrigin, io::Error),
|
||||
|
||||
/// `site-packages` discovery failed because the provided path doesn't appear to point to
|
||||
/// a Python executable or a `sys.prefix` directory.
|
||||
#[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),
|
||||
PathNotExecutableOrDirectory(SystemPathBuf, SysPrefixPathOrigin, Option<io::Error>),
|
||||
|
||||
/// `site-packages` discovery failed because the [`SysPrefixPathOrigin`] indicated that
|
||||
/// the provided path should point to the `sys.prefix` of a virtual environment,
|
||||
/// 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, #[source] io::Error),
|
||||
NoPyvenvCfgFile(SysPrefixPath, io::Error),
|
||||
|
||||
/// `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),
|
||||
|
||||
/// `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
|
||||
/// 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.
|
||||
#[error(
|
||||
"Failed to iterate over the contents of the `lib` directory of the Python installation at {1}"
|
||||
)]
|
||||
CouldNotReadLibDirectory(#[source] io::Error, SysPrefixPath),
|
||||
CouldNotReadLibDirectory(SysPrefixPath, io::Error),
|
||||
|
||||
/// We looked everywhere we could think of for the `site-packages` directory,
|
||||
/// 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),
|
||||
}
|
||||
|
||||
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
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum PyvenvCfgParseErrorKind {
|
||||
@@ -615,7 +736,10 @@ fn site_packages_directory_from_sys_prefix(
|
||||
implementation: PythonImplementation,
|
||||
system: &dyn System,
|
||||
) -> 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") {
|
||||
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
|
||||
.read_directory(&sys_prefix_path.join("lib"))
|
||||
.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 {
|
||||
@@ -743,14 +867,15 @@ impl SysPrefixPath {
|
||||
// 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 = system
|
||||
.canonicalize_path(unvalidated_path)
|
||||
.map_err(|io_err| {
|
||||
let canonicalized = match system.canonicalize_path(unvalidated_path) {
|
||||
Ok(path) => path,
|
||||
Err(io_err) => {
|
||||
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(
|
||||
unvalidated_path,
|
||||
origin,
|
||||
Some(io_err),
|
||||
)
|
||||
} else {
|
||||
SitePackagesDiscoveryError::CanonicalizationError(
|
||||
@@ -758,22 +883,24 @@ impl SysPrefixPath {
|
||||
origin,
|
||||
io_err,
|
||||
)
|
||||
}
|
||||
})?;
|
||||
};
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
if origin.must_point_directly_to_sys_prefix() {
|
||||
return system
|
||||
.is_directory(&canonicalized)
|
||||
.then_some(Self {
|
||||
return if system.is_directory(&canonicalized) {
|
||||
Ok(Self {
|
||||
inner: canonicalized,
|
||||
origin,
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
unvalidated_path.to_path_buf(),
|
||||
origin,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
unvalidated_path.to_path_buf(),
|
||||
origin,
|
||||
None,
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
canonicalized.ancestors().nth(2)
|
||||
};
|
||||
sys_prefix.map(SystemPath::to_path_buf).ok_or_else(|| {
|
||||
SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
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 {
|
||||
return Err(SitePackagesDiscoveryError::PathNotExecutableOrDirectory(
|
||||
unvalidated_path.to_path_buf(),
|
||||
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.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
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
|
||||
PythonCliFlag,
|
||||
/// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
|
||||
@@ -875,10 +1000,13 @@ pub enum SysPrefixPathOrigin {
|
||||
impl SysPrefixPathOrigin {
|
||||
/// Whether the given `sys.prefix` path must be a virtual environment (rather than a system
|
||||
/// Python environment).
|
||||
pub(crate) const fn must_be_virtual_env(self) -> bool {
|
||||
pub(crate) const fn must_be_virtual_env(&self) -> bool {
|
||||
match self {
|
||||
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
|
||||
/// 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 {
|
||||
Self::PythonCliFlag => false,
|
||||
Self::PythonCliFlag | Self::ConfigFileSetting(..) => false,
|
||||
Self::VirtualEnvVar
|
||||
| Self::CondaPrefixVar
|
||||
| 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 {
|
||||
match self {
|
||||
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::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"),
|
||||
Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
|
||||
@@ -1107,7 +1236,7 @@ mod tests {
|
||||
#[track_caller]
|
||||
fn run(self) -> PythonEnvironment {
|
||||
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");
|
||||
|
||||
let expect_virtual_env = self.virtual_env.is_some();
|
||||
@@ -1144,7 +1273,7 @@ mod tests {
|
||||
venv.root_path,
|
||||
SysPrefixPath {
|
||||
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
||||
origin: self.origin,
|
||||
origin: self.origin.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1216,7 +1345,7 @@ mod tests {
|
||||
env.root_path,
|
||||
SysPrefixPath {
|
||||
inner: self.system.canonicalize_path(expected_env_path).unwrap(),
|
||||
origin: self.origin,
|
||||
origin: self.origin.clone(),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -4967,7 +4967,10 @@ impl<'db> Type<'db> {
|
||||
/// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type
|
||||
/// expression, it names the type `Type::NominalInstance(builtins.int)`, that is, all objects whose
|
||||
/// `__class__` is `int`.
|
||||
pub fn in_type_expression(
|
||||
///
|
||||
/// The `scope_id` argument must always be a scope from the file we are currently inferring, so
|
||||
/// as to avoid cross-module AST dependency.
|
||||
pub(crate) fn in_type_expression(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
scope_id: ScopeId<'db>,
|
||||
|
||||
@@ -9021,9 +9021,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
) -> Type<'db> {
|
||||
let arguments = &*subscript_node.slice;
|
||||
let (args, args_number) = if let ast::Expr::Tuple(t) = arguments {
|
||||
(Either::Left(t), t.len())
|
||||
(t.iter(), t.len())
|
||||
} else {
|
||||
(Either::Right([arguments]), 1)
|
||||
(std::slice::from_ref(arguments).iter(), 1)
|
||||
};
|
||||
if args_number != expected_arg_count {
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript_node) {
|
||||
|
||||
@@ -66,7 +66,9 @@ _Using configuration file path:_
|
||||
```lua
|
||||
require('lspconfig').ruff.setup {
|
||||
init_options = {
|
||||
configuration = "~/path/to/ruff.toml"
|
||||
settings = {
|
||||
configuration = "~/path/to/ruff.toml"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -117,20 +119,22 @@ _Using inline configuration:_
|
||||
```lua
|
||||
require('lspconfig').ruff.setup {
|
||||
init_options = {
|
||||
configuration = {
|
||||
lint = {
|
||||
unfixable = {"F401"},
|
||||
["extend-select"] = {"TID251"},
|
||||
["flake8-tidy-imports"] = {
|
||||
["banned-api"] = {
|
||||
["typing.TypedDict"] = {
|
||||
msg = "Use `typing_extensions.TypedDict` instead"
|
||||
settings = {
|
||||
configuration = {
|
||||
lint = {
|
||||
unfixable = {"F401"},
|
||||
["extend-select"] = {"TID251"},
|
||||
["flake8-tidy-imports"] = {
|
||||
["banned-api"] = {
|
||||
["typing.TypedDict"] = {
|
||||
msg = "Use `typing_extensions.TypedDict` instead"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
format = {
|
||||
["quote-style"] = "single"
|
||||
}
|
||||
},
|
||||
format = {
|
||||
["quote-style"] = "single"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user