plain 0.50.0__tar.gz → 0.52.0__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 (167) hide show
  1. {plain-0.50.0 → plain-0.52.0}/PKG-INFO +1 -1
  2. plain-0.52.0/plain/CHANGELOG.md +58 -0
  3. plain-0.52.0/plain/cli/changelog.py +109 -0
  4. {plain-0.50.0 → plain-0.52.0}/plain/cli/core.py +2 -0
  5. {plain-0.50.0 → plain-0.52.0}/plain/cli/docs.py +3 -37
  6. plain-0.52.0/plain/cli/output.py +41 -0
  7. {plain-0.50.0 → plain-0.52.0}/plain/debug.py +4 -3
  8. {plain-0.50.0 → plain-0.52.0}/pyproject.toml +5 -1
  9. {plain-0.50.0 → plain-0.52.0}/tests/test_cli.py +19 -0
  10. plain-0.50.0/plain/CHANGELOG.md +0 -33
  11. {plain-0.50.0 → plain-0.52.0}/.gitignore +0 -0
  12. {plain-0.50.0 → plain-0.52.0}/LICENSE +0 -0
  13. {plain-0.50.0 → plain-0.52.0}/README.md +0 -0
  14. {plain-0.50.0 → plain-0.52.0}/plain/README.md +0 -0
  15. {plain-0.50.0 → plain-0.52.0}/plain/__main__.py +0 -0
  16. {plain-0.50.0 → plain-0.52.0}/plain/assets/README.md +0 -0
  17. {plain-0.50.0 → plain-0.52.0}/plain/assets/__init__.py +0 -0
  18. {plain-0.50.0 → plain-0.52.0}/plain/assets/compile.py +0 -0
  19. {plain-0.50.0 → plain-0.52.0}/plain/assets/finders.py +0 -0
  20. {plain-0.50.0 → plain-0.52.0}/plain/assets/fingerprints.py +0 -0
  21. {plain-0.50.0 → plain-0.52.0}/plain/assets/urls.py +0 -0
  22. {plain-0.50.0 → plain-0.52.0}/plain/assets/views.py +0 -0
  23. {plain-0.50.0 → plain-0.52.0}/plain/chores/README.md +0 -0
  24. {plain-0.50.0 → plain-0.52.0}/plain/chores/__init__.py +0 -0
  25. {plain-0.50.0 → plain-0.52.0}/plain/chores/registry.py +0 -0
  26. {plain-0.50.0 → plain-0.52.0}/plain/cli/README.md +0 -0
  27. {plain-0.50.0 → plain-0.52.0}/plain/cli/__init__.py +0 -0
  28. {plain-0.50.0 → plain-0.52.0}/plain/cli/build.py +0 -0
  29. {plain-0.50.0 → plain-0.52.0}/plain/cli/chores.py +0 -0
  30. {plain-0.50.0 → plain-0.52.0}/plain/cli/formatting.py +0 -0
  31. {plain-0.50.0 → plain-0.52.0}/plain/cli/help.py +0 -0
  32. {plain-0.50.0 → plain-0.52.0}/plain/cli/preflight.py +0 -0
  33. {plain-0.50.0 → plain-0.52.0}/plain/cli/print.py +0 -0
  34. {plain-0.50.0 → plain-0.52.0}/plain/cli/registry.py +0 -0
  35. {plain-0.50.0 → plain-0.52.0}/plain/cli/scaffold.py +0 -0
  36. {plain-0.50.0 → plain-0.52.0}/plain/cli/settings.py +0 -0
  37. {plain-0.50.0 → plain-0.52.0}/plain/cli/shell.py +0 -0
  38. {plain-0.50.0 → plain-0.52.0}/plain/cli/startup.py +0 -0
  39. {plain-0.50.0 → plain-0.52.0}/plain/cli/urls.py +0 -0
  40. {plain-0.50.0 → plain-0.52.0}/plain/cli/utils.py +0 -0
  41. {plain-0.50.0 → plain-0.52.0}/plain/csrf/README.md +0 -0
  42. {plain-0.50.0 → plain-0.52.0}/plain/csrf/middleware.py +0 -0
  43. {plain-0.50.0 → plain-0.52.0}/plain/csrf/views.py +0 -0
  44. {plain-0.50.0 → plain-0.52.0}/plain/exceptions.py +0 -0
  45. {plain-0.50.0 → plain-0.52.0}/plain/forms/README.md +0 -0
  46. {plain-0.50.0 → plain-0.52.0}/plain/forms/__init__.py +0 -0
  47. {plain-0.50.0 → plain-0.52.0}/plain/forms/boundfield.py +0 -0
  48. {plain-0.50.0 → plain-0.52.0}/plain/forms/exceptions.py +0 -0
  49. {plain-0.50.0 → plain-0.52.0}/plain/forms/fields.py +0 -0
  50. {plain-0.50.0 → plain-0.52.0}/plain/forms/forms.py +0 -0
  51. {plain-0.50.0 → plain-0.52.0}/plain/http/README.md +0 -0
  52. {plain-0.50.0 → plain-0.52.0}/plain/http/__init__.py +0 -0
  53. {plain-0.50.0 → plain-0.52.0}/plain/http/cookie.py +0 -0
  54. {plain-0.50.0 → plain-0.52.0}/plain/http/multipartparser.py +0 -0
  55. {plain-0.50.0 → plain-0.52.0}/plain/http/request.py +0 -0
  56. {plain-0.50.0 → plain-0.52.0}/plain/http/response.py +0 -0
  57. {plain-0.50.0 → plain-0.52.0}/plain/internal/__init__.py +0 -0
  58. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/__init__.py +0 -0
  59. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/base.py +0 -0
  60. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/locks.py +0 -0
  61. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/move.py +0 -0
  62. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/temp.py +0 -0
  63. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/uploadedfile.py +0 -0
  64. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/uploadhandler.py +0 -0
  65. {plain-0.50.0 → plain-0.52.0}/plain/internal/files/utils.py +0 -0
  66. {plain-0.50.0 → plain-0.52.0}/plain/internal/handlers/__init__.py +0 -0
  67. {plain-0.50.0 → plain-0.52.0}/plain/internal/handlers/base.py +0 -0
  68. {plain-0.50.0 → plain-0.52.0}/plain/internal/handlers/exception.py +0 -0
  69. {plain-0.50.0 → plain-0.52.0}/plain/internal/handlers/wsgi.py +0 -0
  70. {plain-0.50.0 → plain-0.52.0}/plain/internal/middleware/__init__.py +0 -0
  71. {plain-0.50.0 → plain-0.52.0}/plain/internal/middleware/headers.py +0 -0
  72. {plain-0.50.0 → plain-0.52.0}/plain/internal/middleware/https.py +0 -0
  73. {plain-0.50.0 → plain-0.52.0}/plain/internal/middleware/slash.py +0 -0
  74. {plain-0.50.0 → plain-0.52.0}/plain/json.py +0 -0
  75. {plain-0.50.0 → plain-0.52.0}/plain/logs/README.md +0 -0
  76. {plain-0.50.0 → plain-0.52.0}/plain/logs/__init__.py +0 -0
  77. {plain-0.50.0 → plain-0.52.0}/plain/logs/configure.py +0 -0
  78. {plain-0.50.0 → plain-0.52.0}/plain/logs/loggers.py +0 -0
  79. {plain-0.50.0 → plain-0.52.0}/plain/logs/utils.py +0 -0
  80. {plain-0.50.0 → plain-0.52.0}/plain/packages/README.md +0 -0
  81. {plain-0.50.0 → plain-0.52.0}/plain/packages/__init__.py +0 -0
  82. {plain-0.50.0 → plain-0.52.0}/plain/packages/config.py +0 -0
  83. {plain-0.50.0 → plain-0.52.0}/plain/packages/registry.py +0 -0
  84. {plain-0.50.0 → plain-0.52.0}/plain/paginator.py +0 -0
  85. {plain-0.50.0 → plain-0.52.0}/plain/preflight/README.md +0 -0
  86. {plain-0.50.0 → plain-0.52.0}/plain/preflight/__init__.py +0 -0
  87. {plain-0.50.0 → plain-0.52.0}/plain/preflight/files.py +0 -0
  88. {plain-0.50.0 → plain-0.52.0}/plain/preflight/messages.py +0 -0
  89. {plain-0.50.0 → plain-0.52.0}/plain/preflight/registry.py +0 -0
  90. {plain-0.50.0 → plain-0.52.0}/plain/preflight/security.py +0 -0
  91. {plain-0.50.0 → plain-0.52.0}/plain/preflight/urls.py +0 -0
  92. {plain-0.50.0 → plain-0.52.0}/plain/runtime/README.md +0 -0
  93. {plain-0.50.0 → plain-0.52.0}/plain/runtime/__init__.py +0 -0
  94. {plain-0.50.0 → plain-0.52.0}/plain/runtime/global_settings.py +0 -0
  95. {plain-0.50.0 → plain-0.52.0}/plain/runtime/user_settings.py +0 -0
  96. {plain-0.50.0 → plain-0.52.0}/plain/signals/README.md +0 -0
  97. {plain-0.50.0 → plain-0.52.0}/plain/signals/__init__.py +0 -0
  98. {plain-0.50.0 → plain-0.52.0}/plain/signals/dispatch/__init__.py +0 -0
  99. {plain-0.50.0 → plain-0.52.0}/plain/signals/dispatch/dispatcher.py +0 -0
  100. {plain-0.50.0 → plain-0.52.0}/plain/signals/dispatch/license.txt +0 -0
  101. {plain-0.50.0 → plain-0.52.0}/plain/signing.py +0 -0
  102. {plain-0.50.0 → plain-0.52.0}/plain/templates/README.md +0 -0
  103. {plain-0.50.0 → plain-0.52.0}/plain/templates/__init__.py +0 -0
  104. {plain-0.50.0 → plain-0.52.0}/plain/templates/core.py +0 -0
  105. {plain-0.50.0 → plain-0.52.0}/plain/templates/jinja/__init__.py +0 -0
  106. {plain-0.50.0 → plain-0.52.0}/plain/templates/jinja/environments.py +0 -0
  107. {plain-0.50.0 → plain-0.52.0}/plain/templates/jinja/extensions.py +0 -0
  108. {plain-0.50.0 → plain-0.52.0}/plain/templates/jinja/filters.py +0 -0
  109. {plain-0.50.0 → plain-0.52.0}/plain/templates/jinja/globals.py +0 -0
  110. {plain-0.50.0 → plain-0.52.0}/plain/test/README.md +0 -0
  111. {plain-0.50.0 → plain-0.52.0}/plain/test/__init__.py +0 -0
  112. {plain-0.50.0 → plain-0.52.0}/plain/test/client.py +0 -0
  113. {plain-0.50.0 → plain-0.52.0}/plain/test/encoding.py +0 -0
  114. {plain-0.50.0 → plain-0.52.0}/plain/test/exceptions.py +0 -0
  115. {plain-0.50.0 → plain-0.52.0}/plain/urls/README.md +0 -0
  116. {plain-0.50.0 → plain-0.52.0}/plain/urls/__init__.py +0 -0
  117. {plain-0.50.0 → plain-0.52.0}/plain/urls/converters.py +0 -0
  118. {plain-0.50.0 → plain-0.52.0}/plain/urls/exceptions.py +0 -0
  119. {plain-0.50.0 → plain-0.52.0}/plain/urls/patterns.py +0 -0
  120. {plain-0.50.0 → plain-0.52.0}/plain/urls/resolvers.py +0 -0
  121. {plain-0.50.0 → plain-0.52.0}/plain/urls/routers.py +0 -0
  122. {plain-0.50.0 → plain-0.52.0}/plain/urls/utils.py +0 -0
  123. {plain-0.50.0 → plain-0.52.0}/plain/utils/README.md +0 -0
  124. {plain-0.50.0 → plain-0.52.0}/plain/utils/__init__.py +0 -0
  125. {plain-0.50.0 → plain-0.52.0}/plain/utils/cache.py +0 -0
  126. {plain-0.50.0 → plain-0.52.0}/plain/utils/crypto.py +0 -0
  127. {plain-0.50.0 → plain-0.52.0}/plain/utils/datastructures.py +0 -0
  128. {plain-0.50.0 → plain-0.52.0}/plain/utils/dateparse.py +0 -0
  129. {plain-0.50.0 → plain-0.52.0}/plain/utils/deconstruct.py +0 -0
  130. {plain-0.50.0 → plain-0.52.0}/plain/utils/decorators.py +0 -0
  131. {plain-0.50.0 → plain-0.52.0}/plain/utils/duration.py +0 -0
  132. {plain-0.50.0 → plain-0.52.0}/plain/utils/encoding.py +0 -0
  133. {plain-0.50.0 → plain-0.52.0}/plain/utils/functional.py +0 -0
  134. {plain-0.50.0 → plain-0.52.0}/plain/utils/hashable.py +0 -0
  135. {plain-0.50.0 → plain-0.52.0}/plain/utils/html.py +0 -0
  136. {plain-0.50.0 → plain-0.52.0}/plain/utils/http.py +0 -0
  137. {plain-0.50.0 → plain-0.52.0}/plain/utils/inspect.py +0 -0
  138. {plain-0.50.0 → plain-0.52.0}/plain/utils/ipv6.py +0 -0
  139. {plain-0.50.0 → plain-0.52.0}/plain/utils/itercompat.py +0 -0
  140. {plain-0.50.0 → plain-0.52.0}/plain/utils/module_loading.py +0 -0
  141. {plain-0.50.0 → plain-0.52.0}/plain/utils/regex_helper.py +0 -0
  142. {plain-0.50.0 → plain-0.52.0}/plain/utils/safestring.py +0 -0
  143. {plain-0.50.0 → plain-0.52.0}/plain/utils/text.py +0 -0
  144. {plain-0.50.0 → plain-0.52.0}/plain/utils/timesince.py +0 -0
  145. {plain-0.50.0 → plain-0.52.0}/plain/utils/timezone.py +0 -0
  146. {plain-0.50.0 → plain-0.52.0}/plain/utils/tree.py +0 -0
  147. {plain-0.50.0 → plain-0.52.0}/plain/validators.py +0 -0
  148. {plain-0.50.0 → plain-0.52.0}/plain/views/README.md +0 -0
  149. {plain-0.50.0 → plain-0.52.0}/plain/views/__init__.py +0 -0
  150. {plain-0.50.0 → plain-0.52.0}/plain/views/base.py +0 -0
  151. {plain-0.50.0 → plain-0.52.0}/plain/views/csrf.py +0 -0
  152. {plain-0.50.0 → plain-0.52.0}/plain/views/errors.py +0 -0
  153. {plain-0.50.0 → plain-0.52.0}/plain/views/exceptions.py +0 -0
  154. {plain-0.50.0 → plain-0.52.0}/plain/views/forms.py +0 -0
  155. {plain-0.50.0 → plain-0.52.0}/plain/views/objects.py +0 -0
  156. {plain-0.50.0 → plain-0.52.0}/plain/views/redirect.py +0 -0
  157. {plain-0.50.0 → plain-0.52.0}/plain/views/templates.py +0 -0
  158. {plain-0.50.0 → plain-0.52.0}/plain/wsgi.py +0 -0
  159. {plain-0.50.0 → plain-0.52.0}/tests/.gitignore +0 -0
  160. {plain-0.50.0 → plain-0.52.0}/tests/app/.gitignore +0 -0
  161. {plain-0.50.0 → plain-0.52.0}/tests/app/settings.py +0 -0
  162. {plain-0.50.0 → plain-0.52.0}/tests/app/test/__init__.py +0 -0
  163. {plain-0.50.0 → plain-0.52.0}/tests/app/test/default_settings.py +0 -0
  164. {plain-0.50.0 → plain-0.52.0}/tests/app/urls.py +0 -0
  165. {plain-0.50.0 → plain-0.52.0}/tests/conftest.py +0 -0
  166. {plain-0.50.0 → plain-0.52.0}/tests/test_runtime.py +0 -0
  167. {plain-0.50.0 → plain-0.52.0}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.50.0
