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
@@ -0,0 +1,482 @@
1
+ """
2
+ Pyjo.Server.Reload - Auto-restarting development server
3
+ ========================================================
4
+ ::
5
+
6
+ from Pyjo.Server.Reload import Reload
7
+
8
+ reload = Reload()
9
+ reload.run('/home/sri/myapp.py')
10
+
11
+ # Or import the module, as in Google Python Style Guide
12
+
13
+ import Pyjo.Server.Reload
14
+
15
+ reload = Pyjo.Server.Reload.new()
16
+ reload.run('/home/sri/myapp.py')
17
+ reload.run('/home/sri/myapp.py', 'prefork', '-w', '1')
18
+
19
+ Pyjo.Server.Reload.new(exec=['npx webpack --watch']).run('/home/sri/myapp.py')
20
+
21
+ :mod:`Pyjo.Server.Reload` is a self-restart capable development server. It
22
+ starts your application with a server command, like
23
+ :mod:`Pyjoyment.Command.daemon` or :mod:`Pyjoyment.Command.prefork`, and
24
+ restarts it whenever a file changes. Listen sockets are only created once and
25
+ then passed on to the server, so they should be configured with
26
+ :mod:`Pyjo.Server.Reload` and not the server command. Note that the server
27
+ uses signals for process management, so you should avoid modifying signal
28
+ handlers in your applications.
29
+
30
+ Additional commands, like asset bundlers in watch mode, can be started
31
+ alongside the server with :attr:`~Pyjo_Server_Reload.exec`. They only get
32
+ started once, are not restarted together with the server, and get stopped when
33
+ :mod:`Pyjo.Server.Reload` shuts down.
34
+
35
+ To start applications with it you can use the ``pyjo-reload`` script, or
36
+ ``python -m Pyjo.Server.Reload``. ::
37
+
38
+ $ pyjo-reload ./myapp.py
39
+ Web application available at http://127.0.0.1:3000
40
+
41
+ The server and the commands run in process groups of their own, which the
42
+ signals of POSIX stop, so it does not run on Windows. Name resolution in a
43
+ thread, SOCKS5 and TLS support can be disabled with the ``PYJO_NO_NNR``,
44
+ ``PYJO_NO_SOCKS`` and ``PYJO_NO_TLS`` environment variables.
45
+
46
+ Signals
47
+ -------
48
+
49
+ The :mod:`Pyjo.Server.Reload` process can be controlled at runtime with the
50
+ following signals.
51
+
52
+ INT, TERM
53
+ ~~~~~~~~~
54
+
55
+ Shut down server immediately. The server, all
56
+ :attr:`~Pyjo_Server_Reload.exec` commands and their subprocesses get a
57
+ ``TERM`` signal, and the server also ``QUIT``, and will be killed if they have
58
+ not stopped after 5 seconds, just like for restarts.
59
+
60
+ HUP
61
+ ~~~
62
+
63
+ Restart the server, as a change of a file does, while the
64
+ :attr:`~Pyjo_Server_Reload.exec` commands go on.
65
+
66
+ pyjo-reload
67
+ -----------
68
+ ::
69
+
70
+ Usage: pyjo-reload [OPTIONS] APPLICATION [COMMAND] [ARGS]
71
+
72
+ pyjo-reload ./myapp.py
73
+ pyjo-reload ./myapp.py prefork -w 1
74
+ pyjo-reload -l http://127.0.0.1:8080 -w assets/css ./myapp.py prefork -w 3
75
+ pyjo-reload -m production -l https://*:443 -l http://[::]:3000 ./myapp.py
76
+ pyjo-reload -l 'https://*:443?cert=./server.crt&key=./server.key' ./myapp.py
77
+ pyjo-reload -w /usr/local/lib -w public -w myapp.conf ./myapp.py
78
+ pyjo-reload -x 'npx webpack --watch' ./myapp.py
79
+
80
+ Options:
81
+ -b, --backend <name> Reload backend to use for reloading,
82
+ defaults to "Poll"
83
+ -h, --help Show this message
84
+ -l, --listen <location> One or more locations you want to listen on,
85
+ defaults to the value of PYJO_LISTEN, the
86
+ port of PORT or "http://*:3000"
87
+ --log-format <name> Format of log messages, "text" or "json",
88
+ defaults to the value of PYJO_LOG_FORMAT or
89
+ "text"
90
+ -m, --mode <name> Operating mode for your application,
91
+ defaults to the value of PYJO_MODE or
92
+ "development"
93
+ -v, --verbose Print details about what files changed to
94
+ sys.stdout
95
+ -w, --watch <directory/file> One or more directories and files to watch
96
+ for changes, defaults to the application
97
+ script as well as the "lib" and "templates"
98
+ directories in the current working directory
99
+ -x, --exec <command> One or more shell commands to start
100
+ alongside the server, like asset bundlers in
101
+ watch mode
102
+
103
+ Start :mod:`Pyjoyment` applications with the :mod:`Pyjo.Server.Reload`
104
+ restarter. The server command defaults to ``daemon``, and all options of
105
+ ``pyjo-reload`` need to come before the application. Shell commands started
106
+ with ``--exec`` run for as long as :mod:`Pyjo.Server.Reload` does, they are
107
+ not restarted together with the server.
108
+
109
+ See also
110
+ --------
111
+
112
+ :mod:`Pyjoyment`, https://mojolicious.org.
113
+
114
+ Ported from `Mojo::Server::Morbo <https://github.com/mojolicious/mojo/blob/v9.53/lib/Mojo/Server/Morbo.pm>`_
115
+ and `script/morbo <https://github.com/mojolicious/mojo/blob/v9.53/script/morbo>`_ of Mojolicious 9.53.
116
+
117
+ Classes
118
+ -------
119
+ """
120
+
121
+ import Pyjo.Base
122
+ import Pyjo.Exception
123
+ import Pyjo.GetOpt
124
+ import Pyjo.Server.Daemon
125
+
126
+ from Pyjo.Loader import load_class
127
+ from Pyjo.Util import die, extract_usage, getopt, warn
128
+
129
+ import contextlib
130
+ import os
131
+ import re
132
+ import signal
133
+ import sys
134
+ import time
135
+
136
+ from collections.abc import Sequence
137
+ from pkgutil import extend_path
138
+ from typing import TYPE_CHECKING, Any, NoReturn
139
+
140
+
141
+ if TYPE_CHECKING:
142
+ import Pyjo.Server.Reload.Backend
143
+
144
+
145
+ # Not in Mojolicious: a namespace that other distributions can add to, as Pyjo.Server.Reload.Backend is one
146
+ __path__ = extend_path(__path__, __name__)
147
+
148
+ # The signals that stop the manager, and HUP, which restarts the server, blocked across a fork, so that the child does
149
+ # not run the handlers of the manager before its exec
150
+ _SIGNALS = {signal.SIGINT, signal.SIGTERM, signal.SIGHUP} if hasattr(signal, 'SIGHUP') else set[signal.Signals]()
151
+
152
+
153
+ # Renamed from Mojolicious: Mojo::Server::Morbo, as the names of Futurama are not ours
154
+ class Pyjo_Server_Reload(Pyjo.Base.object):
155
+ """
156
+ :mod:`Pyjo.Server.Reload` inherits all methods from :mod:`Pyjo.Base` and
157
+ implements the following new ones.
158
+ """
159
+
160
+ def __init__(
161
+ self,
162
+ *,
163
+ backend: 'Pyjo.Server.Reload.Backend.object | None' = None,
164
+ daemon: Pyjo.Server.Daemon.object | None = None,
165
+ exec: list[str] | None = None,
166
+ silent: bool = True,
167
+ ) -> None:
168
+ """::
169
+
170
+ reload = Pyjo.Server.Reload.new()
171
+ reload = Pyjo.Server.Reload.new(silent=False)
172
+
173
+ Construct a new :mod:`Pyjo.Server.Reload` object.
174
+ """
175
+ super().__init__()
176
+
177
+ self.exec: list[str] = exec if exec is not None else []
178
+ """::
179
+
180
+ commands = reload.exec
181
+ reload.exec = ['npx webpack --watch', 'npx tailwindcss -i app.css -o public/assets/app.css --watch']
182
+
183
+ Shell commands to start alongside the server, like asset bundlers in
184
+ watch mode. The ``PYJO_MODE`` environment variable will be set to the
185
+ operating mode of your application, so commands can use it too. The
186
+ standard input of all commands is a pipe that gets closed when
187
+ :mod:`Pyjo.Server.Reload` exits, even if it gets killed unexpectedly,
188
+ which some tools use to shut down. When a command exits on its own, a
189
+ message with its exit status is written to ``sys.stderr``.
190
+ """
191
+
192
+ self.silent = silent
193
+ """::
194
+
195
+ boolean = reload.silent
196
+ reload.silent = False
197
+
198
+ Disable console messages, defaults to a true value.
199
+ """
200
+
201
+ self._backend = backend
202
+ self._command: list[str] = []
203
+ self._commands: dict[int, str] = {}
204
+ self._daemon = daemon
205
+ self._finished = False
206
+ self._kill: dict[int, float] = {}
207
+ self._modified = False
208
+ self._stdin: int | None = None
209
+ self._worker: int | None = None
210
+
211
+ @property
212
+ def backend(self) -> 'Pyjo.Server.Reload.Backend.object':
213
+ """::
214
+
215
+ backend = reload.backend
216
+ reload.backend = Pyjo.Server.Reload.Backend.Poll.new()
217
+
218
+ Backend, usually a :mod:`Pyjo.Server.Reload.Backend.Poll` object, or
219
+ ``Pyjo.Server.Reload.Backend.<Name>`` of the ``PYJO_RELOAD_BACKEND``
220
+ environment variable.
221
+ """
222
+ if self._backend is None:
223
+ name = f'Pyjo.Server.Reload.Backend.{os.environ.get("PYJO_RELOAD_BACKEND") or "Poll"}'
224
+ if e := load_class(name):
225
+ if isinstance(e, BaseException):
226
+ raise e
227
+ raise Pyjo.Exception.new(f'Can\'t find Reload backend class "{name}" in sys.path. ({sys.path})\n')
228
+ self._backend = sys.modules[name].new()
229
+ return self._backend
230
+
231
+ @backend.setter
232
+ def backend(self, value: 'Pyjo.Server.Reload.Backend.object') -> None:
233
+ self._backend = value
234
+
235
+ @property
236
+ def daemon(self) -> Pyjo.Server.Daemon.object:
237
+ """::
238
+
239
+ daemon = reload.daemon
240
+ reload.daemon = Pyjo.Server.Daemon.new()
241
+
242
+ :mod:`Pyjo.Server.Daemon` object used to create listen sockets. Other
243
+ settings will be ignored, since the server runs in a separate process,
244
+ use options of the server command instead.
245
+ """
246
+ if self._daemon is None:
247
+ self._daemon = Pyjo.Server.Daemon.new(silent=True)
248
+ return self._daemon
249
+
250
+ @daemon.setter
251
+ def daemon(self, value: Pyjo.Server.Daemon.object) -> None:
252
+ self._daemon = value
253
+
254
+ def run(self, app: str, *args: str) -> NoReturn:
255
+ """::
256
+
257
+ reload.run('myapp.py')
258
+ reload.run('myapp.py', 'prefork', '-w', '1')
259
+
260
+ Run server command for application and wait for the signals. The
261
+ command defaults to ``daemon``.
262
+ """
263
+
264
+ # Unlike Mojolicious: no Windows, where Mojo has no fork emulation
265
+ if not hasattr(os, 'fork'):
266
+ die('Pyjo.Server.Reload does not support Windows.\n')
267
+
268
+ # Clean manager environment
269
+ def stop(signum: int, frame: Any) -> None:
270
+ self._finished = True
271
+ for pid in [self._worker, *self._commands]:
272
+ self._stop(pid)
273
+
274
+ # Unlike Mojolicious: HUP restarts the server, as it reloads the other servers of Pyjoyment, where Mojo stops on
275
+ # it
276
+ def restart(signum: int, frame: Any) -> None:
277
+ if not self._finished:
278
+ self._stop(self._worker)
279
+ self._modified = True
280
+
281
+ # Not in Mojolicious: local %SIG, the handlers of the signals are set back at the end
282
+ handlers = {name: signal.signal(name, restart if name == signal.SIGHUP else stop) for name in _SIGNALS}
283
+ try:
284
+ self.backend.watch.insert(0, app)
285
+ sys.argv[0] = app
286
+ self._command = [sys.executable, app, *(args or ['daemon'])]
287
+ self._modified = True
288
+
289
+ # Prepare and cache listen sockets for smooth restarting (inherited by the server)
290
+ self.daemon.start().stop()
291
+ os.environ['PYJO_LISTEN'] = ','.join(str(location) for location in self.daemon.listen)
292
+ # Renamed from Mojolicious: PERL5LIB, the path of the modules of the manager, with its current directory
293
+ os.environ['PYTHONPATH'] = os.pathsep.join(os.path.abspath(path) for path in sys.path)
294
+
295
+ # Commands (like asset bundlers in watch mode) need to know the mode too, and only get started once
296
+ if not os.environ.get('PYJO_MODE'):
297
+ os.environ['PYJO_MODE'] = 'development'
298
+
299
+ # Commands get a pipe as STDIN that stays open until the manager exits, so they can read from it without
300
+ # getting stopped (process group is not in the foreground) and know when to exit if it dies unexpectedly
301
+ # (like esbuild does)
302
+ stdin, self._stdin = os.pipe()
303
+ self._commands = {self._spawn(stdin, *_shell(command)): command for command in self.exec}
304
+ os.close(stdin)
305
+
306
+ while not (self._finished and not self._worker and not self._commands):
307
+ self._manage()
308
+ finally:
309
+ for name, handler in handlers.items():
310
+ signal.signal(name, handler)
311
+ sys.exit(0)
312
+
313
+ def _manage(self) -> None:
314
+ if files := self.backend.modified_files():
315
+ if not self.silent:
316
+ print(
317
+ f'File "{files[0]}" changed, restarting.'
318
+ if len(files) == 1
319
+ else f'{len(files)} files changed, restarting.'
320
+ )
321
+ self._stop(self._worker)
322
+ self._modified = True
323
+
324
+ for pid in [pid for pid in (self._worker, *self._commands) if pid is not None]:
325
+ try:
326
+ done, status = os.waitpid(pid, os.WNOHANG)
327
+ except ChildProcessError:
328
+ done, status = pid, 0
329
+ if done == pid:
330
+ self._kill.pop(pid, None)
331
+ if self._worker == pid:
332
+ self._worker = None
333
+ elif not self._finished:
334
+ command = self._commands.get(pid)
335
+ warn(
336
+ f'Command "{command}" was killed by signal {os.WTERMSIG(status)}.\n'
337
+ if os.WIFSIGNALED(status)
338
+ else f'Command "{command}" exited with status {os.WEXITSTATUS(status)}.\n'
339
+ )
340
+ self._commands.pop(pid, None)
341
+
342
+ # Process did not stop in time
343
+ elif pid in self._kill:
344
+ if self._kill[pid] <= time.monotonic():
345
+ _kill(signal.SIGKILL, -pid)
346
+
347
+ # Process was started after the signal handler ran
348
+ elif self._finished:
349
+ self._stop(pid)
350
+
351
+ if not self._worker and not self._finished and self._modified:
352
+ self._modified = False
353
+ self._worker = self._spawn(None, *self._command)
354
+
355
+ def _spawn(self, stdin: int | None, *command: str) -> int:
356
+ # Not in Mojolicious: the buffers of the standard streams are written before, so that the child does not write
357
+ # them again, and the signals of the manager are blocked, so that the child does not run its handlers
358
+ _flush()
359
+ mask = signal.pthread_sigmask(signal.SIG_BLOCK, _SIGNALS)
360
+ try:
361
+ pid = os.fork()
362
+ except OSError as e:
363
+ signal.pthread_sigmask(signal.SIG_SETMASK, mask)
364
+ raise OSError(e.errno, f"Can't fork: {e.strerror}") from e
365
+
366
+ # Manager (process group gets set in both processes to avoid a race)
367
+ if pid:
368
+ signal.pthread_sigmask(signal.SIG_SETMASK, mask)
369
+ with contextlib.suppress(OSError):
370
+ os.setpgid(pid, pid)
371
+ return pid
372
+
373
+ # Worker (process group includes subprocesses)
374
+ try:
375
+ os.setpgid(0, 0)
376
+ for name in _SIGNALS:
377
+ signal.signal(name, signal.SIG_DFL)
378
+ # Not in Mojolicious: the server ignores QUIT until it has a handler of its own, as QUIT, which stops it
379
+ # immediately, would dump its core before
380
+ if stdin is None:
381
+ signal.signal(signal.SIGQUIT, signal.SIG_IGN)
382
+ signal.pthread_sigmask(signal.SIG_SETMASK, mask)
383
+ if stdin is not None:
384
+ try:
385
+ os.dup2(stdin, 0)
386
+ except OSError as e:
387
+ warn(f"Can't redirect STDIN: {e.strerror}\n")
388
+ os.execvp(command[0], command)
389
+ except OSError as e:
390
+ warn(f'Can\'t exec "{command[0]}": {e.strerror}\n')
391
+ except BaseException:
392
+ pass
393
+
394
+ # Skip cleanup code inherited from the manager if exec fails
395
+ _flush()
396
+ os._exit(1)
397
+
398
+ def _stop(self, pid: int | None) -> None:
399
+ if not pid:
400
+ return
401
+ # Unlike Mojolicious: the server gets QUIT too, which stops it immediately, as TERM stops the servers of
402
+ # Pyjoyment gracefully
403
+ if pid == self._worker:
404
+ _kill(signal.SIGQUIT, pid)
405
+ _kill(signal.SIGTERM, -pid)
406
+ self._kill.setdefault(pid, time.monotonic() + 5)
407
+
408
+
409
+ # Renamed from Mojolicious: script/morbo, the command line interface, a function of the module, whose usage is the
410
+ # section pyjo-reload of its docstring
411
+ def main(argv: Sequence[str] | None = None) -> NoReturn:
412
+ """::
413
+
414
+ main(['-l', 'http://*:8080', 'myapp.py'])
415
+ main(['-l', 'http://*:8080', 'myapp.py', 'prefork', '-w', '2'])
416
+
417
+ The command line interface of ``pyjo-reload``: parse the options and run
418
+ the server command for the application.
419
+ """
420
+ args = list(sys.argv[1:] if argv is None else argv)
421
+ exec: list[str] = []
422
+ listen: list[str] = []
423
+ watch: list[str] = []
424
+ opts = getopt(
425
+ args,
426
+ {
427
+ 'b|backend=s': Pyjo.GetOpt.env('PYJO_RELOAD_BACKEND'),
428
+ 'h|help': None,
429
+ 'l|listen=s': listen,
430
+ # Not in Mojolicious: the format of the log messages of Pyjo.Log, as the option of the commands
431
+ 'log-format=s': Pyjo.GetOpt.env('PYJO_LOG_FORMAT'),
432
+ 'm|mode=s': Pyjo.GetOpt.env('PYJO_MODE'),
433
+ 'v|verbose': None,
434
+ 'w|watch=s': watch,
435
+ 'x|exec=s': exec,
436
+ },
437
+ require_order=True,
438
+ )
439
+
440
+ if not opts or opts.help or not args:
441
+ # The usage without its last newline, as SystemExit writes the message with one
442
+ die(extract_usage(section='pyjo-reload').rstrip('\n'))
443
+ app = args.pop(0)
444
+ if args and args[0].startswith('-'):
445
+ die(f'pyjo-reload options need to come before the application, e.g. "pyjo-reload {args[0]} ... {app}"')
446
+ reload = Pyjo_Server_Reload.new(silent=not opts.verbose)
447
+ if listen:
448
+ reload.daemon.listen = listen
449
+ if watch:
450
+ reload.backend.watch = watch
451
+ if exec:
452
+ reload.exec = exec
453
+ reload.run(app, *args)
454
+
455
+
456
+ def _flush() -> None:
457
+ for stream in (sys.stdout, sys.stderr):
458
+ try:
459
+ stream.flush()
460
+ except Exception:
461
+ pass
462
+
463
+
464
+ # Perl's kill, true if the signal was sent
465
+ def _kill(sig: int, pid: int) -> bool:
466
+ try:
467
+ os.kill(pid, sig)
468
+ return True
469
+ except OSError:
470
+ return False
471
+
472
+
473
+ # Perl's exec of one string, through the shell only with its metacharacters, as a command of exec
474
+ def _shell(command: str) -> list[str]:
475
+ if re.search(r"""[$&*(){}\[\]'";\\|?<>~`\n]""", command):
476
+ return ['/bin/sh', '-c', command]
477
+ return command.split()
478
+
479
+
480
+ new = Pyjo_Server_Reload.new
481
+ object = Pyjo_Server_Reload
482
+ Reload = Pyjo_Server_Reload
@@ -0,0 +1,5 @@
1
+ import Pyjo.Server.Reload
2
+
3
+
4
+ # python -m Pyjo.Server.Reload, as pyjo-reload
5
+ Pyjo.Server.Reload.main()