Compare commits
19 Commits
0.5.7
...
charlie/wa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23bf395e2 | ||
|
|
383676e332 | ||
|
|
feba5031dc | ||
|
|
0c2b88f224 | ||
|
|
cf1a57df5a | ||
|
|
597c5f9124 | ||
|
|
69e1c567d4 | ||
|
|
37b9bac403 | ||
|
|
83db48d316 | ||
|
|
c4e651921b | ||
|
|
b595346213 | ||
|
|
253474b312 | ||
|
|
a176679b24 | ||
|
|
1f51048fa4 | ||
|
|
2abfab0f9b | ||
|
|
64f1f3468d | ||
|
|
ffaa35eafe | ||
|
|
c906b0183b | ||
|
|
bc5b9b81dd |
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -1918,6 +1918,7 @@ dependencies = [
|
||||
"libc",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_linter",
|
||||
@@ -1941,6 +1942,7 @@ dependencies = [
|
||||
"console_log",
|
||||
"js-sys",
|
||||
"log",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
@@ -2104,6 +2106,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"mimalloc",
|
||||
"once_cell",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_linter",
|
||||
@@ -2154,6 +2157,8 @@ dependencies = [
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
"web-time",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -72,6 +72,26 @@ runs or when restoring from a persistent cache. This can be confusing for users
|
||||
don't understand why a specific lint violation isn't raised. Instead, change your
|
||||
query to return the failure as part of the query's result or use a Salsa accumulator.
|
||||
|
||||
## Tracing in tests
|
||||
|
||||
You can use `ruff_db::testing::setup_logging` or `ruff_db::testing::setup_logging_with_filter` to set up logging in tests.
|
||||
|
||||
```rust
|
||||
use ruff_db::testing::setup_logging;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let _logging = setup_logging();
|
||||
|
||||
tracing::info!("This message will be printed to stderr");
|
||||
}
|
||||
```
|
||||
|
||||
Note: Most test runners capture stderr and only show its output when a test fails.
|
||||
|
||||
Note also that `setup_logging` only sets up logging for the current thread because [`set_global_default`](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html) can only be
|
||||
called **once**.
|
||||
|
||||
## Release builds
|
||||
|
||||
`trace!` events are removed in release builds.
|
||||
|
||||
@@ -7,13 +7,13 @@ use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
use red_knot_python_semantic::{ProgramSettings, SearchPathSettings};
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::site_packages::site_packages_dirs_of_venv;
|
||||
use red_knot_workspace::site_packages::VirtualEnvironment;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings};
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use target_version::TargetVersion;
|
||||
|
||||
@@ -164,16 +164,9 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
|
||||
// TODO: Verify the remaining search path settings eagerly.
|
||||
let site_packages = venv_path
|
||||
.map(|venv_path| {
|
||||
let venv_path = SystemPath::absolute(venv_path, &cli_base_path);
|
||||
|
||||
if system.is_directory(&venv_path) {
|
||||
Ok(site_packages_dirs_of_venv(&venv_path, &system)?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided venv-path {venv_path} is not a directory!"
|
||||
))
|
||||
}
|
||||
.map(|path| {
|
||||
VirtualEnvironment::new(path, &OsSystem::new(cli_base_path))
|
||||
.and_then(|venv| venv.site_packages_directories(&system))
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -13,22 +13,36 @@ pub enum TargetVersion {
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
ruff_db::program::TargetVersion::from(*self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for ruff_db::program::TargetVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::Py37,
|
||||
TargetVersion::Py38 => Self::Py38,
|
||||
TargetVersion::Py39 => Self::Py39,
|
||||
TargetVersion::Py310 => Self::Py310,
|
||||
TargetVersion::Py311 => Self::Py311,
|
||||
TargetVersion::Py312 => Self::Py312,
|
||||
TargetVersion::Py313 => Self::Py313,
|
||||
impl TargetVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "py37",
|
||||
Self::Py38 => "py38",
|
||||
Self::Py39 => "py39",
|
||||
Self::Py310 => "py310",
|
||||
Self::Py311 => "py311",
|
||||
Self::Py312 => "py312",
|
||||
Self::Py313 => "py313",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::PY37,
|
||||
TargetVersion::Py38 => Self::PY38,
|
||||
TargetVersion::Py39 => Self::PY39,
|
||||
TargetVersion::Py310 => Self::PY310,
|
||||
TargetVersion::Py311 => Self::PY311,
|
||||
TargetVersion::Py312 => Self::PY312,
|
||||
TargetVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ use std::time::Duration;
|
||||
use anyhow::{anyhow, Context};
|
||||
use salsa::Setter;
|
||||
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName};
|
||||
use red_knot_python_semantic::{
|
||||
resolve_module, ModuleName, Program, ProgramSettings, PythonVersion, SearchPathSettings,
|
||||
};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::program::{Program, ProgramSettings, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::Upcast;
|
||||
@@ -233,7 +234,7 @@ where
|
||||
}
|
||||
|
||||
let settings = ProgramSettings {
|
||||
target_version: TargetVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths,
|
||||
};
|
||||
|
||||
|
||||
@@ -34,12 +34,14 @@ walkdir = { workspace = true }
|
||||
zip = { workspace = true, features = ["zstd", "deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["os", "testing"]}
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -5,6 +5,8 @@ use rustc_hash::FxHasher;
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, vendored_typeshed_stubs};
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings};
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
@@ -13,6 +15,8 @@ mod db;
|
||||
mod module_name;
|
||||
mod module_resolver;
|
||||
mod node_key;
|
||||
mod program;
|
||||
mod python_version;
|
||||
pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub mod types;
|
||||
|
||||
@@ -620,14 +620,13 @@ impl PartialEq<SearchPath> for VendoredPathBuf {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
|
||||
use super::*;
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
impl ModulePath {
|
||||
#[must_use]
|
||||
@@ -867,7 +866,7 @@ mod tests {
|
||||
|
||||
fn typeshed_test_case(
|
||||
typeshed: MockedTypeshed,
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> (TestDb, SearchPath) {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(typeshed)
|
||||
@@ -879,11 +878,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn py38_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, SearchPath) {
|
||||
typeshed_test_case(typeshed, TargetVersion::Py38)
|
||||
typeshed_test_case(typeshed, PythonVersion::PY38)
|
||||
}
|
||||
|
||||
fn py39_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, SearchPath) {
|
||||
typeshed_test_case(typeshed, TargetVersion::Py39)
|
||||
typeshed_test_case(typeshed, PythonVersion::PY39)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -899,7 +898,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY38);
|
||||
|
||||
let asyncio_regular_package = stdlib_path.join("asyncio");
|
||||
assert!(asyncio_regular_package.is_directory(&resolver));
|
||||
@@ -927,7 +926,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY38);
|
||||
|
||||
let xml_namespace_package = stdlib_path.join("xml");
|
||||
assert!(xml_namespace_package.is_directory(&resolver));
|
||||
@@ -949,7 +948,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY38);
|
||||
|
||||
let functools_module = stdlib_path.join("functools.pyi");
|
||||
assert!(functools_module.to_file(&resolver).is_some());
|
||||
@@ -965,7 +964,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY38);
|
||||
|
||||
let collections_regular_package = stdlib_path.join("collections");
|
||||
assert_eq!(collections_regular_package.to_file(&resolver), None);
|
||||
@@ -981,7 +980,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY38);
|
||||
|
||||
let importlib_namespace_package = stdlib_path.join("importlib");
|
||||
assert_eq!(importlib_namespace_package.to_file(&resolver), None);
|
||||
@@ -1002,7 +1001,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY38);
|
||||
|
||||
let non_existent = stdlib_path.join("doesnt_even_exist");
|
||||
assert_eq!(non_existent.to_file(&resolver), None);
|
||||
@@ -1030,7 +1029,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py39);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY39);
|
||||
|
||||
// Since we've set the target version to Py39,
|
||||
// `collections` should now exist as a directory, according to VERSIONS...
|
||||
@@ -1059,7 +1058,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py39);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY39);
|
||||
|
||||
// The `importlib` directory now also exists
|
||||
let importlib_namespace_package = stdlib_path.join("importlib");
|
||||
@@ -1083,7 +1082,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py39);
|
||||
let resolver = ResolverState::new(&db, PythonVersion::PY39);
|
||||
|
||||
// The `xml` package no longer exists on py39:
|
||||
let xml_namespace_package = stdlib_path.join("xml");
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::system::{DirectoryEntry, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
|
||||
use super::state::ResolverState;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::{Program, PythonVersion, SearchPathSettings};
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> {
|
||||
@@ -145,33 +145,47 @@ fn try_resolve_module_resolution_settings(
|
||||
tracing::info!("Custom typeshed directory: {custom_typeshed}");
|
||||
}
|
||||
|
||||
if !site_packages.is_empty() {
|
||||
tracing::info!("Site-packages directories: {site_packages:?}");
|
||||
}
|
||||
|
||||
let system = db.system();
|
||||
let files = db.files();
|
||||
|
||||
let mut static_search_paths = vec![];
|
||||
|
||||
for path in extra_paths {
|
||||
files.try_add_root(db.upcast(), path, FileRootKind::LibrarySearchPath);
|
||||
static_search_paths.push(SearchPath::extra(system, path.clone())?);
|
||||
let search_path = SearchPath::extra(system, path.clone())?;
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
search_path.as_system_path().unwrap(),
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
static_search_paths.push(search_path);
|
||||
}
|
||||
|
||||
static_search_paths.push(SearchPath::first_party(system, src_root.clone())?);
|
||||
|
||||
static_search_paths.push(if let Some(custom_typeshed) = custom_typeshed.as_ref() {
|
||||
let search_path = SearchPath::custom_stdlib(db, custom_typeshed.clone())?;
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
custom_typeshed,
|
||||
search_path.as_system_path().unwrap(),
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
SearchPath::custom_stdlib(db, custom_typeshed.clone())?
|
||||
search_path
|
||||
} else {
|
||||
SearchPath::vendored_stdlib()
|
||||
});
|
||||
|
||||
let mut site_packages_paths: Vec<_> = Vec::with_capacity(site_packages.len());
|
||||
|
||||
for path in site_packages {
|
||||
let search_path = SearchPath::site_packages(system, path.to_path_buf())?;
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
search_path.as_system_path().unwrap(),
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
site_packages_paths.push(search_path);
|
||||
}
|
||||
|
||||
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
|
||||
|
||||
let target_version = program.target_version(db.upcast());
|
||||
@@ -197,7 +211,7 @@ fn try_resolve_module_resolution_settings(
|
||||
Ok(ModuleResolutionSettings {
|
||||
target_version,
|
||||
static_search_paths,
|
||||
site_packages_paths: site_packages.to_owned(),
|
||||
site_packages_paths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -238,15 +252,19 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
let files = db.files();
|
||||
let system = db.system();
|
||||
|
||||
for site_packages_dir in site_packages_paths {
|
||||
for site_packages_search_path in site_packages_paths {
|
||||
let site_packages_dir = site_packages_search_path
|
||||
.as_system_path()
|
||||
.expect("Expected site package path to be a system path");
|
||||
|
||||
if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) {
|
||||
continue;
|
||||
}
|
||||
let site_packages_root = files.try_add_root(
|
||||
db.upcast(),
|
||||
site_packages_dir,
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
|
||||
let site_packages_root = files
|
||||
.root(db.upcast(), site_packages_dir)
|
||||
.expect("Site-package root to have been created.");
|
||||
|
||||
// This query needs to be re-executed each time a `.pth` file
|
||||
// is added, modified or removed from the `site-packages` directory.
|
||||
// However, we don't use Salsa queries to read the source text of `.pth` files;
|
||||
@@ -254,8 +272,7 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
// site-package directory's revision.
|
||||
site_packages_root.revision(db.upcast());
|
||||
|
||||
dynamic_paths
|
||||
.push(SearchPath::site_packages(system, site_packages_dir.to_owned()).unwrap());
|
||||
dynamic_paths.push(site_packages_search_path.clone());
|
||||
|
||||
// As well as modules installed directly into `site-packages`,
|
||||
// the directory may also contain `.pth` files.
|
||||
@@ -263,22 +280,34 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
// containing a (relative or absolute) path.
|
||||
// Each of these paths may point to an editable install of a package,
|
||||
// so should be considered an additional search path.
|
||||
let Ok(pth_file_iterator) = PthFileIterator::new(db, site_packages_dir) else {
|
||||
continue;
|
||||
let pth_file_iterator = match PthFileIterator::new(db, site_packages_dir) {
|
||||
Ok(iterator) => iterator,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"Failed to search for editable installation in {site_packages_dir}: {error}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// The Python documentation specifies that `.pth` files in `site-packages`
|
||||
// are processed in alphabetical order, so collecting and then sorting is necessary.
|
||||
// https://docs.python.org/3/library/site.html#module-site
|
||||
let mut all_pth_files: Vec<PthFile> = pth_file_iterator.collect();
|
||||
all_pth_files.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
all_pth_files.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
|
||||
for pth_file in &all_pth_files {
|
||||
for installation in pth_file.editable_installations() {
|
||||
if existing_paths.insert(Cow::Owned(
|
||||
installation.as_system_path().unwrap().to_path_buf(),
|
||||
)) {
|
||||
dynamic_paths.push(installation);
|
||||
let installations = all_pth_files.iter().flat_map(PthFile::items);
|
||||
|
||||
for installation in installations {
|
||||
if existing_paths.insert(Cow::Owned(installation.clone())) {
|
||||
match SearchPath::editable(system, installation) {
|
||||
Ok(search_path) => {
|
||||
dynamic_paths.push(search_path);
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
tracing::debug!("Skipping editable installation: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +353,6 @@ impl<'db> FusedIterator for SearchPathIterator<'db> {}
|
||||
/// One or more lines in a `.pth` file may be a (relative or absolute)
|
||||
/// path that represents an editable installation of a package.
|
||||
struct PthFile<'db> {
|
||||
system: &'db dyn System,
|
||||
path: SystemPathBuf,
|
||||
contents: String,
|
||||
site_packages: &'db SystemPath,
|
||||
@@ -333,9 +361,8 @@ struct PthFile<'db> {
|
||||
impl<'db> PthFile<'db> {
|
||||
/// Yield paths in this `.pth` file that appear to represent editable installations,
|
||||
/// and should therefore be added as module-resolution search paths.
|
||||
fn editable_installations(&'db self) -> impl Iterator<Item = SearchPath> + 'db {
|
||||
fn items(&'db self) -> impl Iterator<Item = SystemPathBuf> + 'db {
|
||||
let PthFile {
|
||||
system,
|
||||
path: _,
|
||||
contents,
|
||||
site_packages,
|
||||
@@ -354,8 +381,8 @@ impl<'db> PthFile<'db> {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let possible_editable_install = SystemPath::absolute(line, site_packages);
|
||||
SearchPath::editable(*system, possible_editable_install).ok()
|
||||
|
||||
Some(SystemPath::absolute(line, site_packages))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -404,12 +431,15 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(contents) = db.system().read_to_string(&path) else {
|
||||
continue;
|
||||
let contents = match system.read_to_string(&path) {
|
||||
Ok(contents) => contents,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to read .pth file '{path}': {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
return Some(PthFile {
|
||||
system,
|
||||
path,
|
||||
contents,
|
||||
site_packages,
|
||||
@@ -421,7 +451,7 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
/// Validated and normalized module-resolution settings.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ModuleResolutionSettings {
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
|
||||
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
|
||||
/// These shouldn't ever change unless the config settings themselves change.
|
||||
@@ -433,11 +463,11 @@ pub(crate) struct ModuleResolutionSettings {
|
||||
/// That means we can't know where a second or third `site-packages` path should sit
|
||||
/// in terms of module-resolution priority until we've discovered the editable installs
|
||||
/// for the first `site-packages` path
|
||||
site_packages_paths: Vec<SystemPathBuf>,
|
||||
site_packages_paths: Vec<SearchPath>,
|
||||
}
|
||||
|
||||
impl ModuleResolutionSettings {
|
||||
fn target_version(&self) -> TargetVersion {
|
||||
fn target_version(&self) -> PythonVersion {
|
||||
self.target_version
|
||||
}
|
||||
|
||||
@@ -465,9 +495,8 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod
|
||||
let resolver_settings = module_resolution_settings(db);
|
||||
let target_version = resolver_settings.target_version();
|
||||
let resolver_state = ResolverState::new(db, target_version);
|
||||
let (_, minor_version) = target_version.as_tuple();
|
||||
let is_builtin_module =
|
||||
ruff_python_stdlib::sys::is_builtin_module(minor_version, name.as_str());
|
||||
ruff_python_stdlib::sys::is_builtin_module(target_version.minor, name.as_str());
|
||||
|
||||
for search_path in resolver_settings.search_paths(db) {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
@@ -677,7 +706,7 @@ mod tests {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
@@ -695,7 +724,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -748,7 +777,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
@@ -793,7 +822,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&[
|
||||
@@ -837,7 +866,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&[
|
||||
@@ -879,7 +908,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
|
||||
@@ -903,7 +932,7 @@ mod tests {
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -927,7 +956,7 @@ mod tests {
|
||||
fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_vendored_typeshed()
|
||||
.with_target_version(TargetVersion::default())
|
||||
.with_target_version(PythonVersion::default())
|
||||
.build();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
@@ -1143,7 +1172,7 @@ mod tests {
|
||||
fn symlink() -> anyhow::Result<()> {
|
||||
use anyhow::Context;
|
||||
|
||||
use ruff_db::program::Program;
|
||||
use crate::program::Program;
|
||||
use ruff_db::system::{OsSystem, SystemPath};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
@@ -1180,7 +1209,7 @@ mod tests {
|
||||
site_packages: vec![site_packages],
|
||||
};
|
||||
|
||||
Program::new(&db, TargetVersion::Py38, search_paths);
|
||||
Program::new(&db, PythonVersion::PY38, search_paths);
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let bar_module = resolve_module(&db, ModuleName::new_static("bar").unwrap()).unwrap();
|
||||
@@ -1214,7 +1243,7 @@ mod tests {
|
||||
fn deleting_an_unrelated_file_doesnt_change_module_resolution() {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")])
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
@@ -1302,7 +1331,7 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1350,7 +1379,7 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1390,7 +1419,7 @@ mod tests {
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1676,7 +1705,7 @@ not_a_directory
|
||||
|
||||
Program::new(
|
||||
&db,
|
||||
TargetVersion::default(),
|
||||
PythonVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
|
||||
use super::typeshed::LazyTypeshedVersions;
|
||||
use crate::db::Db;
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
pub(crate) struct ResolverState<'db> {
|
||||
pub(crate) db: &'db dyn Db,
|
||||
pub(crate) typeshed_versions: LazyTypeshedVersions<'db>,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
pub(crate) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl<'db> ResolverState<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, target_version: TargetVersion) -> Self {
|
||||
pub(crate) fn new(db: &'db dyn Db, target_version: PythonVersion) -> Self {
|
||||
Self {
|
||||
db,
|
||||
typeshed_versions: LazyTypeshedVersions::new(),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPathBuf;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
/// A test case for the module resolver.
|
||||
///
|
||||
@@ -16,7 +17,7 @@ pub(crate) struct TestCase<T> {
|
||||
// so this is a single directory instead of a `Vec` of directories,
|
||||
// like it is in `ruff_db::Program`.
|
||||
pub(crate) site_packages: SystemPathBuf,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
pub(crate) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
/// A `(file_name, file_contents)` tuple
|
||||
@@ -98,7 +99,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// to `()`.
|
||||
pub(crate) struct TestCaseBuilder<T> {
|
||||
typeshed_option: T,
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
first_party_files: Vec<FileSpec>,
|
||||
site_packages_files: Vec<FileSpec>,
|
||||
}
|
||||
@@ -117,7 +118,7 @@ impl<T> TestCaseBuilder<T> {
|
||||
}
|
||||
|
||||
/// Specify the target Python version the module resolver should assume
|
||||
pub(crate) fn with_target_version(mut self, target_version: TargetVersion) -> Self {
|
||||
pub(crate) fn with_target_version(mut self, target_version: PythonVersion) -> Self {
|
||||
self.target_version = target_version;
|
||||
self
|
||||
}
|
||||
@@ -144,7 +145,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn new() -> TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
Self {
|
||||
typeshed_option: UnspecifiedTypeshed,
|
||||
target_version: TargetVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
first_party_files: vec![],
|
||||
site_packages_files: vec![],
|
||||
}
|
||||
|
||||
@@ -6,16 +6,15 @@ use std::ops::{RangeFrom, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::system::SystemPath;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LazyTypeshedVersions<'db>(OnceCell<&'db TypeshedVersions>);
|
||||
@@ -44,7 +43,7 @@ impl<'db> LazyTypeshedVersions<'db> {
|
||||
db: &'db dyn Db,
|
||||
module: &ModuleName,
|
||||
stdlib_root: Option<&SystemPath>,
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
let versions = self.0.get_or_init(|| {
|
||||
let versions_path = if let Some(system_path) = stdlib_root {
|
||||
@@ -64,7 +63,7 @@ impl<'db> LazyTypeshedVersions<'db> {
|
||||
// Unwrapping here is not correct...
|
||||
parse_typeshed_versions(db, versions_file).as_ref().unwrap()
|
||||
});
|
||||
versions.query_module(module, PyVersion::from(target_version))
|
||||
versions.query_module(module, target_version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +177,7 @@ impl TypeshedVersions {
|
||||
fn query_module(
|
||||
&self,
|
||||
module: &ModuleName,
|
||||
target_version: PyVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
if let Some(range) = self.exact(module) {
|
||||
if range.contains(target_version) {
|
||||
@@ -323,13 +322,13 @@ impl fmt::Display for TypeshedVersions {
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
enum PyVersionRange {
|
||||
AvailableFrom(RangeFrom<PyVersion>),
|
||||
AvailableWithin(RangeInclusive<PyVersion>),
|
||||
AvailableFrom(RangeFrom<PythonVersion>),
|
||||
AvailableWithin(RangeInclusive<PythonVersion>),
|
||||
}
|
||||
|
||||
impl PyVersionRange {
|
||||
#[must_use]
|
||||
fn contains(&self, version: PyVersion) -> bool {
|
||||
fn contains(&self, version: PythonVersion) -> bool {
|
||||
match self {
|
||||
Self::AvailableFrom(inner) => inner.contains(&version),
|
||||
Self::AvailableWithin(inner) => inner.contains(&version),
|
||||
@@ -343,9 +342,14 @@ impl FromStr for PyVersionRange {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = s.split('-').map(str::trim);
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(lower), Some(""), None) => Ok(Self::AvailableFrom((lower.parse()?)..)),
|
||||
(Some(lower), Some(""), None) => {
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
Ok(Self::AvailableFrom(lower..))
|
||||
}
|
||||
(Some(lower), Some(upper), None) => {
|
||||
Ok(Self::AvailableWithin((lower.parse()?)..=(upper.parse()?)))
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
let upper = PythonVersion::from_versions_file_string(upper)?;
|
||||
Ok(Self::AvailableWithin(lower..=upper))
|
||||
}
|
||||
_ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens),
|
||||
}
|
||||
@@ -363,74 +367,20 @@ impl fmt::Display for PyVersionRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
struct PyVersion {
|
||||
major: u8,
|
||||
minor: u8,
|
||||
}
|
||||
|
||||
impl FromStr for PyVersion {
|
||||
type Err = TypeshedVersionsParseErrorKind;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
impl PythonVersion {
|
||||
fn from_versions_file_string(s: &str) -> Result<Self, TypeshedVersionsParseErrorKind> {
|
||||
let mut parts = s.split('.').map(str::trim);
|
||||
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
|
||||
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
|
||||
s.to_string(),
|
||||
));
|
||||
};
|
||||
let major = match u8::from_str(major) {
|
||||
Ok(major) => major,
|
||||
Err(err) => {
|
||||
return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err,
|
||||
})
|
||||
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
|
||||
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err: int_parse_error,
|
||||
}
|
||||
};
|
||||
let minor = match u8::from_str(minor) {
|
||||
Ok(minor) => minor,
|
||||
Err(err) => {
|
||||
return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(Self { major, minor })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PyVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let PyVersion { major, minor } = self;
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for PyVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => PyVersion { major: 3, minor: 7 },
|
||||
TargetVersion::Py38 => PyVersion { major: 3, minor: 8 },
|
||||
TargetVersion::Py39 => PyVersion { major: 3, minor: 9 },
|
||||
TargetVersion::Py310 => PyVersion {
|
||||
major: 3,
|
||||
minor: 10,
|
||||
},
|
||||
TargetVersion::Py311 => PyVersion {
|
||||
major: 3,
|
||||
minor: 11,
|
||||
},
|
||||
TargetVersion::Py312 => PyVersion {
|
||||
major: 3,
|
||||
minor: 12,
|
||||
},
|
||||
TargetVersion::Py313 => PyVersion {
|
||||
major: 3,
|
||||
minor: 13,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,7 +390,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::program::TargetVersion;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -478,27 +427,27 @@ mod tests {
|
||||
|
||||
assert!(versions.contains_exact(&asyncio));
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio, TargetVersion::Py310.into()),
|
||||
versions.query_module(&asyncio, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
|
||||
assert!(versions.contains_exact(&asyncio_staggered));
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio_staggered, TargetVersion::Py38.into()),
|
||||
versions.query_module(&asyncio_staggered, PythonVersion::PY38),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio_staggered, TargetVersion::Py37.into()),
|
||||
versions.query_module(&asyncio_staggered, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
|
||||
assert!(versions.contains_exact(&audioop));
|
||||
assert_eq!(
|
||||
versions.query_module(&audioop, TargetVersion::Py312.into()),
|
||||
versions.query_module(&audioop, PythonVersion::PY312),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
versions.query_module(&audioop, TargetVersion::Py313.into()),
|
||||
versions.query_module(&audioop, PythonVersion::PY313),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -590,15 +539,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&bar));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -610,15 +559,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&foo));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py38.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY38),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
}
|
||||
@@ -630,15 +579,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&bar_baz));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py39.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY39),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -650,15 +599,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(!parsed_versions.contains_exact(&bar_eggs));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -670,11 +619,11 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(!parsed_versions.contains_exact(&spam));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&spam, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&spam, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&spam, TargetVersion::Py313.into()),
|
||||
parsed_versions.query_module(&spam, PythonVersion::PY313),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::{system::SystemPathBuf, Db};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::Db;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use salsa::Durability;
|
||||
|
||||
#[salsa::input(singleton)]
|
||||
pub struct Program {
|
||||
pub target_version: TargetVersion,
|
||||
pub target_version: PythonVersion,
|
||||
|
||||
#[return_ref]
|
||||
pub search_paths: SearchPathSettings,
|
||||
@@ -19,63 +21,10 @@ impl Program {
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct ProgramSettings {
|
||||
pub target_version: TargetVersion,
|
||||
pub target_version: PythonVersion,
|
||||
pub search_paths: SearchPathSettings,
|
||||
}
|
||||
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
#[default]
|
||||
Py38,
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl TargetVersion {
|
||||
pub const fn as_tuple(self) -> (u8, u8) {
|
||||
match self {
|
||||
Self::Py37 => (3, 7),
|
||||
Self::Py38 => (3, 8),
|
||||
Self::Py39 => (3, 9),
|
||||
Self::Py310 => (3, 10),
|
||||
Self::Py311 => (3, 11),
|
||||
Self::Py312 => (3, 12),
|
||||
Self::Py313 => (3, 13),
|
||||
}
|
||||
}
|
||||
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "py37",
|
||||
Self::Py38 => "py38",
|
||||
Self::Py39 => "py39",
|
||||
Self::Py310 => "py310",
|
||||
Self::Py311 => "py311",
|
||||
Self::Py312 => "py312",
|
||||
Self::Py313 => "py313",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the search paths for module resolution.
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Default)]
|
||||
pub struct SearchPathSettings {
|
||||
62
crates/red_knot_python_semantic/src/python_version.rs
Normal file
62
crates/red_knot_python_semantic/src/python_version.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::fmt;
|
||||
|
||||
/// Representation of a Python version.
|
||||
///
|
||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||
/// this does not necessarily represent a Python version that we actually support.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct PythonVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
pub const PY37: PythonVersion = PythonVersion { major: 3, minor: 7 };
|
||||
pub const PY38: PythonVersion = PythonVersion { major: 3, minor: 8 };
|
||||
pub const PY39: PythonVersion = PythonVersion { major: 3, minor: 9 };
|
||||
pub const PY310: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 10,
|
||||
};
|
||||
pub const PY311: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 11,
|
||||
};
|
||||
pub const PY312: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 12,
|
||||
};
|
||||
pub const PY313: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 13,
|
||||
};
|
||||
|
||||
pub fn free_threaded_build_available(self) -> bool {
|
||||
self >= PythonVersion::PY313
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PythonVersion {
|
||||
fn default() -> Self {
|
||||
Self::PY38
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(&str, &str)> for PythonVersion {
|
||||
type Error = std::num::ParseIntError;
|
||||
|
||||
fn try_from(value: (&str, &str)) -> Result<Self, Self::Error> {
|
||||
let (major, minor) = value;
|
||||
Ok(Self {
|
||||
major: major.parse()?,
|
||||
minor: minor.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let PythonVersion { major, minor } = self;
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
@@ -165,10 +165,11 @@ impl HasTy for ast::Alias {
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::Type;
|
||||
use crate::{HasTy, SemanticModel};
|
||||
|
||||
@@ -176,7 +177,7 @@ mod tests {
|
||||
let db = TestDb::new();
|
||||
Program::new(
|
||||
&db,
|
||||
TargetVersion::Py38,
|
||||
PythonVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
|
||||
@@ -1496,13 +1496,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
mod tests {
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use crate::builtins::builtins_scope;
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::FileScopeId;
|
||||
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
|
||||
@@ -1514,7 +1515,7 @@ mod tests {
|
||||
|
||||
Program::new(
|
||||
&db,
|
||||
TargetVersion::Py38,
|
||||
PythonVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: Vec::new(),
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
@@ -1531,7 +1532,7 @@ mod tests {
|
||||
|
||||
Program::new(
|
||||
&db,
|
||||
TargetVersion::Py38,
|
||||
PythonVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: Vec::new(),
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
|
||||
@@ -11,6 +11,7 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_workspace = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_linter = { workspace = true }
|
||||
|
||||
@@ -8,10 +8,10 @@ use std::sync::Arc;
|
||||
use anyhow::anyhow;
|
||||
use lsp_types::{ClientCapabilities, Url};
|
||||
|
||||
use red_knot_python_semantic::{ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::SystemPath;
|
||||
|
||||
use crate::edit::{DocumentKey, NotebookDocument};
|
||||
@@ -70,7 +70,7 @@ impl Session {
|
||||
let metadata = WorkspaceMetadata::from_path(system_path, &system)?;
|
||||
// TODO(dhruvmanila): Get the values from the client settings
|
||||
let program_settings = ProgramSettings {
|
||||
target_version: TargetVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: system_path.to_path_buf(),
|
||||
|
||||
@@ -19,6 +19,7 @@ doctest = false
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_workspace = { workspace = true }
|
||||
|
||||
ruff_db = { workspace = true }
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::any::Any;
|
||||
use js_sys::Error;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use red_knot_python_semantic::{ProgramSettings, SearchPathSettings};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings};
|
||||
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
|
||||
use ruff_db::system::{
|
||||
DirectoryEntry, MemoryFileSystem, Metadata, System, SystemPath, SystemPathBuf,
|
||||
@@ -184,16 +184,16 @@ pub enum TargetVersion {
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for ruff_db::program::TargetVersion {
|
||||
impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::Py37,
|
||||
TargetVersion::Py38 => Self::Py38,
|
||||
TargetVersion::Py39 => Self::Py39,
|
||||
TargetVersion::Py310 => Self::Py310,
|
||||
TargetVersion::Py311 => Self::Py311,
|
||||
TargetVersion::Py312 => Self::Py312,
|
||||
TargetVersion::Py313 => Self::Py313,
|
||||
TargetVersion::Py37 => Self::PY37,
|
||||
TargetVersion::Py38 => Self::PY38,
|
||||
TargetVersion::Py39 => Self::PY39,
|
||||
TargetVersion::Py310 => Self::PY310,
|
||||
TargetVersion::Py311 => Self::PY311,
|
||||
TargetVersion::Py312 => Self::PY312,
|
||||
TargetVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
@@ -1 +0,0 @@
|
||||
/Users/alexw/.pyenv/versions/3.12.4/bin/python3.12
|
||||
@@ -1 +0,0 @@
|
||||
python
|
||||
@@ -1 +0,0 @@
|
||||
python
|
||||
@@ -1 +0,0 @@
|
||||
import _virtualenv
|
||||
@@ -1,103 +0,0 @@
|
||||
"""Patches that are applied at runtime to the virtual environment."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
VIRTUALENV_PATCH_FILE = os.path.join(__file__)
|
||||
|
||||
|
||||
def patch_dist(dist):
|
||||
"""
|
||||
Distutils allows user to configure some arguments via a configuration file:
|
||||
https://docs.python.org/3/install/index.html#distutils-configuration-files.
|
||||
|
||||
Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up.
|
||||
""" # noqa: D205
|
||||
# we cannot allow some install config as that would get packages installed outside of the virtual environment
|
||||
old_parse_config_files = dist.Distribution.parse_config_files
|
||||
|
||||
def parse_config_files(self, *args, **kwargs):
|
||||
result = old_parse_config_files(self, *args, **kwargs)
|
||||
install = self.get_option_dict("install")
|
||||
|
||||
if "prefix" in install: # the prefix governs where to install the libraries
|
||||
install["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix)
|
||||
for base in ("purelib", "platlib", "headers", "scripts", "data"):
|
||||
key = f"install_{base}"
|
||||
if key in install: # do not allow global configs to hijack venv paths
|
||||
install.pop(key, None)
|
||||
return result
|
||||
|
||||
dist.Distribution.parse_config_files = parse_config_files
|
||||
|
||||
|
||||
# Import hook that patches some modules to ignore configuration values that break package installation in case
|
||||
# of virtual environments.
|
||||
_DISTUTILS_PATCH = "distutils.dist", "setuptools.dist"
|
||||
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
|
||||
|
||||
|
||||
class _Finder:
|
||||
"""A meta path finder that allows patching the imported distutils modules."""
|
||||
|
||||
fullname = None
|
||||
|
||||
# lock[0] is threading.Lock(), but initialized lazily to avoid importing threading very early at startup,
|
||||
# because there are gevent-based applications that need to be first to import threading by themselves.
|
||||
# See https://github.com/pypa/virtualenv/issues/1895 for details.
|
||||
lock = [] # noqa: RUF012
|
||||
|
||||
def find_spec(self, fullname, path, target=None): # noqa: ARG002
|
||||
if fullname in _DISTUTILS_PATCH and self.fullname is None:
|
||||
# initialize lock[0] lazily
|
||||
if len(self.lock) == 0:
|
||||
import threading
|
||||
|
||||
lock = threading.Lock()
|
||||
# there is possibility that two threads T1 and T2 are simultaneously running into find_spec,
|
||||
# observing .lock as empty, and further going into hereby initialization. However due to the GIL,
|
||||
# list.append() operation is atomic and this way only one of the threads will "win" to put the lock
|
||||
# - that every thread will use - into .lock[0].
|
||||
# https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
|
||||
self.lock.append(lock)
|
||||
|
||||
from functools import partial
|
||||
from importlib.util import find_spec
|
||||
|
||||
with self.lock[0]:
|
||||
self.fullname = fullname
|
||||
try:
|
||||
spec = find_spec(fullname, path)
|
||||
if spec is not None:
|
||||
# https://www.python.org/dev/peps/pep-0451/#how-loading-will-work
|
||||
is_new_api = hasattr(spec.loader, "exec_module")
|
||||
func_name = "exec_module" if is_new_api else "load_module"
|
||||
old = getattr(spec.loader, func_name)
|
||||
func = self.exec_module if is_new_api else self.load_module
|
||||
if old is not func:
|
||||
try: # noqa: SIM105
|
||||
setattr(spec.loader, func_name, partial(func, old))
|
||||
except AttributeError:
|
||||
pass # C-Extension loaders are r/o such as zipimporter with <3.7
|
||||
return spec
|
||||
finally:
|
||||
self.fullname = None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def exec_module(old, module):
|
||||
old(module)
|
||||
if module.__name__ in _DISTUTILS_PATCH:
|
||||
patch_dist(module)
|
||||
|
||||
@staticmethod
|
||||
def load_module(old, name):
|
||||
module = old(name)
|
||||
if module.__name__ in _DISTUTILS_PATCH:
|
||||
patch_dist(module)
|
||||
return module
|
||||
|
||||
|
||||
sys.meta_path.insert(0, _Finder())
|
||||
@@ -1,6 +0,0 @@
|
||||
home = /Users/alexw/.pyenv/versions/3.12.4/bin
|
||||
implementation = CPython
|
||||
uv = 0.2.32
|
||||
version_info = 3.12.4
|
||||
include-system-site-packages = false
|
||||
relocatable = false
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::panic::RefUnwindSafe;
|
||||
use std::sync::Arc;
|
||||
|
||||
use red_knot_python_semantic::{vendored_typeshed_stubs, Db as SemanticDb};
|
||||
use red_knot_python_semantic::{
|
||||
vendored_typeshed_stubs, Db as SemanticDb, Program, ProgramSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::program::{Program, ProgramSettings};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
@@ -129,7 +130,18 @@ impl SourceDb for RootDatabase {
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for RootDatabase {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> Event) {}
|
||||
fn salsa_event(&self, event: &dyn Fn() -> Event) {
|
||||
if !tracing::enabled!(tracing::Level::TRACE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = event();
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::trace!("Salsa event: {event:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -305,13 +305,14 @@ enum AnyImportRef<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use red_knot_python_semantic::{Program, PythonVersion, SearchPathSettings};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use super::{lint_semantic, Diagnostics};
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use super::{lint_semantic, Diagnostics};
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
setup_db_with_root(SystemPathBuf::from("/src"))
|
||||
}
|
||||
@@ -321,7 +322,7 @@ mod tests {
|
||||
|
||||
Program::new(
|
||||
&db,
|
||||
TargetVersion::Py38,
|
||||
PythonVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: Vec::new(),
|
||||
src_root,
|
||||
|
||||
@@ -8,43 +8,270 @@
|
||||
//! reasonably ask us to type-check code assuming that the code runs
|
||||
//! on Linux.)
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
|
||||
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
|
||||
|
||||
/// Abstraction for a Python virtual environment.
|
||||
///
|
||||
/// Most of this information is derived from the virtual environment's `pyvenv.cfg` file.
|
||||
/// The format of this file is not defined anywhere, and exactly which keys are present
|
||||
/// depends on the tool that was used to create the virtual environment.
|
||||
#[derive(Debug)]
|
||||
pub struct VirtualEnvironment {
|
||||
venv_path: SysPrefixPath,
|
||||
base_executable_home_path: PythonHomePath,
|
||||
include_system_site_packages: bool,
|
||||
|
||||
/// The version of the Python executable that was used to create this virtual environment.
|
||||
///
|
||||
/// The Python version is encoded under different keys and in different formats
|
||||
/// by different virtual-environment creation tools,
|
||||
/// and the key is never read by the standard-library `site.py` module,
|
||||
/// so it's possible that we might not be able to find this information
|
||||
/// in an acceptable format under any of the keys we expect.
|
||||
/// This field will be `None` if so.
|
||||
version: Option<PythonVersion>,
|
||||
}
|
||||
|
||||
impl VirtualEnvironment {
|
||||
pub fn new(
|
||||
path: impl AsRef<SystemPath>,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Self> {
|
||||
Self::new_impl(path.as_ref(), system)
|
||||
}
|
||||
|
||||
fn new_impl(path: &SystemPath, system: &dyn System) -> SitePackagesDiscoveryResult<Self> {
|
||||
fn pyvenv_cfg_line_number(index: usize) -> NonZeroUsize {
|
||||
index.checked_add(1).and_then(NonZeroUsize::new).unwrap()
|
||||
}
|
||||
|
||||
let venv_path = SysPrefixPath::new(path, system)?;
|
||||
let pyvenv_cfg_path = venv_path.join("pyvenv.cfg");
|
||||
tracing::debug!("Attempting to parse virtual environment metadata at {pyvenv_cfg_path}");
|
||||
|
||||
let pyvenv_cfg = system
|
||||
.read_to_string(&pyvenv_cfg_path)
|
||||
.map_err(SitePackagesDiscoveryError::NoPyvenvCfgFile)?;
|
||||
|
||||
let mut include_system_site_packages = false;
|
||||
let mut base_executable_home_path = None;
|
||||
let mut version_info_string = None;
|
||||
|
||||
// A `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!
|
||||
// The Python standard-library's `site` module parses these files by splitting each line on
|
||||
// '=' characters, so that's what we should do as well.
|
||||
//
|
||||
// See also: https://snarky.ca/how-virtual-environments-work/
|
||||
for (index, line) in pyvenv_cfg.lines().enumerate() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
let key = key.trim();
|
||||
if key.is_empty() {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair {
|
||||
line_number: pyvenv_cfg_line_number(index),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair {
|
||||
line_number: pyvenv_cfg_line_number(index),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if value.contains('=') {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::TooManyEquals {
|
||||
line_number: pyvenv_cfg_line_number(index),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
match key {
|
||||
"include-system-site-packages" => {
|
||||
include_system_site_packages = value.eq_ignore_ascii_case("true");
|
||||
}
|
||||
"home" => base_executable_home_path = Some(value),
|
||||
// `virtualenv` and `uv` call this key `version_info`,
|
||||
// but the stdlib venv module calls it `version`
|
||||
"version" | "version_info" => version_info_string = Some(value),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The `home` key is read by the standard library's `site.py` module,
|
||||
// so if it's missing from the `pyvenv.cfg` file
|
||||
// (or the provided value is invalid),
|
||||
// it's reasonable to consider the virtual environment irredeemably broken.
|
||||
let Some(base_executable_home_path) = base_executable_home_path else {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::NoHomeKey,
|
||||
));
|
||||
};
|
||||
let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system)
|
||||
.map_err(|io_err| {
|
||||
SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::InvalidHomeValue(io_err),
|
||||
)
|
||||
})?;
|
||||
|
||||
// but the `version`/`version_info` key is not read by the standard library,
|
||||
// and is provided under different keys depending on which virtual-environment creation tool
|
||||
// created the `pyvenv.cfg` file. Lenient parsing is appropriate here:
|
||||
// the file isn't really *invalid* if it doesn't have this key,
|
||||
// or if the value doesn't parse according to our expectations.
|
||||
let version = version_info_string.and_then(|version_string| {
|
||||
let mut version_info_parts = version_string.split('.');
|
||||
let (major, minor) = (version_info_parts.next()?, version_info_parts.next()?);
|
||||
PythonVersion::try_from((major, minor)).ok()
|
||||
});
|
||||
|
||||
let metadata = Self {
|
||||
venv_path,
|
||||
base_executable_home_path,
|
||||
include_system_site_packages,
|
||||
version,
|
||||
};
|
||||
|
||||
tracing::trace!("Resolved metadata for virtual environment: {metadata:?}");
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Return a list of `site-packages` directories that are available from this virtual environment
|
||||
///
|
||||
/// See the documentation for `site_packages_dir_from_sys_prefix` for more details.
|
||||
pub fn site_packages_directories(
|
||||
&self,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Vec<SystemPathBuf>> {
|
||||
let VirtualEnvironment {
|
||||
venv_path,
|
||||
base_executable_home_path,
|
||||
include_system_site_packages,
|
||||
version,
|
||||
} = self;
|
||||
|
||||
let mut site_packages_directories = vec![site_packages_directory_from_sys_prefix(
|
||||
venv_path, *version, system,
|
||||
)?];
|
||||
|
||||
if *include_system_site_packages {
|
||||
let system_sys_prefix =
|
||||
SysPrefixPath::from_executable_home_path(base_executable_home_path);
|
||||
|
||||
// If we fail to resolve the `sys.prefix` path from the base executable home path,
|
||||
// or if we fail to resolve the `site-packages` from the `sys.prefix` path,
|
||||
// we should probably print a warning but *not* abort type checking
|
||||
if let Some(sys_prefix_path) = system_sys_prefix {
|
||||
match site_packages_directory_from_sys_prefix(&sys_prefix_path, *version, system) {
|
||||
Ok(site_packages_directory) => {
|
||||
site_packages_directories.push(site_packages_directory);
|
||||
}
|
||||
Err(error) => tracing::warn!(
|
||||
"{error}. System site-packages will not be used for module resolution."
|
||||
),
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Failed to resolve `sys.prefix` of the system Python installation \
|
||||
from the `home` value in the `pyvenv.cfg` file at {}. \
|
||||
System site-packages will not be used for module resolution.",
|
||||
venv_path.join("pyvenv.cfg")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Resolved site-packages directories for this virtual environment are: {site_packages_directories:?}");
|
||||
Ok(site_packages_directories)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SitePackagesDiscoveryError {
|
||||
#[error("Invalid --venv-path argument: {0} could not be canonicalized")]
|
||||
VenvDirCanonicalizationError(SystemPathBuf, #[source] io::Error),
|
||||
#[error("Invalid --venv-path argument: {0} does not point to a directory on disk")]
|
||||
VenvDirIsNotADirectory(SystemPathBuf),
|
||||
#[error("--venv-path points to a broken venv with no pyvenv.cfg file")]
|
||||
NoPyvenvCfgFile(#[source] io::Error),
|
||||
#[error("Failed to parse the pyvenv.cfg file at {0} because {1}")]
|
||||
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
|
||||
#[error("Failed to search the `lib` directory of the Python installation at {1} for `site-packages`")]
|
||||
CouldNotReadLibDirectory(#[source] io::Error, SysPrefixPath),
|
||||
#[error("Could not find the `site-packages` directory for the Python installation at {0}")]
|
||||
NoSitePackagesDirFound(SysPrefixPath),
|
||||
}
|
||||
|
||||
/// The various ways in which parsing a `pyvenv.cfg` file could fail
|
||||
#[derive(Debug)]
|
||||
pub enum PyvenvCfgParseErrorKind {
|
||||
TooManyEquals { line_number: NonZeroUsize },
|
||||
MalformedKeyValuePair { line_number: NonZeroUsize },
|
||||
NoHomeKey,
|
||||
InvalidHomeValue(io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for PyvenvCfgParseErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::TooManyEquals { line_number } => {
|
||||
write!(f, "line {line_number} has too many '=' characters")
|
||||
}
|
||||
Self::MalformedKeyValuePair { line_number } => write!(
|
||||
f,
|
||||
"line {line_number} has a malformed `<key> = <value>` pair"
|
||||
),
|
||||
Self::NoHomeKey => f.write_str("the file does not have a `home` key"),
|
||||
Self::InvalidHomeValue(io_err) => {
|
||||
write!(
|
||||
f,
|
||||
"the following error was encountered \
|
||||
when trying to resolve the `home` value to a directory on disk: {io_err}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to retrieve the `site-packages` directory
|
||||
/// associated with a given Python installation.
|
||||
///
|
||||
/// `sys_prefix_path` is equivalent to the value of [`sys.prefix`]
|
||||
/// at runtime in Python. For the case of a virtual environment, where a
|
||||
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
|
||||
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
|
||||
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
|
||||
/// System Python installations generally work the same way: if a system
|
||||
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
|
||||
/// will be `/opt/homebrew`, and `site-packages` will be at
|
||||
/// `/opt/homebrew/lib/python3.X/site-packages`.
|
||||
///
|
||||
/// This routine does not verify that `sys_prefix_path` points
|
||||
/// to an existing directory on disk; it is assumed that this has already
|
||||
/// been checked.
|
||||
///
|
||||
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
|
||||
fn site_packages_dir_from_sys_prefix(
|
||||
sys_prefix_path: &SystemPath,
|
||||
/// The location of the `site-packages` directory can vary according to the
|
||||
/// Python version that this installation represents. The Python version may
|
||||
/// or may not be known at this point, which is why the `python_version`
|
||||
/// parameter is an `Option`.
|
||||
fn site_packages_directory_from_sys_prefix(
|
||||
sys_prefix_path: &SysPrefixPath,
|
||||
python_version: Option<PythonVersion>,
|
||||
system: &dyn System,
|
||||
) -> Result<SystemPathBuf, SitePackagesDiscoveryError> {
|
||||
tracing::debug!("Searching for site-packages directory in '{sys_prefix_path}'");
|
||||
) -> SitePackagesDiscoveryResult<SystemPathBuf> {
|
||||
tracing::debug!("Searching for site-packages directory in {sys_prefix_path}");
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
let site_packages = sys_prefix_path.join("Lib/site-packages");
|
||||
|
||||
return if system.is_directory(&site_packages) {
|
||||
tracing::debug!("Resolved site-packages directory to '{site_packages}'");
|
||||
Ok(site_packages)
|
||||
} else {
|
||||
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound)
|
||||
};
|
||||
let site_packages = sys_prefix_path.join(r"Lib\site-packages");
|
||||
return system
|
||||
.is_directory(&site_packages)
|
||||
.then_some(site_packages)
|
||||
.ok_or(SitePackagesDiscoveryError::NoSitePackagesDirFound(
|
||||
sys_prefix_path.to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// In the Python standard library's `site.py` module (used for finding `site-packages`
|
||||
@@ -69,7 +296,38 @@ fn site_packages_dir_from_sys_prefix(
|
||||
//
|
||||
// [the non-Windows branch]: https://github.com/python/cpython/blob/a8be8fc6c4682089be45a87bd5ee1f686040116c/Lib/site.py#L401-L410
|
||||
// [the `sys`-module documentation]: https://docs.python.org/3/library/sys.html#sys.platlibdir
|
||||
for entry_result in system.read_directory(&sys_prefix_path.join("lib"))? {
|
||||
|
||||
// If we were able to figure out what Python version this installation is,
|
||||
// we should be able to avoid iterating through all items in the `lib/` directory:
|
||||
if let Some(version) = python_version {
|
||||
let expected_path = sys_prefix_path.join(format!("lib/python{version}/site-packages"));
|
||||
if system.is_directory(&expected_path) {
|
||||
return Ok(expected_path);
|
||||
}
|
||||
if version.free_threaded_build_available() {
|
||||
// Nearly the same as `expected_path`, but with an additional `t` after {version}:
|
||||
let alternative_path =
|
||||
sys_prefix_path.join(format!("lib/python{version}t/site-packages"));
|
||||
if system.is_directory(&alternative_path) {
|
||||
return Ok(alternative_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either we couldn't figure out the version before calling this function
|
||||
// (e.g., from a `pyvenv.cfg` file if this was a venv),
|
||||
// or we couldn't find a `site-packages` folder at the expected location given
|
||||
// the parsed version
|
||||
//
|
||||
// Note: the `python3.x` part of the `site-packages` path can't be computed from
|
||||
// the `--target-version` the user has passed, as they might be running Python 3.12 locally
|
||||
// even if they've requested that we type check their code "as if" they're running 3.8.
|
||||
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())
|
||||
})?
|
||||
{
|
||||
let Ok(entry) = entry_result else {
|
||||
continue;
|
||||
};
|
||||
@@ -80,16 +338,6 @@ fn site_packages_dir_from_sys_prefix(
|
||||
|
||||
let mut path = entry.into_path();
|
||||
|
||||
// The `python3.x` part of the `site-packages` path can't be computed from
|
||||
// the `--target-version` the user has passed, as they might be running Python 3.12 locally
|
||||
// even if they've requested that we type check their code "as if" they're running 3.8.
|
||||
//
|
||||
// The `python3.x` part of the `site-packages` path *could* be computed
|
||||
// by parsing the virtual environment's `pyvenv.cfg` file.
|
||||
// Right now that seems like overkill, but in the future we may need to parse
|
||||
// the `pyvenv.cfg` file anyway, in which case we could switch to that method
|
||||
// rather than iterating through the whole directory until we find
|
||||
// an entry where the last component of the path starts with `python3.`
|
||||
let name = path
|
||||
.file_name()
|
||||
.expect("File name to be non-null because path is guaranteed to be a child of `lib`");
|
||||
@@ -100,55 +348,494 @@ fn site_packages_dir_from_sys_prefix(
|
||||
|
||||
path.push("site-packages");
|
||||
if system.is_directory(&path) {
|
||||
tracing::debug!("Resolved site-packages directory to '{path}'");
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound)
|
||||
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(
|
||||
sys_prefix_path.to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SitePackagesDiscoveryError {
|
||||
#[error("Failed to search the virtual environment directory for `site-packages`")]
|
||||
CouldNotReadLibDirectory(#[from] io::Error),
|
||||
#[error("Could not find the `site-packages` directory in the virtual environment")]
|
||||
NoSitePackagesDirFound,
|
||||
/// A path that represents the value of [`sys.prefix`] at runtime in Python
|
||||
/// for a given Python executable.
|
||||
///
|
||||
/// For the case of a virtual environment, where a
|
||||
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
|
||||
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
|
||||
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
|
||||
/// System Python installations generally work the same way: if a system
|
||||
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
|
||||
/// will be `/opt/homebrew`, and `site-packages` will be at
|
||||
/// `/opt/homebrew/lib/python3.X/site-packages`.
|
||||
///
|
||||
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct SysPrefixPath(SystemPathBuf);
|
||||
|
||||
impl SysPrefixPath {
|
||||
fn new(
|
||||
unvalidated_path: impl AsRef<SystemPath>,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Self> {
|
||||
Self::new_impl(unvalidated_path.as_ref(), system)
|
||||
}
|
||||
|
||||
fn new_impl(
|
||||
unvalidated_path: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Self> {
|
||||
// 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| {
|
||||
SitePackagesDiscoveryError::VenvDirCanonicalizationError(
|
||||
unvalidated_path.to_path_buf(),
|
||||
io_err,
|
||||
)
|
||||
})?;
|
||||
system
|
||||
.is_directory(&canonicalized)
|
||||
.then_some(Self(canonicalized))
|
||||
.ok_or_else(|| {
|
||||
SitePackagesDiscoveryError::VenvDirIsNotADirectory(unvalidated_path.to_path_buf())
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
// is guaranteed to be a directory.
|
||||
if cfg!(target_os = "windows") {
|
||||
Some(Self(path.to_path_buf()))
|
||||
} else {
|
||||
path.parent().map(|path| Self(path.to_path_buf()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a validated, canonicalized path to a virtual environment,
|
||||
/// return a list of `site-packages` directories that are available from that environment.
|
||||
impl Deref for SysPrefixPath {
|
||||
type Target = SystemPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SysPrefixPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "`sys.prefix` path {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The value given by the `home` key in `pyvenv.cfg` files.
|
||||
///
|
||||
/// See the documentation for `site_packages_dir_from_sys_prefix` for more details.
|
||||
/// This is equivalent to `{sys_prefix_path}/bin`, and points
|
||||
/// to a directory in which a Python executable can be found.
|
||||
/// Confusingly, it is *not* the same as the [`PYTHONHOME`]
|
||||
/// environment variable that Python provides! However, it's
|
||||
/// consistent among all mainstream creators of Python virtual
|
||||
/// environments (the stdlib Python `venv` module, the third-party
|
||||
/// `virtualenv` library, and `uv`), was specified by
|
||||
/// [the original PEP adding the `venv` module],
|
||||
/// and it's one of the few fields that's read by the Python
|
||||
/// standard library's `site.py` module.
|
||||
///
|
||||
/// TODO: Currently we only ever return 1 path from this function:
|
||||
/// the `site-packages` directory that is actually inside the virtual environment.
|
||||
/// Some `site-packages` directories are able to also access system `site-packages` and
|
||||
/// user `site-packages`, however.
|
||||
pub fn site_packages_dirs_of_venv(
|
||||
venv_path: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> Result<Vec<SystemPathBuf>, SitePackagesDiscoveryError> {
|
||||
Ok(vec![site_packages_dir_from_sys_prefix(venv_path, system)?])
|
||||
/// Although it doesn't appear to be specified anywhere,
|
||||
/// all existing virtual environment tools always use an absolute path
|
||||
/// for the `home` value, and the Python standard library also assumes
|
||||
/// that the `home` value will be an absolute path.
|
||||
///
|
||||
/// Other values, such as the path to the Python executable or the
|
||||
/// base-executable `sys.prefix` value, are either only provided in
|
||||
/// `pyvenv.cfg` files by some virtual-environment creators,
|
||||
/// or are included under different keys depending on which
|
||||
/// virtual-environment creation tool you've used.
|
||||
///
|
||||
/// [`PYTHONHOME`]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME
|
||||
/// [the original PEP adding the `venv` module]: https://peps.python.org/pep-0405/
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct PythonHomePath(SystemPathBuf);
|
||||
|
||||
impl PythonHomePath {
|
||||
fn new(path: impl AsRef<SystemPath>, system: &dyn System) -> io::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
// 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(path)?;
|
||||
system
|
||||
.is_directory(&canonicalized)
|
||||
.then_some(Self(canonicalized))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "not a directory"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PythonHomePath {
|
||||
type Target = SystemPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonHomePath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "`home` location {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SystemPath> for PythonHomePath {
|
||||
fn eq(&self, other: &SystemPath) -> bool {
|
||||
&*self.0 == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SystemPathBuf> for PythonHomePath {
|
||||
fn eq(&self, other: &SystemPathBuf) -> bool {
|
||||
self == &**other
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::system::{OsSystem, System, SystemPath};
|
||||
use ruff_db::system::TestSystem;
|
||||
|
||||
use crate::site_packages::site_packages_dirs_of_venv;
|
||||
use super::*;
|
||||
|
||||
struct VirtualEnvironmentTester {
|
||||
system: TestSystem,
|
||||
minor_version: u8,
|
||||
free_threaded: bool,
|
||||
system_site_packages: bool,
|
||||
pyvenv_cfg_version_field: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl VirtualEnvironmentTester {
|
||||
/// Builds a mock virtual environment, and returns the path to the venv
|
||||
fn build_mock_venv(&self) -> SystemPathBuf {
|
||||
let VirtualEnvironmentTester {
|
||||
system,
|
||||
minor_version,
|
||||
system_site_packages,
|
||||
free_threaded,
|
||||
pyvenv_cfg_version_field,
|
||||
} = self;
|
||||
let memory_fs = system.memory_file_system();
|
||||
let unix_site_packages = if *free_threaded {
|
||||
format!("lib/python3.{minor_version}t/site-packages")
|
||||
} else {
|
||||
format!("lib/python3.{minor_version}/site-packages")
|
||||
};
|
||||
|
||||
let system_install_sys_prefix =
|
||||
SystemPathBuf::from(&*format!("/Python3.{minor_version}"));
|
||||
let (system_home_path, system_exe_path, system_site_packages_path) =
|
||||
if cfg!(target_os = "windows") {
|
||||
let system_home_path = system_install_sys_prefix.clone();
|
||||
let system_exe_path = system_home_path.join("python.exe");
|
||||
let system_site_packages_path =
|
||||
system_install_sys_prefix.join(r"Lib\site-packages");
|
||||
(system_home_path, system_exe_path, system_site_packages_path)
|
||||
} else {
|
||||
let system_home_path = system_install_sys_prefix.join("bin");
|
||||
let system_exe_path = system_home_path.join("python");
|
||||
let system_site_packages_path =
|
||||
system_install_sys_prefix.join(&unix_site_packages);
|
||||
(system_home_path, system_exe_path, system_site_packages_path)
|
||||
};
|
||||
memory_fs.write_file(system_exe_path, "").unwrap();
|
||||
memory_fs
|
||||
.create_directory_all(&system_site_packages_path)
|
||||
.unwrap();
|
||||
|
||||
let venv_sys_prefix = SystemPathBuf::from("/.venv");
|
||||
let (venv_exe, site_packages_path) = if cfg!(target_os = "windows") {
|
||||
(
|
||||
venv_sys_prefix.join(r"Scripts\python.exe"),
|
||||
venv_sys_prefix.join(r"Lib\site-packages"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
venv_sys_prefix.join("bin/python"),
|
||||
venv_sys_prefix.join(&unix_site_packages),
|
||||
)
|
||||
};
|
||||
memory_fs.write_file(&venv_exe, "").unwrap();
|
||||
memory_fs.create_directory_all(&site_packages_path).unwrap();
|
||||
|
||||
let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg");
|
||||
let mut pyvenv_cfg_contents = format!("home = {system_home_path}\n");
|
||||
if let Some(version_field) = pyvenv_cfg_version_field {
|
||||
pyvenv_cfg_contents.push_str(version_field);
|
||||
pyvenv_cfg_contents.push('\n');
|
||||
}
|
||||
// Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive:
|
||||
if *system_site_packages {
|
||||
pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n");
|
||||
}
|
||||
memory_fs
|
||||
.write_file(pyvenv_cfg_path, &pyvenv_cfg_contents)
|
||||
.unwrap();
|
||||
|
||||
venv_sys_prefix
|
||||
}
|
||||
|
||||
fn test(self) {
|
||||
let venv_path = self.build_mock_venv();
|
||||
let venv = VirtualEnvironment::new(venv_path.clone(), &self.system).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
venv.venv_path,
|
||||
SysPrefixPath(self.system.canonicalize_path(&venv_path).unwrap())
|
||||
);
|
||||
assert_eq!(venv.include_system_site_packages, self.system_site_packages);
|
||||
|
||||
if self.pyvenv_cfg_version_field.is_some() {
|
||||
assert_eq!(
|
||||
venv.version,
|
||||
Some(PythonVersion {
|
||||
major: 3,
|
||||
minor: self.minor_version
|
||||
})
|
||||
);
|
||||
} else {
|
||||
assert_eq!(venv.version, None);
|
||||
}
|
||||
|
||||
let expected_home = if cfg!(target_os = "windows") {
|
||||
SystemPathBuf::from(&*format!(r"\Python3.{}", self.minor_version))
|
||||
} else {
|
||||
SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version))
|
||||
};
|
||||
assert_eq!(venv.base_executable_home_path, expected_home);
|
||||
|
||||
let site_packages_directories = venv.site_packages_directories(&self.system).unwrap();
|
||||
let expected_venv_site_packages = if cfg!(target_os = "windows") {
|
||||
SystemPathBuf::from(r"\.venv\Lib\site-packages")
|
||||
} else if self.free_threaded {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/.venv/lib/python3.{}t/site-packages",
|
||||
self.minor_version
|
||||
))
|
||||
} else {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/.venv/lib/python3.{}/site-packages",
|
||||
self.minor_version
|
||||
))
|
||||
};
|
||||
|
||||
let expected_system_site_packages = if cfg!(target_os = "windows") {
|
||||
SystemPathBuf::from(&*format!(
|
||||
r"\Python3.{}\Lib\site-packages",
|
||||
self.minor_version
|
||||
))
|
||||
} else if self.free_threaded {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages",
|
||||
minor_version = self.minor_version
|
||||
))
|
||||
} else {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages",
|
||||
minor_version = self.minor_version
|
||||
))
|
||||
};
|
||||
|
||||
if self.system_site_packages {
|
||||
assert_eq!(
|
||||
&site_packages_directories,
|
||||
&[expected_venv_site_packages, expected_system_site_packages]
|
||||
);
|
||||
} else {
|
||||
assert_eq!(&site_packages_directories, &[expected_venv_site_packages]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Windows venvs have different layouts, and we only have a Unix venv committed for now.
|
||||
// This test is skipped on Windows until we commit a Windows venv.
|
||||
#[cfg_attr(target_os = "windows", ignore = "Windows has a different venv layout")]
|
||||
fn can_find_site_packages_dir_in_committed_venv() {
|
||||
let path_to_venv = SystemPath::new("resources/test/unix-uv-venv");
|
||||
let system = OsSystem::default();
|
||||
fn can_find_site_packages_directory_no_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: None,
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
// if this doesn't hold true, the premise of the test is incorrect.
|
||||
assert!(system.is_directory(path_to_venv));
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version = 3.12"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
let site_packages_dirs = site_packages_dirs_of_venv(path_to_venv, &system).unwrap();
|
||||
assert_eq!(site_packages_dirs.len(), 1);
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.12"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_freethreaded_build() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 13,
|
||||
free_threaded: true,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.13"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_system_site_packages() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 13,
|
||||
free_threaded: true,
|
||||
system_site_packages: true,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.13"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_venv_that_does_not_exist() {
|
||||
let system = TestSystem::default();
|
||||
assert!(matches!(
|
||||
VirtualEnvironment::new("/.venv", &system),
|
||||
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_venv_with_no_pyvenv_cfg_file() {
|
||||
let system = TestSystem::default();
|
||||
system
|
||||
.memory_file_system()
|
||||
.create_directory_all("/.venv")
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
VirtualEnvironment::new("/.venv", &system),
|
||||
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_too_many_equals() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs
|
||||
.write_file(&pyvenv_cfg_path, "home = bar = /.venv/bin")
|
||||
.unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::TooManyEquals { line_number }
|
||||
))
|
||||
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_key_but_no_value_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs.write_file(&pyvenv_cfg_path, "home =").unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
|
||||
))
|
||||
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_value_but_no_key_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs
|
||||
.write_file(&pyvenv_cfg_path, "= whatever")
|
||||
.unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
|
||||
))
|
||||
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_no_home_key_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs.write_file(&pyvenv_cfg_path, "").unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::NoHomeKey
|
||||
))
|
||||
if path == pyvenv_cfg_path
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_invalid_home_key_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs
|
||||
.write_file(&pyvenv_cfg_path, "home = foo")
|
||||
.unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::InvalidHomeValue(_)
|
||||
))
|
||||
if path == pyvenv_cfg_path
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use red_knot_python_semantic::{ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::lint::lint_semantic;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{OsSystem, SystemPathBuf};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -17,7 +17,7 @@ fn setup_db(workspace_root: SystemPathBuf) -> anyhow::Result<RootDatabase> {
|
||||
site_packages: vec![],
|
||||
};
|
||||
let settings = ProgramSettings {
|
||||
target_version: TargetVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths,
|
||||
};
|
||||
let db = RootDatabase::new(workspace, settings, system);
|
||||
|
||||
@@ -52,6 +52,7 @@ ruff_python_ast = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_workspace = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#![allow(clippy::disallowed_names)]
|
||||
|
||||
use red_knot_python_semantic::{ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_benchmark::criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use ruff_benchmark::TestFile;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{MemoryFileSystem, SystemPath, TestSystem};
|
||||
|
||||
@@ -43,7 +43,7 @@ fn setup_case() -> Case {
|
||||
let src_root = SystemPath::new("/src");
|
||||
let metadata = WorkspaceMetadata::from_path(src_root, &system).unwrap();
|
||||
let settings = ProgramSettings {
|
||||
target_version: TargetVersion::Py312,
|
||||
target_version: PythonVersion::PY312,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src_root.to_path_buf(),
|
||||
|
||||
@@ -28,6 +28,8 @@ matchit = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
tracing-tree = { workspace = true, optional = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
|
||||
@@ -44,3 +46,5 @@ tempfile = { workspace = true }
|
||||
[features]
|
||||
cache = ["ruff_cache"]
|
||||
os = ["ignore"]
|
||||
# Exposes testing utilities.
|
||||
testing = ["tracing-subscriber", "tracing-tree"]
|
||||
|
||||
@@ -9,9 +9,9 @@ use crate::vendored::VendoredFileSystem;
|
||||
pub mod file_revision;
|
||||
pub mod files;
|
||||
pub mod parsed;
|
||||
pub mod program;
|
||||
pub mod source;
|
||||
pub mod system;
|
||||
#[cfg(feature = "testing")]
|
||||
pub mod testing;
|
||||
pub mod vendored;
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! Test helpers for working with Salsa databases
|
||||
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
pub fn assert_function_query_was_not_run<Db, Q, QDb, I, R>(
|
||||
db: &Db,
|
||||
query: Q,
|
||||
@@ -94,6 +97,116 @@ fn query_name<Q>(_query: &Q) -> &'static str {
|
||||
.unwrap_or(full_qualified_query_name)
|
||||
}
|
||||
|
||||
/// Sets up logging for the current thread. It captures all `red_knot` and `ruff` events.
|
||||
///
|
||||
/// Useful for capturing the tracing output in a failing test.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ruff_db::testing::setup_logging;
|
||||
/// let _logging = setup_logging();
|
||||
///
|
||||
/// tracing::info!("This message will be printed to stderr");
|
||||
/// ```
|
||||
pub fn setup_logging() -> LoggingGuard {
|
||||
LoggingBuilder::new().build()
|
||||
}
|
||||
|
||||
/// Sets up logging for the current thread and uses the passed filter to filter the shown events.
|
||||
/// Useful for capturing the tracing output in a failing test.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ruff_db::testing::setup_logging_with_filter;
|
||||
/// let _logging = setup_logging_with_filter("red_knot_module_resolver::resolver");
|
||||
/// ```
|
||||
///
|
||||
/// # Filter
|
||||
/// See [`tracing_subscriber::EnvFilter`] for the `filter`'s syntax.
|
||||
///
|
||||
pub fn setup_logging_with_filter(filter: &str) -> Option<LoggingGuard> {
|
||||
LoggingBuilder::with_filter(filter).map(LoggingBuilder::build)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoggingBuilder {
|
||||
filter: EnvFilter,
|
||||
hierarchical: bool,
|
||||
}
|
||||
|
||||
impl LoggingBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
filter: EnvFilter::default()
|
||||
.add_directive(
|
||||
"red_knot=trace"
|
||||
.parse()
|
||||
.expect("Hardcoded directive to be valid"),
|
||||
)
|
||||
.add_directive(
|
||||
"ruff=trace"
|
||||
.parse()
|
||||
.expect("Hardcoded directive to be valid"),
|
||||
),
|
||||
hierarchical: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_filter(filter: &str) -> Option<Self> {
|
||||
let filter = EnvFilter::builder().parse(filter).ok()?;
|
||||
|
||||
Some(Self {
|
||||
filter,
|
||||
hierarchical: true,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_hierarchical(mut self, hierarchical: bool) -> Self {
|
||||
self.hierarchical = hierarchical;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> LoggingGuard {
|
||||
let registry = tracing_subscriber::registry().with(self.filter);
|
||||
|
||||
let guard = if self.hierarchical {
|
||||
let subscriber = registry.with(
|
||||
tracing_tree::HierarchicalLayer::default()
|
||||
.with_indent_lines(true)
|
||||
.with_indent_amount(2)
|
||||
.with_bracketed_fields(true)
|
||||
.with_thread_ids(true)
|
||||
.with_targets(true)
|
||||
.with_writer(std::io::stderr)
|
||||
.with_timer(tracing_tree::time::Uptime::default()),
|
||||
);
|
||||
|
||||
tracing::subscriber::set_default(subscriber)
|
||||
} else {
|
||||
let subscriber = registry.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.compact()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_timer(tracing_subscriber::fmt::time()),
|
||||
);
|
||||
|
||||
tracing::subscriber::set_default(subscriber)
|
||||
};
|
||||
|
||||
LoggingGuard { _guard: guard }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LoggingBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoggingGuard {
|
||||
_guard: tracing::subscriber::DefaultGuard,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_was_not_run() {
|
||||
use crate::tests::TestDb;
|
||||
|
||||
@@ -18,7 +18,7 @@ impl Display for FixAvailability {
|
||||
}
|
||||
|
||||
pub trait Violation: Debug + PartialEq + Eq {
|
||||
/// `None` in the case an fix is never available or otherwise Some
|
||||
/// `None` in the case a fix is never available or otherwise Some
|
||||
/// [`FixAvailability`] describing the available fix.
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
|
||||
|
||||
@@ -135,3 +135,62 @@ if TYPE_CHECKING:
|
||||
x = 3
|
||||
else:
|
||||
x = 5
|
||||
|
||||
# SIM108 - should suggest
|
||||
# z = cond or other_cond
|
||||
if cond:
|
||||
z = cond
|
||||
else:
|
||||
z = other_cond
|
||||
|
||||
# SIM108 - should suggest
|
||||
# z = cond and other_cond
|
||||
if not cond:
|
||||
z = cond
|
||||
else:
|
||||
z = other_cond
|
||||
|
||||
# SIM108 - should suggest
|
||||
# z = not cond and other_cond
|
||||
if cond:
|
||||
z = not cond
|
||||
else:
|
||||
z = other_cond
|
||||
|
||||
# SIM108 does not suggest
|
||||
# a binary option in these cases,
|
||||
# despite the fact that `bool`
|
||||
# is a subclass of both `int` and `float`
|
||||
# so, e.g. `True == 1`.
|
||||
# (Of course, these specific expressions
|
||||
# should be simplified for other reasons...)
|
||||
if True:
|
||||
z = 1
|
||||
else:
|
||||
z = other
|
||||
|
||||
if False:
|
||||
z = 1
|
||||
else:
|
||||
z = other
|
||||
|
||||
if 1:
|
||||
z = True
|
||||
else:
|
||||
z = other
|
||||
|
||||
# SIM108 does not suggest a binary option in this
|
||||
# case, since we'd be reducing the number of calls
|
||||
# from Two to one.
|
||||
if foo():
|
||||
z = foo()
|
||||
else:
|
||||
z = other
|
||||
|
||||
# SIM108 does not suggest a binary option in this
|
||||
# case, since we'd be reducing the number of calls
|
||||
# from Two to one.
|
||||
if foo():
|
||||
z = not foo()
|
||||
else:
|
||||
z = other
|
||||
|
||||
@@ -123,3 +123,14 @@ class RenamingInMethodBodyClass:
|
||||
class RenamingWithNFKC:
|
||||
def formula(household):
|
||||
hºusehold(1)
|
||||
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class MyMeta(type):
|
||||
def __subclasscheck__(cls, other): ...
|
||||
|
||||
|
||||
class MyProtocolMeta(type(Protocol)):
|
||||
def __subclasscheck__(cls, other): ...
|
||||
|
||||
@@ -108,3 +108,14 @@ def f(num: int):
|
||||
num (int): A number
|
||||
"""
|
||||
return 1
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class A(metaclass=abc.abcmeta):
|
||||
# DOC201
|
||||
@abc.abstractmethod
|
||||
def f(self):
|
||||
"""Lorem ipsum."""
|
||||
return True
|
||||
|
||||
@@ -74,3 +74,14 @@ class Bar:
|
||||
A number
|
||||
"""
|
||||
return 'test'
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class A(metaclass=abc.abcmeta):
|
||||
# DOC201
|
||||
@abc.abstractmethod
|
||||
def f(self):
|
||||
"""Lorem ipsum."""
|
||||
return True
|
||||
|
||||
@@ -59,3 +59,17 @@ class C:
|
||||
x
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class A(metaclass=abc.abcmeta):
|
||||
@abc.abstractmethod
|
||||
def f(self):
|
||||
"""Lorem ipsum
|
||||
|
||||
Returns:
|
||||
dict: The values
|
||||
"""
|
||||
return
|
||||
|
||||
@@ -60,3 +60,19 @@ class Bar:
|
||||
A number
|
||||
"""
|
||||
print('test')
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class A(metaclass=abc.abcmeta):
|
||||
@abc.abstractmethod
|
||||
def f(self):
|
||||
"""Lorem ipsum
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:
|
||||
The values
|
||||
"""
|
||||
return
|
||||
|
||||
@@ -26,4 +26,19 @@ token_features[
|
||||
|
||||
d[1,]
|
||||
d[(1,)]
|
||||
d[()] # empty tuples should be ignored
|
||||
d[()] # empty tuples should be ignored
|
||||
d[:,] # slices in the subscript lead to syntax error if parens are added
|
||||
d[1,2,:]
|
||||
|
||||
# Should keep these parentheses in
|
||||
# Python <=3.10 to avoid syntax error.
|
||||
# https://github.com/astral-sh/ruff/issues/12776
|
||||
d[(*foo,bar)]
|
||||
|
||||
x: dict[str, int] # tuples inside type annotations should never be altered
|
||||
|
||||
import typing
|
||||
|
||||
type Y = typing.Literal[1, 2]
|
||||
Z: typing.TypeAlias = dict[int, int]
|
||||
class Foo(dict[str, int]): pass
|
||||
|
||||
@@ -25,4 +25,20 @@ token_features[
|
||||
] = self._extract_raw_features_from_token
|
||||
d[1,]
|
||||
d[(1,)]
|
||||
d[()] # empty tuples should be ignored
|
||||
d[()] # empty tuples should be ignored
|
||||
|
||||
d[:,] # slices in the subscript lead to syntax error if parens are added
|
||||
d[1,2,:]
|
||||
|
||||
# Should keep these parentheses in
|
||||
# Python <=3.10 to avoid syntax error.
|
||||
# https://github.com/astral-sh/ruff/issues/12776
|
||||
d[(*foo,bar)]
|
||||
|
||||
x: dict[str, int] # tuples inside type annotations should never be altered
|
||||
|
||||
import typing
|
||||
|
||||
type Y = typing.Literal[1, 2]
|
||||
Z: typing.TypeAlias = dict[int, int]
|
||||
class Foo(dict[str, int]): pass
|
||||
|
||||
@@ -43,7 +43,8 @@ pub(crate) fn check_noqa(
|
||||
let exemption = FileExemption::from(&file_noqa_directives);
|
||||
|
||||
// Extract all `noqa` directives.
|
||||
let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
|
||||
let mut noqa_directives =
|
||||
NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator);
|
||||
|
||||
// Indices of diagnostics that were ignored by a `noqa` directive.
|
||||
let mut ignored_diagnostics = vec![];
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn generate_noqa_edits(
|
||||
let file_directives =
|
||||
FileNoqaDirectives::extract(locator.contents(), comment_ranges, external, path, locator);
|
||||
let exemption = FileExemption::from(&file_directives);
|
||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
|
||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
|
||||
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
|
||||
build_noqa_edits_by_diagnostic(comments, locator, line_ending)
|
||||
}
|
||||
@@ -183,7 +183,7 @@ impl<'a> Directive<'a> {
|
||||
// Extract, e.g., the `401` in `F401`.
|
||||
let suffix = line[prefix..]
|
||||
.chars()
|
||||
.take_while(char::is_ascii_digit)
|
||||
.take_while(char::is_ascii_alphanumeric)
|
||||
.count();
|
||||
if prefix > 0 && suffix > 0 {
|
||||
Some(&line[..prefix + suffix])
|
||||
@@ -550,7 +550,7 @@ impl<'a> ParsedFileExemption<'a> {
|
||||
// Extract, e.g., the `401` in `F401`.
|
||||
let suffix = line[prefix..]
|
||||
.chars()
|
||||
.take_while(char::is_ascii_digit)
|
||||
.take_while(char::is_ascii_alphanumeric)
|
||||
.count();
|
||||
if prefix > 0 && suffix > 0 {
|
||||
Some(&line[..prefix + suffix])
|
||||
@@ -623,7 +623,7 @@ fn add_noqa_inner(
|
||||
FileNoqaDirectives::extract(locator.contents(), comment_ranges, external, path, locator);
|
||||
let exemption = FileExemption::from(&directives);
|
||||
|
||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
|
||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
|
||||
|
||||
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
|
||||
|
||||
@@ -897,7 +897,7 @@ pub(crate) struct NoqaDirectiveLine<'a> {
|
||||
pub(crate) directive: Directive<'a>,
|
||||
/// The codes that are ignored by the directive.
|
||||
pub(crate) matches: Vec<NoqaCode>,
|
||||
// Whether the directive applies to range.end
|
||||
/// Whether the directive applies to `range.end`.
|
||||
pub(crate) includes_end: bool,
|
||||
}
|
||||
|
||||
@@ -916,6 +916,7 @@ pub(crate) struct NoqaDirectives<'a> {
|
||||
impl<'a> NoqaDirectives<'a> {
|
||||
pub(crate) fn from_commented_ranges(
|
||||
comment_ranges: &CommentRanges,
|
||||
external: &[String],
|
||||
path: &Path,
|
||||
locator: &'a Locator<'a>,
|
||||
) -> Self {
|
||||
@@ -930,7 +931,29 @@ impl<'a> NoqaDirectives<'a> {
|
||||
warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}");
|
||||
}
|
||||
Ok(Some(directive)) => {
|
||||
// noqa comments are guaranteed to be single line.
|
||||
if let Directive::Codes(codes) = &directive {
|
||||
// Warn on invalid rule codes.
|
||||
for code in &codes.codes {
|
||||
// Ignore externally-defined rules.
|
||||
if !external
|
||||
.iter()
|
||||
.any(|external| code.as_str().starts_with(external))
|
||||
{
|
||||
if Rule::from_code(
|
||||
get_redirect_target(code.as_str()).unwrap_or(code.as_str()),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
let line = locator.compute_line_index(range.start());
|
||||
let path_display = relativize_path(path);
|
||||
warn!("Invalid rule code provided to `# noqa` at {path_display}:{line}: {code}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `# noqa` comments are guaranteed to be single line.
|
||||
let range = locator.line_range(range.start());
|
||||
directives.push(NoqaDirectiveLine {
|
||||
range,
|
||||
@@ -1193,6 +1216,18 @@ mod tests {
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_squashed_codes() {
|
||||
let source = "# noqa: F401F841";
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_empty_comma() {
|
||||
let source = "# noqa: F401,,F841";
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_invalid_suffix() {
|
||||
let source = "# noqa[F401]";
|
||||
|
||||
@@ -24,15 +24,15 @@ use crate::rules::ruff::typing::type_hint_resolves_to_any;
|
||||
/// any provided arguments match expectation.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(x):
|
||||
/// ...
|
||||
/// def foo(x): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(x: int):
|
||||
/// ...
|
||||
/// def foo(x: int): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTypeFunctionArgument {
|
||||
@@ -56,15 +56,15 @@ impl Violation for MissingTypeFunctionArgument {
|
||||
/// any provided arguments match expectation.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(*args):
|
||||
/// ...
|
||||
/// def foo(*args): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(*args: int):
|
||||
/// ...
|
||||
/// def foo(*args: int): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTypeArgs {
|
||||
@@ -88,15 +88,15 @@ impl Violation for MissingTypeArgs {
|
||||
/// any provided arguments match expectation.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(**kwargs):
|
||||
/// ...
|
||||
/// def foo(**kwargs): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(**kwargs: int):
|
||||
/// ...
|
||||
/// def foo(**kwargs: int): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTypeKwargs {
|
||||
@@ -127,17 +127,17 @@ impl Violation for MissingTypeKwargs {
|
||||
/// annotation is not strictly necessary.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def bar(self):
|
||||
/// ...
|
||||
/// def bar(self): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def bar(self: "Foo"):
|
||||
/// ...
|
||||
/// def bar(self: "Foo"): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTypeSelf {
|
||||
@@ -168,19 +168,19 @@ impl Violation for MissingTypeSelf {
|
||||
/// annotation is not strictly necessary.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// @classmethod
|
||||
/// def bar(cls):
|
||||
/// ...
|
||||
/// def bar(cls): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// @classmethod
|
||||
/// def bar(cls: Type["Foo"]):
|
||||
/// ...
|
||||
/// def bar(cls: Type["Foo"]): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct MissingTypeCls {
|
||||
@@ -449,29 +449,29 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
/// `Any` as an "escape hatch" only when it is really needed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(x: Any):
|
||||
/// ...
|
||||
/// def foo(x: Any): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(x: int):
|
||||
/// ...
|
||||
/// def foo(x: int): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// Type aliases are unsupported and can lead to false positives.
|
||||
/// For example, the following will trigger this rule inadvertently:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import Any
|
||||
///
|
||||
/// MyAny = Any
|
||||
///
|
||||
///
|
||||
/// def foo(x: MyAny):
|
||||
/// ...
|
||||
/// def foo(x: MyAny): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -17,9 +17,9 @@ use crate::settings::types::PreviewMode;
|
||||
/// or `anyio.move_on_after`, among others.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// async def long_running_task(timeout):
|
||||
/// ...
|
||||
/// async def long_running_task(timeout): ...
|
||||
///
|
||||
///
|
||||
/// async def main():
|
||||
@@ -27,9 +27,9 @@ use crate::settings::types::PreviewMode;
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// async def long_running_task():
|
||||
/// ...
|
||||
/// async def long_running_task(): ...
|
||||
///
|
||||
///
|
||||
/// async def main():
|
||||
|
||||
@@ -22,18 +22,18 @@ use super::super::helpers::{matches_password_name, string_literal};
|
||||
/// control.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def connect_to_server(password="hunter2"):
|
||||
/// ...
|
||||
/// def connect_to_server(password="hunter2"): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
///
|
||||
/// def connect_to_server(password=os.environ["PASSWORD"]):
|
||||
/// ...
|
||||
/// def connect_to_server(password=os.environ["PASSWORD"]): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -18,21 +18,21 @@ use crate::checkers::ast::Checker;
|
||||
/// - TLS v1.1
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
///
|
||||
/// def func(version=ssl.PROTOCOL_TLSv1):
|
||||
/// ...
|
||||
/// def func(version=ssl.PROTOCOL_TLSv1): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import ssl
|
||||
///
|
||||
///
|
||||
/// def func(version=ssl.PROTOCOL_TLSv1_2):
|
||||
/// ...
|
||||
/// def func(version=ssl.PROTOCOL_TLSv1_2): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct SslWithBadDefaults {
|
||||
|
||||
@@ -10,7 +10,9 @@ use ruff_text_size::Ranged;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `except` clauses that catch all exceptions.
|
||||
/// Checks for `except` clauses that catch all exceptions. This includes
|
||||
/// bare `except`, `except BaseException` and `except Exception`.
|
||||
///
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Overly broad `except` clauses can lead to unexpected behavior, such as
|
||||
@@ -58,6 +60,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ## References
|
||||
/// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement)
|
||||
/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)
|
||||
/// - [PEP8 Programming Recommendations on bare `except`](https://peps.python.org/pep-0008/#programming-recommendations)
|
||||
#[violation]
|
||||
pub struct BlindExcept {
|
||||
name: String,
|
||||
|
||||
@@ -18,18 +18,18 @@ use crate::rules::flake8_boolean_trap::helpers::allow_boolean_trap;
|
||||
/// readers of the code.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def func(flag: bool) -> None:
|
||||
/// ...
|
||||
/// def func(flag: bool) -> None: ...
|
||||
///
|
||||
///
|
||||
/// func(True)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def func(flag: bool) -> None:
|
||||
/// ...
|
||||
/// def func(flag: bool) -> None: ...
|
||||
///
|
||||
///
|
||||
/// func(flag=True)
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// variants, like `bool | int`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
@@ -44,6 +45,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// ```
|
||||
///
|
||||
/// Instead, refactor into separate implementations:
|
||||
///
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
@@ -61,6 +63,7 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// ```
|
||||
///
|
||||
/// Or, refactor to use an `Enum`:
|
||||
///
|
||||
/// ```python
|
||||
/// from enum import Enum
|
||||
///
|
||||
@@ -70,11 +73,11 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
|
||||
/// DOWN = 2
|
||||
///
|
||||
///
|
||||
/// def round_number(value: float, method: RoundingMethod) -> float:
|
||||
/// ...
|
||||
/// def round_number(value: float, method: RoundingMethod) -> float: ...
|
||||
/// ```
|
||||
///
|
||||
/// Or, make the argument a keyword-only argument:
|
||||
///
|
||||
/// ```python
|
||||
/// from math import ceil, floor
|
||||
///
|
||||
|
||||
@@ -67,24 +67,24 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod {
|
||||
/// `@abstractmethod` decorator to the method.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from abc import ABC
|
||||
///
|
||||
///
|
||||
/// class Foo(ABC):
|
||||
/// def method(self):
|
||||
/// ...
|
||||
/// def method(self): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from abc import ABC, abstractmethod
|
||||
///
|
||||
///
|
||||
/// class Foo(ABC):
|
||||
/// @abstractmethod
|
||||
/// def method(self):
|
||||
/// ...
|
||||
/// def method(self): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::checkers::ast::Checker;
|
||||
/// [`lint.flake8-bugbear.extend-immutable-calls`] configuration option as well.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def create_list() -> list[int]:
|
||||
/// return [1, 2, 3]
|
||||
@@ -41,6 +42,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def better(arg: list[int] | None = None) -> list[int]:
|
||||
/// if arg is None:
|
||||
@@ -52,12 +54,12 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// If the use of a singleton is intentional, assign the result call to a
|
||||
/// module-level variable, and use that variable in the default argument:
|
||||
///
|
||||
/// ```python
|
||||
/// ERROR = ValueError("Hosts weren't successfully added")
|
||||
///
|
||||
///
|
||||
/// def add_host(error: Exception = ERROR) -> None:
|
||||
/// ...
|
||||
/// def add_host(error: Exception = ERROR) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_python_semantic::{analyze, SemanticModel};
|
||||
|
||||
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
|
||||
pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["django", "db", "models", "Model"]
|
||||
@@ -14,7 +14,7 @@ pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel)
|
||||
|
||||
/// Return `true` if a Python class appears to be a Django model form, based on its base classes.
|
||||
pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]
|
||||
|
||||
@@ -29,18 +29,18 @@ use crate::checkers::ast::Checker;
|
||||
/// flag such usages if your project targets Python 3.9 or below.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def func(obj: dict[str, int | None]) -> None:
|
||||
/// ...
|
||||
/// def func(obj: dict[str, int | None]) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from __future__ import annotations
|
||||
///
|
||||
///
|
||||
/// def func(obj: dict[str, int | None]) -> None:
|
||||
/// ...
|
||||
/// def func(obj: dict[str, int | None]) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
|
||||
@@ -33,32 +33,32 @@ use crate::checkers::ast::Checker;
|
||||
/// flag such usages if your project targets Python 3.9 or below.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import List, Dict, Optional
|
||||
///
|
||||
///
|
||||
/// def func(obj: Dict[str, Optional[int]]) -> None:
|
||||
/// ...
|
||||
/// def func(obj: Dict[str, Optional[int]]) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from __future__ import annotations
|
||||
///
|
||||
/// from typing import List, Dict, Optional
|
||||
///
|
||||
///
|
||||
/// def func(obj: Dict[str, Optional[int]]) -> None:
|
||||
/// ...
|
||||
/// def func(obj: Dict[str, Optional[int]]) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// After running the additional pyupgrade rules:
|
||||
///
|
||||
/// ```python
|
||||
/// from __future__ import annotations
|
||||
///
|
||||
///
|
||||
/// def func(obj: dict[str, int | None]) -> None:
|
||||
/// ...
|
||||
/// def func(obj: dict[str, int | None]) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
|
||||
@@ -26,17 +26,17 @@ use crate::checkers::ast::Checker;
|
||||
/// these comparison operators -- `__eq__` and `__ne__`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __eq__(self, obj: typing.Any) -> bool:
|
||||
/// ...
|
||||
/// def __eq__(self, obj: typing.Any) -> bool: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __eq__(self, obj: object) -> bool:
|
||||
/// ...
|
||||
/// def __eq__(self, obj: object) -> bool: ...
|
||||
/// ```
|
||||
/// ## References
|
||||
/// - [Python documentation: The `Any` type](https://docs.python.org/3/library/typing.html#the-any-type)
|
||||
|
||||
@@ -75,13 +75,11 @@ impl Violation for BadVersionInfoComparison {
|
||||
///
|
||||
/// if sys.version_info < (3, 10):
|
||||
///
|
||||
/// def read_data(x, *, preserve_order=True):
|
||||
/// ...
|
||||
/// def read_data(x, *, preserve_order=True): ...
|
||||
///
|
||||
/// else:
|
||||
///
|
||||
/// def read_data(x):
|
||||
/// ...
|
||||
/// def read_data(x): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
@@ -89,13 +87,11 @@ impl Violation for BadVersionInfoComparison {
|
||||
/// ```python
|
||||
/// if sys.version_info >= (3, 10):
|
||||
///
|
||||
/// def read_data(x):
|
||||
/// ...
|
||||
/// def read_data(x): ...
|
||||
///
|
||||
/// else:
|
||||
///
|
||||
/// def read_data(x, *, preserve_order=True):
|
||||
/// ...
|
||||
/// def read_data(x, *, preserve_order=True): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BadVersionInfoOrder;
|
||||
|
||||
@@ -19,20 +19,21 @@ use crate::checkers::ast::Checker;
|
||||
/// used.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import TypeAlias
|
||||
///
|
||||
/// a = b = int
|
||||
///
|
||||
///
|
||||
/// class Klass:
|
||||
/// ...
|
||||
/// class Klass: ...
|
||||
///
|
||||
///
|
||||
/// Klass.X: TypeAlias = int
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import TypeAlias
|
||||
///
|
||||
|
||||
@@ -24,34 +24,30 @@ use crate::checkers::ast::Checker;
|
||||
/// methods that return an instance of `cls`, and `__new__` methods.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S:
|
||||
/// ...
|
||||
/// def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ...
|
||||
///
|
||||
/// def foo(self: _S, arg: bytes) -> _S:
|
||||
/// ...
|
||||
/// def foo(self: _S, arg: bytes) -> _S: ...
|
||||
///
|
||||
/// @classmethod
|
||||
/// def bar(cls: type[_S], arg: int) -> _S:
|
||||
/// ...
|
||||
/// def bar(cls: type[_S], arg: int) -> _S: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import Self
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: str, **kwargs: int) -> Self:
|
||||
/// ...
|
||||
/// def __new__(cls, *args: str, **kwargs: int) -> Self: ...
|
||||
///
|
||||
/// def foo(self, arg: bytes) -> Self:
|
||||
/// ...
|
||||
/// def foo(self, arg: bytes) -> Self: ...
|
||||
///
|
||||
/// @classmethod
|
||||
/// def bar(cls, arg: int) -> Self:
|
||||
/// ...
|
||||
/// def bar(cls, arg: int) -> Self: ...
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 673]: https://peps.python.org/pep-0673/#motivation
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::checkers::ast::Checker;
|
||||
/// hints, rather than documentation.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def func(param: int) -> str:
|
||||
/// """This is a docstring."""
|
||||
@@ -21,9 +22,9 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def func(param: int) -> str:
|
||||
/// ...
|
||||
/// def func(param: int) -> str: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct DocstringInStub;
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::checkers::ast::Checker;
|
||||
/// unexpected behavior when interacting with type checkers.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from types import TracebackType
|
||||
///
|
||||
@@ -30,11 +31,11 @@ use crate::checkers::ast::Checker;
|
||||
/// class Foo:
|
||||
/// def __exit__(
|
||||
/// self, typ: BaseException, exc: BaseException, tb: TracebackType
|
||||
/// ) -> None:
|
||||
/// ...
|
||||
/// ) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from types import TracebackType
|
||||
///
|
||||
@@ -45,8 +46,7 @@ use crate::checkers::ast::Checker;
|
||||
/// typ: type[BaseException] | None,
|
||||
/// exc: BaseException | None,
|
||||
/// tb: TracebackType | None,
|
||||
/// ) -> None:
|
||||
/// ...
|
||||
/// ) -> None: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct BadExitAnnotation {
|
||||
|
||||
@@ -50,23 +50,23 @@ use crate::checkers::ast::Checker;
|
||||
/// on the returned object, violating the expectations of the interface.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import collections.abc
|
||||
///
|
||||
///
|
||||
/// class Klass:
|
||||
/// def __iter__(self) -> collections.abc.Iterable[str]:
|
||||
/// ...
|
||||
/// def __iter__(self) -> collections.abc.Iterable[str]: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import collections.abc
|
||||
///
|
||||
///
|
||||
/// class Klass:
|
||||
/// def __iter__(self) -> collections.abc.Iterator[str]:
|
||||
/// ...
|
||||
/// def __iter__(self) -> collections.abc.Iterator[str]: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct IterMethodReturnIterable {
|
||||
|
||||
@@ -50,38 +50,32 @@ use crate::checkers::ast::Checker;
|
||||
/// inheriting directly from `AsyncIterator`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Foo:
|
||||
/// ...
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Foo: ...
|
||||
///
|
||||
/// def __enter__(self) -> Foo:
|
||||
/// ...
|
||||
/// def __enter__(self) -> Foo: ...
|
||||
///
|
||||
/// async def __aenter__(self) -> Foo:
|
||||
/// ...
|
||||
/// async def __aenter__(self) -> Foo: ...
|
||||
///
|
||||
/// def __iadd__(self, other: Foo) -> Foo:
|
||||
/// ...
|
||||
/// def __iadd__(self, other: Foo) -> Foo: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing_extensions import Self
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
||||
/// ...
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Self: ...
|
||||
///
|
||||
/// def __enter__(self) -> Self:
|
||||
/// ...
|
||||
/// def __enter__(self) -> Self: ...
|
||||
///
|
||||
/// async def __aenter__(self) -> Self:
|
||||
/// ...
|
||||
/// async def __aenter__(self) -> Self: ...
|
||||
///
|
||||
/// def __iadd__(self, other: Foo) -> Self:
|
||||
/// ...
|
||||
/// def __iadd__(self, other: Foo) -> Self: ...
|
||||
/// ```
|
||||
/// ## References
|
||||
/// - [`typing.Self` documentation](https://docs.python.org/3/library/typing.html#typing.Self)
|
||||
@@ -254,7 +248,7 @@ fn is_self(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
|
||||
/// Return `true` if the given class extends `collections.abc.Iterator`.
|
||||
fn subclasses_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["typing", "Iterator"] | ["collections", "abc", "Iterator"]
|
||||
@@ -277,7 +271,7 @@ fn is_iterable_or_iterator(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
|
||||
/// Return `true` if the given class extends `collections.abc.AsyncIterator`.
|
||||
fn subclasses_async_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["typing", "AsyncIterator"] | ["collections", "abc", "AsyncIterator"]
|
||||
|
||||
@@ -19,15 +19,15 @@ use crate::checkers::ast::Checker;
|
||||
/// ellipses (`...`) instead.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg: int = 693568516352839939918568862861217771399698285293568) -> None:
|
||||
/// ...
|
||||
/// def foo(arg: int = 693568516352839939918568862861217771399698285293568) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg: int = ...) -> None:
|
||||
/// ...
|
||||
/// def foo(arg: int = ...) -> None: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct NumericLiteralTooLong;
|
||||
|
||||
@@ -18,15 +18,13 @@ use crate::settings::types::PythonVersion;
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(__x: int) -> None:
|
||||
/// ...
|
||||
/// def foo(__x: int) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(x: int, /) -> None:
|
||||
/// ...
|
||||
/// def foo(x: int, /) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 484]: https://peps.python.org/pep-0484/#positional-only-arguments
|
||||
|
||||
@@ -15,15 +15,15 @@ use crate::checkers::ast::Checker;
|
||||
/// annotations in stub files, and should be omitted.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def function() -> "int":
|
||||
/// ...
|
||||
/// def function() -> "int": ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def function() -> int:
|
||||
/// ...
|
||||
/// def function() -> int: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -26,15 +26,15 @@ use crate::checkers::ast::Checker;
|
||||
/// redundant elements.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(x: float | int | str) -> None:
|
||||
/// ...
|
||||
/// def foo(x: float | int | str) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(x: float | str) -> None:
|
||||
/// ...
|
||||
/// def foo(x: float | str) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -30,15 +30,15 @@ use crate::settings::types::PythonVersion;
|
||||
/// or a simple container literal.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg: list[int] = list(range(10_000))) -> None:
|
||||
/// ...
|
||||
/// def foo(arg: list[int] = list(range(10_000))) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg: list[int] = ...) -> None:
|
||||
/// ...
|
||||
/// def foo(arg: list[int] = ...) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
@@ -76,15 +76,15 @@ impl AlwaysFixableViolation for TypedArgumentDefaultInStub {
|
||||
/// or varies according to the current platform or Python version.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg=[]) -> None:
|
||||
/// ...
|
||||
/// def foo(arg=[]) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg=...) -> None:
|
||||
/// ...
|
||||
/// def foo(arg=...) -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -18,10 +18,10 @@ use crate::fix::edits::delete_stmt;
|
||||
/// equivalent, `object.__str__` and `object.__repr__`, respectively.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __repr__(self) -> str:
|
||||
/// ...
|
||||
/// def __repr__(self) -> str: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct StrOrReprDefinedInStub {
|
||||
|
||||
@@ -22,15 +22,15 @@ use crate::checkers::ast::Checker;
|
||||
/// with ellipses (`...`) to simplify the stub.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg: str = "51 character stringgggggggggggggggggggggggggggggggg") -> None:
|
||||
/// ...
|
||||
/// def foo(arg: str = "51 character stringgggggggggggggggggggggggggggggggg") -> None: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def foo(arg: str = ...) -> None:
|
||||
/// ...
|
||||
/// def foo(arg: str = ...) -> None: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct StringOrBytesTooLong;
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::checkers::ast::Checker;
|
||||
/// should instead contain only a single statement (e.g., `...`).
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def function():
|
||||
/// x = 1
|
||||
@@ -23,9 +24,9 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def function():
|
||||
/// ...
|
||||
/// def function(): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct StubBodyMultipleStatements;
|
||||
|
||||
@@ -49,6 +49,7 @@ impl Violation for UnusedPrivateTypeVar {
|
||||
/// confusion.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import typing
|
||||
///
|
||||
@@ -58,6 +59,7 @@ impl Violation for UnusedPrivateTypeVar {
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import typing
|
||||
///
|
||||
@@ -66,8 +68,7 @@ impl Violation for UnusedPrivateTypeVar {
|
||||
/// foo: int
|
||||
///
|
||||
///
|
||||
/// def func(arg: _PrivateProtocol) -> None:
|
||||
/// ...
|
||||
/// def func(arg: _PrivateProtocol) -> None: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct UnusedPrivateProtocol {
|
||||
@@ -91,6 +92,7 @@ impl Violation for UnusedPrivateProtocol {
|
||||
/// confusion.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import typing
|
||||
///
|
||||
@@ -98,14 +100,14 @@ impl Violation for UnusedPrivateProtocol {
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import typing
|
||||
///
|
||||
/// _UsedTypeAlias: typing.TypeAlias = int
|
||||
///
|
||||
///
|
||||
/// def func(arg: _UsedTypeAlias) -> _UsedTypeAlias:
|
||||
/// ...
|
||||
/// def func(arg: _UsedTypeAlias) -> _UsedTypeAlias: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct UnusedPrivateTypeAlias {
|
||||
@@ -129,6 +131,7 @@ impl Violation for UnusedPrivateTypeAlias {
|
||||
/// confusion.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import typing
|
||||
///
|
||||
@@ -138,6 +141,7 @@ impl Violation for UnusedPrivateTypeAlias {
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import typing
|
||||
///
|
||||
@@ -146,8 +150,7 @@ impl Violation for UnusedPrivateTypeAlias {
|
||||
/// foo: set[str]
|
||||
///
|
||||
///
|
||||
/// def func(arg: _UsedPrivateTypedDict) -> _UsedPrivateTypedDict:
|
||||
/// ...
|
||||
/// def func(arg: _UsedPrivateTypedDict) -> _UsedPrivateTypedDict: ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct UnusedPrivateTypedDict {
|
||||
|
||||
@@ -38,23 +38,23 @@ use super::helpers::{
|
||||
/// the behavior of official pytest projects.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// def my_fixture(): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// def my_fixture(): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
@@ -94,23 +94,23 @@ impl AlwaysFixableViolation for PytestFixtureIncorrectParenthesesStyle {
|
||||
/// fixture configuration.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture("module")
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// def my_fixture(): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture(scope="module")
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// def my_fixture(): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
@@ -135,23 +135,23 @@ impl Violation for PytestFixturePositionalArgs {
|
||||
/// `scope="function"` can be omitted, as it is the default.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture(scope="function")
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// def my_fixture(): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture()
|
||||
/// def my_fixture():
|
||||
/// ...
|
||||
/// def my_fixture(): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
@@ -303,32 +303,30 @@ impl Violation for PytestIncorrectFixtureNameUnderscore {
|
||||
/// and avoid the confusion caused by unused arguments.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture
|
||||
/// def _patch_something():
|
||||
/// ...
|
||||
/// def _patch_something(): ...
|
||||
///
|
||||
///
|
||||
/// def test_foo(_patch_something):
|
||||
/// ...
|
||||
/// def test_foo(_patch_something): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.fixture
|
||||
/// def _patch_something():
|
||||
/// ...
|
||||
/// def _patch_something(): ...
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.usefixtures("_patch_something")
|
||||
/// def test_foo():
|
||||
/// ...
|
||||
/// def test_foo(): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -25,23 +25,23 @@ use super::helpers::get_mark_decorators;
|
||||
/// fixtures is fine, but it's best to be consistent.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.foo
|
||||
/// def test_something():
|
||||
/// ...
|
||||
/// def test_something(): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.foo()
|
||||
/// def test_something():
|
||||
/// ...
|
||||
/// def test_something(): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
@@ -86,19 +86,19 @@ impl AlwaysFixableViolation for PytestIncorrectMarkParenthesesStyle {
|
||||
/// useless and should be removed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.usefixtures()
|
||||
/// def test_something():
|
||||
/// ...
|
||||
/// def test_something(): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def test_something():
|
||||
/// ...
|
||||
/// def test_something(): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -27,41 +27,38 @@ use super::helpers::{is_pytest_parametrize, split_names};
|
||||
/// configured via the [`lint.flake8-pytest-style.parametrize-names-type`] setting.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// # single parameter, always expecting string
|
||||
/// @pytest.mark.parametrize(("param",), [1, 2, 3])
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
/// def test_foo(param): ...
|
||||
///
|
||||
///
|
||||
/// # multiple parameters, expecting tuple
|
||||
/// @pytest.mark.parametrize(["param1", "param2"], [(1, 2), (3, 4)])
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
/// def test_bar(param1, param2): ...
|
||||
///
|
||||
///
|
||||
/// # multiple parameters, expecting tuple
|
||||
/// @pytest.mark.parametrize("param1,param2", [(1, 2), (3, 4)])
|
||||
/// def test_baz(param1, param2):
|
||||
/// ...
|
||||
/// def test_baz(param1, param2): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize("param", [1, 2, 3])
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
/// def test_foo(param): ...
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(("param1", "param2"), [(1, 2), (3, 4)])
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
/// def test_bar(param1, param2): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
@@ -149,14 +146,14 @@ impl Violation for PytestParametrizeNamesWrongType {
|
||||
/// - `list`: `@pytest.mark.parametrize(("key", "value"), [["a", "b"], ["c", "d"]])`
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// # expected list, got tuple
|
||||
/// @pytest.mark.parametrize("param", (1, 2))
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
/// def test_foo(param): ...
|
||||
///
|
||||
///
|
||||
/// # expected top-level list, got tuple
|
||||
@@ -167,8 +164,7 @@ impl Violation for PytestParametrizeNamesWrongType {
|
||||
/// (3, 4),
|
||||
/// ),
|
||||
/// )
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
/// def test_bar(param1, param2): ...
|
||||
///
|
||||
///
|
||||
/// # expected individual rows to be tuples, got lists
|
||||
@@ -179,23 +175,21 @@ impl Violation for PytestParametrizeNamesWrongType {
|
||||
/// [3, 4],
|
||||
/// ],
|
||||
/// )
|
||||
/// def test_baz(param1, param2):
|
||||
/// ...
|
||||
/// def test_baz(param1, param2): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize("param", [1, 2, 3])
|
||||
/// def test_foo(param):
|
||||
/// ...
|
||||
/// def test_foo(param): ...
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(("param1", "param2"), [(1, 2), (3, 4)])
|
||||
/// def test_bar(param1, param2):
|
||||
/// ...
|
||||
/// def test_bar(param1, param2): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
@@ -232,6 +226,7 @@ impl Violation for PytestParametrizeValuesWrongType {
|
||||
/// Duplicate test cases are redundant and should be removed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
@@ -243,11 +238,11 @@ impl Violation for PytestParametrizeValuesWrongType {
|
||||
/// (1, 2),
|
||||
/// ],
|
||||
/// )
|
||||
/// def test_foo(param1, param2):
|
||||
/// ...
|
||||
/// def test_foo(param1, param2): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
@@ -258,8 +253,7 @@ impl Violation for PytestParametrizeValuesWrongType {
|
||||
/// (1, 2),
|
||||
/// ],
|
||||
/// )
|
||||
/// def test_foo(param1, param2):
|
||||
/// ...
|
||||
/// def test_foo(param1, param2): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
|
||||
@@ -9,6 +9,8 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -54,4 +56,22 @@ mod tests {
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_simplify").join(path).as_path(),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::helpers::contains_effect;
|
||||
use ruff_python_ast::{self as ast, BoolOp, ElifElseClause, Expr, Stmt};
|
||||
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
@@ -9,12 +11,15 @@ use crate::fix::edits::fits;
|
||||
|
||||
/// ## What it does
|
||||
/// Check for `if`-`else`-blocks that can be replaced with a ternary operator.
|
||||
/// Moreover, in [preview], check if these ternary expressions can be
|
||||
/// further simplified to binary expressions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `if`-`else`-blocks that assign a value to a variable in both branches can
|
||||
/// be expressed more concisely by using a ternary operator.
|
||||
/// be expressed more concisely by using a ternary or binary operator.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// if foo:
|
||||
/// bar = x
|
||||
@@ -27,11 +32,31 @@ use crate::fix::edits::fits;
|
||||
/// bar = x if foo else y
|
||||
/// ```
|
||||
///
|
||||
/// Or, in [preview]:
|
||||
///
|
||||
/// ```python
|
||||
/// if cond:
|
||||
/// z = cond
|
||||
/// else:
|
||||
/// z = other_cond
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// z = cond or other_cond
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Conditional expressions](https://docs.python.org/3/reference/expressions.html#conditional-expressions)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[violation]
|
||||
pub struct IfElseBlockInsteadOfIfExp {
|
||||
/// The ternary or binary expression to replace the `if`-`else`-block.
|
||||
contents: String,
|
||||
/// Whether to use a binary or ternary assignment.
|
||||
kind: AssignmentKind,
|
||||
}
|
||||
|
||||
impl Violation for IfElseBlockInsteadOfIfExp {
|
||||
@@ -39,12 +64,19 @@ impl Violation for IfElseBlockInsteadOfIfExp {
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let IfElseBlockInsteadOfIfExp { contents } = self;
|
||||
format!("Use ternary operator `{contents}` instead of `if`-`else`-block")
|
||||
let IfElseBlockInsteadOfIfExp { contents, kind } = self;
|
||||
match kind {
|
||||
AssignmentKind::Ternary => {
|
||||
format!("Use ternary operator `{contents}` instead of `if`-`else`-block")
|
||||
}
|
||||
AssignmentKind::Binary => {
|
||||
format!("Use binary operator `{contents}` instead of `if`-`else`-block")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let IfElseBlockInsteadOfIfExp { contents } = self;
|
||||
let IfElseBlockInsteadOfIfExp { contents, .. } = self;
|
||||
Some(format!("Replace `if`-`else`-block with `{contents}`"))
|
||||
}
|
||||
}
|
||||
@@ -121,9 +153,59 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
|
||||
return;
|
||||
}
|
||||
|
||||
let target_var = &body_target;
|
||||
let ternary = ternary(target_var, body_value, test, else_value);
|
||||
let contents = checker.generator().stmt(&ternary);
|
||||
// In most cases we should now suggest a ternary operator,
|
||||
// but there are three edge cases where a binary operator
|
||||
// is more appropriate.
|
||||
//
|
||||
// For the reader's convenience, here is how
|
||||
// the notation translates to the if-else block:
|
||||
//
|
||||
// ```python
|
||||
// if test:
|
||||
// target_var = body_value
|
||||
// else:
|
||||
// target_var = else_value
|
||||
// ```
|
||||
//
|
||||
// The match statement below implements the following
|
||||
// logic:
|
||||
// - If `test == body_value` and preview enabled, replace with `target_var = test or else_value`
|
||||
// - If `test == not body_value` and preview enabled, replace with `target_var = body_value and else_value`
|
||||
// - If `not test == body_value` and preview enabled, replace with `target_var = body_value and else_value`
|
||||
// - Otherwise, replace with `target_var = body_value if test else else_value`
|
||||
let (contents, assignment_kind) =
|
||||
match (checker.settings.preview.is_enabled(), test, body_value) {
|
||||
(true, test_node, body_node)
|
||||
if ComparableExpr::from(test_node) == ComparableExpr::from(body_node)
|
||||
&& !contains_effect(test_node, |id| {
|
||||
checker.semantic().has_builtin_binding(id)
|
||||
}) =>
|
||||
{
|
||||
let target_var = &body_target;
|
||||
let binary = assignment_binary_or(target_var, body_value, else_value);
|
||||
(checker.generator().stmt(&binary), AssignmentKind::Binary)
|
||||
}
|
||||
(true, test_node, body_node)
|
||||
if (test_node.as_unary_op_expr().is_some_and(|op_expr| {
|
||||
op_expr.op.is_not()
|
||||
&& ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node)
|
||||
}) || body_node.as_unary_op_expr().is_some_and(|op_expr| {
|
||||
op_expr.op.is_not()
|
||||
&& ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(test_node)
|
||||
})) && !contains_effect(test_node, |id| {
|
||||
checker.semantic().has_builtin_binding(id)
|
||||
}) =>
|
||||
{
|
||||
let target_var = &body_target;
|
||||
let binary = assignment_binary_and(target_var, body_value, else_value);
|
||||
(checker.generator().stmt(&binary), AssignmentKind::Binary)
|
||||
}
|
||||
_ => {
|
||||
let target_var = &body_target;
|
||||
let ternary = assignment_ternary(target_var, body_value, test, else_value);
|
||||
(checker.generator().stmt(&ternary), AssignmentKind::Ternary)
|
||||
}
|
||||
};
|
||||
|
||||
// Don't flag if the resulting expression would exceed the maximum line length.
|
||||
if !fits(
|
||||
@@ -139,6 +221,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
IfElseBlockInsteadOfIfExp {
|
||||
contents: contents.clone(),
|
||||
kind: assignment_kind,
|
||||
},
|
||||
stmt_if.range(),
|
||||
);
|
||||
@@ -154,7 +237,18 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Expr) -> Stmt {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AssignmentKind {
|
||||
Binary,
|
||||
Ternary,
|
||||
}
|
||||
|
||||
fn assignment_ternary(
|
||||
target_var: &Expr,
|
||||
body_value: &Expr,
|
||||
test: &Expr,
|
||||
orelse_value: &Expr,
|
||||
) -> Stmt {
|
||||
let node = ast::ExprIf {
|
||||
test: Box::new(test.clone()),
|
||||
body: Box::new(body_value.clone()),
|
||||
@@ -168,3 +262,33 @@ fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Exp
|
||||
};
|
||||
node1.into()
|
||||
}
|
||||
|
||||
fn assignment_binary_and(target_var: &Expr, left_value: &Expr, right_value: &Expr) -> Stmt {
|
||||
let node = ast::ExprBoolOp {
|
||||
op: BoolOp::And,
|
||||
values: vec![left_value.clone(), right_value.clone()],
|
||||
range: TextRange::default(),
|
||||
};
|
||||
let node1 = ast::StmtAssign {
|
||||
targets: vec![target_var.clone()],
|
||||
value: Box::new(node.into()),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
node1.into()
|
||||
}
|
||||
|
||||
fn assignment_binary_or(target_var: &Expr, left_value: &Expr, right_value: &Expr) -> Stmt {
|
||||
(ast::StmtAssign {
|
||||
range: TextRange::default(),
|
||||
targets: vec![target_var.clone()],
|
||||
value: Box::new(
|
||||
(ast::ExprBoolOp {
|
||||
range: TextRange::default(),
|
||||
op: BoolOp::Or,
|
||||
values: vec![left_value.clone(), right_value.clone()],
|
||||
})
|
||||
.into(),
|
||||
),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -119,4 +119,186 @@ SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
139 | # SIM108 - should suggest
|
||||
140 | # z = cond or other_cond
|
||||
141 | / if cond:
|
||||
142 | | z = cond
|
||||
143 | | else:
|
||||
144 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
145 |
|
||||
146 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond if cond else other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
138 138 |
|
||||
139 139 | # SIM108 - should suggest
|
||||
140 140 | # z = cond or other_cond
|
||||
141 |-if cond:
|
||||
142 |- z = cond
|
||||
143 |-else:
|
||||
144 |- z = other_cond
|
||||
141 |+z = cond if cond else other_cond
|
||||
145 142 |
|
||||
146 143 | # SIM108 - should suggest
|
||||
147 144 | # z = cond and other_cond
|
||||
|
||||
SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
146 | # SIM108 - should suggest
|
||||
147 | # z = cond and other_cond
|
||||
148 | / if not cond:
|
||||
149 | | z = cond
|
||||
150 | | else:
|
||||
151 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
152 |
|
||||
153 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond if not cond else other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
145 145 |
|
||||
146 146 | # SIM108 - should suggest
|
||||
147 147 | # z = cond and other_cond
|
||||
148 |-if not cond:
|
||||
149 |- z = cond
|
||||
150 |-else:
|
||||
151 |- z = other_cond
|
||||
148 |+z = cond if not cond else other_cond
|
||||
152 149 |
|
||||
153 150 | # SIM108 - should suggest
|
||||
154 151 | # z = not cond and other_cond
|
||||
|
||||
SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
153 | # SIM108 - should suggest
|
||||
154 | # z = not cond and other_cond
|
||||
155 | / if cond:
|
||||
156 | | z = not cond
|
||||
157 | | else:
|
||||
158 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
159 |
|
||||
160 | # SIM108 does not suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not cond if cond else other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
152 152 |
|
||||
153 153 | # SIM108 - should suggest
|
||||
154 154 | # z = not cond and other_cond
|
||||
155 |-if cond:
|
||||
156 |- z = not cond
|
||||
157 |-else:
|
||||
158 |- z = other_cond
|
||||
155 |+z = not cond if cond else other_cond
|
||||
159 156 |
|
||||
160 157 | # SIM108 does not suggest
|
||||
161 158 | # a binary option in these cases,
|
||||
|
||||
SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` instead of `if`-`else`-block
|
||||
|
|
||||
165 | # (Of course, these specific expressions
|
||||
166 | # should be simplified for other reasons...)
|
||||
167 | / if True:
|
||||
168 | | z = 1
|
||||
169 | | else:
|
||||
170 | | z = other
|
||||
| |_____________^ SIM108
|
||||
171 |
|
||||
172 | if False:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if True else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
164 164 | # so, e.g. `True == 1`.
|
||||
165 165 | # (Of course, these specific expressions
|
||||
166 166 | # should be simplified for other reasons...)
|
||||
167 |-if True:
|
||||
168 |- z = 1
|
||||
169 |-else:
|
||||
170 |- z = other
|
||||
167 |+z = 1 if True else other
|
||||
171 168 |
|
||||
172 169 | if False:
|
||||
173 170 | z = 1
|
||||
|
||||
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
||||
|
|
||||
175 | z = other
|
||||
176 |
|
||||
177 | / if 1:
|
||||
178 | | z = True
|
||||
179 | | else:
|
||||
180 | | z = other
|
||||
| |_____________^ SIM108
|
||||
181 |
|
||||
182 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = True if 1 else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
174 174 | else:
|
||||
175 175 | z = other
|
||||
176 176 |
|
||||
177 |-if 1:
|
||||
178 |- z = True
|
||||
179 |-else:
|
||||
180 |- z = other
|
||||
177 |+z = True if 1 else other
|
||||
181 178 |
|
||||
182 179 | # SIM108 does not suggest a binary option in this
|
||||
183 180 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:185:1: SIM108 [*] Use ternary operator `z = foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
183 | # case, since we'd be reducing the number of calls
|
||||
184 | # from Two to one.
|
||||
185 | / if foo():
|
||||
186 | | z = foo()
|
||||
187 | | else:
|
||||
188 | | z = other
|
||||
| |_____________^ SIM108
|
||||
189 |
|
||||
190 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
182 182 | # SIM108 does not suggest a binary option in this
|
||||
183 183 | # case, since we'd be reducing the number of calls
|
||||
184 184 | # from Two to one.
|
||||
185 |-if foo():
|
||||
186 |- z = foo()
|
||||
187 |-else:
|
||||
188 |- z = other
|
||||
185 |+z = foo() if foo() else other
|
||||
189 186 |
|
||||
190 187 | # SIM108 does not suggest a binary option in this
|
||||
191 188 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:193:1: SIM108 [*] Use ternary operator `z = not foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
191 | # case, since we'd be reducing the number of calls
|
||||
192 | # from Two to one.
|
||||
193 | / if foo():
|
||||
194 | | z = not foo()
|
||||
195 | | else:
|
||||
196 | | z = other
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
190 190 | # SIM108 does not suggest a binary option in this
|
||||
191 191 | # case, since we'd be reducing the number of calls
|
||||
192 192 | # from Two to one.
|
||||
193 |-if foo():
|
||||
194 |- z = not foo()
|
||||
195 |-else:
|
||||
196 |- z = other
|
||||
193 |+z = not foo() if foo() else other
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM108.py:2:1: SIM108 [*] Use ternary operator `b = c if a else d` instead of `if`-`else`-block
|
||||
|
|
||||
1 | # SIM108
|
||||
2 | / if a:
|
||||
3 | | b = c
|
||||
4 | | else:
|
||||
5 | | b = d
|
||||
| |_________^ SIM108
|
||||
6 |
|
||||
7 | # OK
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = c if a else d`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 1 | # SIM108
|
||||
2 |-if a:
|
||||
3 |- b = c
|
||||
4 |-else:
|
||||
5 |- b = d
|
||||
2 |+b = c if a else d
|
||||
6 3 |
|
||||
7 4 | # OK
|
||||
8 5 | b = c if a else d
|
||||
|
||||
SIM108.py:30:5: SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block
|
||||
|
|
||||
28 | pass
|
||||
29 | else:
|
||||
30 | if a:
|
||||
| _____^
|
||||
31 | | b = 1
|
||||
32 | | else:
|
||||
33 | | b = 2
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = 1 if a else 2`
|
||||
|
||||
ℹ Unsafe fix
|
||||
27 27 | if True:
|
||||
28 28 | pass
|
||||
29 29 | else:
|
||||
30 |- if a:
|
||||
31 |- b = 1
|
||||
32 |- else:
|
||||
33 |- b = 2
|
||||
30 |+ b = 1 if a else 2
|
||||
34 31 |
|
||||
35 32 |
|
||||
36 33 | import sys
|
||||
|
||||
SIM108.py:58:1: SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-block
|
||||
|
|
||||
57 | # SIM108 (without fix due to comments)
|
||||
58 | / if x > 0:
|
||||
59 | | # test test
|
||||
60 | | abc = x
|
||||
61 | | else:
|
||||
62 | | # test test test
|
||||
63 | | abc = -x
|
||||
| |____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `abc = x if x > 0 else -x`
|
||||
|
||||
SIM108.py:82:1: SIM108 [*] Use ternary operator `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` instead of `if`-`else`-block
|
||||
|
|
||||
81 | # SIM108
|
||||
82 | / if a:
|
||||
83 | | b = "cccccccccccccccccccccccccccccccccß"
|
||||
84 | | else:
|
||||
85 | | b = "ddddddddddddddddddddddddddddddddd💣"
|
||||
| |_____________________________________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"`
|
||||
|
||||
ℹ Unsafe fix
|
||||
79 79 |
|
||||
80 80 |
|
||||
81 81 | # SIM108
|
||||
82 |-if a:
|
||||
83 |- b = "cccccccccccccccccccccccccccccccccß"
|
||||
84 |-else:
|
||||
85 |- b = "ddddddddddddddddddddddddddddddddd💣"
|
||||
82 |+b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"
|
||||
86 83 |
|
||||
87 84 |
|
||||
88 85 | # OK (too long)
|
||||
|
||||
SIM108.py:105:1: SIM108 Use ternary operator `exitcode = 0 if True else 1` instead of `if`-`else`-block
|
||||
|
|
||||
104 | # SIM108 (without fix due to trailing comment)
|
||||
105 | / if True:
|
||||
106 | | exitcode = 0
|
||||
107 | | else:
|
||||
108 | | exitcode = 1 # Trailing comment
|
||||
| |________________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `exitcode = 0 if True else 1`
|
||||
|
||||
SIM108.py:112:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
||||
|
|
||||
111 | # SIM108
|
||||
112 | / if True: x = 3 # Foo
|
||||
113 | | else: x = 5
|
||||
| |___________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
||||
|
|
||||
116 | # SIM108
|
||||
117 | / if True: # Foo
|
||||
118 | | x = 3
|
||||
119 | | else:
|
||||
120 | | x = 5
|
||||
| |_________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
|
||||
|
||||
SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
139 | # SIM108 - should suggest
|
||||
140 | # z = cond or other_cond
|
||||
141 | / if cond:
|
||||
142 | | z = cond
|
||||
143 | | else:
|
||||
144 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
145 |
|
||||
146 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond or other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
138 138 |
|
||||
139 139 | # SIM108 - should suggest
|
||||
140 140 | # z = cond or other_cond
|
||||
141 |-if cond:
|
||||
142 |- z = cond
|
||||
143 |-else:
|
||||
144 |- z = other_cond
|
||||
141 |+z = cond or other_cond
|
||||
145 142 |
|
||||
146 143 | # SIM108 - should suggest
|
||||
147 144 | # z = cond and other_cond
|
||||
|
||||
SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
146 | # SIM108 - should suggest
|
||||
147 | # z = cond and other_cond
|
||||
148 | / if not cond:
|
||||
149 | | z = cond
|
||||
150 | | else:
|
||||
151 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
152 |
|
||||
153 | # SIM108 - should suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
145 145 |
|
||||
146 146 | # SIM108 - should suggest
|
||||
147 147 | # z = cond and other_cond
|
||||
148 |-if not cond:
|
||||
149 |- z = cond
|
||||
150 |-else:
|
||||
151 |- z = other_cond
|
||||
148 |+z = cond and other_cond
|
||||
152 149 |
|
||||
153 150 | # SIM108 - should suggest
|
||||
154 151 | # z = not cond and other_cond
|
||||
|
||||
SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block
|
||||
|
|
||||
153 | # SIM108 - should suggest
|
||||
154 | # z = not cond and other_cond
|
||||
155 | / if cond:
|
||||
156 | | z = not cond
|
||||
157 | | else:
|
||||
158 | | z = other_cond
|
||||
| |__________________^ SIM108
|
||||
159 |
|
||||
160 | # SIM108 does not suggest
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not cond and other_cond`
|
||||
|
||||
ℹ Unsafe fix
|
||||
152 152 |
|
||||
153 153 | # SIM108 - should suggest
|
||||
154 154 | # z = not cond and other_cond
|
||||
155 |-if cond:
|
||||
156 |- z = not cond
|
||||
157 |-else:
|
||||
158 |- z = other_cond
|
||||
155 |+z = not cond and other_cond
|
||||
159 156 |
|
||||
160 157 | # SIM108 does not suggest
|
||||
161 158 | # a binary option in these cases,
|
||||
|
||||
SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` instead of `if`-`else`-block
|
||||
|
|
||||
165 | # (Of course, these specific expressions
|
||||
166 | # should be simplified for other reasons...)
|
||||
167 | / if True:
|
||||
168 | | z = 1
|
||||
169 | | else:
|
||||
170 | | z = other
|
||||
| |_____________^ SIM108
|
||||
171 |
|
||||
172 | if False:
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = 1 if True else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
164 164 | # so, e.g. `True == 1`.
|
||||
165 165 | # (Of course, these specific expressions
|
||||
166 166 | # should be simplified for other reasons...)
|
||||
167 |-if True:
|
||||
168 |- z = 1
|
||||
169 |-else:
|
||||
170 |- z = other
|
||||
167 |+z = 1 if True else other
|
||||
171 168 |
|
||||
172 169 | if False:
|
||||
173 170 | z = 1
|
||||
|
||||
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
||||
|
|
||||
175 | z = other
|
||||
176 |
|
||||
177 | / if 1:
|
||||
178 | | z = True
|
||||
179 | | else:
|
||||
180 | | z = other
|
||||
| |_____________^ SIM108
|
||||
181 |
|
||||
182 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = True if 1 else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
174 174 | else:
|
||||
175 175 | z = other
|
||||
176 176 |
|
||||
177 |-if 1:
|
||||
178 |- z = True
|
||||
179 |-else:
|
||||
180 |- z = other
|
||||
177 |+z = True if 1 else other
|
||||
181 178 |
|
||||
182 179 | # SIM108 does not suggest a binary option in this
|
||||
183 180 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:185:1: SIM108 [*] Use ternary operator `z = foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
183 | # case, since we'd be reducing the number of calls
|
||||
184 | # from Two to one.
|
||||
185 | / if foo():
|
||||
186 | | z = foo()
|
||||
187 | | else:
|
||||
188 | | z = other
|
||||
| |_____________^ SIM108
|
||||
189 |
|
||||
190 | # SIM108 does not suggest a binary option in this
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
182 182 | # SIM108 does not suggest a binary option in this
|
||||
183 183 | # case, since we'd be reducing the number of calls
|
||||
184 184 | # from Two to one.
|
||||
185 |-if foo():
|
||||
186 |- z = foo()
|
||||
187 |-else:
|
||||
188 |- z = other
|
||||
185 |+z = foo() if foo() else other
|
||||
189 186 |
|
||||
190 187 | # SIM108 does not suggest a binary option in this
|
||||
191 188 | # case, since we'd be reducing the number of calls
|
||||
|
||||
SIM108.py:193:1: SIM108 [*] Use ternary operator `z = not foo() if foo() else other` instead of `if`-`else`-block
|
||||
|
|
||||
191 | # case, since we'd be reducing the number of calls
|
||||
192 | # from Two to one.
|
||||
193 | / if foo():
|
||||
194 | | z = not foo()
|
||||
195 | | else:
|
||||
196 | | z = other
|
||||
| |_____________^ SIM108
|
||||
|
|
||||
= help: Replace `if`-`else`-block with `z = not foo() if foo() else other`
|
||||
|
||||
ℹ Unsafe fix
|
||||
190 190 | # SIM108 does not suggest a binary option in this
|
||||
191 191 | # case, since we'd be reducing the number of calls
|
||||
192 192 | # from Two to one.
|
||||
193 |-if foo():
|
||||
194 |- z = not foo()
|
||||
195 |-else:
|
||||
196 |- z = other
|
||||
193 |+z = not foo() if foo() else other
|
||||
@@ -78,7 +78,7 @@ fn runtime_required_base_class(
|
||||
base_classes: &[String],
|
||||
semantic: &SemanticModel,
|
||||
) -> bool {
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
base_classes
|
||||
.iter()
|
||||
.any(|base_class| QualifiedName::from_dotted_name(base_class) == qualified_name)
|
||||
|
||||
@@ -91,7 +91,7 @@ pub(super) fn is_typed_dict_class(class_def: &ast::StmtClassDef, semantic: &Sema
|
||||
return false;
|
||||
}
|
||||
|
||||
analyze::class::any_qualified_name(class_def, semantic, &|qualified_name| {
|
||||
analyze::class::any_qualified_base_class(class_def, semantic, &|qualified_name| {
|
||||
semantic.match_typing_qualified_name(&qualified_name, "TypedDict")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// > exception names (if the exception actually is an error).
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Validation(Exception):
|
||||
/// ...
|
||||
/// class Validation(Exception): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class ValidationError(Exception):
|
||||
/// ...
|
||||
/// class ValidationError(Exception): ...
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#exception-names
|
||||
|
||||
@@ -34,17 +34,17 @@ use crate::renamer::Renamer;
|
||||
/// the [`lint.pep8-naming.extend-ignore-names`] option to `["this"]`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Example:
|
||||
/// def function(cls, data):
|
||||
/// ...
|
||||
/// def function(cls, data): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Example:
|
||||
/// def function(self, data):
|
||||
/// ...
|
||||
/// def function(self, data): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
@@ -98,19 +98,19 @@ impl Violation for InvalidFirstArgumentNameForMethod {
|
||||
/// the [`lint.pep8-naming.extend-ignore-names`] option to `["klass"]`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Example:
|
||||
/// @classmethod
|
||||
/// def function(self, data):
|
||||
/// ...
|
||||
/// def function(self, data): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Example:
|
||||
/// @classmethod
|
||||
/// def function(cls, data):
|
||||
/// ...
|
||||
/// def function(cls, data): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
|
||||
@@ -286,3 +286,6 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
||||
125 |- hºusehold(1)
|
||||
124 |+ def formula(self):
|
||||
125 |+ self(1)
|
||||
126 126 |
|
||||
127 127 |
|
||||
128 128 | from typing import Protocol
|
||||
|
||||
@@ -229,3 +229,6 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
||||
125 |- hºusehold(1)
|
||||
124 |+ def formula(self):
|
||||
125 |+ self(1)
|
||||
126 126 |
|
||||
127 127 |
|
||||
128 128 | from typing import Protocol
|
||||
|
||||
@@ -267,3 +267,6 @@ N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
||||
125 |- hºusehold(1)
|
||||
124 |+ def formula(self):
|
||||
125 |+ self(1)
|
||||
126 126 |
|
||||
127 127 |
|
||||
128 128 | from typing import Protocol
|
||||
|
||||
@@ -14,15 +14,15 @@ use crate::rules::pycodestyle::helpers::is_ambiguous_name;
|
||||
/// numerals one and zero. When tempted to use 'l', use 'L' instead.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class I(object):
|
||||
/// ...
|
||||
/// class I(object): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Integer(object):
|
||||
/// ...
|
||||
/// class Integer(object): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct AmbiguousClassName(pub String);
|
||||
|
||||
@@ -14,15 +14,15 @@ use crate::rules::pycodestyle::helpers::is_ambiguous_name;
|
||||
/// numerals one and zero. When tempted to use 'l', use 'L' instead.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def l(x):
|
||||
/// ...
|
||||
/// def l(x): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// def long_name(x):
|
||||
/// ...
|
||||
/// def long_name(x): ...
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct AmbiguousFunctionName(pub String);
|
||||
|
||||
@@ -18,6 +18,22 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// If you want to check for an exact type match, use `is` or `is not`.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// When using libraries that override the `==` (`__eq__`) operator (such as NumPy,
|
||||
/// Pandas, and SQLAlchemy), this rule may produce false positives, as converting
|
||||
/// from `==` to `is` or `is not` will change the behavior of the code.
|
||||
///
|
||||
/// For example, the following operations are _not_ equivalent:
|
||||
/// ```python
|
||||
/// import numpy as np
|
||||
///
|
||||
/// np.array([True, False]) == False
|
||||
/// # array([False, True])
|
||||
///
|
||||
/// np.array([True, False]) is False
|
||||
/// # False
|
||||
/// ```
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// if type(obj) == type(1):
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_python_ast::helpers::map_callable;
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, visitor, Expr, Stmt};
|
||||
use ruff_python_semantic::analyze::function_type;
|
||||
use ruff_python_semantic::analyze::{function_type, visibility};
|
||||
use ruff_python_semantic::{Definition, SemanticModel};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
@@ -25,6 +25,8 @@ use crate::rules::pydocstyle::settings::Convention;
|
||||
/// Docstrings missing return sections are a sign of incomplete documentation
|
||||
/// or refactors.
|
||||
///
|
||||
/// This rule is not enforced for abstract methods and stubs functions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def calculate_speed(distance: float, time: float) -> float:
|
||||
@@ -73,6 +75,8 @@ impl Violation for DocstringMissingReturns {
|
||||
/// Functions without an explicit return should not have a returns section
|
||||
/// in their docstrings.
|
||||
///
|
||||
/// This rule is not enforced for stub functions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def say_hello(n: int) -> None:
|
||||
@@ -121,6 +125,8 @@ impl Violation for DocstringExtraneousReturns {
|
||||
/// Docstrings missing yields sections are a sign of incomplete documentation
|
||||
/// or refactors.
|
||||
///
|
||||
/// This rule is not enforced for abstract methods and stubs functions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def count_to_n(n: int) -> int:
|
||||
@@ -169,6 +175,8 @@ impl Violation for DocstringMissingYields {
|
||||
/// Functions which don't yield anything should not have a yields section
|
||||
/// in their docstrings.
|
||||
///
|
||||
/// This rule is not enforced for stub functions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def say_hello(n: int) -> None:
|
||||
@@ -218,6 +226,8 @@ impl Violation for DocstringExtraneousYields {
|
||||
/// it can be misleading to users and/or a sign of incomplete documentation or
|
||||
/// refactors.
|
||||
///
|
||||
/// This rule is not enforced for abstract methods and stubs functions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def calculate_speed(distance: float, time: float) -> float:
|
||||
@@ -282,6 +292,8 @@ impl Violation for DocstringMissingException {
|
||||
/// Some conventions prefer non-explicit exceptions be omitted from the
|
||||
/// docstring.
|
||||
///
|
||||
/// This rule is not enforced for stub functions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def calculate_speed(distance: float, time: float) -> float:
|
||||
@@ -343,7 +355,7 @@ impl Violation for DocstringExtraneousException {
|
||||
}
|
||||
}
|
||||
|
||||
// A generic docstring section.
|
||||
/// A generic docstring section.
|
||||
#[derive(Debug)]
|
||||
struct GenericSection {
|
||||
range: TextRange,
|
||||
@@ -363,7 +375,7 @@ impl GenericSection {
|
||||
}
|
||||
}
|
||||
|
||||
// A Raises docstring section.
|
||||
/// A "Raises" section in a docstring.
|
||||
#[derive(Debug)]
|
||||
struct RaisesSection<'a> {
|
||||
raised_exceptions: Vec<QualifiedName<'a>>,
|
||||
@@ -378,7 +390,7 @@ impl Ranged for RaisesSection<'_> {
|
||||
|
||||
impl<'a> RaisesSection<'a> {
|
||||
/// Return the raised exceptions for the docstring, or `None` if the docstring does not contain
|
||||
/// a `Raises` section.
|
||||
/// a "Raises" section.
|
||||
fn from_section(section: &SectionContext<'a>, style: Option<SectionStyle>) -> Self {
|
||||
Self {
|
||||
raised_exceptions: parse_entries(section.following_lines_str(), style),
|
||||
@@ -415,7 +427,7 @@ impl<'a> DocstringSections<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the entries in a `Raises` section of a docstring.
|
||||
/// Parse the entries in a "Raises" section of a docstring.
|
||||
///
|
||||
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
|
||||
/// entries are found.
|
||||
@@ -732,17 +744,6 @@ pub(crate) fn check_docstring(
|
||||
}
|
||||
}
|
||||
|
||||
// DOC202
|
||||
if checker.enabled(Rule::DocstringExtraneousReturns) {
|
||||
if let Some(ref docstring_returns) = docstring_sections.returns {
|
||||
if body_entries.returns.is_empty() {
|
||||
let diagnostic =
|
||||
Diagnostic::new(DocstringExtraneousReturns, docstring_returns.range());
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOC402
|
||||
if checker.enabled(Rule::DocstringMissingYields) {
|
||||
if !yields_documented(docstring, &docstring_sections, convention) {
|
||||
@@ -753,17 +754,6 @@ pub(crate) fn check_docstring(
|
||||
}
|
||||
}
|
||||
|
||||
// DOC403
|
||||
if checker.enabled(Rule::DocstringExtraneousYields) {
|
||||
if let Some(docstring_yields) = docstring_sections.yields {
|
||||
if body_entries.yields.is_empty() {
|
||||
let diagnostic =
|
||||
Diagnostic::new(DocstringExtraneousYields, docstring_yields.range());
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOC501
|
||||
if checker.enabled(Rule::DocstringMissingException) {
|
||||
for body_raise in &body_entries.raised_exceptions {
|
||||
@@ -794,28 +784,54 @@ pub(crate) fn check_docstring(
|
||||
}
|
||||
}
|
||||
|
||||
// DOC502
|
||||
if checker.enabled(Rule::DocstringExtraneousException) {
|
||||
if let Some(docstring_raises) = docstring_sections.raises {
|
||||
let mut extraneous_exceptions = Vec::new();
|
||||
for docstring_raise in &docstring_raises.raised_exceptions {
|
||||
if !body_entries.raised_exceptions.iter().any(|exception| {
|
||||
exception
|
||||
.qualified_name
|
||||
.segments()
|
||||
.ends_with(docstring_raise.segments())
|
||||
}) {
|
||||
extraneous_exceptions.push(docstring_raise.to_string());
|
||||
// Avoid applying "extraneous" rules to abstract methods. An abstract method's docstring _could_
|
||||
// document that it raises an exception without including the exception in the implementation.
|
||||
if !visibility::is_abstract(&function_def.decorator_list, checker.semantic()) {
|
||||
// DOC202
|
||||
if checker.enabled(Rule::DocstringExtraneousReturns) {
|
||||
if let Some(ref docstring_returns) = docstring_sections.returns {
|
||||
if body_entries.returns.is_empty() {
|
||||
let diagnostic =
|
||||
Diagnostic::new(DocstringExtraneousReturns, docstring_returns.range());
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if !extraneous_exceptions.is_empty() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
DocstringExtraneousException {
|
||||
ids: extraneous_exceptions,
|
||||
},
|
||||
docstring_raises.range(),
|
||||
);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
// DOC403
|
||||
if checker.enabled(Rule::DocstringExtraneousYields) {
|
||||
if let Some(docstring_yields) = docstring_sections.yields {
|
||||
if body_entries.yields.is_empty() {
|
||||
let diagnostic =
|
||||
Diagnostic::new(DocstringExtraneousYields, docstring_yields.range());
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOC502
|
||||
if checker.enabled(Rule::DocstringExtraneousException) {
|
||||
if let Some(docstring_raises) = docstring_sections.raises {
|
||||
let mut extraneous_exceptions = Vec::new();
|
||||
for docstring_raise in &docstring_raises.raised_exceptions {
|
||||
if !body_entries.raised_exceptions.iter().any(|exception| {
|
||||
exception
|
||||
.qualified_name
|
||||
.segments()
|
||||
.ends_with(docstring_raise.segments())
|
||||
}) {
|
||||
extraneous_exceptions.push(docstring_raise.to_string());
|
||||
}
|
||||
}
|
||||
if !extraneous_exceptions.is_empty() {
|
||||
let diagnostic = Diagnostic::new(
|
||||
DocstringExtraneousException {
|
||||
ids: extraneous_exceptions,
|
||||
},
|
||||
docstring_raises.range(),
|
||||
);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,3 +29,12 @@ DOC201_google.py:71:9: DOC201 `return` is not documented in docstring
|
||||
73 | print("I never return")
|
||||
|
|
||||
= help: Add a "Returns" section to the docstring
|
||||
|
||||
DOC201_google.py:121:9: DOC201 `return` is not documented in docstring
|
||||
|
|
||||
119 | def f(self):
|
||||
120 | """Lorem ipsum."""
|
||||
121 | return True
|
||||
| ^^^^^^^^^^^ DOC201
|
||||
|
|
||||
= help: Add a "Returns" section to the docstring
|
||||
|
||||
@@ -18,3 +18,12 @@ DOC201_numpy.py:62:9: DOC201 `return` is not documented in docstring
|
||||
| ^^^^^^^^^^^^^ DOC201
|
||||
|
|
||||
= help: Add a "Returns" section to the docstring
|
||||
|
||||
DOC201_numpy.py:87:9: DOC201 `return` is not documented in docstring
|
||||
|
|
||||
85 | def f(self):
|
||||
86 | """Lorem ipsum."""
|
||||
87 | return True
|
||||
| ^^^^^^^^^^^ DOC201
|
||||
|
|
||||
= help: Add a "Returns" section to the docstring
|
||||
|
||||
@@ -24,15 +24,16 @@ use crate::registry::Rule;
|
||||
/// For an alternative, see [D211].
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class PhotoMetadata:
|
||||
/// """Metadata about a photo."""
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class PhotoMetadata:
|
||||
///
|
||||
/// """Metadata about a photo."""
|
||||
/// ```
|
||||
///
|
||||
@@ -121,13 +122,14 @@ impl AlwaysFixableViolation for OneBlankLineAfterClass {
|
||||
/// For an alternative, see [D203].
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class PhotoMetadata:
|
||||
///
|
||||
/// """Metadata about a photo."""
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class PhotoMetadata:
|
||||
/// """Metadata about a photo."""
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::docstrings::Docstring;
|
||||
/// the implementation.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import overload
|
||||
///
|
||||
@@ -42,18 +43,17 @@ use crate::docstrings::Docstring;
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import overload
|
||||
///
|
||||
///
|
||||
/// @overload
|
||||
/// def factorial(n: int) -> int:
|
||||
/// ...
|
||||
/// def factorial(n: int) -> int: ...
|
||||
///
|
||||
///
|
||||
/// @overload
|
||||
/// def factorial(n: float) -> float:
|
||||
/// ...
|
||||
/// def factorial(n: float) -> float: ...
|
||||
///
|
||||
///
|
||||
/// def factorial(n):
|
||||
|
||||
@@ -28,16 +28,16 @@ use crate::registry::Rule;
|
||||
/// that format for consistency.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class FasterThanLightError(ZeroDivisionError):
|
||||
/// ...
|
||||
/// class FasterThanLightError(ZeroDivisionError): ...
|
||||
///
|
||||
///
|
||||
/// def calculate_speed(distance: float, time: float) -> float:
|
||||
/// ...
|
||||
/// def calculate_speed(distance: float, time: float) -> float: ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// """Utility functions and classes for calculating speed.
|
||||
///
|
||||
@@ -47,12 +47,10 @@ use crate::registry::Rule;
|
||||
/// """
|
||||
///
|
||||
///
|
||||
/// class FasterThanLightError(ZeroDivisionError):
|
||||
/// ...
|
||||
/// class FasterThanLightError(ZeroDivisionError): ...
|
||||
///
|
||||
///
|
||||
/// def calculate_speed(distance: float, time: float) -> float:
|
||||
/// ...
|
||||
/// def calculate_speed(distance: float, time: float) -> float: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
@@ -430,12 +428,12 @@ impl Violation for UndocumentedMagicMethod {
|
||||
/// that format for consistency.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// """Class Foo."""
|
||||
///
|
||||
/// class Bar:
|
||||
/// ...
|
||||
/// class Bar: ...
|
||||
///
|
||||
///
|
||||
/// bar = Foo.Bar()
|
||||
@@ -443,6 +441,7 @@ impl Violation for UndocumentedMagicMethod {
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// """Class Foo."""
|
||||
|
||||
@@ -15,9 +15,9 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// will instead raise an error when type checking is performed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// def foo() -> "/":
|
||||
/// ...
|
||||
/// def foo() -> "/": ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user