ominfra 0.0.0.dev268__py3-none-any.whl → 0.0.0.dev269__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ominfra/scripts/manage.py CHANGED
@@ -1433,75 +1433,6 @@ def render_ini_sections(
1433
1433
  ##
1434
1434
 
1435
1435
 
1436
- _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]*)?'
1437
-
1438
- TOML_RE_NUMBER = re.compile(
1439
- r"""
1440
- 0
1441
- (?:
1442
- x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
1443
- |
1444
- b[01](?:_?[01])* # bin
1445
- |
1446
- o[0-7](?:_?[0-7])* # oct
1447
- )
1448
- |
1449
- [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
1450
- (?P<floatpart>
1451
- (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
1452
- (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
1453
- )
1454
- """,
1455
- flags=re.VERBOSE,
1456
- )
1457
- TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
1458
- TOML_RE_DATETIME = re.compile(
1459
- rf"""
1460
- ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
1461
- (?:
1462
- [Tt ]
1463
- {_TOML_TIME_RE_STR}
1464
- (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
1465
- )?
1466
- """,
1467
- flags=re.VERBOSE,
1468
- )
1469
-
1470
-
1471
- def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
1472
- """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
1473
-
1474
- Raises ValueError if the match does not correspond to a valid date or datetime.
1475
- """
1476
- (
1477
- year_str,
1478
- month_str,
1479
- day_str,
1480
- hour_str,
1481
- minute_str,
1482
- sec_str,
1483
- micros_str,
1484
- zulu_time,
1485
- offset_sign_str,
1486
- offset_hour_str,
1487
- offset_minute_str,
1488
- ) = match.groups()
1489
- year, month, day = int(year_str), int(month_str), int(day_str)
1490
- if hour_str is None:
1491
- return datetime.date(year, month, day)
1492
- hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
1493
- micros = int(micros_str.ljust(6, '0')) if micros_str else 0
1494
- if offset_sign_str:
1495
- tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
1496
- offset_hour_str, offset_minute_str, offset_sign_str,
1497
- )
1498
- elif zulu_time:
1499
- tz = datetime.UTC
1500
- else: # local date-time
1501
- tz = None
1502
- return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
1503
-
1504
-
1505
1436
  @functools.lru_cache() # noqa
1506
1437
  def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
1507
1438
  sign = 1 if sign_str == '+' else -1
@@ -1513,47 +1444,25 @@ def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.ti
1513
1444
  )
1514
1445
 
1515
1446
 
1516
- def toml_match_to_localtime(match: re.Match) -> datetime.time:
1517
- hour_str, minute_str, sec_str, micros_str = match.groups()
1518
- micros = int(micros_str.ljust(6, '0')) if micros_str else 0
1519
- return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
1520
-
1521
-
1522
- def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
1523
- if match.group('floatpart'):
1524
- return parse_float(match.group())
1525
- return int(match.group(), 0)
1526
-
1527
-
1528
- TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
1529
-
1530
- # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
1531
- # functions.
1532
- TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
1533
- TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
1447
+ def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
1448
+ """
1449
+ A decorator to make `parse_float` safe.
1534
1450
 
1535
- TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
1536
- TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
1451
+ `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
1452
+ thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
1453
+ """
1537
1454
 
1538
- TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
1455
+ # The default `float` callable never returns illegal types. Optimize it.
1456
+ if parse_float is float:
1457
+ return float
1539
1458
 
1540
- TOML_WS = frozenset(' \t')
1541
- TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
1542
- TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
1543
- TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
1544
- TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
1459
+ def safe_parse_float(float_str: str) -> ta.Any:
1460
+ float_value = parse_float(float_str)
1461
+ if isinstance(float_value, (dict, list)):
1462
+ raise ValueError('parse_float must not return dicts or lists') # noqa
1463
+ return float_value
1545
1464
 
1546
- TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
1547
- {
1548
- '\\b': '\u0008', # backspace
1549
- '\\t': '\u0009', # tab
1550
- '\\n': '\u000A', # linefeed
1551
- '\\f': '\u000C', # form feed
1552
- '\\r': '\u000D', # carriage return
1553
- '\\"': '\u0022', # quote
1554
- '\\\\': '\u005C', # backslash
1555
- },
1556
- )
1465
+ return safe_parse_float
1557
1466
 
1558
1467
 
1559
1468
  class TomlDecodeError(ValueError):
@@ -1578,63 +1487,15 @@ def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str
1578
1487
  src = s.replace('\r\n', '\n')
1579
1488
  except (AttributeError, TypeError):
1580
1489
  raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
1581
- pos = 0
1582
- out = TomlOutput(TomlNestedDict(), TomlFlags())
1583
- header: TomlKey = ()
1584
- parse_float = toml_make_safe_parse_float(parse_float)
1585
1490
 
1586
- # Parse one statement at a time (typically means one line in TOML source)
1587
- while True:
1588
- # 1. Skip line leading whitespace
1589
- pos = toml_skip_chars(src, pos, TOML_WS)
1590
-
1591
- # 2. Parse rules. Expect one of the following:
1592
- # - end of file
1593
- # - end of line
1594
- # - comment
1595
- # - key/value pair
1596
- # - append dict to list (and move to its namespace)
1597
- # - create dict (and move to its namespace)
1598
- # Skip trailing whitespace when applicable.
1599
- try:
1600
- char = src[pos]
1601
- except IndexError:
1602
- break
1603
- if char == '\n':
1604
- pos += 1
1605
- continue
1606
- if char in TOML_KEY_INITIAL_CHARS:
1607
- pos = toml_key_value_rule(src, pos, out, header, parse_float)
1608
- pos = toml_skip_chars(src, pos, TOML_WS)
1609
- elif char == '[':
1610
- try:
1611
- second_char: ta.Optional[str] = src[pos + 1]
1612
- except IndexError:
1613
- second_char = None
1614
- out.flags.finalize_pending()
1615
- if second_char == '[':
1616
- pos, header = toml_create_list_rule(src, pos, out)
1617
- else:
1618
- pos, header = toml_create_dict_rule(src, pos, out)
1619
- pos = toml_skip_chars(src, pos, TOML_WS)
1620
- elif char != '#':
1621
- raise toml_suffixed_err(src, pos, 'Invalid statement')
1622
-
1623
- # 3. Skip comment
1624
- pos = toml_skip_comment(src, pos)
1491
+ parse_float = toml_make_safe_parse_float(parse_float)
1625
1492
 
