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.
Files changed (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. 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__}>"
@@ -0,0 +1,14 @@
1
+ # Signals
2
+
3
+ Run code when certain events happen.
4
+
5
+ ```python
6
+ from plain.signals import request_finished
7
+
8
+
9
+ def on_request_finished(sender, **kwargs):
10
+ print("Request finished!")
11
+
12
+
13
+ request_finished.connect(on_request_finished)
14
+ ```
@@ -0,0 +1,5 @@
1
+ from plain.signals.dispatch import Signal
2
+
3
+ request_started = Signal()
4
+ request_finished = Signal()
5
+ got_request_exception = Signal()
@@ -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