omdev 0.0.0.dev36__py3-none-any.whl → 0.0.0.dev37__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.

Potentially problematic release.


This version of omdev might be problematic. Click here for more details.

@@ -77,12 +77,7 @@ if sys.version_info < (3, 8):
77
77
  ########################################
78
78
 
79
79
 
80
- # ../../toml/parser.py
81
- TomlParseFloat = ta.Callable[[str], ta.Any]
82
- TomlKey = ta.Tuple[str, ...]
83
- TomlPos = int # ta.TypeAlias
84
-
85
- # ../../versioning/versions.py
80
+ # ../../packaging/versions.py
86
81
  VersionLocalType = ta.Tuple[ta.Union[int, str], ...]
87
82
  VersionCmpPrePostDevType = ta.Union['InfinityVersionType', 'NegativeInfinityVersionType', ta.Tuple[str, int]]
88
83
  _VersionCmpLocalType0 = ta.Tuple[ta.Union[ta.Tuple[int, str], ta.Tuple['NegativeInfinityVersionType', ta.Union[int, str]]], ...] # noqa
@@ -90,10 +85,15 @@ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalTy
90
85
  VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # noqa
91
86
  VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool]
92
87
 
88
+ # ../../toml/parser.py
89
+ TomlParseFloat = ta.Callable[[str], ta.Any]
90
+ TomlKey = ta.Tuple[str, ...]
91
+ TomlPos = int # ta.TypeAlias
92
+
93
93
  # ../../../omlish/lite/check.py
94
94
  T = ta.TypeVar('T')
95
95
 
96
- # ../../versioning/specifiers.py
96
+ # ../../packaging/specifiers.py
97
97
  UnparsedVersion = ta.Union['Version', str]
98
98
  UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
99
99
  CallableVersionOperator = ta.Callable[['Version', str], bool]
@@ -270,1340 +270,1340 @@ def get_git_revision(
270
270
 
271
271
 
272
272
  ########################################
273
- # ../../toml/parser.py
274
- # SPDX-License-Identifier: MIT
275
- # SPDX-FileCopyrightText: 2021 Taneli Hukkinen
276
- # Licensed to PSF under a Contributor Agreement.
277
- #
278
- # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
279
- # --------------------------------------------
280
- #
281
- # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
282
- # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
283
- # documentation.
284
- #
285
- # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
286
- # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
287
- # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
288
- # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
289
- # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
290
- # Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
291
- #
292
- # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
293
- # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
294
- # any such work a brief summary of the changes made to Python.
295
- #
296
- # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
297
- # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
298
- # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
299
- # RIGHTS.
300
- #
301
- # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
302
- # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
303
- # ADVISED OF THE POSSIBILITY THEREOF.
273
+ # ../../packaging/versions.py
274
+ # Copyright (c) Donald Stufft and individual contributors.
275
+ # All rights reserved.
304
276
  #
305
- # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
277
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
278
+ # following conditions are met:
306
279
  #
307
- # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
308
- # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
309
- # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
280
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
281
+ # following disclaimer.
310
282
  #
311
- # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
312
- # License Agreement.
283
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
284
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
313
285
  #
314
- # https://github.com/python/cpython/blob/f5009b69e0cd94b990270e04e65b9d4d2b365844/Lib/tomllib/_parser.py
286
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
287
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
288
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
289
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
290
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
291
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
292
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
293
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
294
+ # details.
295
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/version.py
315
296
 
316
297
 
317
298
  ##
318
299
 
319
300
 
320
- _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]*)?'
301
+ class InfinityVersionType:
302
+ def __repr__(self) -> str:
303
+ return 'Infinity'
321
304
 
322
- TOML_RE_NUMBER = re.compile(
323
- r"""
324
- 0
325
- (?:
326
- x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
327
- |
328
- b[01](?:_?[01])* # bin
329
- |
330
- o[0-7](?:_?[0-7])* # oct
331
- )
332
- |
333
- [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
334
- (?P<floatpart>
335
- (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
336
- (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
337
- )
338
- """,
339
- flags=re.VERBOSE,
340
- )
341
- TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
342
- TOML_RE_DATETIME = re.compile(
343
- rf"""
344
- ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
345
- (?:
346
- [Tt ]
347
- {_TOML_TIME_RE_STR}
348
- (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
349
- )?
350
- """,
351
- flags=re.VERBOSE,
352
- )
305
+ def __hash__(self) -> int:
306
+ return hash(repr(self))
353
307
 
308
+ def __lt__(self, other: object) -> bool:
309
+ return False
354
310
 
355
- def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
356
- """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
311
+ def __le__(self, other: object) -> bool:
312
+ return False
357
313
 
358
- Raises ValueError if the match does not correspond to a valid date or datetime.
359
- """
360
- (
361
- year_str,
362
- month_str,
363
- day_str,
364
- hour_str,
365
- minute_str,
366
- sec_str,
367
- micros_str,
368
- zulu_time,
369
- offset_sign_str,
370
- offset_hour_str,
371
- offset_minute_str,
372
- ) = match.groups()
373
- year, month, day = int(year_str), int(month_str), int(day_str)
374
- if hour_str is None:
375
- return datetime.date(year, month, day)
376
- hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
377
- micros = int(micros_str.ljust(6, '0')) if micros_str else 0
378
- if offset_sign_str:
379
- tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
380
- offset_hour_str, offset_minute_str, offset_sign_str,
381
- )
382
- elif zulu_time:
383
- tz = datetime.UTC
384
- else: # local date-time
385
- tz = None
386
- return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
314
+ def __eq__(self, other: object) -> bool:
315
+ return isinstance(other, self.__class__)
387
316
 
317
+ def __gt__(self, other: object) -> bool:
318
+ return True
388
319
 
389
- @functools.lru_cache() # noqa
390
- def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
391
- sign = 1 if sign_str == '+' else -1
392
- return datetime.timezone(
393
- datetime.timedelta(
394
- hours=sign * int(hour_str),
395
- minutes=sign * int(minute_str),
396
- ),
397
- )
320
+ def __ge__(self, other: object) -> bool:
321
+ return True
398
322
 
323
+ def __neg__(self: object) -> 'NegativeInfinityVersionType':
324
+ return NegativeInfinityVersion
399
325
 
400
- def toml_match_to_localtime(match: re.Match) -> datetime.time:
401
- hour_str, minute_str, sec_str, micros_str = match.groups()
402
- micros = int(micros_str.ljust(6, '0')) if micros_str else 0
403
- return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
404
326
 
327
+ InfinityVersion = InfinityVersionType()
405
328
 
406
- def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
407
- if match.group('floatpart'):
408
- return parse_float(match.group())
409
- return int(match.group(), 0)
410
329
 
330
+ class NegativeInfinityVersionType:
331
+ def __repr__(self) -> str:
332
+ return '-Infinity'
411
333
 
412
- TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
334
+ def __hash__(self) -> int:
335
+ return hash(repr(self))
413
336
 
414
- # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
415
- # functions.
416
- TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
417
- TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
337
+ def __lt__(self, other: object) -> bool:
338
+ return True
418
339
 
419
- TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
420
- TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
340
+ def __le__(self, other: object) -> bool:
341
+ return True
421
342
 
422
- TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
343
+ def __eq__(self, other: object) -> bool:
344
+ return isinstance(other, self.__class__)
423
345
 
424
- TOML_WS = frozenset(' \t')
425
- TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
426
- TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
427
- TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
428
- TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
346
+ def __gt__(self, other: object) -> bool:
347
+ return False
429
348
 
430
- TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
431
- {
432
- '\\b': '\u0008', # backspace
433
- '\\t': '\u0009', # tab
434
- '\\n': '\u000A', # linefeed
435
- '\\f': '\u000C', # form feed
436
- '\\r': '\u000D', # carriage return
437
- '\\"': '\u0022', # quote
438
- '\\\\': '\u005C', # backslash
439
- },
440
- )
349
+ def __ge__(self, other: object) -> bool:
350
+ return False
441
351
 
352
+ def __neg__(self: object) -> InfinityVersionType:
353
+ return InfinityVersion
442
354
 
443
- class TomlDecodeError(ValueError):
444
- """An error raised if a document is not valid TOML."""
445
355
 
356
+ NegativeInfinityVersion = NegativeInfinityVersionType()
446
357
 
447
- def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
448
- """Parse TOML from a binary file object."""
449
- b = fp.read()
450
- try:
451
- s = b.decode()
452
- except AttributeError:
453
- raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
454
- return toml_loads(s, parse_float=parse_float)
455
358
 
359
+ ##
456
360
 
457
- def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
458
- """Parse TOML from a string."""
459
361
 
460
- # The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
461
- src = s.replace('\r\n', '\n')
462
- pos = 0
463
- out = TomlOutput(TomlNestedDict(), TomlFlags())
464
- header: TomlKey = ()
465
- parse_float = toml_make_safe_parse_float(parse_float)
362
+ class _Version(ta.NamedTuple):
363
+ epoch: int
364
+ release: ta.Tuple[int, ...]
365
+ dev: ta.Optional[ta.Tuple[str, int]]
366
+ pre: ta.Optional[ta.Tuple[str, int]]
367
+ post: ta.Optional[ta.Tuple[str, int]]
368
+ local: ta.Optional[VersionLocalType]
466
369
 
467
- # Parse one statement at a time (typically means one line in TOML source)
468
- while True:
469
- # 1. Skip line leading whitespace
470
- pos = toml_skip_chars(src, pos, TOML_WS)
471
370
 
472
- # 2. Parse rules. Expect one of the following:
473
- # - end of file
474
- # - end of line
475
- # - comment
476
- # - key/value pair
477
- # - append dict to list (and move to its namespace)
478
- # - create dict (and move to its namespace)
479
- # Skip trailing whitespace when applicable.
480
- try:
481
- char = src[pos]
482
- except IndexError:
483
- break
484
- if char == '\n':
485
- pos += 1
486
- continue
487
- if char in TOML_KEY_INITIAL_CHARS:
488
- pos = toml_key_value_rule(src, pos, out, header, parse_float)
489
- pos = toml_skip_chars(src, pos, TOML_WS)
490
- elif char == '[':
491
- try:
492
- second_char: ta.Optional[str] = src[pos + 1]
493
- except IndexError:
494
- second_char = None
495
- out.flags.finalize_pending()
496
- if second_char == '[':
497
- pos, header = toml_create_list_rule(src, pos, out)
498
- else:
499
- pos, header = toml_create_dict_rule(src, pos, out)
500
- pos = toml_skip_chars(src, pos, TOML_WS)
501
- elif char != '#':
502
- raise toml_suffixed_err(src, pos, 'Invalid statement')
371
+ class InvalidVersion(ValueError): # noqa
372
+ pass
503
373
 
504
- # 3. Skip comment
505
- pos = toml_skip_comment(src, pos)
506
374
 
507
- # 4. Expect end of line or end of file
508
- try:
509
- char = src[pos]
510
- except IndexError:
511
- break
512
- if char != '\n':
513
- raise toml_suffixed_err(
514
- src, pos, 'Expected newline or end of document after a statement',
515
- )
516
- pos += 1
375
+ class _BaseVersion:
376
+ _key: ta.Tuple[ta.Any, ...]
517
377
 
518
- return out.data.dict
378
+ def __hash__(self) -> int:
379
+ return hash(self._key)
519
380
 
381
+ def __lt__(self, other: '_BaseVersion') -> bool:
382
+ if not isinstance(other, _BaseVersion):
383
+ return NotImplemented # type: ignore
384
+ return self._key < other._key
520
385
 
