ominfra 0.0.0.dev191__py3-none-any.whl → 0.0.0.dev193__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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