Pyjoyment 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 (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Loader.py ADDED
@@ -0,0 +1,342 @@
1
+ r"""
2
+ Pyjo.Loader - Load all kinds of things
3
+ ======================================
4
+ ::
5
+
6
+ from Pyjo.Loader import data_section, find_modules, load_class
7
+ from Pyjo.Util import warn
8
+
9
+ # Find modules in a namespace
10
+ for module in find_modules('Some.Namespace'):
11
+
12
+ # Load them safely
13
+ e = load_class(module)
14
+ if isinstance(e, Exception):
15
+ warn(f'Loading "{module}" failed: {e}')
16
+ continue
17
+
18
+ # And extract files from the DATA section
19
+ print(data_section(module, 'some_file.txt'))
20
+
21
+ :mod:`Pyjo.Loader` is a class loader and plugin framework. Aside from finding
22
+ modules and loading classes, it allows multiple files to be stored in the
23
+ ``DATA`` section of a module, which can then be accessed individually. The
24
+ section is the ``__DATA__`` variable of the module, or its docstring after a
25
+ line ``__DATA__``. ::
26
+
27
+ # Foo.py
28
+
29
+ __DATA__ = r'''
30
+ @@ test.txt
31
+ This is the first file.
32
+
33
+ @@ test2.html (base64)
34
+ VGhpcyBpcyB0aGUgc2Vjb25kIGZpbGUu
35
+
36
+ @@ test
37
+ This is the
38
+ third file.
39
+ '''
40
+
41
+ Each file has a header starting with ``@@``, followed by the file name and
42
+ optional instructions for decoding its content. Currently only the Base64
43
+ encoding is supported, which can be quite convenient for the storage of binary
44
+ data.
45
+
46
+ A class is named as a module of Python, with dots, and is loaded from
47
+ :data:`sys.path`. A function that takes a class takes the module, a class, or
48
+ the dotted name of a module or of a class in it.
49
+
50
+ See also
51
+ --------
52
+
53
+ :mod:`Pyjoyment`, https://mojolicious.org.
54
+
55
+ Ported from `Mojo::Loader <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Loader.pm>`_
56
+ of Mojolicious 9.49.
57
+
58
+ Functions
59
+ ---------
60
+ """
61
+
62
+ import Pyjo.Exception
63
+ import Pyjo.File
64
+ import Pyjo.String.Unicode
65
+
66
+ from Pyjo.Util import b64_decode
67
+
68
+ import importlib
69
+ import importlib.util
70
+ import os
71
+ import re
72
+ import sys
73
+ import traceback
74
+
75
+ from importlib.machinery import ModuleSpec
76
+ from types import ModuleType
77
+
78
+
79
+ # A class: its module, a class in it, or the dotted name of either
80
+ type Class = ModuleType | type | str
81
+
82
+ _BIN: dict[str, dict[str, bool]] = {}
83
+ _CACHE: dict[str, dict[str, str | bytes]] = {}
84
+
85
+
86
+ def data_section(cls: Class | None, name: str | None = None) -> 'dict[str, str | bytes] | str | bytes | None':
87
+ """::
88
+
89
+ all = data_section('Foo.Bar')
90
+ index = data_section('Foo.Bar', 'index.html')
91
+
92
+ Extract embedded file from the ``DATA`` section of a module, all files
93
+ will be cached once they have been accessed for the first time. A file
94
+ encoded in Base64 is :class:`bytes`, any other a :class:`str`. ::
95
+
96
+ # List embedded files
97
+ for name in data_section('Foo.Bar'):
98
+ print(name)
99
+ """
100
+ if not cls:
101
+ return None
102
+ return _all(cls).get(name) if name else _all(cls)
103
+
104
+
105
+ def file_is_binary(cls: Class, name: str) -> bool | None:
106
+ """::
107
+
108
+ boolean = file_is_binary('Foo.Bar', 'test.png')
109
+
110
+ Check if embedded file from the ``DATA`` section of a module was Base64
111
+ encoded.
112
+ """
113
+ return bool(_BIN.get(_key(cls) or '', {}).get(name)) if len(_all(cls)) else None
114
+
115
+
116
+ def find_modules(ns: str, *, recursive: bool = False) -> list[str]:
117
+ """::
118
+
119
+ modules = find_modules('MyApp.Namespace')
120
+ modules = find_modules('MyApp.Namespace', recursive=True)
121
+
122
+ Search for modules in a namespace, in the directories of
123
+ :data:`sys.path`, without loading them. A package, a directory with an
124
+ ``__init__.py``, is a module too.
125
+
126
+ These options are currently available:
127
+
128
+ recursive
129
+ ::
130
+
131
+ recursive=True
132
+
133
+ Search namespace recursively.
134
+ """
135
+ parts = ns.split('.')
136
+ dirs = [d for d in (Pyjo.File.new(p or '.', *parts) for p in sys.path) if os.path.isdir(d)]
137
+
138
+ modules: set[str] = set()
139
+ for dir in dirs:
140
+ for file in dir.list_tree() if recursive else dir.list(dir=True):
141
+ path = file.to_str()
142
+
143
+ # Not in Mojolicious: a package is a module, as Foo.pm next to Foo/ is in Perl
144
+ if os.path.isdir(path):
145
+ if not os.path.isfile(path := os.path.join(path, '__init__.py')):
146
+ continue
147
+
148
+ if not path.endswith('.py'):
149
+ continue
150
+ rel = Pyjo.File.new(path[:-3]).to_rel(dir).to_list()
151
+ if rel[-1] == '__init__':
152
+ rel.pop()
153
+ if len(rel):
154
+ modules.add('.'.join([ns, *rel]))
155
+
156
+ return sorted(modules)
157
+
158
+
159
+ def find_packages(ns: str) -> list[str]:
160
+ """::
161
+
162
+ pkgs = find_packages('MyApp.Namespace')
163
+
164
+ Search for packages in a namespace non-recursively: the modules in it that
165
+ are loaded, as :data:`sys.modules` has them.
166
+ """
167
+ prefix = f'{ns}.'
168
+ return sorted(n for n in list(sys.modules) if n.startswith(prefix) and '.' not in n[len(prefix) :])
169
+
170
+
171
+ def load_class(cls: str) -> 'Pyjo.Exception.object | bool | None':
172
+ """::
173
+
174
+ e = load_class('Foo.Bar')
175
+
176
+ Load a class and catch exceptions, returns a false value if loading was
177
+ successful, a true value if the class was not found, or a
178
+ :mod:`Pyjo.Exception` object if loading failed. A directory without an
179
+ ``__init__.py`` (a namespace package) is not a class. The context of the
180
+ exception is the line of the module that failed to load, and the exception
181
+ of Python is its ``__cause__``. ::
182
+
183
+ # Handle exceptions
184
+ if e := load_class('Foo.Bar'):
185
+ raise Exception(f'Exception: {e}' if isinstance(e, Exception) else 'Not found!')
186
+ """
187
+ # Invalid class name
188
+ if not re.fullmatch(r'\w(?:[\w.]*\w)?', cls or ''):
189
+ return True
190
+ # Left out from Mojolicious: the deprecated package separator "'", which Python has never had
191
+
192
+ # Load if not already loaded
193
+ module = sys.modules.get(cls)
194
+ try:
195
+ spec = module.__spec__ if module is not None else _find_spec(cls)
196
+ except ModuleNotFoundError as e:
197
+ # Does not exist
198
+ if e.name is not None and (cls == e.name or cls.startswith(f'{e.name}.')):
199
+ return True
200
+ return _exception(e, None)
201
+ except Exception as e:
202
+ return _exception(e, None)
203
+ if module is not None and spec is None:
204
+ return None
205
+
206
+ # Does not exist, or a namespace package
207
+ if spec is None or spec.origin is None:
208
+ return True
209
+
210
+ try:
211
+ importlib.import_module(cls)
212
+ except Exception as e:
213
+ # Real error
214
+ return _exception(e, spec.origin)
215
+ return None
216
+
217
+
218
+ def load_classes(ns: str) -> list[str]:
219
+ """::
220
+
221
+ classes = load_classes('Foo.Bar')
222
+
223
+ Load all classes in a namespace recursively.
224
+ """
225
+ classes: list[str] = []
226
+ for module in find_modules(ns, recursive=True):
227
+ if not (e := load_class(module)):
228
+ classes.append(module)
229
+ if isinstance(e, Exception):
230
+ raise e
231
+
232
+ return classes
233
+
234
+
235
+ def _all(cls: Class) -> dict[str, str | bytes]:
236
+ if (module := _module(cls)) is None:
237
+ return {}
238
+ name = module.__name__
239
+ if name in _CACHE:
240
+ return _CACHE[name]
241
+
242
+ # Not in Mojolicious: the __DATA__ variable of a module, or its docstring after a line __DATA__
243
+ data = getattr(module, '__DATA__', None)
244
+ if not isinstance(data, str):
245
+ doc = module.__doc__
246
+ if not isinstance(doc, str) or not re.search(r'^__DATA__\r?$', doc, re.M):
247
+ return {}
248
+ data = doc
249
+
250
+ # Ignore everything before __DATA__
251
+ data = re.sub(r'\A.*\n__DATA__\r?\n', '\n', data, count=1, flags=re.S)
252
+
253
+ # Ignore everything after __END__
254
+ data = re.sub(r'\n__END__\r?\n.*\Z', '\n', data, count=1, flags=re.S)
255
+
256
+ # Split files
257
+ files = Pyjo.String.Unicode.split(re.compile(r'^@@\s*(.+?)\s*\r?\n', re.M), data)[1:]
258
+
259
+ # Find data
260
+ all: dict[str, str | bytes] = {}
261
+ _CACHE[name] = all
262
+ while len(files):
263
+ file, content = files[0] or '', files[1] if len(files) > 1 else None
264
+ del files[:2]
265
+ content = content or ''
266
+ if (base := re.sub(r'\s*\(\s*base64\s*\)$', '', file)) != file:
267
+ _BIN.setdefault(name, {})[base] = True
268
+ all[base] = b64_decode(content)
269
+ else:
270
+ all[file] = content
271
+
272
+ return all
273
+
274
+
275
+ # The exception of a module that failed to load, with the context of the line of the module
276
+ def _exception(e: Exception, origin: str | None) -> Pyjo.Exception.object:
277
+ err = Pyjo.Exception.new(e)
278
+ err.__cause__ = e
279
+
280
+ # Unlike Mojolicious: croak reports the line of the caller, Python the line of the raise, so the frames of the
281
+ # module come first, or the location of a syntax error, which is not in a frame
282
+ if isinstance(e, SyntaxError) and e.filename is not None and e.lineno is not None:
283
+ err.frames.insert(0, traceback.FrameSummary(e.filename, e.lineno, '<module>', lookup_line=False, line=e.text))
284
+ elif origin is not None:
285
+ for i, frame in enumerate(err.frames):
286
+ if frame.filename == origin and frame.name == '<module>':
287
+ del err.frames[:i]
288
+ break
289
+
290
+ return err.inspect()
291
+
292
+
293
+ # The module of a class, or the longest prefix of a dotted name that is loaded
294
+ # Not in Mojolicious: the finders of importlib keep the files of a directory until its time of modification changes,
295
+ # which may not show a module written right after they looked, where require of Perl looks at the files each time, so a
296
+ # module that is not found is looked for again without them
297
+ def _find_spec(cls: str) -> ModuleSpec | None:
298
+ try:
299
+ if (spec := importlib.util.find_spec(cls)) is not None:
300
+ return spec
301
+ except ModuleNotFoundError:
302
+ pass
303
+ importlib.invalidate_caches()
304
+ return importlib.util.find_spec(cls)
305
+
306
+
307
+ def _module(cls: Class) -> ModuleType | None:
308
+ if isinstance(cls, ModuleType):
309
+ return cls
310
+ name = cls.__module__ if isinstance(cls, type) else cls
311
+ parts = name.split('.')
312
+ for i in range(len(parts), 0, -1):
313
+ if (module := sys.modules.get('.'.join(parts[:i]))) is not None:
314
+ return module
315
+ return None
316
+
317
+
318
+ # The name of the module of a class, the key of the cache
319
+ def _key(cls: Class) -> str | None:
320
+ return module.__name__ if (module := _module(cls)) is not None else None
321
+
322
+
323
+ # Not in Mojolicious: the class of a dotted name, which a Perl package is, for the modules of Pyjoyment that load
324
+ # plugins and controllers by name: the class that a module names its object, or an attribute of the longest prefix of
325
+ # the name that is a module
326
+ def _class(name: str) -> type | None: # pyright: ignore[reportUnusedFunction]
327
+ if (module := sys.modules.get(name)) is not None:
328
+ found: object = getattr(module, 'object', None)
329
+ return found if isinstance(found, type) else None
330
+ parts = name.split('.')
331
+ for i in range(len(parts) - 1, 0, -1):
332
+ try:
333
+ found = importlib.import_module('.'.join(parts[:i]))
334
+ except ImportError:
335
+ continue
336
+ try:
337
+ for attr in parts[i:]:
338
+ found = getattr(found, attr)
339
+ except AttributeError:
340
+ return None
341
+ return found if isinstance(found, type) else None
342
+ return None