Skip to content

Commit 750cebc

Browse files
committed
Add skipif option
1 parent e7c17de commit 750cebc

File tree

7 files changed

+107
-4
lines changed

7 files changed

+107
-4
lines changed

CHANGELOG.md

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

33
### Unreleased
44

5+
### dev
6+
7+
- Add option `skipif` that accepts user function
8+
- Deprecate `ignore_types`
9+
510
### 0.9.1
611

712
- Replace DeprecationWarning with FutureWarning for `@ignore_checks`

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,32 @@ class MyModel(models.Model):
5050
...
5151
```
5252

53-
Another way is to specify type of the object that need to be ignored in `ignore_types` option:
53+
Another way is to provide function that accepts field, model or
54+
serializer class as its first argument and returns `True` if it must be skipped.
55+
_Be aware that the more computation expensive your skipif functions the
56+
slower django check will run._
57+
58+
`skipif` example:
5459

5560
```python
61+
def skipif_streamfield(field, *args, **kwargs):
62+
return isinstance(field, wagtail.core.fields.StreamField)
63+
64+
def skipif_non_core_app(model_cls, *args, **kwargs):
65+
return model_cls._meta.app_label != "my_core_app"
66+
5667
EXTRA_CHECKS = {
5768
"check": [
5869
{
5970
"id": "field-verbose-name-gettext",
6071
# make this check skip wagtail's StreamField
61-
"ignore_types": ["wagtail.core.fields.StreamField"],
62-
}
72+
"skipif": skipif_streamfield
73+
},
74+
{
75+
"id": "model-admin",
76+
# models from non core app shouldn't be registered in admin
77+
"skipif": skipif_non_core_app,
78+
},
6379
]
6480
}
6581
```

src/extra_checks/checks/base_checks.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import ABC
2-
from typing import TYPE_CHECKING, Any, ClassVar, Iterator, Optional, Set, Type
2+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterator, Optional, Set, Type
33

44
import django.core.checks
55

@@ -25,10 +25,12 @@ def __init__(
2525
level: Optional[int] = None,
2626
ignore_objects: Optional[Set[Any]] = None,
2727
ignore_types: Optional[set] = None,
28+
skipif: Optional[Callable] = None,
2829
) -> None:
2930
self.level = level or self.level
3031
self.ignore_objects = ignore_objects or set()
3132
self.ignore_types = ignore_types or set()
33+
self.skipif = skipif
3234

3335
def __call__(
3436
self, obj: Any, ast: Optional[DisableCommentProtocol] = None, **kwargs: Any
@@ -39,6 +41,8 @@ def __call__(
3941
yield error
4042

4143
def is_ignored(self, obj: Any) -> bool:
44+
if self.skipif and self.skipif(obj):
45+
return True
4246
return obj in self.ignore_objects or type(obj) in self.ignore_types
4347

4448
def message(

src/extra_checks/checks/model_field_checks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def __call__(
3333
pass
3434

3535
def is_ignored(self, obj: Any) -> bool:
36+
if self.skipif and self.skipif(obj):
37+
return True
3638
return obj.model in self.ignore_objects or type(obj) in self.ignore_types
3739

3840

src/extra_checks/forms.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import importlib
22
import typing
3+
import warnings
34

45
import django.core.checks
56
from django import forms
@@ -35,6 +36,21 @@ def validate(self, value: list) -> None:
3536
self.base_field.validate(val)
3637

3738

39+
class FilterField(forms.Field):
40+
default_error_messages = {
41+
"invalid_callable": _("%(value)s is not valid callable for skipif."),
42+
}
43+
44+
def to_python(self, value: typing.Any) -> typing.Optional[typing.Callable]:
45+
if not value:
46+
return None
47+
if not callable(value):
48+
raise forms.ValidationError(
49+
self.error_messages["invalid_callable"], code="invalid_callable"
50+
)
51+
return value
52+
53+
3854
class UnionField(forms.Field):
3955
default_error_messages = {
4056
"type_invalid": _("%(value)s is not one of the available types."),
@@ -176,6 +192,7 @@ class BaseCheckForm(forms.Form):
176192
required=False,
177193
)
178194
ignore_types = ListField(forms.CharField(), required=False)
195+
skipif = FilterField(required=False)
179196

180197
def clean_level(self) -> typing.Optional[int]:
181198
if self.cleaned_data["level"]:
@@ -195,6 +212,11 @@ def clean_ignore_types(self) -> set:
195212
raise forms.ValidationError(
196213
f"ignore_types contains entry that can't be imported: '{import_path}'."
197214
)
215+
if result:
216+
warnings.warn(
217+
"ignore_types is deprecated and will be removed in version 0.11.0, replace it with skipif option.",
218+
FutureWarning,
219+
)
198220
return result
199221

200222
def clean(self) -> typing.Dict[str, typing.Any]:
@@ -203,6 +225,8 @@ def clean(self) -> typing.Dict[str, typing.Any]:
203225
and not self.cleaned_data["ignore_types"]
204226
):
205227
del self.cleaned_data["ignore_types"]
228+
if "skipif" in self.cleaned_data and not self.cleaned_data["skipif"]:
229+
del self.cleaned_data["skipif"]
206230
return self.cleaned_data
207231

208232

tests/example/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ class Article(models.Model):
2121
author = models.ForeignKey(
2222
Author, related_name="articles", on_delete=models.CASCADE
2323
)
24+
created = models.DateTimeField(auto_now=True)
2425

2526
class Meta:
2627
verbose_name = "Site Article"
28+
get_latest_by = ["created"]
2729

2830

2931
class NestedField(models.Field):

tests/test_ignore.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import django.db.models
12
import pytest
23

34
from extra_checks.checks import model_checks, model_field_checks
@@ -94,3 +95,52 @@ def test_field_ignore_types(test_case):
9495
)
9596
assert len(messages) == 1
9697
assert {m.obj.name for m in messages} == {"file_fail"}
98+
99+
100+
def test_field_skipif(test_case):
101+
def skipif(field, *args, **kwargs):
102+
return isinstance(field, django.db.models.ImageField)
103+
104+
messages = (
105+
test_case.settings(
106+
{
107+
"checks": [
108+
{
109+
"id": model_field_checks.CheckFieldFileUploadTo.Id.value,
110+
"skipif": skipif,
111+
}
112+
],
113+
}
114+
)
115+
.models(models.ModelFieldFileUploadTo)
116+
.check(model_field_checks.CheckFieldFileUploadTo)
117+
.run()
118+
)
119+
assert len(messages) == 1
120+
assert {m.obj.name for m in messages} == {"file_fail"}
121+
122+
123+
def test_model_skipif(test_case):
124+
def skipif(model, *args, **kwargs):
125+
return not any(
126+
isinstance(f, django.db.models.DateTimeField)
127+
for f in model._meta.get_fields()
128+
)
129+
130+
messages = (
131+
test_case.settings(
132+
{
133+
"checks": [
134+
{
135+
"id": model_checks.CheckModelMetaAttribute.Id.value,
136+
"attrs": ["get_latest_by"],
137+
"skipif": skipif,
138+
},
139+
],
140+
}
141+
)
142+
.models(models.Article, models.Author)
143+
.check(model_checks.CheckModelMetaAttribute)
144+
.run()
145+
)
146+
assert len(messages) == 0

0 commit comments

Comments
 (0)