1626
- # 4. Expect end of line or end of file
1627
- try:
1628
- char = src[pos]
1629
- except IndexError:
1630
- break
1631
- if char != '\n':
1632
- raise toml_suffixed_err(
1633
- src, pos, 'Expected newline or end of document after a statement',
1634
- )
1635
- pos += 1
1493
+ parser = TomlParser(
1494
+ src,
1495
+ parse_float=parse_float,
1496
+ )
1636
1497
 
1637
- return out.data.dict
1498
+ return parser.parse()
1638
1499
 
1639
1500
 
1640
1501
  class TomlFlags:
@@ -1646,6 +1507,8 @@ class TomlFlags:
1646
1507
  EXPLICIT_NEST = 1
1647
1508
 
1648
1509
  def __init__(self) -> None:
1510
+ super().__init__()
1511
+
1649
1512
  self._flags: ta.Dict[str, dict] = {}
1650
1513
  self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
1651
1514
 
@@ -1696,6 +1559,8 @@ class TomlFlags:
1696
1559
 
1697
1560
  class TomlNestedDict:
1698
1561
  def __init__(self) -> None:
1562
+ super().__init__()
1563
+
1699
1564
  # The parsed content of the TOML document
1700
1565
  self.dict: ta.Dict[str, ta.Any] = {}
1701
1566
 
@@ -1728,479 +1593,613 @@ class TomlNestedDict:
1728
1593
  cont[last_key] = [{}]
1729
1594
 
1730
1595
 
1731
- class TomlOutput(ta.NamedTuple):
1732
- data: TomlNestedDict
1733
- flags: TomlFlags
1734
-
1596
+ class TomlParser:
1597
+ def __init__(
1598
+ self,
1599
+ src: str,
1600
+ *,
1601
+ parse_float: TomlParseFloat = float,
1602
+ ) -> None:
1603
+ super().__init__()
1735
1604
 
1736
- def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
1737
- try:
1738
- while src[pos] in chars:
1739
- pos += 1
1740
- except IndexError:
1741
- pass
1742
- return pos
1605
+ self.src = src
1743
1606
 
1607
+ self.parse_float = parse_float
1744
1608
 
1745
- def toml_skip_until(
1746
- src: str,
1747
- pos: TomlPos,
1748
- expect: str,
1749
- *,
1750
- error_on: ta.FrozenSet[str],
1751
- error_on_eof: bool,
1752
- ) -> TomlPos:
1753
- try:
1754
- new_pos = src.index(expect, pos)
1755
- except ValueError:
1756
- new_pos = len(src)
1757
- if error_on_eof:
1758
- raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
1609
+ self.data = TomlNestedDict()
1610
+ self.flags = TomlFlags()
1611
+ self.pos = 0
1759
1612
 
1760
- if not error_on.isdisjoint(src[pos:new_pos]):
1761
- while src[pos] not in error_on:
1762
- pos += 1
1763
- raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
1764
- return new_pos
1613
+ ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
1765
1614
 
1615
+ # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the
1616
+ # parser functions.
1617
+ ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset('\t')
1618
+ ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset('\t\n')
1766
1619
 
1767
- def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
1768
- try:
1769
- char: ta.Optional[str] = src[pos]
1770
- except IndexError:
1771
- char = None
1772
- if char == '#':
1773
- return toml_skip_until(
1774
- src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
1775
- )
1776
- return pos
1620
+ ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS
1621
+ ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS
1777
1622
 
1623
+ ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS
1778
1624
 
1779
- def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
1780
- while True:
1781
- pos_before_skip = pos
1782
- pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
1783
- pos = toml_skip_comment(src, pos)
1784
- if pos == pos_before_skip:
1785
- return pos
1625
+ WS = frozenset(' \t')
1626
+ WS_AND_NEWLINE = WS | frozenset('\n')
1627
+ BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
1628
+ KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")
1629
+ HEXDIGIT_CHARS = frozenset(string.hexdigits)
1786
1630
 
1631
+ BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType({
1632
+ '\\b': '\u0008', # backspace
1633
+ '\\t': '\u0009', # tab
1634
+ '\\n': '\u000A', # linefeed
1635
+ '\\f': '\u000C', # form feed
1636
+ '\\r': '\u000D', # carriage return
1637
+ '\\"': '\u0022', # quote
1638
+ '\\\\': '\u005C', # backslash
1639
+ })
1787
1640
 
1788
- def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
1789
- pos += 1 # Skip "["
1790
- pos = toml_skip_chars(src, pos, TOML_WS)
1791
- pos, key = toml_parse_key(src, pos)
1641
+ def parse(self) -> ta.Dict[str, ta.Any]: # noqa: C901
1642
+ header: TomlKey = ()
1792
1643
 
