plain 0.25.0__py3-none-any.whl → 0.26.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.
@@ -1,4 +1,4 @@
1
1
  from .config import PackageConfig
2
- from .registry import packages_registry
2
+ from .registry import packages_registry, register_config
3
3
 
4
- __all__ = ["PackageConfig", "packages_registry"]
4
+ __all__ = ["PackageConfig", "packages_registry", "register_config"]
plain/packages/config.py CHANGED
@@ -1,9 +1,8 @@
1
- import inspect
2
1
  import os
2
+ from functools import cached_property
3
3
  from importlib import import_module
4
4
 
5
5
  from plain.exceptions import ImproperlyConfigured
6
- from plain.utils.module_loading import import_string, module_has_submodule
7
6
 
8
7
  CONFIG_MODULE_NAME = "config"
9
8
 
@@ -13,13 +12,9 @@ class PackageConfig:
13
12
 
14
13
  migrations_module = "migrations"
15
14
 
16
- def __init__(self, package_name, package_module):
15
+ def __init__(self, name, *, label=""):
17
16
  # Full Python path to the application e.g. 'plain.admin.admin'.
18
- self.name = package_name
19
-
20
- # Root module for the application e.g. <module 'plain.admin.admin'
21
- # from 'admin/__init__.py'>.
22
- self.module = package_module
17
+ self.name = name
23
18
 
24
19
  # Reference to the Packages registry that holds this PackageConfig. Set by the
25
20
  # registry when it registers the PackageConfig instance.
@@ -27,168 +22,61 @@ class PackageConfig:
27
22
 
28
23
  # The following attributes could be defined at the class level in a
29
24
  # subclass, hence the test-and-set pattern.
25
+ if label and hasattr(self, "label"):
26
+ raise ImproperlyConfigured(
27
+ "PackageConfig class should not define a class label attribute and an init label"
28
+ )
29
+
30
+ if label:
31
+ # Set the label explicitly from the init
32
+ self.label = label
33
+ elif not hasattr(self, "label"):
34
+ # Last component of the Python path to the application e.g. 'admin'.
35
+ # This value must be unique across a Plain project.
36
+ self.label = self.name.rpartition(".")[2]
30
37
 
31
- # Last component of the Python path to the application e.g. 'admin'.
32
- # This value must be unique across a Plain project.
33
- if not hasattr(self, "label"):
34
- self.label = package_name.rpartition(".")[2]
35
38
  if not self.label.isidentifier():
36
39
  raise ImproperlyConfigured(
37
40
  f"The app label '{self.label}' is not a valid Python identifier."
38
41
  )
39
42
 
40
- # Filesystem path to the application directory e.g.
41
- # '/path/to/admin'.
42
- if not hasattr(self, "path"):
43
- self.path = self._path_from_module(package_module)
44
-
45
43
  def __repr__(self):
46
44
  return f"<{self.__class__.__name__}: {self.label}>"
47
45
 
