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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- 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
|