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,591 @@
1
+ """
2
+ Pyjo.IOLoop.ChildProcess - Non-blocking child process
3
+ ======================================================
4
+ ::
5
+
6
+ from Pyjo.IOLoop.ChildProcess import ChildProcess
7
+
8
+ child = ChildProcess()
9
+ child.spawn(['ls', '-l'])
10
+
11
+ # Or import the module, as in Google Python Style Guide
12
+
13
+ import Pyjo.IOLoop.ChildProcess
14
+
15
+ # Run a program, with its output and its exit status
16
+ child = Pyjo.IOLoop.ChildProcess.new()
17
+ child.on('spawn', lambda child: child.stdout.on('read', lambda stream, chunk: print(chunk)))
18
+ child.on('close', lambda child, code, signal: print(f'Exited with {code}'))
19
+ child.on('error', lambda child, err: print(f"Can't run: {err}"))
20
+ child.spawn(['ls', '-l'])
21
+
22
+ # Write to its standard input, and end it
23
+ child = Pyjo.IOLoop.ChildProcess.new().spawn(['sort'])
24
+ child.stdout.on('read', lambda stream, chunk: print(chunk))
25
+ child.stdin.write(b'b\\na\\n')
26
+ child.stdin.close_gracefully()
27
+
28
+ # Its output as a promise, also in a coroutine
29
+ child = Pyjo.IOLoop.ChildProcess.new()
30
+ child.run_p(['ls', '-l']).then(lambda code, out, err: print(out))
31
+
32
+ # Start reactor if necessary
33
+ if not child.reactor.is_running:
34
+ child.reactor.start()
35
+
36
+ :mod:`Pyjo.IOLoop.ChildProcess` runs a program in a process of its own, as
37
+ ``CGI`` programs and command line tools are, with its standard input, output
38
+ and error as pipes that the reactor watches, as
39
+ :mod:`Pyjo.IOLoop.Stream` objects, without blocking the event loop. It runs
40
+ the program, not a function of Python, as :mod:`Pyjo.IOLoop.Subprocess` does
41
+ in a fork of the process.
42
+
43
+ The process is reaped by the reactor, which checks for its end with a timer,
44
+ and at once when its output and its error have ended, as most programs end
45
+ them when they exit. The end of the interpreter kills and reaps the processes
46
+ that are left, and closes their pipes.
47
+
48
+ The child of a fork leaves the processes of its parent to it: it closes its
49
+ copies of their pipes, and neither reaps them nor emits their events, which
50
+ the parent does.
51
+
52
+ The reactor watches pipes on POSIX systems, not on Windows, where
53
+ :meth:`~Pyjo_IOLoop_ChildProcess.spawn` emits the event ``error``.
54
+
55
+ Events
56
+ ------
57
+
58
+ :mod:`Pyjo.IOLoop.ChildProcess` inherits all events from
59
+ :mod:`Pyjo.EventEmitter` and can emit the following new ones.
60
+
61
+ close
62
+ ~~~~~
63
+ ::
64
+
65
+ child.on('close', lambda child, code, signal: ...)
66
+
67
+ Emitted once the process has exited and its output and its error have ended,
68
+ with its exit code, or the signal that terminated it, after the event
69
+ ``exit``. ::
70
+
71
+ child.on('close', lambda child, code, signal: print(f'Process {child.pid} closed with {code}'))
72
+
73
+ error
74
+ ~~~~~
75
+ ::
76
+
77
+ child.on('error', lambda child, err: ...)
78
+
79
+ Emitted if the program could not be run, as one that does not exist. ::
80
+
81
+ child.on('error', lambda child, err: print(f'Error: {err}'))
82
+
83
+ exit
84
+ ~~~~
85
+ ::
86
+
87
+ child.on('exit', lambda child, code, signal: ...)
88
+
89
+ Emitted once the process has exited, with its exit code, or ``None`` and the
90
+ signal that terminated it. Its output and its error may not have ended yet.
91
+ ::
92
+
93
+ child.on('exit', lambda child, code, signal: print(f'Process {child.pid} exited with {code}'))
94
+
95
+ spawn
96
+ ~~~~~
97
+ ::
98
+
99
+ child.on('spawn', lambda child: ...)
100
+
101
+ Emitted once the process has been spawned, before its output and its error
102
+ are read, so that the subscribers of this event get all of them. ::
103
+
104
+ child.on('spawn', lambda child: print(f'Running process {child.pid}'))
105
+
106
+ See also
107
+ --------
108
+
109
+ :mod:`Pyjoyment`, https://mojolicious.org.
110
+
111
+ Inspired by `child_process <https://nodejs.org/api/child_process.html>`_ of
112
+ Node.js, as Mojolicious runs no programs in its event loop.
113
+
114
+ Classes
115
+ -------
116
+ """
117
+
118
+ import Pyjo.EventEmitter
119
+ import Pyjo.Exception
120
+ import Pyjo.IOLoop
121
+ import Pyjo.IOLoop.Stream
122
+ import Pyjo.Reactor
123
+
124
+ import atexit
125
+ import os
126
+ import signal as signals
127
+ import subprocess
128
+ import weakref
129
+
130
+ from collections.abc import Mapping, Sequence
131
+ from typing import Any, Self
132
+
133
+
134
+ # The child processes that have not closed yet, with the process that started them, which the end of the interpreter
135
+ # kills and reaps, with their pipes, as Python warns of a process that still runs when it frees its object. A process
136
+ # stays until it closes, also after it has been reaped, as its output and its error, which a process that it has
137
+ # started can keep open, are read until their end, and nothing else may refer to it, as an application that is done
138
+ # with its output
139
+ _running: dict['Pyjo_IOLoop_ChildProcess', int] = {}
140
+
141
+ # The delay of the next check for the end of a process, which grows to the longest one
142
+ _DELAYS = (0, 0.01, 0.02, 0.05, 0.1)
143
+
144
+
145
+ class Pyjo_IOLoop_ChildProcess(Pyjo.EventEmitter.object):
146
+ """
147
+ :mod:`Pyjo.IOLoop.ChildProcess` inherits all methods from
148
+ :mod:`Pyjo.EventEmitter` and implements the following new ones.
149
+ """
150
+
151
+ def __init__(self, *, reactor: Pyjo.Reactor.object | None = None) -> None:
152
+ """::
153
+
154
+ child = Pyjo.IOLoop.ChildProcess.new()
155
+ child = Pyjo.IOLoop.ChildProcess.new(reactor=Pyjo.Reactor.Poll.new())
156
+
157
+ Construct a new :mod:`Pyjo.IOLoop.ChildProcess` object.
158
+ """
159
+ super().__init__()
160
+
161
+ self._check = 0
162
+ self._detached = False
163
+ self._ended = 0
164
+ self._exit_code: int | None = None
165
+ self._process: subprocess.Popen[bytes] | None = None
166
+ self._reactor: weakref.ref[Pyjo.Reactor.object] | None = None
167
+ self._signal_code: int | None = None
168
+ self._stderr: Pyjo.IOLoop.Stream.object | None = None
169
+ self._stdin: Pyjo.IOLoop.Stream.object | None = None
170
+ self._stdout: Pyjo.IOLoop.Stream.object | None = None
171
+ self._timer: str | None = None
172
+ if reactor is not None:
173
+ self.reactor = reactor
174
+
175
+ @property
176
+ def exit_code(self) -> int | None:
177
+ """::
178
+
179
+ code = child.exit_code
180
+
181
+ Exit code of the process, or ``None`` if it is still running or a
182
+ signal terminated it.
183
+ """
184
+ return self._exit_code
185
+
186
+ @property
187
+ def pid(self) -> int | None:
188
+ """::
189
+
190
+ pid = child.pid
191
+
192
+ Process id of the spawned process if available.
193
+ """
194
+ return self._process.pid if self._process is not None else None
195
+
196
+ @property
197
+ def reactor(self) -> Pyjo.Reactor.object:
198
+ """::
199
+
200
+ reactor = child.reactor
201
+ child.reactor = Pyjo.Reactor.Poll.new()
202
+
203
+ Low-level event reactor, defaults to the ``reactor`` attribute value of
204
+ the global :mod:`Pyjo.IOLoop` singleton. Note that this attribute is
205
+ weakened.
206
+ """
207
+ if self._reactor is None:
208
+ self._reactor = weakref.ref(Pyjo.IOLoop.singleton.reactor)
209
+ if (reactor := self._reactor()) is None:
210
+ raise ReferenceError('weakly-referenced object no longer exists')
211
+ return reactor
212
+
213
+ @reactor.setter
214
+ def reactor(self, value: Pyjo.Reactor.object) -> None:
215
+ self._reactor = weakref.ref(value)
216
+
217
+ @property
218
+ def signal_code(self) -> int | None:
219
+ """::
220
+
221
+ signal = child.signal_code
222
+
223
+ Signal that terminated the process, or ``None``.
224
+ """
225
+ return self._signal_code
226
+
227
+ @property
228
+ def stderr(self) -> Pyjo.IOLoop.Stream.object | None:
229
+ """::
230
+
231
+ stream = child.stderr
232
+
233
+ :mod:`Pyjo.IOLoop.Stream` object of the standard error of the process,
234
+ which the reactor reads without an inactivity timeout.
235
+ """
236
+ return self._stderr
237
+
238
+ @property
239
+ def stdin(self) -> Pyjo.IOLoop.Stream.object | None:
240
+ """::
241
+
242
+ stream = child.stdin
243
+
244
+ :mod:`Pyjo.IOLoop.Stream` object of the standard input of the process,
245
+ which the reactor writes without an inactivity timeout, and which ends
246
+ with :meth:`~Pyjo.IOLoop.Stream.Pyjo_IOLoop_Stream.close_gracefully`.
247
+ It closes once the process closes its end, so that a write that fails
248
+ is no error.
249
+ """
250
+ return self._stdin
251
+
252
+ @property
253
+ def stdout(self) -> Pyjo.IOLoop.Stream.object | None:
254
+ """::
255
+
256
+ stream = child.stdout
257
+
258
+ :mod:`Pyjo.IOLoop.Stream` object of the standard output of the
259
+ process, which the reactor reads without an inactivity timeout. ::
260
+
261
+ # Kill a process that has been quiet for 60 seconds
262
+ child.stdout.timeout = 60
263
+ child.stdout.on('timeout', lambda stream: child.kill())
264
+ """
265
+ return self._stdout
266
+
267
+ def kill(self, signal: int = signals.SIGTERM) -> bool:
268
+ """::
269
+
270
+ boolean = child.kill()
271
+ boolean = child.kill(signal.SIGKILL)
272
+
273
+ Send a signal to the process, ``SIGTERM`` by default, if it is still
274
+ running, or to the processes of its group, which it may have started,
275
+ when it has been spawned ``detached``, while any of them runs.
276
+ """
277
+ if (process := self._process) is None:
278
+ return False
279
+ if self._detached:
280
+ try:
281
+ os.killpg(process.pid, signal)
282
+
283
+ # No process of the group, or only the process that has exited and has not been reaped yet, for which macOS
284
+ # gives EPERM
285
+ except (ProcessLookupError, PermissionError):
286
+ return False
287
+ return True
288
+ if process.poll() is not None:
289
+ return False
290
+ process.send_signal(signal)
291
+ return True
292
+
293
+ def run_p(self, args: Sequence[str | bytes | os.PathLike[str]], *, input: bytes = b'', **options: Any) -> Any:
294
+ """::
295
+
296
+ promise = child.run_p(['ls', '-l'])
297
+ promise = child.run_p(['sort'], input=b'b\\na\\n', cwd='/tmp')
298
+
299
+ Same as :meth:`spawn`, with the options of :meth:`spawn`, but writes
300
+ ``input`` to the standard input of the process and ends it, and
301
+ returns a :mod:`Pyjo.Promise` object, which is fulfilled with the exit
302
+ code, ``None`` for a signal, and the whole output and error of the
303
+ process as :mod:`Pyjo.String.Bytes` objects once it closes, or
304
+ rejected if the program could not be run. ::
305
+
306
+ # The output of a program in a coroutine
307
+ code, out, err = await Pyjo.IOLoop.ChildProcess.new().run_p(['git', 'log', '-1'])
308
+ """
309
+ # Imported here, as Pyjo.Promise uses Pyjo.IOLoop
310
+ import Pyjo.Promise
311
+ import Pyjo.String.Bytes
312
+
313
+ p = Pyjo.Promise.new()
314
+ out: list[bytes] = []
315
+ err: list[bytes] = []
316
+
317
+ def close(child: Pyjo_IOLoop_ChildProcess, code: int | None, signal: int | None) -> None:
318
+ p.resolve(code, Pyjo.String.Bytes.new(b''.join(out)), Pyjo.String.Bytes.new(b''.join(err)))
319
+
320
+ def error(child: Pyjo_IOLoop_ChildProcess, e: BaseException) -> None:
321
+ p.reject(e)
322
+
323
+ def output(stream: Any, chunk: bytes) -> None:
324
+ out.append(chunk)
325
+
326
+ def errors(stream: Any, chunk: bytes) -> None:
327
+ err.append(chunk)
328
+
329
+ self.once('close', close)
330
+ self.once('error', error)
331
+ self.spawn(args, **options)
332
+ if (stdout := self._stdout) is not None and (stderr := self._stderr) is not None:
333
+ stdout.on('read', output)
334
+ stderr.on('read', errors)
335
+ if (stdin := self._stdin) is not None:
336
+ stdin.write(input).close_gracefully()
337
+ return p
338
+
339
+ def spawn(
340
+ self,
341
+ args: Sequence[str | bytes | os.PathLike[str]],
342
+ *,
343
+ cwd: str | bytes | os.PathLike[str] | None = None,
344
+ detached: bool = False,
345
+ env: Mapping[str, str] | Mapping[bytes, bytes] | None = None,
346
+ ) -> Self:
347
+ """::
348
+
349
+ child = child.spawn(['ls', '-l'])
350
+ child = child.spawn(['php-cgi', 'index.php'], cwd='/var/www', env={'REDIRECT_STATUS': '200'})
351
+ child = child.spawn(['make', 'all'], detached=True)
352
+
353
+ Run a program, the first of ``args``, with the others as its
354
+ arguments, in the directory ``cwd`` and with the environment ``env``,
355
+ the ones of this process by default. With ``detached``, the process
356
+ runs in a session of its own, as the leader of a process group, as
357
+ ``detached`` of Node.js, so that :meth:`kill` signals the processes
358
+ that it starts too, and a signal of the terminal of this process, as
359
+ of Ctrl-C, does not reach it. The events ``spawn`` or ``error`` are
360
+ emitted in the next tick of the reactor, so that the ones that are
361
+ subscribed after the call get them too, and the output and the error
362
+ are read from the event ``spawn`` on.
363
+ """
364
+ reactor = self.reactor
365
+ if os.name == 'nt':
366
+ return self._later('error', Pyjo.Exception.new('Child processes need pipes that the reactor watches'))
367
+ try:
368
+ self._process = process = subprocess.Popen(
369
+ args,
370
+ bufsize=0,
371
+ cwd=cwd,
372
+ env=env,
373
+ start_new_session=detached,
374
+ stdin=subprocess.PIPE,
375
+ stdout=subprocess.PIPE,
376
+ stderr=subprocess.PIPE,
377
+ )
378
+ except OSError as e:
379
+ return self._later('error', e)
380
+ self._detached = detached
381
+ _running[self] = os.getpid()
382
+ _register()
383
+ assert process.stdin is not None and process.stdout is not None and process.stderr is not None
384
+
385
+ # The output and the error, until their end, which the end of the process usually is, read from the event
386
+ # spawn on
387
+ for name, handle in (('_stdout', process.stdout), ('_stderr', process.stderr)):
388
+ os.set_blocking(handle.fileno(), False)
389
+ stream = _Output(handle)
390
+ stream.reactor = reactor
391
+ stream.timeout = 0
392
+ stream.on('close', self._closed)
393
+ setattr(self, name, stream)
394
+
395
+ # The input, which the reactor writes without reading
396
+ os.set_blocking(process.stdin.fileno(), False)
397
+ self._stdin = stdin = _Input(process.stdin)
398
+ stdin.reactor = reactor
399
+ stdin.timeout = 0
400
+ stdin.start()
401
+
402
+ self._wait(_DELAYS[-1])
403
+ ref = weakref.ref(self)
404
+
405
+ # The output and the error are read once the subscribers of the event spawn can read them, as a reactor may
406
+ # read before the next tick
407
+ def spawned(reactor: Any) -> None:
408
+ if (child := ref()) is None or child._process is None:
409
+ return
410
+ for stream in (child._stdout, child._stderr):
411
+ if isinstance(stream, _Output):
412
+ stream.begin()
413
+ child.emit('spawn')
414
+
415
+ reactor.next_tick(spawned)
416
+ return self
417
+
418
+ # The output or the error of the process has ended, so that it has likely exited
419
+ def _closed(self, stream: Any) -> None:
420
+ if (process := self._process) is None:
421
+ return
422
+ self._ended += 1
423
+ if self._ended < 2:
424
+ return
425
+ if process.returncode is not None:
426
+ return self._emit_close()
427
+ self._check = 0
428
+ self._wait(_DELAYS[0])
429
+
430
+ # The process of the parent of a fork, whose pipes the child closes, without the events of its end, as Popen.poll
431
+ # gives the exit code 0 for a process that is not a child of the process that calls it
432
+ def _leave(self) -> None:
433
+ # The poll of a process that is not a child gives it the exit code 0, so that Popen does not warn that it still
434
+ # runs when it is freed
435
+ if (process := self._process) is not None:
436
+ self._process = None
437
+ process.poll()
438
+ if (timer := self._timer) is not None:
439
+ self._timer = None
440
+ # The reactor of an event loop that is gone has no timer to remove, and the pipes are closed all the same
441
+ if self._reactor is not None and (reactor := self._reactor()) is not None:
442
+ reactor.remove(timer)
443
+ for stream in (self._stdin, self._stdout, self._stderr):
444
+ if stream is not None:
445
+ stream.close()
446
+
447
+ def _emit_close(self) -> None:
448
+ _running.pop(self, None)
449
+ self.emit('close', self._exit_code, self._signal_code)
450
+
451
+ def _later(self, name: str, *args: Any) -> Self:
452
+ ref = weakref.ref(self)
453
+
454
+ def emit(reactor: Any) -> None:
455
+ if (child := ref()) is not None:
456
+ child.emit(name, *args)
457
+
458
+ self.reactor.next_tick(emit)
459
+ return self
460
+
461
+ # The end of the process
462
+ def _poll(self, reactor: Any) -> None:
463
+ self._timer = None
464
+ if (process := self._process) is None:
465
+ return
466
+ if (code := process.poll()) is None:
467
+ self._check = min(self._check + 1, len(_DELAYS) - 1)
468
+ return self._wait(_DELAYS[self._check])
469
+
470
+ # The input that the process can read no more
471
+ if (stdin := self._stdin) is not None:
472
+ stdin.close()
473
+ if code < 0:
474
+ self._signal_code = -code
475
+ else:
476
+ self._exit_code = code
477
+ self.emit('exit', self._exit_code, self._signal_code)
478
+ if self._ended >= 2:
479
+ self._emit_close()
480
+
481
+ def _wait(self, after: float) -> None:
482
+ reactor = self.reactor
483
+ if self._timer is not None:
484
+ reactor.remove(self._timer)
485
+ self._timer = reactor.timer(after, self._poll)
486
+
487
+
488
+ # The output or the error of a process, which the reactor reads from the event spawn on, and which keeps whether it is
489
+ # paused until then
490
+ class _Output(Pyjo.IOLoop.Stream.object):
491
+ _begun = False
492
+
493
+ def begin(self) -> None:
494
+ self._begun = True
495
+ paused, self._paused = self._paused, False
496
+ super().start()
497
+ if paused:
498
+ super().stop()
499
+
500
+ def start(self) -> None:
501
+ if self._begun:
502
+ return super().start()
503
+ self._paused = False
504
+
505
+ def stop(self) -> None:
506
+ if self._begun:
507
+ return super().stop()
508
+ self._paused = True
509
+
510
+
511
+ # The standard input of a process, which the reactor writes and does not read, so that it closes when a write fails,
512
+ # where a stream finds an error when it reads
513
+ class _Input(Pyjo.IOLoop.Stream.object):
514
+ def start(self) -> None:
515
+ super().start()
516
+ self.stop()
517
+
518
+ # A hang-up or an error of the pipe, which a reactor of poll reports also when it watches for nothing, as the
519
+ # process has closed its end
520
+ def _read_handle(self) -> None:
521
+ self.close()
522
+
523
+ def _write(self) -> None:
524
+ handle = self._handle
525
+ if len(self._buffer):
526
+ try:
527
+ written = os.write(handle.fileno(), self._buffer)
528
+ except (BlockingIOError, InterruptedError):
529
+ return
530
+
531
+ # The process has closed its end
532
+ except OSError:
533
+ return self.close()
534
+ self._written += written
535
+ chunk = bytes(self._buffer[:written])
536
+ del self._buffer[:written]
537
+ self.emit('write', chunk)._again()
538
+
539
+ if not len(self._buffer):
540
+ self.emit('drain')
541
+ if self.is_writing:
542
+ return
543
+ if self._graceful:
544
+ return self.close()
545
+ if self._handle is not None:
546
+ self.reactor.watch(self._handle, False, False)
547
+
548
+
549
+ # The child of a fork leaves the processes of its parent, before its event loop reads their pipes. The handler is
550
+ # registered when a process is spawned, after the one of the reactor, which os.register_at_fork calls first, so that the
551
+ # reactor has moved to a loop of its own, as a loop of epoll shares its state with the parent
552
+ _registered = False
553
+
554
+
555
+ def _register() -> None:
556
+ global _registered
557
+ if not _registered and hasattr(os, 'register_at_fork'):
558
+ _registered = True
559
+ os.register_at_fork(after_in_child=_after_fork_in_child)
560
+
561
+
562
+ def _after_fork_in_child() -> None:
563
+ pid = os.getpid()
564
+ for child, owner in list(_running.items()):
565
+ if owner != pid:
566
+ del _running[child]
567
+ try:
568
+ child._leave() # pyright: ignore[reportPrivateUsage]
569
+ except Exception:
570
+ pass
571
+
572
+
573
+ # The processes that are left, killed and reaped, of this process, not of the one that forked it
574
+ @atexit.register
575
+ def _kill() -> None:
576
+ pid = os.getpid()
577
+ for child, owner in list(_running.items()):
578
+ if owner != pid or (process := child._process) is None: # pyright: ignore[reportPrivateUsage]
579
+ continue
580
+ if process.poll() is None:
581
+ child.kill(signals.SIGKILL)
582
+ process.wait()
583
+ for stream in (child.stdin, child.stdout, child.stderr):
584
+ if stream is not None:
585
+ stream.close()
586
+ _running.clear()
587
+
588
+
589
+ new = Pyjo_IOLoop_ChildProcess.new
590
+ object = Pyjo_IOLoop_ChildProcess
591
+ ChildProcess = Pyjo_IOLoop_ChildProcess