plain 0.25.0__py3-none-any.whl → 0.27.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/packages/__init__.py +2 -2
- plain/packages/config.py +48 -160
- plain/packages/registry.py +59 -228
- plain/templates/jinja/__init__.py +18 -13
- plain/utils/module_loading.py +0 -19
- {plain-0.25.0.dist-info → plain-0.27.0.dist-info}/METADATA +1 -1
- {plain-0.25.0.dist-info → plain-0.27.0.dist-info}/RECORD +10 -10
- {plain-0.25.0.dist-info → plain-0.27.0.dist-info}/WHEEL +0 -0
- {plain-0.25.0.dist-info → plain-0.27.0.dist-info}/entry_points.txt +0 -0
- {plain-0.25.0.dist-info → plain-0.27.0.dist-info}/licenses/LICENSE +0 -0
plain/packages/__init__.py
CHANGED
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,
|
15
|
+
def __init__(self, name, *, label=""):
|
17
16
|
# Full Python path to the application e.g. 'plain.admin.admin'.
|
18
|
-
self.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
|
-
|
49
|
-
|
50
|
-
#
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
#
|
116
|
-
#
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
"""
|
plain/packages/registry.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
import functools
|
2
1
|
import sys
|
3
2
|
import threading
|
4
|
-
import
|
5
|
-
from
|
6
|
-
from functools import partial
|
3
|
+
from collections import Counter
|
4
|
+
from importlib import import_module
|
7
5
|
|
8
6
|
from plain.exceptions import ImproperlyConfigured, PackageRegistryNotReady
|
9
7
|
|
10
8
|
from .config import PackageConfig
|
11
9
|
|
10
|
+
CONFIG_MODULE_NAME = "config"
|
11
|
+
|
12
12
|
|
13
13
|
class PackagesRegistry:
|
14
14
|
"""
|
@@ -26,30 +26,16 @@ class PackagesRegistry:
|
|
26
26
|
):
|
27
27
|
raise RuntimeError("You must supply an installed_packages argument.")
|
28
28
|
|
29
|
-
# Mapping of app labels => model names => model classes.
|
30
|
-
# Models are registered with @models.register_model, which
|
31
|
-
# creates an entry in all_models. All imported models are registered,
|
32
|
-
# regardless of whether they're defined in an installed application
|
33
|
-
# and whether the registry has been populated. Since it isn't possible
|
34
|
-
# to reimport a module safely (it could reexecute initialization code)
|
35
|
-
# all_models is never overridden or reset.
|
36
|
-
self.all_models = defaultdict(dict)
|
37
|
-
|
38
29
|
# Mapping of labels to PackageConfig instances for installed packages.
|
39
30
|
self.package_configs = {}
|
40
31
|
|
41
32
|
# Whether the registry is populated.
|
42
|
-
self.packages_ready = self.
|
33
|
+
self.packages_ready = self.ready = False
|
43
34
|
|
44
35
|
# Lock for thread-safe population.
|
45
36
|
self._lock = threading.RLock()
|
46
37
|
self.loading = False
|
47
38
|
|
48
|
-
# Maps ("package_label", "modelname") tuples to lists of functions to be
|
49
|
-
# called when the corresponding model is ready. Used by this class's
|
50
|
-
# `lazy_model_operation()` and `do_pending_operations()` methods.
|
51
|
-
self._pending_operations = defaultdict(list)
|
52
|
-
|
53
39
|
# Populate packages and models, unless it's the main registry.
|
54
40
|
if installed_packages is not None:
|
55
41
|
self.populate(installed_packages)
|
@@ -82,17 +68,34 @@ class PackagesRegistry:
|
|
82
68
|
# Phase 1: initialize app configs and import app modules.
|
83
69
|
for entry in installed_packages:
|
84
70
|
if isinstance(entry, PackageConfig):
|
85
|
-
|
71
|
+
# Some instances of the registry pass in the
|
72
|
+
# PackageConfig directly...
|
73
|
+
self.register_config(package_config=entry)
|
86
74
|
else:
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
75
|
+
try:
|
76
|
+
import_module(f"{entry}.{CONFIG_MODULE_NAME}")
|
77
|
+
except ModuleNotFoundError:
|
78
|
+
pass
|
79
|
+
|
80
|
+
# The config for the package should now be registered, if it existed.
|
81
|
+
# And if it didn't, now we can auto generate one.
|
82
|
+
entry_config = None
|
83
|
+
for config in self.package_configs.values():
|
84
|
+
if config.name == entry:
|
85
|
+
entry_config = config
|
86
|
+
break
|
87
|
+
|
88
|
+
if not entry_config:
|
89
|
+
# Use PackageConfig class as-is, without any customization.
|
90
|
+
auto_package_config = PackageConfig(entry)
|
91
|
+
entry_config = self.register_config(auto_package_config)
|
92
|
+
|
93
|
+
# Make sure we have the same number of configs as we have installed packages
|
94
|
+
if len(self.package_configs) != len(installed_packages):
|
95
|
+
raise ImproperlyConfigured(
|
96
|
+
f"The number of installed packages ({len(installed_packages)}) does not match the number of "
|
97
|
+
f"registered configs ({len(self.package_configs)})."
|
98
|
+
)
|
96
99
|
|
97
100
|
# Check for duplicate app names.
|
98
101
|
counts = Counter(
|
@@ -112,10 +115,6 @@ class PackagesRegistry:
|
|
112
115
|
for package_config in self.get_package_configs():
|
113
116
|
package_config.ready()
|
114
117
|
|
115
|
-
self.clear_cache()
|
116
|
-
|
117
|
-
self.models_ready = True
|
118
|
-
|
119
118
|
self.ready = True
|
120
119
|
|
121
120
|
def check_packages_ready(self):
|
@@ -129,11 +128,6 @@ class PackagesRegistry:
|
|
129
128
|
settings.INSTALLED_PACKAGES
|
130
129
|
raise PackageRegistryNotReady("Packages aren't loaded yet.")
|
131
130
|
|
132
|
-
def check_models_ready(self):
|
133
|
-
"""Raise an exception if all models haven't been imported yet."""
|
134
|
-
if not self.models_ready:
|
135
|
-
raise PackageRegistryNotReady("Models aren't loaded yet.")
|
136
|
-
|
137
131
|
def get_package_configs(self):
|
138
132
|
"""Import applications and return an iterable of app configs."""
|
139
133
|
self.check_packages_ready()
|
@@ -156,102 +150,6 @@ class PackagesRegistry:
|
|
156
150
|
break
|
157
151
|
raise LookupError(message)
|
158
152
|
|
159
|
-
# This method is performance-critical at least for Plain's test suite.
|
160
|
-
@functools.cache
|
161
|
-
def get_models(
|
162
|
-
self, *, package_label="", include_auto_created=False, include_swapped=False
|
163
|
-
):
|
164
|
-
"""
|
165
|
-
Return a list of all installed models.
|
166
|
-
|
167
|
-
By default, the following models aren't included:
|
168
|
-
|
169
|
-
- auto-created models for many-to-many relations without
|
170
|
-
an explicit intermediate table,
|
171
|
-
- models that have been swapped out.
|
172
|
-
|
173
|
-
Set the corresponding keyword argument to True to include such models.
|
174
|
-
"""
|
175
|
-
self.check_models_ready()
|
176
|
-
|
177
|
-
result = []
|
178
|
-
|
179
|
-
# Get models for a single package
|
180
|
-
if package_label:
|
181
|
-
package_models = self.all_models[package_label]
|
182
|
-
for model in package_models.values():
|
183
|
-
if model._meta.auto_created and not include_auto_created:
|
184
|
-
continue
|
185
|
-
if model._meta.swapped and not include_swapped:
|
186
|
-
continue
|
187
|
-
result.append(model)
|
188
|
-
return result
|
189
|
-
|
190
|
-
# Get models for all packages
|
191
|
-
for package_models in self.all_models.values():
|
192
|
-
for model in package_models.values():
|
193
|
-
if model._meta.auto_created and not include_auto_created:
|
194
|
-
continue
|
195
|
-
if model._meta.swapped and not include_swapped:
|
196
|
-
continue
|
197
|
-
result.append(model)
|
198
|
-
|
199
|
-
return result
|
200
|
-
|
201
|
-
def get_model(self, package_label, model_name=None, require_ready=True):
|
202
|
-
"""
|
203
|
-
Return the model matching the given package_label and model_name.
|
204
|
-
|
205
|
-
As a shortcut, package_label may be in the form <package_label>.<model_name>.
|
206
|
-
|
207
|
-
model_name is case-insensitive.
|
208
|
-
|
209
|
-
Raise LookupError if no application exists with this label, or no
|
210
|
-
model exists with this name in the application. Raise ValueError if
|
211
|
-
called with a single argument that doesn't contain exactly one dot.
|
212
|
-
"""
|
213
|
-
if require_ready:
|
214
|
-
self.check_models_ready()
|
215
|
-
else:
|
216
|
-
self.check_packages_ready()
|
217
|
-
|
218
|
-
if model_name is None:
|
219
|
-
package_label, model_name = package_label.split(".")
|
220
|
-
|
221
|
-
# package_config = self.get_package_config(package_label)
|
222
|
-
|
223
|
-
# if not require_ready and package_config.models is None:
|
224
|
-
# package_config.import_models()
|
225
|
-
|
226
|
-
package_models = self.all_models[package_label]
|
227
|
-
return package_models[model_name.lower()]
|
228
|
-
|
229
|
-
def register_model(self, package_label, model):
|
230
|
-
# Since this method is called when models are imported, it cannot
|
231
|
-
# perform imports because of the risk of import loops. It mustn't
|
232
|
-
# call get_package_config().
|
233
|
-
model_name = model._meta.model_name
|
234
|
-
app_models = self.all_models[package_label]
|
235
|
-
if model_name in app_models:
|
236
|
-
if (
|
237
|
-
model.__name__ == app_models[model_name].__name__
|
238
|
-
and model.__module__ == app_models[model_name].__module__
|
239
|
-
):
|
240
|
-
warnings.warn(
|
241
|
-
f"Model '{package_label}.{model_name}' was already registered. Reloading models is not "
|
242
|
-
"advised as it can lead to inconsistencies, most notably with "
|
243
|
-
"related models.",
|
244
|
-
RuntimeWarning,
|
245
|
-
stacklevel=2,
|
246
|
-
)
|
247
|
-
else:
|
248
|
-
raise RuntimeError(
|
249
|
-
f"Conflicting '{model_name}' models in application '{package_label}': {app_models[model_name]} and {model}."
|
250
|
-
)
|
251
|
-
app_models[model_name] = model
|
252
|
-
self.do_pending_operations(model)
|
253
|
-
self.clear_cache()
|
254
|
-
|
255
153
|
def get_containing_package_config(self, object_name):
|
256
154
|
"""
|
257
155
|
Look for an app config containing a given object.
|
@@ -271,106 +169,39 @@ class PackagesRegistry:
|
|
271
169
|
if candidates:
|
272
170
|
return sorted(candidates, key=lambda ac: -len(ac.name))[0]
|
273
171
|
|
274
|
-
def
|
172
|
+
def register_config(self, package_config):
|
275
173
|
"""
|
276
|
-
|
277
|
-
the given package_label.
|
174
|
+
Add a config to the registry.
|
278
175
|
|
279
|
-
|
280
|
-
is being populated.
|
281
|
-
"""
|
282
|
-
model = self.all_models[package_label].get(model_name.lower())
|
283
|
-
if model is None:
|
284
|
-
raise LookupError(f"Model '{package_label}.{model_name}' not registered.")
|
285
|
-
return model
|
176
|
+
Typically used as a decorator on a PackageConfig subclass. Example:
|
286
177
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
For a given model string (e.g. "auth.User"), return the name of the
|
291
|
-
corresponding settings name if it refers to a swappable model. If the
|
292
|
-
referred model is not swappable, return None.
|
293
|
-
|
294
|
-
This method is decorated with @functools.cache because it's performance
|
295
|
-
critical when it comes to migrations. Since the swappable settings don't
|
296
|
-
change after Plain has loaded the settings, there is no reason to get
|
297
|
-
the respective settings attribute over and over again.
|
178
|
+
@register_config
|
179
|
+
class Config(PackageConfig):
|
180
|
+
pass
|
298
181
|
"""
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
# Is this model swappable and the one given by to_string?
|
306
|
-
if model._meta.swappable and model._meta.label_lower == to_string:
|
307
|
-
return model._meta.swappable
|
308
|
-
return None
|
309
|
-
|
310
|
-
def clear_cache(self):
|
311
|
-
"""
|
312
|
-
Clear all internal caches, for methods that alter the app registry.
|
182
|
+
if package_config.label in self.package_configs:
|
183
|
+
raise ImproperlyConfigured(
|
184
|
+
f"Package labels aren't unique, duplicates: {package_config.label}"
|
185
|
+
)
|
186
|
+
self.package_configs[package_config.label] = package_config
|
187
|
+
package_config.packages = self
|
313
188
|
|
314
|
-
|
315
|
-
"""
|
316
|
-
# Call expire cache on each model. This will purge
|
317
|
-
# the relation tree and the fields cache.
|
318
|
-
self.get_models.cache_clear()
|
319
|
-
if self.ready:
|
320
|
-
# Circumvent self.get_models() to prevent that the cache is refilled.
|
321
|
-
# This particularly prevents that an empty value is cached while cloning.
|
322
|
-
for package_models in self.all_models.values():
|
323
|
-
for model in package_models.values():
|
324
|
-
model._meta._expire_cache()
|
189
|
+
return package_config
|
325
190
|
|
326
|
-
def lazy_model_operation(self, function, *model_keys):
|
327
|
-
"""
|
328
|
-
Take a function and a number of ("package_label", "modelname") tuples, and
|
329
|
-
when all the corresponding models have been imported and registered,
|
330
|
-
call the function with the model classes as its arguments.
|
331
191
|
|
332
|
-
|
333
|
-
arguments, where n=len(model_keys).
|
334
|
-
"""
|
335
|
-
# Base case: no arguments, just execute the function.
|
336
|
-
if not model_keys:
|
337
|
-
function()
|
338
|
-
# Recursive case: take the head of model_keys, wait for the
|
339
|
-
# corresponding model class to be imported and registered, then apply
|
340
|
-
# that argument to the supplied function. Pass the resulting partial
|
341
|
-
# to lazy_model_operation() along with the remaining model args and
|
342
|
-
# repeat until all models are loaded and all arguments are applied.
|
343
|
-
else:
|
344
|
-
next_model, *more_models = model_keys
|
345
|
-
|
346
|
-
# This will be executed after the class corresponding to next_model
|
347
|
-
# has been imported and registered. The `func` attribute provides
|
348
|
-
# duck-type compatibility with partials.
|
349
|
-
def apply_next_model(model):
|
350
|
-
next_function = partial(apply_next_model.func, model)
|
351
|
-
self.lazy_model_operation(next_function, *more_models)
|
352
|
-
|
353
|
-
apply_next_model.func = function
|
354
|
-
|
355
|
-
# If the model has already been imported and registered, partially
|
356
|
-
# apply it to the function now. If not, add it to the list of
|
357
|
-
# pending operations for the model, where it will be executed with
|
358
|
-
# the model class as its sole argument once the model is ready.
|
359
|
-
try:
|
360
|
-
model_class = self.get_registered_model(*next_model)
|
361
|
-
except LookupError:
|
362
|
-
self._pending_operations[next_model].append(apply_next_model)
|
363
|
-
else:
|
364
|
-
apply_next_model(model_class)
|
365
|
-
|
366
|
-
def do_pending_operations(self, model):
|
367
|
-
"""
|
368
|
-
Take a newly-prepared model and pass it to each function waiting for
|
369
|
-
it. This is called at the very end of Packages.register_model().
|
370
|
-
"""
|
371
|
-
key = model._meta.package_label, model._meta.model_name
|
372
|
-
for function in self._pending_operations.pop(key, []):
|
373
|
-
function(model)
|
192
|
+
packages_registry = PackagesRegistry(installed_packages=None)
|
374
193
|
|
375
194
|
|
376
|
-
|
195
|
+
def register_config(package_config_class):
|
196
|
+
"""A decorator to register a PackageConfig subclass."""
|
197
|
+
module_name = package_config_class.__module__
|
198
|
+
|
199
|
+
# If it is in .config like expected, return the parent module name
|
200
|
+
if module_name.endswith(f".{CONFIG_MODULE_NAME}"):
|
201
|
+
module_name = module_name[: -len(CONFIG_MODULE_NAME) - 1]
|
202
|
+
|
203
|
+
package_config = package_config_class(module_name)
|
204
|
+
|
205
|
+
packages_registry.register_config(package_config)
|
206
|
+
|
207
|
+
return package_config_class
|
@@ -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
|
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
|
-
|
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()
|
plain/utils/module_loading.py
CHANGED
@@ -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.
|
@@ -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=
|
64
|
-
plain/packages/config.py,sha256=
|
65
|
-
plain/packages/registry.py,sha256=
|
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=Aklno7y7UrBZlidtUR_YO3B5xqF46UbUtalReNcYHm8,7937
|
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=
|
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=
|
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.
|
138
|
-
plain-0.
|
139
|
-
plain-0.
|
140
|
-
plain-0.
|
141
|
-
plain-0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|