1793
- if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
1794
- raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
1795
- out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1796
- try:
1797
- out.data.get_or_create_nest(key)
1798
- except KeyError:
1799
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1644
+ # Parse one statement at a time (typically means one line in TOML source)
1645
+ while True:
1646
+ # 1. Skip line leading whitespace
1647
+ self.skip_chars(self.WS)
1648
+
1649
+ # 2. Parse rules. Expect one of the following:
1650
+ # - end of file
1651
+ # - end of line
1652
+ # - comment
1653
+ # - key/value pair
1654
+ # - append dict to list (and move to its namespace)
1655
+ # - create dict (and move to its namespace)
1656
+ # Skip trailing whitespace when applicable.
1657
+ try:
1658
+ char = self.src[self.pos]
1659
+ except IndexError:
1660
+ break
1661
+ if char == '\n':
1662
+ self.pos += 1
1663
+ continue
1664
+ if char in self.KEY_INITIAL_CHARS:
1665
+ self.key_value_rule(header)
1666
+ self.skip_chars(self.WS)
1667
+ elif char == '[':
1668
+ try:
1669
+ second_char: ta.Optional[str] = self.src[self.pos + 1]
1670
+ except IndexError:
1671
+ second_char = None
1672
+ self.flags.finalize_pending()
1673
+ if second_char == '[':
1674
+ header = self.create_list_rule()
1675
+ else:
1676
+ header = self.create_dict_rule()
1677
+ self.skip_chars(self.WS)
1678
+ elif char != '#':
1679
+ raise self.suffixed_err('Invalid statement')
1800
1680
 
1801
- if not src.startswith(']', pos):
1802
- raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
1803
- return pos + 1, key
1681
+ # 3. Skip comment
1682
+ self.skip_comment()
1804
1683
 
1684
+ # 4. Expect end of line or end of file
1685
+ try:
1686
+ char = self.src[self.pos]
1687
+ except IndexError:
1688
+ break
1689
+ if char != '\n':
1690
+ raise self.suffixed_err('Expected newline or end of document after a statement')
1691
+ self.pos += 1
1805
1692
 
1806
- def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
1807
- pos += 2 # Skip "[["
1808
- pos = toml_skip_chars(src, pos, TOML_WS)
1809
- pos, key = toml_parse_key(src, pos)
1693
+ return self.data.dict
1810
1694
 
1811
- if out.flags.is_(key, TomlFlags.FROZEN):
1812
- raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
1813
- # Free the namespace now that it points to another empty list item...
1814
- out.flags.unset_all(key)
1815
- # ...but this key precisely is still prohibited from table declaration
1816
- out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1817
- try:
1818
- out.data.append_nest_to_list(key)
1819
- except KeyError:
1820
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1695
+ def skip_chars(self, chars: ta.Iterable[str]) -> None:
1696
+ try:
1697
+ while self.src[self.pos] in chars:
1698
+ self.pos += 1
1699
+ except IndexError:
1700
+ pass
1821
1701
 
1822
- if not src.startswith(']]', pos):
1823
- raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
1824
- return pos + 2, key
1702
+ def skip_until(
1703
+ self,
1704
+ expect: str,
1705
+ *,
1706
+ error_on: ta.FrozenSet[str],
1707
+ error_on_eof: bool,
1708
+ ) -> None:
1709
+ try:
1710
+ new_pos = self.src.index(expect, self.pos)
1711
+ except ValueError:
1712
+ new_pos = len(self.src)
1713
+ if error_on_eof:
1714
+ raise self.suffixed_err(f'Expected {expect!r}', pos=new_pos) from None
1825
1715
 
1716
+ if not error_on.isdisjoint(self.src[self.pos:new_pos]):
1717
+ while self.src[self.pos] not in error_on:
1718
+ self.pos += 1
1719
+ raise self.suffixed_err(f'Found invalid character {self.src[self.pos]!r}')
1720
+ self.pos = new_pos
1826
1721
 
1827
- def toml_key_value_rule(
1828
- src: str,
1829
- pos: TomlPos,
1830
- out: TomlOutput,
1831
- header: TomlKey,
1832
- parse_float: TomlParseFloat,
1833
- ) -> TomlPos:
1834
- pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
1835
- key_parent, key_stem = key[:-1], key[-1]
1836
- abs_key_parent = header + key_parent
1837
-
1838
- relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
1839
- for cont_key in relative_path_cont_keys:
1840
- # Check that dotted key syntax does not redefine an existing table
1841
- if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
1842
- raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
1843
- # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
1844
- # table sections.
1845
- out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
1846
-
1847
- if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
1848
- raise toml_suffixed_err(
1849
- src,
1850
- pos,
1851
- f'Cannot mutate immutable namespace {abs_key_parent}',
1852
- )
1722
+ def skip_comment(self) -> None:
1723
+ try:
1724
+ char: ta.Optional[str] = self.src[self.pos]
1725
+ except IndexError:
1726
+ char = None
1727
+ if char == '#':
1728
+ self.pos += 1
1729
+ self.skip_until(
1730
+ '\n',
1731
+ error_on=self.ILLEGAL_COMMENT_CHARS,
1732
+ error_on_eof=False,
1733
+ )
1853
1734
 
1854
- try:
1855
- nest = out.data.get_or_create_nest(abs_key_parent)
1856
- except KeyError:
1857
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1858
- if key_stem in nest:
1859
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
1860
- # Mark inline table and array namespaces recursively immutable
1861
- if isinstance(value, (dict, list)):
1862
- out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
1863
- nest[key_stem] = value
1864
- return pos
1735
+ def skip_comments_and_array_ws(self) -> None:
1736
+ while True:
1737
+ pos_before_skip = self.pos
1738
+ self.skip_chars(self.WS_AND_NEWLINE)
1739
+ self.skip_comment()
1740
+ if self.pos == pos_before_skip:
1741
+ return
1865
1742
 
1743
+ def create_dict_rule(self) -> TomlKey:
1744
+ self.pos += 1 # Skip "["
1745
+ self.skip_chars(self.WS)
1746
+ key = self.parse_key()
1866
1747
 
