plain 0.27.0__py3-none-any.whl → 0.29.0__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/__main__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .cli import cli
1
+ from .cli.core import cli
2
2
 
3
3
  # Make the CLI available as `python -m plain`
4
4
  if __name__ == "__main__":
plain/cli/README.md CHANGED
@@ -99,19 +99,3 @@ then any commands you defined.
99
99
  $ plain <pkg> hello
100
100
  Hello, world!
101
101
  ```
102
-
103
- ### Add CLI commands to published packages
104
-
105
- Some packages, like [plain-dev](https://plainframework.com/docs/plain-dev/),
106
- never show up in `INSTALLED_PACKAGES` but still have CLI commands.
107
- These are detected via Python entry points.
108
-
109
- An example with `pyproject.toml` and UV:
110
-
111
- ```toml
112
- # pyproject.toml
113
- [project.entry-points."plain.cli"]
114
- "dev" = "plain.dev:cli"
115
- "pre-commit" = "plain.dev.precommit:cli"
116
- "contrib" = "plain.dev.contribute:cli"
117
- ```
plain/cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from .cli import cli
1
+ from .registry import register_cli
2
2
 
3
- __all__ = ["cli"]
3
+ __all__ = ["register_cli"]
@@ -1,4 +1,3 @@
1
- import importlib
2
1
  import os
3
2
  import shutil
4
3
  import subprocess
@@ -6,7 +5,6 @@ import sys
6
5
  import tomllib
7
6
  import traceback
8
7
  from importlib.metadata import entry_points
9
- from importlib.util import find_spec
10
8
  from pathlib import Path
11
9
 
12
10
  import click
@@ -20,7 +18,7 @@ from plain.packages import packages_registry
20
18
  from plain.utils.crypto import get_random_string
21
19
 
22
20
  from .formatting import PlainContext
23
- from .packages import EntryPointGroup, InstalledPackagesGroup
21
+ from .registry import cli_registry
24
22
 
25
23
 
26
24
  @click.group()
@@ -500,25 +498,21 @@ def urls(flat):
500
498
  print_tree(resolver.url_patterns)
501
499
 
502
500
 
503
- class AppCLIGroup(click.Group):
501
+ class CLIRegistryGroup(click.Group):
504
502
  """
505
- Loads app.cli if it exists as `plain app`
503
+ Click Group that exposes commands from the CLI registry.
506
504
  """
507
505
 
508
- MODULE_NAME = "app.cli"
506
+ def __init__(self, *args, **kwargs):
507
+ super().__init__(*args, **kwargs)
508
+ cli_registry.import_modules()
509
509
 
510
510
  def list_commands(self, ctx):
511
- if find_spec(self.MODULE_NAME):
512
- return ["app"]
513
- else:
514
- return []
511
+ return sorted(cli_registry.get_commands().keys())
515
512
 
516
513
  def get_command(self, ctx, name):
517
- if name != "app":
518
- return
519
-
520
- cli = importlib.import_module(self.MODULE_NAME)
521
- return cli.cli
514
+ commands = cli_registry.get_commands()
515
+ return commands.get(name)
522
516
 
523
517
 
524
518
  class PlainCommandCollection(click.CommandCollection):
@@ -531,9 +525,7 @@ class PlainCommandCollection(click.CommandCollection):
531
525
  plain.runtime.setup()
532
526
 
533
527
  sources = [
534
- InstalledPackagesGroup(),
535
- EntryPointGroup(),
536
- AppCLIGroup(),
528
+ CLIRegistryGroup(),
537
529
  plain_cli,
538
530
  ]
539
531
  except plain.runtime.AppPathNotFound:
@@ -545,7 +537,6 @@ class PlainCommandCollection(click.CommandCollection):
545
537
  )
546
538
 
547
539
  sources = [
548
- EntryPointGroup(),
549
540
  plain_cli,
550
541
  ]
551
542
  except ImproperlyConfigured as e:
plain/cli/registry.py ADDED
@@ -0,0 +1,62 @@
1
+ from importlib import import_module
2
+ from importlib.util import find_spec
3
+
4
+ from plain.packages import packages_registry
5
+
6
+
7
+ class CLIRegistry:
8
+ def __init__(self):
9
+ self._commands = {}
10
+
11
+ def register_command(self, cmd, name):
12
+ """
13
+ Register a CLI command or group with the specified name.
14
+ """
15
+ self._commands[name] = cmd
16
+
17
+ def import_modules(self):
18
+ """
19
+ Import modules from installed packages and app to trigger registration.
20
+ """
21
+ # Import from installed packages
22
+ for package_config in packages_registry.get_package_configs():
23
+ import_name = f"{package_config.name}.cli"
24
+ try:
25
+ import_module(import_name)
26
+ except ModuleNotFoundError:
27
+ pass
28
+
29
+ # Import from app
30
+ import_name = "app.cli"
31
+ if find_spec(import_name):
32
+ try:
33
+ import_module(import_name)
34
+ except ModuleNotFoundError:
35
+ pass
36
+
37
+ def get_commands(self):
38
+ """
39
+ Get all registered commands.
40
+ """
41
+ return self._commands
42
+
43
+
44
+ cli_registry = CLIRegistry()
45
+
46
+
47
+ def register_cli(name):
48
+ """
49
+ Register a CLI command or group with the given name.
50
+
51
+ Usage:
52
+ @register_cli("users")
53
+ @click.group()
54
+ def users_cli():
55
+ pass
56
+ """
57
+
58
+ def wrapper(cmd):
59
+ cli_registry.register_command(cmd, name)
60
+ return cmd
61
+
62
+ return wrapper
plain/packages/config.py CHANGED
@@ -10,8 +10,6 @@ CONFIG_MODULE_NAME = "config"
10
10
  class PackageConfig:
11
11
  """Class representing a Plain application and its configuration."""
12
12
 
13
- migrations_module = "migrations"
14
-
15
13
  def __init__(self, name, *, label=""):
16
14
  # Full Python path to the application e.g. 'plain.admin.admin'.
17
15
  self.name = name
plain/runtime/__init__.py CHANGED
@@ -51,9 +51,24 @@ def setup():
51
51
  packages_registry.populate(settings.INSTALLED_PACKAGES)
52
52
 
53
53
 
54
+ class SettingsReference(str):
55
+ """
56
+ String subclass which references a current settings value. It's treated as
57
+ the value in memory but serializes to a settings.NAME attribute reference.
58
+ """
59
+
60
+ def __new__(self, setting_name):
61
+ value = getattr(settings, setting_name)
62
+ return str.__new__(self, value)
63
+
64
+ def __init__(self, setting_name):
65
+ self.setting_name = setting_name
66
+
67
+
54
68
  __all__ = [
55
69
  "setup",
56
70
  "settings",
71
+ "SettingsReference",
57
72
  "APP_PATH",
58
73
  "__version__",
59
74
  ]
@@ -298,16 +298,3 @@ class SettingDefinition:
298
298
 
299
299
  def __str__(self):
300
300
  return f"SettingDefinition(name={self.name}, value={self.value}, source={self.source})"
301
-
302
-
303
- class SettingsReference(str):
304
- """
305
- String subclass which references a current settings value. It's treated as
306
- the value in memory but serializes to a settings.NAME attribute reference.
307
- """
308
-
309
- def __new__(self, value, setting_name):
310
- return str.__new__(self, value)
311
-
312
- def __init__(self, value, setting_name):
313
- self.setting_name = setting_name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.27.0
3
+ Version: 0.29.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
@@ -1,5 +1,5 @@
1
1
  plain/README.md,sha256=nW3Ioj3IxPb6aoCGaFMN2n7Cd7LMx0s8Lph6pMkKnh4,8
2
- plain/__main__.py,sha256=BiYbF-txGNbeRqp_CHQ9EZ_bCbbKq2iw51Z8RRUgIBY,105
2
+ plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
3
3
  plain/debug.py,sha256=abPkJY4aSbBYGEYSZST_ZY3ohXPGDdz9uWQBYRqfd3M,730
4
4
  plain/exceptions.py,sha256=Z9cbPE5im_Y-bjVq8cqC85gBoqOr80rLFG5wTKixrwE,5894
5
5
  plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
@@ -14,12 +14,12 @@ plain/assets/finders.py,sha256=rhkHG5QW3H3IlBGHB5WJf9J6VTdDWgUC0qEs6u2Z4RQ,1233
14
14
  plain/assets/fingerprints.py,sha256=1NKAnnXVlncY5iimXztr0NL3RIjBKsNlZRIe6nmItJc,931
15
15
  plain/assets/urls.py,sha256=lW7VzKNzTKY11JqbszhJQ1Yy0HtljZlsHDnnkTPdLOM,992
16
16
  plain/assets/views.py,sha256=z7noLzoelGw_8-MXcvGKjXs9KZ43Tivmy2TIfnZIpgw,9253
17
- plain/cli/README.md,sha256=CwrqK-NV5ZK1JDYfR4480uHXmJEyxWF6jA_K40_llE8,2366
18
- plain/cli/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
19
- plain/cli/cli.py,sha256=NbkgCiJk-QaAGUBU7Dne37B_Xv3sZPcGyA5IBG4nFIo,18420
17
+ plain/cli/README.md,sha256=t3k4jmSK0QFALO3bVWTUsJC09KhY4CvauStTvVLLUdI,1922
18
+ plain/cli/__init__.py,sha256=6w9T7K2WrPwh6DcaMb2oNt_CWU6Bc57nUTO2Bt1p38Y,63
19
+ plain/cli/core.py,sha256=5_4hsXxzk2ocIez5BBY2YoYtFUdyfKks-2twpqy-zoU,18256
20
20
  plain/cli/formatting.py,sha256=1hZH13y1qwHcU2K2_Na388nw9uvoeQH8LrWL-O9h8Yc,2207
21
- plain/cli/packages.py,sha256=GLvDgQ1o93tSHae_B2i0YNimpt3LGu4QMQpFYrO48d8,2758
22
21
  plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
22
+ plain/cli/registry.py,sha256=yKVMSDjW8g10nlV9sPXFGJQmhC_U-k4J4kM7N2OQVLA,1467
23
23
  plain/cli/startup.py,sha256=3LIz9JrIZoF52Sa0j0SCypQwEaBDkhvuGaBdtiQLr5Q,680
24
24
  plain/csrf/README.md,sha256=RXMWMtHmzf30gVVNOfj0kD4xlSqFIPgJh-n7dIciaEM,163
25
25
  plain/csrf/middleware.py,sha256=FYhT7KPJ664Sm0nKjeej1OIXalvVTYiotQX3ytI0dfY,17417
@@ -61,7 +61,7 @@ plain/logs/loggers.py,sha256=iz9SYcwP9w5QAuwpULl48SFkVyJuuMoQ_fdLgdCHpNg,2121
61
61
  plain/logs/utils.py,sha256=9UzdCCQXJinGDs71Ngw297mlWkhgZStSd67ya4NOW98,1257
62
62
  plain/packages/README.md,sha256=Vq1Nw3mmEmZ2IriQavuVi4BjcQC2nb8k7YIbnm8QjIg,799
63
63
  plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
64
- plain/packages/config.py,sha256=Gmu7QW4Z3Cx9_d7N2D5o7t292t7vAOE8aL-3yO7sGqc,3327
64
+ plain/packages/config.py,sha256=uOO7uE9jajqDhqFBafJQ3ZnfLmQiHikTzOSJ1AlP7ZM,3289
65
65
  plain/packages/registry.py,sha256=Aklno7y7UrBZlidtUR_YO3B5xqF46UbUtalReNcYHm8,7937
66
66
  plain/preflight/README.md,sha256=-PKVd0RBMh4ROiMkegPS2PgvT1Kq9qqN1KfNkmUSdFc,177
67
67
  plain/preflight/__init__.py,sha256=H-TNRvaddPtOGmv4RXoc1fxDV1AOb7_K3u7ECF8mV58,607
@@ -71,9 +71,9 @@ plain/preflight/registry.py,sha256=7s7f_iEwURzv-Ye515P5lJWcHltd5Ca2fsX1Wpbf1wQ,2
71
71
  plain/preflight/security.py,sha256=sNpv5AHobPcaO48cOUGRNe2EjusTducjY8vyShR8EhI,2645
72
72
  plain/preflight/urls.py,sha256=OSTLvCpftAD_8VbQ0V3p1CTPlRRwtlnXVBZeWgr7l2k,2881
73
73
  plain/runtime/README.md,sha256=Q8VVO7JRGuYrDxzuYL6ptoilhclbecxKzpRXKgbWGkU,2061
74
- plain/runtime/__init__.py,sha256=FyGTIx-633bNPrPv8IBarBKvu_XspbCiqj8W8eOd_mA,1528
74
+ plain/runtime/__init__.py,sha256=o2RVETiL8U0lMFBpbtfnxflhw_4MFllMV6CEpX3RqZs,1965
75
75
  plain/runtime/global_settings.py,sha256=SfOhwzpZe2zpNqSpdx3hHgCN89xdbW9KJVR4KJfS_Gk,5498
76
- plain/runtime/user_settings.py,sha256=r6uQ-h0QxX3gSB_toJJekEbSikXXdNSb8ykUtwGTpdY,11280
76
+ plain/runtime/user_settings.py,sha256=uRHHVfzUvHon91_fOKj7K2WaBYwJ1gCPLfeXqKj5CTs,10902
77
77
  plain/signals/README.md,sha256=cd3tKEgH-xc88CUWyDxl4-qv-HBXx8VT32BXVwA5azA,230
78
78
  plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
79
79
  plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
@@ -134,8 +134,8 @@ plain/views/forms.py,sha256=RhlaUcZCkeqokY_fvv-NOS-kgZAG4XhDLOPbf9K_Zlc,2691
134
134
  plain/views/objects.py,sha256=g5Lzno0Zsv0K449UpcCtxwCoO7WMRAWqKlxxV2V0_qg,8263
135
135
  plain/views/redirect.py,sha256=9zHZgKvtSkdrMX9KmsRM8hJTPmBktxhc4d8OitbuniI,1724
136
136
  plain/views/templates.py,sha256=cBkFNCSXgVi8cMqQbhsqJ4M_rIQYVl8cUvq9qu4YIes,1951
137
- plain-0.27.0.dist-info/METADATA,sha256=5bkOqMEQNCnkyEx011gMDQNmLjzncCOXJ2zPGti0HHo,319
138
- plain-0.27.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
- plain-0.27.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
140
- plain-0.27.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
141
- plain-0.27.0.dist-info/RECORD,,
137
+ plain-0.29.0.dist-info/METADATA,sha256=ONh32xHnWT3cqrGSkuvVsTvkci_KvLkY95bBGf3GW2Y,319
138
+ plain-0.29.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
+ plain-0.29.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
140
+ plain-0.29.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
141
+ plain-0.29.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ plain = plain.cli.core:cli
plain/cli/packages.py DELETED
@@ -1,94 +0,0 @@
1
- import importlib
2
- from importlib.metadata import entry_points
3
- from importlib.util import find_spec
4
-
5
- import click
6
-
7
- from plain.packages import packages_registry
8
-
9
-
10
- class InstalledPackagesGroup(click.Group):
11
- """
12
- Packages in INSTALLED_PACKAGES with a cli.py module
13
- will be discovered automatically.
14
- """
15
-
16
- PLAIN_APPS_PREFIX = "plain."
17
- APP_PREFIX = "app."
18
- MODULE_NAME = "cli"
19
-
20
- def list_commands(self, ctx):
21
- command_names = []
22
-
23
- # Get installed packages with a cli.py module
24
- for app in packages_registry.get_package_configs():
25
- if not find_spec(f"{app.name}.{self.MODULE_NAME}"):
26
- continue
27
-
28
- cli_name = app.name
29
-
30
- # Change plain.{pkg} to just {pkg}
31
- if cli_name.startswith(self.PLAIN_APPS_PREFIX):
32
- cli_name = cli_name[len(self.PLAIN_APPS_PREFIX) :]
33
-
34
- # Change app.{pkg} to just {pkg}
35
- if cli_name.startswith(self.APP_PREFIX):
36
- cli_name = cli_name[len(self.APP_PREFIX) :]
37
-
38
- if cli_name in command_names:
39
- raise ValueError(
40
- f"Duplicate command name {cli_name} found in installed packages."
41
- )
42
-
43
- command_names.append(cli_name)
44
-
45
- return command_names
46
-
47
- def get_command(self, ctx, name):
48
- # Try it as plain.x, app.x, and just x (we don't know ahead of time which it is)
49
- for n in [self.PLAIN_APPS_PREFIX + name, self.APP_PREFIX + name, name]:
50
- try:
51
- if not find_spec(n):
52
- # plain.<name> doesn't exist at all
53
- continue
54
- except ModuleNotFoundError:
55
- continue
56
-
57
- try:
58
- if not find_spec(f"{n}.{self.MODULE_NAME}"):
59
- continue
60
- except ModuleNotFoundError:
61
- continue
62
-
63
- cli = importlib.import_module(f"{n}.{self.MODULE_NAME}")
64
-
65
- # Get the app's cli.py group
66
- try:
67
- return cli.cli
68
- except AttributeError:
69
- continue
70
-
71
-
72
- class EntryPointGroup(click.Group):
73
- """
74
- Python packages can be added to the Plain CLI
75
- via the plain_cli entrypoint in their setup.py.
76
-
77
- This is intended for packages that don't go in INSTALLED_PACKAGES.
78
- """
79
-
80
- ENTRYPOINT_NAME = "plain.cli"
81
-
82
- def list_commands(self, ctx):
83
- rv = []
84
-
85
- for entry_point in entry_points().select(group=self.ENTRYPOINT_NAME):
86
- rv.append(entry_point.name)
87
-
88
- rv.sort()
89
- return rv
90
-
91
- def get_command(self, ctx, name):
92
- for entry_point in entry_points().select(group=self.ENTRYPOINT_NAME):
93
- if entry_point.name == name:
94
- return entry_point.load()
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- plain = plain.cli:cli
File without changes