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.
@@ -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
- # ../../../omdev/toml/parser.py
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
- _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]*)?'
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
- Raises ValueError if the match does not correspond to a valid date or datetime.
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
- @functools.lru_cache() # noqa
273
- def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
274
- sign = 1 if sign_str == '+' else -1
275
- return datetime.timezone(
276
- datetime.timedelta(
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
- def toml_match_to_localtime(match: re.Match) -> datetime.time:
284
- hour_str, minute_str, sec_str, micros_str = match.groups()
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 toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
290
- if match.group('floatpart'):
291
- return parse_float(match.group())
292
- return int(match.group(), 0)
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
- TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
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
- # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
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
- TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
303
- TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
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
- TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
219
+ if current_uid != 0:
220
+ return "Can't drop privilege as nonroot user"
306
221
 
307
- TOML_WS = frozenset(' \t')
308
- TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
309
- TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
310
- TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
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
- TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
314
- {
315
- '\\b': '\u0008', # backspace
316
- '\\t': '\u0009', # tab
317
- '\\n': '\u000A', # linefeed
318
- '\\f': '\u000C', # form feed
319
- '\\r': '\u000D', # carriage return
320
- '\\"': '\u0022', # quote
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
- class TomlDecodeError(ValueError):
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
- # The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
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
- # 2. Parse rules. Expect one of the following:
359
- # - end of file
360
- # - end of line
361
- # - comment
362
- # - key/value pair
363
- # - append dict to list (and move to its namespace)
364
- # - create dict (and move to its namespace)
365
- # Skip trailing whitespace when applicable.
366
- try:
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
- # 3. Skip comment
391
- pos = toml_skip_comment(src, pos)
263
+ @property
264
+ def stopped(self) -> bool:
265
+ return self in STOPPED_STATES
392
266
 
393
- # 4. Expect end of line or end of file
394
- try:
395
- char = src[pos]
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
- return out.data.dict
271
+ @property
272
+ def signalable(self) -> bool:
273
+ return self in SIGNALABLE_STATES
405
274
 
406
275
 
407
- class TomlFlags:
408
- """Flags that map to parsed keys/namespaces."""
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
- # Marks an immutable namespace (inline array or inline table).
411
- FROZEN = 0
412
- # Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
413
- EXPLICIT_NEST = 1
287
+ STOPPED_STATES = (
288
+ ProcessState.STOPPED,
289
+ ProcessState.EXITED,
290
+ ProcessState.FATAL,
291
+ ProcessState.UNKNOWN,
292
+ )
414
293
 
415
- def __init__(self) -> None:
416
- self._flags: ta.Dict[str, dict] = {}
417
- self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
294
+ RUNNING_STATES = (
295
+ ProcessState.RUNNING,
296
+ ProcessState.BACKOFF,
297
+ ProcessState.STARTING,
298
+ )
418
299
 
419
- def add_pending(self, key: TomlKey, flag: int) -> None:
420
- self._pending_flags.add((key, flag))
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
- def unset_all(self, key: TomlKey) -> None:
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
- def is_(self, key: TomlKey, flag: int) -> bool:
447
- if not key:
448
- return False # document root has no flags
449
- cont = self._flags
450
- for k in key[:-1]:
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
- class TomlNestedDict:
465
- def __init__(self) -> None:
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
- def append_nest_to_list(self, key: TomlKey) -> None:
487
- cont = self.get_or_create_nest(key[:-1])
488
- last_key = key[-1]
489
- if last_key in cont:
490
- list_ = cont[last_key]
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
- class TomlOutput(ta.NamedTuple):
499
- data: TomlNestedDict
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 toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
504
- try:
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 toml_skip_until(
513
- src: str,
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
- def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
535
- try:
536
- char: ta.Optional[str] = src[pos]
537
- except IndexError:
538
- char = None
539
- if char == '#':
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
- def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
547
- while True:
548
- pos_before_skip = pos
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
- def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
556
- pos += 1 # Skip "["
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
- if not src.startswith(']', pos):
569
- raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
570
- return pos + 1, key
571
-
572
-
573
- def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
574
- pos += 2 # Skip "[["
575
- pos = toml_skip_chars(src, pos, TOML_WS)
576
- pos, key = toml_parse_key(src, pos)
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
- if out.flags.is_(key, TomlFlags.FROZEN):
579
- raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
580
- # Free the namespace now that it points to another empty list item...
581
- out.flags.unset_all(key)
582
- # ...but this key precisely is still prohibited from table declaration
583
- out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
584
- try:
585
- out.data.append_nest_to_list(key)
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
- if not src.startswith(']]', pos):
590
- raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
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
- relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
606
- for cont_key in relative_path_cont_keys:
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
- nest = out.data.get_or_create_nest(abs_key_parent)
623
- except KeyError:
624
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
625
- if key_stem in nest:
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 toml_parse_key_value_pair(
635
- src: str,
636
- pos: TomlPos,
637
- parse_float: TomlParseFloat,
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 toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
653
- pos, key_part = toml_parse_key_part(src, pos)
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
- pos = toml_skip_comments_and_array_ws(src, pos)
696
- if src.startswith(']', pos):
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
- pos = toml_skip_comments_and_array_ws(src, pos)
711
- if src.startswith(']', pos):
712
- return pos + 1, array
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
- pos = toml_skip_chars(src, pos, TOML_WS)
721
- if src.startswith('}', pos):
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
- def toml_parse_basic_str_escape(
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
- def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
780
- return toml_parse_basic_str_escape(src, pos, multiline=True)
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 toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
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
- char: ta.Optional[str] = src[pos]
872
- except IndexError:
873
- char = None
469
+ num = int(value)
874
470
 
875
- # IMPORTANT: order conditions based on speed of checking and likelihood
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
- # Basic strings
878
- if char == '"':
879
- if src.startswith('"""', pos):
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
- # Literal strings
884
- if char == "'":
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
- # Booleans
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
- # Inline tables
902
- if char == '{':
903
- return toml_parse_inline_table(src, pos, parse_float)
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
- # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
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
- raise toml_suffixed_err(src, pos, 'Invalid value')
495
+ class SignalReceiver:
496
+ def __init__(self) -> None:
497
+ super().__init__()
932
498
 
499
+ self._signals_recvd: ta.List[int] = []
933
500
 
934
- def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
935
- """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
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 coord_repr(src: str, pos: TomlPos) -> str:
938
- if pos >= len(src):
939
- return 'end of document'
940
- line = src.count('\n', 0, pos) + 1
941
- if line == 1:
942
- column = pos + 1
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
- column = pos - src.rindex('\n', 0, pos)
945
- return f'line {line}, column {column}'
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
- `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
958
- thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
959
- """
960
- # The default `float` callable never returns illegal types. Optimize it.
961
- if parse_float is float:
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
- return safe_parse_float
531
+ @ta.overload
532
+ def find_prefix_at_end(haystack: str, needle: str) -> int:
533
+ ...
971
534
 
972
535
 
973
- ########################################
974
- # ../exceptions.py
536
+ @ta.overload
537
+ def find_prefix_at_end(haystack: bytes, needle: bytes) -> int:
538
+ ...
975
539
 
976
540
 
977
- class ProcessError(Exception):
978
- """Specialized exceptions used when attempting to start a process."""
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
- class BadCommandError(ProcessError):
982
- """Indicates the command could not be parsed properly."""
548
+ ##
983
549
 
984
550
 
985
- class NotExecutableError(ProcessError):
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
- class NotFoundError(ProcessError):
993
- """Indicates that the filespec cannot be executed because it could not be found."""
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 drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
1008
- """
1009
- Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup and
1010
- when spawning subprocesses. Returns None on success or a string error message if privileges could not be dropped.
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
- # get uid for user, which can be a number or username
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
- uid = int(user)
1019
- except ValueError:
1020
- try:
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
- if current_uid == uid:
1034
- # do nothing and return successfully if the uid is already the current one. this allows a supervisord running as
1035
- # an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in it.
1036
- return None
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
- gid = pwrec[3]
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
- try:
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
- os.setuid(uid)
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
- # ../states.py
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
- class ProcessState(enum.IntEnum):
1073
- STOPPED = 0
1074
- STARTING = 10
1075
- RUNNING = 20
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
- @property
1087
- def running(self) -> bool:
1088
- return self in RUNNING_STATES
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
- # http://supervisord.org/subprocess.html
1096
- STATE_TRANSITIONS = {
1097
- ProcessState.STOPPED: (ProcessState.STARTING,),
1098
- ProcessState.STARTING: (ProcessState.RUNNING, ProcessState.BACKOFF, ProcessState.STOPPING),
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
- STOPPED_STATES = (
1107
- ProcessState.STOPPED,
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
- RUNNING_STATES = (
1114
- ProcessState.RUNNING,
1115
- ProcessState.BACKOFF,
1116
- ProcessState.STARTING,
1117
- )
774
+ TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
1118
775
 
1119
- SIGNALABLE_STATES = (
1120
- ProcessState.RUNNING,
1121
- ProcessState.STARTING,
1122
- ProcessState.STOPPING,
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
- class SupervisorState(enum.IntEnum):
1130
- FATAL = 2
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 KeyedCollectionAccessors(abc.ABC, ta.Generic[K, V]):
1141
- @property
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
- def __iter__(self) -> ta.Iterator[V]:
1147
- return iter(self._by_key.values())
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 __len__(self) -> int:
1150
- return len(self._by_key)
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 __contains__(self, key: K) -> bool:
1153
- return key in self._by_key
888
+ def add_pending(self, key: TomlKey, flag: int) -> None:
889
+ self._pending_flags.add((key, flag))
1154
890
 
1155
- def __getitem__(self, key: K) -> V:
1156
- return self._by_key[key]
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 get(self, key: K, default: ta.Optional[V] = None) -> ta.Optional[V]:
1159
- return self._by_key.get(key, default)
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 items(self) -> ta.Iterator[ta.Tuple[K, V]]:
1162
- return iter(self._by_key.items())
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
- by_key: ta.Dict[K, V] = {}
1170
- for v in items:
1171
- if (k := self._key(v)) in by_key:
1172
- raise KeyError(f'key {k} of {v} already registered by {by_key[k]}')
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
- @property
1177
- def _by_key(self) -> ta.Mapping[K, V]:
1178
- return self.__by_key
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
- @abc.abstractmethod
1181
- def _key(self, v: V) -> K:
1182
- raise NotImplementedError
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
- # ../utils/diag.py
967
+ class TomlOutput(ta.NamedTuple):
968
+ data: TomlNestedDict
969
+ flags: TomlFlags
1187
970
 
1188
971
 
1189
- def compact_traceback() -> ta.Tuple[
1190
- ta.Tuple[str, str, int],
1191
- ta.Type[BaseException],
1192
- BaseException,
1193
- types.TracebackType,
1194
- ]:
1195
- t, v, tb = sys.exc_info()
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
- # just to be safe
1209
- del tb
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
- file, function, line = tbinfo[-1]
1212
- info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) # noqa
1213
- return (file, function, line), t, v, info # type: ignore
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
- # ../utils/fs.py
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 try_unlink(path: str) -> bool:
1221
- try:
1222
- os.unlink(path)
1223
- except OSError:
1224
- return False
1225
- return True
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 mktempfile(suffix: str, prefix: str, dir: str) -> str: # noqa
1229
- fd, filename = tempfile.mkstemp(suffix, prefix, dir)
1230
- os.close(fd)
1231
- return filename
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
- def get_path() -> ta.Sequence[str]:
1235
- """Return a list corresponding to $PATH, or a default."""
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
- def check_existing_dir(v: str) -> str:
1246
- nv = os.path.expanduser(v)
1247
- if os.path.isdir(nv):
1248
- return nv
1249
- raise ValueError(f'{v} is not an existing directory')
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
- # ../utils/ostypes.py
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
- Fd = ta.NewType('Fd', int)
1268
- Pid = ta.NewType('Pid', int)
1269
- Rc = ta.NewType('Rc', int)
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
- Uid = ta.NewType('Uid', int)
1272
- Gid = ta.NewType('Gid', int)
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
- # ../utils/signals.py
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
- _SIGS_BY_NUM: ta.Mapping[int, signal.Signals] = {s.value: s for s in signal.Signals}
1283
- _SIGS_BY_NAME: ta.Mapping[str, signal.Signals] = {s.name: s for s in signal.Signals}
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 sig_num(value: ta.Union[int, str]) -> int:
1287
- try:
1288
- num = int(value)
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
- except (ValueError, TypeError):
1291
- name = value.strip().upper() # type: ignore
1292
- if not name.startswith('SIG'):
1293
- name = f'SIG{name}'
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
- if (sn := _SIGS_BY_NAME.get(name)) is None:
1296
- raise ValueError(f'value {value!r} is not a valid signal name') # noqa
1297
- num = sn
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
- if num not in _SIGS_BY_NUM:
1300
- raise ValueError(f'value {value!r} is not a valid signal number')
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
- def sig_name(num: int) -> str:
1306
- if (sig := _SIGS_BY_NUM.get(num)) is not None:
1307
- return sig.name
1308
- return f'signal {sig}'
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
- class SignalReceiver:
1315
- def __init__(self) -> None:
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
- def receive(self, sig: int, frame: ta.Any = None) -> None:
1321
- if sig not in self._signals_recvd:
1322
- self._signals_recvd.append(sig)
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
- def get_signal(self) -> ta.Optional[int]:
1329
- if self._signals_recvd:
1330
- sig = self._signals_recvd.pop(0)
1331
- else:
1332
- sig = None
1333
- return sig
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
- # ../utils/strings.py
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 as_bytes(s: ta.Union[str, bytes], encoding: str = 'utf8') -> bytes:
1344
- if isinstance(s, bytes):
1345
- return s
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
- return s.encode(encoding)
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
- @ta.overload
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
- def find_prefix_at_end(haystack, needle):
1361
- l = len(needle) - 1
1362
- while l and not haystack.endswith(needle[:l]):
1363
- l -= 1
1364
- return l
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
- ANSI_ESCAPE_BEGIN = b'\x1b['
1371
- 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')
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
- def strip_escapes(s: bytes) -> bytes:
1375
- """Remove all ANSI color escapes from the given string."""
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
- result = b''
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
- class SuffixMultiplier:
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
- def parse_octal(arg: ta.Union[str, int]) -> int:
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
- def _provide_http_handler(s: SupervisorHttpHandler) -> HttpServer.Handler:
9621
- return HttpServer.Handler(s.handle)
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
- lst.extend([
9624
- inj.bind(HttpServer, singleton=True, eager=True),
9625
- inj.bind(HasDispatchers, array=True, to_key=HttpServer),
9605
+ lst.extend([
9606
+ inj.bind(HttpServer, singleton=True, eager=True),
9607
+ inj.bind(HasDispatchers, array=True, to_key=HttpServer),
9626
9608
 
9627
- inj.bind(SupervisorHttpHandler, singleton=True),
9628
- inj.bind(_provide_http_handler),
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