Skip to content

Commit

Permalink
[pylint] Implement singledispatch-method (E1519) (astral-sh#10140)
Browse files Browse the repository at this point in the history
Implementing the rule 

https://pylint.readthedocs.io/en/stable/user_guide/messages/error/singledispatch-method.html#singledispatch-method-e1519

Implementation simply checks the function type and name of the
decorators.
  • Loading branch information
Glyphack authored and nkxxll committed Mar 4, 2024
1 parent 29d381d commit ea8836a
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from functools import singledispatch, singledispatchmethod


@singledispatch
def convert_position(position):
pass


class Board:
@singledispatch # [singledispatch-method]
@classmethod
def convert_position(cls, position):
pass

@singledispatch # [singledispatch-method]
def move(self, position):
pass

@singledispatchmethod
def place(self, position):
pass

@singledispatch
@staticmethod
def do(position):
pass

# False negative (flagged by Pylint).
@convert_position.register
@classmethod
def _(cls, position: str) -> tuple:
position_a, position_b = position.split(",")
return (int(position_a), int(position_b))

# False negative (flagged by Pylint).
@convert_position.register
@classmethod
def _(cls, position: tuple) -> str:
return f"{position[0]},{position[1]}"
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::SingledispatchMethod,
]) {
return;
}
Expand Down Expand Up @@ -389,6 +390,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if checker.enabled(Rule::TooManyLocals) {
pylint::rules::too_many_locals(checker, scope, &mut diagnostics);
}

if checker.enabled(Rule::SingledispatchMethod) {
pylint::rules::singledispatch_method(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E1307") => (RuleGroup::Stable, rules::pylint::rules::BadStringFormatType),
(Pylint, "E1310") => (RuleGroup::Stable, rules::pylint::rules::BadStrStripCall),
(Pylint, "E1507") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarValue),
(Pylint, "E1519") => (RuleGroup::Preview, rules::pylint::rules::SingledispatchMethod),
(Pylint, "E1700") => (RuleGroup::Stable, rules::pylint::rules::YieldFromInAsyncFunction),
(Pylint, "E2502") => (RuleGroup::Stable, rules::pylint::rules::BidirectionalUnicode),
(Pylint, "E2510") => (RuleGroup::Stable, rules::pylint::rules::InvalidCharacterBackspace),
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &Semanti
/// ```python
/// from functools import singledispatch
///
///
/// @singledispatch
/// def fun(arg, verbose=False):
/// ...
Expand All @@ -167,6 +168,7 @@ pub(crate) fn is_singledispatch_interface(
/// ```python
/// from functools import singledispatch
///
///
/// @singledispatch
/// def fun(arg, verbose=False):
/// ...
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pylint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;

#[test_case(Rule::SingledispatchMethod, Path::new("singledispatch_method.py"))]
#[test_case(Rule::AssertOnStringLiteral, Path::new("assert_on_string_literal.py"))]
#[test_case(Rule::AwaitOutsideAsync, Path::new("await_outside_async.py"))]
#[test_case(Rule::BadOpenMode, Path::new("bad_open_mode.py"))]
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pylint/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub(crate) use repeated_keyword_argument::*;
pub(crate) use return_in_init::*;
pub(crate) use self_assigning_variable::*;
pub(crate) use single_string_slots::*;
pub(crate) use singledispatch_method::*;
pub(crate) use subprocess_popen_preexec_fn::*;
pub(crate) use subprocess_run_without_check::*;
pub(crate) use super_without_brackets::*;
Expand Down Expand Up @@ -142,6 +143,7 @@ mod repeated_keyword_argument;
mod return_in_init;
mod self_assigning_variable;
mod single_string_slots;
mod singledispatch_method;
mod subprocess_popen_preexec_fn;
mod subprocess_run_without_check;
mod super_without_brackets;
Expand Down
119 changes: 119 additions & 0 deletions crates/ruff_linter/src/rules/pylint/rules/singledispatch_method.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_semantic::analyze::function_type;
use ruff_python_semantic::Scope;
use ruff_text_size::Ranged;

use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;

/// ## What it does
/// Checks for `@singledispatch` decorators on class and instance methods.
///
/// ## Why is this bad?
/// The `@singledispatch` decorator is intended for use with functions, not methods.
///
/// Instead, use the `@singledispatchmethod` decorator, or migrate the method to a
/// standalone function or `@staticmethod`.
///
/// ## Example
/// ```python
/// from functools import singledispatch
///
///
/// class Class:
/// @singledispatch
/// def method(self, arg):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from functools import singledispatchmethod
///
///
/// class Class:
/// @singledispatchmethod
/// def method(self, arg):
/// ...
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as migrating from `@singledispatch` to
/// `@singledispatchmethod` may change the behavior of the code.
#[violation]
pub struct SingledispatchMethod;

impl Violation for SingledispatchMethod {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;

#[derive_message_formats]
fn message(&self) -> String {
format!("`@singledispatch` decorator should not be used on methods")
}

fn fix_title(&self) -> Option<String> {
Some("Replace with `@singledispatchmethod`".to_string())
}
}

/// E1519
pub(crate) fn singledispatch_method(
checker: &Checker,
scope: &Scope,
diagnostics: &mut Vec<Diagnostic>,
) {
let Some(func) = scope.kind.as_function() else {
return;
};

let ast::StmtFunctionDef {
name,
decorator_list,
..
} = func;

let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

let type_ = function_type::classify(
name,
decorator_list,
parent,
checker.semantic(),
&checker.settings.pep8_naming.classmethod_decorators,
&checker.settings.pep8_naming.staticmethod_decorators,
);
if !matches!(
type_,
function_type::FunctionType::Method | function_type::FunctionType::ClassMethod
) {
return;
}

for decorator in decorator_list {
if checker
.semantic()
.resolve_call_path(&decorator.expression)
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["functools", "singledispatch"])
})
{
let mut diagnostic = Diagnostic::new(SingledispatchMethod, decorator.range());
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("functools", "singledispatchmethod"),
decorator.start(),
checker.semantic(),
)?;
Ok(Fix::unsafe_edits(
Edit::range_replacement(binding, decorator.expression.range()),
[import_edit],
))
});
diagnostics.push(diagnostic);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
singledispatch_method.py:10:5: PLE1519 [*] `@singledispatch` decorator should not be used on methods
|
9 | class Board:
10 | @singledispatch # [singledispatch-method]
| ^^^^^^^^^^^^^^^ PLE1519
11 | @classmethod
12 | def convert_position(cls, position):
|
= help: Replace with `@singledispatchmethod`

Unsafe fix
7 7 |
8 8 |
9 9 | class Board:
10 |- @singledispatch # [singledispatch-method]
10 |+ @singledispatchmethod # [singledispatch-method]
11 11 | @classmethod
12 12 | def convert_position(cls, position):
13 13 | pass

singledispatch_method.py:15:5: PLE1519 [*] `@singledispatch` decorator should not be used on methods
|
13 | pass
14 |
15 | @singledispatch # [singledispatch-method]
| ^^^^^^^^^^^^^^^ PLE1519
16 | def move(self, position):
17 | pass
|
= help: Replace with `@singledispatchmethod`

Unsafe fix
12 12 | def convert_position(cls, position):
13 13 | pass
14 14 |
15 |- @singledispatch # [singledispatch-method]
15 |+ @singledispatchmethod # [singledispatch-method]
16 16 | def move(self, position):
17 17 | pass
18 18 |
2 changes: 2 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ea8836a

Please sign in to comment.