Skip to content

Commit a76234e

Browse files
committed
Tests
1 parent f456b7a commit a76234e

File tree

15 files changed

+224
-36
lines changed

15 files changed

+224
-36
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ graft .github
33

44

55
include src/extra_checks/py.typed
6+
include conftest.py
67
recursive-include tests *.py
78

89
global-exclude __pycache__

conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
from extra_checks.checks.self_checks import CheckConfig
3+
from extra_checks.controller import Registry
4+
5+
6+
@pytest.fixture
7+
def registry() -> Registry:
8+
registry = Registry()
9+
registry._register(["extra_checks_selfcheck"], CheckConfig)
10+
return registry
11+
12+
13+
@pytest.fixture
14+
def use_models(monkeypatch):
15+
def f(*models):
16+
monkeypatch.setattr(
17+
"extra_checks.controller.ChecksController._get_models_to_check",
18+
lambda *a: (m for m in models),
19+
)
20+
21+
return f

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[pytest]
22
addopts = -p no:doctest
33
--cov=extra_checks
4-
--cov-fail-under 60
4+
--cov-branch
55
--ds=tests.settings
66
django_find_project = false

src/extra_checks/ast.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ def verbose_name(self) -> Union[None, ast.Constant, ast.Call]:
122122
node = self.args[0]
123123
if isinstance(node, ast.Call) and hasattr(node.func, "id"):
124124
return node
125+
elif isinstance(node, (ast.Constant, ast.Str)):
126+
return node
125127
return None
126128

127129
@cached_property

src/extra_checks/checks/model_checks.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
from .base_checks import BaseCheck
1414

1515

16-
class CheckAttrsForm(BaseCheckForm):
16+
class AttrsForm(BaseCheckForm):
1717
attrs = ListField(forms.CharField())
1818

1919

20-
class CheckMetaAttrsForm(BaseCheckForm):
20+
class MetaAttrsForm(BaseCheckForm):
2121
attrs = forms.MultipleChoiceField(choices=[(o, o) for o in META_ATTRS])
2222

2323