3
+ Version: 0.52.0
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
@@ -0,0 +1,58 @@
1
+ # plain changelog
2
+
3
+ ## [0.52.0](https://github.com/dropseed/plain/releases/plain@0.52.0) (2025-06-26)
4
+
5
+ ### What's changed
6
+
7
+ - Added `plain-changelog` as a standalone executable so you can view changelogs without importing the full framework ([e4e7324](https://github.com/dropseed/plain/commit/e4e7324cd284c800ff957933748d6639615cbea6))
8
+ - Removed the runtime dependency on the `packaging` library by replacing it with an internal version-comparison helper ([e4e7324](https://github.com/dropseed/plain/commit/e4e7324cd284c800ff957933748d6639615cbea6))
9
+ - Improved the error message when a package changelog cannot be found, now showing the path that was looked up ([f3c82bb](https://github.com/dropseed/plain/commit/f3c82bb59e07c1bddbdb2557f2043e039c1cd1e9))
10
+ - Fixed an f-string issue that broke `plain.debug.dd` on Python 3.11 ([ed24276](https://github.com/dropseed/plain/commit/ed24276a12191e4c8903369002dd32b69eb358b3))
11
+
12
+ ### Upgrade instructions
13
+
14
+ - No changes required
15
+
16
+ ## [0.51.0](https://github.com/dropseed/plain/releases/plain@0.51.0) (2025-06-24)
17
+
18
+ ### What's changed
19
+
20
+ - New `plain changelog` CLI sub-command to quickly view a package’s changelog from the terminal. Supports `--from`/`--to` flags to limit the version range ([50f0de7](https://github.com/dropseed/plain/commit/50f0de721f263ec6274852bd8838f4e5037b27dc)).
21
+
22
+ ### Upgrade instructions
23
+
24
+ - No changes required
25
+
26
+ ## [0.50.0](https://github.com/dropseed/plain/releases/plain@0.50.0) (2025-06-23)
27
+
28
+ ### What's changed
29
+
30
+ - The URL inspection command has moved; run `plain urls list` instead of the old `plain urls` command ([6146fcb](https://github.com/dropseed/plain/commit/6146fcba536c551277d625bd750c385431ea18eb))
31
+ - `plain preflight` gains a simpler `--database` flag that enables database checks for your default database. The previous behaviour that accepted one or more database aliases has been removed ([d346d81](https://github.com/dropseed/plain/commit/d346d81567d2cc45bbed93caba18a195de10c572))
32
+ - Settings overhaul: use a single `DATABASE` setting instead of `DATABASES`/`DATABASE_ROUTERS` ([d346d81](https://github.com/dropseed/plain/commit/d346d81567d2cc45bbed93caba18a195de10c572))
33
+
34
+ ### Upgrade instructions
35
+
36
+ - Update any scripts or documentation that call `plain urls …`:
37
+ - Replace `plain urls --flat` with `plain urls list --flat`
38
+ - If you invoke preflight checks in CI or locally:
39
+ - Replace `plain preflight --database <alias>` (or multiple aliases) with the new boolean flag: `plain preflight --database`
40
+ - In `settings.py` migrate to the new database configuration:
41
+
42
+ ```python
43
+ # Before
44
+ DATABASES = {
45
+ "default": {
46
+ "ENGINE": "plain.backends.sqlite3",
47
+ "NAME": BASE_DIR / "db.sqlite3",
48
+ }
49
+ }
50
+
51
+ # After
52
+ DATABASE = {
53
+ "ENGINE": "plain.backends.sqlite3",
54
+ "NAME": BASE_DIR / "db.sqlite3",
55
+ }
56
+ ```
57
+
58
+ Remove any `DATABASES` and `DATABASE_ROUTERS` settings – they are no longer read.
@@ -0,0 +1,109 @@
1
+ import re
2
+ from importlib.util import find_spec
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from .output import style_markdown
8
+
9
+
10
+ def parse_version(version_str):
11
+ """Parse a version string into a tuple of integers for comparison."""
12
+ # Remove 'v' prefix if present and split by dots
13
+ clean_version = version_str.lstrip("v")
14
+ parts = []
15
+ for part in clean_version.split("."):
16
+ # Extract numeric part from each segment
17
+ numeric_part = re.match(r"\d+", part)
18
+ if numeric_part:
19
+ parts.append(int(numeric_part.group()))
20
+ else:
21
+ parts.append(0)
22
+ return tuple(parts)
23
+
24
+
25
+ def compare_versions(v1, v2):
26
+ """Compare two version strings. Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
27
+ parsed_v1 = parse_version(v1)
28
+ parsed_v2 = parse_version(v2)
29
+
30
+ # Pad shorter version with zeros
31
+ max_len = max(len(parsed_v1), len(parsed_v2))
32
+ parsed_v1 += (0,) * (max_len - len(parsed_v1))
33
+ parsed_v2 += (0,) * (max_len - len(parsed_v2))
34
+
35
+ if parsed_v1 < parsed_v2:
36
+ return -1
37
+ elif parsed_v1 > parsed_v2:
38
+ return 1
39
+ else:
40
+ return 0
41
+
42
+
43
+ @click.command("changelog")
44
+ @click.argument("package_label")
45
+ @click.option("--from", "from_version", help="Show entries from this version onwards")
46
+ @click.option("--to", "to_version", help="Show entries up to this version")
47
+ def changelog(package_label, from_version, to_version):
48
+ """Show changelog entries for a package."""
49
+ module_name = package_label.replace("-", ".")
50
+ spec = find_spec(module_name)
51
+ if not spec:
52
+ raise click.ClickException(f"Package {package_label} not found")
53
+
54
+ if spec.origin:
55
+ package_path = Path(spec.origin).resolve().parent
56
+ elif spec.submodule_search_locations:
57
+ package_path = Path(list(spec.submodule_search_locations)[0]).resolve()
58
+ else:
59
+ raise click.ClickException(f"Package {package_label} not found")
60
+
61
+ changelog_path = package_path / "CHANGELOG.md"
62
+ if not changelog_path.exists():
63
+ raise click.ClickException(
64
+ f"Changelog not found for {package_label} ({changelog_path})"
65
+ )
66
+
67
+ content = changelog_path.read_text()
68
+
69
+ entries = []
70
+ current_version = None
71
+ current_lines = []
72
+ version_re = re.compile(r"^## \[([^\]]+)\]")
73
+
74
+ for line in content.splitlines(keepends=True):
75
+ m = version_re.match(line)
76
+ if m:
77
+ if current_version is not None:
78
+ entries.append((current_version, current_lines))
79
+ current_version = m.group(1)
80
+ current_lines = [line]
81
+ else:
82
+ if current_version is not None:
83
+ current_lines.append(line)
84
+
85
+ if current_version is not None:
86
+ entries.append((current_version, current_lines))
87
+
88
+ def version_found(version):
89
+ return any(compare_versions(v, version) == 0 for v, _ in entries)
90
+
91
+ if from_version and not version_found(from_version):
92
+ click.secho(
93
+ f"Warning: version {from_version} not found in changelog",
94
+ fg="yellow",
95
+ err=True,
96
+ )
97
+
98
+ selected_lines = []
99
+ for version, lines in entries:
100
+ if from_version and compare_versions(version, from_version) <= 0:
101
+ continue
102
+ if to_version and compare_versions(version, to_version) > 0:
103
+ continue
104
+ selected_lines.extend(lines)
105
+
106
+ if not selected_lines:
107
+ return
108
+
109
+ click.echo(style_markdown("".join(selected_lines)))
@@ -7,6 +7,7 @@ import plain.runtime
7
7
  from plain.exceptions import ImproperlyConfigured
8
8
 
9
9
  from .build import build
10
+ from .changelog import changelog
10
11
  from .chores import chores
11
12
  from .docs import docs
12
13
  from .formatting import PlainContext
@@ -32,6 +33,7 @@ plain_cli.add_command(chores)
32
33
  plain_cli.add_command(build)
33
34
  plain_cli.add_command(utils)
34
35
  plain_cli.add_command(urls)
36
+ plain_cli.add_command(changelog)
35
37
  plain_cli.add_command(setting)
36
38
  plain_cli.add_command(shell)
37
39
  plain_cli.add_command(run)
@@ -7,6 +7,8 @@ import click
7
7
 
8
8
  from plain.packages import packages_registry
9
9
 
10
+ from .output import iterate_markdown
11
+
10
12
 
11
13
  @click.command()
12
14
  @click.option("--llm", "llm", is_flag=True)
@@ -59,43 +61,7 @@ def docs(module, llm, open):
59
61
  if open:
60
62
  click.launch(str(readme_path))
61
63
  else:
62
-
63
- def _iterate_markdown(content):
64
- """
65
- Iterator that does basic markdown for a Click pager.
66
-
67
- Headings are yellow and bright, code blocks are indented.
68
- """
69
-
70
- in_code_block = False
71
- for line in content.splitlines():
72
- if line.startswith("```"):
73
- in_code_block = not in_code_block
74
-
75
- if in_code_block:
76
- yield click.style(line, dim=True)
77
- elif line.startswith("# "):
78
- yield click.style(line, fg="yellow", bold=True)
79
- elif line.startswith("## "):
80
- yield click.style(line, fg="yellow", bold=True)
81
- elif line.startswith("### "):
82
- yield click.style(line, fg="yellow", bold=True)
83
- elif line.startswith("#### "):
84
- yield click.style(line, fg="yellow", bold=True)
85
- elif line.startswith("##### "):
86
- yield click.style(line, fg="yellow", bold=True)
87
- elif line.startswith("###### "):
88
- yield click.style(line, fg="yellow", bold=True)
89
- elif line.startswith("**") and line.endswith("**"):
90
- yield click.style(line, bold=True)
91
- elif line.startswith("> "):
92
- yield click.style(line, italic=True)
93
- else:
94
- yield line
95
-
96
- yield "\n"
97
-
98
- click.echo_via_pager(_iterate_markdown(readme_path.read_text()))
64
+ click.echo_via_pager(iterate_markdown(readme_path.read_text()))
99
65
 
100
66
 
101
67
  class LLMDocs:
@@ -0,0 +1,41 @@
1
+ import click
2
+
3
+
4
+ def style_markdown(content):
5
+ return "".join(iterate_markdown(content))
6
+
7
+
8
+ def iterate_markdown(content):
9
+ """
10
+ Iterator that does basic markdown for a Click pager.
11
+
12
+ Headings are yellow and bright, code blocks are indented.
13
+ """
14
+
15
+ in_code_block = False
16
+ for line in content.splitlines():
17
+ if line.startswith("```"):
18
+ in_code_block = not in_code_block
19
+
20
+ if in_code_block:
21
+ yield click.style(line, dim=True)
22
+ elif line.startswith("# "):
23
+ yield click.style(line, fg="yellow", bold=True)
24
+ elif line.startswith("## "):
25
+ yield click.style(line, fg="yellow", bold=True)
26
+ elif line.startswith("### "):
27
+ yield click.style(line, fg="yellow", bold=True)
28
+ elif line.startswith("#### "):
29
+ yield click.style(line, fg="yellow", bold=True)
30
+ elif line.startswith("##### "):
31
+ yield click.style(line, fg="yellow", bold=True)
32
+ elif line.startswith("###### "):
33
+ yield click.style(line, fg="yellow", bold=True)
34
+ elif line.startswith("**") and line.endswith("**"):
35
+ yield click.style(line, bold=True)
36
+ elif line.startswith("> "):
37
+ yield click.style(line, italic=True)
38
+ else:
39
+ yield line
40
+
41
+ yield "\n"
@@ -13,13 +13,14 @@ def dd(*objs):
13
13
  Dump the object and raise a ResponseException with the dump as the response content.
14
14
  """
15
15
 
16
- print(f"Dumping objects:\n{'\n'.join([pformat(obj) for obj in objs])}")
16
+ print_objs = "\n".join([pformat(obj) for obj in objs])
17
+ print(f"Dumping objects:\n{print_objs}")
17
18
 
18
- dump_strs = [
19
+ html_objs = [
19
20
  Markup("<pre><code>") + escape(pformat(obj)) + Markup("</code></pre>")
20
21
  for obj in objs
21
22
  ]
22
- combined_dump_str = Markup("\n\n").join(dump_strs)
23
+ combined_dump_str = Markup("\n\n").join(html_objs)
23
24
 
24
25
  response = Response()
25
26
  response.status_code = 500
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain"
3
- version = "0.50.0"
3
+ version = "0.52.0"
4
4
  description = "A web framework for building products with Python."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
@@ -13,6 +13,10 @@ requires-python = ">=3.11"
13
13
  [project.scripts]
14
14
  plain = "plain.cli.core:cli"
15
15
 
16
+ # Make this directly available without loading Plain,
17
+ # for use in upgrade scripts.
18
+ plain-changelog = "plain.cli.changelog:changelog"
19
+
16
20
  [tool.uv]
17
21
  dev-dependencies = [
18
22
  "pytest>=8.0.0",
@@ -23,3 +23,22 @@ def test_plain_help_command():
23
23
  assert result.exit_code == 0
24
24
  assert "Usage: plain build" in result.output
25
25
  assert "Usage: plain shell" in result.output
26
+
27
+
28
+ def test_plain_changelog_plain():
29
+ runner = CliRunner()
30
+ result = runner.invoke(cli, ["changelog", "plain"], prog_name="plain")
31
+ assert result.exit_code == 0
32
+ assert "0.50.0" in result.output
33
+
34
+
35
+ def test_plain_changelog_range_warning():
36
+ runner = CliRunner()
37
+ result = runner.invoke(
38
+ cli,
39
+ ["changelog", "plain", "--from", "0.49.0", "--to", "0.50.0"],
40
+ prog_name="plain",
41
+ )
42
+ assert result.exit_code == 0
43
+ assert "0.50.0" in result.output
44
+ assert "Warning" in result.output
@@ -1,33 +0,0 @@
1
- # plain changelog
2
-
3
- ## [0.50.0](https://github.com/dropseed/plain/releases/plain@0.50.0) (2025-06-23)
4
-
5
- ### What's changed
6
-
7
- - The URL inspection command has moved; run `plain urls list` instead of the old `plain urls` command ([6146fcb](https://github.com/dropseed/plain/commit/6146fcba536c551277d625bd750c385431ea18eb))
8
- - `plain preflight` gains a simpler `--database` flag that enables database checks for your default database. The previous behaviour that accepted one or more database aliases has been removed ([d346d81](https://github.com/dropseed/plain/commit/d346d81567d2cc45bbed93caba18a195de10c572))
9
- - Settings overhaul: use a single `DATABASE` setting instead of `DATABASES`/`DATABASE_ROUTERS` ([d346d81](https://github.com/dropseed/plain/commit/d346d81567d2cc45bbed93caba18a195de10c572))
10
-
11
- ### Upgrade instructions
12
-
13
- - Update any scripts or documentation that call `plain urls …`:
14
- - Replace `plain urls --flat` with `plain urls list --flat`
15
- - If you invoke preflight checks in CI or locally:
16
- - Replace `plain preflight --database <alias>` (or multiple aliases) with the new boolean flag: `plain preflight --database`
17
- - In `settings.py` migrate to the new database configuration:
18
- ```python
19
- # Before
20
- DATABASES = {
21
- "default": {
22
- "ENGINE": "plain.backends.sqlite3",
23
- "NAME": BASE_DIR / "db.sqlite3",
24
- }
25
- }
26
-
27
- # After
28
- DATABASE = {
29
- "ENGINE": "plain.backends.sqlite3",
30
- "NAME": BASE_DIR / "db.sqlite3",
31
- }
32
- ```
33
- Remove any `DATABASES` and `DATABASE_ROUTERS` settings – they are no longer read.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes