1import inspect
2from collections import Counter
3
4from django.conf import settings
5from django.core.exceptions import ViewDoesNotExist
6
7from . import Error, Tags, Warning, register
8
9
10@register(Tags.urls)
11def check_url_config(app_configs, **kwargs):
12 if getattr(settings, "ROOT_URLCONF", None):
13 from django.urls import get_resolver
14
15 resolver = get_resolver()
16 return check_resolver(resolver)
17 return []
18
19
20def check_resolver(resolver):
21 """
22 Recursively check the resolver.
23 """
24 check_method = getattr(resolver, "check", None)
25 if check_method is not None:
26 return check_method()
27 elif not hasattr(resolver, "resolve"):
28 return get_warning_for_invalid_pattern(resolver)
29 else:
30 return []
31
32
33@register(Tags.urls)
34def check_url_namespaces_unique(app_configs, **kwargs):
35 """
36 Warn if URL namespaces used in applications aren't unique.
37 """
38 if not getattr(settings, "ROOT_URLCONF", None):
39 return []
40
41 from django.urls import get_resolver
42
43 resolver = get_resolver()
44 all_namespaces = _load_all_namespaces(resolver)
45 counter = Counter(all_namespaces)
46 non_unique_namespaces = [n for n, count in counter.items() if count > 1]
47 errors = []
48 for namespace in non_unique_namespaces:
49 errors.append(
50 Warning(
51 "URL namespace '{}' isn't unique. You may not be able to reverse "
52 "all URLs in this namespace".format(namespace),
53 id="urls.W005",
54 )
55 )
56 return errors
57
58
59def _load_all_namespaces(resolver, parents=()):
60 """
61 Recursively load all namespaces from URL patterns.
62 """
63 url_patterns = getattr(resolver, "url_patterns", [])
64 namespaces = [
65 ":".join(parents + (url.namespace,))
66 for url in url_patterns
67 if getattr(url, "namespace", None) is not None
68 ]
69 for pattern in url_patterns:
70 namespace = getattr(pattern, "namespace", None)
71 current = parents
72 if namespace is not None:
73 current += (namespace,)
74 namespaces.extend(_load_all_namespaces(pattern, current))
75 return namespaces
76
77
78def get_warning_for_invalid_pattern(pattern):
79 """
80 Return a list containing a warning that the pattern is invalid.
81
82 describe_pattern() cannot be used here, because we cannot rely on the
83 urlpattern having regex or name attributes.
84 """
85 if isinstance(pattern, str):
86 hint = (
87 "Try removing the string '{}'. The list of urlpatterns should not "
88 "have a prefix string as the first element.".format(pattern)
89 )
90 elif isinstance(pattern, tuple):
91 hint = "Try using path() instead of a tuple."
92 else:
93 hint = None
94
95 return [
96 Error(
97 "Your URL pattern {!r} is invalid. Ensure that urlpatterns is a list "
98 "of path() and/or re_path() instances.".format(pattern),
99 hint=hint,
100 id="urls.E004",
101 )
102 ]
103
104
105@register(Tags.urls)
106def check_url_settings(app_configs, **kwargs):
107 errors = []
108 for name in ("STATIC_URL", "MEDIA_URL"):
109 value = getattr(settings, name)
110 if value and not value.endswith("/"):
111 errors.append(E006(name))
112 return errors
113
114
115def E006(name):
116 return Error(
117 "The {} setting must end with a slash.".format(name),
118 id="urls.E006",
119 )
120
121
122@register(Tags.urls)
123def check_custom_error_handlers(app_configs, **kwargs):
124 if not getattr(settings, "ROOT_URLCONF", None):
125 return []
126
127 from django.urls import get_resolver
128
129 resolver = get_resolver()
130
131 errors = []
132 # All handlers take (request, exception) arguments except handler500
133 # which takes (request).
134 for status_code, num_parameters in [(400, 2), (403, 2), (404, 2), (500, 1)]:
135 try:
136 handler = resolver.resolve_error_handler(status_code)
137 except (ImportError, ViewDoesNotExist) as e:
138 path = getattr(resolver.urlconf_module, "handler%s" % status_code)
139 msg = (
140 "The custom handler{status_code} view '{path}' could not be "
141 "imported."
142 ).format(status_code=status_code, path=path)
143 errors.append(Error(msg, hint=str(e), id="urls.E008"))
144 continue
145 signature = inspect.signature(handler)
146 args = [None] * num_parameters
147 try:
148 signature.bind(*args)
149 except TypeError:
150 msg = (
151 "The custom handler{status_code} view '{path}' does not "
152 "take the correct number of arguments ({args})."
153 ).format(
154 status_code=status_code,
155 path=handler.__module__ + "." + handler.__qualname__,
156 args="request, exception" if num_parameters == 2 else "request",
157 )
158 errors.append(Error(msg, id="urls.E007"))
159 return errors