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.
@@ -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
  """
@@ -1,14 +1,14 @@
1
- import functools
2
1
  import sys
3
2
  import threading
4
- import warnings
5
- from collections import Counter, defaultdict
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.models_ready = self.ready = False
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
- package_config = entry
71
+ # Some instances of the registry pass in the
72
+ # PackageConfig directly...
73
+ self.register_config(package_config=entry)
86
74
  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
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 get_registered_model(self, package_label, model_name):
172
+ def register_config(self, package_config):
275
173
  """
276
- Similar to get_model(), but doesn't require that an app exists with
277
- the given package_label.
174
+ Add a config to the registry.
278
175
 
279
- It's safe to call this method at import time, even while the registry
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
- @functools.cache
288
- def get_swappable_settings_name(self, to_string):
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
- to_string = to_string.lower()
300
- for model in self.get_models(include_swapped=True):
301
- swapped = model._meta.swapped
302
- # Is this model swapped out for the model given by to_string?
303
- if swapped and swapped.lower() == to_string:
304
- return model._meta.swappable
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
- This is mostly used in tests.
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
- The function passed to this method must accept exactly n models as
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
- packages_registry = PackagesRegistry(installed_packages=None)
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, 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.27.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=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=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.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