48
- def _path_from_module(self, module):
49
- """Attempt to determine app's filesystem path from its module."""
50
- # See #21874 for extended discussion of the behavior of this method in
51
- # various cases.
52
- # Convert to list because __path__ may not support indexing.
53
- paths = list(getattr(module, "__path__", []))
54
- if len(paths) != 1:
55
- filename = getattr(module, "__file__", None)
56
- if filename is not None:
57
- paths = [os.path.dirname(filename)]
58
- else:
59
- # For unknown reasons, sometimes the list returned by __path__
60
- # contains duplicates that must be removed (#25246).
61
- paths = list(set(paths))
62
- if len(paths) > 1:
63
- raise ImproperlyConfigured(
64
- f"The app module {module!r} has multiple filesystem locations ({paths!r}); "
65
- "you must configure this app with an PackageConfig subclass "
66
- "with a 'path' class attribute."
67
- )
68
- elif not paths:
69
- raise ImproperlyConfigured(
70
- f"The app module {module!r} has no filesystem location, "
71
- "you must configure this app with an PackageConfig subclass "
72
- "with a 'path' class attribute."
73
- )
74
- return paths[0]
75
-
76
- @classmethod
77
- def create(cls, entry):
78
- """
79
- Factory that creates an app config from an entry in INSTALLED_PACKAGES.
80
- """
81
- # create() eventually returns package_config_class(package_name, package_module).
82
- package_config_class = None
83
- package_name = None
84
- package_module = None
85
-
86
- # If import_module succeeds, entry points to the app module.
87
- try:
88
- package_module = import_module(entry)
89
- except Exception:
90
- pass
91
- else:
92
- # If package_module has an packages submodule that defines a single
93
- # PackageConfig subclass, use it automatically.
94
- # To prevent this, an PackageConfig subclass can declare a class
95
- # variable default = False.
96
- # If the packages module defines more than one PackageConfig subclass,
97
- # the default one can declare default = True.
98
- if module_has_submodule(package_module, CONFIG_MODULE_NAME):
99
- mod_path = f"{entry}.{CONFIG_MODULE_NAME}"
100
- mod = import_module(mod_path)
101
- # Check if there's exactly one PackageConfig candidate,
102
- # excluding those that explicitly define default = False.
103
- package_configs = [
104
- (name, candidate)
105
- for name, candidate in inspect.getmembers(mod, inspect.isclass)
106
- if (
107
- issubclass(candidate, cls)
108
- and candidate is not cls
109
- and getattr(candidate, "default", True)
110
- )
111
- ]
112
- if len(package_configs) == 1:
113
- package_config_class = package_configs[0][1]
46
+ @cached_property
47
+ def path(self):
48
+ # Filesystem path to the application directory e.g.
49
+ # '/path/to/admin'.
50
+ def _path_from_module(module):
51
+ """Attempt to determine app's filesystem path from its module."""
52
+ # See #21874 for extended discussion of the behavior of this method in
53
+ # various cases.
54
+ # Convert to list because __path__ may not support indexing.
55
+ paths = list(getattr(module, "__path__", []))
56
+ if len(paths) != 1:
57
+ filename = getattr(module, "__file__", None)
58
+ if filename is not None:
59
+ paths = [os.path.dirname(filename)]
114
60
  else:
115
- # Check if there's exactly one PackageConfig subclass,
116
- # among those that explicitly define default = True.
117
- package_configs = [
118
- (name, candidate)
119
- for name, candidate in package_configs
120
- if getattr(candidate, "default", False)
121
- ]
122
- if len(package_configs) > 1:
123
- candidates = [repr(name) for name, _ in package_configs]
124
- raise RuntimeError(
125
- "{!r} declares more than one default PackageConfig: "
126
- "{}.".format(mod_path, ", ".join(candidates))
127
- )
128
- elif len(package_configs) == 1:
129
- package_config_class = package_configs[0][1]
130
-
131
- # Use the default app config class if we didn't find anything.
132
- if package_config_class is None:
133
- package_config_class = cls
134
- package_name = entry
135
-
136
- # If import_string succeeds, entry is an app config class.
137
- if package_config_class is None:
138
- try:
139
- package_config_class = import_string(entry)
140
- except Exception:
141
- pass
142
- # If both import_module and import_string failed, it means that entry
143
- # doesn't have a valid value.
144
- if package_module is None and package_config_class is None:
145
- # If the last component of entry starts with an uppercase letter,
146
- # then it was likely intended to be an app config class; if not,
147
- # an app module. Provide a nice error message in both cases.
148
- mod_path, _, cls_name = entry.rpartition(".")
149
- if mod_path and cls_name[0].isupper():
150
- # We could simply re-trigger the string import exception, but
151
- # we're going the extra mile and providing a better error
152
- # message for typos in INSTALLED_PACKAGES.
153
- # This may raise ImportError, which is the best exception
154
- # possible if the module at mod_path cannot be imported.
155
- mod = import_module(mod_path)
156
- candidates = [
157
- repr(name)
158
- for name, candidate in inspect.getmembers(mod, inspect.isclass)
159
- if issubclass(candidate, cls) and candidate is not cls
160
- ]
161
- msg = f"Module '{mod_path}' does not contain a '{cls_name}' class."
162
- if candidates:
163
- msg += " Choices are: {}.".format(", ".join(candidates))
164
- raise ImportError(msg)
165
- else:
166
- # Re-trigger the module import exception.
167
- import_module(entry)
168
-
169
- # Check for obvious errors. (This check prevents duck typing, but
170
- # it could be removed if it became a problem in practice.)
171
- if not issubclass(package_config_class, PackageConfig):
172
- raise ImproperlyConfigured(f"'{entry}' isn't a subclass of PackageConfig.")
173
-
174
- # Obtain package name here rather than in PackageClass.__init__ to keep
175
- # all error checking for entries in INSTALLED_PACKAGES in one place.
176
- if package_name is None:
177
- try:
178
- package_name = package_config_class.name
179
- except AttributeError:
180
- raise ImproperlyConfigured(f"'{entry}' must supply a name attribute.")
181
-
182
- # Ensure package_name points to a valid module.
183
- try:
184
- package_module = import_module(package_name)
185
- except ImportError:
186
- raise ImproperlyConfigured(
187
- f"Cannot import '{package_name}'. Check that '{package_config_class.__module__}.{package_config_class.__qualname__}.name' is correct."
188
- )
189
-
190
- # Entry is a path to an app config class.
191
- return package_config_class(package_name, package_module)
61
+ # For unknown reasons, sometimes the list returned by __path__
62
+ # contains duplicates that must be removed (#25246).
63
+ paths = list(set(paths))
64
+ if len(paths) > 1:
65
+ raise ImproperlyConfigured(
66
+ f"The app module {module!r} has multiple filesystem locations ({paths!r}); "
67
+ "you must configure this app with an PackageConfig subclass "
68
+ "with a 'path' class attribute."
69
+ )
70
+ elif not paths:
71
+ raise ImproperlyConfigured(
72
+ f"The app module {module!r} has no filesystem location, "
73
+ "you must configure this app with an PackageConfig subclass "
74
+ "with a 'path' class attribute."
75
+ )
76
+ return paths[0]
77
+
78
+ module = import_module(self.name)
79
+ return _path_from_module(module)
192
80
 