1867
- def toml_parse_key_value_pair(
1868
- src: str,
1869
- pos: TomlPos,
1870
- parse_float: TomlParseFloat,
1871
- ) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
1872
- pos, key = toml_parse_key(src, pos)
1873
- try:
1874
- char: ta.Optional[str] = src[pos]
1875
- except IndexError:
1876
- char = None
1877
- if char != '=':
1878
- raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
1879
- pos += 1
1880
- pos = toml_skip_chars(src, pos, TOML_WS)
1881
- pos, value = toml_parse_value(src, pos, parse_float)
1882
- return pos, key, value
1883
-
1884
-
1885
- def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
1886
- pos, key_part = toml_parse_key_part(src, pos)
1887
- key: TomlKey = (key_part,)
1888
- pos = toml_skip_chars(src, pos, TOML_WS)
1889
- while True:
1748
+ if self.flags.is_(key, TomlFlags.EXPLICIT_NEST) or self.flags.is_(key, TomlFlags.FROZEN):
1749
+ raise self.suffixed_err(f'Cannot declare {key} twice')
1750
+ self.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1890
1751
  try:
1891
- char: ta.Optional[str] = src[pos]
1892
- except IndexError:
1893
- char = None
1894
- if char != '.':
1895
- return pos, key
1896
- pos += 1
1897
- pos = toml_skip_chars(src, pos, TOML_WS)
1898
- pos, key_part = toml_parse_key_part(src, pos)
1899
- key += (key_part,)
1900
- pos = toml_skip_chars(src, pos, TOML_WS)
1752
+ self.data.get_or_create_nest(key)
1753
+ except KeyError:
1754
+ raise self.suffixed_err('Cannot overwrite a value') from None
1755
+
1756
+ if not self.src.startswith(']', self.pos):
1757
+ raise self.suffixed_err("Expected ']' at the end of a table declaration")
1758
+ self.pos += 1
1759
+ return key
1760
+
1761
+ def create_list_rule(self) -> TomlKey:
1762
+ self.pos += 2 # Skip "[["
1763
+ self.skip_chars(self.WS)
1764
+ key = self.parse_key()
1765
+
1766
+ if self.flags.is_(key, TomlFlags.FROZEN):
1767
+ raise self.suffixed_err(f'Cannot mutate immutable namespace {key}')
1768
+ # Free the namespace now that it points to another empty list item...
1769
+ self.flags.unset_all(key)
1770
+ # ...but this key precisely is still prohibited from table declaration
1771
+ self.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
1772
+ try:
1773
+ self.data.append_nest_to_list(key)
1774
+ except KeyError:
1775
+ raise self.suffixed_err('Cannot overwrite a value') from None
1901
1776
 
1777
+ if not self.src.startswith(']]', self.pos):
1778
+ raise self.suffixed_err("Expected ']]' at the end of an array declaration")
1779
+ self.pos += 2
1780
+ return key
1902
1781
 
1903
- def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1904
- try:
1905
- char: ta.Optional[str] = src[pos]
1906
- except IndexError:
1907
- char = None
1908
- if char in TOML_BARE_KEY_CHARS:
1909
- start_pos = pos
1910
- pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
1911
- return pos, src[start_pos:pos]
1912
- if char == "'":
1913
- return toml_parse_literal_str(src, pos)
1914
- if char == '"':
1915
- return toml_parse_one_line_basic_str(src, pos)
1916
- raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
1917
-
1918
-
1919
- def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
1920
- pos += 1
1921
- return toml_parse_basic_str(src, pos, multiline=False)
1922
-
1923
-
1924
- def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
1925
- pos += 1
1926
- array: list = []
1927
-
1928
- pos = toml_skip_comments_and_array_ws(src, pos)
1929
- if src.startswith(']', pos):
1930
- return pos + 1, array
1931
- while True:
1932
- pos, val = toml_parse_value(src, pos, parse_float)
1933
- array.append(val)
1934
- pos = toml_skip_comments_and_array_ws(src, pos)
1935
-
1936
- c = src[pos:pos + 1]
1937
- if c == ']':
1938
- return pos + 1, array
1939
- if c != ',':
1940
- raise toml_suffixed_err(src, pos, 'Unclosed array')
1941
- pos += 1
1942
-
1943
- pos = toml_skip_comments_and_array_ws(src, pos)
1944
- if src.startswith(']', pos):
1945
- return pos + 1, array
1946
-
1947
-
1948
- def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
1949
- pos += 1
1950
- nested_dict = TomlNestedDict()
1951
- flags = TomlFlags()
1952
-
1953
- pos = toml_skip_chars(src, pos, TOML_WS)
1954
- if src.startswith('}', pos):
1955
- return pos + 1, nested_dict.dict
1956
- while True:
1957
- pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
1782
+ def key_value_rule(self, header: TomlKey) -> None:
1783
+ key, value = self.parse_key_value_pair()
1958
1784
  key_parent, key_stem = key[:-1], key[-1]
1959
- if flags.is_(key, TomlFlags.FROZEN):
1960
- raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
1785
+ abs_key_parent = header + key_parent
1786
+
1787
+ relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
1788
+ for cont_key in relative_path_cont_keys:
1789
+ # Check that dotted key syntax does not redefine an existing table
1790
+ if self.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
1791
+ raise self.suffixed_err(f'Cannot redefine namespace {cont_key}')
1792
+ # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in
1793
+ # following table sections.
1794
+ self.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
1795
+
1796
+ if self.flags.is_(abs_key_parent, TomlFlags.FROZEN):
1797
+ raise self.suffixed_err(f'Cannot mutate immutable namespace {abs_key_parent}')
1798
+
1961
1799
  try:
1962
- nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
1800
+ nest = self.data.get_or_create_nest(abs_key_parent)
1963
1801
  except KeyError:
1964
- raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
1802
+ raise self.suffixed_err('Cannot overwrite a value') from None
1965
1803
  if key_stem in nest:
