mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:10:09 +00:00
Respect __str__
definitions from super classes (#9338)
Closes https://github.com/astral-sh/ruff/issues/9242.
This commit is contained in:
parent
cea2ec8dd0
commit
1f9353fed3
9 changed files with 111 additions and 49 deletions
|
@ -165,3 +165,19 @@ class AbstractTestModel5(models.Model):
|
||||||
|
|
||||||
def my_beautiful_method(self):
|
def my_beautiful_method(self):
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
# Subclass with its own __str__
|
||||||
|
class SubclassTestModel1(TestModel1):
|
||||||
|
def __str__(self):
|
||||||
|
return self.new_field
|
||||||
|
|
||||||
|
|
||||||
|
# Subclass with inherited __str__
|
||||||
|
class SubclassTestModel2(TestModel4):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Subclass without __str__
|
||||||
|
class SubclassTestModel3(TestModel1):
|
||||||
|
pass
|
||||||
|
|
|
@ -4,14 +4,14 @@ use ruff_python_semantic::{analyze, SemanticModel};
|
||||||
|
|
||||||
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
|
/// 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 {
|
pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
analyze::class::any_call_path(class_def, semantic, &|call_path| {
|
||||||
matches!(call_path.as_slice(), ["django", "db", "models", "Model"])
|
matches!(call_path.as_slice(), ["django", "db", "models", "Model"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if a Python class appears to be a Django model form, based on its base classes.
|
/// 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 {
|
pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
analyze::class::any_call_path(class_def, semantic, &|call_path| {
|
||||||
matches!(
|
matches!(
|
||||||
call_path.as_slice(),
|
call_path.as_slice(),
|
||||||
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]
|
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]
|
||||||
|
|
|
@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::helpers::is_const_true;
|
use ruff_python_ast::helpers::is_const_true;
|
||||||
use ruff_python_ast::identifier::Identifier;
|
use ruff_python_ast::identifier::Identifier;
|
||||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||||
use ruff_python_semantic::SemanticModel;
|
use ruff_python_semantic::{analyze, SemanticModel};
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
@ -55,9 +55,11 @@ pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::S
|
||||||
if !is_non_abstract_model(class_def, checker.semantic()) {
|
if !is_non_abstract_model(class_def, checker.semantic()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if has_dunder_method(class_def) {
|
|
||||||
|
if has_dunder_method(class_def, checker.semantic()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
DjangoModelWithoutDunderStr,
|
DjangoModelWithoutDunderStr,
|
||||||
class_def.identifier(),
|
class_def.identifier(),
|
||||||
|
@ -65,10 +67,12 @@ pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::S
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the class has `__str__` method.
|
/// Returns `true` if the class has `__str__` method.
|
||||||
fn has_dunder_method(class_def: &ast::StmtClassDef) -> bool {
|
fn has_dunder_method(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
|
||||||
class_def.body.iter().any(|val| match val {
|
analyze::class::any_super_class(class_def, semantic, &|class_def| {
|
||||||
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => name == "__str__",
|
class_def.body.iter().any(|val| match val {
|
||||||
_ => false,
|
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => name == "__str__",
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
|
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::helpers::is_const_true;
|
use ruff_python_ast::helpers::is_const_true;
|
||||||
|
use ruff_python_semantic::SemanticModel;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
@ -48,7 +49,7 @@ impl Violation for DjangoNullableModelStringField {
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let DjangoNullableModelStringField { field_name } = self;
|
let DjangoNullableModelStringField { field_name } = self;
|
||||||
format!("Avoid using `null=True` on string-based fields such as {field_name}")
|
format!("Avoid using `null=True` on string-based fields such as `{field_name}`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt])
|
||||||
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
|
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(field_name) = is_nullable_field(checker, value) {
|
if let Some(field_name) = is_nullable_field(value, checker.semantic()) {
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
DjangoNullableModelStringField {
|
DjangoNullableModelStringField {
|
||||||
field_name: field_name.to_string(),
|
field_name: field_name.to_string(),
|
||||||
|
@ -69,22 +70,12 @@ pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a str> {
|
fn is_nullable_field<'a>(value: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a str> {
|
||||||
let Expr::Call(ast::ExprCall {
|
let call = value.as_call_expr()?;
|
||||||
func,
|
|
||||||
arguments: Arguments { keywords, .. },
|
|
||||||
..
|
|
||||||
}) = value
|
|
||||||
else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(valid_field_name) = helpers::get_model_field_name(func, checker.semantic()) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let field_name = helpers::get_model_field_name(&call.func, semantic)?;
|
||||||
if !matches!(
|
if !matches!(
|
||||||
valid_field_name,
|
field_name,
|
||||||
"CharField" | "TextField" | "SlugField" | "EmailField" | "FilePathField" | "URLField"
|
"CharField" | "TextField" | "SlugField" | "EmailField" | "FilePathField" | "URLField"
|
||||||
) {
|
) {
|
||||||
return None;
|
return None;
|
||||||
|
@ -93,7 +84,7 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st
|
||||||
let mut null_key = false;
|
let mut null_key = false;
|
||||||
let mut blank_key = false;
|
let mut blank_key = false;
|
||||||
let mut unique_key = false;
|
let mut unique_key = false;
|
||||||
for keyword in keywords {
|
for keyword in &call.arguments.keywords {
|
||||||
let Some(argument) = &keyword.arg else {
|
let Some(argument) = &keyword.arg else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
@ -113,5 +104,6 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st
|
||||||
if !null_key {
|
if !null_key {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(valid_field_name)
|
|
||||||
|
Some(field_name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff_linter/src/rules/flake8_django/mod.rs
|
source: crates/ruff_linter/src/rules/flake8_django/mod.rs
|
||||||
---
|
---
|
||||||
DJ001.py:7:17: DJ001 Avoid using `null=True` on string-based fields such as CharField
|
DJ001.py:7:17: DJ001 Avoid using `null=True` on string-based fields such as `CharField`
|
||||||
|
|
|
|
||||||
6 | class IncorrectModel(models.Model):
|
6 | class IncorrectModel(models.Model):
|
||||||
7 | charfield = models.CharField(max_length=255, null=True)
|
7 | charfield = models.CharField(max_length=255, null=True)
|
||||||
|
@ -10,7 +10,7 @@ DJ001.py:7:17: DJ001 Avoid using `null=True` on string-based fields such as Char
|
||||||
9 | slugfield = models.SlugField(max_length=255, null=True)
|
9 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:8:17: DJ001 Avoid using `null=True` on string-based fields such as TextField
|
DJ001.py:8:17: DJ001 Avoid using `null=True` on string-based fields such as `TextField`
|
||||||
|
|
|
|
||||||
6 | class IncorrectModel(models.Model):
|
6 | class IncorrectModel(models.Model):
|
||||||
7 | charfield = models.CharField(max_length=255, null=True)
|
7 | charfield = models.CharField(max_length=255, null=True)
|
||||||
|
@ -20,7 +20,7 @@ DJ001.py:8:17: DJ001 Avoid using `null=True` on string-based fields such as Text
|
||||||
10 | emailfield = models.EmailField(max_length=255, null=True)
|
10 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:9:17: DJ001 Avoid using `null=True` on string-based fields such as SlugField
|
DJ001.py:9:17: DJ001 Avoid using `null=True` on string-based fields such as `SlugField`
|
||||||
|
|
|
|
||||||
7 | charfield = models.CharField(max_length=255, null=True)
|
7 | charfield = models.CharField(max_length=255, null=True)
|
||||||
8 | textfield = models.TextField(max_length=255, null=True)
|
8 | textfield = models.TextField(max_length=255, null=True)
|
||||||
|
@ -30,7 +30,7 @@ DJ001.py:9:17: DJ001 Avoid using `null=True` on string-based fields such as Slug
|
||||||
11 | filepathfield = models.FilePathField(max_length=255, null=True)
|
11 | filepathfield = models.FilePathField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:10:18: DJ001 Avoid using `null=True` on string-based fields such as EmailField
|
DJ001.py:10:18: DJ001 Avoid using `null=True` on string-based fields such as `EmailField`
|
||||||
|
|
|
|
||||||
8 | textfield = models.TextField(max_length=255, null=True)
|
8 | textfield = models.TextField(max_length=255, null=True)
|
||||||
9 | slugfield = models.SlugField(max_length=255, null=True)
|
9 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
|
@ -40,7 +40,7 @@ DJ001.py:10:18: DJ001 Avoid using `null=True` on string-based fields such as Ema
|
||||||
12 | urlfield = models.URLField(max_length=255, null=True)
|
12 | urlfield = models.URLField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:11:21: DJ001 Avoid using `null=True` on string-based fields such as FilePathField
|
DJ001.py:11:21: DJ001 Avoid using `null=True` on string-based fields such as `FilePathField`
|
||||||
|
|
|
|
||||||
9 | slugfield = models.SlugField(max_length=255, null=True)
|
9 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
10 | emailfield = models.EmailField(max_length=255, null=True)
|
10 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
|
@ -49,7 +49,7 @@ DJ001.py:11:21: DJ001 Avoid using `null=True` on string-based fields such as Fil
|
||||||
12 | urlfield = models.URLField(max_length=255, null=True)
|
12 | urlfield = models.URLField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:12:16: DJ001 Avoid using `null=True` on string-based fields such as URLField
|
DJ001.py:12:16: DJ001 Avoid using `null=True` on string-based fields such as `URLField`
|
||||||
|
|
|
|
||||||
10 | emailfield = models.EmailField(max_length=255, null=True)
|
10 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
11 | filepathfield = models.FilePathField(max_length=255, null=True)
|
11 | filepathfield = models.FilePathField(max_length=255, null=True)
|
||||||
|
@ -57,7 +57,7 @@ DJ001.py:12:16: DJ001 Avoid using `null=True` on string-based fields such as URL
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ001
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ001
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:16:17: DJ001 Avoid using `null=True` on string-based fields such as CharField
|
DJ001.py:16:17: DJ001 Avoid using `null=True` on string-based fields such as `CharField`
|
||||||
|
|
|
|
||||||
15 | class IncorrectModelWithAlias(DjangoModel):
|
15 | class IncorrectModelWithAlias(DjangoModel):
|
||||||
16 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
16 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
||||||
|
@ -66,7 +66,7 @@ DJ001.py:16:17: DJ001 Avoid using `null=True` on string-based fields such as Cha
|
||||||
18 | slugfield = models.SlugField(max_length=255, null=True)
|
18 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:17:17: DJ001 Avoid using `null=True` on string-based fields such as CharField
|
DJ001.py:17:17: DJ001 Avoid using `null=True` on string-based fields such as `CharField`
|
||||||
|
|
|
|
||||||
15 | class IncorrectModelWithAlias(DjangoModel):
|
15 | class IncorrectModelWithAlias(DjangoModel):
|
||||||
16 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
16 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
||||||
|
@ -76,7 +76,7 @@ DJ001.py:17:17: DJ001 Avoid using `null=True` on string-based fields such as Cha
|
||||||
19 | emailfield = models.EmailField(max_length=255, null=True)
|
19 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:18:17: DJ001 Avoid using `null=True` on string-based fields such as SlugField
|
DJ001.py:18:17: DJ001 Avoid using `null=True` on string-based fields such as `SlugField`
|
||||||
|
|
|
|
||||||
16 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
16 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
||||||
17 | textfield = SmthCharField(max_length=255, null=True)
|
17 | textfield = SmthCharField(max_length=255, null=True)
|
||||||
|
@ -86,7 +86,7 @@ DJ001.py:18:17: DJ001 Avoid using `null=True` on string-based fields such as Slu
|
||||||
20 | filepathfield = models.FilePathField(max_length=255, null=True)
|
20 | filepathfield = models.FilePathField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:19:18: DJ001 Avoid using `null=True` on string-based fields such as EmailField
|
DJ001.py:19:18: DJ001 Avoid using `null=True` on string-based fields such as `EmailField`
|
||||||
|
|
|
|
||||||
17 | textfield = SmthCharField(max_length=255, null=True)
|
17 | textfield = SmthCharField(max_length=255, null=True)
|
||||||
18 | slugfield = models.SlugField(max_length=255, null=True)
|
18 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
|
@ -96,7 +96,7 @@ DJ001.py:19:18: DJ001 Avoid using `null=True` on string-based fields such as Ema
|
||||||
21 | urlfield = models.URLField(max_length=255, null=True)
|
21 | urlfield = models.URLField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:20:21: DJ001 Avoid using `null=True` on string-based fields such as FilePathField
|
DJ001.py:20:21: DJ001 Avoid using `null=True` on string-based fields such as `FilePathField`
|
||||||
|
|
|
|
||||||
18 | slugfield = models.SlugField(max_length=255, null=True)
|
18 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
19 | emailfield = models.EmailField(max_length=255, null=True)
|
19 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
|
@ -105,7 +105,7 @@ DJ001.py:20:21: DJ001 Avoid using `null=True` on string-based fields such as Fil
|
||||||
21 | urlfield = models.URLField(max_length=255, null=True)
|
21 | urlfield = models.URLField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:21:16: DJ001 Avoid using `null=True` on string-based fields such as URLField
|
DJ001.py:21:16: DJ001 Avoid using `null=True` on string-based fields such as `URLField`
|
||||||
|
|
|
|
||||||
19 | emailfield = models.EmailField(max_length=255, null=True)
|
19 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
20 | filepathfield = models.FilePathField(max_length=255, null=True)
|
20 | filepathfield = models.FilePathField(max_length=255, null=True)
|
||||||
|
@ -113,7 +113,7 @@ DJ001.py:21:16: DJ001 Avoid using `null=True` on string-based fields such as URL
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ001
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ001
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:25:17: DJ001 Avoid using `null=True` on string-based fields such as CharField
|
DJ001.py:25:17: DJ001 Avoid using `null=True` on string-based fields such as `CharField`
|
||||||
|
|
|
|
||||||
24 | class IncorrectModelWithoutSuperclass:
|
24 | class IncorrectModelWithoutSuperclass:
|
||||||
25 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
25 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
||||||
|
@ -122,7 +122,7 @@ DJ001.py:25:17: DJ001 Avoid using `null=True` on string-based fields such as Cha
|
||||||
27 | slugfield = models.SlugField(max_length=255, null=True)
|
27 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:26:17: DJ001 Avoid using `null=True` on string-based fields such as CharField
|
DJ001.py:26:17: DJ001 Avoid using `null=True` on string-based fields such as `CharField`
|
||||||
|
|
|
|
||||||
24 | class IncorrectModelWithoutSuperclass:
|
24 | class IncorrectModelWithoutSuperclass:
|
||||||
25 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
25 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
||||||
|
@ -132,7 +132,7 @@ DJ001.py:26:17: DJ001 Avoid using `null=True` on string-based fields such as Cha
|
||||||
28 | emailfield = models.EmailField(max_length=255, null=True)
|
28 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:27:17: DJ001 Avoid using `null=True` on string-based fields such as SlugField
|
DJ001.py:27:17: DJ001 Avoid using `null=True` on string-based fields such as `SlugField`
|
||||||
|
|
|
|
||||||
25 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
25 | charfield = DjangoModel.CharField(max_length=255, null=True)
|
||||||
26 | textfield = SmthCharField(max_length=255, null=True)
|
26 | textfield = SmthCharField(max_length=255, null=True)
|
||||||
|
@ -142,7 +142,7 @@ DJ001.py:27:17: DJ001 Avoid using `null=True` on string-based fields such as Slu
|
||||||
29 | filepathfield = models.FilePathField(max_length=255, null=True)
|
29 | filepathfield = models.FilePathField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:28:18: DJ001 Avoid using `null=True` on string-based fields such as EmailField
|
DJ001.py:28:18: DJ001 Avoid using `null=True` on string-based fields such as `EmailField`
|
||||||
|
|
|
|
||||||
26 | textfield = SmthCharField(max_length=255, null=True)
|
26 | textfield = SmthCharField(max_length=255, null=True)
|
||||||
27 | slugfield = models.SlugField(max_length=255, null=True)
|
27 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
|
@ -152,7 +152,7 @@ DJ001.py:28:18: DJ001 Avoid using `null=True` on string-based fields such as Ema
|
||||||
30 | urlfield = models.URLField(max_length=255, null=True)
|
30 | urlfield = models.URLField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:29:21: DJ001 Avoid using `null=True` on string-based fields such as FilePathField
|
DJ001.py:29:21: DJ001 Avoid using `null=True` on string-based fields such as `FilePathField`
|
||||||
|
|
|
|
||||||
27 | slugfield = models.SlugField(max_length=255, null=True)
|
27 | slugfield = models.SlugField(max_length=255, null=True)
|
||||||
28 | emailfield = models.EmailField(max_length=255, null=True)
|
28 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
|
@ -161,7 +161,7 @@ DJ001.py:29:21: DJ001 Avoid using `null=True` on string-based fields such as Fil
|
||||||
30 | urlfield = models.URLField(max_length=255, null=True)
|
30 | urlfield = models.URLField(max_length=255, null=True)
|
||||||
|
|
|
|
||||||
|
|
||||||
DJ001.py:30:16: DJ001 Avoid using `null=True` on string-based fields such as URLField
|
DJ001.py:30:16: DJ001 Avoid using `null=True` on string-based fields such as `URLField`
|
||||||
|
|
|
|
||||||
28 | emailfield = models.EmailField(max_length=255, null=True)
|
28 | emailfield = models.EmailField(max_length=255, null=True)
|
||||||
29 | filepathfield = models.FilePathField(max_length=255, null=True)
|
29 | filepathfield = models.FilePathField(max_length=255, null=True)
|
||||||
|
|
|
@ -23,4 +23,12 @@ DJ008.py:36:7: DJ008 Model does not define `__str__` method
|
||||||
37 | new_field = models.CharField(max_length=10)
|
37 | new_field = models.CharField(max_length=10)
|
||||||
|
|
|
|
||||||
|
|
||||||
|
DJ008.py:182:7: DJ008 Model does not define `__str__` method
|
||||||
|
|
|
||||||
|
181 | # Subclass without __str__
|
||||||
|
182 | class SubclassTestModel3(TestModel1):
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ DJ008
|
||||||
|
183 | pass
|
||||||
|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ fn runtime_required_base_class(
|
||||||
base_classes: &[String],
|
base_classes: &[String],
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
analyze::class::any_call_path(class_def, semantic, &|call_path| {
|
||||||
base_classes
|
base_classes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|base_class| from_qualified_name(base_class) == call_path)
|
.any(|base_class| from_qualified_name(base_class) == call_path)
|
||||||
|
|
|
@ -56,7 +56,7 @@ pub(super) fn has_default_copy_semantics(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
analyze::class::any_over_body(class_def, semantic, &|call_path| {
|
analyze::class::any_call_path(class_def, semantic, &|call_path| {
|
||||||
matches!(
|
matches!(
|
||||||
call_path.as_slice(),
|
call_path.as_slice(),
|
||||||
["pydantic", "BaseModel" | "BaseSettings"]
|
["pydantic", "BaseModel" | "BaseSettings"]
|
||||||
|
|
|
@ -6,8 +6,8 @@ use ruff_python_ast::helpers::map_subscript;
|
||||||
|
|
||||||
use crate::{BindingId, SemanticModel};
|
use crate::{BindingId, SemanticModel};
|
||||||
|
|
||||||
/// Return `true` if any base class of a class definition matches a predicate.
|
/// Return `true` if any base class matches a [`CallPath`] predicate.
|
||||||
pub fn any_over_body(
|
pub fn any_call_path(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
func: &dyn Fn(CallPath) -> bool,
|
func: &dyn Fn(CallPath) -> bool,
|
||||||
|
@ -55,3 +55,45 @@ pub fn any_over_body(
|
||||||
|
|
||||||
inner(class_def, semantic, func, &mut FxHashSet::default())
|
inner(class_def, semantic, func, &mut FxHashSet::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` if any base class matches an [`ast::StmtClassDef`] predicate.
|
||||||
|
pub fn any_super_class(
|
||||||
|
class_def: &ast::StmtClassDef,
|
||||||
|
semantic: &SemanticModel,
|
||||||
|
func: &dyn Fn(&ast::StmtClassDef) -> bool,
|
||||||
|
) -> bool {
|
||||||
|
fn inner(
|
||||||
|
class_def: &ast::StmtClassDef,
|
||||||
|
semantic: &SemanticModel,
|
||||||
|
func: &dyn Fn(&ast::StmtClassDef) -> bool,
|
||||||
|
seen: &mut FxHashSet<BindingId>,
|
||||||
|
) -> bool {
|
||||||
|
// If the function itself matches the pattern, then this does too.
|
||||||
|
if func(class_def) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check every base class.
|
||||||
|
class_def.bases().iter().any(|expr| {
|
||||||
|
// If the base class extends a class that matches the pattern, then this does too.
|
||||||
|
if let Some(id) = semantic.lookup_attribute(map_subscript(expr)) {
|
||||||
|
if seen.insert(id) {
|
||||||
|
let binding = semantic.binding(id);
|
||||||
|
if let Some(base_class) = binding
|
||||||
|
.kind
|
||||||
|
.as_class_definition()
|
||||||
|
.map(|id| &semantic.scopes[*id])
|
||||||
|
.and_then(|scope| scope.kind.as_class())
|
||||||
|
{
|
||||||
|
if inner(base_class, semantic, func, seen) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
inner(class_def, semantic, func, &mut FxHashSet::default())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue