plain 0.21.5__tar.gz → 0.22.1__tar.gz

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.
Files changed (161) hide show
  1. {plain-0.21.5 → plain-0.22.1}/.gitignore +0 -3
  2. {plain-0.21.5 → plain-0.22.1}/PKG-INFO +1 -1
  3. {plain-0.21.5 → plain-0.22.1}/plain/assets/urls.py +9 -8
  4. {plain-0.21.5 → plain-0.22.1}/plain/assets/views.py +4 -4
  5. {plain-0.21.5 → plain-0.22.1}/plain/cli/cli.py +90 -7
  6. {plain-0.21.5 → plain-0.22.1}/plain/http/request.py +1 -2
  7. {plain-0.21.5 → plain-0.22.1}/plain/internal/handlers/base.py +9 -23
  8. {plain-0.21.5 → plain-0.22.1}/plain/internal/handlers/exception.py +2 -0
  9. {plain-0.21.5 → plain-0.22.1}/plain/internal/middleware/slash.py +15 -7
  10. {plain-0.21.5 → plain-0.22.1}/plain/packages/registry.py +3 -77
  11. {plain-0.21.5 → plain-0.22.1}/plain/preflight/urls.py +2 -2
  12. {plain-0.21.5 → plain-0.22.1}/plain/runtime/global_settings.py +1 -1
  13. {plain-0.21.5 → plain-0.22.1}/plain/templates/jinja/globals.py +2 -8
  14. {plain-0.21.5 → plain-0.22.1}/plain/test/client.py +10 -10
  15. {plain-0.21.5 → plain-0.22.1}/plain/urls/__init__.py +8 -19
  16. plain-0.22.1/plain/urls/patterns.py +271 -0
  17. plain-0.22.1/plain/urls/resolvers.py +408 -0
  18. plain-0.22.1/plain/urls/routers.py +112 -0
  19. plain-0.21.5/plain/urls/base.py → plain-0.22.1/plain/urls/utils.py +5 -61
  20. {plain-0.21.5 → plain-0.22.1}/plain/utils/functional.py +1 -2
  21. {plain-0.21.5 → plain-0.22.1}/plain/views/redirect.py +1 -1
  22. {plain-0.21.5 → plain-0.22.1}/plain/views/templates.py +1 -1
  23. {plain-0.21.5 → plain-0.22.1}/pyproject.toml +1 -1
  24. plain-0.22.1/tests/app/urls.py +14 -0
  25. plain-0.21.5/plain/urls/conf.py +0 -95
  26. plain-0.21.5/plain/urls/resolvers.py +0 -718
  27. plain-0.21.5/plain/utils/deprecation.py +0 -6
  28. plain-0.21.5/tests/app/urls.py +0 -12
  29. plain-0.21.5/uv.lock +0 -148
  30. {plain-0.21.5 → plain-0.22.1}/LICENSE +0 -0
  31. {plain-0.21.5 → plain-0.22.1}/README.md +0 -0
  32. {plain-0.21.5 → plain-0.22.1}/plain/README.md +0 -0
  33. {plain-0.21.5 → plain-0.22.1}/plain/__main__.py +0 -0
  34. {plain-0.21.5 → plain-0.22.1}/plain/assets/README.md +0 -0
  35. {plain-0.21.5 → plain-0.22.1}/plain/assets/__init__.py +0 -0
  36. {plain-0.21.5 → plain-0.22.1}/plain/assets/compile.py +0 -0
  37. {plain-0.21.5 → plain-0.22.1}/plain/assets/finders.py +0 -0
  38. {plain-0.21.5 → plain-0.22.1}/plain/assets/fingerprints.py +0 -0
  39. {plain-0.21.5 → plain-0.22.1}/plain/cli/README.md +0 -0
  40. {plain-0.21.5 → plain-0.22.1}/plain/cli/__init__.py +0 -0
  41. {plain-0.21.5 → plain-0.22.1}/plain/cli/formatting.py +0 -0
  42. {plain-0.21.5 → plain-0.22.1}/plain/cli/packages.py +0 -0
  43. {plain-0.21.5 → plain-0.22.1}/plain/cli/print.py +0 -0
  44. {plain-0.21.5 → plain-0.22.1}/plain/cli/startup.py +0 -0
  45. {plain-0.21.5 → plain-0.22.1}/plain/csrf/README.md +0 -0
  46. {plain-0.21.5 → plain-0.22.1}/plain/csrf/middleware.py +0 -0
  47. {plain-0.21.5 → plain-0.22.1}/plain/csrf/views.py +0 -0
  48. {plain-0.21.5 → plain-0.22.1}/plain/debug.py +0 -0
  49. {plain-0.21.5 → plain-0.22.1}/plain/exceptions.py +0 -0
  50. {plain-0.21.5 → plain-0.22.1}/plain/forms/README.md +0 -0
  51. {plain-0.21.5 → plain-0.22.1}/plain/forms/__init__.py +0 -0
  52. {plain-0.21.5 → plain-0.22.1}/plain/forms/boundfield.py +0 -0
  53. {plain-0.21.5 → plain-0.22.1}/plain/forms/exceptions.py +0 -0
  54. {plain-0.21.5 → plain-0.22.1}/plain/forms/fields.py +0 -0
  55. {plain-0.21.5 → plain-0.22.1}/plain/forms/forms.py +0 -0
  56. {plain-0.21.5 → plain-0.22.1}/plain/http/README.md +0 -0
  57. {plain-0.21.5 → plain-0.22.1}/plain/http/__init__.py +0 -0
  58. {plain-0.21.5 → plain-0.22.1}/plain/http/cookie.py +0 -0
  59. {plain-0.21.5 → plain-0.22.1}/plain/http/multipartparser.py +0 -0
  60. {plain-0.21.5 → plain-0.22.1}/plain/http/response.py +0 -0
  61. {plain-0.21.5 → plain-0.22.1}/plain/internal/__init__.py +0 -0
  62. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/README.md +0 -0
  63. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/__init__.py +0 -0
  64. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/base.py +0 -0
  65. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/locks.py +0 -0
  66. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/move.py +0 -0
  67. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/temp.py +0 -0
  68. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/uploadedfile.py +0 -0
  69. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/uploadhandler.py +0 -0
  70. {plain-0.21.5 → plain-0.22.1}/plain/internal/files/utils.py +0 -0
  71. {plain-0.21.5 → plain-0.22.1}/plain/internal/handlers/__init__.py +0 -0
  72. {plain-0.21.5 → plain-0.22.1}/plain/internal/handlers/wsgi.py +0 -0
  73. {plain-0.21.5 → plain-0.22.1}/plain/internal/middleware/__init__.py +0 -0
  74. {plain-0.21.5 → plain-0.22.1}/plain/internal/middleware/headers.py +0 -0
  75. {plain-0.21.5 → plain-0.22.1}/plain/internal/middleware/https.py +0 -0
  76. {plain-0.21.5 → plain-0.22.1}/plain/json.py +0 -0
  77. {plain-0.21.5 → plain-0.22.1}/plain/logs/README.md +0 -0
  78. {plain-0.21.5 → plain-0.22.1}/plain/logs/__init__.py +0 -0
  79. {plain-0.21.5 → plain-0.22.1}/plain/logs/configure.py +0 -0
  80. {plain-0.21.5 → plain-0.22.1}/plain/logs/loggers.py +0 -0
  81. {plain-0.21.5 → plain-0.22.1}/plain/logs/utils.py +0 -0
  82. {plain-0.21.5 → plain-0.22.1}/plain/packages/README.md +0 -0
  83. {plain-0.21.5 → plain-0.22.1}/plain/packages/__init__.py +0 -0
  84. {plain-0.21.5 → plain-0.22.1}/plain/packages/config.py +0 -0
  85. {plain-0.21.5 → plain-0.22.1}/plain/paginator.py +0 -0
  86. {plain-0.21.5 → plain-0.22.1}/plain/preflight/README.md +0 -0
  87. {plain-0.21.5 → plain-0.22.1}/plain/preflight/__init__.py +0 -0
  88. {plain-0.21.5 → plain-0.22.1}/plain/preflight/files.py +0 -0
  89. {plain-0.21.5 → plain-0.22.1}/plain/preflight/messages.py +0 -0
  90. {plain-0.21.5 → plain-0.22.1}/plain/preflight/registry.py +0 -0
  91. {plain-0.21.5 → plain-0.22.1}/plain/preflight/security.py +0 -0
  92. {plain-0.21.5 → plain-0.22.1}/plain/runtime/README.md +0 -0
  93. {plain-0.21.5 → plain-0.22.1}/plain/runtime/__init__.py +0 -0
  94. {plain-0.21.5 → plain-0.22.1}/plain/runtime/user_settings.py +0 -0
  95. {plain-0.21.5 → plain-0.22.1}/plain/signals/README.md +0 -0
  96. {plain-0.21.5 → plain-0.22.1}/plain/signals/__init__.py +0 -0
  97. {plain-0.21.5 → plain-0.22.1}/plain/signals/dispatch/__init__.py +0 -0
  98. {plain-0.21.5 → plain-0.22.1}/plain/signals/dispatch/dispatcher.py +0 -0
  99. {plain-0.21.5 → plain-0.22.1}/plain/signals/dispatch/license.txt +0 -0
  100. {plain-0.21.5 → plain-0.22.1}/plain/signing.py +0 -0
  101. {plain-0.21.5 → plain-0.22.1}/plain/templates/README.md +0 -0
  102. {plain-0.21.5 → plain-0.22.1}/plain/templates/__init__.py +0 -0
  103. {plain-0.21.5 → plain-0.22.1}/plain/templates/core.py +0 -0
  104. {plain-0.21.5 → plain-0.22.1}/plain/templates/jinja/README.md +0 -0
  105. {plain-0.21.5 → plain-0.22.1}/plain/templates/jinja/__init__.py +0 -0
  106. {plain-0.21.5 → plain-0.22.1}/plain/templates/jinja/environments.py +0 -0
  107. {plain-0.21.5 → plain-0.22.1}/plain/templates/jinja/extensions.py +0 -0
  108. {plain-0.21.5 → plain-0.22.1}/plain/templates/jinja/filters.py +0 -0
  109. {plain-0.21.5 → plain-0.22.1}/plain/test/README.md +0 -0
  110. {plain-0.21.5 → plain-0.22.1}/plain/test/__init__.py +0 -0
  111. {plain-0.21.5 → plain-0.22.1}/plain/urls/README.md +0 -0
  112. {plain-0.21.5 → plain-0.22.1}/plain/urls/converters.py +0 -0
  113. {plain-0.21.5 → plain-0.22.1}/plain/urls/exceptions.py +0 -0
  114. {plain-0.21.5 → plain-0.22.1}/plain/utils/README.md +0 -0
  115. {plain-0.21.5 → plain-0.22.1}/plain/utils/__init__.py +0 -0
  116. {plain-0.21.5 → plain-0.22.1}/plain/utils/_os.py +0 -0
  117. {plain-0.21.5 → plain-0.22.1}/plain/utils/cache.py +0 -0
  118. {plain-0.21.5 → plain-0.22.1}/plain/utils/connection.py +0 -0
  119. {plain-0.21.5 → plain-0.22.1}/plain/utils/crypto.py +0 -0
  120. {plain-0.21.5 → plain-0.22.1}/plain/utils/datastructures.py +0 -0
  121. {plain-0.21.5 → plain-0.22.1}/plain/utils/dateformat.py +0 -0
  122. {plain-0.21.5 → plain-0.22.1}/plain/utils/dateparse.py +0 -0
  123. {plain-0.21.5 → plain-0.22.1}/plain/utils/dates.py +0 -0
  124. {plain-0.21.5 → plain-0.22.1}/plain/utils/deconstruct.py +0 -0
  125. {plain-0.21.5 → plain-0.22.1}/plain/utils/decorators.py +0 -0
  126. {plain-0.21.5 → plain-0.22.1}/plain/utils/duration.py +0 -0
  127. {plain-0.21.5 → plain-0.22.1}/plain/utils/email.py +0 -0
  128. {plain-0.21.5 → plain-0.22.1}/plain/utils/encoding.py +0 -0
  129. {plain-0.21.5 → plain-0.22.1}/plain/utils/hashable.py +0 -0
  130. {plain-0.21.5 → plain-0.22.1}/plain/utils/html.py +0 -0
  131. {plain-0.21.5 → plain-0.22.1}/plain/utils/http.py +0 -0
  132. {plain-0.21.5 → plain-0.22.1}/plain/utils/inspect.py +0 -0
  133. {plain-0.21.5 → plain-0.22.1}/plain/utils/ipv6.py +0 -0
  134. {plain-0.21.5 → plain-0.22.1}/plain/utils/itercompat.py +0 -0
  135. {plain-0.21.5 → plain-0.22.1}/plain/utils/module_loading.py +0 -0
  136. {plain-0.21.5 → plain-0.22.1}/plain/utils/regex_helper.py +0 -0
  137. {plain-0.21.5 → plain-0.22.1}/plain/utils/safestring.py +0 -0
  138. {plain-0.21.5 → plain-0.22.1}/plain/utils/text.py +0 -0
  139. {plain-0.21.5 → plain-0.22.1}/plain/utils/timesince.py +0 -0
  140. {plain-0.21.5 → plain-0.22.1}/plain/utils/timezone.py +0 -0
  141. {plain-0.21.5 → plain-0.22.1}/plain/utils/tree.py +0 -0
  142. {plain-0.21.5 → plain-0.22.1}/plain/validators.py +0 -0
  143. {plain-0.21.5 → plain-0.22.1}/plain/views/README.md +0 -0
  144. {plain-0.21.5 → plain-0.22.1}/plain/views/__init__.py +0 -0
  145. {plain-0.21.5 → plain-0.22.1}/plain/views/base.py +0 -0
  146. {plain-0.21.5 → plain-0.22.1}/plain/views/csrf.py +0 -0
  147. {plain-0.21.5 → plain-0.22.1}/plain/views/errors.py +0 -0
  148. {plain-0.21.5 → plain-0.22.1}/plain/views/exceptions.py +0 -0
  149. {plain-0.21.5 → plain-0.22.1}/plain/views/forms.py +0 -0
  150. {plain-0.21.5 → plain-0.22.1}/plain/views/objects.py +0 -0
  151. {plain-0.21.5 → plain-0.22.1}/plain/wsgi.py +0 -0
  152. {plain-0.21.5 → plain-0.22.1}/tests/.bolt/assets_collected/assets.json +0 -0
  153. {plain-0.21.5 → plain-0.22.1}/tests/.gitignore +0 -0
  154. {plain-0.21.5 → plain-0.22.1}/tests/app/.gitignore +0 -0
  155. {plain-0.21.5 → plain-0.22.1}/tests/app/settings.py +0 -0
  156. {plain-0.21.5 → plain-0.22.1}/tests/app/test/__init__.py +0 -0
  157. {plain-0.21.5 → plain-0.22.1}/tests/app/test/default_settings.py +0 -0
  158. {plain-0.21.5 → plain-0.22.1}/tests/conftest.py +0 -0
  159. {plain-0.21.5 → plain-0.22.1}/tests/test_cli.py +0 -0
  160. {plain-0.21.5 → plain-0.22.1}/tests/test_runtime.py +0 -0
  161. {plain-0.21.5 → plain-0.22.1}/tests/test_wsgi.py +0 -0
@@ -6,9 +6,6 @@ __pycache__
6
6
  *.DS_Store
7
7
  .coverage
8
8
 
9
- # Build files from publish
10
- plain*/dist/
11
-
12
9
  # Test apps
13
10
  plain*/tests/.plain
14
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.21.5
3
+ Version: 0.22.1
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,10 +1,16 @@
1
1
  from plain.runtime import settings
2
- from plain.urls import path, reverse
2
+ from plain.urls import RouterBase, path, register_router, reverse
3
3
 
4
4
  from .fingerprints import get_fingerprinted_url_path
5
5
  from .views import AssetView
6
6
 
7
- default_namespace = "assets"
7
+
8
+ @register_router
9
+ class Router(RouterBase):
10
+ namespace = "assets"
11
+ urls = [
12
+ path("<path:path>", AssetView, name="asset"),
13
+ ]
8
14
 
9
15
 
10
16
  def get_asset_url(url_path):
@@ -23,9 +29,4 @@ def get_asset_url(url_path):
23
29
  if settings.ASSETS_BASE_URL:
24
30
  return settings.ASSETS_BASE_URL + resolved_url_path
25
31
 
26
- return reverse(default_namespace + ":asset", kwargs={"path": resolved_url_path})
27
-
28
-
29
- urlpatterns = [
30
- path("<path:path>", AssetView, name="asset"),
31
- ]
32
+ return reverse(Router.namespace + ":asset", path=resolved_url_path)
@@ -206,12 +206,12 @@ class AssetView(View):
206
206
  # or we're already looking at it.
