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/GetOpt.py ADDED
@@ -0,0 +1,605 @@
1
+ """
2
+ Pyjo.GetOpt - Command line options
3
+ ==================================
4
+ ::
5
+
6
+ from Pyjo.GetOpt import GetOpt
7
+
8
+ getopt = GetOpt()
9
+
10
+ # The value goes to the result
11
+ getopt.option('m|mode=s')
12
+
13
+ opts = getopt.parse(['-m', 'production', 'myapp.py'])
14
+ if not opts:
15
+ raise SystemExit('Usage: ...')
16
+ print(opts.mode)
17
+
18
+ # Or import the module, as in Google Python Style Guide
19
+
20
+ import Pyjo.GetOpt
21
+
22
+ getopt = Pyjo.GetOpt.new()
23
+
24
+ # The value goes to the result
25
+ getopt.option('m|mode=s')
26
+
27
+ # The values go to a list
28
+ listen = []
29
+ getopt.option('l|listen=s', listen)
30
+
31
+ # A callback gets the value
32
+ @getopt.on('i|inactivity-timeout=i')
33
+ def timeout(name, value):
34
+ print(f'{name}: {value}')
35
+
36
+ opts = getopt.parse(['-m', 'production', '-l', 'http://*:8080', 'myapp.py'])
37
+ if not opts:
38
+ raise SystemExit('Usage: ...')
39
+ print(opts.mode)
40
+
41
+ :mod:`Pyjo.GetOpt` extracts options from a list of command line arguments,
42
+ the way `Getopt::Long <https://metacpan.org/pod/Getopt::Long>`_ of Perl does
43
+ with the configuration that :func:`Pyjo.Util.getopt` gives it, as the
44
+ commands of Mojolicious do. It is the engine of :func:`Pyjo.Util.getopt`,
45
+ with decorators for the callbacks of the options.
46
+
47
+ An option is ``-name`` or ``--name``, with its value after it or after ``=``,
48
+ as ``--mode production``, ``--mode=production`` or ``-m production``. The
49
+ names are case sensitive and are not abbreviated, and the options of a
50
+ single letter are not bundled. The arguments that are not options stay in the
51
+ list, in their order, and ``--`` ends the options.
52
+
53
+ An unknown option, an option without its value and a value of a wrong type are
54
+ errors: their messages go to ``sys.stderr``, as warnings, the parsing goes on,
55
+ and the result is false.
56
+
57
+ Options
58
+ -------
59
+
60
+ The specification of an option is its names, separated by ``|``, and what it
61
+ takes. ::
62
+
63
+ # A flag, True
64
+ getopt.option('v|verbose')
65
+
66
+ # A flag that --no-color and --nocolor negate, False
67
+ getopt.option('color!')
68
+
69
+ # A counter, which each -v increments
70
+ getopt.option('v|verbose+')
71
+
72
+ # A string, an integer or a float
73
+ getopt.option('m|mode=s')
74
+ getopt.option('b|backlog=i')
75
+ getopt.option('r|ratio=f')
76
+
77
+ # An optional value, which is "" or 0 without it
78
+ getopt.option('p|proxy:s')
79
+
80
+ # A list of values in the result
81
+ getopt.option('H|header=s@')
82
+
83
+ # A dict of the keys and the values of --define key=value in the result
84
+ getopt.option('D|define=s%')
85
+
86
+ Next to what Getopt::Long has, an option can have a default, a type, which
87
+ converts its value, and the values that it accepts, as the arguments of
88
+ argparse. ::
89
+
90
+ getopt.option('M|method=s', default='GET')
91
+ getopt.option('home=s', type=Pyjo.File.path)
92
+ getopt.option('m|mode=s', choices=['development', 'production'])
93
+
94
+ The name of an option in the result, and the name that a callback gets, is
95
+ its first name that is longer than one letter, or its only name, with ``_``
96
+ for ``-``: ``inactivity_timeout`` for ``i|inactivity-timeout=i``.
97
+
98
+ Environment
99
+ -----------
100
+
101
+ With ``env_prefix``, an option that is not given has the value of the
102
+ environment variable of the prefix and its name in upper case, as
103
+ ``PYJO_INACTIVITY_TIMEOUT`` for ``i|inactivity-timeout=i``, which is
104
+ converted and checked as a value of the command line, before the default,
105
+ the way Cobra and Viper of Go have it. An option of one letter has none, and
106
+ an empty variable is not set. A flag is false as ``0``, ``false``, ``no`` or
107
+ ``off``, a counter is the number, and an option of a list or a dict takes
108
+ values separated by commas, as ``PYJO_LISTEN=http://*:80,https://*:443``. ::
109
+
110
+ # PYJO_WORKERS=12 python myapp.py prefork
111
+ getopt = Pyjo.GetOpt.new(env_prefix='PYJO_').option('w|workers=i', default=4)
112
+
113
+ The other way around, the target :func:`env` sets an environment variable to
114
+ the value of an option, as a script of Mojolicious binds an option to
115
+ ``$ENV{...}``, for the code that reads the variable later, as the
116
+ application its mode. ::
117
+
118
+ # python myapp.py --mode production sets PYJO_MODE
119
+ getopt = Pyjo.GetOpt.new().option('m|mode=s', Pyjo.GetOpt.env('PYJO_MODE'))
120
+
121
+ Usage
122
+ -----
123
+
124
+ The usage of a command, which :func:`Pyjo.Util.extract_usage` reads from its
125
+ documentation, lists its options in a block after a line ``Options:``, an
126
+ option with a short name as ``-h, --help`` or ``-V``, and one with a long name
127
+ only as `` --home <path>``, aligned with the long names of the others, and
128
+ its description after them, in a column of its own. :func:`format_usage` lays
129
+ out these blocks, so that a new option or a longer one needs no alignment by
130
+ hand. ::
131
+
132
+ Options:
133
+ -h, --help Show this summary of available options
134
+ --home <path> Path to home directory of your application, defaults to
135
+ the value of PYJO_HOME or auto-detection
136
+
137
+ See also
138
+ --------
139
+
140
+ :func:`Pyjo.Util.getopt`, https://mojolicious.org.
141
+
142
+ Inspired by `Getopt::Long::Parser <https://metacpan.org/pod/Getopt::Long>`_,
143
+ the object interface of Getopt::Long, which ``getopt`` of Mojo::Util
144
+ configures.
145
+
146
+ Classes
147
+ -------
148
+ """
149
+
150
+ import Pyjo.Base
151
+ import Pyjo.Exception
152
+
153
+ from Pyjo.Util import warn
154
+
155
+ import os
156
+ import re
157
+ import sys
158
+ import textwrap
159
+
160
+ from collections.abc import Callable, Sequence
161
+ from typing import Any, Self, cast, overload
162
+
163
+
164
+ type Callback = Callable[[str, Any], Any]
165
+ type Target = list[Any] | dict[Any, Any] | Callback | None
166
+
167
+ _RE_FLOAT = re.compile(r'[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?')
168
+ _RE_INT = re.compile(r'[-+]?\d+')
169
+
170
+ # The default width of the lines of a usage, as the usages of Mojolicious have it
171
+ WIDTH = 77
172
+
173
+ # A line of options and a line of an option of a usage, with its label and its description
174
+ _RE_USAGE_OPTIONS = re.compile(r'^Options\b[^:]*:$')
175
+ _RE_USAGE_OPTION = re.compile(r'^ ((?:-\w(?:, --[\w-]+)?| --[\w-]+)(?: \[?<[^>]+>\]?)?)(?: {2,}(\S.*))?$')
176
+
177
+
178
+ class Pyjo_GetOpt(Pyjo.Base.object):
179
+ """
180
+ :mod:`Pyjo.GetOpt` inherits all methods from :mod:`Pyjo.Base` and
181
+ implements the following new ones.
182
+ """
183
+
184
+ def __init__(
185
+ self, *, pass_through: bool = False, require_order: bool = False, env_prefix: str | None = None
186
+ ) -> None:
187
+ """::
188
+
189
+ getopt = Pyjo.GetOpt.new()
190
+ getopt = Pyjo.GetOpt.new(pass_through=True)
191
+ getopt = Pyjo.GetOpt.new(require_order=True)
192
+ getopt = Pyjo.GetOpt.new(env_prefix='PYJO_')
193
+
194
+ Construct a new :mod:`Pyjo.GetOpt` object. With ``pass_through``,
195
+ unknown options and ``--`` stay in the list of arguments, without a
196
+ warning. With ``require_order``, the options end at the first argument
197
+ that is not one, and what follows it stays in the list of arguments,
198
+ options too, as the arguments of a command that the program runs. With
199
+ ``env_prefix``, an option that is not given has the value of the
200
+ environment variable of its name, see `Environment`_.
201
+ """
202
+ self.env_prefix = env_prefix
203
+ self.pass_through = pass_through
204
+ self.require_order = require_order
205
+ self._names: dict[str, _Option] = {}
206
+ self._negations: set[str] = set()
207
+ self._options: list[_Option] = []
208
+
209
+ @overload
210
+ def on[F: Callback](
211
+ self,
212
+ spec: str,
213
+ cb: None = None,
214
+ *,
215
+ type: Callable[[Any], Any] | None = None,
216
+ choices: Sequence[Any] | None = None,
217
+ ) -> Callable[[F], F]: ...
218
+
219
+ @overload
220
+ def on[F: Callback](
221
+ self, spec: str, cb: F, *, type: Callable[[Any], Any] | None = None, choices: Sequence[Any] | None = None
222
+ ) -> F: ...
223
+
224
+ def on(
225
+ self,
226
+ spec: str,
227
+ cb: Callback | None = None,
228
+ *,
229
+ type: Callable[[Any], Any] | None = None,
230
+ choices: Sequence[Any] | None = None,
231
+ ) -> Any:
232
+ """::
233
+
234
+ cb = getopt.on('b|backlog=i', lambda name, value: ...)
235
+
236
+ @getopt.on('b|backlog=i')
237
+ def backlog(name, value): ...
238
+
239
+ Add an option whose callback gets its name and its value, or a tuple
240
+ of the key and the value of a hash, as a decorator without the
241
+ callback. ``type`` and ``choices`` are those of :meth:`option`.
242
+ """
243
+ if cb is None:
244
+
245
+ def decorator(cb: Callback) -> Callback:
246
+ return self.on(spec, cb, type=type, choices=choices)
247
+
248
+ return decorator
249
+ self.option(spec, cb, type=type, choices=choices)
250
+ return cb
251
+
252
+ def option(
253
+ self,
254
+ spec: str,
255
+ target: Target = None,
256
+ *,
257
+ default: Any = None,
258
+ type: Callable[[Any], Any] | None = None,
259
+ choices: Sequence[Any] | None = None,
260
+ ) -> Self:
261
+ """::
262
+
263
+ getopt = getopt.option('m|mode=s')
264
+ getopt = getopt.option('l|listen=s', [])
265
+ getopt = getopt.option('D|define=s%', {})
266
+ getopt = getopt.option('b|backlog=i', lambda name, value: ...)
267
+ getopt = getopt.option('M|method=s', default='GET')
268
+ getopt = getopt.option('home=s', type=Pyjo.File.path)
269
+ getopt = getopt.option('m|mode=s', choices=['development', 'production'])
270
+
271
+ Add an option, whose value goes to the result, to a list, to a dict or
272
+ to a callback. ``default`` is the value in the result of an option
273
+ that is not given. ``type`` converts a value, after the type of the
274
+ specification, and a :class:`ValueError`, a :class:`TypeError` or a
275
+ :class:`LookupError` of it makes the value invalid, as a value that is
276
+ not in ``choices``.
277
+ """
278
+ if not (m := re.fullmatch(r'([\w?-]+(?:\|[\w?-]+)*)(?:(!)|(\+)|([=:])([sif])([@%])?)?', spec)):
279
+ raise Pyjo.Exception.new(f'Error in option spec: "{spec}"')
280
+ names = m[1].split('|')
281
+ name = next((name for name in names if len(name) > 1), names[0]).replace('-', '_')
282
+ kind = 'negatable' if m[2] else 'increment' if m[3] else 'value' if m[4] else 'flag'
283
+ option = _Option(name, kind, m[5] or '', m[4] == ':', m[6] or '', target, default, type, choices)
284
+ self._options.append(option)
285
+ for name in names:
286
+ self._names[name] = option
287
+ if kind == 'negatable':
288
+ self._names[f'no{name}'] = self._names[f'no-{name}'] = option
289
+ self._negations.update((f'no{name}', f'no-{name}'))
290
+ return self
291
+
292
+ def parse(self, args: list[str] | None = None) -> 'Pyjo_GetOpt_Result':
293
+ """::
294
+
295
+ opts = getopt.parse()
296
+ opts = getopt.parse(['--mode', 'production', 'myapp.py'])
297
+
298
+ Extract the options from the list of arguments, which defaults to the
299
+ ones of ``sys.argv``, and remove them from it. The result has the
300
+ values of the options without a target, and is false after an error.
301
+ """
302
+ argv = sys.argv if args is None else args
303
+ start = 1 if args is None else 0
304
+ rest, keep = argv[start:], list[str]()
305
+ values: dict[str, Any] = {
306
+ o.name: [] if o.multi == '@' else {} if o.multi == '%' else None for o in self._options if o.target is None
307
+ }
308
+ seen: set[str] = set()
309
+ success = True
310
+
311
+ i = 0
312
+ while i < len(rest):
313
+ arg = rest[i]
314
+ i += 1
315
+
316
+ # The end of the options
317
+ if arg == '--':
318
+ keep.extend(rest[i - 1 :] if self.pass_through else rest[i:])
319
+ break
320
+
321
+ # An argument, after which the options end with require_order
322
+ if not arg.startswith('-') or arg == '-':
323
+ if self.require_order:
324
+ keep.extend(rest[i - 1 :])
325
+ break
326
+ keep.append(arg)
327
+ continue
328
+
329
+ # An unknown option
330
+ given, _, value = arg.lstrip('-').partition('=')
331
+ value = value if '=' in arg else None
332
+ if (option := self._names.get(given)) is None:
333
+ if self.pass_through:
334
+ keep.append(arg)
335
+ else:
336
+ warn(f'Unknown option: {given}')
337
+ success = False
338
+ continue
339
+
340
+ # A flag
341
+ if option.kind != 'value':
342
+ if value is not None:
343
+ warn(f'Option {given} does not take an argument')
344
+ success = False
345
+ continue
346
+ seen.add(option.name)
347
+ if option.kind == 'increment':
348
+ _store(option, values, (values.get(option.name) or 0) + 1)
349
+ else:
350
+ _store(option, values, given not in self._negations)
351
+ continue
352
+
353
+ # A value after the option, which an optional one takes only when it is not an option
354
+ if value is None and i < len(rest) and (not option.optional or _is_value(option.type, rest[i])):
355
+ value = rest[i]
356
+ i += 1
357
+ if value is None:
358
+ if not option.optional:
359
+ warn(f'Option {given} requires an argument')
360
+ success = False
361
+ continue
362
+ value = ''
363
+
364
+ # The key of a hash
365
+ key = None
366
+ if option.multi == '%':
367
+ key, equals, value = value.partition('=')
368
+ if not equals and not option.optional:
369
+ warn(f'Option {given}, key "{key}", requires a value')
370
+ success = False
371
+ continue
372
+
373
+ # The type of the value
374
+ if (converted := self._convert(option, given, value)) is _INVALID:
375
+ success = False
376
+ continue
377
+ seen.add(option.name)
378
+ _store(option, values, converted if key is None else (key, converted))
379
+
380
+ # Not in Getopt::Long: the options that are not given, from the environment variables of their names
381
+ if self.env_prefix is not None:
382
+ for option in self._options:
383
+ if option.name in seen or len(option.name) < 2:
384
+ continue
385
+ name = f'{self.env_prefix}{option.name.upper()}'
386
+ if not (value := os.environ.get(name)):
387
+ continue
388
+ if self._environ(option, name, value, values):
389
+ seen.add(option.name)
390
+ else:
391
+ success = False
392
+
393
+ # The defaults of the options that are not given
394
+ for option in self._options:
395
+ if option.target is None and option.default is not None and option.name not in seen:
396
+ default = option.default
397
+ values[option.name] = (
398
+ list(default) if option.multi == '@' else dict(default) if option.multi == '%' else default
399
+ )
400
+
401
+ argv[start:] = keep
402
+ return Pyjo_GetOpt_Result(values, success)
403
+
404
+ def _environ(self, option: '_Option', name: str, value: str, values: dict[str, Any]) -> bool:
405
+ # A flag, which is false as 0, false, no and off
406
+ if option.kind in ('flag', 'negatable'):
407
+ _store(option, values, value.lower() not in ('0', 'false', 'no', 'off'))
408
+ return True
409
+
410
+ # A counter, which is the number
411
+ if option.kind == 'increment':
412
+ if not _RE_INT.fullmatch(value):
413
+ warn(f'Value "{value}" invalid for option {name} (integer number expected)')
414
+ return False
415
+ _store(option, values, int(value))
416
+ return True
417
+
418
+ # The values of a list or a dict, separated by commas, as PYJO_LISTEN has them
419
+ target: Any = option.target
420
+ mapping = option.multi == '%' or isinstance(target, dict)
421
+ several = mapping or option.multi == '@' or isinstance(target, list)
422
+ converted: list[Any] = []
423
+ for item in value.split(',') if several else [value]:
424
+ key = None
425
+ if mapping:
426
+ key, equals, item = item.partition('=')
427
+ if not equals and not option.optional:
428
+ warn(f'Option {name}, key "{key}", requires a value')
429
+ return False
430
+ if (result := self._convert(option, name, item)) is _INVALID:
431
+ return False
432
+ converted.append(result if key is None else (key, result))
433
+ for result in converted:
434
+ _store(option, values, result)
435
+ return True
436
+
437
+ def _convert(self, option: '_Option', given: str, value: str) -> Any:
438
+ if option.optional and value == '' and option.type != 's':
439
+ value = '0'
440
+ if option.type == 'i' and not _RE_INT.fullmatch(value):
441
+ warn(f'Value "{value}" invalid for option {given} (integer number expected)')
442
+ return _INVALID
443
+ if option.type == 'f' and not _RE_FLOAT.fullmatch(value):
444
+ warn(f'Value "{value}" invalid for option {given} (real number expected)')
445
+ return _INVALID
446
+ converted: Any = int(value) if option.type == 'i' else float(value) if option.type == 'f' else value
447
+
448
+ # Not in Getopt::Long: the type and the choices of argparse
449
+ if option.convert is not None:
450
+ try:
451
+ converted = option.convert(converted)
452
+ except (LookupError, TypeError, ValueError) as e:
453
+ warn(f'Value "{value}" invalid for option {given} ({str(e).strip(chr(39))})')
454
+ return _INVALID
455
+ if option.choices is not None and converted not in option.choices:
456
+ warn(f'Value "{value}" invalid for option {given} (choose from {", ".join(map(str, option.choices))})')
457
+ return _INVALID
458
+ return converted
459
+
460
+
461
+ class Pyjo_GetOpt_Result:
462
+ """
463
+ The result of :meth:`~Pyjo_GetOpt.parse`, the values of the options as
464
+ attributes, which is false after an error.
465
+ """
466
+
467
+ def __init__(self, values: dict[str, Any], success: bool) -> None:
468
+ self.__dict__.update(values)
469
+ self._success = success
470
+
471
+ def __bool__(self) -> bool:
472
+ return self._success
473
+
474
+ # The values are attributes that the specifications of the options name
475
+ def __getattr__(self, name: str) -> Any:
476
+ raise AttributeError(f'{type(self).__name__!r} object has no attribute {name!r}', name=name, obj=self)
477
+
478
+ def __repr__(self) -> str:
479
+ values = ', '.join(f'{name}={value!r}' for name, value in vars(self).items() if not name.startswith('_'))
480
+ return f'{type(self).__name__}({values})'
481
+
482
+
483
+ class _Option:
484
+ def __init__(
485
+ self,
486
+ name: str,
487
+ kind: str,
488
+ type: str,
489
+ optional: bool,
490
+ multi: str,
491
+ target: Target,
492
+ default: Any,
493
+ convert: Callable[[Any], Any] | None,
494
+ choices: Sequence[Any] | None,
495
+ ) -> None:
496
+ self.choices = choices
497
+ self.convert = convert
498
+ self.default = default
499
+ self.kind = kind
500
+ self.multi = multi
501
+ self.name = name
502
+ self.optional = optional
503
+ self.target = target
504
+ self.type = type
505
+
506
+
507
+ # A value that is not valid, as None is one
508
+ _INVALID = Pyjo.Base.new()
509
+
510
+
511
+ def _is_value(type: str, arg: str) -> bool:
512
+ if type == 's':
513
+ return not arg.startswith('-') or arg == '-'
514
+ return (_RE_FLOAT if type == 'f' else _RE_INT).fullmatch(arg) is not None
515
+
516
+
517
+ def _store(option: _Option, values: dict[str, Any], value: Any) -> None:
518
+ target = option.target
519
+ if isinstance(target, list):
520
+ cast(list[Any], target).append(value)
521
+ elif isinstance(target, dict):
522
+ key, value = cast(tuple[Any, Any], value)
523
+ cast(dict[Any, Any], target)[key] = value
524
+ elif target is not None:
525
+ target(option.name, value)
526
+ elif option.multi == '@':
527
+ values[option.name].append(value)
528
+ elif option.multi == '%':
529
+ key, value = cast(tuple[Any, Any], value)
530
+ values[option.name][key] = value
531
+ else:
532
+ values[option.name] = value
533
+
534
+
535
+ # Not in Getopt::Long: a target for an environment variable, as Perl binds an option to $ENV{...}
536
+ def env(name: str) -> Callable[[str, Any], None]:
537
+ """::
538
+
539
+ target = Pyjo.GetOpt.env('PYJO_MODE')
540
+
541
+ A target of an option that sets the environment variable ``name`` to its
542
+ value, ``1`` for a flag and ``0`` for a negated one, see `Environment`_. ::
543
+
544
+ # --foreground sets PYJO_HOT_FOREGROUND to 1
545
+ getopt.option('f|foreground', Pyjo.GetOpt.env('PYJO_HOT_FOREGROUND'))
546
+ """
547
+
548
+ def target(option: str, value: Any) -> None:
549
+ os.environ[name] = '1' if value is True else '0' if value is False else str(value)
550
+
551
+ return target
552
+
553
+
554
+ # Not in Getopt::Long: the layout of the options of a usage, which Pod::Usage leaves to the author of the POD
555
+ def format_usage(usage: str, width: int = WIDTH) -> str:
556
+ """::
557
+
558
+ usage = Pyjo.GetOpt.format_usage(usage)
559
+ usage = Pyjo.GetOpt.format_usage(usage, width=79)
560
+
561
+ Lay out the blocks of options of a usage message, see `Usage`_: the
562
+ descriptions in a column three spaces after the longest option of the
563
+ block, wrapped to the width of the lines, ``77`` by default, and the rest
564
+ of the message as it is. ::
565
+
566
+ # The usage of a command, laid out
567
+ print(Pyjo.GetOpt.format_usage(extract_usage()))
568
+ """
569
+ lines = usage.split('\n')
570
+ result: list[str] = []
571
+ block: list[list[str]] = []
572
+
573
+ def flush() -> None:
574
+ if not block:
575
+ return
576
+ labels = [entry[0] for entry in block]
577
+ column = 2 + max(len(label) for label in labels) + 3
578
+ for label, *description in block:
579
+ wrapped = textwrap.wrap(
580
+ ' '.join(description), max(width - column, 1), break_long_words=False, break_on_hyphens=False
581
+ )
582
+ result.append(f' {label:<{column - 2}}{wrapped[0]}'.rstrip() if wrapped else f' {label}')
583
+ result.extend(' ' * column + line for line in wrapped[1:])
584
+ block.clear()
585
+
586
+ in_block = False
587
+ for line in lines:
588
+ if in_block and line.strip():
589
+ if m := _RE_USAGE_OPTION.match(line):
590
+ block.append([m[1].rstrip(), *([m[2]] if m[2] else [])])
591
+ continue
592
+ if block and line.startswith(' '):
593
+ block[-1].append(line.strip())
594
+ continue
595
+ if in_block:
596
+ flush()
597
+ in_block = _RE_USAGE_OPTIONS.match(line) is not None
598
+ result.append(line)
599
+ flush()
600
+ return '\n'.join(result)
601
+
602
+
603
+ new = Pyjo_GetOpt.new
604
+ object = Pyjo_GetOpt
605
+ GetOpt = Pyjo_GetOpt