Compare commits

..

19 Commits

Author SHA1 Message Date
Charlie Marsh
c23bf395e2 Warn on invalid # noqa rule codes 2024-08-11 19:23:33 -04:00
Charlie Marsh
383676e332 Avoid parsing joint rule codes as distinct codes in # noqa 2024-08-11 19:23:00 -04:00
Yury Fedotov
feba5031dc [Minor typo] Fix article in "an fix" (#12797) 2024-08-10 21:22:00 -04:00
Dylan
0c2b88f224 [flake8-simplify] Further simplify to binary in preview for if-else-block-instead-of-if-exp (SIM108) (#12796)
In most cases we should suggest a ternary operator, but there are three
edge cases where a binary operator is more appropriate.

Given an if-else block of the form

```python
if test:
    target_var = body_value
else:
    target_var = else_value
```
This PR updates the check for SIM108 to the following:

- If `test == body_value` and preview enabled, suggest to replace with
`target_var = test or else_value`
- If `test == not body_value` and preview enabled, suggest to replace
with `target_var = body_value and else_value`
- If `not test == body_value` and preview enabled, suggest to replace
with `target_var = body_value and else_value`
- Otherwise, suggest to replace with `target_var = body_value if test
else else_value`

Closes #12189.
2024-08-10 16:49:25 +00:00
Alex Waygood
cf1a57df5a Remove red_knot_python_semantic::python_version::TargetVersion (#12790) 2024-08-10 14:28:31 +01:00
renovate[bot]
597c5f9124 Update dependency black to v24 (#12728) 2024-08-10 18:04:37 +05:30
Charlie Marsh
69e1c567d4 Treat type(Protocol) et al as metaclass base (#12770)
## Summary

Closes https://github.com/astral-sh/ruff/issues/12736.
2024-08-09 20:10:12 +00:00
Alex Waygood
37b9bac403 [red-knot] Add support for --system-site-packages virtual environments (#12759) 2024-08-09 21:02:16 +01:00
Alex Waygood
83db48d316 RUF031: Ignore unparenthesized tuples in subscripts when the subscript is obviously a type annotation or type alias (#12762) 2024-08-09 20:31:27 +01:00
Alex Waygood
c4e651921b [red-knot] Move, rename and make public the PyVersion type (#12782) 2024-08-09 16:49:17 +01:00
Dylan
b595346213 [ruff] Do not remove parens for tuples with starred expressions in Python <=3.10 RUF031 (#12784) 2024-08-09 17:30:29 +02:00
Ryan Hoban
253474b312 Document that BLE001 supports both BaseException and Exception (#12788) 2024-08-09 17:28:50 +02:00
Micha Reiser
a176679b24 Log warnings when skipping editable installations (#12779) 2024-08-09 16:29:43 +02:00
Charlie Marsh
1f51048fa4 Don't enforce returns and yields in abstract methods (#12771)
## Summary

Closes https://github.com/astral-sh/ruff/issues/12685.
2024-08-09 13:34:14 +00:00
Micha Reiser
2abfab0f9b Move Program and related structs to red_knot_python_semantic (#12777) 2024-08-09 11:50:45 +02:00
Dylan
64f1f3468d [ruff] Skip tuples with slice expressions in incorrectly-parenthesized-tuple-in-subscript (RUF031) (#12768)
## Summary

Adding parentheses to a tuple in a subscript with elements that include
slice expressions causes a syntax error. For example, `d[(1,2,:)]` is a
syntax error.

So, when `lint.ruff.parenthesize-tuple-in-subscript = true` and the
tuple includes a slice expression, we skip this check and fix.

Closes #12766.
2024-08-09 09:22:58 +00:00
Micha Reiser
ffaa35eafe Add test helper to setup tracing (#12741) 2024-08-09 07:04:04 +00:00
Charlie Marsh
c906b0183b Add known problems warning to type-comparison rule (#12769)
## Summary

See: https://github.com/astral-sh/ruff/issues/4560
2024-08-09 01:41:15 +00:00
Carl Meyer
bc5b9b81dd [red-knot] add dev dependency on ruff_db os feature from red_knot_pyt… (#12760) 2024-08-08 18:10:30 +01:00
126 changed files with 2785 additions and 866 deletions

5
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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.

View File

@@ -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();

View File

@@ -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,
}
}
}

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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;

View File

@@ -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");

View File

@@ -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"),

View File

@@ -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(),

View File

@@ -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![],
}

View File

@@ -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
);
}

View File

@@ -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 {

View 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}")
}
}

View File

@@ -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"),

View File

@@ -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"),

View File

@@ -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 }

View File

@@ -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(),

View File

@@ -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 }

View File

@@ -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,
}
}
}

View File

@@ -1 +0,0 @@
Signature: 8a477f597d28d172789f06886806bc55

View File

@@ -1 +0,0 @@
/Users/alexw/.pyenv/versions/3.12.4/bin/python3.12

View File

@@ -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())

View File

@@ -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

View File

@@ -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]

View File

@@ -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,

View File

@@ -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
));
}
}

View File

@@ -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);

View File

@@ -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]

View File

@@ -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(),

View File

@@ -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"]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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): ...

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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![];

View File

@@ -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]";

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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
///

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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;

View File

@@ -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
///

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"]

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(())
}
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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")
})
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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):

View File

@@ -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);
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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."""

View File

@@ -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