Skip to content

Commit 6ccf6bc

Browse files
committed
Fixes and include_types feature
1 parent ccdae65 commit 6ccf6bc

File tree

15 files changed

+366
-119
lines changed

15 files changed

+366
-119
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
### Unreleased
44

5+
- Fix ignore_checks
6+
- Skip models fields not inherited from fields.Field
7+
- Add ignore_types option
8+
59
### 0.4.1
610

711
- Fix message for *field-verbose-name-gettext-case*

conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ def handler(self, handler):
3232
return self
3333

3434
def run(self):
35-
assert self._registry.is_healthy
3635
self._registry.enabled_checks = {}
3736
handlers = self._registry.bind()
37+
assert (
38+
self._registry.is_healthy
39+
), f"Settings has errors: {self._registry._config.errors.as_text()}"
3840
return list(handlers[self.TEST_TAG]())
3941

4042
def models(self, *models):

src/extra_checks/__init__.py

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,9 @@
1-
import enum
2-
from typing import Any, Callable, Union
3-
4-
5-
class CheckId(str, enum.Enum):
6-
X001 = "extra-checks-config"
7-
X010 = "model-attribute"
8-
X011 = "model-meta-attribute"
9-
X012 = "model-admin"
10-
X050 = "field-verbose-name"
11-
X051 = "field-verbose-name-gettext"
12-
X052 = "field-verbose-name-gettext-case"
13-
X053 = "field-help-text-gettext"
14-
X054 = "field-file-upload-to"
15-
X055 = "field-text-null"
16-
X056 = "field-boolean-null"
17-
X057 = "field-null"
18-
X058 = "field-foreign-key-db-index"
19-
X301 = "drf-model-serializer-extra-kwargs"
20-
X302 = "drf-model-serializer-meta-attribute"
21-
22-
23-
_IGNORED = {}
24-
25-
EXTRA_CHECKS_ALL_RULES = list(CheckId.__members__.keys())
26-
27-
28-
def ignore_checks(*args: Union[CheckId, str]) -> Callable[[Any], Any]:
29-
def f(entity: Any) -> Any:
30-
_IGNORED[entity] = set(args)
31-
return entity
32-
33-
return f
34-
1+
from .check_id import CheckId
2+
from .registry import ignore_checks
353

364
default_app_config = "extra_checks.apps.ExtraChecksConfig"
375

