Skip to content

Commit

Permalink
Treat type(Protocol) et al as metaclass base (astral-sh#12770)
Browse files Browse the repository at this point in the history
## Summary

Closes astral-sh#12736.
  • Loading branch information
charliermarsh authored Aug 9, 2024
1 parent 37b9bac commit 69e1c56
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 22 deletions.
11 changes: 11 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pep8_naming/N805.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,14 @@ def bad_method(this):
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): ...
4 changes: 2 additions & 2 deletions crates/ruff_linter/src/rules/flake8_django/rules/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,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"]
Expand All @@ -277,7 +277,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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/pep8_naming/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ struct BodyEntries<'a> {
struct BodyVisitor<'a> {
returns: Vec<Entry>,
yields: Vec<Entry>,
currently_suspended_exceptions: Option<&'a Expr>,
currently_suspended_exceptions: Option<&'a ast::Expr>,
raised_exceptions: Vec<ExceptionEntry<'a>>,
semantic: &'a SemanticModel<'a>,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/rules/ruff/rules/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ pub(super) fn has_default_copy_semantics(
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(),
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
Expand Down
54 changes: 40 additions & 14 deletions crates/ruff_python_semantic/src/analyze/class.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
use rustc_hash::FxHashSet;

use crate::{BindingId, SemanticModel};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::name::QualifiedName;

use crate::{BindingId, SemanticModel};
use ruff_python_ast::Expr;

/// Return `true` if any base class matches a [`QualifiedName`] predicate.
pub fn any_qualified_name(
pub fn any_qualified_base_class(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
func: &dyn Fn(QualifiedName) -> bool,
) -> bool {
any_base_class(class_def, semantic, &|expr| {
semantic
.resolve_qualified_name(map_subscript(expr))
.is_some_and(func)
})
}

/// Return `true` if any base class matches an [`Expr`] predicate.
pub fn any_base_class(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
func: &dyn Fn(&Expr) -> bool,
) -> bool {
fn inner(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
func: &dyn Fn(QualifiedName) -> bool,
func: &dyn Fn(&Expr) -> bool,
seen: &mut FxHashSet<BindingId>,
) -> bool {
class_def.bases().iter().any(|expr| {
// If the base class itself matches the pattern, then this does too.
// Ex) `class Foo(BaseModel): ...`
if semantic
.resolve_qualified_name(map_subscript(expr))
.is_some_and(func)
{
if func(expr) {
return true;
}

Expand Down Expand Up @@ -100,7 +110,7 @@ pub fn any_super_class(

/// Return `true` if `class_def` is a class that has one or more enum classes in its mro
pub fn is_enumeration(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
any_qualified_name(class_def, semantic, &|qualified_name| {
any_qualified_base_class(class_def, semantic, &|qualified_name| {
matches!(
qualified_name.segments(),
[
Expand All @@ -113,10 +123,26 @@ pub fn is_enumeration(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -

/// Returns `true` if the given class is a metaclass.
pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
any_qualified_name(class_def, semantic, &|qualified_name| {
matches!(
qualified_name.segments(),
["" | "builtins", "type"] | ["abc", "ABCMeta"] | ["enum", "EnumMeta" | "EnumType"]
)
any_base_class(class_def, semantic, &|expr| match expr {
Expr::Call(ast::ExprCall {
func, arguments, ..
}) => {
// Ex) `class Foo(type(Protocol)): ...`
arguments.len() == 1 && semantic.match_builtin_expr(func.as_ref(), "type")
}
Expr::Subscript(ast::ExprSubscript { value, .. }) => {
// Ex) `class Foo(type[int]): ...`
semantic.match_builtin_expr(value.as_ref(), "type")
}
_ => semantic
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["" | "builtins", "type"]
| ["abc", "ABCMeta"]
| ["enum", "EnumMeta" | "EnumType"]
)
}),
})
}

0 comments on commit 69e1c56

Please sign in to comment.