521
- class TomlFlags:
522
- """Flags that map to parsed keys/namespaces."""
386
+ def __le__(self, other: '_BaseVersion') -> bool:
387
+ if not isinstance(other, _BaseVersion):
388
+ return NotImplemented # type: ignore
389
+ return self._key <= other._key
523
390
 
524
- # Marks an immutable namespace (inline array or inline table).
525
- FROZEN = 0
526
- # Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
527
- EXPLICIT_NEST = 1
391
+ def __eq__(self, other: object) -> bool:
392
+ if not isinstance(other, _BaseVersion):
393
+ return NotImplemented
394
+ return self._key == other._key
528
395
 
529
- def __init__(self) -> None:
530
- self._flags: ta.Dict[str, dict] = {}
531
- self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
396
+ def __ge__(self, other: '_BaseVersion') -> bool:
397
+ if not isinstance(other, _BaseVersion):
398
+ return NotImplemented # type: ignore
399
+ return self._key >= other._key
532
400
 
533
- def add_pending(self, key: TomlKey, flag: int) -> None:
534
- self._pending_flags.add((key, flag))
401
+ def __gt__(self, other: '_BaseVersion') -> bool:
402
+ if not isinstance(other, _BaseVersion):
403
+ return NotImplemented # type: ignore
404
+ return self._key > other._key
535
405
 
536
- def finalize_pending(self) -> None:
537
- for key, flag in self._pending_flags:
538
- self.set(key, flag, recursive=False)
539
- self._pending_flags.clear()
406
+ def __ne__(self, other: object) -> bool:
407
+ if not isinstance(other, _BaseVersion):
408
+ return NotImplemented
409
+ return self._key != other._key
540
410
 
541
- def unset_all(self, key: TomlKey) -> None:
542
- cont = self._flags
543
- for k in key[:-1]:
544
- if k not in cont:
545
- return
546
- cont = cont[k]['nested']
547
- cont.pop(key[-1], None)
548
411
 
549
- def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
550
- cont = self._flags
551
- key_parent, key_stem = key[:-1], key[-1]
552
- for k in key_parent:
553
- if k not in cont:
554
- cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
555
- cont = cont[k]['nested']
556
- if key_stem not in cont:
557
- cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
558
- cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
412
+ _VERSION_PATTERN = r"""
413
+ v?
414
+ (?:
415
+ (?:(?P<epoch>[0-9]+)!)?
416
+ (?P<release>[0-9]+(?:\.[0-9]+)*)
417
+ (?P<pre>
418
+ [-_\.]?
419
+ (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
420
+ [-_\.]?
421
+ (?P<pre_n>[0-9]+)?
422
+ )?
423
+ (?P<post>
424
+ (?:-(?P<post_n1>[0-9]+))
425
+ |
426
+ (?:
427
+ [-_\.]?
428
+ (?P<post_l>post|rev|r)
429
+ [-_\.]?
430
+ (?P<post_n2>[0-9]+)?
431
+ )
432
+ )?
433
+ (?P<dev>
434
+ [-_\.]?
435
+ (?P<dev_l>dev)
436
+ [-_\.]?
437
+ (?P<dev_n>[0-9]+)?
438
+ )?
439
+ )
440
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?
441
+ """
559
442
 
560
- def is_(self, key: TomlKey, flag: int) -> bool:
561
- if not key:
562
- return False # document root has no flags
563
- cont = self._flags
564
- for k in key[:-1]:
565
- if k not in cont:
566
- return False
567
- inner_cont = cont[k]
568
- if flag in inner_cont['recursive_flags']:
569
- return True
570
- cont = inner_cont['nested']
571
- key_stem = key[-1]
572
- if key_stem in cont:
573
- cont = cont[key_stem]
574
- return flag in cont['flags'] or flag in cont['recursive_flags']
575
- return False
443
+ VERSION_PATTERN = _VERSION_PATTERN
576
444
 
577
445
 
578
- class TomlNestedDict:
579
- def __init__(self) -> None:
580
- # The parsed content of the TOML document
581
- self.dict: ta.Dict[str, ta.Any] = {}
446
+ class Version(_BaseVersion):
447
+ _regex = re.compile(r'^\s*' + VERSION_PATTERN + r'\s*$', re.VERBOSE | re.IGNORECASE)
448
+ _key: VersionCmpKey
582
449
 
583
- def get_or_create_nest(
584
- self,
585
- key: TomlKey,
586
- *,
587
- access_lists: bool = True,
588
- ) -> dict:
589
- cont: ta.Any = self.dict
590
- for k in key:
591
- if k not in cont:
592
- cont[k] = {}
593
- cont = cont[k]
594
- if access_lists and isinstance(cont, list):
595
- cont = cont[-1]
596
- if not isinstance(cont, dict):
597
- raise KeyError('There is no nest behind this key')
598
- return cont
450
+ def __init__(self, version: str) -> None:
451
+ match = self._regex.search(version)
452
+ if not match:
453
+ raise InvalidVersion(f"Invalid version: '{version}'")
599
454
 
600
- def append_nest_to_list(self, key: TomlKey) -> None:
601
- cont = self.get_or_create_nest(key[:-1])
602
- last_key = key[-1]
603
- if last_key in cont:
604
- list_ = cont[last_key]
605
- if not isinstance(list_, list):
606
- raise KeyError('An object other than list found behind this key')
607
- list_.append({})
608
- else:
609
- cont[last_key] = [{}]
455
+ self._version = _Version(
456
+ epoch=int(match.group('epoch')) if match.group('epoch') else 0,
457
+ release=tuple(int(i) for i in match.group('release').split('.')),
458
+ pre=_parse_letter_version(match.group('pre_l'), match.group('pre_n')),
459
+ post=_parse_letter_version(match.group('post_l'), match.group('post_n1') or match.group('post_n2')),
460
+ dev=_parse_letter_version(match.group('dev_l'), match.group('dev_n')),
461
+ local=_parse_local_version(match.group('local')),
462
+ )
610
463
 
464
+ self._key = _version_cmpkey(
465
+ self._version.epoch,
466
+ self._version.release,
467
+ self._version.pre,
468
+ self._version.post,
469
+ self._version.dev,
470
+ self._version.local,
471
+ )
611
472
 
612
- class TomlOutput(ta.NamedTuple):
613
- data: TomlNestedDict
614
- flags: TomlFlags
473
+ def __repr__(self) -> str:
474
+ return f"<Version('{self}')>"
615
475
 
476
+ def __str__(self) -> str:
477
+ parts = []
616
478
 
617
- def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
618
- try:
619
- while src[pos] in chars:
620
- pos += 1
621
- except IndexError:
622
- pass
623
- return pos
479
+ if self.epoch != 0:
480
+ parts.append(f'{self.epoch}!')
624
481
 
482
+ parts.append('.'.join(str(x) for x in self.release))
625
483
 
626
- def toml_skip_until(
627
- src: str,
628
- pos: TomlPos,
629
- expect: str,
630
- *,
631
- error_on: ta.FrozenSet[str],
632
- error_on_eof: bool,
633
- ) -> TomlPos:
634
- try:
635
- new_pos = src.index(expect, pos)
636
- except ValueError:
637
- new_pos = len(src)
638
- if error_on_eof:
639
- raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
484
+ if self.pre is not None:
485
+ parts.append(''.join(str(x) for x in self.pre))
640
486
 
641
- if not error_on.isdisjoint(src[pos:new_pos]):
642
- while src[pos] not in error_on:
643
- pos += 1
644
- raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
645
- return new_pos
487
+ if self.post is not None:
488
+ parts.append(f'.post{self.post}')
646
489
 
490
+ if self.dev is not None:
491
+ parts.append(f'.dev{self.dev}')
647
492
 
648
- def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
649
- try:
650
- char: ta.Optional[str] = src[pos]
651
- except IndexError:
652
- char = None
653
- if char == '#':
654
- return toml_skip_until(
655
- src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
656
- )
657
- return pos
493
+ if self.local is not None:
494
+ parts.append(f'+{self.local}')
658
495
 
496
+ return ''.join(parts)
659
497
 
660
- def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
661
- while True:
662
- pos_before_skip = pos
663
- pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
664
- pos = toml_skip_comment(src, pos)
665
- if pos == pos_before_skip:
666
- return pos
498
+ @property
499
+ def epoch(self) -> int:
500
+ return self._version.epoch
667
501
 
502
+ @property
503
+ def release(self) -> ta.Tuple[int, ...]:
504
+ return self._version.release
668
505
 
669
- def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
670
- pos += 1 # Skip "["
671
- pos = toml_skip_chars(src, pos, TOML_WS)
672
- pos, key = toml_parse_key(src, pos)
506
+ @property
507
+ def pre(self) -> ta.Optional[ta.Tuple[str, int]]:
508
+ return self._version.pre
673
509
 
674
- if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
675
- raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
676
- out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
677
- try:
678
- out.data.get_or_create_nest(key)
679
- except KeyError:
680
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
510
+ @property
511
+ def post(self) -> ta.Optional[int]:
512
+ return self._version.post[1] if self._version.post else None
681
513
 
682
- if not src.startswith(']', pos):
683
- raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
684
- return pos + 1, key
514
+ @property
515
+ def dev(self) -> ta.Optional[int]:
516
+ return self._version.dev[1] if self._version.dev else None
685
517
 
518
+ @property
519
+ def local(self) -> ta.Optional[str]:
520
+ if self._version.local:
521
+ return '.'.join(str(x) for x in self._version.local)
522
+ else:
523
+ return None
686
524
 
687
- def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
688
- pos += 2 # Skip "[["
689
- pos = toml_skip_chars(src, pos, TOML_WS)
690
- pos, key = toml_parse_key(src, pos)
525
+ @property
526
+ def public(self) -> str:
527
+ return str(self).split('+', 1)[0]
691
528
 
692
- if out.flags.is_(key, TomlFlags.FROZEN):
693
- raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
694
- # Free the namespace now that it points to another empty list item...
695
- out.flags.unset_all(key)
696
- # ...but this key precisely is still prohibited from table declaration
697
- out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
698
- try:
699
- out.data.append_nest_to_list(key)
700
- except KeyError:
701
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
529
+ @property
530
+ def base_version(self) -> str:
531
+ parts = []
702
532
 
703
- if not src.startswith(']]', pos):
704
- raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
705
- return pos + 2, key
533
+ if self.epoch != 0:
534
+ parts.append(f'{self.epoch}!')
706
535
 
536
+ parts.append('.'.join(str(x) for x in self.release))
707
537
 
708
- def toml_key_value_rule(
709
- src: str,
710
- pos: TomlPos,
711
- out: TomlOutput,
712
- header: TomlKey,
713
- parse_float: TomlParseFloat,
714
- ) -> TomlPos:
715
- pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
716
- key_parent, key_stem = key[:-1], key[-1]
717
- abs_key_parent = header + key_parent
538
+ return ''.join(parts)
718
539
 
719
- relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
720
- for cont_key in relative_path_cont_keys:
721
- # Check that dotted key syntax does not redefine an existing table
722
- if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
723
- raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
724
- # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
725
- # table sections.
726
- out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
540
+ @property
541
+ def is_prerelease(self) -> bool:
542
+ return self.dev is not None or self.pre is not None
727
543
 
728
- if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
729
- raise toml_suffixed_err(
730
- src,
731
- pos,
732
- f'Cannot mutate immutable namespace {abs_key_parent}',
733
- )
544
+ @property
545
+ def is_postrelease(self) -> bool:
546
+ return self.post is not None
734
547
 