1966
- raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
1967
- nest[key_stem] = value
1968
- pos = toml_skip_chars(src, pos, TOML_WS)
1969
- c = src[pos:pos + 1]
1970
- if c == '}':
1971
- return pos + 1, nested_dict.dict
1972
- if c != ',':
1973
- raise toml_suffixed_err(src, pos, 'Unclosed inline table')
1804
+ raise self.suffixed_err('Cannot overwrite a value')
1805
+ # Mark inline table and array namespaces recursively immutable
1974
1806
  if isinstance(value, (dict, list)):
1975
- flags.set(key, TomlFlags.FROZEN, recursive=True)
1976
- pos += 1
1977
- pos = toml_skip_chars(src, pos, TOML_WS)
1978
-
1807
+ self.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
1808
+ nest[key_stem] = value
1979
1809
 
1980
- def toml_parse_basic_str_escape(
1981
- src: str,
1982
- pos: TomlPos,
1983
- *,
1984
- multiline: bool = False,
1985
- ) -> ta.Tuple[TomlPos, str]:
1986
- escape_id = src[pos:pos + 2]
1987
- pos += 2
1988
- if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
1989
- # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
1990
- # newline.
1991
- if escape_id != '\\\n':
1992
- pos = toml_skip_chars(src, pos, TOML_WS)
1810
+ def parse_key_value_pair(self) -> ta.Tuple[TomlKey, ta.Any]:
1811
+ key = self.parse_key()
1812
+ try:
1813
+ char: ta.Optional[str] = self.src[self.pos]
1814
+ except IndexError:
1815
+ char = None
1816
+ if char != '=':
1817
+ raise self.suffixed_err("Expected '=' after a key in a key/value pair")
1818
+ self.pos += 1
1819
+ self.skip_chars(self.WS)
1820
+ value = self.parse_value()
1821
+ return key, value
1822
+
1823
+ def parse_key(self) -> TomlKey:
1824
+ key_part = self.parse_key_part()
1825
+ key: TomlKey = (key_part,)
1826
+ self.skip_chars(self.WS)
1827
+ while True:
1993
1828
  try:
1994
- char = src[pos]
1829
+ char: ta.Optional[str] = self.src[self.pos]
1995
1830
  except IndexError:
1996
- return pos, ''
1997
- if char != '\n':
1998
- raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
1999
- pos += 1
2000
- pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
2001
- return pos, ''
2002
- if escape_id == '\\u':
2003
- return toml_parse_hex_char(src, pos, 4)
2004
- if escape_id == '\\U':
2005
- return toml_parse_hex_char(src, pos, 8)
2006
- try:
2007
- return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
2008
- except KeyError:
2009
- raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
1831
+ char = None
1832
+ if char != '.':
1833
+ return key
1834
+ self.pos += 1
1835
+ self.skip_chars(self.WS)
1836
+ key_part = self.parse_key_part()
1837
+ key += (key_part,)
1838
+ self.skip_chars(self.WS)
1839
+
1840
+ def parse_key_part(self) -> str:
1841
+ try:
1842
+ char: ta.Optional[str] = self.src[self.pos]
1843
+ except IndexError:
1844
+ char = None
1845
+ if char in self.BARE_KEY_CHARS:
1846
+ start_pos = self.pos
1847
+ self.skip_chars(self.BARE_KEY_CHARS)
1848
+ return self.src[start_pos:self.pos]
1849
+ if char == "'":
1850
+ return self.parse_literal_str()
1851
+ if char == '"':
1852
+ return self.parse_one_line_basic_str()
1853
+ raise self.suffixed_err('Invalid initial character for a key part')
2010
1854
 
1855
+ def parse_one_line_basic_str(self) -> str:
1856
+ self.pos += 1
1857
+ return self.parse_basic_str(multiline=False)
2011
1858
 
2012
- def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
2013
- return toml_parse_basic_str_escape(src, pos, multiline=True)
1859
+ def parse_array(self) -> list:
1860
+ self.pos += 1
1861
+ array: list = []
2014
1862
 
1863
+ self.skip_comments_and_array_ws()
1864
+ if self.src.startswith(']', self.pos):
1865
+ self.pos += 1
1866
+ return array
1867
+ while True:
1868
+ val = self.parse_value()
1869
+ array.append(val)
1870
+ self.skip_comments_and_array_ws()
1871
+
1872
+ c = self.src[self.pos:self.pos + 1]
1873
+ if c == ']':
1874
+ self.pos += 1
1875
+ return array
1876
+ if c != ',':
1877
+ raise self.suffixed_err('Unclosed array')
1878
+ self.pos += 1
1879
+
1880
+ self.skip_comments_and_array_ws()
1881
+ if self.src.startswith(']', self.pos):
1882
+ self.pos += 1
1883
+ return array
1884
+
1885
+ def parse_inline_table(self) -> dict:
1886
+ self.pos += 1
1887
+ nested_dict = TomlNestedDict()
1888
+ flags = TomlFlags()
1889
+
1890
+ self.skip_chars(self.WS)
1891
+ if self.src.startswith('}', self.pos):
1892
+ self.pos += 1
1893
+ return nested_dict.dict
1894
+ while True:
1895
+ key, value = self.parse_key_value_pair()
1896
+ key_parent, key_stem = key[:-1], key[-1]
1897
+ if flags.is_(key, TomlFlags.FROZEN):
1898
+ raise self.suffixed_err(f'Cannot mutate immutable namespace {key}')
1899
+ try:
1900
+ nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
1901
+ except KeyError:
1902
+ raise self.suffixed_err('Cannot overwrite a value') from None
1903
+ if key_stem in nest:
1904
+ raise self.suffixed_err(f'Duplicate inline table key {key_stem!r}')
1905
+ nest[key_stem] = value
1906
+ self.skip_chars(self.WS)
1907
+ c = self.src[self.pos:self.pos + 1]
1908
+ if c == '}':
1909
+ self.pos += 1
1910
+ return nested_dict.dict
1911
+ if c != ',':
1912
+ raise self.suffixed_err('Unclosed inline table')
1913
+ if isinstance(value, (dict, list)):
1914
+ flags.set(key, TomlFlags.FROZEN, recursive=True)
1915
+ self.pos += 1
1916
+ self.skip_chars(self.WS)
1917
+
1918
+ def parse_basic_str_escape(self, multiline: bool = False) -> str:
1919
+ escape_id = self.src[self.pos:self.pos + 2]
1920
+ self.pos += 2
1921
+ if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
1922
+ # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found
1923
+ # before newline.
1924
+ if escape_id != '\\\n':
1925
+ self.skip_chars(self.WS)
1926
+ try:
1927
+ char = self.src[self.pos]
1928
+ except IndexError:
1929
+ return ''
1930
+ if char != '\n':
1931
+ raise self.suffixed_err("Unescaped '\\' in a string")
1932
+ self.pos += 1
1933
+ self.skip_chars(self.WS_AND_NEWLINE)
1934
+ return ''
1935
+ if escape_id == '\\u':
1936
+ return self.parse_hex_char(4)
1937
+ if escape_id == '\\U':
1938
+ return self.parse_hex_char(8)
1939
+ try:
1940
+ return self.BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
1941
+ except KeyError:
1942
+ raise self.suffixed_err("Unescaped '\\' in a string") from None
2015
1943
 