207
207
  return
208
208
 
209
- from .urls import default_namespace
209
+ from .urls import Router
210
+
211
+ namespace = Router.namespace
210
212
 
211
213
  return ResponseRedirect(
212
- redirect_to=reverse(
213
- f"{default_namespace}:asset", args=[fingerprinted_url_path]
214
- ),
214
+ redirect_to=reverse(f"{namespace}:asset", fingerprinted_url_path),
215
215
  headers={
216
216
  "Cache-Control": "max-age=60", # Can cache this for a short time, but the fingerprinted path can change
217
217
  },
@@ -346,6 +346,9 @@ def compile(keep_original, fingerprint, compress):
346
346
  f"\nCompiled {total_files} assets into {total_compiled} files", fg="green"
347
347
  )
348
348
 
349
+ # TODO could do a jinja pre-compile here too?
350
+ # environment.compile_templates() but it needs a target, ignore_errors=False
351
+
349
352
 
350
353
  @plain_cli.command()
351
354
  @click.argument("package_name")
@@ -378,13 +381,14 @@ def create(package_name):
378
381
  # Create a urls.py file with a default namespace
379
382
  if not (package_dir / "urls.py").exists():
380
383
  (package_dir / "urls.py").write_text(
381
- f"""from plain.urls import path
382
-
383
- default_namespace = f"{package_name}"
384
-
385
- urlpatterns = [
386
- # path("", views.IndexView, name="index"),
387
- ]
384
+ f"""from plain.urls import path, RouterBase, register_router
385
+
386
+ @register_router
387
+ class Router(RouterBase):
388
+ namespace = f"{package_name}"
389
+ urls = [
390
+ # path("", views.IndexView, name="index"),
391
+ ]
388
392
  """
389
393
  )
390
394
 
@@ -417,6 +421,85 @@ def generate_secret_key():
417
421
  click.echo(new_secret_key)
418
422
 
419
423
 
424
+ @plain_cli.command()
425
+ @click.option("--flat", is_flag=True, help="List all URLs in a flat list")
426
+ def urls(flat):
427
+ """Print all URL patterns under settings.URLS_MODULE"""
428
+ from plain.urls import URLResolver, get_resolver
429
+
430
+ resolver = get_resolver()
431
+ if flat:
432
+
433
+ def flat_list(patterns, prefix="", curr_ns=""):
434
+ for pattern in patterns:
435
+ full_pattern = f"{prefix}{pattern.pattern}"
436
+ if isinstance(pattern, URLResolver):
437
+ # Update current namespace
438
+ new_ns = (
439
+ f"{curr_ns}:{pattern.namespace}"
440
+ if curr_ns and pattern.namespace
441
+ else (pattern.namespace or curr_ns)
442
+ )
443
+ yield from flat_list(
444
+ pattern.url_patterns, prefix=full_pattern, curr_ns=new_ns
445
+ )
446
+ else:
447
+ if pattern.name:
448
+ if curr_ns:
449
+ styled_namespace = click.style(f"{curr_ns}:", fg="yellow")
450
+ styled_name = click.style(pattern.name, fg="blue")
451
+ full_name = f"{styled_namespace}{styled_name}"
452
+ else:
453
+ full_name = click.style(pattern.name, fg="blue")
454
+ name_part = f" [{full_name}]"
455
+ else:
456
+ name_part = ""
457
+ yield f"{click.style(full_pattern)}{name_part}"
458
+
459
+ for p in flat_list(resolver.url_patterns):
460
+ click.echo(p)
461
+ else:
462
+
463
+ def print_tree(patterns, prefix="", curr_ns=""):
464
+ count = len(patterns)
465
+ for idx, pattern in enumerate(patterns):
466
+ is_last = idx == (count - 1)
467
+ connector = "└── " if is_last else "├── "
468
+ styled_connector = click.style(connector)
469
+ styled_pattern = click.style(pattern.pattern)
470
+ if isinstance(pattern, URLResolver):
471
+ if pattern.namespace:
472
+ new_ns = (
473
+ f"{curr_ns}:{pattern.namespace}"
474
+ if curr_ns
475
+ else pattern.namespace
476
+ )
477
+ styled_namespace = click.style(f"[{new_ns}]", fg="yellow")
478
+ click.echo(
479
+ f"{prefix}{styled_connector}{styled_pattern} {styled_namespace}"
480
+ )
481
+ else:
482
+ new_ns = curr_ns
483
+ click.echo(f"{prefix}{styled_connector}{styled_pattern}")
484
+ extension = " " if is_last else "│ "
485
+ print_tree(pattern.url_patterns, prefix + extension, new_ns)
486
+ else:
487
+ if pattern.name:
488
+ if curr_ns:
489
+ styled_namespace = click.style(f"{curr_ns}:", fg="yellow")
490
+ styled_name = click.style(pattern.name, fg="blue")
491
+ full_name = f"[{styled_namespace}{styled_name}]"
492
+ else:
493
+ full_name = click.style(f"[{pattern.name}]", fg="blue")
494
+ click.echo(
495
+ f"{prefix}{styled_connector}{styled_pattern} {full_name}"
496
+ )
497
+ else:
498
+ click.echo(f"{prefix}{styled_connector}{styled_pattern}")
499
+
500
+ print_tree(resolver.url_patterns)
501
+
502
+
420
503
  class AppCLIGroup(click.Group):
421
504
  """
422
505
  Loads app.cli if it exists as `plain app`
@@ -328,8 +328,7 @@ class HttpRequest:
328
328
  self.upload_handlers = ImmutableList(
329
329
  self.upload_handlers,
330
330
  warning=(
331
- "You cannot alter upload handlers after the upload has been "
332
- "processed."
331
+ "You cannot alter upload handlers after the upload has been processed."
333
332
  ),
334
333
  )
335
334
  parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
@@ -4,8 +4,7 @@ import types
4
4
  from plain.exceptions import ImproperlyConfigured
5
5
  from plain.logs import log_response
6
6
  from plain.runtime import settings
7
- from plain.signals import request_finished
8
- from plain.urls import get_resolver, set_urlconf
7
+ from plain.urls import get_resolver
9
8
  from plain.utils.module_loading import import_string
10
9
 
11
10
  from .exception import convert_exception_to_response
@@ -61,7 +60,6 @@ class BaseHandler:
61
60
  def get_response(self, request):
62
61
  """Return a Response object for the given HttpRequest."""
63
62
  # Setup default url resolver for this thread
64
- set_urlconf(settings.ROOT_URLCONF)
65
63
  response = self._middleware_chain(request)
66
64
  response._resource_closers.append(request.close)
67
65
  if response.status_code >= 400:
@@ -80,27 +78,23 @@ class BaseHandler:
80
78
  template_response middleware. This method is everything that happens
81
79
  inside the request/response middleware.
82
80
  """
83
- callback, callback_args, callback_kwargs = self.resolve_request(request)
81
+ resolver_match = self.resolve_request(request)
84
82
 
85
- response = callback(request, *callback_args, **callback_kwargs)
83
+ response = resolver_match.func(
84
+ request, *resolver_match.args, **resolver_match.kwargs
85
+ )
86
86
 
87
87
  # Complain if the view returned None (a common error).
88
- self.check_response(response, callback)
88
+ self.check_response(response, resolver_match.func)
89
89
 
90
90
  return response
91
91
 
92
92
  def resolve_request(self, request):
93
93
  """
94
- Retrieve/set the urlconf for the request. Return the view resolved,
94
+ Retrieve/set the urlrouter for the request. Return the view resolved,
95
95
  with its args and kwargs.
96
96
  """
97
- # Work out the resolver.
98
- if hasattr(request, "urlconf"):
99
- urlconf = request.urlconf
100
- set_urlconf(urlconf)
101
- resolver = get_resolver(urlconf)
102
- else:
103
- resolver = get_resolver()
97
+ resolver = get_resolver()
104
98
  # Resolve the view, and assign the match object back to the request.
105
99
  resolver_match = resolver.resolve(request.path_info)
106
100
  request.resolver_match = resolver_match
@@ -117,13 +111,5 @@ class BaseHandler:
117
111
  name = f"The view {callback.__module__}.{callback.__class__.__name__}.__call__"
118
112
  if response is None:
119
113
  raise ValueError(
120
- f"{name} didn't return a Response object. It returned None " "instead."
114
+ f"{name} didn't return a Response object. It returned None instead."
121
115
  )
122
-
123
-
124
- def reset_urlconf(sender, **kwargs):
125
- """Reset the URLconf after each request is finished."""
126
- set_urlconf(None)
127
-
128
-
129
- request_finished.connect(reset_urlconf)
@@ -127,6 +127,8 @@ def handle_uncaught_exception():
127
127
  Processing for any otherwise uncaught exceptions (those that will
128
128
  generate HTTP 500 responses).
129
129
  """
130
+ if settings.DEBUG:
131
+ raise
130
132
  return ResponseServerError()
131
133
 
132
134
 
@@ -1,6 +1,6 @@
1
1
  from plain.http import ResponsePermanentRedirect
2
2
  from plain.runtime import settings
3
- from plain.urls import is_valid_path
3
+ from plain.urls import Resolver404, get_resolver
4
4
  from plain.utils.http import escape_leading_slashes
5
5
 
6
6
 
@@ -26,18 +26,26 @@ class RedirectSlashMiddleware:
26
26
 
27
27
  return response
28
28
 
29
+ @staticmethod
30
+ def _is_valid_path(path):
31
+ """
32
+ Return the ResolverMatch if the given path resolves against the default URL
33
+ resolver, False otherwise. This is a convenience method to make working
34
+ with "is this a match?" cases easier, avoiding try...except blocks.
35
+ """
36
+ try:
37
+ return get_resolver().resolve(path)
38
+ except Resolver404:
39
+ return False
40
+
29
41
  def should_redirect_with_slash(self, request):
30
42
  """
31
43
  Return True if settings.APPEND_SLASH is True and appending a slash to
32
44
  the request path turns an invalid path into a valid one.
33
45
  """
34
46
  if settings.APPEND_SLASH and not request.path_info.endswith("/"):
35
- urlconf = getattr(request, "urlconf", None)
36
- if not is_valid_path(request.path_info, urlconf):
37
- match = is_valid_path(f"{request.path_info}/", urlconf)
38
- if match:
39
- view = match.func
40
- return getattr(view, "should_append_slash", True)
47
+ if not self._is_valid_path(request.path_info):
48
+ return self._is_valid_path(f"{request.path_info}/")
41
49
  return False
42
50
 
43
51
  def get_full_path_with_slash(self, request):
@@ -24,8 +24,8 @@ class Packages:
24
24
  if installed_packages is None and hasattr(sys.modules[__name__], "packages"):
25
25
  raise RuntimeError("You must supply an installed_packages argument.")
26
26
 
27
- # Mapping of app labels => model names => model classes. Every time a
28
- # model is imported, ModelBase.__new__ calls packages.register_model which
27
+ # Mapping of app labels => model names => model classes.
28
+ # Models are registered with @models.register_model, which
29
29
  # creates an entry in all_models. All imported models are registered,
30
30
  # regardless of whether they're defined in an installed application
31
31
  # and whether the registry has been populated. Since it isn't possible
@@ -36,10 +36,6 @@ class Packages:
36
36
  # Mapping of labels to PackageConfig instances for installed packages.
37
37
  self.package_configs = {}
38
38
 
39
- # Stack of package_configs. Used to store the current state in
40
- # set_available_packages and set_installed_packages.
41
- self.stored_package_configs = []
42
-
43
39
  # Whether the registry is populated.
44
40
  self.packages_ready = self.models_ready = self.ready = False
45
41
 
@@ -103,7 +99,7 @@ class Packages:
103
99
  duplicates = [name for name, count in counts.most_common() if count > 1]
104
100
  if duplicates:
105
101
  raise ImproperlyConfigured(
106
- "Package names aren't unique, " "duplicates: {}".format(
102
+ "Package names aren't unique, duplicates: {}".format(
107
103
  ", ".join(duplicates)
108
104
  )
109
105
  )
@@ -238,15 +234,6 @@ class Packages:
238
234
  self.do_pending_operations(model)
239
235
  self.clear_cache()
240
236
 
241
- def is_installed(self, package_name):
242
- """
243
- Check whether an application with this name exists in the registry.
244
-
245
- package_name is the full name of the app e.g. 'plain.admin'.
246
- """
247
- self.check_packages_ready()
248
- return any(ac.name == package_name for ac in self.package_configs.values())
249
-
250
237
  def get_containing_package_config(self, object_name):
251
238
  """
252
239
  Look for an app config containing a given object.
@@ -302,67 +289,6 @@ class Packages:
302
289
  return model._meta.swappable
303
290
  return None
304
291
 
305
- def set_available_packages(self, available):
306
- """
307
- Restrict the set of installed packages used by get_package_config[s].
308
-
309
- available must be an iterable of application names.
310
-
311
- set_available_packages() must be balanced with unset_available_packages().
312
-
313
- Primarily used for performance optimization in TransactionTestCase.
314
-
315
- This method is safe in the sense that it doesn't trigger any imports.
316
- """
317
- available = set(available)
318
- installed = {
319
- package_config.name for package_config in self.get_package_configs()
320
- }
321
- if not available.issubset(installed):
322
- raise ValueError(
323
- "Available packages isn't a subset of installed packages, extra packages: {}".format(
324
- ", ".join(available - installed)
325
- )
326
- )
327
-
328
- self.stored_package_configs.append(self.package_configs)
329
- self.package_configs = {
330
- label: package_config
331
- for label, package_config in self.package_configs.items()
332
- if package_config.name in available
333
- }
334
- self.clear_cache()
335
-
336
- def unset_available_packages(self):
337
- """Cancel a previous call to set_available_packages()."""
338
- self.package_configs = self.stored_package_configs.pop()
339
- self.clear_cache()
340
-
341
- def set_installed_packages(self, installed):
342
- """
343
- Enable a different set of installed packages for get_package_config[s].
344
-
345
- installed must be an iterable in the same format as INSTALLED_PACKAGES.
346
-
347
- set_installed_packages() must be balanced with unset_installed_packages(),
348
- even if it exits with an exception.
349
-
350
- Primarily used as a receiver of the setting_changed signal in tests.
351
-
352
- This method may trigger new imports, which may add new models to the
353
- registry of all imported models. They will stay in the registry even
354
- after unset_installed_packages(). Since it isn't possible to replay
355
- imports safely (e.g. that could lead to registering listeners twice),
356
- models are registered when they're imported and never removed.
357
- """
358
- if not self.ready:
359
- raise PackageRegistryNotReady("Package registry isn't ready yet.")
360
- self.stored_package_configs.append(self.package_configs)
361
- self.package_configs = {}
362
- self.packages_ready = self.models_ready = self.loading = self.ready = False
363
- self.clear_cache()
364
- self.populate(installed)
365
-
366
292
  def clear_cache(self):
367
293
  """
368
294
  Clear all internal caches, for methods that alter the app registry.
@@ -7,7 +7,7 @@ from . import Error, Warning, register
7
7
 
8
8
  @register
9
9
  def check_url_config(package_configs, **kwargs):
10
- if getattr(settings, "ROOT_URLCONF", None):
10
+ if getattr(settings, "URLS_MODULE", None):
11
11
  from plain.urls import get_resolver
12
12
 
13
13
  resolver = get_resolver()
@@ -33,7 +33,7 @@ def check_url_namespaces_unique(package_configs, **kwargs):
33
33
  """
34
34
  Warn if URL namespaces used in applications aren't unique.
35
35
  """
36
- if not getattr(settings, "ROOT_URLCONF", None):
36
+ if not getattr(settings, "URLS_MODULE", None):
37
37
  return []
38
38
 
39
39
  from plain.urls import get_resolver
@@ -74,7 +74,7 @@ SECRET_KEY: str
74
74
  # secret key rotation.
75
75
  SECRET_KEY_FALLBACKS: list[str] = []
76
76
 
77
- ROOT_URLCONF = "app.urls"
77
+ URLS_MODULE = "app.urls"
78
78
 
79
79
  # List of upload handler classes to be applied in order.
80
80
  FILE_UPLOAD_HANDLERS = [
@@ -1,14 +1,8 @@
1
1
  from plain.paginator import Paginator
2
+ from plain.urls import reverse
2
3
  from plain.utils import timezone
3
4
 
4
5
 
5
- def url(viewname, *args, **kwargs):
6
- # A modified reverse that lets you pass args directly, excluding urlconf
7
- from plain.urls import reverse
8
-
9
- return reverse(viewname, args=args, kwargs=kwargs)
10
-
11
-
12
6
  def asset(url_path):
13
7
  # An explicit callable we can control, but also delay the import of asset.urls->views->templates
14
8
  # for circular import reasons
@@ -19,7 +13,7 @@ def asset(url_path):
19
13
 
20
14
  default_globals = {
21
15
  "asset": asset,
22
- "url": url,
16
+ "url": reverse,
23
17
  "Paginator": Paginator,
24
18
  "now": timezone.now,
25
19
  "localtime": timezone.localtime,
@@ -15,7 +15,7 @@ from plain.internal.handlers.wsgi import WSGIRequest
15
15
  from plain.json import PlainJSONEncoder
16
16
  from plain.runtime import settings
17
17
  from plain.signals import got_request_exception, request_started
18
- from plain.urls import resolve
18
+ from plain.urls import get_resolver
19
19
  from plain.utils.encoding import force_bytes
20
20
  from plain.utils.functional import SimpleLazyObject
21
21
  from plain.utils.http import urlencode
@@ -107,9 +107,9 @@ class FakePayload(IOBase):
107
107
  self.read_started = True
108
108
  if size == -1 or size is None:
109
109
  size = self.__len
110
- assert (
111
- self.__len >= size
112
- ), "Cannot read more than the available bytes from the HTTP incoming data."
110
+ assert self.__len >= size, (
111
+ "Cannot read more than the available bytes from the HTTP incoming data."
112
+ )
113
113
  content = self.__content.read(size)
114
114
  self.__len -= len(content)
115
115
  return content
@@ -120,9 +120,9 @@ class FakePayload(IOBase):
120
120
  self.read_started = True
121
121
  if size == -1 or size is None:
122
122
  size = self.__len
123
- assert (
124
- self.__len >= size
125
- ), "Cannot read more than the available bytes from the HTTP incoming data."
123
+ assert self.__len >= size, (
124
+ "Cannot read more than the available bytes from the HTTP incoming data."
125
+ )
126
126
  content = self.__content.readline(size)
127
127
  self.__len -= len(content)
128
128
  return content
@@ -298,7 +298,7 @@ class RequestFactory:
298
298
  post_request = rf.post('/submit/', {'foo': 'bar'})
299
299
 
300
300
  Once you have a request object you can pass it to any view function,
301
- just as if that view had been hooked up using a URLconf.
301
+ just as if that view had been hooked up using a urlrouter.
302
302
  """
303
303
 
304
304
  def __init__(self, *, json_encoder=PlainJSONEncoder, headers=None, **defaults):
@@ -690,9 +690,9 @@ class Client(ClientMixin, RequestFactory):
690
690
  response.user = response.wsgi_request.user
691
691
 
692
692
  # Attach the ResolverMatch instance to the response.
693
- urlconf = getattr(response.wsgi_request, "urlconf", None)
693
+ resolver = get_resolver()
694
694
  response.resolver_match = SimpleLazyObject(
695
- lambda: resolve(request["PATH_INFO"], urlconf=urlconf),
695
+ lambda: resolver.resolve(request["PATH_INFO"]),
696
696
  )
697
697
 
698
698
  # Update persistent cookie data.
@@ -1,22 +1,16 @@
1
- from .base import (
2
- clear_url_caches,
3
- get_urlconf,
4
- is_valid_path,
5
- resolve,
6
- reverse,
7
- reverse_lazy,
8
- set_urlconf,
9
- )
10
- from .conf import include, path, re_path
11
1
  from .converters import register_converter
12
2
  from .exceptions import NoReverseMatch, Resolver404
3
+ from .patterns import URLPattern
13
4
  from .resolvers import (
14
5
  ResolverMatch,
15
- URLPattern,
16
6
  URLResolver,
17
- get_ns_resolver,
18
7
  get_resolver,
19
8
  )
9
+ from .routers import RouterBase, include, path, register_router
10
+ from .utils import (
11
+ reverse,
12
+ reverse_lazy,
13
+ )
20
14
 
21
15
  __all__ = [
22
16
  "NoReverseMatch",
@@ -24,17 +18,12 @@ __all__ = [
24
18
  "URLResolver",
25
19
  "Resolver404",
26
20
  "ResolverMatch",
27
- "clear_url_caches",
28
- "get_ns_resolver",
29
21
  "get_resolver",
30
- "get_urlconf",
31
22
  "include",
32
- "is_valid_path",
33
23
  "path",
34
- "re_path",
35
24
  "register_converter",
36
- "resolve",
37
25
  "reverse",
38
26
  "reverse_lazy",
39
- "set_urlconf",
27
+ "RouterBase",
28
+ "register_router",
40
29
  ]