plain 0.1.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/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings and configuration for Plain.
|
|
3
|
+
|
|
4
|
+
Read values from the module specified by the PLAIN_SETTINGS_MODULE environment
|
|
5
|
+
variable, and then from plain.global_settings; see the global_settings.py
|
|
6
|
+
for a list of all possible variables.
|
|
7
|
+
"""
|
|
8
|
+
import importlib
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
import types
|
|
14
|
+
import typing
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from plain.exceptions import ImproperlyConfigured
|
|
18
|
+
from plain.packages import PackageConfig
|
|
19
|
+
from plain.utils.functional import LazyObject, empty
|
|
20
|
+
|
|
21
|
+
ENVIRONMENT_VARIABLE = "PLAIN_SETTINGS_MODULE"
|
|
22
|
+
ENV_SETTINGS_PREFIX = "PLAIN_"
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("plain.runtime")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SettingsReference(str):
|
|
28
|
+
"""
|
|
29
|
+
String subclass which references a current settings value. It's treated as
|
|
30
|
+
the value in memory but serializes to a settings.NAME attribute reference.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __new__(self, value, setting_name):
|
|
34
|
+
return str.__new__(self, value)
|
|
35
|
+
|
|
36
|
+
def __init__(self, value, setting_name):
|
|
37
|
+
self.setting_name = setting_name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LazySettings(LazyObject):
|
|
41
|
+
"""
|
|
42
|
+
A lazy proxy for either global Plain settings or a custom settings object.
|
|
43
|
+
The user can manually configure settings prior to using them. Otherwise,
|
|
44
|
+
Plain uses the settings module pointed to by PLAIN_SETTINGS_MODULE.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def _setup(self, name=None):
|
|
48
|
+
"""
|
|
49
|
+
Load the settings module pointed to by the environment variable. This
|
|
50
|
+
is used the first time settings are needed, if the user hasn't
|
|
51
|
+
configured settings manually.
|
|
52
|
+
"""
|
|
53
|
+
settings_module = os.environ.get(ENVIRONMENT_VARIABLE, "settings")
|
|
54
|
+
self._wrapped = Settings(settings_module)
|
|
55
|
+
|
|
56
|
+
def __repr__(self):
|
|
57
|
+
# Hardcode the class name as otherwise it yields 'Settings'.
|
|
58
|
+
if self._wrapped is empty:
|
|
59
|
+
return "<LazySettings [Unevaluated]>"
|
|
60
|
+
return f'<LazySettings "{self._wrapped.SETTINGS_MODULE}">'
|
|
61
|
+
|
|
62
|
+
def __getattr__(self, name):
|
|
63
|
+
"""Return the value of a setting and cache it in self.__dict__."""
|
|
64
|
+
if (_wrapped := self._wrapped) is empty:
|
|
65
|
+
self._setup(name)
|
|
66
|
+
_wrapped = self._wrapped
|
|
67
|
+
val = getattr(_wrapped, name)
|
|
68
|
+
|
|
69
|
+
# Special case some settings which require further modification.
|
|
70
|
+
# This is done here for performance reasons so the modified value is cached.
|
|
71
|
+
if name == "SECRET_KEY" and not val:
|
|
72
|
+
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
|
|
73
|
+
|
|
74
|
+
self.__dict__[name] = val
|
|
75
|
+
return val
|
|
76
|
+
|
|
77
|
+
def __setattr__(self, name, value):
|
|
78
|
+
"""
|
|
79
|
+
Set the value of setting. Clear all cached values if _wrapped changes
|
|
80
|
+
(@override_settings does this) or clear single values when set.
|
|
81
|
+
"""
|
|
82
|
+
if name == "_wrapped":
|
|
83
|
+
self.__dict__.clear()
|
|
84
|
+
else:
|
|
85
|
+
self.__dict__.pop(name, None)
|
|
86
|
+
super().__setattr__(name, value)
|
|
87
|
+
|
|
88
|
+
def __delattr__(self, name):
|
|
89
|
+
"""Delete a setting and clear it from cache if needed."""
|
|
90
|
+
super().__delattr__(name)
|
|
91
|
+
self.__dict__.pop(name, None)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def configured(self):
|
|
95
|
+
"""Return True if the settings have already been configured."""
|
|
96
|
+
return self._wrapped is not empty
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SettingDefinition:
|
|
100
|
+
"""Store some basic info about default settings and where they came from"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, name, value, annotation, module, required=False):
|
|
103
|
+
self.name = name
|
|
104
|
+
self.value = value
|
|
105
|
+
self.annotation = annotation
|
|
106
|
+
self.module = module
|
|
107
|
+
self.required = required
|
|
108
|
+
|
|
109
|
+
def __str__(self):
|
|
110
|
+
return self.name
|
|
111
|
+
|
|
112
|
+
def check_type(self, obj):
|
|
113
|
+
if not self.annotation:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if not SettingDefinition._is_instance_of_type(obj, self.annotation):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"The {self.name} setting must be of type {self.annotation}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _is_instance_of_type(value, type_hint) -> bool:
|
|
123
|
+
# Simple types
|
|
124
|
+
if isinstance(type_hint, type):
|
|
125
|
+
return isinstance(value, type_hint)
|
|
126
|
+
|
|
127
|
+
# Union types
|
|
128
|
+
if (
|
|
129
|
+
typing.get_origin(type_hint) is typing.Union
|
|
130
|
+
or typing.get_origin(type_hint) is types.UnionType
|
|
131
|
+
):
|
|
132
|
+
return any(
|
|
133
|
+
SettingDefinition._is_instance_of_type(value, arg)
|
|
134
|
+
for arg in typing.get_args(type_hint)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# List types
|
|
138
|
+
if typing.get_origin(type_hint) is list:
|
|
139
|
+
return isinstance(value, list) and all(
|
|
140
|
+
SettingDefinition._is_instance_of_type(
|
|
141
|
+
item, typing.get_args(type_hint)[0]
|
|
142
|
+
)
|
|
143
|
+
for item in value
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Tuple types
|
|
147
|
+
if typing.get_origin(type_hint) is tuple:
|
|
148
|
+
return isinstance(value, tuple) and all(
|
|
149
|
+
SettingDefinition._is_instance_of_type(
|
|
150
|
+
item, typing.get_args(type_hint)[i]
|
|
151
|
+
)
|
|
152
|
+
for i, item in enumerate(value)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
raise ValueError("Unsupported type hint: %s" % type_hint)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Settings:
|
|
159
|
+
def __init__(self, settings_module):
|
|
160
|
+
self._default_settings = {}
|
|
161
|
+
self._explicit_settings = set()
|
|
162
|
+
|
|
163
|
+
# First load the global settings from plain
|
|
164
|
+
self._load_module_settings(
|
|
165
|
+
importlib.import_module("plain.runtime.global_settings")
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# store the settings module in case someone later cares
|
|
169
|
+
self.SETTINGS_MODULE = settings_module
|
|
170
|
+
|
|
171
|
+
mod = importlib.import_module(self.SETTINGS_MODULE)
|
|
172
|
+
|
|
173
|
+
# Keep a reference to the settings.py module path
|
|
174
|
+
# so we can find files next to it (assume it's at the app root)
|
|
175
|
+
self.path = Path(mod.__file__).resolve()
|
|
176
|
+
|
|
177
|
+
# First, get all the default_settings from the INSTALLED_PACKAGES and set those values
|
|
178
|
+
self._load_default_settings(mod)
|
|
179
|
+
# Second, look at the environment variables and overwrite with those
|
|
180
|
+
self._load_env_settings()
|
|
181
|
+
# Finally, load the explicit settings from the settings module
|
|
182
|
+
self._load_explicit_settings(mod)
|
|
183
|
+
# Check for any required settings that are missing
|
|
184
|
+
self._check_required_settings()
|
|
185
|
+
|
|
186
|
+
def _load_default_settings(self, settings_module):
|
|
187
|
+
# Get INSTALLED_PACKAGES from mod,
|
|
188
|
+
# then (without populating packages) do a check for default_settings in each
|
|
189
|
+
# app and load those now too.
|
|
190
|
+
for entry in getattr(settings_module, "INSTALLED_PACKAGES", []):
|
|
191
|
+
try:
|
|
192
|
+
if isinstance(entry, PackageConfig):
|
|
193
|
+
app_settings = entry.module.default_settings
|
|
194
|
+
else:
|
|
195
|
+
app_settings = importlib.import_module(f"{entry}.default_settings")
|
|
196
|
+
except ModuleNotFoundError:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
self._load_module_settings(app_settings)
|
|
200
|
+
|
|
201
|
+
def _load_module_settings(self, module):
|
|
202
|
+
annotations = getattr(module, "__annotations__", {})
|
|
203
|
+
settings = dir(module)
|
|
204
|
+
|
|
205
|
+
for setting in settings:
|
|
206
|
+
if setting.isupper():
|
|
207
|
+
if hasattr(self, setting):
|
|
208
|
+
raise ImproperlyConfigured("The %s setting is duplicated" % setting)
|
|
209
|
+
|
|
210
|
+
setting_value = getattr(module, setting)
|
|
211
|
+
|
|
212
|
+
# Set a simple attr on the settings object
|
|
213
|
+
setattr(self, setting, setting_value)
|
|
214
|
+
|
|
215
|
+
# Store a more complex setting reference for more detail
|
|
216
|
+
self._default_settings[setting] = SettingDefinition(
|
|
217
|
+
name=setting,
|
|
218
|
+
value=setting_value,
|
|
219
|
+
annotation=annotations.get(setting, ""),
|
|
220
|
+
module=module,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Store any annotations that didn't have a value (these are required settings)
|
|
224
|
+
for setting, annotation in annotations.items():
|
|
225
|
+
if setting not in self._default_settings:
|
|
226
|
+
self._default_settings[setting] = SettingDefinition(
|
|
227
|
+
name=setting,
|
|
228
|
+
value=None,
|
|
229
|
+
annotation=annotation,
|
|
230
|
+
module=module,
|
|
231
|
+
required=True,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _load_env_settings(self):
|
|
235
|
+
env_settings = {
|
|
236
|
+
k[len(ENV_SETTINGS_PREFIX) :]: v
|
|
237
|
+
for k, v in os.environ.items()
|
|
238
|
+
if k.startswith(ENV_SETTINGS_PREFIX)
|
|
239
|
+
}
|
|
240
|
+
logger.debug("Loading environment settings: %s", env_settings)
|
|
241
|
+
for setting, value in env_settings.items():
|
|
242
|
+
if setting not in self._default_settings:
|
|
243
|
+
# Ignore anything not defined in the default settings
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
default_setting = self._default_settings[setting]
|
|
247
|
+
if not default_setting.annotation:
|
|
248
|
+
raise ValueError(
|
|
249
|
+
f"Setting {setting} needs a type hint to be set from the environment"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if default_setting.annotation is bool:
|
|
253
|
+
# Special case for bools
|
|
254
|
+
parsed_value = value.lower() in ("true", "1", "yes")
|
|
255
|
+
elif default_setting.annotation is str:
|
|
256
|
+
parsed_value = value
|
|
257
|
+
else:
|
|
258
|
+
# Anything besides a string will be parsed as JSON
|
|
259
|
+
# (works for ints, lists, etc.)
|
|
260
|
+
parsed_value = json.loads(value)
|
|
261
|
+
|
|
262
|
+
default_setting.check_type(parsed_value)
|
|
263
|
+
|
|
264
|
+
setattr(self, setting, parsed_value)
|
|
265
|
+
self._explicit_settings.add(setting)
|
|
266
|
+
|
|
267
|
+
def _load_explicit_settings(self, settings_module):
|
|
268
|
+
for setting in dir(settings_module):
|
|
269
|
+
if setting.isupper():
|
|
270
|
+
setting_value = getattr(settings_module, setting)
|
|
271
|
+
|
|
272
|
+
if setting in self._default_settings:
|
|
273
|
+
self._default_settings[setting].check_type(setting_value)
|
|
274
|
+
|
|
275
|
+
setattr(self, setting, setting_value)
|
|
276
|
+
self._explicit_settings.add(setting)
|
|
277
|
+
|
|
278
|
+
if hasattr(time, "tzset") and self.TIME_ZONE:
|
|
279
|
+
# When we can, attempt to validate the timezone. If we can't find
|
|
280
|
+
# this file, no check happens and it's harmless.
|
|
281
|
+
zoneinfo_root = Path("/usr/share/zoneinfo")
|
|
282
|
+
zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split("/"))
|
|
283
|
+
if zoneinfo_root.exists() and not zone_info_file.exists():
|
|
284
|
+
raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
|
|
285
|
+
# Move the time zone info into os.environ. See ticket #2315 for why
|
|
286
|
+
# we don't do this unconditionally (breaks Windows).
|
|
287
|
+
os.environ["TZ"] = self.TIME_ZONE
|
|
288
|
+
time.tzset()
|
|
289
|
+
|
|
290
|
+
def _check_required_settings(self):
|
|
291
|
+
# Required settings have to be explicitly defined (there is no default)
|
|
292
|
+
# so we can check whether they have been set by the user
|
|
293
|
+
required_settings = {k for k, v in self._default_settings.items() if v.required}
|
|
294
|
+
if missing := required_settings - self._explicit_settings:
|
|
295
|
+
raise ImproperlyConfigured(
|
|
296
|
+
"The following settings are required: %s" % ", ".join(missing)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Seems like this could almost be removed
|
|
300
|
+
def is_overridden(self, setting):
|
|
301
|
+
return setting in self._explicit_settings
|
|
302
|
+
|
|
303
|
+
def __repr__(self):
|
|
304
|
+
return f'<{self.__class__.__name__} "{self.SETTINGS_MODULE}">'
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# Currently used for test settings override... nothing else
|
|
308
|
+
class UserSettingsHolder:
|
|
309
|
+
"""Holder for user configured settings."""
|
|
310
|
+
|
|
311
|
+
# SETTINGS_MODULE doesn't make much sense in the manually configured
|
|
312
|
+
# (standalone) case.
|
|
313
|
+
SETTINGS_MODULE = None
|
|
314
|
+
|
|
315
|
+
def __init__(self, default_settings):
|
|
316
|
+
"""
|
|
317
|
+
Requests for configuration variables not in this class are satisfied
|
|
318
|
+
from the module specified in default_settings (if possible).
|
|
319
|
+
"""
|
|
320
|
+
self.__dict__["_deleted"] = set()
|
|
321
|
+
self.default_settings = default_settings
|
|
322
|
+
|
|
323
|
+
def __getattr__(self, name):
|
|
324
|
+
if not name.isupper() or name in self._deleted:
|
|
325
|
+
raise AttributeError
|
|
326
|
+
return getattr(self.default_settings, name)
|
|
327
|
+
|
|
328
|
+
def __setattr__(self, name, value):
|
|
329
|
+
self._deleted.discard(name)
|
|
330
|
+
super().__setattr__(name, value)
|
|
331
|
+
|
|
332
|
+
def __delattr__(self, name):
|
|
333
|
+
self._deleted.add(name)
|
|
334
|
+
if hasattr(self, name):
|
|
335
|
+
super().__delattr__(name)
|
|
336
|
+
|
|
337
|
+
def __dir__(self):
|
|
338
|
+
return sorted(
|
|
339
|
+
s
|
|
340
|
+
for s in [*self.__dict__, *dir(self.default_settings)]
|
|
341
|
+
if s not in self._deleted
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def is_overridden(self, setting):
|
|
345
|
+
deleted = setting in self._deleted
|
|
346
|
+
set_locally = setting in self.__dict__
|
|
347
|
+
set_on_default = getattr(
|
|
348
|
+
self.default_settings, "is_overridden", lambda s: False
|
|
349
|
+
)(setting)
|
|
350
|
+
return deleted or set_locally or set_on_default
|
|
351
|
+
|
|
352
|
+
def __repr__(self):
|
|
353
|
+
return f"<{self.__class__.__name__}>"
|
plain/signals/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Multi-consumer multi-producer dispatching mechanism
|
|
2
|
+
|
|
3
|
+
Originally based on pydispatch (BSD) https://pypi.org/project/PyDispatcher/2.0.1/
|
|
4
|
+
See license.txt for original license.
|
|
5
|
+
|
|
6
|
+
Heavily modified for Plain's purposes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from plain.signals.dispatch.dispatcher import Signal, receiver # NOQA
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import weakref
|
|
4
|
+
|
|
5
|
+
from plain.utils.inspect import func_accepts_kwargs
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("plain.signals.dispatch")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _make_id(target):
|
|
11
|
+
if hasattr(target, "__func__"):
|
|
12
|
+
return (id(target.__self__), id(target.__func__))
|
|
13
|
+
return id(target)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
NONE_ID = _make_id(None)
|
|
17
|
+
|
|
18
|
+
# A marker for caching
|
|
19
|
+
NO_RECEIVERS = object()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Signal:
|
|
23
|
+
"""
|
|
24
|
+
Base class for all signals
|
|
25
|
+
|
|
26
|
+
Internal attributes:
|
|
27
|
+
|
|
28
|
+
receivers
|
|
29
|
+
{ receiverkey (id) : weakref(receiver) }
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, use_caching=False):
|
|
33
|
+
"""
|
|
34
|
+
Create a new signal.
|
|
35
|
+
"""
|
|
36
|
+
self.receivers = []
|
|
37
|
+
self.lock = threading.Lock()
|
|
38
|
+
self.use_caching = use_caching
|
|
39
|
+
# For convenience we create empty caches even if they are not used.
|
|
40
|
+
# A note about caching: if use_caching is defined, then for each
|
|
41
|
+
# distinct sender we cache the receivers that sender has in
|
|
42
|
+
# 'sender_receivers_cache'. The cache is cleaned when .connect() or
|
|
43
|
+
# .disconnect() is called and populated on send().
|
|
44
|
+
self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}
|
|
45
|
+
self._dead_receivers = False
|
|
46
|
+
|
|
47
|
+
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
|
|
48
|
+
"""
|
|
49
|
+
Connect receiver to sender for signal.
|
|
50
|
+
|
|
51
|
+
Arguments:
|
|
52
|
+
|
|
53
|
+
receiver
|
|
54
|
+
A function or an instance method which is to receive signals.
|
|
55
|
+
Receivers must be hashable objects. Receivers can be
|
|
56
|
+
asynchronous.
|
|
57
|
+
|
|
58
|
+
If weak is True, then receiver must be weak referenceable.
|
|
59
|
+
|
|
60
|
+
Receivers must be able to accept keyword arguments.
|
|
61
|
+
|
|
62
|
+
If a receiver is connected with a dispatch_uid argument, it
|
|
63
|
+
will not be added if another receiver was already connected
|
|
64
|
+
with that dispatch_uid.
|
|
65
|
+
|
|
66
|
+
sender
|
|
67
|
+
The sender to which the receiver should respond. Must either be
|
|
68
|
+
a Python object, or None to receive events from any sender.
|
|
69
|
+
|
|
70
|
+
weak
|
|
71
|
+
Whether to use weak references to the receiver. By default, the
|
|
72
|
+
module will attempt to use weak references to the receiver
|
|
73
|
+
objects. If this parameter is false, then strong references will
|
|
74
|
+
be used.
|
|
75
|
+
|
|
76
|
+
dispatch_uid
|
|
77
|
+
An identifier used to uniquely identify a particular instance of
|
|
78
|
+
a receiver. This will usually be a string, though it may be
|
|
79
|
+
anything hashable.
|
|
80
|
+
"""
|
|
81
|
+
from plain.runtime import settings
|
|
82
|
+
|
|
83
|
+
# If DEBUG is on, check that we got a good receiver
|
|
84
|
+
if settings.configured and settings.DEBUG:
|
|
85
|
+
if not callable(receiver):
|
|
86
|
+
raise TypeError("Signal receivers must be callable.")
|
|
87
|
+
# Check for **kwargs
|
|
88
|
+
if not func_accepts_kwargs(receiver):
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"Signal receivers must accept keyword arguments (**kwargs)."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if dispatch_uid:
|
|
94
|
+
lookup_key = (dispatch_uid, _make_id(sender))
|
|
95
|
+
else:
|
|
96
|
+
lookup_key = (_make_id(receiver), _make_id(sender))
|
|
97
|
+
|
|
98
|
+
if weak:
|
|
99
|
+
ref = weakref.ref
|
|
100
|
+
receiver_object = receiver
|
|
101
|
+
# Check for bound methods
|
|
102
|
+
if hasattr(receiver, "__self__") and hasattr(receiver, "__func__"):
|
|
103
|
+
ref = weakref.WeakMethod
|
|
104
|
+
receiver_object = receiver.__self__
|
|
105
|
+
receiver = ref(receiver)
|
|
106
|
+
weakref.finalize(receiver_object, self._remove_receiver)
|
|
107
|
+
|
|
108
|
+
with self.lock:
|
|
109
|
+
self._clear_dead_receivers()
|
|
110
|
+
if not any(r_key == lookup_key for r_key, _ in self.receivers):
|
|
111
|
+
self.receivers.append((lookup_key, receiver))
|
|
112
|
+
self.sender_receivers_cache.clear()
|
|
113
|
+
|
|
114
|
+
def disconnect(self, receiver=None, sender=None, dispatch_uid=None):
|
|
115
|
+
"""
|
|
116
|
+
Disconnect receiver from sender for signal.
|
|
117
|
+
|
|
118
|
+
If weak references are used, disconnect need not be called. The receiver
|
|
119
|
+
will be removed from dispatch automatically.
|
|
120
|
+
|
|
121
|
+
Arguments:
|
|
122
|
+
|
|
123
|
+
receiver
|
|
124
|
+
The registered receiver to disconnect. May be none if
|
|
125
|
+
dispatch_uid is specified.
|
|
126
|
+
|
|
127
|
+
sender
|
|
128
|
+
The registered sender to disconnect
|
|
129
|
+
|
|
130
|
+
dispatch_uid
|
|
131
|
+
the unique identifier of the receiver to disconnect
|
|
132
|
+
"""
|
|
133
|
+
if dispatch_uid:
|
|
134
|
+
lookup_key = (dispatch_uid, _make_id(sender))
|
|
135
|
+
else:
|
|
136
|
+
lookup_key = (_make_id(receiver), _make_id(sender))
|
|
137
|
+
|
|
138
|
+
disconnected = False
|
|
139
|
+
with self.lock:
|
|
140
|
+
self._clear_dead_receivers()
|
|
141
|
+
for index in range(len(self.receivers)):
|
|
142
|
+
r_key, *_ = self.receivers[index]
|
|
143
|
+
if r_key == lookup_key:
|
|
144
|
+
disconnected = True
|
|
145
|
+
del self.receivers[index]
|
|
146
|
+
break
|
|
147
|
+
self.sender_receivers_cache.clear()
|
|
148
|
+
return disconnected
|
|
149
|
+
|
|
150
|
+
def has_listeners(self, sender=None):
|
|
151
|
+
sync_receivers = self._live_receivers(sender)
|
|
152
|
+
return bool(sync_receivers)
|
|
153
|
+
|
|
154
|
+
def send(self, sender, **named):
|
|
155
|
+
"""
|
|
156
|
+
Send signal from sender to all connected receivers.
|
|
157
|
+
|
|
158
|
+
If any receiver raises an error, the error propagates back through send,
|
|
159
|
+
terminating the dispatch loop. So it's possible that all receivers
|
|
160
|
+
won't be called if an error is raised.
|
|
161
|
+
|
|
162
|
+
If any receivers are asynchronous, they are called after all the
|
|
163
|
+
synchronous receivers via a single call to async_to_sync(). They are
|
|
164
|
+
also executed concurrently with asyncio.gather().
|
|
165
|
+
|
|
166
|
+
Arguments:
|
|
167
|
+
|
|
168
|
+
sender
|
|
169
|
+
The sender of the signal. Either a specific object or None.
|
|
170
|
+
|
|
171
|
+
named
|
|
172
|
+
Named arguments which will be passed to receivers.
|
|
173
|
+
|
|
174
|
+
Return a list of tuple pairs [(receiver, response), ... ].
|
|
175
|
+
"""
|
|
176
|
+
if (
|
|
177
|
+
not self.receivers
|
|
178
|
+
or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
|
|
179
|
+
):
|
|
180
|
+
return []
|
|
181
|
+
responses = []
|
|
182
|
+
sync_receivers = self._live_receivers(sender)
|
|
183
|
+
for receiver in sync_receivers:
|
|
184
|
+
response = receiver(signal=self, sender=sender, **named)
|
|
185
|
+
responses.append((receiver, response))
|
|
186
|
+
return responses
|
|
187
|
+
|
|
188
|
+
def _log_robust_failure(self, receiver, err):
|
|
189
|
+
logger.error(
|
|
190
|
+
"Error calling %s in Signal.send_robust() (%s)",
|
|
191
|
+
receiver.__qualname__,
|
|
192
|
+
err,
|
|
193
|
+
exc_info=err,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def send_robust(self, sender, **named):
|
|
197
|
+
"""
|
|
198
|
+
Send signal from sender to all connected receivers catching errors.
|
|
199
|
+
|
|
200
|
+
If any receivers are asynchronous, they are called after all the
|
|
201
|
+
synchronous receivers via a single call to async_to_sync(). They are
|
|
202
|
+
also executed concurrently with asyncio.gather().
|
|
203
|
+
|
|
204
|
+
Arguments:
|
|
205
|
+
|
|
206
|
+
sender
|
|
207
|
+
The sender of the signal. Can be any Python object (normally one
|
|
208
|
+
registered with a connect if you actually want something to
|
|
209
|
+
occur).
|
|
210
|
+
|
|
211
|
+
named
|
|
212
|
+
Named arguments which will be passed to receivers.
|
|
213
|
+
|
|
214
|
+
Return a list of tuple pairs [(receiver, response), ... ].
|
|
215
|
+
|
|
216
|
+
If any receiver raises an error (specifically any subclass of
|
|
217
|
+
Exception), return the error instance as the result for that receiver.
|
|
218
|
+
"""
|
|
219
|
+
if (
|
|
220
|
+
not self.receivers
|
|
221
|
+
or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
|
|
222
|
+
):
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
# Call each receiver with whatever arguments it can accept.
|
|
226
|
+
# Return a list of tuple pairs [(receiver, response), ... ].
|
|
227
|
+
responses = []
|
|
228
|
+
sync_receivers = self._live_receivers(sender)
|
|
229
|
+
for receiver in sync_receivers:
|
|
230
|
+
try:
|
|
231
|
+
response = receiver(signal=self, sender=sender, **named)
|
|
232
|
+
except Exception as err:
|
|
233
|
+
self._log_robust_failure(receiver, err)
|
|
234
|
+
responses.append((receiver, err))
|
|
235
|
+
else:
|
|
236
|
+
responses.append((receiver, response))
|
|
237
|
+
return responses
|
|
238
|
+
|
|
239
|
+
def _clear_dead_receivers(self):
|
|
240
|
+
# Note: caller is assumed to hold self.lock.
|
|
241
|
+
if self._dead_receivers:
|
|
242
|
+
self._dead_receivers = False
|
|
243
|
+
self.receivers = [
|
|
244
|
+
r
|
|
245
|
+
for r in self.receivers
|
|
246
|
+
if not (isinstance(r[1], weakref.ReferenceType) and r[1]() is None)
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
def _live_receivers(self, sender):
|
|
250
|
+
"""
|
|
251
|
+
Filter sequence of receivers to get resolved, live receivers.
|
|
252
|
+
|
|
253
|
+
This checks for weak references and resolves them, then returning only
|
|
254
|
+
live receivers.
|
|
255
|
+
"""
|
|
256
|
+
receivers = None
|
|
257
|
+
if self.use_caching and not self._dead_receivers:
|
|
258
|
+
receivers = self.sender_receivers_cache.get(sender)
|
|
259
|
+
# We could end up here with NO_RECEIVERS even if we do check this case in
|
|
260
|
+
# .send() prior to calling _live_receivers() due to concurrent .send() call.
|
|
261
|
+
if receivers is NO_RECEIVERS:
|
|
262
|
+
return []
|
|
263
|
+
if receivers is None:
|
|
264
|
+
with self.lock:
|
|
265
|
+
self._clear_dead_receivers()
|
|
266
|
+
senderkey = _make_id(sender)
|
|
267
|
+
receivers = []
|
|
268
|
+
for (_receiverkey, r_senderkey), receiver in self.receivers:
|
|
269
|
+
if r_senderkey == NONE_ID or r_senderkey == senderkey:
|
|
270
|
+
receivers.append(receiver)
|
|
271
|
+
if self.use_caching:
|
|
272
|
+
if not receivers:
|
|
273
|
+
self.sender_receivers_cache[sender] = NO_RECEIVERS
|
|
274
|
+
else:
|
|
275
|
+
# Note, we must cache the weakref versions.
|
|
276
|
+
self.sender_receivers_cache[sender] = receivers
|
|
277
|
+
non_weak_sync_receivers = []
|
|
278
|
+
for receiver in receivers:
|
|
279
|
+
if isinstance(receiver, weakref.ReferenceType):
|
|
280
|
+
# Dereference the weak reference.
|
|
281
|
+
receiver = receiver()
|
|
282
|
+
if receiver is not None:
|
|
283
|
+
non_weak_sync_receivers.append(receiver)
|
|
284
|
+
else:
|
|
285
|
+
non_weak_sync_receivers.append(receiver)
|
|
286
|
+
return non_weak_sync_receivers
|
|
287
|
+
|
|
288
|
+
def _remove_receiver(self, receiver=None):
|
|
289
|
+
# Mark that the self.receivers list has dead weakrefs. If so, we will
|
|
290
|
+
# clean those up in connect, disconnect and _live_receivers while
|
|
291
|
+
# holding self.lock. Note that doing the cleanup here isn't a good
|
|
292
|
+
# idea, _remove_receiver() will be called as side effect of garbage
|
|
293
|
+
# collection, and so the call can happen while we are already holding
|
|
294
|
+
# self.lock.
|
|
295
|
+
self._dead_receivers = True
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def receiver(signal, **kwargs):
|
|
299
|
+
"""
|
|
300
|
+
A decorator for connecting receivers to signals. Used by passing in the
|
|
301
|
+
signal (or list of signals) and keyword arguments to connect::
|
|
302
|
+
|
|
303
|
+
@receiver(post_save, sender=MyModel)
|
|
304
|
+
def signal_receiver(sender, **kwargs):
|
|
305
|
+
...
|
|
306
|
+
|
|
307
|
+
@receiver([post_save, post_delete], sender=MyModel)
|
|
308
|
+
def signals_receiver(sender, **kwargs):
|
|
309
|
+
...
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def _decorator(func):
|
|
313
|
+
if isinstance(signal, list | tuple):
|
|
314
|
+
for s in signal:
|
|
315
|
+
s.connect(func, **kwargs)
|
|
316
|
+
else:
|
|
317
|
+
signal.connect(func, **kwargs)
|
|
318
|
+
return func
|
|
319
|
+
|
|
320
|
+
return _decorator
|