735
- try:
736
- nest = out.data.get_or_create_nest(abs_key_parent)
737
- except KeyError:
738
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
739
- if key_stem in nest:
740
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
741
- # Mark inline table and array namespaces recursively immutable
742
- if isinstance(value, (dict, list)):
743
- out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
744
- nest[key_stem] = value
745
- return pos
548
+ @property
549
+ def is_devrelease(self) -> bool:
550
+ return self.dev is not None
746
551
 
552
+ @property
553
+ def major(self) -> int:
554
+ return self.release[0] if len(self.release) >= 1 else 0
747
555
 
748
- def toml_parse_key_value_pair(
749
- src: str,
750
- pos: TomlPos,
751
- parse_float: TomlParseFloat,
752
- ) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
753
- pos, key = toml_parse_key(src, pos)
754
- try:
755
- char: ta.Optional[str] = src[pos]
756
- except IndexError:
757
- char = None
758
- if char != '=':
759
- raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
760
- pos += 1
761
- pos = toml_skip_chars(src, pos, TOML_WS)
762
- pos, value = toml_parse_value(src, pos, parse_float)
763
- return pos, key, value
556
+ @property
557
+ def minor(self) -> int:
558
+ return self.release[1] if len(self.release) >= 2 else 0
764
559
 
560
+ @property
561
+ def micro(self) -> int:
562
+ return self.release[2] if len(self.release) >= 3 else 0
765
563
 
766
- def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
767
- pos, key_part = toml_parse_key_part(src, pos)
768
- key: TomlKey = (key_part,)
769
- pos = toml_skip_chars(src, pos, TOML_WS)
770
- while True:
771
- try:
772
- char: ta.Optional[str] = src[pos]
773
- except IndexError:
774
- char = None
775
- if char != '.':
776
- return pos, key
777
- pos += 1
778
- pos = toml_skip_chars(src, pos, TOML_WS)
779
- pos, key_part = toml_parse_key_part(src, pos)
780
- key += (key_part,)
781
- pos = toml_skip_chars(src, pos, TOML_WS)
782
564
 
565
+ def _parse_letter_version(
566
+ letter: ta.Optional[str],
567
+ number: ta.Union[str, bytes, ta.SupportsInt, None],
568
+ ) -> ta.Optional[ta.Tuple[str, int]]:
569
+ if letter:
570
+ if number is None:
571
+ number = 0
783
572
 
784
- def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
785
- try:
786
- char: ta.Optional[str] = src[pos]
787
- except IndexError:
788
- char = None
789
- if char in TOML_BARE_KEY_CHARS:
790
- start_pos = pos
791
- pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
792
- return pos, src[start_pos:pos]
793
- if char == "'":
794
- return toml_parse_literal_str(src, pos)
795
- if char == '"':
796
- return toml_parse_one_line_basic_str(src, pos)
797
- raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
573
+ letter = letter.lower()
574
+ if letter == 'alpha':
575
+ letter = 'a'
576
+ elif letter == 'beta':
577
+ letter = 'b'
578
+ elif letter in ['c', 'pre', 'preview']:
579
+ letter = 'rc'
580
+ elif letter in ['rev', 'r']:
581
+ letter = 'post'
798
582
 
583
+ return letter, int(number)
584
+ if not letter and number:
585
+ letter = 'post'
586
+ return letter, int(number)
799
587
 
800
- def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
801
- pos += 1
802
- return toml_parse_basic_str(src, pos, multiline=False)
588
+ return None
803
589
 
804
590
 
805
- def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
806
- pos += 1
807
- array: list = []
591
+ _local_version_separators = re.compile(r'[\._-]')
808
592
 
809
- pos = toml_skip_comments_and_array_ws(src, pos)
810
- if src.startswith(']', pos):
811
- return pos + 1, array
812
- while True:
813
- pos, val = toml_parse_value(src, pos, parse_float)
814
- array.append(val)
815
- pos = toml_skip_comments_and_array_ws(src, pos)
816
593
 
817
- c = src[pos:pos + 1]
818
- if c == ']':
819
- return pos + 1, array
820
- if c != ',':
821
- raise toml_suffixed_err(src, pos, 'Unclosed array')
822
- pos += 1
594
+ def _parse_local_version(local: ta.Optional[str]) -> ta.Optional[VersionLocalType]:
595
+ if local is not None:
596
+ return tuple(
597
+ part.lower() if not part.isdigit() else int(part)
598
+ for part in _local_version_separators.split(local)
599
+ )
600
+ return None
823
601
 
824
- pos = toml_skip_comments_and_array_ws(src, pos)
825
- if src.startswith(']', pos):
826
- return pos + 1, array
827
602
 
603
+ def _version_cmpkey(
604
+ epoch: int,
605
+ release: ta.Tuple[int, ...],
606
+ pre: ta.Optional[ta.Tuple[str, int]],
607
+ post: ta.Optional[ta.Tuple[str, int]],
608
+ dev: ta.Optional[ta.Tuple[str, int]],
609
+ local: ta.Optional[VersionLocalType],
610
+ ) -> VersionCmpKey:
611
+ _release = tuple(reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))))
828
612
 
829
- def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
830
- pos += 1
831
- nested_dict = TomlNestedDict()
832
- flags = TomlFlags()
613
+ if pre is None and post is None and dev is not None:
614
+ _pre: VersionCmpPrePostDevType = NegativeInfinityVersion
615
+ elif pre is None:
616
+ _pre = InfinityVersion
617
+ else:
618
+ _pre = pre
833
619
 
834
- pos = toml_skip_chars(src, pos, TOML_WS)
835
- if src.startswith('}', pos):
836
- return pos + 1, nested_dict.dict
837
- while True:
838
- pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
839
- key_parent, key_stem = key[:-1], key[-1]
840
- if flags.is_(key, TomlFlags.FROZEN):
841
- raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
842
- try:
843
- nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
844
- except KeyError:
845
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
846
- if key_stem in nest:
847
- raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
848
- nest[key_stem] = value
849
- pos = toml_skip_chars(src, pos, TOML_WS)
850
- c = src[pos:pos + 1]
851
- if c == '}':
852
- return pos + 1, nested_dict.dict
853
- if c != ',':
854
- raise toml_suffixed_err(src, pos, 'Unclosed inline table')
855
- if isinstance(value, (dict, list)):
856
- flags.set(key, TomlFlags.FROZEN, recursive=True)
857
- pos += 1
858
- pos = toml_skip_chars(src, pos, TOML_WS)
620
+ if post is None:
621
+ _post: VersionCmpPrePostDevType = NegativeInfinityVersion
622
+ else:
623
+ _post = post
859
624
 
625
+ if dev is None:
626
+ _dev: VersionCmpPrePostDevType = InfinityVersion
627
+ else:
628
+ _dev = dev
860
629
 
861
- def toml_parse_basic_str_escape(
862
- src: str,
863
- pos: TomlPos,
864
- *,
865
- multiline: bool = False,
866
- ) -> ta.Tuple[TomlPos, str]:
867
- escape_id = src[pos:pos + 2]
868
- pos += 2
869
- if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
870
- # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
871
- # newline.
872
- if escape_id != '\\\n':
873
- pos = toml_skip_chars(src, pos, TOML_WS)
874
- try:
875
- char = src[pos]
876
- except IndexError:
877
- return pos, ''
878
- if char != '\n':
879
- raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
880
- pos += 1
881
- pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
882
- return pos, ''
883
- if escape_id == '\\u':
884
- return toml_parse_hex_char(src, pos, 4)
885
- if escape_id == '\\U':
886
- return toml_parse_hex_char(src, pos, 8)
887
- try:
888
- return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
889
- except KeyError:
890
- raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
891
-
892
-
893
- def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
894
- return toml_parse_basic_str_escape(src, pos, multiline=True)
630
+ if local is None:
631
+ _local: VersionCmpLocalType = NegativeInfinityVersion
632
+ else:
633
+ _local = tuple((i, '') if isinstance(i, int) else (NegativeInfinityVersion, i) for i in local)
895
634
 
635
+ return epoch, _release, _pre, _post, _dev, _local
896
636
 
897
- def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
898
- hex_str = src[pos:pos + hex_len]
899
- if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
900
- raise toml_suffixed_err(src, pos, 'Invalid hex value')
901
- pos += hex_len
902
- hex_int = int(hex_str, 16)
903
- if not toml_is_unicode_scalar_value(hex_int):
904
- raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
905
- return pos, chr(hex_int)
906
637
 
638
+ ##
907
639
 
908
- def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
909
- pos += 1 # Skip starting apostrophe
910
- start_pos = pos
911
- pos = toml_skip_until(
912
- src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
913
- )
914
- return pos + 1, src[start_pos:pos] # Skip ending apostrophe
915
640
 
641
+ def canonicalize_version(
642
+ version: ta.Union[Version, str],
643
+ *,
644
+ strip_trailing_zero: bool = True,
645
+ ) -> str:
646
+ if isinstance(version, str):
647
+ try:
648
+ parsed = Version(version)
649
+ except InvalidVersion:
650
+ return version
651
+ else:
652
+ parsed = version
916
653
 
917
- def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
918
- pos += 3
919
- if src.startswith('\n', pos):
920
- pos += 1
654
+ parts = []
921
655
 
922
- if literal:
923
- delim = "'"
924
- end_pos = toml_skip_until(
925
- src,
926
- pos,
927
- "'''",
928
- error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
929
- error_on_eof=True,
930
- )
931
- result = src[pos:end_pos]
932
- pos = end_pos + 3
933
- else:
934
- delim = '"'
935
- pos, result = toml_parse_basic_str(src, pos, multiline=True)
656
+ if parsed.epoch != 0:
657
+ parts.append(f'{parsed.epoch}!')
936
658
 
937
- # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
938
- if not src.startswith(delim, pos):
939
- return pos, result
940
- pos += 1
941
- if not src.startswith(delim, pos):
942
- return pos, result + delim
943
- pos += 1
944
- return pos, result + (delim * 2)
659
+ release_segment = '.'.join(str(x) for x in parsed.release)
660
+ if strip_trailing_zero:
661
+ release_segment = re.sub(r'(\.0)+$', '', release_segment)
662
+ parts.append(release_segment)
945
663
 
664
+ if parsed.pre is not None:
665
+ parts.append(''.join(str(x) for x in parsed.pre))
946
666
 
947
- def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
948
- if multiline:
949
- error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
950
- parse_escapes = toml_parse_basic_str_escape_multiline
951
- else:
952
- error_on = TOML_ILLEGAL_BASIC_STR_CHARS
953
- parse_escapes = toml_parse_basic_str_escape
954
- result = ''
955
- start_pos = pos
956
- while True:
957
- try:
958
- char = src[pos]
959
- except IndexError:
960
- raise toml_suffixed_err(src, pos, 'Unterminated string') from None
961
- if char == '"':
962
- if not multiline:
963
- return pos + 1, result + src[start_pos:pos]
964
- if src.startswith('"""', pos):
965
- return pos + 3, result + src[start_pos:pos]
966
- pos += 1
967
- continue
968
- if char == '\\':
969
- result += src[start_pos:pos]
970
- pos, parsed_escape = parse_escapes(src, pos)
971
- result += parsed_escape
972
- start_pos = pos
973
- continue
974
- if char in error_on:
975
- raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
976
- pos += 1
667
+ if parsed.post is not None:
668
+ parts.append(f'.post{parsed.post}')
977
669
 
670
+ if parsed.dev is not None:
671
+ parts.append(f'.dev{parsed.dev}')
978
672
 
