ominfra 0.0.0.dev191__py3-none-any.whl → 0.0.0.dev193__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.
- ominfra/configs.py +1 -28
- ominfra/manage/deploy/apps.py +10 -0
- ominfra/manage/deploy/conf/manager.py +10 -4
- ominfra/manage/deploy/conf/specs.py +2 -2
- ominfra/manage/deploy/deploy.py +12 -5
- ominfra/manage/deploy/git.py +3 -1
- ominfra/manage/deploy/specs.py +2 -0
- ominfra/manage/deploy/systemd.py +2 -1
- ominfra/scripts/journald2aws.py +2 -27
- ominfra/scripts/manage.py +1495 -1448
- ominfra/scripts/supervisor.py +1056 -1072
- ominfra/supervisor/configs.py +6 -0
- ominfra/supervisor/http.py +1 -1
- ominfra/supervisor/inject.py +11 -8
- {ominfra-0.0.0.dev191.dist-info → ominfra-0.0.0.dev193.dist-info}/METADATA +4 -4
- {ominfra-0.0.0.dev191.dist-info → ominfra-0.0.0.dev193.dist-info}/RECORD +20 -20
- {ominfra-0.0.0.dev191.dist-info → ominfra-0.0.0.dev193.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev191.dist-info → ominfra-0.0.0.dev193.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev191.dist-info → ominfra-0.0.0.dev193.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev191.dist-info → ominfra-0.0.0.dev193.dist-info}/top_level.txt +0 -0
ominfra/scripts/supervisor.py
CHANGED
@@ -92,15 +92,15 @@ if sys.version_info < (3, 8):
|
|
92
92
|
########################################
|
93
93
|
|
94
94
|
|
95
|
-
# ../../omdev/toml/parser.py
|
96
|
-
TomlParseFloat = ta.Callable[[str], ta.Any]
|
97
|
-
TomlKey = ta.Tuple[str, ...]
|
98
|
-
TomlPos = int # ta.TypeAlias
|
99
|
-
|
100
95
|
# utils/collections.py
|
101
96
|
K = ta.TypeVar('K')
|
102
97
|
V = ta.TypeVar('V')
|
103
98
|
|
99
|
+
# ../../omlish/formats/toml/parser.py
|
100
|
+
TomlParseFloat = ta.Callable[[str], ta.Any]
|
101
|
+
TomlKey = ta.Tuple[str, ...]
|
102
|
+
TomlPos = int # ta.TypeAlias
|
103
|
+
|
104
104
|
# ../../omlish/lite/cached.py
|
105
105
|
T = ta.TypeVar('T')
|
106
106
|
CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
|
@@ -143,7 +143,6 @@ SocketHandlerFactory = ta.Callable[[SocketAddress, ta.BinaryIO, ta.BinaryIO], 'S
|
|
143
143
|
|
144
144
|
# ../configs.py
|
145
145
|
ConfigMapping = ta.Mapping[str, ta.Any]
|
146
|
-
IniConfigSectionSettingsMap = ta.Mapping[str, ta.Mapping[str, ta.Union[str, ta.Sequence[str]]]] # ta.TypeAlias
|
147
146
|
|
148
147
|
# ../../omlish/http/handlers.py
|
149
148
|
HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse'] # ta.TypeAlias
|
@@ -153,1291 +152,1291 @@ CoroHttpServerFactory = ta.Callable[[SocketAddress], 'CoroHttpServer']
|
|
153
152
|
|
154
153
|
|
155
154
|
########################################
|
156
|
-
#
|
157
|
-
# SPDX-License-Identifier: MIT
|
158
|
-
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
159
|
-
# Licensed to PSF under a Contributor Agreement.
|
160
|
-
#
|
161
|
-
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
162
|
-
# --------------------------------------------
|
163
|
-
#
|
164
|
-
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
165
|
-
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
166
|
-
# documentation.
|
167
|
-
#
|
168
|
-
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
169
|
-
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
170
|
-
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
171
|
-
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
172
|
-
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
|
173
|
-
# Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
|
174
|
-
#
|
175
|
-
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
176
|
-
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
177
|
-
# any such work a brief summary of the changes made to Python.
|
178
|
-
#
|
179
|
-
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
180
|
-
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
181
|
-
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
182
|
-
# RIGHTS.
|
183
|
-
#
|
184
|
-
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
185
|
-
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
186
|
-
# ADVISED OF THE POSSIBILITY THEREOF.
|
187
|
-
#
|
188
|
-
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
189
|
-
#
|
190
|
-
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
191
|
-
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
192
|
-
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
193
|
-
#
|
194
|
-
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
195
|
-
# License Agreement.
|
196
|
-
#
|
197
|
-
# https://github.com/python/cpython/blob/9ce90206b7a4649600218cf0bd4826db79c9a312/Lib/tomllib/_parser.py
|
155
|
+
# ../exceptions.py
|
198
156
|
|
199
157
|
|
200
|
-
|
158
|
+
class ProcessError(Exception):
|
159
|
+
"""Specialized exceptions used when attempting to start a process."""
|
201
160
|
|
202
161
|
|
203
|
-
|
162
|
+
class BadCommandError(ProcessError):
|
163
|
+
"""Indicates the command could not be parsed properly."""
|
204
164
|
|
205
|
-
TOML_RE_NUMBER = re.compile(
|
206
|
-
r"""
|
207
|
-
0
|
208
|
-
(?:
|
209
|
-
x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
|
210
|
-
|
|
211
|
-
b[01](?:_?[01])* # bin
|
212
|
-
|
|
213
|
-
o[0-7](?:_?[0-7])* # oct
|
214
|
-
)
|
215
|
-
|
|
216
|
-
[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
|
217
|
-
(?P<floatpart>
|
218
|
-
(?:\.[0-9](?:_?[0-9])*)? # optional fractional part
|
219
|
-
(?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
|
220
|
-
)
|
221
|
-
""",
|
222
|
-
flags=re.VERBOSE,
|
223
|
-
)
|
224
|
-
TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
|
225
|
-
TOML_RE_DATETIME = re.compile(
|
226
|
-
rf"""
|
227
|
-
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
|
228
|
-
(?:
|
229
|
-
[Tt ]
|
230
|
-
{_TOML_TIME_RE_STR}
|
231
|
-
(?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
|
232
|
-
)?
|
233
|
-
""",
|
234
|
-
flags=re.VERBOSE,
|
235
|
-
)
|
236
165
|
|
166
|
+
class NotExecutableError(ProcessError):
|
167
|
+
"""
|
168
|
+
Indicates that the filespec cannot be executed because its path resolves to a file which is not executable, or which
|
169
|
+
is a directory.
|
170
|
+
"""
|
237
171
|
|
238
|
-
def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
|
239
|
-
"""Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
|
240
172
|
|
241
|
-
|
242
|
-
"""
|
243
|
-
(
|
244
|
-
year_str,
|
245
|
-
month_str,
|
246
|
-
day_str,
|
247
|
-
hour_str,
|
248
|
-
minute_str,
|
249
|
-
sec_str,
|
250
|
-
micros_str,
|
251
|
-
zulu_time,
|
252
|
-
offset_sign_str,
|
253
|
-
offset_hour_str,
|
254
|
-
offset_minute_str,
|
255
|
-
) = match.groups()
|
256
|
-
year, month, day = int(year_str), int(month_str), int(day_str)
|
257
|
-
if hour_str is None:
|
258
|
-
return datetime.date(year, month, day)
|
259
|
-
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
|
260
|
-
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
261
|
-
if offset_sign_str:
|
262
|
-
tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
|
263
|
-
offset_hour_str, offset_minute_str, offset_sign_str,
|
264
|
-
)
|
265
|
-
elif zulu_time:
|
266
|
-
tz = datetime.UTC
|
267
|
-
else: # local date-time
|
268
|
-
tz = None
|
269
|
-
return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
|
173
|
+
class NotFoundError(ProcessError):
|
174
|
+
"""Indicates that the filespec cannot be executed because it could not be found."""
|
270
175
|
|
271
176
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
hours=sign * int(hour_str),
|
278
|
-
minutes=sign * int(minute_str),
|
279
|
-
),
|
280
|
-
)
|
177
|
+
class NoPermissionError(ProcessError):
|
178
|
+
"""
|
179
|
+
Indicates that the file cannot be executed because the supervisor process does not possess the appropriate UNIX
|
180
|
+
filesystem permission to execute the file.
|
181
|
+
"""
|
281
182
|
|
282
183
|
|
283
|
-
|
284
|
-
|
285
|
-
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
286
|
-
return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
|
184
|
+
########################################
|
185
|
+
# ../privileges.py
|
287
186
|
|
288
187
|
|
289
|
-
def
|
290
|
-
|
291
|
-
|
292
|
-
|
188
|
+
def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
|
189
|
+
"""
|
190
|
+
Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup and
|
191
|
+
when spawning subprocesses. Returns None on success or a string error message if privileges could not be dropped.
|
192
|
+
"""
|
293
193
|
|
194
|
+
if user is None:
|
195
|
+
return 'No user specified to setuid to!'
|
294
196
|
|
295
|
-
|
197
|
+
# get uid for user, which can be a number or username
|
198
|
+
try:
|
199
|
+
uid = int(user)
|
200
|
+
except ValueError:
|
201
|
+
try:
|
202
|
+
pwrec = pwd.getpwnam(user) # type: ignore
|
203
|
+
except KeyError:
|
204
|
+
return f"Can't find username {user!r}"
|
205
|
+
uid = pwrec[2]
|
206
|
+
else:
|
207
|
+
try:
|
208
|
+
pwrec = pwd.getpwuid(uid)
|
209
|
+
except KeyError:
|
210
|
+
return f"Can't find uid {uid!r}"
|
296
211
|
|
297
|
-
|
298
|
-
# functions.
|
299
|
-
TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
|
300
|
-
TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
|
212
|
+
current_uid = os.getuid()
|
301
213
|
|
302
|
-
|
303
|
-
|
214
|
+
if current_uid == uid:
|
215
|
+
# do nothing and return successfully if the uid is already the current one. this allows a supervisord running as
|
216
|
+
# an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in it.
|
217
|
+
return None
|
304
218
|
|
305
|
-
|
219
|
+
if current_uid != 0:
|
220
|
+
return "Can't drop privilege as nonroot user"
|
306
221
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
|
222
|
+
gid = pwrec[3]
|
223
|
+
if hasattr(os, 'setgroups'):
|
224
|
+
user = pwrec[0]
|
225
|
+
groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
|
312
226
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
'\\\\': '\u005C', # backslash
|
322
|
-
},
|
323
|
-
)
|
227
|
+
# always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
|
228
|
+
# group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
|
229
|
+
# python 2.7 - this will be safe though for all unix /python version combos)
|
230
|
+
groups.insert(0, gid)
|
231
|
+
try:
|
232
|
+
os.setgroups(groups)
|
233
|
+
except OSError:
|
234
|
+
return 'Could not set groups of effective user'
|
324
235
|
|
236
|
+
try:
|
237
|
+
os.setgid(gid)
|
238
|
+
except OSError:
|
239
|
+
return 'Could not set group id of effective user'
|
325
240
|
|
326
|
-
|
327
|
-
"""An error raised if a document is not valid TOML."""
|
241
|
+
os.setuid(uid)
|
328
242
|
|
243
|
+
return None
|
329
244
|
|
330
|
-
def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
|
331
|
-
"""Parse TOML from a binary file object."""
|
332
|
-
b = fp.read()
|
333
|
-
try:
|
334
|
-
s = b.decode()
|
335
|
-
except AttributeError:
|
336
|
-
raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
|
337
|
-
return toml_loads(s, parse_float=parse_float)
|
338
245
|
|
246
|
+
########################################
|
247
|
+
# ../states.py
|
339
248
|
|
340
|
-
def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
|
341
|
-
"""Parse TOML from a string."""
|
342
249
|
|
343
|
-
|
344
|
-
try:
|
345
|
-
src = s.replace('\r\n', '\n')
|
346
|
-
except (AttributeError, TypeError):
|
347
|
-
raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
|
348
|
-
pos = 0
|
349
|
-
out = TomlOutput(TomlNestedDict(), TomlFlags())
|
350
|
-
header: TomlKey = ()
|
351
|
-
parse_float = toml_make_safe_parse_float(parse_float)
|
250
|
+
##
|
352
251
|
|
353
|
-
# Parse one statement at a time (typically means one line in TOML source)
|
354
|
-
while True:
|
355
|
-
# 1. Skip line leading whitespace
|
356
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
357
252
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
char = src[pos]
|
368
|
-
except IndexError:
|
369
|
-
break
|
370
|
-
if char == '\n':
|
371
|
-
pos += 1
|
372
|
-
continue
|
373
|
-
if char in TOML_KEY_INITIAL_CHARS:
|
374
|
-
pos = toml_key_value_rule(src, pos, out, header, parse_float)
|
375
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
376
|
-
elif char == '[':
|
377
|
-
try:
|
378
|
-
second_char: ta.Optional[str] = src[pos + 1]
|
379
|
-
except IndexError:
|
380
|
-
second_char = None
|
381
|
-
out.flags.finalize_pending()
|
382
|
-
if second_char == '[':
|
383
|
-
pos, header = toml_create_list_rule(src, pos, out)
|
384
|
-
else:
|
385
|
-
pos, header = toml_create_dict_rule(src, pos, out)
|
386
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
387
|
-
elif char != '#':
|
388
|
-
raise toml_suffixed_err(src, pos, 'Invalid statement')
|
253
|
+
class ProcessState(enum.IntEnum):
|
254
|
+
STOPPED = 0
|
255
|
+
STARTING = 10
|
256
|
+
RUNNING = 20
|
257
|
+
BACKOFF = 30
|
258
|
+
STOPPING = 40
|
259
|
+
EXITED = 100
|
260
|
+
FATAL = 200
|
261
|
+
UNKNOWN = 1000
|
389
262
|
|
390
|
-
|
391
|
-
|
263
|
+
@property
|
264
|
+
def stopped(self) -> bool:
|
265
|
+
return self in STOPPED_STATES
|
392
266
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
except IndexError:
|
397
|
-
break
|
398
|
-
if char != '\n':
|
399
|
-
raise toml_suffixed_err(
|
400
|
-
src, pos, 'Expected newline or end of document after a statement',
|
401
|
-
)
|
402
|
-
pos += 1
|
267
|
+
@property
|
268
|
+
def running(self) -> bool:
|
269
|
+
return self in RUNNING_STATES
|
403
270
|
|
404
|
-
|
271
|
+
@property
|
272
|
+
def signalable(self) -> bool:
|
273
|
+
return self in SIGNALABLE_STATES
|
405
274
|
|
406
275
|
|
407
|
-
|
408
|
-
|
276
|
+
# http://supervisord.org/subprocess.html
|
277
|
+
STATE_TRANSITIONS = {
|
278
|
+
ProcessState.STOPPED: (ProcessState.STARTING,),
|
279
|
+
ProcessState.STARTING: (ProcessState.RUNNING, ProcessState.BACKOFF, ProcessState.STOPPING),
|
280
|
+
ProcessState.RUNNING: (ProcessState.STOPPING, ProcessState.EXITED),
|
281
|
+
ProcessState.BACKOFF: (ProcessState.STARTING, ProcessState.FATAL),
|
282
|
+
ProcessState.STOPPING: (ProcessState.STOPPED,),
|
283
|
+
ProcessState.EXITED: (ProcessState.STARTING,),
|
284
|
+
ProcessState.FATAL: (ProcessState.STARTING,),
|
285
|
+
}
|
409
286
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
287
|
+
STOPPED_STATES = (
|
288
|
+
ProcessState.STOPPED,
|
289
|
+
ProcessState.EXITED,
|
290
|
+
ProcessState.FATAL,
|
291
|
+
ProcessState.UNKNOWN,
|
292
|
+
)
|
414
293
|
|
415
|
-
|
416
|
-
|
417
|
-
|
294
|
+
RUNNING_STATES = (
|
295
|
+
ProcessState.RUNNING,
|
296
|
+
ProcessState.BACKOFF,
|
297
|
+
ProcessState.STARTING,
|
298
|
+
)
|
418
299
|
|
419
|
-
|
420
|
-
|
300
|
+
SIGNALABLE_STATES = (
|
301
|
+
ProcessState.RUNNING,
|
302
|
+
ProcessState.STARTING,
|
303
|
+
ProcessState.STOPPING,
|
304
|
+
)
|
421
305
|
|
422
|
-
def finalize_pending(self) -> None:
|
423
|
-
for key, flag in self._pending_flags:
|
424
|
-
self.set(key, flag, recursive=False)
|
425
|
-
self._pending_flags.clear()
|
426
306
|
|
427
|
-
|
428
|
-
cont = self._flags
|
429
|
-
for k in key[:-1]:
|
430
|
-
if k not in cont:
|
431
|
-
return
|
432
|
-
cont = cont[k]['nested']
|
433
|
-
cont.pop(key[-1], None)
|
307
|
+
##
|
434
308
|
|
435
|
-
def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
|
436
|
-
cont = self._flags
|
437
|
-
key_parent, key_stem = key[:-1], key[-1]
|
438
|
-
for k in key_parent:
|
439
|
-
if k not in cont:
|
440
|
-
cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
441
|
-
cont = cont[k]['nested']
|
442
|
-
if key_stem not in cont:
|
443
|
-
cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
444
|
-
cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
|
445
309
|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
if k not in cont:
|
452
|
-
return False
|
453
|
-
inner_cont = cont[k]
|
454
|
-
if flag in inner_cont['recursive_flags']:
|
455
|
-
return True
|
456
|
-
cont = inner_cont['nested']
|
457
|
-
key_stem = key[-1]
|
458
|
-
if key_stem in cont:
|
459
|
-
cont = cont[key_stem]
|
460
|
-
return flag in cont['flags'] or flag in cont['recursive_flags']
|
461
|
-
return False
|
310
|
+
class SupervisorState(enum.IntEnum):
|
311
|
+
FATAL = 2
|
312
|
+
RUNNING = 1
|
313
|
+
RESTARTING = 0
|
314
|
+
SHUTDOWN = -1
|
462
315
|
|
463
316
|
|
464
|
-
|
465
|
-
|
466
|
-
# The parsed content of the TOML document
|
467
|
-
self.dict: ta.Dict[str, ta.Any] = {}
|
317
|
+
########################################
|
318
|
+
# ../utils/collections.py
|
468
319
|
|
469
|
-
def get_or_create_nest(
|
470
|
-
self,
|
471
|
-
key: TomlKey,
|
472
|
-
*,
|
473
|
-
access_lists: bool = True,
|
474
|
-
) -> dict:
|
475
|
-
cont: ta.Any = self.dict
|
476
|
-
for k in key:
|
477
|
-
if k not in cont:
|
478
|
-
cont[k] = {}
|
479
|
-
cont = cont[k]
|
480
|
-
if access_lists and isinstance(cont, list):
|
481
|
-
cont = cont[-1]
|
482
|
-
if not isinstance(cont, dict):
|
483
|
-
raise KeyError('There is no nest behind this key')
|
484
|
-
return cont
|
485
320
|
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
if not isinstance(list_, list):
|
492
|
-
raise KeyError('An object other than list found behind this key')
|
493
|
-
list_.append({})
|
494
|
-
else:
|
495
|
-
cont[last_key] = [{}]
|
321
|
+
class KeyedCollectionAccessors(abc.ABC, ta.Generic[K, V]):
|
322
|
+
@property
|
323
|
+
@abc.abstractmethod
|
324
|
+
def _by_key(self) -> ta.Mapping[K, V]:
|
325
|
+
raise NotImplementedError
|
496
326
|
|
327
|
+
def __iter__(self) -> ta.Iterator[V]:
|
328
|
+
return iter(self._by_key.values())
|
497
329
|
|
498
|
-
|
499
|
-
|
500
|
-
flags: TomlFlags
|
330
|
+
def __len__(self) -> int:
|
331
|
+
return len(self._by_key)
|
501
332
|
|
333
|
+
def __contains__(self, key: K) -> bool:
|
334
|
+
return key in self._by_key
|
502
335
|
|
503
|
-
def
|
504
|
-
|
505
|
-
while src[pos] in chars:
|
506
|
-
pos += 1
|
507
|
-
except IndexError:
|
508
|
-
pass
|
509
|
-
return pos
|
336
|
+
def __getitem__(self, key: K) -> V:
|
337
|
+
return self._by_key[key]
|
510
338
|
|
339
|
+
def get(self, key: K, default: ta.Optional[V] = None) -> ta.Optional[V]:
|
340
|
+
return self._by_key.get(key, default)
|
511
341
|
|
512
|
-
def
|
513
|
-
|
514
|
-
pos: TomlPos,
|
515
|
-
expect: str,
|
516
|
-
*,
|
517
|
-
error_on: ta.FrozenSet[str],
|
518
|
-
error_on_eof: bool,
|
519
|
-
) -> TomlPos:
|
520
|
-
try:
|
521
|
-
new_pos = src.index(expect, pos)
|
522
|
-
except ValueError:
|
523
|
-
new_pos = len(src)
|
524
|
-
if error_on_eof:
|
525
|
-
raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
|
342
|
+
def items(self) -> ta.Iterator[ta.Tuple[K, V]]:
|
343
|
+
return iter(self._by_key.items())
|
526
344
|
|
527
|
-
if not error_on.isdisjoint(src[pos:new_pos]):
|
528
|
-
while src[pos] not in error_on:
|
529
|
-
pos += 1
|
530
|
-
raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
|
531
|
-
return new_pos
|
532
345
|
|
346
|
+
class KeyedCollection(KeyedCollectionAccessors[K, V]):
|
347
|
+
def __init__(self, items: ta.Iterable[V]) -> None:
|
348
|
+
super().__init__()
|
533
349
|
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
return toml_skip_until(
|
541
|
-
src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
|
542
|
-
)
|
543
|
-
return pos
|
350
|
+
by_key: ta.Dict[K, V] = {}
|
351
|
+
for v in items:
|
352
|
+
if (k := self._key(v)) in by_key:
|
353
|
+
raise KeyError(f'key {k} of {v} already registered by {by_key[k]}')
|
354
|
+
by_key[k] = v
|
355
|
+
self.__by_key = by_key
|
544
356
|
|
357
|
+
@property
|
358
|
+
def _by_key(self) -> ta.Mapping[K, V]:
|
359
|
+
return self.__by_key
|
545
360
|
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
550
|
-
pos = toml_skip_comment(src, pos)
|
551
|
-
if pos == pos_before_skip:
|
552
|
-
return pos
|
361
|
+
@abc.abstractmethod
|
362
|
+
def _key(self, v: V) -> K:
|
363
|
+
raise NotImplementedError
|
553
364
|
|
554
365
|
|
555
|
-
|
556
|
-
|
557
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
558
|
-
pos, key = toml_parse_key(src, pos)
|
366
|
+
########################################
|
367
|
+
# ../utils/diag.py
|
559
368
|
|
560
|
-
if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
|
561
|
-
raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
|
562
|
-
out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
|
563
|
-
try:
|
564
|
-
out.data.get_or_create_nest(key)
|
565
|
-
except KeyError:
|
566
|
-
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
567
369
|
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
370
|
+
def compact_traceback() -> ta.Tuple[
|
371
|
+
ta.Tuple[str, str, int],
|
372
|
+
ta.Type[BaseException],
|
373
|
+
BaseException,
|
374
|
+
types.TracebackType,
|
375
|
+
]:
|
376
|
+
t, v, tb = sys.exc_info()
|
377
|
+
if not tb:
|
378
|
+
raise RuntimeError('No traceback')
|
577
379
|
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
except KeyError:
|
587
|
-
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
380
|
+
tbinfo = []
|
381
|
+
while tb:
|
382
|
+
tbinfo.append((
|
383
|
+
tb.tb_frame.f_code.co_filename,
|
384
|
+
tb.tb_frame.f_code.co_name,
|
385
|
+
str(tb.tb_lineno),
|
386
|
+
))
|
387
|
+
tb = tb.tb_next
|
588
388
|
|
589
|
-
|
590
|
-
|
591
|
-
return pos + 2, key
|
389
|
+
# just to be safe
|
390
|
+
del tb
|
592
391
|
|
392
|
+
file, function, line = tbinfo[-1]
|
393
|
+
info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) # noqa
|
394
|
+
return (file, function, line), t, v, info # type: ignore
|
593
395
|
|
594
|
-
def toml_key_value_rule(
|
595
|
-
src: str,
|
596
|
-
pos: TomlPos,
|
597
|
-
out: TomlOutput,
|
598
|
-
header: TomlKey,
|
599
|
-
parse_float: TomlParseFloat,
|
600
|
-
) -> TomlPos:
|
601
|
-
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
602
|
-
key_parent, key_stem = key[:-1], key[-1]
|
603
|
-
abs_key_parent = header + key_parent
|
604
396
|
|
605
|
-
|
606
|
-
|
607
|
-
# Check that dotted key syntax does not redefine an existing table
|
608
|
-
if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
|
609
|
-
raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
|
610
|
-
# Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
|
611
|
-
# table sections.
|
612
|
-
out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
|
397
|
+
########################################
|
398
|
+
# ../utils/fs.py
|
613
399
|
|
614
|
-
if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
|
615
|
-
raise toml_suffixed_err(
|
616
|
-
src,
|
617
|
-
pos,
|
618
|
-
f'Cannot mutate immutable namespace {abs_key_parent}',
|
619
|
-
)
|
620
400
|
|
401
|
+
def try_unlink(path: str) -> bool:
|
621
402
|
try:
|
622
|
-
|
623
|
-
except
|
624
|
-
|
625
|
-
|
626
|
-
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
|
627
|
-
# Mark inline table and array namespaces recursively immutable
|
628
|
-
if isinstance(value, (dict, list)):
|
629
|
-
out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
|
630
|
-
nest[key_stem] = value
|
631
|
-
return pos
|
403
|
+
os.unlink(path)
|
404
|
+
except OSError:
|
405
|
+
return False
|
406
|
+
return True
|
632
407
|
|
633
408
|
|
634
|
-
def
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
|
639
|
-
pos, key = toml_parse_key(src, pos)
|
640
|
-
try:
|
641
|
-
char: ta.Optional[str] = src[pos]
|
642
|
-
except IndexError:
|
643
|
-
char = None
|
644
|
-
if char != '=':
|
645
|
-
raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
|
646
|
-
pos += 1
|
647
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
648
|
-
pos, value = toml_parse_value(src, pos, parse_float)
|
649
|
-
return pos, key, value
|
409
|
+
def mktempfile(suffix: str, prefix: str, dir: str) -> str: # noqa
|
410
|
+
fd, filename = tempfile.mkstemp(suffix, prefix, dir)
|
411
|
+
os.close(fd)
|
412
|
+
return filename
|
650
413
|
|
651
414
|
|
652
|
-
def
|
653
|
-
|
654
|
-
key: TomlKey = (key_part,)
|
655
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
656
|
-
while True:
|
657
|
-
try:
|
658
|
-
char: ta.Optional[str] = src[pos]
|
659
|
-
except IndexError:
|
660
|
-
char = None
|
661
|
-
if char != '.':
|
662
|
-
return pos, key
|
663
|
-
pos += 1
|
664
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
665
|
-
pos, key_part = toml_parse_key_part(src, pos)
|
666
|
-
key += (key_part,)
|
667
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
415
|
+
def get_path() -> ta.Sequence[str]:
|
416
|
+
"""Return a list corresponding to $PATH, or a default."""
|
668
417
|
|
418
|
+
path = ['/bin', '/usr/bin', '/usr/local/bin']
|
419
|
+
if 'PATH' in os.environ:
|
420
|
+
p = os.environ['PATH']
|
421
|
+
if p:
|
422
|
+
path = p.split(os.pathsep)
|
423
|
+
return path
|
669
424
|
|
670
|
-
def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
671
|
-
try:
|
672
|
-
char: ta.Optional[str] = src[pos]
|
673
|
-
except IndexError:
|
674
|
-
char = None
|
675
|
-
if char in TOML_BARE_KEY_CHARS:
|
676
|
-
start_pos = pos
|
677
|
-
pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
|
678
|
-
return pos, src[start_pos:pos]
|
679
|
-
if char == "'":
|
680
|
-
return toml_parse_literal_str(src, pos)
|
681
|
-
if char == '"':
|
682
|
-
return toml_parse_one_line_basic_str(src, pos)
|
683
|
-
raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
|
684
425
|
|
426
|
+
def check_existing_dir(v: str) -> str:
|
427
|
+
nv = os.path.expanduser(v)
|
428
|
+
if os.path.isdir(nv):
|
429
|
+
return nv
|
430
|
+
raise ValueError(f'{v} is not an existing directory')
|
685
431
|
|
686
|
-
def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
687
|
-
pos += 1
|
688
|
-
return toml_parse_basic_str(src, pos, multiline=False)
|
689
432
|
|
433
|
+
def check_path_with_existing_dir(v: str) -> str:
|
434
|
+
nv = os.path.expanduser(v)
|
435
|
+
dir = os.path.dirname(nv) # noqa
|
436
|
+
if not dir:
|
437
|
+
# relative pathname with no directory component
|
438
|
+
return nv
|
439
|
+
if os.path.isdir(dir):
|
440
|
+
return nv
|
441
|
+
raise ValueError(f'The directory named as part of the path {v} does not exist')
|
690
442
|
|
691
|
-
def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
|
692
|
-
pos += 1
|
693
|
-
array: list = []
|
694
443
|
|
695
|
-
|
696
|
-
|
697
|
-
return pos + 1, array
|
698
|
-
while True:
|
699
|
-
pos, val = toml_parse_value(src, pos, parse_float)
|
700
|
-
array.append(val)
|
701
|
-
pos = toml_skip_comments_and_array_ws(src, pos)
|
444
|
+
########################################
|
445
|
+
# ../utils/ostypes.py
|
702
446
|
|
703
|
-
c = src[pos:pos + 1]
|
704
|
-
if c == ']':
|
705
|
-
return pos + 1, array
|
706
|
-
if c != ',':
|
707
|
-
raise toml_suffixed_err(src, pos, 'Unclosed array')
|
708
|
-
pos += 1
|
709
447
|
|
710
|
-
|
711
|
-
|
712
|
-
|
448
|
+
Fd = ta.NewType('Fd', int)
|
449
|
+
Pid = ta.NewType('Pid', int)
|
450
|
+
Rc = ta.NewType('Rc', int)
|
713
451
|
|
452
|
+
Uid = ta.NewType('Uid', int)
|
453
|
+
Gid = ta.NewType('Gid', int)
|
714
454
|
|
715
|
-
def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
|
716
|
-
pos += 1
|
717
|
-
nested_dict = TomlNestedDict()
|
718
|
-
flags = TomlFlags()
|
719
455
|
|
720
|
-
|
721
|
-
|
722
|
-
return pos + 1, nested_dict.dict
|
723
|
-
while True:
|
724
|
-
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
725
|
-
key_parent, key_stem = key[:-1], key[-1]
|
726
|
-
if flags.is_(key, TomlFlags.FROZEN):
|
727
|
-
raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
|
728
|
-
try:
|
729
|
-
nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
|
730
|
-
except KeyError:
|
731
|
-
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
732
|
-
if key_stem in nest:
|
733
|
-
raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
|
734
|
-
nest[key_stem] = value
|
735
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
736
|
-
c = src[pos:pos + 1]
|
737
|
-
if c == '}':
|
738
|
-
return pos + 1, nested_dict.dict
|
739
|
-
if c != ',':
|
740
|
-
raise toml_suffixed_err(src, pos, 'Unclosed inline table')
|
741
|
-
if isinstance(value, (dict, list)):
|
742
|
-
flags.set(key, TomlFlags.FROZEN, recursive=True)
|
743
|
-
pos += 1
|
744
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
456
|
+
########################################
|
457
|
+
# ../utils/signals.py
|
745
458
|
|
746
459
|
|
747
|
-
|
748
|
-
src: str,
|
749
|
-
pos: TomlPos,
|
750
|
-
*,
|
751
|
-
multiline: bool = False,
|
752
|
-
) -> ta.Tuple[TomlPos, str]:
|
753
|
-
escape_id = src[pos:pos + 2]
|
754
|
-
pos += 2
|
755
|
-
if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
|
756
|
-
# Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
|
757
|
-
# newline.
|
758
|
-
if escape_id != '\\\n':
|
759
|
-
pos = toml_skip_chars(src, pos, TOML_WS)
|
760
|
-
try:
|
761
|
-
char = src[pos]
|
762
|
-
except IndexError:
|
763
|
-
return pos, ''
|
764
|
-
if char != '\n':
|
765
|
-
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
|
766
|
-
pos += 1
|
767
|
-
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
768
|
-
return pos, ''
|
769
|
-
if escape_id == '\\u':
|
770
|
-
return toml_parse_hex_char(src, pos, 4)
|
771
|
-
if escape_id == '\\U':
|
772
|
-
return toml_parse_hex_char(src, pos, 8)
|
773
|
-
try:
|
774
|
-
return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
|
775
|
-
except KeyError:
|
776
|
-
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
|
460
|
+
##
|
777
461
|
|
778
462
|
|
779
|
-
|
780
|
-
|
463
|
+
_SIGS_BY_NUM: ta.Mapping[int, signal.Signals] = {s.value: s for s in signal.Signals}
|
464
|
+
_SIGS_BY_NAME: ta.Mapping[str, signal.Signals] = {s.name: s for s in signal.Signals}
|
781
465
|
|
782
466
|
|
783
|
-
def
|
784
|
-
hex_str = src[pos:pos + hex_len]
|
785
|
-
if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
|
786
|
-
raise toml_suffixed_err(src, pos, 'Invalid hex value')
|
787
|
-
pos += hex_len
|
788
|
-
hex_int = int(hex_str, 16)
|
789
|
-
if not toml_is_unicode_scalar_value(hex_int):
|
790
|
-
raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
|
791
|
-
return pos, chr(hex_int)
|
792
|
-
|
793
|
-
|
794
|
-
def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
795
|
-
pos += 1 # Skip starting apostrophe
|
796
|
-
start_pos = pos
|
797
|
-
pos = toml_skip_until(
|
798
|
-
src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
|
799
|
-
)
|
800
|
-
return pos + 1, src[start_pos:pos] # Skip ending apostrophe
|
801
|
-
|
802
|
-
|
803
|
-
def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
|
804
|
-
pos += 3
|
805
|
-
if src.startswith('\n', pos):
|
806
|
-
pos += 1
|
807
|
-
|
808
|
-
if literal:
|
809
|
-
delim = "'"
|
810
|
-
end_pos = toml_skip_until(
|
811
|
-
src,
|
812
|
-
pos,
|
813
|
-
"'''",
|
814
|
-
error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
|
815
|
-
error_on_eof=True,
|
816
|
-
)
|
817
|
-
result = src[pos:end_pos]
|
818
|
-
pos = end_pos + 3
|
819
|
-
else:
|
820
|
-
delim = '"'
|
821
|
-
pos, result = toml_parse_basic_str(src, pos, multiline=True)
|
822
|
-
|
823
|
-
# Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
|
824
|
-
if not src.startswith(delim, pos):
|
825
|
-
return pos, result
|
826
|
-
pos += 1
|
827
|
-
if not src.startswith(delim, pos):
|
828
|
-
return pos, result + delim
|
829
|
-
pos += 1
|
830
|
-
return pos, result + (delim * 2)
|
831
|
-
|
832
|
-
|
833
|
-
def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
|
834
|
-
if multiline:
|
835
|
-
error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
836
|
-
parse_escapes = toml_parse_basic_str_escape_multiline
|
837
|
-
else:
|
838
|
-
error_on = TOML_ILLEGAL_BASIC_STR_CHARS
|
839
|
-
parse_escapes = toml_parse_basic_str_escape
|
840
|
-
result = ''
|
841
|
-
start_pos = pos
|
842
|
-
while True:
|
843
|
-
try:
|
844
|
-
char = src[pos]
|
845
|
-
except IndexError:
|
846
|
-
raise toml_suffixed_err(src, pos, 'Unterminated string') from None
|
847
|
-
if char == '"':
|
848
|
-
if not multiline:
|
849
|
-
return pos + 1, result + src[start_pos:pos]
|
850
|
-
if src.startswith('"""', pos):
|
851
|
-
return pos + 3, result + src[start_pos:pos]
|
852
|
-
pos += 1
|
853
|
-
continue
|
854
|
-
if char == '\\':
|
855
|
-
result += src[start_pos:pos]
|
856
|
-
pos, parsed_escape = parse_escapes(src, pos)
|
857
|
-
result += parsed_escape
|
858
|
-
start_pos = pos
|
859
|
-
continue
|
860
|
-
if char in error_on:
|
861
|
-
raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
|
862
|
-
pos += 1
|
863
|
-
|
864
|
-
|
865
|
-
def toml_parse_value( # noqa: C901
|
866
|
-
src: str,
|
867
|
-
pos: TomlPos,
|
868
|
-
parse_float: TomlParseFloat,
|
869
|
-
) -> ta.Tuple[TomlPos, ta.Any]:
|
467
|
+
def sig_num(value: ta.Union[int, str]) -> int:
|
870
468
|
try:
|
871
|
-
|
872
|
-
except IndexError:
|
873
|
-
char = None
|
469
|
+
num = int(value)
|
874
470
|
|
875
|
-
|
471
|
+
except (ValueError, TypeError):
|
472
|
+
name = value.strip().upper() # type: ignore
|
473
|
+
if not name.startswith('SIG'):
|
474
|
+
name = f'SIG{name}'
|
876
475
|
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
return toml_parse_multiline_str(src, pos, literal=False)
|
881
|
-
return toml_parse_one_line_basic_str(src, pos)
|
476
|
+
if (sn := _SIGS_BY_NAME.get(name)) is None:
|
477
|
+
raise ValueError(f'value {value!r} is not a valid signal name') # noqa
|
478
|
+
num = sn
|
882
479
|
|
883
|
-
|
884
|
-
|
885
|
-
if src.startswith("'''", pos):
|
886
|
-
return toml_parse_multiline_str(src, pos, literal=True)
|
887
|
-
return toml_parse_literal_str(src, pos)
|
480
|
+
if num not in _SIGS_BY_NUM:
|
481
|
+
raise ValueError(f'value {value!r} is not a valid signal number')
|
888
482
|
|
889
|
-
|
890
|
-
if char == 't':
|
891
|
-
if src.startswith('true', pos):
|
892
|
-
return pos + 4, True
|
893
|
-
if char == 'f':
|
894
|
-
if src.startswith('false', pos):
|
895
|
-
return pos + 5, False
|
483
|
+
return num
|
896
484
|
|
897
|
-
# Arrays
|
898
|
-
if char == '[':
|
899
|
-
return toml_parse_array(src, pos, parse_float)
|
900
485
|
|
901
|
-
|
902
|
-
if
|
903
|
-
return
|
486
|
+
def sig_name(num: int) -> str:
|
487
|
+
if (sig := _SIGS_BY_NUM.get(num)) is not None:
|
488
|
+
return sig.name
|
489
|
+
return f'signal {sig}'
|
904
490
|
|
905
|
-
# Dates and times
|
906
|
-
datetime_match = TOML_RE_DATETIME.match(src, pos)
|
907
|
-
if datetime_match:
|
908
|
-
try:
|
909
|
-
datetime_obj = toml_match_to_datetime(datetime_match)
|
910
|
-
except ValueError as e:
|
911
|
-
raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
|
912
|
-
return datetime_match.end(), datetime_obj
|
913
|
-
localtime_match = TOML_RE_LOCALTIME.match(src, pos)
|
914
|
-
if localtime_match:
|
915
|
-
return localtime_match.end(), toml_match_to_localtime(localtime_match)
|
916
491
|
|
917
|
-
|
918
|
-
# located after handling of dates and times.
|
919
|
-
number_match = TOML_RE_NUMBER.match(src, pos)
|
920
|
-
if number_match:
|
921
|
-
return number_match.end(), toml_match_to_number(number_match, parse_float)
|
492
|
+
##
|
922
493
|
|
923
|
-
# Special floats
|
924
|
-
first_three = src[pos:pos + 3]
|
925
|
-
if first_three in {'inf', 'nan'}:
|
926
|
-
return pos + 3, parse_float(first_three)
|
927
|
-
first_four = src[pos:pos + 4]
|
928
|
-
if first_four in {'-inf', '+inf', '-nan', '+nan'}:
|
929
|
-
return pos + 4, parse_float(first_four)
|
930
494
|
|
931
|
-
|
495
|
+
class SignalReceiver:
|
496
|
+
def __init__(self) -> None:
|
497
|
+
super().__init__()
|
932
498
|
|
499
|
+
self._signals_recvd: ta.List[int] = []
|
933
500
|
|
934
|
-
def
|
935
|
-
|
501
|
+
def receive(self, sig: int, frame: ta.Any = None) -> None:
|
502
|
+
if sig not in self._signals_recvd:
|
503
|
+
self._signals_recvd.append(sig)
|
936
504
|
|
937
|
-
def
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
505
|
+
def install(self, *sigs: int) -> None:
|
506
|
+
for sig in sigs:
|
507
|
+
signal.signal(sig, self.receive)
|
508
|
+
|
509
|
+
def get_signal(self) -> ta.Optional[int]:
|
510
|
+
if self._signals_recvd:
|
511
|
+
sig = self._signals_recvd.pop(0)
|
943
512
|
else:
|
944
|
-
|
945
|
-
return
|
513
|
+
sig = None
|
514
|
+
return sig
|
946
515
|
|
947
|
-
return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
|
948
516
|
|
517
|
+
########################################
|
518
|
+
# ../utils/strings.py
|
949
519
|
|
950
|
-
def toml_is_unicode_scalar_value(codepoint: int) -> bool:
|
951
|
-
return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
|
952
520
|
|
521
|
+
##
|
953
522
|
|
954
|
-
def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
|
955
|
-
"""A decorator to make `parse_float` safe.
|
956
523
|
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
return float
|
524
|
+
def as_bytes(s: ta.Union[str, bytes], encoding: str = 'utf8') -> bytes:
|
525
|
+
if isinstance(s, bytes):
|
526
|
+
return s
|
527
|
+
else:
|
528
|
+
return s.encode(encoding)
|
963
529
|
|
964
|
-
def safe_parse_float(float_str: str) -> ta.Any:
|
965
|
-
float_value = parse_float(float_str)
|
966
|
-
if isinstance(float_value, (dict, list)):
|
967
|
-
raise ValueError('parse_float must not return dicts or lists') # noqa
|
968
|
-
return float_value
|
969
530
|
|
970
|
-
|
531
|
+
@ta.overload
|
532
|
+
def find_prefix_at_end(haystack: str, needle: str) -> int:
|
533
|
+
...
|
971
534
|
|
972
535
|
|
973
|
-
|
974
|
-
|
536
|
+
@ta.overload
|
537
|
+
def find_prefix_at_end(haystack: bytes, needle: bytes) -> int:
|
538
|
+
...
|
975
539
|
|
976
540
|
|
977
|
-
|
978
|
-
|
541
|
+
def find_prefix_at_end(haystack, needle):
|
542
|
+
l = len(needle) - 1
|
543
|
+
while l and not haystack.endswith(needle[:l]):
|
544
|
+
l -= 1
|
545
|
+
return l
|
979
546
|
|
980
547
|
|
981
|
-
|
982
|
-
"""Indicates the command could not be parsed properly."""
|
548
|
+
##
|
983
549
|
|
984
550
|
|
985
|
-
|
986
|
-
|
987
|
-
Indicates that the filespec cannot be executed because its path resolves to a file which is not executable, or which
|
988
|
-
is a directory.
|
989
|
-
"""
|
551
|
+
ANSI_ESCAPE_BEGIN = b'\x1b['
|
552
|
+
ANSI_TERMINATORS = (b'H', b'f', b'A', b'B', b'C', b'D', b'R', b's', b'u', b'J', b'K', b'h', b'l', b'p', b'm')
|
990
553
|
|
991
554
|
|
992
|
-
|
993
|
-
"""
|
555
|
+
def strip_escapes(s: bytes) -> bytes:
|
556
|
+
"""Remove all ANSI color escapes from the given string."""
|
994
557
|
|
558
|
+
result = b''
|
559
|
+
show = 1
|
560
|
+
i = 0
|
561
|
+
l = len(s)
|
562
|
+
while i < l:
|
563
|
+
if show == 0 and s[i:i + 1] in ANSI_TERMINATORS:
|
564
|
+
show = 1
|
565
|
+
elif show:
|
566
|
+
n = s.find(ANSI_ESCAPE_BEGIN, i)
|
567
|
+
if n == -1:
|
568
|
+
return result + s[i:]
|
569
|
+
else:
|
570
|
+
result = result + s[i:n]
|
571
|
+
i = n
|
572
|
+
show = 0
|
573
|
+
i += 1
|
574
|
+
return result
|
995
575
|
|
996
|
-
class NoPermissionError(ProcessError):
|
997
|
-
"""
|
998
|
-
Indicates that the file cannot be executed because the supervisor process does not possess the appropriate UNIX
|
999
|
-
filesystem permission to execute the file.
|
1000
|
-
"""
|
1001
576
|
|
577
|
+
##
|
1002
578
|
|
1003
|
-
########################################
|
1004
|
-
# ../privileges.py
|
1005
579
|
|
580
|
+
class SuffixMultiplier:
|
581
|
+
# d is a dictionary of suffixes to integer multipliers. If no suffixes match, default is the multiplier. Matches are
|
582
|
+
# case insensitive. Return values are in the fundamental unit.
|
583
|
+
def __init__(self, d, default=1):
|
584
|
+
super().__init__()
|
585
|
+
self._d = d
|
586
|
+
self._default = default
|
587
|
+
# all keys must be the same size
|
588
|
+
self._keysz = None
|
589
|
+
for k in d:
|
590
|
+
if self._keysz is None:
|
591
|
+
self._keysz = len(k)
|
592
|
+
elif self._keysz != len(k): # type: ignore
|
593
|
+
raise ValueError(k)
|
1006
594
|
|
1007
|
-
def
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
595
|
+
def __call__(self, v: ta.Union[str, int]) -> int:
|
596
|
+
if isinstance(v, int):
|
597
|
+
return v
|
598
|
+
v = v.lower()
|
599
|
+
for s, m in self._d.items():
|
600
|
+
if v[-self._keysz:] == s: # type: ignore
|
601
|
+
return int(v[:-self._keysz]) * m # type: ignore
|
602
|
+
return int(v) * self._default
|
1012
603
|
|
1013
|
-
if user is None:
|
1014
|
-
return 'No user specified to setuid to!'
|
1015
604
|
|
1016
|
-
|
605
|
+
parse_bytes_size = SuffixMultiplier({
|
606
|
+
'kb': 1024,
|
607
|
+
'mb': 1024 * 1024,
|
608
|
+
'gb': 1024 * 1024 * 1024,
|
609
|
+
})
|
610
|
+
|
611
|
+
|
612
|
+
#
|
613
|
+
|
614
|
+
|
615
|
+
def parse_octal(arg: ta.Union[str, int]) -> int:
|
616
|
+
if isinstance(arg, int):
|
617
|
+
return arg
|
1017
618
|
try:
|
1018
|
-
|
1019
|
-
except ValueError:
|
1020
|
-
|
1021
|
-
pwrec = pwd.getpwnam(user) # type: ignore
|
1022
|
-
except KeyError:
|
1023
|
-
return f"Can't find username {user!r}"
|
1024
|
-
uid = pwrec[2]
|
1025
|
-
else:
|
1026
|
-
try:
|
1027
|
-
pwrec = pwd.getpwuid(uid)
|
1028
|
-
except KeyError:
|
1029
|
-
return f"Can't find uid {uid!r}"
|
619
|
+
return int(arg, 8)
|
620
|
+
except (TypeError, ValueError):
|
621
|
+
raise ValueError(f'{arg} can not be converted to an octal type') # noqa
|
1030
622
|
|
1031
|
-
current_uid = os.getuid()
|
1032
623
|
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
624
|
+
########################################
|
625
|
+
# ../../../omlish/formats/toml/parser.py
|
626
|
+
# SPDX-License-Identifier: MIT
|
627
|
+
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
628
|
+
# Licensed to PSF under a Contributor Agreement.
|
629
|
+
#
|
630
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
631
|
+
# --------------------------------------------
|
632
|
+
#
|
633
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
634
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
635
|
+
# documentation.
|
636
|
+
#
|
637
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
638
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
639
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
640
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
641
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
|
642
|
+
# Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
|
643
|
+
#
|
644
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
645
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
646
|
+
# any such work a brief summary of the changes made to Python.
|
647
|
+
#
|
648
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
649
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
650
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
651
|
+
# RIGHTS.
|
652
|
+
#
|
653
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
654
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
655
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
656
|
+
#
|
657
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
658
|
+
#
|
659
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
660
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
661
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
662
|
+
#
|
663
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
664
|
+
# License Agreement.
|
665
|
+
#
|
666
|
+
# https://github.com/python/cpython/blob/9ce90206b7a4649600218cf0bd4826db79c9a312/Lib/tomllib/_parser.py
|
1037
667
|
|
1038
|
-
if current_uid != 0:
|
1039
|
-
return "Can't drop privilege as nonroot user"
|
1040
668
|
|
1041
|
-
|
1042
|
-
if hasattr(os, 'setgroups'):
|
1043
|
-
user = pwrec[0]
|
1044
|
-
groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
|
669
|
+
##
|
1045
670
|
|
1046
|
-
# always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
|
1047
|
-
# group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
|
1048
|
-
# python 2.7 - this will be safe though for all unix /python version combos)
|
1049
|
-
groups.insert(0, gid)
|
1050
|
-
try:
|
1051
|
-
os.setgroups(groups)
|
1052
|
-
except OSError:
|
1053
|
-
return 'Could not set groups of effective user'
|
1054
671
|
|
1055
|
-
|
1056
|
-
os.setgid(gid)
|
1057
|
-
except OSError:
|
1058
|
-
return 'Could not set group id of effective user'
|
672
|
+
_TOML_TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
|
1059
673
|
|
1060
|
-
|
674
|
+
TOML_RE_NUMBER = re.compile(
|
675
|
+
r"""
|
676
|
+
0
|
677
|
+
(?:
|
678
|
+
x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
|
679
|
+
|
|
680
|
+
b[01](?:_?[01])* # bin
|
681
|
+
|
|
682
|
+
o[0-7](?:_?[0-7])* # oct
|
683
|
+
)
|
684
|
+
|
|
685
|
+
[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
|
686
|
+
(?P<floatpart>
|
687
|
+
(?:\.[0-9](?:_?[0-9])*)? # optional fractional part
|
688
|
+
(?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
|
689
|
+
)
|
690
|
+
""",
|
691
|
+
flags=re.VERBOSE,
|
692
|
+
)
|
693
|
+
TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
|
694
|
+
TOML_RE_DATETIME = re.compile(
|
695
|
+
rf"""
|
696
|
+
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
|
697
|
+
(?:
|
698
|
+
[Tt ]
|
699
|
+
{_TOML_TIME_RE_STR}
|
700
|
+
(?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
|
701
|
+
)?
|
702
|
+
""",
|
703
|
+
flags=re.VERBOSE,
|
704
|
+
)
|
1061
705
|
|
1062
|
-
return None
|
1063
706
|
|
707
|
+
def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
|
708
|
+
"""Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
|
1064
709
|
|
1065
|
-
|
1066
|
-
|
710
|
+
Raises ValueError if the match does not correspond to a valid date or datetime.
|
711
|
+
"""
|
712
|
+
(
|
713
|
+
year_str,
|
714
|
+
month_str,
|
715
|
+
day_str,
|
716
|
+
hour_str,
|
717
|
+
minute_str,
|
718
|
+
sec_str,
|
719
|
+
micros_str,
|
720
|
+
zulu_time,
|
721
|
+
offset_sign_str,
|
722
|
+
offset_hour_str,
|
723
|
+
offset_minute_str,
|
724
|
+
) = match.groups()
|
725
|
+
year, month, day = int(year_str), int(month_str), int(day_str)
|
726
|
+
if hour_str is None:
|
727
|
+
return datetime.date(year, month, day)
|
728
|
+
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
|
729
|
+
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
730
|
+
if offset_sign_str:
|
731
|
+
tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
|
732
|
+
offset_hour_str, offset_minute_str, offset_sign_str,
|
733
|
+
)
|
734
|
+
elif zulu_time:
|
735
|
+
tz = datetime.UTC
|
736
|
+
else: # local date-time
|
737
|
+
tz = None
|
738
|
+
return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
|
1067
739
|
|
1068
740
|
|
1069
|
-
|
741
|
+
@functools.lru_cache() # noqa
|
742
|
+
def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
|
743
|
+
sign = 1 if sign_str == '+' else -1
|
744
|
+
return datetime.timezone(
|
745
|
+
datetime.timedelta(
|
746
|
+
hours=sign * int(hour_str),
|
747
|
+
minutes=sign * int(minute_str),
|
748
|
+
),
|
749
|
+
)
|
1070
750
|
|
1071
751
|
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
BACKOFF = 30
|
1077
|
-
STOPPING = 40
|
1078
|
-
EXITED = 100
|
1079
|
-
FATAL = 200
|
1080
|
-
UNKNOWN = 1000
|
752
|
+
def toml_match_to_localtime(match: re.Match) -> datetime.time:
|
753
|
+
hour_str, minute_str, sec_str, micros_str = match.groups()
|
754
|
+
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
755
|
+
return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
|
1081
756
|
|
1082
|
-
@property
|
1083
|
-
def stopped(self) -> bool:
|
1084
|
-
return self in STOPPED_STATES
|
1085
757
|
|
1086
|
-
|
1087
|
-
|
1088
|
-
return
|
758
|
+
def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
|
759
|
+
if match.group('floatpart'):
|
760
|
+
return parse_float(match.group())
|
761
|
+
return int(match.group(), 0)
|
1089
762
|
|
1090
|
-
@property
|
1091
|
-
def signalable(self) -> bool:
|
1092
|
-
return self in SIGNALABLE_STATES
|
1093
763
|
|
764
|
+
TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
|
1094
765
|
|
1095
|
-
#
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
ProcessState.RUNNING: (ProcessState.STOPPING, ProcessState.EXITED),
|
1100
|
-
ProcessState.BACKOFF: (ProcessState.STARTING, ProcessState.FATAL),
|
1101
|
-
ProcessState.STOPPING: (ProcessState.STOPPED,),
|
1102
|
-
ProcessState.EXITED: (ProcessState.STARTING,),
|
1103
|
-
ProcessState.FATAL: (ProcessState.STARTING,),
|
1104
|
-
}
|
766
|
+
# Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
|
767
|
+
# functions.
|
768
|
+
TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
|
769
|
+
TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
|
1105
770
|
|
1106
|
-
|
1107
|
-
|
1108
|
-
ProcessState.EXITED,
|
1109
|
-
ProcessState.FATAL,
|
1110
|
-
ProcessState.UNKNOWN,
|
1111
|
-
)
|
771
|
+
TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
|
772
|
+
TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
1112
773
|
|
1113
|
-
|
1114
|
-
ProcessState.RUNNING,
|
1115
|
-
ProcessState.BACKOFF,
|
1116
|
-
ProcessState.STARTING,
|
1117
|
-
)
|
774
|
+
TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
|
1118
775
|
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
776
|
+
TOML_WS = frozenset(' \t')
|
777
|
+
TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
|
778
|
+
TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
|
779
|
+
TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
|
780
|
+
TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
|
781
|
+
|
782
|
+
TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
|
783
|
+
{
|
784
|
+
'\\b': '\u0008', # backspace
|
785
|
+
'\\t': '\u0009', # tab
|
786
|
+
'\\n': '\u000A', # linefeed
|
787
|
+
'\\f': '\u000C', # form feed
|
788
|
+
'\\r': '\u000D', # carriage return
|
789
|
+
'\\"': '\u0022', # quote
|
790
|
+
'\\\\': '\u005C', # backslash
|
791
|
+
},
|
1123
792
|
)
|
1124
793
|
|
1125
794
|
|
1126
|
-
|
795
|
+
class TomlDecodeError(ValueError):
|
796
|
+
"""An error raised if a document is not valid TOML."""
|
797
|
+
|
798
|
+
|
799
|
+
def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
|
800
|
+
"""Parse TOML from a binary file object."""
|
801
|
+
b = fp.read()
|
802
|
+
try:
|
803
|
+
s = b.decode()
|
804
|
+
except AttributeError:
|
805
|
+
raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
|
806
|
+
return toml_loads(s, parse_float=parse_float)
|
807
|
+
|
808
|
+
|
809
|
+
def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
|
810
|
+
"""Parse TOML from a string."""
|
811
|
+
|
812
|
+
# The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
|
813
|
+
try:
|
814
|
+
src = s.replace('\r\n', '\n')
|
815
|
+
except (AttributeError, TypeError):
|
816
|
+
raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
|
817
|
+
pos = 0
|
818
|
+
out = TomlOutput(TomlNestedDict(), TomlFlags())
|
819
|
+
header: TomlKey = ()
|
820
|
+
parse_float = toml_make_safe_parse_float(parse_float)
|
821
|
+
|
822
|
+
# Parse one statement at a time (typically means one line in TOML source)
|
823
|
+
while True:
|
824
|
+
# 1. Skip line leading whitespace
|
825
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1127
826
|
|
827
|
+
# 2. Parse rules. Expect one of the following:
|
828
|
+
# - end of file
|
829
|
+
# - end of line
|
830
|
+
# - comment
|
831
|
+
# - key/value pair
|
832
|
+
# - append dict to list (and move to its namespace)
|
833
|
+
# - create dict (and move to its namespace)
|
834
|
+
# Skip trailing whitespace when applicable.
|
835
|
+
try:
|
836
|
+
char = src[pos]
|
837
|
+
except IndexError:
|
838
|
+
break
|
839
|
+
if char == '\n':
|
840
|
+
pos += 1
|
841
|
+
continue
|
842
|
+
if char in TOML_KEY_INITIAL_CHARS:
|
843
|
+
pos = toml_key_value_rule(src, pos, out, header, parse_float)
|
844
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
845
|
+
elif char == '[':
|
846
|
+
try:
|
847
|
+
second_char: ta.Optional[str] = src[pos + 1]
|
848
|
+
except IndexError:
|
849
|
+
second_char = None
|
850
|
+
out.flags.finalize_pending()
|
851
|
+
if second_char == '[':
|
852
|
+
pos, header = toml_create_list_rule(src, pos, out)
|
853
|
+
else:
|
854
|
+
pos, header = toml_create_dict_rule(src, pos, out)
|
855
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
856
|
+
elif char != '#':
|
857
|
+
raise toml_suffixed_err(src, pos, 'Invalid statement')
|
1128
858
|
|
1129
|
-
|
1130
|
-
|
1131
|
-
RUNNING = 1
|
1132
|
-
RESTARTING = 0
|
1133
|
-
SHUTDOWN = -1
|
859
|
+
# 3. Skip comment
|
860
|
+
pos = toml_skip_comment(src, pos)
|
1134
861
|
|
862
|
+
# 4. Expect end of line or end of file
|
863
|
+
try:
|
864
|
+
char = src[pos]
|
865
|
+
except IndexError:
|
866
|
+
break
|
867
|
+
if char != '\n':
|
868
|
+
raise toml_suffixed_err(
|
869
|
+
src, pos, 'Expected newline or end of document after a statement',
|
870
|
+
)
|
871
|
+
pos += 1
|
1135
872
|
|
1136
|
-
|
1137
|
-
# ../utils/collections.py
|
873
|
+
return out.data.dict
|
1138
874
|
|
1139
875
|
|
1140
|
-
class
|
1141
|
-
|
1142
|
-
@abc.abstractmethod
|
1143
|
-
def _by_key(self) -> ta.Mapping[K, V]:
|
1144
|
-
raise NotImplementedError
|
876
|
+
class TomlFlags:
|
877
|
+
"""Flags that map to parsed keys/namespaces."""
|
1145
878
|
|
1146
|
-
|
1147
|
-
|
879
|
+
# Marks an immutable namespace (inline array or inline table).
|
880
|
+
FROZEN = 0
|
881
|
+
# Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
|
882
|
+
EXPLICIT_NEST = 1
|
1148
883
|
|
1149
|
-
def
|
1150
|
-
|
884
|
+
def __init__(self) -> None:
|
885
|
+
self._flags: ta.Dict[str, dict] = {}
|
886
|
+
self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
|
1151
887
|
|
1152
|
-
def
|
1153
|
-
|
888
|
+
def add_pending(self, key: TomlKey, flag: int) -> None:
|
889
|
+
self._pending_flags.add((key, flag))
|
1154
890
|
|
1155
|
-
def
|
1156
|
-
|
891
|
+
def finalize_pending(self) -> None:
|
892
|
+
for key, flag in self._pending_flags:
|
893
|
+
self.set(key, flag, recursive=False)
|
894
|
+
self._pending_flags.clear()
|
1157
895
|
|
1158
|
-
def
|
1159
|
-
|
896
|
+
def unset_all(self, key: TomlKey) -> None:
|
897
|
+
cont = self._flags
|
898
|
+
for k in key[:-1]:
|
899
|
+
if k not in cont:
|
900
|
+
return
|
901
|
+
cont = cont[k]['nested']
|
902
|
+
cont.pop(key[-1], None)
|
1160
903
|
|
1161
|
-
def
|
1162
|
-
|
904
|
+
def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
|
905
|
+
cont = self._flags
|
906
|
+
key_parent, key_stem = key[:-1], key[-1]
|
907
|
+
for k in key_parent:
|
908
|
+
if k not in cont:
|
909
|
+
cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
910
|
+
cont = cont[k]['nested']
|
911
|
+
if key_stem not in cont:
|
912
|
+
cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
913
|
+
cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
|
1163
914
|
|
915
|
+
def is_(self, key: TomlKey, flag: int) -> bool:
|
916
|
+
if not key:
|
917
|
+
return False # document root has no flags
|
918
|
+
cont = self._flags
|
919
|
+
for k in key[:-1]:
|
920
|
+
if k not in cont:
|
921
|
+
return False
|
922
|
+
inner_cont = cont[k]
|
923
|
+
if flag in inner_cont['recursive_flags']:
|
924
|
+
return True
|
925
|
+
cont = inner_cont['nested']
|
926
|
+
key_stem = key[-1]
|
927
|
+
if key_stem in cont:
|
928
|
+
cont = cont[key_stem]
|
929
|
+
return flag in cont['flags'] or flag in cont['recursive_flags']
|
930
|
+
return False
|
1164
931
|
|
1165
|
-
class KeyedCollection(KeyedCollectionAccessors[K, V]):
|
1166
|
-
def __init__(self, items: ta.Iterable[V]) -> None:
|
1167
|
-
super().__init__()
|
1168
932
|
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
by_key[k] = v
|
1174
|
-
self.__by_key = by_key
|
933
|
+
class TomlNestedDict:
|
934
|
+
def __init__(self) -> None:
|
935
|
+
# The parsed content of the TOML document
|
936
|
+
self.dict: ta.Dict[str, ta.Any] = {}
|
1175
937
|
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
938
|
+
def get_or_create_nest(
|
939
|
+
self,
|
940
|
+
key: TomlKey,
|
941
|
+
*,
|
942
|
+
access_lists: bool = True,
|
943
|
+
) -> dict:
|
944
|
+
cont: ta.Any = self.dict
|
945
|
+
for k in key:
|
946
|
+
if k not in cont:
|
947
|
+
cont[k] = {}
|
948
|
+
cont = cont[k]
|
949
|
+
if access_lists and isinstance(cont, list):
|
950
|
+
cont = cont[-1]
|
951
|
+
if not isinstance(cont, dict):
|
952
|
+
raise KeyError('There is no nest behind this key')
|
953
|
+
return cont
|
1179
954
|
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
955
|
+
def append_nest_to_list(self, key: TomlKey) -> None:
|
956
|
+
cont = self.get_or_create_nest(key[:-1])
|
957
|
+
last_key = key[-1]
|
958
|
+
if last_key in cont:
|
959
|
+
list_ = cont[last_key]
|
960
|
+
if not isinstance(list_, list):
|
961
|
+
raise KeyError('An object other than list found behind this key')
|
962
|
+
list_.append({})
|
963
|
+
else:
|
964
|
+
cont[last_key] = [{}]
|
1183
965
|
|
1184
966
|
|
1185
|
-
|
1186
|
-
|
967
|
+
class TomlOutput(ta.NamedTuple):
|
968
|
+
data: TomlNestedDict
|
969
|
+
flags: TomlFlags
|
1187
970
|
|
1188
971
|
|
1189
|
-
def
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
if not tb:
|
1197
|
-
raise RuntimeError('No traceback')
|
972
|
+
def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
|
973
|
+
try:
|
974
|
+
while src[pos] in chars:
|
975
|
+
pos += 1
|
976
|
+
except IndexError:
|
977
|
+
pass
|
978
|
+
return pos
|
1198
979
|
|
1199
|
-
tbinfo = []
|
1200
|
-
while tb:
|
1201
|
-
tbinfo.append((
|
1202
|
-
tb.tb_frame.f_code.co_filename,
|
1203
|
-
tb.tb_frame.f_code.co_name,
|
1204
|
-
str(tb.tb_lineno),
|
1205
|
-
))
|
1206
|
-
tb = tb.tb_next
|
1207
980
|
|
1208
|
-
|
1209
|
-
|
981
|
+
def toml_skip_until(
|
982
|
+
src: str,
|
983
|
+
pos: TomlPos,
|
984
|
+
expect: str,
|
985
|
+
*,
|
986
|
+
error_on: ta.FrozenSet[str],
|
987
|
+
error_on_eof: bool,
|
988
|
+
) -> TomlPos:
|
989
|
+
try:
|
990
|
+
new_pos = src.index(expect, pos)
|
991
|
+
except ValueError:
|
992
|
+
new_pos = len(src)
|
993
|
+
if error_on_eof:
|
994
|
+
raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
|
1210
995
|
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
996
|
+
if not error_on.isdisjoint(src[pos:new_pos]):
|
997
|
+
while src[pos] not in error_on:
|
998
|
+
pos += 1
|
999
|
+
raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
|
1000
|
+
return new_pos
|
1214
1001
|
|
1215
1002
|
|
1216
|
-
|
1217
|
-
|
1003
|
+
def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
|
1004
|
+
try:
|
1005
|
+
char: ta.Optional[str] = src[pos]
|
1006
|
+
except IndexError:
|
1007
|
+
char = None
|
1008
|
+
if char == '#':
|
1009
|
+
return toml_skip_until(
|
1010
|
+
src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
|
1011
|
+
)
|
1012
|
+
return pos
|
1218
1013
|
|
1219
1014
|
|
1220
|
-
def
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1015
|
+
def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
|
1016
|
+
while True:
|
1017
|
+
pos_before_skip = pos
|
1018
|
+
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
1019
|
+
pos = toml_skip_comment(src, pos)
|
1020
|
+
if pos == pos_before_skip:
|
1021
|
+
return pos
|
1226
1022
|
|
1227
1023
|
|
1228
|
-
def
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1024
|
+
def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
|
1025
|
+
pos += 1 # Skip "["
|
1026
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1027
|
+
pos, key = toml_parse_key(src, pos)
|
1232
1028
|
|
1029
|
+
if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
|
1030
|
+
raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
|
1031
|
+
out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
|
1032
|
+
try:
|
1033
|
+
out.data.get_or_create_nest(key)
|
1034
|
+
except KeyError:
|
1035
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
1233
1036
|
|
1234
|
-
|
1235
|
-
|
1037
|
+
if not src.startswith(']', pos):
|
1038
|
+
raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
|
1039
|
+
return pos + 1, key
|
1236
1040
|
|
1237
|
-
path = ['/bin', '/usr/bin', '/usr/local/bin']
|
1238
|
-
if 'PATH' in os.environ:
|
1239
|
-
p = os.environ['PATH']
|
1240
|
-
if p:
|
1241
|
-
path = p.split(os.pathsep)
|
1242
|
-
return path
|
1243
1041
|
|
1042
|
+
def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
|
1043
|
+
pos += 2 # Skip "[["
|
1044
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1045
|
+
pos, key = toml_parse_key(src, pos)
|
1244
1046
|
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1047
|
+
if out.flags.is_(key, TomlFlags.FROZEN):
|
1048
|
+
raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
|
1049
|
+
# Free the namespace now that it points to another empty list item...
|
1050
|
+
out.flags.unset_all(key)
|
1051
|
+
# ...but this key precisely is still prohibited from table declaration
|
1052
|
+
out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
|
1053
|
+
try:
|
1054
|
+
out.data.append_nest_to_list(key)
|
1055
|
+
except KeyError:
|
1056
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
1250
1057
|
|
1058
|
+
if not src.startswith(']]', pos):
|
1059
|
+
raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
|
1060
|
+
return pos + 2, key
|
1251
1061
|
|
1252
|
-
def check_path_with_existing_dir(v: str) -> str:
|
1253
|
-
nv = os.path.expanduser(v)
|
1254
|
-
dir = os.path.dirname(nv) # noqa
|
1255
|
-
if not dir:
|
1256
|
-
# relative pathname with no directory component
|
1257
|
-
return nv
|
1258
|
-
if os.path.isdir(dir):
|
1259
|
-
return nv
|
1260
|
-
raise ValueError(f'The directory named as part of the path {v} does not exist')
|
1261
1062
|
|
1063
|
+
def toml_key_value_rule(
|
1064
|
+
src: str,
|
1065
|
+
pos: TomlPos,
|
1066
|
+
out: TomlOutput,
|
1067
|
+
header: TomlKey,
|
1068
|
+
parse_float: TomlParseFloat,
|
1069
|
+
) -> TomlPos:
|
1070
|
+
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
1071
|
+
key_parent, key_stem = key[:-1], key[-1]
|
1072
|
+
abs_key_parent = header + key_parent
|
1262
1073
|
|
1263
|
-
|
1264
|
-
|
1074
|
+
relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
|
1075
|
+
for cont_key in relative_path_cont_keys:
|
1076
|
+
# Check that dotted key syntax does not redefine an existing table
|
1077
|
+
if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
|
1078
|
+
raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
|
1079
|
+
# Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
|
1080
|
+
# table sections.
|
1081
|
+
out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
|
1265
1082
|
|
1083
|
+
if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
|
1084
|
+
raise toml_suffixed_err(
|
1085
|
+
src,
|
1086
|
+
pos,
|
1087
|
+
f'Cannot mutate immutable namespace {abs_key_parent}',
|
1088
|
+
)
|
1266
1089
|
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1090
|
+
try:
|
1091
|
+
nest = out.data.get_or_create_nest(abs_key_parent)
|
1092
|
+
except KeyError:
|
1093
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
1094
|
+
if key_stem in nest:
|
1095
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
|
1096
|
+
# Mark inline table and array namespaces recursively immutable
|
1097
|
+
if isinstance(value, (dict, list)):
|
1098
|
+
out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
|
1099
|
+
nest[key_stem] = value
|
1100
|
+
return pos
|
1270
1101
|
|
1271
|
-
|
1272
|
-
|
1102
|
+
|
1103
|
+
def toml_parse_key_value_pair(
|
1104
|
+
src: str,
|
1105
|
+
pos: TomlPos,
|
1106
|
+
parse_float: TomlParseFloat,
|
1107
|
+
) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
|
1108
|
+
pos, key = toml_parse_key(src, pos)
|
1109
|
+
try:
|
1110
|
+
char: ta.Optional[str] = src[pos]
|
1111
|
+
except IndexError:
|
1112
|
+
char = None
|
1113
|
+
if char != '=':
|
1114
|
+
raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
|
1115
|
+
pos += 1
|
1116
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1117
|
+
pos, value = toml_parse_value(src, pos, parse_float)
|
1118
|
+
return pos, key, value
|
1273
1119
|
|
1274
1120
|
|
1275
|
-
|
1276
|
-
|
1121
|
+
def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
|
1122
|
+
pos, key_part = toml_parse_key_part(src, pos)
|
1123
|
+
key: TomlKey = (key_part,)
|
1124
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1125
|
+
while True:
|
1126
|
+
try:
|
1127
|
+
char: ta.Optional[str] = src[pos]
|
1128
|
+
except IndexError:
|
1129
|
+
char = None
|
1130
|
+
if char != '.':
|
1131
|
+
return pos, key
|
1132
|
+
pos += 1
|
1133
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1134
|
+
pos, key_part = toml_parse_key_part(src, pos)
|
1135
|
+
key += (key_part,)
|
1136
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1277
1137
|
|
1278
1138
|
|
1279
|
-
|
1139
|
+
def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1140
|
+
try:
|
1141
|
+
char: ta.Optional[str] = src[pos]
|
1142
|
+
except IndexError:
|
1143
|
+
char = None
|
1144
|
+
if char in TOML_BARE_KEY_CHARS:
|
1145
|
+
start_pos = pos
|
1146
|
+
pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
|
1147
|
+
return pos, src[start_pos:pos]
|
1148
|
+
if char == "'":
|
1149
|
+
return toml_parse_literal_str(src, pos)
|
1150
|
+
if char == '"':
|
1151
|
+
return toml_parse_one_line_basic_str(src, pos)
|
1152
|
+
raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
|
1280
1153
|
|
1281
1154
|
|
1282
|
-
|
1283
|
-
|
1155
|
+
def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1156
|
+
pos += 1
|
1157
|
+
return toml_parse_basic_str(src, pos, multiline=False)
|
1284
1158
|
|
1285
1159
|
|
1286
|
-
def
|
1287
|
-
|
1288
|
-
|
1160
|
+
def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
|
1161
|
+
pos += 1
|
1162
|
+
array: list = []
|
1289
1163
|
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1164
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
1165
|
+
if src.startswith(']', pos):
|
1166
|
+
return pos + 1, array
|
1167
|
+
while True:
|
1168
|
+
pos, val = toml_parse_value(src, pos, parse_float)
|
1169
|
+
array.append(val)
|
1170
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
1294
1171
|
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1172
|
+
c = src[pos:pos + 1]
|
1173
|
+
if c == ']':
|
1174
|
+
return pos + 1, array
|
1175
|
+
if c != ',':
|
1176
|
+
raise toml_suffixed_err(src, pos, 'Unclosed array')
|
1177
|
+
pos += 1
|
1298
1178
|
|
1299
|
-
|
1300
|
-
|
1179
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
1180
|
+
if src.startswith(']', pos):
|
1181
|
+
return pos + 1, array
|
1301
1182
|
|
1302
|
-
return num
|
1303
1183
|
|
1184
|
+
def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
|
1185
|
+
pos += 1
|
1186
|
+
nested_dict = TomlNestedDict()
|
1187
|
+
flags = TomlFlags()
|
1304
1188
|
|
1305
|
-
|
1306
|
-
if (
|
1307
|
-
return
|
1308
|
-
|
1189
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1190
|
+
if src.startswith('}', pos):
|
1191
|
+
return pos + 1, nested_dict.dict
|
1192
|
+
while True:
|
1193
|
+
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
1194
|
+
key_parent, key_stem = key[:-1], key[-1]
|
1195
|
+
if flags.is_(key, TomlFlags.FROZEN):
|
1196
|
+
raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
|
1197
|
+
try:
|
1198
|
+
nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
|
1199
|
+
except KeyError:
|
1200
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
1201
|
+
if key_stem in nest:
|
1202
|
+
raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
|
1203
|
+
nest[key_stem] = value
|
1204
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1205
|
+
c = src[pos:pos + 1]
|
1206
|
+
if c == '}':
|
1207
|
+
return pos + 1, nested_dict.dict
|
1208
|
+
if c != ',':
|
1209
|
+
raise toml_suffixed_err(src, pos, 'Unclosed inline table')
|
1210
|
+
if isinstance(value, (dict, list)):
|
1211
|
+
flags.set(key, TomlFlags.FROZEN, recursive=True)
|
1212
|
+
pos += 1
|
1213
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1309
1214
|
|
1310
1215
|
|
1311
|
-
|
1216
|
+
def toml_parse_basic_str_escape(
|
1217
|
+
src: str,
|
1218
|
+
pos: TomlPos,
|
1219
|
+
*,
|
1220
|
+
multiline: bool = False,
|
1221
|
+
) -> ta.Tuple[TomlPos, str]:
|
1222
|
+
escape_id = src[pos:pos + 2]
|
1223
|
+
pos += 2
|
1224
|
+
if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
|
1225
|
+
# Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
|
1226
|
+
# newline.
|
1227
|
+
if escape_id != '\\\n':
|
1228
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1229
|
+
try:
|
1230
|
+
char = src[pos]
|
1231
|
+
except IndexError:
|
1232
|
+
return pos, ''
|
1233
|
+
if char != '\n':
|
1234
|
+
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
|
1235
|
+
pos += 1
|
1236
|
+
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
1237
|
+
return pos, ''
|
1238
|
+
if escape_id == '\\u':
|
1239
|
+
return toml_parse_hex_char(src, pos, 4)
|
1240
|
+
if escape_id == '\\U':
|
1241
|
+
return toml_parse_hex_char(src, pos, 8)
|
1242
|
+
try:
|
1243
|
+
return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
|
1244
|
+
except KeyError:
|
1245
|
+
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
|
1312
1246
|
|
1313
1247
|
|
1314
|
-
|
1315
|
-
|
1316
|
-
super().__init__()
|
1248
|
+
def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1249
|
+
return toml_parse_basic_str_escape(src, pos, multiline=True)
|
1317
1250
|
|
1318
|
-
self._signals_recvd: ta.List[int] = []
|
1319
1251
|
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1252
|
+
def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
|
1253
|
+
hex_str = src[pos:pos + hex_len]
|
1254
|
+
if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
|
1255
|
+
raise toml_suffixed_err(src, pos, 'Invalid hex value')
|
1256
|
+
pos += hex_len
|
1257
|
+
hex_int = int(hex_str, 16)
|
1258
|
+
if not toml_is_unicode_scalar_value(hex_int):
|
1259
|
+
raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
|
1260
|
+
return pos, chr(hex_int)
|
1323
1261
|
|
1324
|
-
def install(self, *sigs: int) -> None:
|
1325
|
-
for sig in sigs:
|
1326
|
-
signal.signal(sig, self.receive)
|
1327
1262
|
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1263
|
+
def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1264
|
+
pos += 1 # Skip starting apostrophe
|
1265
|
+
start_pos = pos
|
1266
|
+
pos = toml_skip_until(
|
1267
|
+
src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
|
1268
|
+
)
|
1269
|
+
return pos + 1, src[start_pos:pos] # Skip ending apostrophe
|
1334
1270
|
|
1335
1271
|
|
1336
|
-
|
1337
|
-
|
1272
|
+
def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
|
1273
|
+
pos += 3
|
1274
|
+
if src.startswith('\n', pos):
|
1275
|
+
pos += 1
|
1338
1276
|
|
1277
|
+
if literal:
|
1278
|
+
delim = "'"
|
1279
|
+
end_pos = toml_skip_until(
|
1280
|
+
src,
|
1281
|
+
pos,
|
1282
|
+
"'''",
|
1283
|
+
error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
|
1284
|
+
error_on_eof=True,
|
1285
|
+
)
|
1286
|
+
result = src[pos:end_pos]
|
1287
|
+
pos = end_pos + 3
|
1288
|
+
else:
|
1289
|
+
delim = '"'
|
1290
|
+
pos, result = toml_parse_basic_str(src, pos, multiline=True)
|
1339
1291
|
|
1340
|
-
|
1292
|
+
# Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
|
1293
|
+
if not src.startswith(delim, pos):
|
1294
|
+
return pos, result
|
1295
|
+
pos += 1
|
1296
|
+
if not src.startswith(delim, pos):
|
1297
|
+
return pos, result + delim
|
1298
|
+
pos += 1
|
1299
|
+
return pos, result + (delim * 2)
|
1341
1300
|
|
1342
1301
|
|
1343
|
-
def
|
1344
|
-
if
|
1345
|
-
|
1302
|
+
def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
|
1303
|
+
if multiline:
|
1304
|
+
error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
1305
|
+
parse_escapes = toml_parse_basic_str_escape_multiline
|
1346
1306
|
else:
|
1347
|
-
|
1348
|
-
|
1307
|
+
error_on = TOML_ILLEGAL_BASIC_STR_CHARS
|
1308
|
+
parse_escapes = toml_parse_basic_str_escape
|
1309
|
+
result = ''
|
1310
|
+
start_pos = pos
|
1311
|
+
while True:
|
1312
|
+
try:
|
1313
|
+
char = src[pos]
|
1314
|
+
except IndexError:
|
1315
|
+
raise toml_suffixed_err(src, pos, 'Unterminated string') from None
|
1316
|
+
if char == '"':
|
1317
|
+
if not multiline:
|
1318
|
+
return pos + 1, result + src[start_pos:pos]
|
1319
|
+
if src.startswith('"""', pos):
|
1320
|
+
return pos + 3, result + src[start_pos:pos]
|
1321
|
+
pos += 1
|
1322
|
+
continue
|
1323
|
+
if char == '\\':
|
1324
|
+
result += src[start_pos:pos]
|
1325
|
+
pos, parsed_escape = parse_escapes(src, pos)
|
1326
|
+
result += parsed_escape
|
1327
|
+
start_pos = pos
|
1328
|
+
continue
|
1329
|
+
if char in error_on:
|
1330
|
+
raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
|
1331
|
+
pos += 1
|
1349
1332
|
|
1350
|
-
@ta.overload
|
1351
|
-
def find_prefix_at_end(haystack: str, needle: str) -> int:
|
1352
|
-
...
|
1353
1333
|
|
1334
|
+
def toml_parse_value( # noqa: C901
|
1335
|
+
src: str,
|
1336
|
+
pos: TomlPos,
|
1337
|
+
parse_float: TomlParseFloat,
|
1338
|
+
) -> ta.Tuple[TomlPos, ta.Any]:
|
1339
|
+
try:
|
1340
|
+
char: ta.Optional[str] = src[pos]
|
1341
|
+
except IndexError:
|
1342
|
+
char = None
|
1354
1343
|
|
1355
|
-
|
1356
|
-
def find_prefix_at_end(haystack: bytes, needle: bytes) -> int:
|
1357
|
-
...
|
1344
|
+
# IMPORTANT: order conditions based on speed of checking and likelihood
|
1358
1345
|
|
1346
|
+
# Basic strings
|
1347
|
+
if char == '"':
|
1348
|
+
if src.startswith('"""', pos):
|
1349
|
+
return toml_parse_multiline_str(src, pos, literal=False)
|
1350
|
+
return toml_parse_one_line_basic_str(src, pos)
|
1359
1351
|
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1352
|
+
# Literal strings
|
1353
|
+
if char == "'":
|
1354
|
+
if src.startswith("'''", pos):
|
1355
|
+
return toml_parse_multiline_str(src, pos, literal=True)
|
1356
|
+
return toml_parse_literal_str(src, pos)
|
1365
1357
|
|
1358
|
+
# Booleans
|
1359
|
+
if char == 't':
|
1360
|
+
if src.startswith('true', pos):
|
1361
|
+
return pos + 4, True
|
1362
|
+
if char == 'f':
|
1363
|
+
if src.startswith('false', pos):
|
1364
|
+
return pos + 5, False
|
1366
1365
|
|
1367
|
-
|
1366
|
+
# Arrays
|
1367
|
+
if char == '[':
|
1368
|
+
return toml_parse_array(src, pos, parse_float)
|
1368
1369
|
|
1370
|
+
# Inline tables
|
1371
|
+
if char == '{':
|
1372
|
+
return toml_parse_inline_table(src, pos, parse_float)
|
1369
1373
|
|
1370
|
-
|
1371
|
-
|
1374
|
+
# Dates and times
|
1375
|
+
datetime_match = TOML_RE_DATETIME.match(src, pos)
|
1376
|
+
if datetime_match:
|
1377
|
+
try:
|
1378
|
+
datetime_obj = toml_match_to_datetime(datetime_match)
|
1379
|
+
except ValueError as e:
|
1380
|
+
raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
|
1381
|
+
return datetime_match.end(), datetime_obj
|
1382
|
+
localtime_match = TOML_RE_LOCALTIME.match(src, pos)
|
1383
|
+
if localtime_match:
|
1384
|
+
return localtime_match.end(), toml_match_to_localtime(localtime_match)
|
1372
1385
|
|
1386
|
+
# Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
|
1387
|
+
# located after handling of dates and times.
|
1388
|
+
number_match = TOML_RE_NUMBER.match(src, pos)
|
1389
|
+
if number_match:
|
1390
|
+
return number_match.end(), toml_match_to_number(number_match, parse_float)
|
1373
1391
|
|
1374
|
-
|
1375
|
-
|
1392
|
+
# Special floats
|
1393
|
+
first_three = src[pos:pos + 3]
|
1394
|
+
if first_three in {'inf', 'nan'}:
|
1395
|
+
return pos + 3, parse_float(first_three)
|
1396
|
+
first_four = src[pos:pos + 4]
|
1397
|
+
if first_four in {'-inf', '+inf', '-nan', '+nan'}:
|
1398
|
+
return pos + 4, parse_float(first_four)
|
1376
1399
|
|
1377
|
-
|
1378
|
-
show = 1
|
1379
|
-
i = 0
|
1380
|
-
l = len(s)
|
1381
|
-
while i < l:
|
1382
|
-
if show == 0 and s[i:i + 1] in ANSI_TERMINATORS:
|
1383
|
-
show = 1
|
1384
|
-
elif show:
|
1385
|
-
n = s.find(ANSI_ESCAPE_BEGIN, i)
|
1386
|
-
if n == -1:
|
1387
|
-
return result + s[i:]
|
1388
|
-
else:
|
1389
|
-
result = result + s[i:n]
|
1390
|
-
i = n
|
1391
|
-
show = 0
|
1392
|
-
i += 1
|
1393
|
-
return result
|
1400
|
+
raise toml_suffixed_err(src, pos, 'Invalid value')
|
1394
1401
|
|
1395
1402
|
|
1396
|
-
|
1403
|
+
def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
|
1404
|
+
"""Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
|
1397
1405
|
|
1406
|
+
def coord_repr(src: str, pos: TomlPos) -> str:
|
1407
|
+
if pos >= len(src):
|
1408
|
+
return 'end of document'
|
1409
|
+
line = src.count('\n', 0, pos) + 1
|
1410
|
+
if line == 1:
|
1411
|
+
column = pos + 1
|
1412
|
+
else:
|
1413
|
+
column = pos - src.rindex('\n', 0, pos)
|
1414
|
+
return f'line {line}, column {column}'
|
1398
1415
|
|
1399
|
-
|
1400
|
-
# d is a dictionary of suffixes to integer multipliers. If no suffixes match, default is the multiplier. Matches are
|
1401
|
-
# case insensitive. Return values are in the fundamental unit.
|
1402
|
-
def __init__(self, d, default=1):
|
1403
|
-
super().__init__()
|
1404
|
-
self._d = d
|
1405
|
-
self._default = default
|
1406
|
-
# all keys must be the same size
|
1407
|
-
self._keysz = None
|
1408
|
-
for k in d:
|
1409
|
-
if self._keysz is None:
|
1410
|
-
self._keysz = len(k)
|
1411
|
-
elif self._keysz != len(k): # type: ignore
|
1412
|
-
raise ValueError(k)
|
1416
|
+
return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
|
1413
1417
|
|
1414
|
-
def __call__(self, v: ta.Union[str, int]) -> int:
|
1415
|
-
if isinstance(v, int):
|
1416
|
-
return v
|
1417
|
-
v = v.lower()
|
1418
|
-
for s, m in self._d.items():
|
1419
|
-
if v[-self._keysz:] == s: # type: ignore
|
1420
|
-
return int(v[:-self._keysz]) * m # type: ignore
|
1421
|
-
return int(v) * self._default
|
1422
1418
|
|
1419
|
+
def toml_is_unicode_scalar_value(codepoint: int) -> bool:
|
1420
|
+
return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
|
1423
1421
|
|
1424
|
-
parse_bytes_size = SuffixMultiplier({
|
1425
|
-
'kb': 1024,
|
1426
|
-
'mb': 1024 * 1024,
|
1427
|
-
'gb': 1024 * 1024 * 1024,
|
1428
|
-
})
|
1429
1422
|
|
1423
|
+
def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
|
1424
|
+
"""A decorator to make `parse_float` safe.
|
1430
1425
|
|
1431
|
-
|
1426
|
+
`parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
|
1427
|
+
thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
|
1428
|
+
"""
|
1429
|
+
# The default `float` callable never returns illegal types. Optimize it.
|
1430
|
+
if parse_float is float:
|
1431
|
+
return float
|
1432
1432
|
|
1433
|
+
def safe_parse_float(float_str: str) -> ta.Any:
|
1434
|
+
float_value = parse_float(float_str)
|
1435
|
+
if isinstance(float_value, (dict, list)):
|
1436
|
+
raise ValueError('parse_float must not return dicts or lists') # noqa
|
1437
|
+
return float_value
|
1433
1438
|
|
1434
|
-
|
1435
|
-
if isinstance(arg, int):
|
1436
|
-
return arg
|
1437
|
-
try:
|
1438
|
-
return int(arg, 8)
|
1439
|
-
except (TypeError, ValueError):
|
1440
|
-
raise ValueError(f'{arg} can not be converted to an octal type') # noqa
|
1439
|
+
return safe_parse_float
|
1441
1440
|
|
1442
1441
|
|
1443
1442
|
########################################
|
@@ -5949,30 +5948,6 @@ def build_config_named_children(
|
|
5949
5948
|
return lst
|
5950
5949
|
|
5951
5950
|
|
5952
|
-
##
|
5953
|
-
|
5954
|
-
|
5955
|
-
def render_ini_config(
|
5956
|
-
settings_by_section: IniConfigSectionSettingsMap,
|
5957
|
-
) -> str:
|
5958
|
-
out = io.StringIO()
|
5959
|
-
|
5960
|
-
for i, (section, settings) in enumerate(settings_by_section.items()):
|
5961
|
-
if i:
|
5962
|
-
out.write('\n')
|
5963
|
-
|
5964
|
-
out.write(f'[{section}]\n')
|
5965
|
-
|
5966
|
-
for k, v in settings.items():
|
5967
|
-
if isinstance(v, str):
|
5968
|
-
out.write(f'{k}={v}\n')
|
5969
|
-
else:
|
5970
|
-
for vv in v:
|
5971
|
-
out.write(f'{k}={vv}\n')
|
5972
|
-
|
5973
|
-
return out.getvalue()
|
5974
|
-
|
5975
|
-
|
5976
5951
|
########################################
|
5977
5952
|
# ../pipes.py
|
5978
5953
|
|
@@ -6481,6 +6456,12 @@ class ServerConfig:
|
|
6481
6456
|
# TODO: implement - make sure to accept broken symlinks
|
6482
6457
|
group_config_dirs: ta.Optional[ta.Sequence[str]] = None
|
6483
6458
|
|
6459
|
+
#
|
6460
|
+
|
6461
|
+
http_port: ta.Optional[int] = None
|
6462
|
+
|
6463
|
+
#
|
6464
|
+
|
6484
6465
|
@classmethod
|
6485
6466
|
def new(
|
6486
6467
|
cls,
|
@@ -8354,7 +8335,7 @@ class HttpServer(HasDispatchers):
|
|
8354
8335
|
def __init__(
|
8355
8336
|
self,
|
8356
8337
|
handler: Handler,
|
8357
|
-
addr: Address = Address(('localhost', 8000)),
|
8338
|
+
addr: Address, # = Address(('localhost', 8000)),
|
8358
8339
|
*,
|
8359
8340
|
exit_stack: contextlib.ExitStack,
|
8360
8341
|
) -> None:
|
@@ -9617,16 +9598,19 @@ def bind_server(
|
|
9617
9598
|
|
9618
9599
|
#
|
9619
9600
|
|
9620
|
-
|
9621
|
-
|
9601
|
+
if config.http_port is not None:
|
9602
|
+
def _provide_http_handler(s: SupervisorHttpHandler) -> HttpServer.Handler:
|
9603
|
+
return HttpServer.Handler(s.handle)
|
9622
9604
|
|
9623
|
-
|
9624
|
-
|
9625
|
-
|
9605
|
+
lst.extend([
|
9606
|
+
inj.bind(HttpServer, singleton=True, eager=True),
|
9607
|
+
inj.bind(HasDispatchers, array=True, to_key=HttpServer),
|
9626
9608
|
|
9627
|
-
|
9628
|
-
|
9629
|
-
|
9609
|
+
inj.bind(HttpServer.Address(('localhost', config.http_port))),
|
9610
|
+
|
9611
|
+
inj.bind(SupervisorHttpHandler, singleton=True),
|
9612
|
+
inj.bind(_provide_http_handler),
|
9613
|
+
])
|
9630
9614
|
|
9631
9615
|
#
|
9632
9616
|
|