6+
387
__all__ = [
398
"ignore_checks",
409
"CheckId",

src/extra_checks/apps.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from django.apps import AppConfig
22

3+
from . import checks # noqa
4+
from .registry import registry
5+
36

47
class ExtraChecksConfig(AppConfig):
58
name = "extra_checks"
69

710
def ready(self) -> None:
811
super(ExtraChecksConfig, self).ready()
9-
from . import checks # noqa
10-
from .registry import registry
11-
1212
registry.bind()

src/extra_checks/ast.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,7 @@ def kwargs(self) -> Dict[str, ast.keyword]:
116116

117117
@cached_property
118118
def verbose_name(self) -> Union[None, ast.Constant, ast.Call]:
119-
result = getattr(self.kwargs.get("verbose_name"), "value", None)
120-
if result:
121-
return result
122-
if (
123-
self.field_class_name
124-
not in ("OneToOneField", "ManyToManyField", "ForeignKey")
125-
and self.args
126-
):
127-
node = self.args[0]
128-
if isinstance(node, ast.Call) and hasattr(node.func, "id"):
129-
return node
130-
elif isinstance(node, (ast.Constant, ast.Str)):
131-
return node
132-
return None
119+
return getattr(self.kwargs.get("verbose_name"), "value", None)
133120

134121
@cached_property
135122
def help_text(self) -> Optional[ast.AST]:

src/extra_checks/check_id.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import enum
2+
3+
4+
class CheckId(str, enum.Enum):
5+
X001 = "extra-checks-config"
6+
X010 = "model-attribute"
7+
X011 = "model-meta-attribute"
8+
X012 = "model-admin"
9+
X050 = "field-verbose-name"
10+
X051 = "field-verbose-name-gettext"
11+
X052 = "field-verbose-name-gettext-case"
12+
X053 = "field-help-text-gettext"
13+
X054 = "field-file-upload-to"
14+
X055 = "field-text-null"
15+
X056 = "field-boolean-null"
16+
X057 = "field-null"
17+
X058 = "field-foreign-key-db-index"
18+
X301 = "drf-model-serializer-extra-kwargs"
19+
X302 = "drf-model-serializer-meta-attribute"

src/extra_checks/checks/base_checks.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ class BaseCheck(ABC):
2020
level = django.core.checks.WARNING
2121

2222
def __init__(
23-
self, level: Optional[int] = None, ignored_objects: Optional[Set[Any]] = None
23+
self,
24+
level: Optional[int] = None,
25+
ignore_objects: Optional[Set[Any]] = None,
26+
ignore_types: Optional[Set[str]] = None,
2427
) -> None:
2528
self.level = level or self.level
26-
self.ignored_objects = ignored_objects or set()
29+
self.ignore_objects = ignore_objects or set()
30+
self.ignore_types = ignore_types or set()
2731

2832
def __call__(
2933
self, obj: Any, **kwargs: Any
@@ -32,7 +36,7 @@ def __call__(
3236
yield from self.apply(obj, **kwargs) # type: ignore
3337

3438
def is_ignored(self, obj: Any) -> bool:
35-
return obj in self.ignored_objects
39+
return obj in self.ignore_objects or type(obj) in self.ignore_types
3640

3741
def message(
3842
self, message: str, hint: Optional[str] = None, obj: Any = None

src/extra_checks/checks/model_checks.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ def check_models(
5959
yield from check(model, model_ast=model_ast)
6060
if field_checks:
6161
for field, node in model_ast.field_nodes:
62-
field_ast = FieldAST(node)
63-
for check in field_checks:
64-
yield from check(field, field_ast=field_ast, model=model)
62+
if isinstance(field, models.fields.Field):
63+
field_ast = FieldAST(node)
64+
for check in field_checks:
65+
yield from check(field, field_ast=field_ast, model=model)
6566

6667

6768
class CheckModel(BaseCheck):

src/extra_checks/checks/model_field_checks.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ast
22
from abc import abstractmethod
3-
from typing import Any, Iterator, Type
3+
from typing import Any, Iterator, Type, Union
44

55
import django.core.checks
66
from django import forms
@@ -16,10 +16,17 @@
1616
class CheckModelField(BaseCheck):
1717
@abstractmethod
1818
def apply(
19-
self, field: object, *, field_ast: FieldAST, model: Type[models.Model]
19+
self,
20+
field: models.fields.Field,
21+
*,
22+
field_ast: FieldAST,
23+
model: Type[models.Model],
2024
) -> Iterator[django.core.checks.CheckMessage]:
2125
raise NotImplementedError()
2226

27+
def is_ignored(self, obj: Any) -> bool:
28+
return obj.model in self.ignore_objects or type(obj) in self.ignore_types
29+
2330

2431
class GetTextMixin(BaseCheckMixin):
2532
class GettTextFuncForm(BaseCheckForm):
@@ -38,14 +45,32 @@ def _is_gettext_node(self, node: ast.AST) -> bool:
3845
)
3946

4047

48+
def get_verbose_name(
49+
field: models.fields.Field, field_ast: FieldAST
50+
) -> Union[None, ast.Constant, ast.Call]:
51+
result = field_ast.verbose_name
52+
if result:
53+
return result
54+
if isinstance(field, models.fields.related.RelatedField):
55+
return None
56+
if field_ast.args:
57+
node = field_ast.args[0]
58+
if isinstance(node, ast.Call) and hasattr(node.func, "id"):
59+
return node
60+
elif isinstance(node, (ast.Constant, ast.Str)):
61+
return node
62+
return None
63+
64+
4165
@registry.register(django.core.checks.Tags.models)
4266
class CheckFieldVerboseName(CheckModelField):
4367
Id = CheckId.X050
4468

4569
def apply(
46-
self, field: object, field_ast: FieldAST, **kwargs: Any
70+
self, field: models.fields.Field, field_ast: FieldAST, **kwargs: Any
4771
) -> Iterator[django.core.checks.CheckMessage]:
48-
if not field_ast.verbose_name:
72+
verbose_name = get_verbose_name(field, field_ast)
73+
if not verbose_name:
4974
yield self.message(
5075
"Field has no verbose name.",
5176
hint="Set verbose name on the field.",
@@ -58,9 +83,10 @@ class CheckFieldVerboseNameGettext(GetTextMixin, CheckModelField):
5883
Id = CheckId.X051
5984

6085
def apply(
61-
self, field: object, field_ast: FieldAST, **kwargs: Any
86+
self, field: models.fields.Field, field_ast: FieldAST, **kwargs: Any
6287
) -> Iterator[django.core.checks.CheckMessage]:
63-
if field_ast.verbose_name and not self._is_gettext_node(field_ast.verbose_name):
88+
verbose_name = get_verbose_name(field, field_ast)
89+
if verbose_name and not self._is_gettext_node(verbose_name):
6490
yield self.message(
6591
"Verbose name should use gettext.",
6692
hint="Use gettext on the verbose name.",
@@ -73,10 +99,11 @@ class CheckFieldVerboseNameGettextCase(GetTextMixin, CheckModelField):
7399
Id = CheckId.X052
74100

75101
def apply(
76-
self, field: object, field_ast: FieldAST, **kwargs: Any
102+
self, field: models.fields.Field, field_ast: FieldAST, **kwargs: Any
77103
) -> Iterator[django.core.checks.CheckMessage]:
78-
if field_ast.verbose_name and self._is_gettext_node(field_ast.verbose_name):
79-
value = field_ast.verbose_name.args[0].s # type: ignore
104+
verbose_name = get_verbose_name(field, field_ast)
105+
if verbose_name and self._is_gettext_node(verbose_name):
106+
value = verbose_name.args[0].s # type: ignore
80107
if not all(
81108
w.islower() or w.isupper() or w.isdigit() for w in value.split(" ")
82109
):
@@ -92,7 +119,7 @@ class CheckFieldHelpTextGettext(GetTextMixin, CheckModelField):
92119
Id = CheckId.X053
93120

94121
def apply(
95-
self, field: object, field_ast: FieldAST, **kwargs: Any
122+
self, field: models.fields.Field, field_ast: FieldAST, **kwargs: Any
96123
) -> Iterator[django.core.checks.CheckMessage]:
97124
if field_ast.help_text and not self._is_gettext_node(field_ast.help_text):
98125
yield self.message(
@@ -107,7 +134,7 @@ class CheckFieldFileUploadTo(CheckModelField):
107134
Id = CheckId.X054
108135

109136
def apply(
110-
self, field: object, **kwargs: Any
137+
self, field: models.fields.Field, **kwargs: Any
111138
) -> Iterator[django.core.checks.CheckMessage]:
112139
if isinstance(field, models.FileField):
113140
if not field.upload_to:
@@ -123,7 +150,7 @@ class CheckFieldTextNull(CheckModelField):
123150
Id = CheckId.X055
124151

125152
def apply(
126-
self, field: object, **kwargs: Any
153+
self, field: models.fields.Field, **kwargs: Any
127154
) -> Iterator[django.core.checks.CheckMessage]:
128155
if isinstance(field, (models.CharField, models.TextField)):
129156
if field.null:
@@ -140,7 +167,7 @@ class CheckFieldNullBoolean(CheckModelField):
140167
Id = CheckId.X056
141168

142169
def apply(
143-
self, field: object, **kwargs: Any
170+
self, field: models.fields.Field, **kwargs: Any
144171
) -> Iterator[django.core.checks.CheckMessage]:
145172
if isinstance(field, models.NullBooleanField):
146173
yield self.message(
@@ -155,15 +182,14 @@ class CheckFieldNullFalse(CheckModelField):
155182
Id = CheckId.X057
156183

157184
def apply(
158-
self, field: object, field_ast: FieldAST, **kwargs: Any
185+
self, field: models.fields.Field, field_ast: FieldAST, **kwargs: Any
159186
) -> Iterator[django.core.checks.CheckMessage]:
160-
if isinstance(field, models.fields.Field):
161-
if field.null is False and "null" in field_ast.kwargs:
162-
yield self.message(
163-
"Argument `null=False` is default.",
164-
hint="Remove `null=False` from field arguments.",
165-
obj=field,
166-
)
187+
if field.null is False and "null" in field_ast.kwargs:
188+
yield self.message(
189+
"Argument `null=False` is default.",
190+
hint="Remove `null=False` from field arguments.",
191+
obj=field,
192+
)
167193

168194

169195
@registry.register(django.core.checks.Tags.models)
@@ -183,7 +209,10 @@ def __init__(self, when: str, **kwargs: Any) -> None:
183209
super().__init__(**kwargs)
184210

185211
def apply(
186-
self, field: object, field_ast: FieldAST, model: Type[models.Model],
212+
self,
213+
field: models.fields.Field,
214+
field_ast: FieldAST,
215+
model: Type[models.Model],
187216
) -> Iterator[django.core.checks.CheckMessage]:
188217
if isinstance(field, models.fields.related.RelatedField):
189218
if field.many_to_one and field_ast.kwargs.get("db_index") is None:

src/extra_checks/forms.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib
12
import typing
23

34
import django.core.checks
@@ -165,12 +166,36 @@ class BaseCheckForm(forms.Form):
165166
choices=[(c, c) for c in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]],
166167
required=False,
167168
)
169+
ignore_types = ListField(forms.CharField(), required=False)
168170

169171
def clean_level(self) -> typing.Optional[int]:
170172
if self.cleaned_data["level"]:
171173
return getattr(django.core.checks, self.cleaned_data["level"])
172174
return None
173175

176+
def clean_ignore_types(self) -> set:
177+
value = self.cleaned_data["ignore_types"]
178+
if not value:
179+
return value
180+
result = []
181+
for import_path in value:
182+
try:
183+
path, entry = import_path.rsplit(".", 1)
184+
result.append(getattr(importlib.import_module(path), entry))
185+
except (ImportError, ValueError, AttributeError):
186+
raise forms.ValidationError(
187+
f"ignore_types contains entry that can't be imported: '{import_path}'."
188+
)
189+
return result
190+
191+
def clean(self) -> typing.Dict[str, typing.Any]:
192+
if (
193+
"ignore_types" in self.cleaned_data
194+
and not self.cleaned_data["ignore_types"]
195+
):
196+
del self.cleaned_data["ignore_types"]
197+
return self.cleaned_data
198+
174199

175200
class AttrsForm(BaseCheckForm):
176201
attrs = ListField(forms.CharField())

0 commit comments

Comments
 (0)