979
- def toml_parse_value( # noqa: C901
980
- src: str,
981
- pos: TomlPos,
982
- parse_float: TomlParseFloat,
983
- ) -> ta.Tuple[TomlPos, ta.Any]:
984
- try:
985
- char: ta.Optional[str] = src[pos]
986
- except IndexError:
987
- char = None
673
+ if parsed.local is not None:
674
+ parts.append(f'+{parsed.local}')
988
675
 
989
- # IMPORTANT: order conditions based on speed of checking and likelihood
676
+ return ''.join(parts)
990
677
 
991
- # Basic strings
992
- if char == '"':
993
- if src.startswith('"""', pos):
994
- return toml_parse_multiline_str(src, pos, literal=False)
995
- return toml_parse_one_line_basic_str(src, pos)
996
678
 
997
- # Literal strings
998
- if char == "'":
999
- if src.startswith("'''", pos):
1000
- return toml_parse_multiline_str(src, pos, literal=True)
1001
- return toml_parse_literal_str(src, pos)
679
+ ########################################
680
+ # ../../toml/parser.py
681
+ # SPDX-License-Identifier: MIT
682
+ # SPDX-FileCopyrightText: 2021 Taneli Hukkinen
683
+ # Licensed to PSF under a Contributor Agreement.
684
+ #
685
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
686
+ # --------------------------------------------
687
+ #
688
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
689
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
690
+ # documentation.
691
+ #
692
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
693
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
694
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
695
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
696
+ # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
697
+ # Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
698
+ #
699
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
700
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
701
+ # any such work a brief summary of the changes made to Python.
702
+ #
703
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
704
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
705
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
706
+ # RIGHTS.
707
+ #
708
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
709
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
710
+ # ADVISED OF THE POSSIBILITY THEREOF.
711
+ #
712
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
713
+ #
714
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
715
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
716
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
717
+ #
718
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
719
+ # License Agreement.
720
+ #
721
+ # https://github.com/python/cpython/blob/f5009b69e0cd94b990270e04e65b9d4d2b365844/Lib/tomllib/_parser.py
1002
722
 
1003
- # Booleans
1004
- if char == 't':
1005
- if src.startswith('true', pos):
1006
- return pos + 4, True
1007
- if char == 'f':
1008
- if src.startswith('false', pos):
1009
- return pos + 5, False
1010
723
 
1011
- # Arrays
1012
- if char == '[':
1013
- return toml_parse_array(src, pos, parse_float)
724
+ ##
1014
725
 
1015
- # Inline tables
1016
- if char == '{':
1017
- return toml_parse_inline_table(src, pos, parse_float)
1018
726
 
1019
- # Dates and times
1020
- datetime_match = TOML_RE_DATETIME.match(src, pos)
1021
- if datetime_match:
1022
- try:
1023
- datetime_obj = toml_match_to_datetime(datetime_match)
1024
- except ValueError as e:
1025
- raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
1026
- return datetime_match.end(), datetime_obj
1027
- localtime_match = TOML_RE_LOCALTIME.match(src, pos)
1028
- if localtime_match:
1029
- return localtime_match.end(), toml_match_to_localtime(localtime_match)
727
+ _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]*)?'
1030
728
 
1031
- # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
1032
- # located after handling of dates and times.
1033
- number_match = TOML_RE_NUMBER.match(src, pos)
1034
- if number_match:
1035
- return number_match.end(), toml_match_to_number(number_match, parse_float)
729
+ TOML_RE_NUMBER = re.compile(
730
+ r"""
731
+ 0
732
+ (?:
733
+ x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
734
+ |
735
+ b[01](?:_?[01])* # bin
736
+ |
737
+ o[0-7](?:_?[0-7])* # oct
738
+ )
739
+ |
740
+ [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
741
+ (?P<floatpart>
742
+ (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
743
+ (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
744
+ )
745
+ """,
746
+ flags=re.VERBOSE,
747
+ )
748
+ TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
749
+ TOML_RE_DATETIME = re.compile(
750
+ rf"""
751
+ ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
752
+ (?:
753
+ [Tt ]
754
+ {_TOML_TIME_RE_STR}
755
+ (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
756
+ )?
757
+ """,
758
+ flags=re.VERBOSE,
759
+ )
1036
760
 
1037
- # Special floats
1038
- first_three = src[pos:pos + 3]
1039
- if first_three in {'inf', 'nan'}:
1040
- return pos + 3, parse_float(first_three)
1041
- first_four = src[pos:pos + 4]
1042
- if first_four in {'-inf', '+inf', '-nan', '+nan'}:
1043
- return pos + 4, parse_float(first_four)
1044
761
 
1045
- raise toml_suffixed_err(src, pos, 'Invalid value')
762
+ def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
763
+ """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
1046
764
 
765
+ Raises ValueError if the match does not correspond to a valid date or datetime.
766
+ """
767
+ (
768
+ year_str,
769
+ month_str,
770
+ day_str,
771
+ hour_str,
772
+ minute_str,
773
+ sec_str,
774
+ micros_str,
775
+ zulu_time,
776
+ offset_sign_str,
777
+ offset_hour_str,
778
+ offset_minute_str,
779
+ ) = match.groups()
780
+ year, month, day = int(year_str), int(month_str), int(day_str)
781
+ if hour_str is None:
782
+ return datetime.date(year, month, day)
783
+ hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
784
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
785
+ if offset_sign_str:
786
+ tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
787
+ offset_hour_str, offset_minute_str, offset_sign_str,
788
+ )
789
+ elif zulu_time:
790
+ tz = datetime.UTC
791
+ else: # local date-time
792
+ tz = None
793
+ return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
1047
794
 
1048
- def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
1049
- """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
1050
795
 
1051
- def coord_repr(src: str, pos: TomlPos) -> str:
1052
- if pos >= len(src):
1053
- return 'end of document'
1054
- line = src.count('\n', 0, pos) + 1
1055
- if line == 1:
1056
- column = pos + 1
1057
- else:
1058
- column = pos - src.rindex('\n', 0, pos)
1059
- return f'line {line}, column {column}'
796
+ @functools.lru_cache() # noqa
797
+ def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
798
+ sign = 1 if sign_str == '+' else -1
799
+ return datetime.timezone(
800
+ datetime.timedelta(
801
+ hours=sign * int(hour_str),
802
+ minutes=sign * int(minute_str),
803
+ ),
804
+ )
1060
805
 
1061
- return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
1062
806
 
807
+ def toml_match_to_localtime(match: re.Match) -> datetime.time:
808
+ hour_str, minute_str, sec_str, micros_str = match.groups()
809
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
810
+ return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
1063
811
 
1064
- def toml_is_unicode_scalar_value(codepoint: int) -> bool:
1065
- return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
1066
812
 
813
+ def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
814
+ if match.group('floatpart'):
815
+ return parse_float(match.group())
816
+ return int(match.group(), 0)
1067
817
 
1068
- def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
1069
- """A decorator to make `parse_float` safe.
1070
818
 