2016
- def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
2017
- hex_str = src[pos:pos + hex_len]
2018
- if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
2019
- raise toml_suffixed_err(src, pos, 'Invalid hex value')
2020
- pos += hex_len
2021
- hex_int = int(hex_str, 16)
2022
- if not toml_is_unicode_scalar_value(hex_int):
2023
- raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
2024
- return pos, chr(hex_int)
1944
+ def parse_basic_str_escape_multiline(self) -> str:
1945
+ return self.parse_basic_str_escape(multiline=True)
2025
1946
 
1947
+ @classmethod
1948
+ def is_unicode_scalar_value(cls, codepoint: int) -> bool:
1949
+ return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
1950
+
1951
+ def parse_hex_char(self, hex_len: int) -> str:
1952
+ hex_str = self.src[self.pos:self.pos + hex_len]
1953
+ if len(hex_str) != hex_len or not self.HEXDIGIT_CHARS.issuperset(hex_str):
1954
+ raise self.suffixed_err('Invalid hex value')
1955
+ self.pos += hex_len
1956
+ hex_int = int(hex_str, 16)
1957
+ if not self.is_unicode_scalar_value(hex_int):
1958
+ raise self.suffixed_err('Escaped character is not a Unicode scalar value')
1959
+ return chr(hex_int)
1960
+
1961
+ def parse_literal_str(self) -> str:
1962
+ self.pos += 1 # Skip starting apostrophe
1963
+ start_pos = self.pos
1964
+ self.skip_until("'", error_on=self.ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True)
1965
+ end_pos = self.pos
1966
+ self.pos += 1
1967
+ return self.src[start_pos:end_pos] # Skip ending apostrophe
1968
+
1969
+ def parse_multiline_str(self, *, literal: bool) -> str:
1970
+ self.pos += 3
1971
+ if self.src.startswith('\n', self.pos):
1972
+ self.pos += 1
1973
+
1974
+ if literal:
1975
+ delim = "'"
1976
+ start_pos = self.pos
1977
+ self.skip_until(
1978
+ "'''",
1979
+ error_on=self.ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
1980
+ error_on_eof=True,
1981
+ )
1982
+ result = self.src[start_pos:self.pos]
1983
+ self.pos += 3
1984
+ else:
1985
+ delim = '"'
1986
+ result = self.parse_basic_str(multiline=True)
1987
+
1988
+ # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
1989
+ if not self.src.startswith(delim, self.pos):
1990
+ return result
1991
+ self.pos += 1
1992
+ if not self.src.startswith(delim, self.pos):
1993
+ return result + delim
1994
+ self.pos += 1
1995
+ return result + (delim * 2)
1996
+
1997
+ def parse_basic_str(self, *, multiline: bool) -> str:
1998
+ if multiline:
1999
+ error_on = self.ILLEGAL_MULTILINE_BASIC_STR_CHARS
2000
+ parse_escapes = self.parse_basic_str_escape_multiline
2001
+ else:
2002
+ error_on = self.ILLEGAL_BASIC_STR_CHARS
2003
+ parse_escapes = self.parse_basic_str_escape
2004
+ result = ''
2005
+ start_pos = self.pos
2006
+ while True:
2007
+ try:
2008
+ char = self.src[self.pos]
2009
+ except IndexError:
2010
+ raise self.suffixed_err('Unterminated string') from None
2011
+ if char == '"':
2012
+ if not multiline:
2013
+ end_pos = self.pos
2014
+ self.pos += 1
2015
+ return result + self.src[start_pos:end_pos]
2016
+ if self.src.startswith('"""', self.pos):
2017
+ end_pos = self.pos
2018
+ self.pos += 3
2019
+ return result + self.src[start_pos:end_pos]
2020
+ self.pos += 1
2021
+ continue
2022
+ if char == '\\':
2023
+ result += self.src[start_pos:self.pos]
2024
+ parsed_escape = parse_escapes()
2025
+ result += parsed_escape
2026
+ start_pos = self.pos
2027
+ continue
2028
+ if char in error_on:
2029
+ raise self.suffixed_err(f'Illegal character {char!r}')
2030
+ self.pos += 1
2026
2031
 