193
81
  def ready(self):
194
82
  """
@@ -4,11 +4,14 @@ import threading
4
4
  import warnings
5
5
  from collections import Counter, defaultdict
6
6
  from functools import partial
7
+ from importlib import import_module
7
8
 
8
9
  from plain.exceptions import ImproperlyConfigured, PackageRegistryNotReady
9
10
 
10
11
  from .config import PackageConfig
11
12
 
13
+ CONFIG_MODULE_NAME = "config"
14
+
12
15
 
13
16
  class PackagesRegistry:
14
17
  """
@@ -82,17 +85,35 @@ class PackagesRegistry:
82
85
  # Phase 1: initialize app configs and import app modules.
83
86
  for entry in installed_packages:
84
87
  if isinstance(entry, PackageConfig):
85
- package_config = entry
88
+ # Some instances of the registry pass in the
89
+ # PackageConfig directly...
90
+ self.register_config(entry)
86
91
  else:
87
- package_config = PackageConfig.create(entry)
88
- if package_config.label in self.package_configs:
89
- raise ImproperlyConfigured(
90
- "Package labels aren't unique, "
91
- f"duplicates: {package_config.label}"
92
- )
93
-
94
- self.package_configs[package_config.label] = package_config
95
- package_config.packages_registry = self
92
+ try:
93
+ import_module(f"{entry}.{CONFIG_MODULE_NAME}")
94
+ except ModuleNotFoundError:
95
+ pass
96
+
97
+ # The config for the package should now be registered, if it existed.
98
+ # And if it didn't, now we can auto generate one.
99
+ entry_config = None
100
+ for config in self.package_configs.values():
101
+ if config.name == entry:
102
+ entry_config = config
103
+ break
104
+
105
+ if not entry_config:
106
+ # Use PackageConfig class as-is, without any customization.
107
+ entry_config = self.register_config(
108
+ PackageConfig, module_name=entry
109
+ )
110
+
111
+ # Make sure we have the same number of configs as we have installed packages
112
+ if len(self.package_configs) != len(installed_packages):
113
+ raise ImproperlyConfigured(
114
+ f"The number of installed packages ({len(installed_packages)}) does not match the number of "
115
+ f"registered configs ({len(self.package_configs)})."
116
+ )
96
117
 
97
118
  # Check for duplicate app names.
