Skip to content

Commit 92fc2cc

Browse files
authored
Merge pull request #4 from kraken-tech/some-improvements
Some improvements
2 parents e0b13ab + 884acc5 commit 92fc2cc

File tree

5 files changed

+235
-2
lines changed

5 files changed

+235
-2
lines changed

django_consistency_enforcer/_view_patterns.py

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

33
import abc
44
import inspect
5+
import pathlib
56
from collections.abc import Callable, Iterator, Sequence
67
from typing import Generic
78

89
import attrs
10+
import django
911
from django import http
1012
from django.views import generic
1113
from typing_extensions import TypeVar
@@ -142,6 +144,12 @@ def exclude_function(
142144
# Ignore code that comes from django itself
143145
# Nothing to be gained from complaining about code that is out of the user's control
144146
return True
147+
elif function.view_class is None and function.module.startswith(
148+
str(pathlib.Path(django.__file__).parent)
149+
):
150+
# Ignore code that comes from django itself
151+
# Nothing to be gained from complaining about code that is out of the user's control
152+
return True
145153

146154
return False
147155

example/djangoexample/urls.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,12 @@
2020

2121
from . import views
2222

23-
urlpatterns = [path("admin/", admin.site.urls), path("", views.index)]
23+
urlpatterns = [
24+
path("admin/", admin.site.urls),
25+
path("", views.index),
26+
path("/with-extra-args", views.with_extra_args),
27+
path("/missing-specific-args/<str:missing>/<int:in_url>", views.missing_specific_args),
28+
path("/wrong-type/<int:should_be_int>", views.wrong_type),
29+
path("/incorrect-view/<int:should_be_int>", views.IncorrectView.as_view()),
30+
path("/correct-view/<str:should_be_str>", views.CorrectView.as_view()),
31+
]

example/djangoexample/views.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django import http
2+
from django.views import generic
3+
4+
5+
def index(request: http.HttpRequest) -> http.HttpResponse:
6+
return http.HttpResponse("")
7+
8+
9+
def with_extra_args(request: http.HttpRequest, *, not_in_url: int) -> http.HttpResponse:
10+
return http.HttpResponse("")
11+
12+
13+
def missing_specific_args(request: http.HttpRequest, *, in_url: int) -> http.HttpResponse:
14+
return http.HttpResponse("")
15+
16+
17+
def wrong_type(request: http.HttpRequest, *, should_be_int: str) -> http.HttpResponse:
18+
return http.HttpResponse("")
19+
20+
21+
class IncorrectView(generic.View):
22+
def get(self, request: http.HttpRequest, *, should_be_int: str) -> http.HttpResponse:
23+
return http.HttpResponse("")
24+
25+
def post(self, request: http.HttpRequest, *, not_in_url: int) -> http.HttpResponse:
26+
return http.HttpResponse("")
27+
28+
29+
class CorrectView(generic.View):
30+
def get(self, request: http.HttpRequest, *, should_be_str: str) -> http.HttpResponse:
31+
return http.HttpResponse("")
32+
33+
def post(self, request: http.HttpRequest, *, should_be_str: str) -> http.HttpResponse:
34+
return http.HttpResponse("")

scripts/test_helpers/django_consistency_enforcer_test_driver/patterns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django_consistency_enforcer import urls as enforcer
22

33

4-
def from_raw_pattern(raw_pattern: enforcer.RawPattern, /) -> enforcer.ViewPattern:
4+
def from_raw_pattern(raw_pattern: enforcer.RawPattern) -> enforcer.ViewPattern:
55
"""
66
Used as a `pattern_maker` in TestRunner instances in the tests.
77
"""
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import importlib.resources
2+
import pathlib
3+
from collections.abc import Iterator
4+
5+
import django_consistency_enforcer_test_driver as test_helpers
6+
import pytest
7+
from django import http
8+
from django.apps import apps
9+
from django.conf import settings
10+
from django.urls import resolvers
11+
12+
from django_consistency_enforcer import errors as enforcer_errors
13+
from django_consistency_enforcer import urls as enforcer
14+
15+
16+
class _CustomInvalidRequestAnnotation(enforcer_errors.InvalidRequestAnnotation):
17+
@property
18+
def expect(self) -> str:
19+
return "http.HttpRequest"
20+
21+
@property
22+
def expanded_note(self) -> Iterator[str]:
23+
yield "In our example we have no specific request annotation"
24+
25+
26+
class _CustomCheckPositionalArgsAreCorrectFunctionScenario[T_Pattern: enforcer.Pattern](
27+
enforcer.CheckPositionalArgsAreCorrectFunctionScenario[T_Pattern]
28+
):
29+
def is_mistyped(
30+
self,
31+
*,
32+
function: enforcer.DispatchFunction,
33+
want_annotation: object,
34+
got_annotation: object,
35+
auth_user_model: type,
36+
name: str,
37+
position: int,
38+
) -> enforcer_errors.MismatchedRequiredArgs.Incorrect | None:
39+
return enforcer_errors.MismatchedRequiredArgs.Incorrect.mistyped(
40+
name=name, want=str(want_annotation), got=got_annotation
41+
)
42+
43+
44+
# The test replaces $PROJECT with absolute path to where this repository is checked out
45+
_expected = r"""
46+
[RequiredArgOnViewNotAlwaysRequiredByPattern]
47+
module = $PROJECT/example/djangoexample/views.py
48+
function = with_extra_args
49+
missing_from_urlpatterns = ['not_in_url']
50+
url patterns >>
51+
0 >
52+
module = $PROJECT/example/djangoexample/urls.py
53+
regex = ^/
54+
1 >
55+
module = $PROJECT/example/djangoexample/urls.py
56+
regex = ^/with\-extra\-args\Z
57+
:: Found arguments on the view that are not provided by any of the patterns that lead to that view
58+
:: You likely want to use Unpack on the kwargs with a NotRequired on these args
59+
:: Or give them default values if you have provided explicit keywords to the function
60+
61+
[RequiredArgOnViewNotAlwaysRequiredByPattern]
62+
module = $PROJECT/example/djangoexample/views.py
63+
class = IncorrectView
64+
method = post
65+
missing_from_urlpatterns = ['not_in_url']
66+
url patterns >>
67+
0 >
68+
module = $PROJECT/example/djangoexample/urls.py
69+
regex = ^/
70+
1 >
71+
module = $PROJECT/example/djangoexample/urls.py
72+
regex = ^/incorrect\-view/(?P<should_be_int>[0-9]+)\Z
73+
:: Found arguments on the view that are not provided by any of the patterns that lead to that view
74+
:: You likely want to use Unpack on the kwargs with a NotRequired on these args
75+
:: Or give them default values if you have provided explicit keywords to the function
76+
77+
[ViewDoesNotAcceptCapturedArg]
78+
Originating:
79+
module = $PROJECT/example/djangoexample/urls.py
80+
regex = ^/missing\-specific\-args/(?P<missing>[^/]+)/(?P<in_url>[0-9]+)\Z
81+
module = $PROJECT/example/djangoexample/views.py
82+
function = missing_specific_args
83+
Missing captured arg: missing
84+
module = $PROJECT/example/djangoexample/urls.py
85+
regex = ^/missing\-specific\-args/(?P<missing>[^/]+)/(?P<in_url>[0-9]+)\Z
86+
:: There are args in the pattern that the view is not aware of
87+
:: You likely want to add those extra arguments to the view!
88+
89+
[ViewDoesNotAcceptCapturedArg]
90+
Originating:
91+
module = $PROJECT/example/djangoexample/urls.py
92+
regex = ^/incorrect\-view/(?P<should_be_int>[0-9]+)\Z
93+
module = $PROJECT/example/djangoexample/views.py
94+
class = IncorrectView
95+
method = post
96+
Missing captured arg: should_be_int
97+
module = $PROJECT/example/djangoexample/urls.py
98+
regex = ^/incorrect\-view/(?P<should_be_int>[0-9]+)\Z
99+
:: There are args in the pattern that the view is not aware of
100+
:: You likely want to add those extra arguments to the view!
101+
102+
[InvalidArgAnnotations]
103+
Originating:
104+
module = $PROJECT/example/djangoexample/urls.py
105+
regex = ^/wrong\-type/(?P<should_be_int>[0-9]+)\Z
106+
module = $PROJECT/example/djangoexample/views.py
107+
function = wrong_type
108+
Found some args that have incorrect annotations:
109+
* Expected 'should_be_int' to be '<class 'int'>', found '<class 'str'>'
110+
:: When we defined url patterns we end up using converters that can change what
111+
:: type the view gets and we want to mirror this in our dispatch related signatures
112+
113+
[InvalidArgAnnotations]
114+
Originating:
115+
module = $PROJECT/example/djangoexample/urls.py
116+
regex = ^/incorrect\-view/(?P<should_be_int>[0-9]+)\Z
117+
module = $PROJECT/example/djangoexample/views.py
118+
class = IncorrectView
119+
method = get
120+
Found some args that have incorrect annotations:
121+
* Expected 'should_be_int' to be '<class 'int'>', found '<class 'str'>'
122+
:: When we defined url patterns we end up using converters that can change what
123+
:: type the view gets and we want to mirror this in our dispatch related signatures
124+
"""
125+
126+
127+
def test_it_works_on_a_django_project() -> None:
128+
try:
129+
test_runner = enforcer.TestRunner.from_raw_patterns(
130+
raw_patterns=enforcer.all_django_patterns(resolver=resolvers.get_resolver()),
131+
raw_pattern_excluder=lambda raw_pattern: False,
132+
pattern_maker=test_helpers.patterns.from_raw_pattern,
133+
)
134+
except enforcer_errors.FoundInvalidPatterns as e:
135+
raise AssertionError(
136+
"Found invalid patterns in ROOT_URLCONF\n\n" + "\n\n".join(e.errors.by_most_repeated)
137+
) from e
138+
139+
auth_user_model = apps.get_model(settings.AUTH_USER_MODEL)
140+
141+
with pytest.raises(enforcer_errors.FoundInvalidPatterns) as exc:
142+
test_runner.run_scenarios(
143+
auth_user_model=auth_user_model,
144+
pattern_scenarios=(
145+
#
146+
# Ensure the request annotation on the class is correct
147+
enforcer.CheckViewClassRequestAnnotationScenario(
148+
acceptable_annotations=(http.HttpRequest,),
149+
acceptable_request_annotation_containers=(http.HttpRequest,),
150+
error_class=_CustomInvalidRequestAnnotation,
151+
),
152+
),
153+
function_scenarios=(
154+
#
155+
# Make sure kwargs has an annotation that makes sense
156+
enforcer.CheckKwargsMustBeAnnotatedFunctionScenario(
157+
# Exit before other checks in case it's a case of the kwargs
158+
# Trying to be an Unpack[SomeTypeDict] and forgetting the Unpack
159+
exit_early=True,
160+
),
161+
#
162+
# Make sure the positional args are correct
163+
_CustomCheckPositionalArgsAreCorrectFunctionScenario[enforcer.ViewPattern](),
164+
#
165+
# Make sure that if the view has a required arg, that this arg is provided by the
166+
# pattern
167+
enforcer.CheckRequiredArgsMatchUrlPatternFunctionScenario(),
168+
#
169+
# Make sure the view accepts every captured argument
170+
enforcer.CheckAcceptsArgsFunctionScenario(),
171+
#
172+
# Make sure the view has the correct annotations
173+
enforcer.CheckHasCorrectAnnotationsFunctionScenario(),
174+
),
175+
)
176+
177+
errors = "\n\n".join(exc.value.errors.by_most_repeated)
178+
expected = _expected.replace(
179+
"$PROJECT",
180+
str(pathlib.Path(str(importlib.resources.files("django_consistency_enforcer"))).parent),
181+
)
182+
183+
pytest.LineMatcher(errors.strip().split("\n")).fnmatch_lines(expected.strip().split("\n"))

0 commit comments

Comments
 (0)