2027
- def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
2028
- pos += 1 # Skip starting apostrophe
2029
- start_pos = pos
2030
- pos = toml_skip_until(
2031
- src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
2032
- )
2033
- return pos + 1, src[start_pos:pos] # Skip ending apostrophe
2034
-
2035
-
2036
- def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
2037
- pos += 3
2038
- if src.startswith('\n', pos):
2039
- pos += 1
2040
-
2041
- if literal:
2042
- delim = "'"
2043
- end_pos = toml_skip_until(
2044
- src,
2045
- pos,
2046
- "'''",
2047
- error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
2048
- error_on_eof=True,
2049
- )
2050
- result = src[pos:end_pos]
2051
- pos = end_pos + 3
2052
- else:
2053
- delim = '"'
2054
- pos, result = toml_parse_basic_str(src, pos, multiline=True)
2055
-
2056
- # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
2057
- if not src.startswith(delim, pos):
2058
- return pos, result
2059
- pos += 1
2060
- if not src.startswith(delim, pos):
2061
- return pos, result + delim
2062
- pos += 1
2063
- return pos, result + (delim * 2)
2064
-
2065
-
2066
- def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
2067
- if multiline:
2068
- error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
2069
- parse_escapes = toml_parse_basic_str_escape_multiline
2070
- else:
2071
- error_on = TOML_ILLEGAL_BASIC_STR_CHARS
2072
- parse_escapes = toml_parse_basic_str_escape
2073
- result = ''
2074
- start_pos = pos
2075
- while True:
2032
+ def parse_value(self) -> ta.Any: # noqa: C901
2076
2033
  try:
2077
- char = src[pos]
2034
+ char: ta.Optional[str] = self.src[self.pos]
2078
2035
  except IndexError:
2079
- raise toml_suffixed_err(src, pos, 'Unterminated string') from None
2036
+ char = None
2037
+
2038
+ # IMPORTANT: order conditions based on speed of checking and likelihood
2039
+
2040
+ # Basic strings
2080
2041
  if char == '"':
2081
- if not multiline:
2082
- return pos + 1, result + src[start_pos:pos]
2083
- if src.startswith('"""', pos):
2084
- return pos + 3, result + src[start_pos:pos]
2085
- pos += 1
2086
- continue
2087
- if char == '\\':
2088
- result += src[start_pos:pos]
2089
- pos, parsed_escape = parse_escapes(src, pos)
2090
- result += parsed_escape
2091
- start_pos = pos
2092
- continue
2093
- if char in error_on:
2094
- raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
2095
- pos += 1
2042
+ if self.src.startswith('"""', self.pos):
2043
+ return self.parse_multiline_str(literal=False)
2044
+ return self.parse_one_line_basic_str()
2045
+
2046
+ # Literal strings
2047
+ if char == "'":
2048
+ if self.src.startswith("'''", self.pos):
2049
+ return self.parse_multiline_str(literal=True)
2050
+ return self.parse_literal_str()
2051
+
2052
+ # Booleans
2053
+ if char == 't':
2054
+ if self.src.startswith('true', self.pos):
2055
+ self.pos += 4
2056
+ return True
2057
+ if char == 'f':
2058
+ if self.src.startswith('false', self.pos):
2059
+ self.pos += 5
2060
+ return False
2096
2061
 
2062
+ # Arrays
2063
+ if char == '[':
2064
+ return self.parse_array()
2097
2065
 
2098
- def toml_parse_value( # noqa: C901
2099
- src: str,
2100
- pos: TomlPos,
2101
- parse_float: TomlParseFloat,
2102
- ) -> ta.Tuple[TomlPos, ta.Any]:
2103
- try:
2104
- char: ta.Optional[str] = src[pos]
2105
- except IndexError:
2106
- char = None
2107
-
2108
- # IMPORTANT: order conditions based on speed of checking and likelihood
2109
-
2110
- # Basic strings
2111
- if char == '"':
2112
- if src.startswith('"""', pos):
2113
- return toml_parse_multiline_str(src, pos, literal=False)
2114
- return toml_parse_one_line_basic_str(src, pos)
2115
-
2116
- # Literal strings
2117
- if char == "'":
2118
- if src.startswith("'''", pos):
2119
- return toml_parse_multiline_str(src, pos, literal=True)
2120
- return toml_parse_literal_str(src, pos)
2121
-
2122
- # Booleans
2123
- if char == 't':
2124
- if src.startswith('true', pos):
2125
- return pos + 4, True
2126
- if char == 'f':
2127
- if src.startswith('false', pos):
2128
- return pos + 5, False
2129
-
2130
- # Arrays
2131
- if char == '[':
2132
- return toml_parse_array(src, pos, parse_float)
2133
-
2134
- # Inline tables
2135
- if char == '{':
2136
- return toml_parse_inline_table(src, pos, parse_float)
2137
-
2138
- # Dates and times
2139
- datetime_match = TOML_RE_DATETIME.match(src, pos)
2140
- if datetime_match:
2141
- try:
2142
- datetime_obj = toml_match_to_datetime(datetime_match)
2143
- except ValueError as e:
2144
- raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
2145
- return datetime_match.end(), datetime_obj
2146
- localtime_match = TOML_RE_LOCALTIME.match(src, pos)
2147
- if localtime_match:
2148
- return localtime_match.end(), toml_match_to_localtime(localtime_match)
2149
-
2150
- # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
2151
- # located after handling of dates and times.
2152
- number_match = TOML_RE_NUMBER.match(src, pos)
2153
- if number_match:
2154
- return number_match.end(), toml_match_to_number(number_match, parse_float)
2155
-
2156
- # Special floats
2157
- first_three = src[pos:pos + 3]
2158
- if first_three in {'inf', 'nan'}:
2159
- return pos + 3, parse_float(first_three)
2160
- first_four = src[pos:pos + 4]
2161
- if first_four in {'-inf', '+inf', '-nan', '+nan'}:
2162
- return pos + 4, parse_float(first_four)
2163
-
2164
- raise toml_suffixed_err(src, pos, 'Invalid value')
2165
-
2166
-
2167
- def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
2168
- """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
2169
-
2170
- def coord_repr(src: str, pos: TomlPos) -> str:
2171
- if pos >= len(src):
2066
+ # Inline tables
2067
+ if char == '{':
2068
+ return self.parse_inline_table()
2069
+
2070
+ # Dates and times
2071
+ datetime_match = self.RE_DATETIME.match(self.src, self.pos)
2072
+ if datetime_match:
2073
+ try:
2074
+ datetime_obj = self.match_to_datetime(datetime_match)
2075
+ except ValueError as e:
2076
+ raise self.suffixed_err('Invalid date or datetime') from e
2077
+ self.pos = datetime_match.end()
2078
+ return datetime_obj
2079
+ localtime_match = self.RE_LOCALTIME.match(self.src, self.pos)
2080
+ if localtime_match:
2081
+ self.pos = localtime_match.end()
2082
+ return self.match_to_localtime(localtime_match)
2083
+
2084
+ # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to
2085
+ # be located after handling of dates and times.
2086
+ number_match = self.RE_NUMBER.match(self.src, self.pos)
2087
+ if number_match:
2088
+ self.pos = number_match.end()
2089
+ return self.match_to_number(number_match, self.parse_float)
2090
+
2091
+ # Special floats
2092
+ first_three = self.src[self.pos:self.pos + 3]
2093
+ if first_three in {'inf', 'nan'}:
2094
+ self.pos += 3
2095
+ return self.parse_float(first_three)
2096
+ first_four = self.src[self.pos:self.pos + 4]
2097
+ if first_four in {'-inf', '+inf', '-nan', '+nan'}:
2098
+ self.pos += 4
2099
+ return self.parse_float(first_four)
2100
+
2101
+ raise self.suffixed_err('Invalid value')
2102
+
2103
+ def coord_repr(self, pos: TomlPos) -> str:
2104
+ if pos >= len(self.src):
2172
2105
  return 'end of document'
2173
- line = src.count('\n', 0, pos) + 1
2106
+ line = self.src.count('\n', 0, pos) + 1
2174
2107
  if line == 1:
2175
2108
  column = pos + 1
2176
2109
  else:
2177
- column = pos - src.rindex('\n', 0, pos)
2110
+ column = pos - self.src.rindex('\n', 0, pos)
2178
2111
  return f'line {line}, column {column}'
2179
2112
 
2180
- return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
2113
+ def suffixed_err(self, msg: str, *, pos: ta.Optional[TomlPos] = None) -> TomlDecodeError:
2114
+ """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
2181
2115
 