98
119
  counts = Counter(
@@ -372,5 +393,38 @@ class PackagesRegistry:
372
393
  for function in self._pending_operations.pop(key, []):
373
394
  function(model)
374
395
 
396
+ def register_config(self, package_config, module_name=""):
397
+ """
398
+ Add a config to the registry.
399
+
400
+ Typically used as a decorator on a PackageConfig subclass. Example:
401
+
402
+ @register_config
403
+ class Config(PackageConfig):
404
+ pass
405
+ """
406
+ if not module_name:
407
+ module_name = package_config.__module__
408
+
409
+ # If it is in .config like expected, return the parent module name
410
+ if module_name.endswith(f".{CONFIG_MODULE_NAME}"):
411
+ module_name = module_name[: -len(CONFIG_MODULE_NAME) - 1]
412
+
413
+ if isinstance(package_config, type) and issubclass(
414
+ package_config, PackageConfig
415
+ ):
416
+ # A class was passed, so init it
417
+ package_config = package_config(module_name)
418
+
419
+ if package_config.label in self.package_configs:
420
+ raise ImproperlyConfigured(
421
+ f"Package labels aren't unique, duplicates: {package_config.label}"
422
+ )
423
+ self.package_configs[package_config.label] = package_config
424
+ package_config.packages = self
425
+
426
+ return package_config
427
+
375
428
 
376
429
  packages_registry = PackagesRegistry(installed_packages=None)
430
+ register_config = packages_registry.register_config
@@ -3,7 +3,7 @@ from importlib import import_module
3
3
  from plain.packages import packages_registry
4
4
  from plain.runtime import settings
5
5
  from plain.utils.functional import LazyObject
6
- from plain.utils.module_loading import import_string, module_has_submodule
6
+ from plain.utils.module_loading import import_string
7
7
 
8
8
  from .environments import DefaultEnvironment, get_template_dirs
9
9
 
@@ -24,20 +24,25 @@ class JinjaEnvironment(LazyObject):
24
24
  # We have to set _wrapped before we trigger the autoloading of "register" commands
25
25
  self._wrapped = env
26
26
 
27
- def _maybe_import_module(name):
28
- if name not in self._imported_modules:
29
- import_module(name)
30
- self._imported_modules.add(name)
31
-
32
27
  for package_config in packages_registry.get_package_configs():
33
- if module_has_submodule(package_config.module, "templates"):
34
- # Allow this to fail in case there are import errors inside of their file
35
- _maybe_import_module(f"{package_config.name}.templates")
36
-
37
- app = import_module("app")
38
- if module_has_submodule(app, "templates"):
39
28
  # Allow this to fail in case there are import errors inside of their file
40
- _maybe_import_module("app.templates")
29
+ import_name = f"{package_config.name}.templates"
30
+ if import_name in self._imported_modules:
31
+ continue
32
+ try:
33
+ import_module(import_name)
34
+ self._imported_modules.add(import_name)
35
+ except ModuleNotFoundError:
36
+ pass
37
+
38
+ # Allow this to fail in case there are import errors inside of their file
39
+ import_name = "app.templates"
40
+ if import_name not in self._imported_modules:
41
+ try:
42
+ import_module(import_name)
43
+ self._imported_modules.add(import_name)
44
+ except ModuleNotFoundError:
45
+ pass
41
46
 
42
47
 
43
48
  environment = JinjaEnvironment()
@@ -1,7 +1,6 @@
1
1
  import os
2
2
  import sys
3
3
  from importlib import import_module
4
- from importlib.util import find_spec as importlib_find
5
4
 
6
5
 
7
6
  def cached_import(module_path, class_name):
@@ -33,24 +32,6 @@ def import_string(dotted_path):
33
32
  ) from err
34
33
 
35
34
 
36
- def module_has_submodule(package, module_name):
37
- """See if 'module' is in 'package'."""
38
- try:
39
- package_name = package.__name__
40
- package_path = package.__path__
41
- except AttributeError:
42
- # package isn't a package.
43
- return False
44
-
45
- full_module_name = package_name + "." + module_name
46
- try:
47
- return importlib_find(full_module_name, package_path) is not None
48
- except ModuleNotFoundError:
49
- # When module_name is an invalid dotted path, Python raises
50
- # ModuleNotFoundError.
51
- return False
52
-
53
-
54
35
  def module_dir(module):