1071
- `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
1072
- thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
1073
- """
1074
- # The default `float` callable never returns illegal types. Optimize it.
1075
- if parse_float is float:
1076
- return float
819
+ TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
1077
820
 
1078
- def safe_parse_float(float_str: str) -> ta.Any:
1079
- float_value = parse_float(float_str)
1080
- if isinstance(float_value, (dict, list)):
1081
- raise ValueError('parse_float must not return dicts or lists') # noqa
1082
- return float_value
821
+ # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
822
+ # functions.
823
+ TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
824
+ TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
1083
825
 
1084
- return safe_parse_float
826
+ TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
827
+ TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
1085
828
 
829
+ TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
1086
830
 
1087
- ########################################
1088
- # ../../toml/writer.py
831
+ TOML_WS = frozenset(' \t')
832
+ TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
833
+ TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
834
+ TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
835
+ TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
1089
836
 
837
+ TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
838
+ {
839
+ '\\b': '\u0008', # backspace
840
+ '\\t': '\u0009', # tab
841
+ '\\n': '\u000A', # linefeed
842
+ '\\f': '\u000C', # form feed
843
+ '\\r': '\u000D', # carriage return
844
+ '\\"': '\u0022', # quote
845
+ '\\\\': '\u005C', # backslash
846
+ },
847
+ )
1090
848
 
1091
- class TomlWriter:
1092
- @dc.dataclass(frozen=True)
1093
- class Literal:
1094
- s: str
1095
849
 
1096
- def __init__(self, out: ta.TextIO) -> None:
1097
- super().__init__()
1098
- self._out = out
850
+ class TomlDecodeError(ValueError):
851
+ """An error raised if a document is not valid TOML."""
1099
852
 
1100
- self._indent = 0
1101
- self._wrote_indent = False
1102
853
 
1103
- #
854
+ def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
855
+ """Parse TOML from a binary file object."""
856
+ b = fp.read()
857
+ try:
858
+ s = b.decode()
859
+ except AttributeError:
860
+ raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
861
+ return toml_loads(s, parse_float=parse_float)
1104
862
 
1105
- def _w(self, s: str) -> None:
1106
- if not self._wrote_indent:
1107
- self._out.write(' ' * self._indent)
1108
- self._wrote_indent = True
1109
- self._out.write(s)
1110
863
 
1111
- def _nl(self) -> None:
1112
- self._out.write('\n')
1113
- self._wrote_indent = False
864
+ def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
865
+ """Parse TOML from a string."""
1114
866
 
1115
- def _needs_quote(self, s: str) -> bool:
1116
- return (
1117
- not s or
1118
- any(c in s for c in '\'"\n') or
1119
- s[0] not in string.ascii_letters
1120
- )
867
+ # The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
868
+ src = s.replace('\r\n', '\n')
869
+ pos = 0
870
+ out = TomlOutput(TomlNestedDict(), TomlFlags())
871
+ header: TomlKey = ()
872
+ parse_float = toml_make_safe_parse_float(parse_float)
1121
873
 
1122
- def _maybe_quote(self, s: str) -> str:
1123
- if self._needs_quote(s):
1124
- return repr(s)
1125
- else:
1126
- return s
874
+ # Parse one statement at a time (typically means one line in TOML source)
875
+ while True:
876
+ # 1. Skip line leading whitespace
877
+ pos = toml_skip_chars(src, pos, TOML_WS)
1127
878
 
1128
- #
879
+ # 2. Parse rules. Expect one of the following:
880
+ # - end of file
881
+ # - end of line
882
+ # - comment
883
+ # - key/value pair
884
+ # - append dict to list (and move to its namespace)
885
+ # - create dict (and move to its namespace)
886
+ # Skip trailing whitespace when applicable.
887
+ try:
888
+ char = src[pos]
889
+ except IndexError:
890
+ break
891
+ if char == '\n':
892
+ pos += 1
893
+ continue
894
+ if char in TOML_KEY_INITIAL_CHARS:
895
+ pos = toml_key_value_rule(src, pos, out, header, parse_float)
896
+ pos = toml_skip_chars(src, pos, TOML_WS)
897
+ elif char == '[':
898
+ try:
899
+ second_char: ta.Optional[str] = src[pos + 1]
900
+ except IndexError:
901
+ second_char = None
902
+ out.flags.finalize_pending()
903
+ if second_char == '[':
904
+ pos, header = toml_create_list_rule(src, pos, out)
905
+ else:
906
+ pos, header = toml_create_dict_rule(src, pos, out)
907
+ pos = toml_skip_chars(src, pos, TOML_WS)
908
+ elif char != '#':
909
+ raise toml_suffixed_err(src, pos, 'Invalid statement')
1129
910
 
1130
- def write_root(self, obj: ta.Mapping) -> None:
1131
- for i, (k, v) in enumerate(obj.items()):
1132
- if i:
1133
- self._nl()
1134
- self._w('[')
1135
- self._w(self._maybe_quote(k))
1136
- self._w(']')
1137
- self._nl()
1138
- self.write_table_contents(v)
911
+ # 3. Skip comment
912
+ pos = toml_skip_comment(src, pos)
1139
913
 
1140
- def write_table_contents(self, obj: ta.Mapping) -> None:
1141
- for k, v in obj.items():
1142
- self.write_key(k)
1143
- self._w(' = ')
1144
- self.write_value(v)
1145
- self._nl()
914
+ # 4. Expect end of line or end of file
915
+ try:
916
+ char = src[pos]
917
+ except IndexError:
918
+ break
919
+ if char != '\n':
920
+ raise toml_suffixed_err(
921
+ src, pos, 'Expected newline or end of document after a statement',
922
+ )
923
+ pos += 1
1146
924
 
1147
- def write_array(self, obj: ta.Sequence) -> None:
1148
- self._w('[')
1149
- self._nl()
1150
- self._indent += 1
1151
- for e in obj:
1152
- self.write_value(e)
1153
- self._w(',')
1154
- self._nl()
1155
- self._indent -= 1
1156
- self._w(']')
925
+ return out.data.dict
1157
926
 
1158
- def write_inline_table(self, obj: ta.Mapping) -> None:
1159
- self._w('{')
1160
- for i, (k, v) in enumerate(obj.items()):
1161
- if i:
1162
- self._w(', ')
1163
- self.write_key(k)
1164
- self._w(' = ')
1165
- self.write_value(v)
1166
- self._w('}')
1167
927
 
1168
- def write_inline_array(self, obj: ta.Sequence) -> None:
1169
- self._w('[')
1170
- for i, e in enumerate(obj):
1171
- if i:
1172
- self._w(', ')
1173
- self.write_value(e)
1174
- self._w(']')
928
+ class TomlFlags:
929
+ """Flags that map to parsed keys/namespaces."""
1175
930
 
1176
- def write_key(self, obj: ta.Any) -> None:
1177
- if isinstance(obj, TomlWriter.Literal):
1178
- self._w(obj.s)
1179
- elif isinstance(obj, str):
1180
- self._w(self._maybe_quote(obj.replace('_', '-')))
1181
- elif isinstance(obj, int):
1182
- self._w(repr(str(obj)))
1183
- else:
1184
- raise TypeError(obj)
931
+ # Marks an immutable namespace (inline array or inline table).
932
+ FROZEN = 0
933
+ # Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
934
+ EXPLICIT_NEST = 1
1185
935
 
1186
- def write_value(self, obj: ta.Any) -> None:
1187
- if isinstance(obj, bool):
1188
- self._w(str(obj).lower())
1189
- elif isinstance(obj, (str, int, float)):
1190
- self._w(repr(obj))
1191
- elif isinstance(obj, ta.Mapping):
1192
- self.write_inline_table(obj)
1193
- elif isinstance(obj, ta.Sequence):
1194
- if not obj:
1195
- self.write_inline_array(obj)
1196
- else:
1197
- self.write_array(obj)
1198
- else:
1199
- raise TypeError(obj)
936
+ def __init__(self) -> None:
937
+ self._flags: ta.Dict[str, dict] = {}
938
+ self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
1200
939
 
940
+ def add_pending(self, key: TomlKey, flag: int) -> None:
941
+ self._pending_flags.add((key, flag))
1201
942
 
1202
- ########################################
1203
- # ../../versioning/versions.py
1204
- # Copyright (c) Donald Stufft and individual contributors.
1205
- # All rights reserved.
1206
- #
1207
- # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
1208
- # following conditions are met:
1209
- #
1210
- # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
1211
- # following disclaimer.
1212
- #
1213
- # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
1214
- # following disclaimer in the documentation and/or other materials provided with the distribution.
1215
- #
1216
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
1217
- # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1218
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
1219
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
1220
- # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
1221
- # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1222
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
1223
- # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
1224
- # details.
1225
- # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/version.py
943
+ def finalize_pending(self) -> None:
944
+ for key, flag in self._pending_flags:
945
+ self.set(key, flag, recursive=False)
946
+ self._pending_flags.clear()
1226
947
 
948
+ def unset_all(self, key: TomlKey) -> None:
949
+ cont = self._flags
950
+ for k in key[:-1]:
951
+ if k not in cont:
952
+ return
953
+ cont = cont[k]['nested']
954
+ cont.pop(key[-1], None)
1227
955
 
1228
- ##
956
+ def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
957
+ cont = self._flags
958
+ key_parent, key_stem = key[:-1], key[-1]
959
+ for k in key_parent:
960
+ if k not in cont:
961
+ cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
962
+ cont = cont[k]['nested']
963
+ if key_stem not in cont:
964
+ cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
965
+ cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
1229
966
 
967
+ def is_(self, key: TomlKey, flag: int) -> bool:
968
+ if not key:
969
+ return False # document root has no flags
970
+ cont = self._flags
971
+ for k in key[:-1]:
972
+ if k not in cont:
973
+ return False
974
+ inner_cont = cont[k]
975
+ if flag in inner_cont['recursive_flags']:
976
+ return True
977
+ cont = inner_cont['nested']
978
+ key_stem = key[-1]
979
+ if key_stem in cont:
980
+ cont = cont[key_stem]
981
+ return flag in cont['flags'] or flag in cont['recursive_flags']
982
+ return False
1230
983
 
1231
- class InfinityVersionType:
1232
- def __repr__(self) -> str:
1233
- return 'Infinity'
1234
984
 
1235
- def __hash__(self) -> int:
1236
- return hash(repr(self))
985
+ class TomlNestedDict:
986
+ def __init__(self) -> None:
987
+ # The parsed content of the TOML document
988
+ self.dict: ta.Dict[str, ta.Any] = {}
1237
989
 
1238
- def __lt__(self, other: object) -> bool:
1239
- return False
990
+ def get_or_create_nest(
991
+ self,
992
+ key: TomlKey,
993
+ *,
994
+ access_lists: bool = True,
995
+ ) -> dict:
996
+ cont: ta.Any = self.dict
997
+ for k in key:
998
+ if k not in cont:
999
+ cont[k] = {}
1000
+ cont = cont[k]
1001
+ if access_lists and isinstance(cont, list):
1002
+ cont = cont[-1]
1003
+ if not isinstance(cont, dict):
1004
+ raise KeyError('There is no nest behind this key')
1005
+ return cont
1240
1006
 
1241
- def __le__(self, other: object) -> bool:
1242
- return False
1007
+ def append_nest_to_list(self, key: TomlKey) -> None:
1008
+ cont = self.get_or_create_nest(key[:-1])
1009
+ last_key = key[-1]
1010
+ if last_key in cont:
1011
+ list_ = cont[last_key]
1012
+ if not isinstance(list_, list):
1013
+ raise KeyError('An object other than list found behind this key')
1014
+ list_.append({})
1015
+ else:
1016
+ cont[last_key] = [{}]
1243
1017
 
1244
- def __eq__(self, other: object) -> bool:
1245
- return isinstance(other, self.__class__)
1246
1018
 
1247
- def __gt__(self, other: object) -> bool:
1248
- return True
1019
+ class TomlOutput(ta.NamedTuple):
1020
+ data: TomlNestedDict
1021
+ flags: TomlFlags
1249
1022
 
1250
- def __ge__(self, other: object) -> bool:
1251
- return True
1252
1023
 
1253
- def __neg__(self: object) -> 'NegativeInfinityVersionType':
1254
- return NegativeInfinityVersion
1024
+ def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
1025
+ try:
1026
+ while src[pos] in chars:
1027
+ pos += 1
1028
+ except IndexError:
1029
+ pass
1030
+ return pos
1255
1031
 
1256
1032
 
1257
- InfinityVersion = InfinityVersionType()
1033
+ def toml_skip_until(
1034
+ src: str,
1035
+ pos: TomlPos,
1036
+ expect: str,
1037
+ *,
1038
+ error_on: ta.FrozenSet[str],
1039
+ error_on_eof: bool,
1040
+ ) -> TomlPos:
1041
+ try:
1042
+ new_pos = src.index(expect, pos)
1043
+ except ValueError:
1044
+ new_pos = len(src)
1045
+ if error_on_eof:
1046
+ raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
1258
1047
 
1048
+ if not error_on.isdisjoint(src[pos:new_pos]):
1049
+ while src[pos] not in error_on:
1050
+ pos += 1
1051
+ raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
1052
+ return new_pos
1259
1053
 
1260
- class NegativeInfinityVersionType:
1261
- def __repr__(self) -> str:
1262
- return '-Infinity'
1263
1054
 
1264
- def __hash__(self) -> int:
1265
- return hash(repr(self))
1055
+ def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
1056
+ try:
1057
+ char: ta.Optional[str] = src[pos]
1058
+ except IndexError:
1059
+ char = None
1060
+ if char == '#':
1061
+ return toml_skip_until(
1062
+ src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
1063
+ )
1064
+ return pos
1266
1065
 
1267
- def __lt__(self, other: object) -> bool:
1268
- return True
1269
1066
 
1270
- def __le__(self, other: object) -> bool:
1271
- return True
1067
+ def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
1068
+ while True:
1069
+ pos_before_skip = pos
1070
+ pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
1071
+ pos = toml_skip_comment(src, pos)
1072
+ if pos == pos_before_skip:
1073
+ return pos
1272
1074
 
1273
- def __eq__(self, other: object) -> bool:
1274
- return isinstance(other, self.__class__)
1275
1075
 
1276
- def __gt__(self, other: object) -> bool:
1277
- return False
1076
+ def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
1077
+ pos += 1 # Skip "["
1078
+ pos = toml_skip_chars(src, pos, TOML_WS)
1079
+ pos, key = toml_parse_key(src, pos)
1278
1080
 
1279
- def __ge__(self, other: object) -> bool:
1280
- return False
1081
+ if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
1082
+ raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
1083
+ out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1084
+ try:
1085
+ out.data.get_or_create_nest(key)
1086
+ except KeyError:
1087
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1281
1088
 
1282
- def __neg__(self: object) -> InfinityVersionType:
1283
- return InfinityVersion
1089
+ if not src.startswith(']', pos):
1090
+ raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
1091
+ return pos + 1, key
1284
1092
 
1285
1093
 
1286
- NegativeInfinityVersion = NegativeInfinityVersionType()
1094
+ def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
1095
+ pos += 2 # Skip "[["
1096
+ pos = toml_skip_chars(src, pos, TOML_WS)
1097
+ pos, key = toml_parse_key(src, pos)
1287
1098
 
1099
+ if out.flags.is_(key, TomlFlags.FROZEN):
1100
+ raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
1101
+ # Free the namespace now that it points to another empty list item...
1102
+ out.flags.unset_all(key)
1103
+ # ...but this key precisely is still prohibited from table declaration
1104
+ out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1105
+ try:
1106
+ out.data.append_nest_to_list(key)
1107
+ except KeyError:
1108
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1288
1109
 
1289
- ##
1110
+ if not src.startswith(']]', pos):
1111
+ raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
1112
+ return pos + 2, key
1290
1113
 
1291
1114
 
1292
- class _Version(ta.NamedTuple):
1293
- epoch: int
1294
- release: ta.Tuple[int, ...]
1295
- dev: ta.Optional[ta.Tuple[str, int]]
1296
- pre: ta.Optional[ta.Tuple[str, int]]
1297
- post: ta.Optional[ta.Tuple[str, int]]
1298
- local: ta.Optional[VersionLocalType]
1115
+ def toml_key_value_rule(
1116
+ src: str,
1117
+ pos: TomlPos,
1118
+ out: TomlOutput,
1119
+ header: TomlKey,
1120
+ parse_float: TomlParseFloat,
1121
+ ) -> TomlPos:
1122
+ pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
1123
+ key_parent, key_stem = key[:-1], key[-1]
1124
+ abs_key_parent = header + key_parent
1299
1125
 
1126
+ relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
1127
+ for cont_key in relative_path_cont_keys:
1128
+ # Check that dotted key syntax does not redefine an existing table
1129
+ if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
1130
+ raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
1131
+ # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
1132
+ # table sections.
1133
+ out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
1134
+
1135
+ if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
1136
+ raise toml_suffixed_err(
1137
+ src,
1138
+ pos,
1139
+ f'Cannot mutate immutable namespace {abs_key_parent}',
1140
+ )
1141
+
1142
+ try:
1143
+ nest = out.data.get_or_create_nest(abs_key_parent)
1144
+ except KeyError:
1145
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1146
+ if key_stem in nest:
1147
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
1148
+ # Mark inline table and array namespaces recursively immutable
1149
+ if isinstance(value, (dict, list)):
1150
+ out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
1151
+ nest[key_stem] = value
1152
+ return pos
1300
1153
 
1301
- class InvalidVersion(ValueError): # noqa
1302
- pass
1303
1154
 
1155
+ def toml_parse_key_value_pair(
1156
+ src: str,
1157
+ pos: TomlPos,
1158
+ parse_float: TomlParseFloat,
1159
+ ) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
1160
+ pos, key = toml_parse_key(src, pos)
1161
+ try:
1162
+ char: ta.Optional[str] = src[pos]
1163
+ except IndexError:
1164
+ char = None
1165
+ if char != '=':
1166
+ raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
1167
+ pos += 1
1168
+ pos = toml_skip_chars(src, pos, TOML_WS)
1169
+ pos, value = toml_parse_value(src, pos, parse_float)
1170
+ return pos, key, value
1304
1171
 
1305
- class _BaseVersion:
1306
- _key: ta.Tuple[ta.Any, ...]
1307
1172
 
1308
- def __hash__(self) -> int:
1309
- return hash(self._key)
1173
+ def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
1174
+ pos, key_part = toml_parse_key_part(src, pos)
1175
+ key: TomlKey = (key_part,)
1176
+ pos = toml_skip_chars(src, pos, TOML_WS)
1177
+ while True:
1178
+ try:
1179
+ char: ta.Optional[str] = src[pos]
1180
+ except IndexError:
1181
+ char = None
1182
+ if char != '.':
1183
+ return pos, key
1184
+ pos += 1
1185
+ pos = toml_skip_chars(src, pos, TOML_WS)
1186
+ pos, key_part = toml_parse_key_part(src, pos)
1187
+ key += (key_part,)
1188
+ pos = toml_skip_chars(src, pos, TOML_WS)
1310
1189
 
1311
- def __lt__(self, other: '_BaseVersion') -> bool:
1312
- if not isinstance(other, _BaseVersion):
1313
- return NotImplemented # type: ignore
1314
- return self._key < other._key
1315
1190
 
1316
- def __le__(self, other: '_BaseVersion') -> bool:
1317
- if not isinstance(other, _BaseVersion):
1318
- return NotImplemented # type: ignore
1319
- return self._key <= other._key
1191
+ def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1192
+ try:
1193
+ char: ta.Optional[str] = src[pos]
1194
+ except IndexError:
1195
+ char = None
1196
+ if char in TOML_BARE_KEY_CHARS:
1197
+ start_pos = pos
1198
+ pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
1199
+ return pos, src[start_pos:pos]
1200
+ if char == "'":
1201
+ return toml_parse_literal_str(src, pos)
1202
+ if char == '"':
1203
+ return toml_parse_one_line_basic_str(src, pos)
1204
+ raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
1320
1205
 
1321
- def __eq__(self, other: object) -> bool:
1322
- if not isinstance(other, _BaseVersion):
1323
- return NotImplemented
1324
- return self._key == other._key
1325
1206
 
1326
- def __ge__(self, other: '_BaseVersion') -> bool:
1327
- if not isinstance(other, _BaseVersion):
1328
- return NotImplemented # type: ignore
1329
- return self._key >= other._key
1207
+ def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1208
+ pos += 1
1209
+ return toml_parse_basic_str(src, pos, multiline=False)
1330
1210
 
1331
- def __gt__(self, other: '_BaseVersion') -> bool:
1332
- if not isinstance(other, _BaseVersion):
1333
- return NotImplemented # type: ignore
1334
- return self._key > other._key
1335
1211
 
1336
- def __ne__(self, other: object) -> bool:
1337
- if not isinstance(other, _BaseVersion):
1338
- return NotImplemented
1339
- return self._key != other._key
1212
+ def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
1213
+ pos += 1
1214
+ array: list = []
1340
1215
 
1216
+ pos = toml_skip_comments_and_array_ws(src, pos)
1217
+ if src.startswith(']', pos):
1218
+ return pos + 1, array
1219
+ while True:
1220
+ pos, val = toml_parse_value(src, pos, parse_float)
1221
+ array.append(val)
1222
+ pos = toml_skip_comments_and_array_ws(src, pos)
1341
1223
 
1342
- _VERSION_PATTERN = r"""
1343
- v?
1344
- (?:
1345
- (?:(?P<epoch>[0-9]+)!)?
1346
- (?P<release>[0-9]+(?:\.[0-9]+)*)
1347
- (?P<pre>
1348
- [-_\.]?
1349
- (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
1350
- [-_\.]?
1351
- (?P<pre_n>[0-9]+)?
1352
- )?
1353
- (?P<post>
1354
- (?:-(?P<post_n1>[0-9]+))
1355
- |
1356
- (?:
1357
- [-_\.]?
1358
- (?P<post_l>post|rev|r)
1359
- [-_\.]?
1360
- (?P<post_n2>[0-9]+)?
1361
- )
1362
- )?
1363
- (?P<dev>
1364
- [-_\.]?
1365
- (?P<dev_l>dev)
1366
- [-_\.]?
1367
- (?P<dev_n>[0-9]+)?
1368
- )?
1369
- )
1370
- (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?
1371
- """
1224
+ c = src[pos:pos + 1]
1225
+ if c == ']':
1226
+ return pos + 1, array
1227
+ if c != ',':
1228
+ raise toml_suffixed_err(src, pos, 'Unclosed array')
1229
+ pos += 1
1372
1230
 
1373
- VERSION_PATTERN = _VERSION_PATTERN
1231
+ pos = toml_skip_comments_and_array_ws(src, pos)
1232
+ if src.startswith(']', pos):
1233
+ return pos + 1, array
1374
1234
 
1375
1235
 
1376
- class Version(_BaseVersion):
1377
- _regex = re.compile(r'^\s*' + VERSION_PATTERN + r'\s*$', re.VERBOSE | re.IGNORECASE)
1378
- _key: VersionCmpKey
1236
+ def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
1237
+ pos += 1
1238
+ nested_dict = TomlNestedDict()
1239
+ flags = TomlFlags()
1379
1240
 
1380
- def __init__(self, version: str) -> None:
1381
- match = self._regex.search(version)
1382
- if not match:
1383
- raise InvalidVersion(f"Invalid version: '{version}'")
1241
+ pos = toml_skip_chars(src, pos, TOML_WS)
1242
+ if src.startswith('}', pos):
1243
+ return pos + 1, nested_dict.dict
1244
+ while True:
1245
+ pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
1246
+ key_parent, key_stem = key[:-1], key[-1]
1247
+ if flags.is_(key, TomlFlags.FROZEN):
1248
+ raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
1249
+ try:
1250
+ nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
1251
+ except KeyError:
1252
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1253
+ if key_stem in nest:
1254
+ raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
1255
+ nest[key_stem] = value
1256
+ pos = toml_skip_chars(src, pos, TOML_WS)
1257
+ c = src[pos:pos + 1]
1258
+ if c == '}':
1259
+ return pos + 1, nested_dict.dict
1260
+ if c != ',':
1261
+ raise toml_suffixed_err(src, pos, 'Unclosed inline table')
1262
+ if isinstance(value, (dict, list)):
1263
+ flags.set(key, TomlFlags.FROZEN, recursive=True)
1264
+ pos += 1
1265
+ pos = toml_skip_chars(src, pos, TOML_WS)
1384
1266
 
1385
- self._version = _Version(
1386
- epoch=int(match.group('epoch')) if match.group('epoch') else 0,
1387
- release=tuple(int(i) for i in match.group('release').split('.')),
1388
- pre=_parse_letter_version(match.group('pre_l'), match.group('pre_n')),
1389
- post=_parse_letter_version(match.group('post_l'), match.group('post_n1') or match.group('post_n2')),
1390
- dev=_parse_letter_version(match.group('dev_l'), match.group('dev_n')),
1391
- local=_parse_local_version(match.group('local')),
1392
- )
1393
1267
 
1394
- self._key = _version_cmpkey(
1395
- self._version.epoch,
1396
- self._version.release,
1397
- self._version.pre,
1398
- self._version.post,
1399
- self._version.dev,
1400
- self._version.local,
1401
- )
1268
+ def toml_parse_basic_str_escape(
1269
+ src: str,
1270
+ pos: TomlPos,
1271
+ *,
1272
+ multiline: bool = False,
1273
+ ) -> ta.Tuple[TomlPos, str]:
1274
+ escape_id = src[pos:pos + 2]
1275
+ pos += 2
1276
+ if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
1277
+ # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
1278
+ # newline.
1279
+ if escape_id != '\\\n':
1280
+ pos = toml_skip_chars(src, pos, TOML_WS)
1281
+ try:
1282
+ char = src[pos]
1283
+ except IndexError:
1284
+ return pos, ''
1285
+ if char != '\n':
1286
+ raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
1287
+ pos += 1
1288
+ pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
1289
+ return pos, ''
1290
+ if escape_id == '\\u':
1291
+ return toml_parse_hex_char(src, pos, 4)
1292
+ if escape_id == '\\U':
1293
+ return toml_parse_hex_char(src, pos, 8)
1294
+ try:
1295
+ return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
1296
+ except KeyError:
1297
+ raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
1402
1298
 
1403
- def __repr__(self) -> str:
1404
- return f"<Version('{self}')>"
1405
1299
 
1406
- def __str__(self) -> str:
1407
- parts = []
1300
+ def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1301
+ return toml_parse_basic_str_escape(src, pos, multiline=True)
1408
1302
 
1409
- if self.epoch != 0:
1410
- parts.append(f'{self.epoch}!')
1411
1303
 
1412
- parts.append('.'.join(str(x) for x in self.release))
1304
+ def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
1305
+ hex_str = src[pos:pos + hex_len]
1306
+ if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
1307
+ raise toml_suffixed_err(src, pos, 'Invalid hex value')
1308
+ pos += hex_len
1309
+ hex_int = int(hex_str, 16)
1310
+ if not toml_is_unicode_scalar_value(hex_int):
1311
+ raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
1312
+ return pos, chr(hex_int)
1413
1313
 
1414
- if self.pre is not None:
1415
- parts.append(''.join(str(x) for x in self.pre))
1416
1314
 
1417
- if self.post is not None:
1418
- parts.append(f'.post{self.post}')
1315
+ def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1316
+ pos += 1 # Skip starting apostrophe
1317
+ start_pos = pos
1318
+ pos = toml_skip_until(
1319
+ src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
1320
+ )
1321
+ return pos + 1, src[start_pos:pos] # Skip ending apostrophe
1419
1322
 
1420
- if self.dev is not None:
1421
- parts.append(f'.dev{self.dev}')
1422
1323
 
1423
- if self.local is not None:
1424
- parts.append(f'+{self.local}')
1324
+ def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
1325
+ pos += 3
1326
+ if src.startswith('\n', pos):
1327
+ pos += 1
1425
1328
 
1426
- return ''.join(parts)
1329
+ if literal:
1330
+ delim = "'"
1331
+ end_pos = toml_skip_until(
1332
+ src,
1333
+ pos,
1334
+ "'''",
1335
+ error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
1336
+ error_on_eof=True,
1337
+ )
1338
+ result = src[pos:end_pos]
1339
+ pos = end_pos + 3
1340
+ else:
1341
+ delim = '"'
1342
+ pos, result = toml_parse_basic_str(src, pos, multiline=True)
1427
1343
 
1428
- @property
1429
- def epoch(self) -> int:
1430
- return self._version.epoch
1344
+ # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
1345
+ if not src.startswith(delim, pos):
1346
+ return pos, result
1347
+ pos += 1
1348
+ if not src.startswith(delim, pos):
1349
+ return pos, result + delim
1350
+ pos += 1
1351
+ return pos, result + (delim * 2)
1431
1352
 
1432
- @property
1433
- def release(self) -> ta.Tuple[int, ...]:
1434
- return self._version.release
1435
1353
 
1436
- @property
1437
- def pre(self) -> ta.Optional[ta.Tuple[str, int]]:
1438
- return self._version.pre
1354
+ def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
1355
+ if multiline:
1356
+ error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
1357
+ parse_escapes = toml_parse_basic_str_escape_multiline
1358
+ else:
1359
+ error_on = TOML_ILLEGAL_BASIC_STR_CHARS
1360
+ parse_escapes = toml_parse_basic_str_escape
1361
+ result = ''
1362
+ start_pos = pos
1363
+ while True:
1364
+ try:
1365
+ char = src[pos]
1366
+ except IndexError:
1367
+ raise toml_suffixed_err(src, pos, 'Unterminated string') from None
1368
+ if char == '"':
1369
+ if not multiline:
1370
+ return pos + 1, result + src[start_pos:pos]
1371
+ if src.startswith('"""', pos):
1372
+ return pos + 3, result + src[start_pos:pos]
1373
+ pos += 1
1374
+ continue
1375
+ if char == '\\':
1376
+ result += src[start_pos:pos]
1377
+ pos, parsed_escape = parse_escapes(src, pos)
1378
+ result += parsed_escape
1379
+ start_pos = pos
1380
+ continue
1381
+ if char in error_on:
1382
+ raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
1383
+ pos += 1
1439
1384
 
1440
- @property
1441
- def post(self) -> ta.Optional[int]:
1442
- return self._version.post[1] if self._version.post else None
1443
1385
 
1444
- @property
1445
- def dev(self) -> ta.Optional[int]:
1446
- return self._version.dev[1] if self._version.dev else None
1386
+ def toml_parse_value( # noqa: C901
1387
+ src: str,
1388
+ pos: TomlPos,
1389
+ parse_float: TomlParseFloat,
1390
+ ) -> ta.Tuple[TomlPos, ta.Any]:
1391
+ try:
1392
+ char: ta.Optional[str] = src[pos]
1393
+ except IndexError:
1394
+ char = None
1447
1395
 
1448
- @property
1449
- def local(self) -> ta.Optional[str]:
1450
- if self._version.local:
1451
- return '.'.join(str(x) for x in self._version.local)
1452
- else:
1453
- return None
1396
+ # IMPORTANT: order conditions based on speed of checking and likelihood
1454
1397
 
1455
- @property
1456
- def public(self) -> str:
1457
- return str(self).split('+', 1)[0]
1398
+ # Basic strings
1399
+ if char == '"':
1400
+ if src.startswith('"""', pos):
1401
+ return toml_parse_multiline_str(src, pos, literal=False)
1402
+ return toml_parse_one_line_basic_str(src, pos)
1458
1403
 
1459
- @property
1460
- def base_version(self) -> str:
1461
- parts = []
1404
+ # Literal strings
1405
+ if char == "'":
1406
+ if src.startswith("'''", pos):
1407
+ return toml_parse_multiline_str(src, pos, literal=True)
1408
+ return toml_parse_literal_str(src, pos)
1462
1409
 
1463
- if self.epoch != 0:
1464
- parts.append(f'{self.epoch}!')
1410
+ # Booleans
1411
+ if char == 't':
1412
+ if src.startswith('true', pos):
1413
+ return pos + 4, True
1414
+ if char == 'f':
1415
+ if src.startswith('false', pos):
1416
+ return pos + 5, False
1465
1417
 
1466
- parts.append('.'.join(str(x) for x in self.release))
1418
+ # Arrays
1419
+ if char == '[':
1420
+ return toml_parse_array(src, pos, parse_float)
1467
1421
 
1468
- return ''.join(parts)
1422
+ # Inline tables
1423
+ if char == '{':
1424
+ return toml_parse_inline_table(src, pos, parse_float)
1469
1425
 
1470
- @property
1471
- def is_prerelease(self) -> bool:
1472
- return self.dev is not None or self.pre is not None
1426
+ # Dates and times
1427
+ datetime_match = TOML_RE_DATETIME.match(src, pos)
1428
+ if datetime_match:
1429
+ try:
1430
+ datetime_obj = toml_match_to_datetime(datetime_match)
1431
+ except ValueError as e:
1432
+ raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
1433
+ return datetime_match.end(), datetime_obj
1434
+ localtime_match = TOML_RE_LOCALTIME.match(src, pos)
1435
+ if localtime_match:
1436
+ return localtime_match.end(), toml_match_to_localtime(localtime_match)
1473
1437
 
1474
- @property
1475
- def is_postrelease(self) -> bool:
1476
- return self.post is not None
1438
+ # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
1439
+ # located after handling of dates and times.
1440
+ number_match = TOML_RE_NUMBER.match(src, pos)
1441
+ if number_match:
1442
+ return number_match.end(), toml_match_to_number(number_match, parse_float)
1477
1443
 
1478
- @property
1479
- def is_devrelease(self) -> bool:
1480
- return self.dev is not None
1444
+ # Special floats
1445
+ first_three = src[pos:pos + 3]
1446
+ if first_three in {'inf', 'nan'}:
1447
+ return pos + 3, parse_float(first_three)
1448
+ first_four = src[pos:pos + 4]
1449
+ if first_four in {'-inf', '+inf', '-nan', '+nan'}:
1450
+ return pos + 4, parse_float(first_four)
1481
1451
 
1482
- @property
1483
- def major(self) -> int:
1484
- return self.release[0] if len(self.release) >= 1 else 0
1452
+ raise toml_suffixed_err(src, pos, 'Invalid value')
1485
1453
 
1486
- @property
1487
- def minor(self) -> int:
1488
- return self.release[1] if len(self.release) >= 2 else 0
1489
1454
 
1490
- @property
1491
- def micro(self) -> int:
1492
- return self.release[2] if len(self.release) >= 3 else 0
1455
+ def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
1456
+ """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
1493
1457
 
1458
+ def coord_repr(src: str, pos: TomlPos) -> str:
1459
+ if pos >= len(src):
1460
+ return 'end of document'
1461
+ line = src.count('\n', 0, pos) + 1
1462
+ if line == 1:
1463
+ column = pos + 1
1464
+ else:
1465
+ column = pos - src.rindex('\n', 0, pos)
1466
+ return f'line {line}, column {column}'
1494
1467
 
1495
- def _parse_letter_version(
1496
- letter: ta.Optional[str],
1497
- number: ta.Union[str, bytes, ta.SupportsInt, None],
1498
- ) -> ta.Optional[ta.Tuple[str, int]]:
1499
- if letter:
1500
- if number is None:
1501
- number = 0
1468
+ return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
1502
1469
 
1503
- letter = letter.lower()
1504
- if letter == 'alpha':
1505
- letter = 'a'
1506
- elif letter == 'beta':
1507
- letter = 'b'
1508
- elif letter in ['c', 'pre', 'preview']:
1509
- letter = 'rc'
1510
- elif letter in ['rev', 'r']:
1511
- letter = 'post'
1512
1470
 
1513
- return letter, int(number)
1514
- if not letter and number:
1515
- letter = 'post'
1516
- return letter, int(number)
1471
+ def toml_is_unicode_scalar_value(codepoint: int) -> bool:
1472
+ return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
1517
1473
 
1518
- return None
1519
1474
 
1475
+ def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
1476
+ """A decorator to make `parse_float` safe.
1520
1477
 
1521
- _local_version_separators = re.compile(r'[\._-]')
1478
+ `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
1479
+ thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
1480
+ """
1481
+ # The default `float` callable never returns illegal types. Optimize it.
1482
+ if parse_float is float:
1483
+ return float
1522
1484
 
1485
+ def safe_parse_float(float_str: str) -> ta.Any:
1486
+ float_value = parse_float(float_str)
1487
+ if isinstance(float_value, (dict, list)):
1488
+ raise ValueError('parse_float must not return dicts or lists') # noqa
1489
+ return float_value
1523
1490
 
1524
- def _parse_local_version(local: ta.Optional[str]) -> ta.Optional[VersionLocalType]:
1525
- if local is not None:
1526
- return tuple(
1527
- part.lower() if not part.isdigit() else int(part)
1528
- for part in _local_version_separators.split(local)
1529
- )
1530
- return None
1491
+ return safe_parse_float
1531
1492
 
1532
1493
 
1533
- def _version_cmpkey(
1534
- epoch: int,
1535
- release: ta.Tuple[int, ...],
1536
- pre: ta.Optional[ta.Tuple[str, int]],
1537
- post: ta.Optional[ta.Tuple[str, int]],
1538
- dev: ta.Optional[ta.Tuple[str, int]],
1539
- local: ta.Optional[VersionLocalType],
1540
- ) -> VersionCmpKey:
1541
- _release = tuple(reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))))
1494
+ ########################################
1495
+ # ../../toml/writer.py
1542
1496
 
1543
- if pre is None and post is None and dev is not None:
1544
- _pre: VersionCmpPrePostDevType = NegativeInfinityVersion
1545
- elif pre is None:
1546
- _pre = InfinityVersion
1547
- else:
1548
- _pre = pre
1549
1497
 
1550
- if post is None:
1551
- _post: VersionCmpPrePostDevType = NegativeInfinityVersion
1552
- else:
1553
- _post = post
1498
+ class TomlWriter:
1499
+ @dc.dataclass(frozen=True)
1500
+ class Literal:
1501
+ s: str
1554
1502
 
1555
- if dev is None:
1556
- _dev: VersionCmpPrePostDevType = InfinityVersion
1557
- else:
1558
- _dev = dev
1503
+ def __init__(self, out: ta.TextIO) -> None:
1504
+ super().__init__()
1505
+ self._out = out
1559
1506
 
1560
- if local is None:
1561
- _local: VersionCmpLocalType = NegativeInfinityVersion
1562
- else:
1563
- _local = tuple((i, '') if isinstance(i, int) else (NegativeInfinityVersion, i) for i in local)
1507
+ self._indent = 0
1508
+ self._wrote_indent = False
1564
1509
 
1565
- return epoch, _release, _pre, _post, _dev, _local
1510
+ #
1566
1511
 
1512
+ def _w(self, s: str) -> None:
1513
+ if not self._wrote_indent:
1514
+ self._out.write(' ' * self._indent)
1515
+ self._wrote_indent = True
1516
+ self._out.write(s)
1567
1517
 
1568
- ##
1518
+ def _nl(self) -> None:
1519
+ self._out.write('\n')
1520
+ self._wrote_indent = False
1569
1521
 
1522
+ def _needs_quote(self, s: str) -> bool:
1523
+ return (
1524
+ not s or
1525
+ any(c in s for c in '\'"\n') or
1526
+ s[0] not in string.ascii_letters
1527
+ )
1570
1528
 
1571
- def canonicalize_version(
1572
- version: ta.Union[Version, str],
1573
- *,
1574
- strip_trailing_zero: bool = True,
1575
- ) -> str:
1576
- if isinstance(version, str):
1577
- try:
1578
- parsed = Version(version)
1579
- except InvalidVersion:
1580
- return version
1581
- else:
1582
- parsed = version
1529
+ def _maybe_quote(self, s: str) -> str:
1530
+ if self._needs_quote(s):
1531
+ return repr(s)
1532
+ else:
1533
+ return s
1583
1534
 
1584
- parts = []
1535
+ #
1585
1536
 
1586
- if parsed.epoch != 0:
1587
- parts.append(f'{parsed.epoch}!')
1537
+ def write_root(self, obj: ta.Mapping) -> None:
1538
+ for i, (k, v) in enumerate(obj.items()):
1539
+ if i:
1540
+ self._nl()
1541
+ self._w('[')
1542
+ self._w(self._maybe_quote(k))
1543
+ self._w(']')
1544
+ self._nl()
1545
+ self.write_table_contents(v)
1588
1546
 
1589
- release_segment = '.'.join(str(x) for x in parsed.release)
1590
- if strip_trailing_zero:
1591
- release_segment = re.sub(r'(\.0)+$', '', release_segment)
1592
- parts.append(release_segment)
1547
+ def write_table_contents(self, obj: ta.Mapping) -> None:
1548
+ for k, v in obj.items():
1549
+ self.write_key(k)
1550
+ self._w(' = ')
1551
+ self.write_value(v)
1552
+ self._nl()
1593
1553
 
1594
- if parsed.pre is not None:
1595
- parts.append(''.join(str(x) for x in parsed.pre))
1554
+ def write_array(self, obj: ta.Sequence) -> None:
1555
+ self._w('[')
1556
+ self._nl()
1557
+ self._indent += 1
1558
+ for e in obj:
1559
+ self.write_value(e)
1560
+ self._w(',')
1561
+ self._nl()
1562
+ self._indent -= 1
1563
+ self._w(']')
1596
1564
 
1597
- if parsed.post is not None:
1598
- parts.append(f'.post{parsed.post}')
1565
+ def write_inline_table(self, obj: ta.Mapping) -> None:
1566
+ self._w('{')
1567
+ for i, (k, v) in enumerate(obj.items()):
1568
+ if i:
1569
+ self._w(', ')
1570
+ self.write_key(k)
1571
+ self._w(' = ')
1572
+ self.write_value(v)
1573
+ self._w('}')
1599
1574
 
1600
- if parsed.dev is not None:
1601
- parts.append(f'.dev{parsed.dev}')
1575
+ def write_inline_array(self, obj: ta.Sequence) -> None:
1576
+ self._w('[')
1577
+ for i, e in enumerate(obj):
1578
+ if i:
1579
+ self._w(', ')
1580
+ self.write_value(e)
1581
+ self._w(']')
1602
1582
 
1603
- if parsed.local is not None:
1604
- parts.append(f'+{parsed.local}')
1583
+ def write_key(self, obj: ta.Any) -> None:
1584
+ if isinstance(obj, TomlWriter.Literal):
1585
+ self._w(obj.s)
1586
+ elif isinstance(obj, str):
1587
+ self._w(self._maybe_quote(obj.replace('_', '-')))
1588
+ elif isinstance(obj, int):
1589
+ self._w(repr(str(obj)))
1590
+ else:
1591
+ raise TypeError(obj)
1605
1592
 
1606
- return ''.join(parts)
1593
+ def write_value(self, obj: ta.Any) -> None:
1594
+ if isinstance(obj, bool):
1595
+ self._w(str(obj).lower())
1596
+ elif isinstance(obj, (str, int, float)):
1597
+ self._w(repr(obj))
1598
+ elif isinstance(obj, ta.Mapping):
1599
+ self.write_inline_table(obj)
1600
+ elif isinstance(obj, ta.Sequence):
1601
+ if not obj:
1602
+ self.write_inline_array(obj)
1603
+ else:
1604
+ self.write_array(obj)
1605
+ else:
1606
+ raise TypeError(obj)
1607
1607
 
1608
1608
 
1609
1609
  ########################################
@@ -1899,6 +1899,11 @@ def check_non_empty_str(v: ta.Optional[str]) -> str:
1899
1899
  return v
1900
1900
 
1901
1901
 
1902
+ def check_state(v: bool, msg: str = 'Illegal state') -> None:
1903
+ if not v:
1904
+ raise ValueError(msg)
1905
+
1906
+
1902
1907
  ########################################
1903
1908
  # ../../../omlish/lite/json.py
1904
1909
 
@@ -2009,83 +2014,7 @@ def is_sunder(name: str) -> bool:
2009
2014
 
2010
2015
 
2011
2016
  ########################################
2012
- # ../reqs.py
2013
- """
2014
- TODO:
2015
- - embed pip._internal.req.parse_requirements, add additional env stuff? breaks compat with raw pip
2016
- """
2017
-
2018
-
2019
- class RequirementsRewriter:
2020
- def __init__(
2021
- self,
2022
- venv: ta.Optional[str] = None,
2023
- ) -> None:
2024
- super().__init__()
2025
- self._venv = venv
2026
-
2027
- @cached_nullary
2028
- def _tmp_dir(self) -> str:
2029
- return tempfile.mkdtemp('-omlish-reqs')
2030
-
2031
- VENV_MAGIC = '# @omlish-venv'
2032
-
2033
- def rewrite_file(self, in_file: str) -> str:
2034
- with open(in_file) as f:
2035
- src = f.read()
2036
-
2037
- in_lines = src.splitlines(keepends=True)
2038
- out_lines = []
2039
-
2040
- for l in in_lines:
2041
- if self.VENV_MAGIC in l:
2042
- lp, _, rp = l.partition(self.VENV_MAGIC)
2043
- rp = rp.partition('#')[0]
2044
- omit = False
2045
- for v in rp.split():
2046
- if v[0] == '!':
2047
- if self._venv is not None and self._venv == v[1:]:
2048
- omit = True
2049
- break
2050
- else:
2051
- raise NotImplementedError
2052
-
2053
- if omit:
2054
- out_lines.append('# OMITTED: ' + l)
2055
- continue
2056
-
2057
- out_req = self.rewrite(l.rstrip('\n'), for_file=True)
2058
- out_lines.append(out_req + '\n')
2059
-
2060
- out_file = os.path.join(self._tmp_dir(), os.path.basename(in_file))
2061
- if os.path.exists(out_file):
2062
- raise Exception(f'file exists: {out_file}')
2063
-
2064
- with open(out_file, 'w') as f:
2065
- f.write(''.join(out_lines))
2066
- return out_file
2067
-
2068
- def rewrite(self, in_req: str, *, for_file: bool = False) -> str:
2069
- if in_req.strip().startswith('-r'):
2070
- l = in_req.strip()
2071
- lp, _, rp = l.partition(' ')
2072
- if lp == '-r':
2073
- inc_in_file, _, rest = rp.partition(' ')
2074
- else:
2075
- inc_in_file, rest = lp[2:], rp
2076
-
2077
- inc_out_file = self.rewrite_file(inc_in_file)
2078
- if for_file:
2079
- return ' '.join(['-r ', inc_out_file, rest])
2080
- else:
2081
- return '-r' + inc_out_file
2082
-
2083
- else:
2084
- return in_req
2085
-
2086
-
2087
- ########################################
2088
- # ../../versioning/specifiers.py
2017
+ # ../../packaging/specifiers.py
2089
2018
  # Copyright (c) Donald Stufft and individual contributors.
2090
2019
  # All rights reserved.
2091
2020
  #
@@ -2606,6 +2535,82 @@ class SpecifierSet(BaseSpecifier):
2606
2535
  return iter(filtered)
2607
2536
 
2608
2537
 
2538
+ ########################################
2539
+ # ../reqs.py
2540
+ """
2541
+ TODO:
2542
+ - embed pip._internal.req.parse_requirements, add additional env stuff? breaks compat with raw pip
2543
+ """
2544
+
2545
+
2546
+ class RequirementsRewriter:
2547
+ def __init__(
2548
+ self,
2549
+ venv: ta.Optional[str] = None,
2550
+ ) -> None:
2551
+ super().__init__()
2552
+ self._venv = venv
2553
+
2554
+ @cached_nullary
2555
+ def _tmp_dir(self) -> str:
2556
+ return tempfile.mkdtemp('-omlish-reqs')
2557
+
2558
+ VENV_MAGIC = '# @omlish-venv'
2559
+
2560
+ def rewrite_file(self, in_file: str) -> str:
2561
+ with open(in_file) as f:
2562
+ src = f.read()
2563
+
2564
+ in_lines = src.splitlines(keepends=True)
2565
+ out_lines = []
2566
+
2567
+ for l in in_lines:
2568
+ if self.VENV_MAGIC in l:
2569
+ lp, _, rp = l.partition(self.VENV_MAGIC)
2570
+ rp = rp.partition('#')[0]
2571
+ omit = False
2572
+ for v in rp.split():
2573
+ if v[0] == '!':
2574
+ if self._venv is not None and self._venv == v[1:]:
2575
+ omit = True
2576
+ break
2577
+ else:
2578
+ raise NotImplementedError
2579
+
2580
+ if omit:
2581
+ out_lines.append('# OMITTED: ' + l)
2582
+ continue
2583
+
2584
+ out_req = self.rewrite(l.rstrip('\n'), for_file=True)
2585
+ out_lines.append(out_req + '\n')
2586
+
2587
+ out_file = os.path.join(self._tmp_dir(), os.path.basename(in_file))
2588
+ if os.path.exists(out_file):
2589
+ raise Exception(f'file exists: {out_file}')
2590
+
2591
+ with open(out_file, 'w') as f:
2592
+ f.write(''.join(out_lines))
2593
+ return out_file
2594
+
2595
+ def rewrite(self, in_req: str, *, for_file: bool = False) -> str:
2596
+ if in_req.strip().startswith('-r'):
2597
+ l = in_req.strip()
2598
+ lp, _, rp = l.partition(' ')
2599
+ if lp == '-r':
2600
+ inc_in_file, _, rest = rp.partition(' ')
2601
+ else:
2602
+ inc_in_file, rest = lp[2:], rp
2603
+
2604
+ inc_out_file = self.rewrite_file(inc_in_file)
2605
+ if for_file:
2606
+ return ' '.join(['-r ', inc_out_file, rest])
2607
+ else:
2608
+ return '-r' + inc_out_file
2609
+
2610
+ else:
2611
+ return in_req
2612
+
2613
+
2609
2614
  ########################################
2610
2615
  # ../../../omlish/lite/logs.py
2611
2616
  """