[flake8-pyi] Respect pep8_naming.classmethod-decorators settings when determining if a method is a classmethod in custom-type-var-return-type (PYI019) (#13162)

This commit is contained in:
Alex Waygood 2024-08-30 14:24:01 +01:00 committed by GitHub
parent ce68f1cc1b
commit 34b4732c46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 160 additions and 82 deletions

View file

@ -44,3 +44,11 @@ class PEP695BadDunderNew[T]:
class PEP695GoodDunderNew[T]: class PEP695GoodDunderNew[T]:
def __new__(cls, *args: Any, **kwargs: Any) -> Self: ... def __new__(cls, *args: Any, **kwargs: Any) -> Self: ...
class CustomClassMethod:
# Should be recognised as a classmethod decorator
# due to `foo_classmethod being listed in `pep8_naming.classmethod-decorators`
# in the settings for this test:
@foo_classmethod
def foo[S](cls: type[S]) -> S: ... # PYI019

View file

@ -44,3 +44,11 @@ class PEP695BadDunderNew[T]:
class PEP695GoodDunderNew[T]: class PEP695GoodDunderNew[T]:
def __new__(cls, *args: Any, **kwargs: Any) -> Self: ... def __new__(cls, *args: Any, **kwargs: Any) -> Self: ...
class CustomClassMethod:
# Should be recognised as a classmethod decorator
# due to `foo_classmethod being listed in `pep8_naming.classmethod-decorators`
# in the settings for this test:
@foo_classmethod
def foo[S](cls: type[S]) -> S: ... # PYI019

View file

@ -9,6 +9,7 @@ mod tests {
use test_case::test_case; use test_case::test_case;
use crate::registry::Rule; use crate::registry::Rule;
use crate::rules::pep8_naming;
use crate::settings::types::{PreviewMode, PythonVersion}; use crate::settings::types::{PreviewMode, PythonVersion};
use crate::test::test_path; use crate::test::test_path;
use crate::{assert_messages, settings}; use crate::{assert_messages, settings};
@ -33,8 +34,6 @@ mod tests {
#[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.pyi"))] #[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.pyi"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))] #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))] #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))]
#[test_case(Rule::CustomTypeVarReturnType, Path::new("PYI019.py"))]
#[test_case(Rule::CustomTypeVarReturnType, Path::new("PYI019.pyi"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
#[test_case(Rule::DuplicateLiteralMember, Path::new("PYI062.py"))] #[test_case(Rule::DuplicateLiteralMember, Path::new("PYI062.py"))]
@ -135,6 +134,24 @@ mod tests {
Ok(()) Ok(())
} }
#[test_case(Rule::CustomTypeVarReturnType, Path::new("PYI019.py"))]
#[test_case(Rule::CustomTypeVarReturnType, Path::new("PYI019.pyi"))]
fn custom_classmethod_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_pyi").join(path).as_path(),
&settings::LinterSettings {
pep8_naming: pep8_naming::settings::Settings {
classmethod_decorators: vec!["foo_classmethod".to_string()],
..pep8_naming::settings::Settings::default()
},
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))] #[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))] #[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
fn py38(rule_code: Rule, path: &Path) -> Result<()> { fn py38(rule_code: Rule, path: &Path) -> Result<()> {

View file

@ -3,9 +3,8 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::{Decorator, Expr, Parameters, TypeParam, TypeParams}; use ruff_python_ast::{Decorator, Expr, Parameters, TypeParam, TypeParams};
use ruff_python_semantic::analyze::visibility::{ use ruff_python_semantic::analyze::function_type::{self, FunctionType};
is_abstract, is_classmethod, is_new, is_overload, is_staticmethod, use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload};
};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -71,7 +70,7 @@ pub(crate) fn custom_type_var_return_type(
type_params: Option<&TypeParams>, type_params: Option<&TypeParams>,
) { ) {
// Given, e.g., `def foo(self: _S, arg: bytes) -> _T`, extract `_T`. // Given, e.g., `def foo(self: _S, arg: bytes) -> _T`, extract `_T`.
let Some(return_annotation) = returns else { let Some(returns) = returns else {
return; return;
}; };
@ -86,44 +85,72 @@ pub(crate) fn custom_type_var_return_type(
return; return;
}; };
if !checker.semantic().current_scope().kind.is_class() { let semantic = checker.semantic();
return;
};
// Skip any abstract, static, and overloaded methods. // Skip any abstract, static, and overloaded methods.
if is_abstract(decorator_list, checker.semantic()) if is_abstract(decorator_list, semantic) || is_overload(decorator_list, semantic) {
|| is_overload(decorator_list, checker.semantic())
|| is_staticmethod(decorator_list, checker.semantic())
{
return; return;
} }
let uses_custom_var: bool = let method = match function_type::classify(
if is_classmethod(decorator_list, checker.semantic()) || is_new(name) { name,
class_method(self_or_cls_annotation, return_annotation, type_params) decorator_list,
} else { semantic.current_scope(),
// If not static, or a class method or __new__ we know it is an instance method semantic,
instance_method(self_or_cls_annotation, return_annotation, type_params) &checker.settings.pep8_naming.classmethod_decorators,
&checker.settings.pep8_naming.staticmethod_decorators,
) {
FunctionType::Function => return,
FunctionType::StaticMethod => return,
FunctionType::ClassMethod => Method::Class(ClassMethod {
cls_annotation: self_or_cls_annotation,
returns,
type_params,
}),
FunctionType::Method => Method::Instance(InstanceMethod {
self_annotation: self_or_cls_annotation,
returns,
type_params,
}),
}; };
if uses_custom_var { if method.uses_custom_var() {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
CustomTypeVarReturnType { CustomTypeVarReturnType {
method_name: name.to_string(), method_name: name.to_string(),
}, },
return_annotation.range(), returns.range(),
)); ));
} }
} }
#[derive(Debug)]
enum Method<'a> {
Class(ClassMethod<'a>),
Instance(InstanceMethod<'a>),
}
impl<'a> Method<'a> {
fn uses_custom_var(&self) -> bool {
match self {
Self::Class(class_method) => class_method.uses_custom_var(),
Self::Instance(instance_method) => instance_method.uses_custom_var(),
}
}
}
#[derive(Debug)]
struct ClassMethod<'a> {
cls_annotation: &'a Expr,
returns: &'a Expr,
type_params: Option<&'a TypeParams>,
}
impl<'a> ClassMethod<'a> {
/// Returns `true` if the class method is annotated with a custom `TypeVar` that is likely /// Returns `true` if the class method is annotated with a custom `TypeVar` that is likely
/// private. /// private.
fn class_method( fn uses_custom_var(&self) -> bool {
cls_annotation: &Expr, let Expr::Subscript(ast::ExprSubscript { slice, value, .. }) = self.cls_annotation else {
return_annotation: &Expr,
type_params: Option<&TypeParams>,
) -> bool {
let Expr::Subscript(ast::ExprSubscript { slice, value, .. }) = cls_annotation else {
return false; return false;
}; };
@ -141,7 +168,7 @@ fn class_method(
return false; return false;
}; };
let Expr::Name(return_annotation) = map_subscript(return_annotation) else { let Expr::Name(return_annotation) = map_subscript(self.returns) else {
return false; return false;
}; };
@ -149,26 +176,31 @@ fn class_method(
return false; return false;
} }
is_likely_private_typevar(&slice.id, type_params) is_likely_private_typevar(&slice.id, self.type_params)
}
} }
#[derive(Debug)]
struct InstanceMethod<'a> {
self_annotation: &'a Expr,
returns: &'a Expr,
type_params: Option<&'a TypeParams>,
}
impl<'a> InstanceMethod<'a> {
/// Returns `true` if the instance method is annotated with a custom `TypeVar` that is likely /// Returns `true` if the instance method is annotated with a custom `TypeVar` that is likely
/// private. /// private.
fn instance_method( fn uses_custom_var(&self) -> bool {
self_annotation: &Expr,
return_annotation: &Expr,
type_params: Option<&TypeParams>,
) -> bool {
let Expr::Name(ast::ExprName { let Expr::Name(ast::ExprName {
id: first_arg_type, .. id: first_arg_type, ..
}) = self_annotation }) = self.self_annotation
else { else {
return false; return false;
}; };
let Expr::Name(ast::ExprName { let Expr::Name(ast::ExprName {
id: return_type, .. id: return_type, ..
}) = map_subscript(return_annotation) }) = map_subscript(self.returns)
else { else {
return false; return false;
}; };
@ -177,7 +209,8 @@ fn instance_method(
return false; return false;
} }
is_likely_private_typevar(first_arg_type, type_params) is_likely_private_typevar(first_arg_type, self.type_params)
}
} }
/// Returns `true` if the type variable is likely private. /// Returns `true` if the type variable is likely private.

View file

@ -42,4 +42,10 @@ PYI019.py:42:46: PYI019 Methods like `generic_instance_method` should return `ty
| ^ PYI019 | ^ PYI019
| |
PYI019.py:54:32: PYI019 Methods like `foo` should return `typing.Self` instead of a custom `TypeVar`
|
52 | # in the settings for this test:
53 | @foo_classmethod
54 | def foo[S](cls: type[S]) -> S: ... # PYI019
| ^ PYI019
|

View file

@ -42,4 +42,10 @@ PYI019.pyi:42:46: PYI019 Methods like `generic_instance_method` should return `t
| ^ PYI019 | ^ PYI019
| |
PYI019.pyi:54:32: PYI019 Methods like `foo` should return `typing.Self` instead of a custom `TypeVar`
|
52 | # in the settings for this test:
53 | @foo_classmethod
54 | def foo[S](cls: type[S]) -> S: ... # PYI019
| ^ PYI019
|