24-
class ModelCheck(BaseCheck):
24+
class CheckModel(BaseCheck):
2525
@abstractmethod
2626
def apply(
2727
self, model: Type[models.Model], model_ast: ModelAST
@@ -30,9 +30,9 @@ def apply(
3030

3131

3232
@register(django.core.checks.Tags.models)
33-
class CheckModelAttribute(ModelCheck):
33+
class CheckModelAttribute(CheckModel):
3434
Id = CheckId.X010
35-
settings_form_class = CheckAttrsForm
35+
settings_form_class = AttrsForm
3636

3737
def __init__(self, attrs: List[str], **kwargs: Any) -> None:
3838
self.attrs = attrs
@@ -55,9 +55,9 @@ def apply(
5555

5656

5757
@register(django.core.checks.Tags.models)
58-
class CheckModelMetaAttribute(ModelCheck):
58+
class CheckModelMetaAttribute(CheckModel):
5959
Id = CheckId.X011
60-
settings_form_class = CheckMetaAttrsForm
60+
settings_form_class = MetaAttrsForm
6161

6262
def __init__(self, attrs: List[str], **kwargs: Any) -> None:
6363
self.attrs = attrs

src/extra_checks/checks/model_field_checks.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .base_checks import BaseCheck, BaseCheckMixin
1414

1515

16-
class ModelFieldCheck(BaseCheck):
16+
class CheckModelField(BaseCheck):
1717
@abstractmethod
1818
def apply(
1919
self,
@@ -25,12 +25,12 @@ def apply(
2525
raise NotImplementedError()
2626

2727

28-
class CheckGettTextFuncForm(BaseCheckForm):
28+
class GettTextFuncForm(BaseCheckForm):
2929
gettext_func = forms.CharField(required=False)
3030

3131

3232
class GetTextMixin(BaseCheckMixin):
33-
settings_form_class = CheckGettTextFuncForm
33+
settings_form_class = GettTextFuncForm
3434

3535
def __init__(self, gettext_func: str, **kwargs: Any) -> None:
3636
self.gettext_func = gettext_func or "_"
@@ -44,7 +44,7 @@ def _is_gettext_node(self, node: ast.AST) -> bool:
4444

4545

4646
@register(django.core.checks.Tags.models)
47-
class CheckFieldVerboseName(ModelFieldCheck):
47+
class CheckFieldVerboseName(CheckModelField):
4848
Id = CheckId.X050
4949

5050
def apply(
@@ -59,7 +59,7 @@ def apply(
5959

6060

6161
@register(django.core.checks.Tags.models)
62-
class CheckFieldVerboseNameGettext(GetTextMixin, ModelFieldCheck):
62+
class CheckFieldVerboseNameGettext(GetTextMixin, CheckModelField):
6363
Id = CheckId.X051
6464

6565
def apply(
@@ -74,7 +74,7 @@ def apply(
7474

7575

7676
@register(django.core.checks.Tags.models)
77-
class CheckFieldVerboseNameGettextCase(GetTextMixin, ModelFieldCheck):
77+
class CheckFieldVerboseNameGettextCase(GetTextMixin, CheckModelField):
7878
Id = CheckId.X052
7979

8080
def apply(
@@ -93,7 +93,7 @@ def apply(
9393

9494

9595
@register(django.core.checks.Tags.models)
96-
class CheckFieldHelpTextGettext(GetTextMixin, ModelFieldCheck):
96+
class CheckFieldHelpTextGettext(GetTextMixin, CheckModelField):
9797
Id = CheckId.X053
9898

9999
def apply(
@@ -108,7 +108,7 @@ def apply(
108108

109109

110110
@register(django.core.checks.Tags.models)
111-
class CheckFieldFileUploadTo(ModelFieldCheck):
111+
class CheckFieldFileUploadTo(CheckModelField):
112112
Id = CheckId.X054
113113

114114
def apply(
@@ -124,7 +124,7 @@ def apply(
124124

125125

126126
@register(django.core.checks.Tags.models)
127-
class CheckFieldTextNull(ModelFieldCheck):
127+
class CheckFieldTextNull(CheckModelField):
128128
Id = CheckId.X055
129129

130130
def apply(
@@ -141,7 +141,7 @@ def apply(
141141

142142

143143
@register(django.core.checks.Tags.models)
144-
class CheckFieldNullBoolean(ModelFieldCheck):
144+
class CheckFieldNullBoolean(CheckModelField):
145145
Id = CheckId.X056
146146

147147
def apply(
@@ -156,7 +156,7 @@ def apply(
156156

157157

158158
@register(django.core.checks.Tags.models)
159-
class CheckFieldNullFalse(ModelFieldCheck):
159+
class CheckFieldNullFalse(CheckModelField):
160160
Id = CheckId.X057
161161

162162
def apply(
@@ -178,7 +178,7 @@ class CheckFieldForeignKeyIndexForm(BaseCheckForm):
178178

179179

180180
@register(django.core.checks.Tags.models)
181-
class CheckFieldForeignKeyIndex(ModelFieldCheck):
181+
class CheckFieldForeignKeyIndex(CheckModelField):
182182
Id = CheckId.X058
183183
settings_form_class = CheckFieldForeignKeyIndexForm
184184

src/extra_checks/checks/self_checks.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,23 @@
77
from .base_checks import BaseCheck
88

99

10+
def dict_to_text(data: dict, indent_level: int = 0) -> str:
11+
output = []
12+
for field, errors in data.items():
13+
if not errors:
14+
continue
15+
output.append(f"{' '*indent_level * 2}* {field}")
16+
if isinstance(errors, dict):
17+
output.append(dict_to_text(errors, indent_level + 1))
18+
else:
19+
output.append(
20+
"\n".join(f"{' '*(indent_level * 2 + 2)}* {e}" for e in errors)
21+
)
22+
return "\n".join(output)
23+
24+
1025
@register("extra_checks_selfcheck")
11-
class CheckX001(BaseCheck):
26+
class CheckConfig(BaseCheck):
1227
Id = CheckId.X001
1328
level = django.core.checks.CRITICAL
1429

@@ -19,5 +34,5 @@ def apply(
1934
yield self.message(
2035
"Invalid EXTRA_CHECKS config.",
2136
hint="Fix EXTRA_CHECKS in your settings. Errors:\n"
22-
+ (obj.errors.as_text() if obj.errors else "No details."),
37+
+ (dict_to_text(obj.errors) if obj.errors else "No details."),
2338
)

src/extra_checks/controller.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import site
2+
from functools import partial
23
from typing import (
34
TYPE_CHECKING,
45
Any,
@@ -35,17 +36,24 @@ class Registry:
3536
def __init__(self) -> None:
3637
self.checks: Dict["Type[BaseCheck]", Sequence[str]] = {}
3738

38-
def register(self, *tags: str) -> Callable[["Type[BaseCheck]"], "Type[BaseCheck]"]:
39-
def inner(check_class: "Type[BaseCheck]") -> "Type[BaseCheck]":
40-
self.checks[check_class] = tags
41-
return check_class
39+
def _register(
40+
self, tags: List[str], check_class: "Type[BaseCheck]"
41+
) -> "Type[BaseCheck]":
42+
self.checks[check_class] = tags
43+
return check_class
4244

43-
return inner
45+
def register(self, *tags: str) -> Callable[["Type[BaseCheck]"], "Type[BaseCheck]"]:
46+
return partial(self._register, tags)
4447

45-
def finish(self) -> None:
48+
def finish(self) -> "ChecksController":
4649
controller = ChecksController.create(self.checks)
4750

4851
def f(callback: Callable) -> Callable:
52+
"""
53+
Django does `check.tags = ...`, callback is a method of the controller
54+
and setattr will fail on it so we wrap method with a function.
55+
"""
56+
4957
def inner(*args: Any, **kwargs: Any) -> Any:
5058
return callback(*args, **kwargs)
5159

@@ -58,6 +66,8 @@ def inner(*args: Any, **kwargs: Any) -> Any:
5866
f(controller.check_models), django.core.checks.Tags.models
5967
)
6068

69+
return controller
70+
6171

6272
class ChecksController:
6373
def __init__(
@@ -100,7 +110,7 @@ def is_healthy(self) -> bool:
100110
return not self.errors
101111

102112
def check_extra_checks_health(
103-
self, app_configs: Optional[List[Any]], **kwargs: Any
113+
self, app_configs: Optional[List[Any]] = None, **kwargs: Any
104114
) -> Iterator[django.core.checks.CheckMessage]:
105115
for check in self.registered_checks.get("extra_checks_selfcheck", []):
106116
yield from check(self)
@@ -117,14 +127,14 @@ def _get_models_to_check(
117127
yield from app.get_models()
118128

119129
def check_models(
120-
self, app_configs: Optional[List[Any]], **kwargs: Any
130+
self, app_configs: Optional[List[Any]] = None, **kwargs: Any
121131
) -> Iterator[Any]:
122-
from .checks import ModelFieldCheck
132+
from .checks import CheckModelField
123133

124134
model_checks = []
125135
field_checks = []
126136
for check in self.registered_checks.get(django.core.checks.Tags.models, []):
127-
if isinstance(check, ModelFieldCheck):
137+
if isinstance(check, CheckModelField):
128138
field_checks.append(check)
129139
else:
130140
model_checks.append(check)

tests/example/models.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1+
from django.contrib.sites.models import Site
12
from django.db import models
23

34

4-
class CheckOne(models.Model):
5-
title = models.CharField(max_length=100)
5+
class Article(models.Model):
6+
site = models.ForeignKey(Site, verbose_name="site", on_delete=models.CASCADE)
7+
title = models.CharField("article title", max_length=100)
8+
text = models.TextField(verbose_name="article text")
9+
10+
class Meta:
11+
verbose_name = "Site Article"
12+
13+
14+
class Author(models.Model):
15+
first_name = models.CharField(max_length=100)
16+
last_name = models.CharField(max_length=100)

tests/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
ALLOWED_HOSTS = ["*"]
44

5-
INSTALLED_APPS = ["tests.example"]
5+
INSTALLED_APPS = ["django.contrib.sites", "tests.example"]
66

77
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}}
88

0 commit comments

Comments
 (0)