plain 0.67.0__py3-none-any.whl → 0.68.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/CHANGELOG.md +28 -0
- plain/chores/registry.py +1 -18
- plain/cli/core.py +2 -2
- plain/cli/preflight.py +219 -98
- plain/cli/registry.py +1 -18
- plain/packages/registry.py +14 -0
- plain/preflight/README.md +3 -3
- plain/preflight/__init__.py +4 -24
- plain/preflight/checks.py +10 -0
- plain/preflight/files.py +17 -13
- plain/preflight/registry.py +64 -57
- plain/preflight/results.py +29 -0
- plain/preflight/security.py +58 -66
- plain/preflight/urls.py +7 -48
- plain/runtime/global_settings.py +5 -5
- plain/templates/jinja/__init__.py +2 -23
- plain/urls/patterns.py +21 -21
- plain/urls/resolvers.py +4 -4
- {plain-0.67.0.dist-info → plain-0.68.1.dist-info}/METADATA +1 -1
- {plain-0.67.0.dist-info → plain-0.68.1.dist-info}/RECORD +23 -22
- {plain-0.67.0.dist-info → plain-0.68.1.dist-info}/entry_points.txt +1 -0
- plain/preflight/messages.py +0 -81
- {plain-0.67.0.dist-info → plain-0.68.1.dist-info}/WHEEL +0 -0
- {plain-0.67.0.dist-info → plain-0.68.1.dist-info}/licenses/LICENSE +0 -0
plain/preflight/registry.py
CHANGED
@@ -1,72 +1,79 @@
|
|
1
|
-
from plain.
|
2
|
-
from plain.utils.itercompat import is_iterable
|
1
|
+
from plain.runtime import settings
|
3
2
|
|
4
3
|
|
5
4
|
class CheckRegistry:
|
6
5
|
def __init__(self):
|
7
|
-
self.
|
8
|
-
self.deployment_checks = set()
|
6
|
+
self.checks = {} # name -> (check_class, deploy)
|
9
7
|
|
10
|
-
def
|
11
|
-
"""
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
Example::
|
17
|
-
|
18
|
-
registry = CheckRegistry()
|
19
|
-
@registry.register('mytag', 'anothertag')
|
20
|
-
def my_check(package_configs, **kwargs):
|
21
|
-
# ... perform checks and collect `errors` ...
|
22
|
-
return errors
|
23
|
-
# or
|
24
|
-
registry.register(my_check, 'mytag', 'anothertag')
|
25
|
-
"""
|
26
|
-
|
27
|
-
def inner(check):
|
28
|
-
if not func_accepts_kwargs(check):
|
29
|
-
raise TypeError(
|
30
|
-
"Check functions must accept keyword arguments (**kwargs)."
|
31
|
-
)
|
32
|
-
checks = self.deployment_checks if deploy else self.registered_checks
|
33
|
-
checks.add(check)
|
34
|
-
return check
|
35
|
-
|
36
|
-
if callable(check):
|
37
|
-
return inner(check)
|
38
|
-
else:
|
39
|
-
return inner
|
8
|
+
def register_check(self, check_class, name, deploy=False):
|
9
|
+
"""Register a check class with a unique name."""
|
10
|
+
if name in self.checks:
|
11
|
+
raise ValueError(f"Check {name} already registered")
|
12
|
+
self.checks[name] = (check_class, deploy)
|
40
13
|
|
41
14
|
def run_checks(
|
42
15
|
self,
|
43
|
-
|
44
|
-
include_deployment_checks=False,
|
45
|
-
database=False,
|
16
|
+
include_deploy_checks=False,
|
46
17
|
):
|
47
18
|
"""
|
48
|
-
Run all registered checks and
|
19
|
+
Run all registered checks and yield (check_class, name, results) tuples.
|
49
20
|
"""
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
21
|
+
# Validate silenced check names
|
22
|
+
silenced_checks = settings.PREFLIGHT_SILENCED_CHECKS
|
23
|
+
unknown_silenced = set(silenced_checks) - set(self.checks.keys())
|
24
|
+
if unknown_silenced:
|
25
|
+
unknown_names = ", ".join(sorted(unknown_silenced))
|
26
|
+
raise ValueError(
|
27
|
+
f"Unknown check names in PREFLIGHT_SILENCED_CHECKS: {unknown_names}. "
|
28
|
+
"Check for typos or remove outdated check names."
|
29
|
+
)
|
30
|
+
|
31
|
+
for name, (check_class, deploy) in sorted(self.checks.items()):
|
32
|
+
# Skip silenced checks
|
33
|
+
if name in silenced_checks:
|
34
|
+
continue
|
35
|
+
|
36
|
+
# Skip deployment checks if not requested
|
37
|
+
if deploy and not include_deploy_checks:
|
38
|
+
continue
|
39
|
+
|
40
|
+
# Instantiate and run check
|
41
|
+
check = check_class()
|
42
|
+
results = check.run()
|
43
|
+
yield check_class, name, results
|
44
|
+
|
45
|
+
def get_checks(self, include_deploy_checks=False):
|
46
|
+
"""Get list of (check_class, name) tuples."""
|
47
|
+
result = []
|
48
|
+
for name, (check_class, deploy) in self.checks.items():
|
49
|
+
if deploy and not include_deploy_checks:
|
50
|
+
continue
|
51
|
+
result.append((check_class, name))
|
52
|
+
return result
|
68
53
|
|
69
54
|
|
70
55
|
checks_registry = CheckRegistry()
|
71
|
-
|
56
|
+
|
57
|
+
|
58
|
+
def register_check(name: str, *, deploy: bool = False):
|
59
|
+
"""
|
60
|
+
Decorator to register a check class.
|
61
|
+
|
62
|
+
Usage:
|
63
|
+
@register_check("security.secret_key", deploy=True)
|
64
|
+
class CheckSecretKey(PreflightCheck):
|
65
|
+
pass
|
66
|
+
|
67
|
+
@register_check("files.upload_temp_dir")
|
68
|
+
class CheckUploadTempDir(PreflightCheck):
|
69
|
+
pass
|
70
|
+
"""
|
71
|
+
|
72
|
+
def wrapper(cls):
|
73
|
+
checks_registry.register_check(cls, name=name, deploy=deploy)
|
74
|
+
return cls
|
75
|
+
|
76
|
+
return wrapper
|
77
|
+
|
78
|
+
|
72
79
|
run_checks = checks_registry.run_checks
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from plain.runtime import settings
|
2
|
+
|
3
|
+
|
4
|
+
class PreflightResult:
|
5
|
+
def __init__(self, *, fix: str, id: str, obj=None, warning: bool = False):
|
6
|
+
self.fix = fix
|
7
|
+
self.obj = obj
|
8
|
+
self.id = id
|
9
|
+
self.warning = warning
|
10
|
+
|
11
|
+
def __eq__(self, other):
|
12
|
+
return isinstance(other, self.__class__) and all(
|
13
|
+
getattr(self, attr) == getattr(other, attr)
|
14
|
+
for attr in ["fix", "obj", "id", "warning"]
|
15
|
+
)
|
16
|
+
|
17
|
+
def __str__(self):
|
18
|
+
if self.obj is None:
|
19
|
+
obj = ""
|
20
|
+
elif hasattr(self.obj, "_meta") and hasattr(self.obj._meta, "label"):
|
21
|
+
# Duck type for model objects - use their meta label
|
22
|
+
obj = self.obj._meta.label
|
23
|
+
else:
|
24
|
+
obj = str(self.obj)
|
25
|
+
id_part = f"({self.id}) " if self.id else ""
|
26
|
+
return f"{obj}: {id_part}{self.fix}"
|
27
|
+
|
28
|
+
def is_silenced(self):
|
29
|
+
return self.id and self.id in settings.PREFLIGHT_SILENCED_RESULTS
|
plain/preflight/security.py
CHANGED
@@ -1,21 +1,12 @@
|
|
1
|
-
from plain.exceptions import ImproperlyConfigured
|
2
1
|
from plain.runtime import settings
|
3
2
|
|
4
|
-
from .
|
3
|
+
from .checks import PreflightCheck
|
5
4
|
from .registry import register_check
|
5
|
+
from .results import PreflightResult
|
6
6
|
|
7
7
|
SECRET_KEY_MIN_LENGTH = 50
|
8
8
|
SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5
|
9
9
|
|
10
|
-
SECRET_KEY_WARNING_MSG = (
|
11
|
-
f"Your %s has less than {SECRET_KEY_MIN_LENGTH} characters or less than "
|
12
|
-
f"{SECRET_KEY_MIN_UNIQUE_CHARACTERS} unique characters. Please generate "
|
13
|
-
f"a long and random value, otherwise many of Plain's security-critical "
|
14
|
-
f"features will be vulnerable to attack."
|
15
|
-
)
|
16
|
-
|
17
|
-
W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
|
18
|
-
|
19
10
|
|
20
11
|
def _check_secret_key(secret_key):
|
21
12
|
return (
|
@@ -24,66 +15,67 @@ def _check_secret_key(secret_key):
|
|
24
15
|
)
|
25
16
|
|
26
17
|
|
27
|
-
@register_check(deploy=True)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
)
|
43
|
-
]
|
44
|
-
)
|
18
|
+
@register_check(name="security.secret_key", deploy=True)
|
19
|
+
class CheckSecretKey(PreflightCheck):
|
20
|
+
"""Validates that SECRET_KEY is long and random enough for security."""
|
21
|
+
|
22
|
+
def run(self):
|
23
|
+
if not _check_secret_key(settings.SECRET_KEY):
|
24
|
+
return [
|
25
|
+
PreflightResult(
|
26
|
+
fix=f"SECRET_KEY is too weak (needs {SECRET_KEY_MIN_LENGTH}+ characters, "
|
27
|
+
f"{SECRET_KEY_MIN_UNIQUE_CHARACTERS}+ unique). Generate a new long random value or "
|
28
|
+
f"Plain's security features will be vulnerable to attack.",
|
29
|
+
id="security.secret_key_weak",
|
30
|
+
)
|
31
|
+
]
|
32
|
+
return []
|
45
33
|
|
46
34
|
|
47
|
-
@register_check(deploy=True)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
else:
|
55
|
-
for index, key in enumerate(fallbacks):
|
35
|
+
@register_check(name="security.secret_key_fallbacks", deploy=True)
|
36
|
+
class CheckSecretKeyFallbacks(PreflightCheck):
|
37
|
+
"""Validates that SECRET_KEY_FALLBACKS are long and random enough for security."""
|
38
|
+
|
39
|
+
def run(self):
|
40
|
+
errors = []
|
41
|
+
for index, key in enumerate(settings.SECRET_KEY_FALLBACKS):
|
56
42
|
if not _check_secret_key(key):
|
57
|
-
|
58
|
-
|
43
|
+
errors.append(
|
44
|
+
PreflightResult(
|
45
|
+
fix=f"SECRET_KEY_FALLBACKS[{index}] is too weak (needs {SECRET_KEY_MIN_LENGTH}+ characters, "
|
46
|
+
f"{SECRET_KEY_MIN_UNIQUE_CHARACTERS}+ unique). Generate a new long random value or "
|
47
|
+
f"Plain's security features will be vulnerable to attack.",
|
48
|
+
id="security.secret_key_fallback_weak",
|
49
|
+
)
|
59
50
|
)
|
60
|
-
|
51
|
+
return errors
|
61
52
|
|
62
53
|
|
63
|
-
@register_check(deploy=True)
|
64
|
-
|
65
|
-
|
66
|
-
return (
|
67
|
-
[]
|
68
|
-
if passed_check
|
69
|
-
else [
|
70
|
-
Warning(
|
71
|
-
"You should not have DEBUG set to True in deployment.",
|
72
|
-
id="security.W018",
|
73
|
-
)
|
74
|
-
]
|
75
|
-
)
|
54
|
+
@register_check(name="security.debug", deploy=True)
|
55
|
+
class CheckDebug(PreflightCheck):
|
56
|
+
"""Ensures DEBUG is False in production deployment."""
|
76
57
|
|
58
|
+
def run(self):
|
59
|
+
if settings.DEBUG:
|
60
|
+
return [
|
61
|
+
PreflightResult(
|
62
|
+
fix="DEBUG is True in deployment. Set DEBUG=False to prevent exposing sensitive information.",
|
63
|
+
id="security.debug_enabled_in_production",
|
64
|
+
)
|
65
|
+
]
|
66
|
+
return []
|
77
67
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
68
|
+
|
69
|
+
@register_check(name="security.allowed_hosts", deploy=True)
|
70
|
+
class CheckAllowedHosts(PreflightCheck):
|
71
|
+
"""Ensures ALLOWED_HOSTS is not empty in production deployment."""
|
72
|
+
|
73
|
+
def run(self):
|
74
|
+
if not settings.ALLOWED_HOSTS:
|
75
|
+
return [
|
76
|
+
PreflightResult(
|
77
|
+
fix="ALLOWED_HOSTS is empty in deployment. Add your domain(s) to prevent host header attacks.",
|
78
|
+
id="security.allowed_hosts_empty",
|
79
|
+
)
|
80
|
+
]
|
81
|
+
return []
|
plain/preflight/urls.py
CHANGED
@@ -1,54 +1,13 @@
|
|
1
|
-
from
|
1
|
+
from .checks import PreflightCheck
|
2
|
+
from .registry import register_check
|
2
3
|
|
3
|
-
from . import Error, register_check
|
4
4
|
|
5
|
+
@register_check("urls.config")
|
6
|
+
class CheckUrlConfig(PreflightCheck):
|
7
|
+
"""Validates the URL configuration for common issues."""
|
5
8
|
|
6
|
-
|
7
|
-
def check_url_config(package_configs, **kwargs):
|
8
|
-
if getattr(settings, "URLS_ROUTER", None):
|
9
|
+
def run(self):
|
9
10
|
from plain.urls import get_resolver
|
10
11
|
|
11
12
|
resolver = get_resolver()
|
12
|
-
return
|
13
|
-
|
14
|
-
return []
|
15
|
-
|
16
|
-
|
17
|
-
def check_resolver(resolver):
|
18
|
-
"""
|
19
|
-
Recursively check the resolver.
|
20
|
-
"""
|
21
|
-
check_method = getattr(resolver, "check", None)
|
22
|
-
if check_method is not None:
|
23
|
-
return check_method()
|
24
|
-
elif not hasattr(resolver, "resolve"):
|
25
|
-
return get_warning_for_invalid_pattern(resolver)
|
26
|
-
else:
|
27
|
-
return []
|
28
|
-
|
29
|
-
|
30
|
-
def get_warning_for_invalid_pattern(pattern):
|
31
|
-
"""
|
32
|
-
Return a list containing a warning that the pattern is invalid.
|
33
|
-
|
34
|
-
describe_pattern() cannot be used here, because we cannot rely on the
|
35
|
-
urlpattern having regex or name attributes.
|
36
|
-
"""
|
37
|
-
if isinstance(pattern, str):
|
38
|
-
hint = (
|
39
|
-
f"Try removing the string '{pattern}'. The list of urlpatterns should not "
|
40
|
-
"have a prefix string as the first element."
|
41
|
-
)
|
42
|
-
elif isinstance(pattern, tuple):
|
43
|
-
hint = "Try using path() instead of a tuple."
|
44
|
-
else:
|
45
|
-
hint = None
|
46
|
-
|
47
|
-
return [
|
48
|
-
Error(
|
49
|
-
f"Your URL pattern {pattern!r} is invalid. Ensure that urlpatterns is a list "
|
50
|
-
"of path() and/or re_path() instances.",
|
51
|
-
hint=hint,
|
52
|
-
id="urls.E004",
|
53
|
-
)
|
54
|
-
]
|
13
|
+
return resolver.preflight()
|
plain/runtime/global_settings.py
CHANGED
@@ -152,11 +152,11 @@ ASSETS_BASE_URL: str = ""
|
|
152
152
|
|
153
153
|
# MARK: Preflight Checks
|
154
154
|
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
#
|
159
|
-
|
155
|
+
# Silence checks by name
|
156
|
+
PREFLIGHT_SILENCED_CHECKS: list[str] = []
|
157
|
+
|
158
|
+
# Silence specific check results by id
|
159
|
+
PREFLIGHT_SILENCED_RESULTS: list[str] = []
|
160
160
|
|
161
161
|
# MARK: Templates
|
162
162
|
|
@@ -1,6 +1,3 @@
|
|
1
|
-
import importlib.util
|
2
|
-
from importlib import import_module
|
3
|
-
|
4
1
|
from plain.packages import packages_registry
|
5
2
|
from plain.runtime import settings
|
6
3
|
from plain.utils.functional import LazyObject
|
@@ -10,10 +7,6 @@ from .environments import DefaultEnvironment, get_template_dirs
|
|
10
7
|
|
11
8
|
|
12
9
|
class JinjaEnvironment(LazyObject):
|
13
|
-
def __init__(self, *args, **kwargs):
|
14
|
-
self.__dict__["_imported_modules"] = set()
|
15
|
-
super().__init__(*args, **kwargs)
|
16
|
-
|
17
10
|
def _setup(self):
|
18
11
|
environment_setting = settings.TEMPLATES_JINJA_ENVIRONMENT
|
19
12
|
|
@@ -25,22 +18,8 @@ class JinjaEnvironment(LazyObject):
|
|
25
18
|
# We have to set _wrapped before we trigger the autoloading of "register" commands
|
26
19
|
self._wrapped = env
|
27
20
|
|
28
|
-
|
29
|
-
|
30
|
-
import_name = f"{package_config.name}.templates"
|
31
|
-
if import_name in self._imported_modules:
|
32
|
-
continue
|
33
|
-
if importlib.util.find_spec(import_name) is None:
|
34
|
-
continue
|
35
|
-
import_module(import_name)
|
36
|
-
self._imported_modules.add(import_name)
|
37
|
-
|
38
|
-
# Autoload template helpers from the local ``app`` package if present
|
39
|
-
import_name = "app.templates"
|
40
|
-
if import_name not in self._imported_modules:
|
41
|
-
if importlib.util.find_spec(import_name) is not None:
|
42
|
-
import_module(import_name)
|
43
|
-
self._imported_modules.add(import_name)
|
21
|
+
# Autoload template helpers using the registry method
|
22
|
+
packages_registry.autodiscover_modules("templates", include_app=True)
|
44
23
|
|
45
24
|
|
46
25
|
environment = JinjaEnvironment()
|
plain/urls/patterns.py
CHANGED
@@ -3,7 +3,7 @@ import string
|
|
3
3
|
|
4
4
|
from plain.exceptions import ImproperlyConfigured
|
5
5
|
from plain.internal import internalcode
|
6
|
-
from plain.preflight import
|
6
|
+
from plain.preflight import PreflightResult
|
7
7
|
from plain.runtime import settings
|
8
8
|
from plain.utils.regex_helper import _lazy_re_compile
|
9
9
|
|
@@ -33,11 +33,10 @@ class CheckURLMixin:
|
|
33
33
|
if regex_pattern.startswith(("/", "^/", "^\\/")) and not regex_pattern.endswith(
|
34
34
|
"/"
|
35
35
|
):
|
36
|
-
warning =
|
37
|
-
f"
|
38
|
-
|
39
|
-
"
|
40
|
-
id="urls.W002",
|
36
|
+
warning = PreflightResult(
|
37
|
+
fix=f"URL pattern {self.describe()} starts with unnecessary '/'. Remove the leading slash.",
|
38
|
+
warning=True,
|
39
|
+
id="urls.pattern_starts_with_slash",
|
41
40
|
)
|
42
41
|
return [warning]
|
43
42
|
else:
|
@@ -68,7 +67,7 @@ class RegexPattern(CheckURLMixin):
|
|
68
67
|
return path[match.end() :], args, kwargs
|
69
68
|
return None
|
70
69
|
|
71
|
-
def
|
70
|
+
def preflight(self):
|
72
71
|
warnings = []
|
73
72
|
warnings.extend(self._check_pattern_startswith_slash())
|
74
73
|
if not self._is_endpoint:
|
@@ -79,11 +78,10 @@ class RegexPattern(CheckURLMixin):
|
|
79
78
|
regex_pattern = self.regex.pattern
|
80
79
|
if regex_pattern.endswith("$") and not regex_pattern.endswith(r"\$"):
|
81
80
|
return [
|
82
|
-
|
83
|
-
f"
|
84
|
-
|
85
|
-
"
|
86
|
-
id="urls.W001",
|
81
|
+
PreflightResult(
|
82
|
+
fix=f"Include pattern {self.describe()} ends with '$' which prevents URL inclusion. Remove the dollar sign.",
|
83
|
+
warning=True,
|
84
|
+
id="urls.include_pattern_ends_with_dollar",
|
87
85
|
)
|
88
86
|
]
|
89
87
|
else:
|
@@ -174,16 +172,17 @@ class RoutePattern(CheckURLMixin):
|
|
174
172
|
return path[match.end() :], (), kwargs
|
175
173
|
return None
|
176
174
|
|
177
|
-
def
|
175
|
+
def preflight(self):
|
178
176
|
warnings = self._check_pattern_startswith_slash()
|
179
177
|
route = self._route
|
180
178
|
if "(?P<" in route or route.startswith("^") or route.endswith("$"):
|
181
179
|
warnings.append(
|
182
|
-
|
183
|
-
f"Your URL pattern {self.describe()} has a route that contains '(?P<', begins "
|
180
|
+
PreflightResult(
|
181
|
+
fix=f"Your URL pattern {self.describe()} has a route that contains '(?P<', begins "
|
184
182
|
"with a '^', or ends with a '$'. This was likely an oversight "
|
185
183
|
"when migrating to plain.urls.path().",
|
186
|
-
|
184
|
+
warning=True,
|
185
|
+
id="urls.path_migration_warning",
|
187
186
|
)
|
188
187
|
)
|
189
188
|
return warnings
|
@@ -204,9 +203,9 @@ class URLPattern:
|
|
204
203
|
def __repr__(self):
|
205
204
|
return f"<{self.__class__.__name__} {self.pattern.describe()}>"
|
206
205
|
|
207
|
-
def
|
206
|
+
def preflight(self):
|
208
207
|
warnings = self._check_pattern_name()
|
209
|
-
warnings.extend(self.pattern.
|
208
|
+
warnings.extend(self.pattern.preflight())
|
210
209
|
return warnings
|
211
210
|
|
212
211
|
def _check_pattern_name(self):
|
@@ -214,10 +213,11 @@ class URLPattern:
|
|
214
213
|
Check that the pattern name does not contain a colon.
|
215
214
|
"""
|
216
215
|
if self.pattern.name is not None and ":" in self.pattern.name:
|
217
|
-
warning =
|
218
|
-
f"Your URL pattern {self.pattern.describe()} has a name including a ':'. Remove the colon, to "
|
216
|
+
warning = PreflightResult(
|
217
|
+
fix=f"Your URL pattern {self.pattern.describe()} has a name including a ':'. Remove the colon, to "
|
219
218
|
"avoid ambiguous namespace references.",
|
220
|
-
|
219
|
+
warning=True,
|
220
|
+
id="urls.pattern_name_contains_colon",
|
221
221
|
)
|
222
222
|
return [warning]
|
223
223
|
else:
|
plain/urls/resolvers.py
CHANGED
@@ -11,7 +11,6 @@ import re
|
|
11
11
|
from threading import local
|
12
12
|
from urllib.parse import quote
|
13
13
|
|
14
|
-
from plain.preflight.urls import check_resolver
|
15
14
|
from plain.runtime import settings
|
16
15
|
from plain.utils.datastructures import MultiValueDict
|
17
16
|
from plain.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
|
@@ -114,11 +113,12 @@ class URLResolver:
|
|
114
113
|
def __repr__(self):
|
115
114
|
return f"<{self.__class__.__name__} {repr(self.router)} ({self.namespace}) {self.pattern.describe()}>"
|
116
115
|
|
117
|
-
def
|
116
|
+
def preflight(self):
|
118
117
|
messages = []
|
118
|
+
messages.extend(self.pattern.preflight())
|
119
119
|
for pattern in self.url_patterns:
|
120
|
-
messages.extend(
|
121
|
-
return messages
|
120
|
+
messages.extend(pattern.preflight())
|
121
|
+
return messages
|
122
122
|
|
123
123
|
def _populate(self):
|
124
124
|
# Short-circuit if called recursively in this thread to prevent
|