55
36
  """
56
37
  Find the name of the directory that contains a module, if possible.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.25.0
3
+ Version: 0.26.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
@@ -60,9 +60,9 @@ plain/logs/configure.py,sha256=6mV7d1IxkDYT3VBz61qhIj0Esuy5l5QdQfsHaGCfI6w,1063
60
60
  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
- plain/packages/__init__.py,sha256=75ILri5V7gm9k5RzvrYNBHPdLUZNbO65Go3kBg95O8w,124
64
- plain/packages/config.py,sha256=oOXy_k2qzy1i221hCsayVx0xPVLBxb_pwOVDDxPVfDI,8842
65
- plain/packages/registry.py,sha256=PeVoYbMwYqg7VTMB3QIWL3YUve9l2Ki-AJx29tm6SWs,15432
63
+ plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
64
+ plain/packages/config.py,sha256=Gmu7QW4Z3Cx9_d7N2D5o7t292t7vAOE8aL-3yO7sGqc,3327
65
+ plain/packages/registry.py,sha256=UEagFzHXCSuGeMlz2E1JLj0fgigOR89i5yVZe8Pq9Ds,17589
66
66
  plain/preflight/README.md,sha256=-PKVd0RBMh4ROiMkegPS2PgvT1Kq9qqN1KfNkmUSdFc,177
67
67
  plain/preflight/__init__.py,sha256=H-TNRvaddPtOGmv4RXoc1fxDV1AOb7_K3u7ECF8mV58,607
68
68
  plain/preflight/files.py,sha256=wbHCNgps7o1c1zQNBd8FDCaVaqX90UwuvLgEQ_DbUpY,510
@@ -83,7 +83,7 @@ plain/templates/README.md,sha256=VfA2HmrklG5weE1md85q9g84cWnMBEiXAynKzM7S1Sk,464
83
83
  plain/templates/__init__.py,sha256=bX76FakE9T7mfK3N0deN85HlwHNQpeigytSC9Z8LcOs,451
84
84
  plain/templates/core.py,sha256=iw58EAmyyv8N5HDA-Sq4-fLgz_qx8v8WJfurgR116jw,625
85
85
  plain/templates/jinja/README.md,sha256=ft4781b4IAVI6fsIdAHIpOigdsZ6wGg06LK7BHxoj-g,6996
86
- plain/templates/jinja/__init__.py,sha256=Nm34l_heWGYKFetO-GcAmr5qVbuQtyCJBy2jrEAoK-c,2671
86
+ plain/templates/jinja/__init__.py,sha256=qBESSL8XfwdxtwujjR5mZvk4VddlMn1-jOsSxGQy0oE,2768
87
87
  plain/templates/jinja/environments.py,sha256=9plifzvQj--aTN1cCpJ2WdzQxZJpzB8S_4hghgQRQT0,2064
88
88
  plain/templates/jinja/extensions.py,sha256=AEmmmHDbdRW8fhjYDzq9eSSNbp9WHsXenD8tPthjc0s,1351
89
89
  plain/templates/jinja/filters.py,sha256=3KJKKbxcv9dLzUDWPcaa88k3NU2m1GG3iMIgFhzXrBA,860
@@ -117,7 +117,7 @@ plain/utils/http.py,sha256=VOOnwRXnDp5PL_qEmkInLTm10fF58vlhVjeSTdzV2cQ,6031
117
117
  plain/utils/inspect.py,sha256=O3VMH5f4aGOrVpXJBKtQOxx01XrKnjjz6VO_MCV0xkE,1140
118
118
  plain/utils/ipv6.py,sha256=pISQ2AIlG8xXlxpphn388q03fq-fOrlu4GZR0YYjQXw,1267
119
119
  plain/utils/itercompat.py,sha256=lacIDjczhxbwG4ON_KfG1H6VNPOGOpbRhnVhbedo2CY,184
120
- plain/utils/module_loading.py,sha256=CWl7Shoax9Zkevf1pM9PpS_0V69J5Cukjyj078UPCAw,2252
120
+ plain/utils/module_loading.py,sha256=11a1JbASB-KbahQe2Dlhiw_2VD71lKKZYmo3y_wfJeI,1640
121
121
  plain/utils/regex_helper.py,sha256=pAdh_xG52BOyXLsiuIMPFgduUAoWOEje1ZpjhcefxiA,12769
122
122
  plain/utils/safestring.py,sha256=sawOehuWjr4bkF5jXXCcziILQGoqUcA_eEfsURrAyN0,1801
123
123
  plain/utils/text.py,sha256=42hJv06sadbWfsaAHNhqCQaP1W9qZ69trWDTS-Xva7k,9496
@@ -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.25.0.dist-info/METADATA,sha256=ErgfCIZpGA9-NDGsjKHueIWtZ2LhJquSTQ37ycDo2BQ,319
138
- plain-0.25.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
- plain-0.25.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
140
- plain-0.25.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
141
- plain-0.25.0.dist-info/RECORD,,
137
+ plain-0.26.0.dist-info/METADATA,sha256=ubQaY6xdMzXNiRH6AvYXOWNyOdmthru0UB3-AKlPnxU,319
138
+ plain-0.26.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
+ plain-0.26.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
140
+ plain-0.26.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
141
+ plain-0.26.0.dist-info/RECORD,,
File without changes