Support local and dynamic class- and static-method decorators (#8592)
## Summary This brings ruff's behavior in line with what `pep8-naming` already does and thus closes #8397. I had initially implemented this to look at the last segment of a dotted path only when the entry in the `*-decorators` setting started with a `.`, but in the end I thought it's better to remain consistent w/ `pep8-naming` and doing a match against the last segment of the decorator name in any case. If you prefer to diverge from this in favor of less ambiguity in the configuration let me know and I'll change it so you would need to put e.g. `.expression` in the `classmethod-decorators` list. ## Test Plan Tested against the file in the issue linked below, plus the new testcase added in this PR.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use ruff_python_ast::call_path::collect_call_path;
|
||||
use ruff_python_ast::call_path::from_qualified_name;
|
||||
use ruff_python_ast::helpers::map_callable;
|
||||
use ruff_python_ast::Decorator;
|
||||
@@ -25,20 +26,10 @@ pub fn classify(
|
||||
let ScopeKind::Class(class_def) = &scope.kind else {
|
||||
return FunctionType::Function;
|
||||
};
|
||||
if decorator_list.iter().any(|decorator| {
|
||||
// The method is decorated with a static method decorator (like
|
||||
// `@staticmethod`).
|
||||
semantic
|
||||
.resolve_call_path(map_callable(&decorator.expression))
|
||||
.is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["", "staticmethod"] | ["abc", "abstractstaticmethod"]
|
||||
) || staticmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| call_path == from_qualified_name(decorator))
|
||||
})
|
||||
}) {
|
||||
if decorator_list
|
||||
.iter()
|
||||
.any(|decorator| is_static_method(decorator, semantic, staticmethod_decorators))
|
||||
{
|
||||
FunctionType::StaticMethod
|
||||
} else if matches!(name, "__new__" | "__init_subclass__" | "__class_getitem__")
|
||||
// Special-case class method, like `__new__`.
|
||||
@@ -50,19 +41,7 @@ pub fn classify(
|
||||
matches!(call_path.as_slice(), ["", "type"] | ["abc", "ABCMeta"])
|
||||
})
|
||||
})
|
||||
|| decorator_list.iter().any(|decorator| {
|
||||
// The method is decorated with a class method decorator (like `@classmethod`).
|
||||
semantic
|
||||
.resolve_call_path(map_callable(&decorator.expression))
|
||||
.is_some_and( |call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["", "classmethod"] | ["abc", "abstractclassmethod"]
|
||||
) || classmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| call_path == from_qualified_name(decorator))
|
||||
})
|
||||
})
|
||||
|| decorator_list.iter().any(|decorator| is_class_method(decorator, semantic, classmethod_decorators))
|
||||
{
|
||||
FunctionType::ClassMethod
|
||||
} else {
|
||||
@@ -70,3 +49,83 @@ pub fn classify(
|
||||
FunctionType::Method
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if a [`Decorator`] is indicative of a static method.
|
||||
fn is_static_method(
|
||||
decorator: &Decorator,
|
||||
semantic: &SemanticModel,
|
||||
staticmethod_decorators: &[String],
|
||||
) -> bool {
|
||||
let decorator = map_callable(&decorator.expression);
|
||||
|
||||
// The decorator is an import, so should match against a qualified path.
|
||||
if semantic
|
||||
.resolve_call_path(decorator)
|
||||
.is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["", "staticmethod"] | ["abc", "abstractstaticmethod"]
|
||||
) || staticmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| call_path == from_qualified_name(decorator))
|
||||
})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We do not have a resolvable call path, most likely from a decorator like
|
||||
// `@someproperty.setter`. Instead, match on the last element.
|
||||
if !staticmethod_decorators.is_empty() {
|
||||
if collect_call_path(decorator).is_some_and(|call_path| {
|
||||
call_path.last().is_some_and(|tail| {
|
||||
staticmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| tail == decorator)
|
||||
})
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Return `true` if a [`Decorator`] is indicative of a class method.
|
||||
fn is_class_method(
|
||||
decorator: &Decorator,
|
||||
semantic: &SemanticModel,
|
||||
classmethod_decorators: &[String],
|
||||
) -> bool {
|
||||
let decorator = map_callable(&decorator.expression);
|
||||
|
||||
// The decorator is an import, so should match against a qualified path.
|
||||
if semantic
|
||||
.resolve_call_path(decorator)
|
||||
.is_some_and(|call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["", "classmethod"] | ["abc", "abstractclassmethod"]
|
||||
) || classmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| call_path == from_qualified_name(decorator))
|
||||
})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We do not have a resolvable call path, most likely from a decorator like
|
||||
// `@someproperty.setter`. Instead, match on the last element.
|
||||
if !classmethod_decorators.is_empty() {
|
||||
if collect_call_path(decorator).is_some_and(|call_path| {
|
||||
call_path.last().is_some_and(|tail| {
|
||||
classmethod_decorators
|
||||
.iter()
|
||||
.any(|decorator| tail == decorator)
|
||||
})
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user