2116
+ if pos is None:
2117
+ pos = self.pos
2118
+ return TomlDecodeError(f'{msg} (at {self.coord_repr(pos)})')
2182
2119
 
2183
- def toml_is_unicode_scalar_value(codepoint: int) -> bool:
2184
- return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
2120
+ _TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
2185
2121
 
2122
+ RE_NUMBER = re.compile(
2123
+ r"""
2124
+ 0
2125
+ (?:
2126
+ x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
2127
+ |
2128
+ b[01](?:_?[01])* # bin
2129
+ |
2130
+ o[0-7](?:_?[0-7])* # oct
2131
+ )
2132
+ |
2133
+ [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
2134
+ (?P<floatpart>
2135
+ (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
2136
+ (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
2137
+ )
2138
+ """,
2139
+ flags=re.VERBOSE,
2140
+ )
2186
2141
 
2187
- def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
2188
- """A decorator to make `parse_float` safe.
2142
+ RE_LOCALTIME = re.compile(_TIME_RE_STR)
2189
2143
 
2190
- `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
2191
- thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
2192
- """
2193
- # The default `float` callable never returns illegal types. Optimize it.
2194
- if parse_float is float:
2195
- return float
2144
+ RE_DATETIME = re.compile(
2145
+ rf"""
2146
+ ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
2147
+ (?:
2148
+ [Tt ]
2149
+ {_TIME_RE_STR}
2150
+ (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
2151
+ )?
2152
+ """,
2153
+ flags=re.VERBOSE,
2154
+ )
2196
2155
 
2197
- def safe_parse_float(float_str: str) -> ta.Any:
2198
- float_value = parse_float(float_str)
2199
- if isinstance(float_value, (dict, list)):
2200
- raise ValueError('parse_float must not return dicts or lists') # noqa
2201
- return float_value
2156
+ @classmethod
2157
+ def match_to_datetime(cls, match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
2158
+ """
2159
+ Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
2202
2160
 
2203
- return safe_parse_float
2161
+ Raises ValueError if the match does not correspond to a valid date or datetime.
2162
+ """
2163
+
2164
+ (
2165
+ year_str,
2166
+ month_str,
2167
+ day_str,
2168
+ hour_str,
2169
+ minute_str,
2170
+ sec_str,
2171
+ micros_str,
2172
+ zulu_time,
2173
+ offset_sign_str,
2174
+ offset_hour_str,
2175
+ offset_minute_str,
2176
+ ) = match.groups()
2177
+ year, month, day = int(year_str), int(month_str), int(day_str)
2178
+ if hour_str is None:
2179
+ return datetime.date(year, month, day)
2180
+ hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
2181
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
2182
+ if offset_sign_str:
2183
+ tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
2184
+ offset_hour_str, offset_minute_str, offset_sign_str,
2185
+ )
2186
+ elif zulu_time:
2187
+ tz = datetime.UTC
2188
+ else: # local date-time
2189
+ tz = None
2190
+ return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
2191
+
2192
+ @classmethod
2193
+ def match_to_localtime(cls, match: re.Match) -> datetime.time:
2194
+ hour_str, minute_str, sec_str, micros_str = match.groups()
2195
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
2196
+ return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
2197
+
2198
+ @classmethod
2199
+ def match_to_number(cls, match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
2200
+ if match.group('floatpart'):
2201
+ return parse_float(match.group())
2202
+ return int(match.group(), 0)
2204
2203
 
2205
2204
 
2206
2205
  ########################################