Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tomlkit/items.py: 60%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import abc
4import copy
5import dataclasses
6import inspect
7import re
8import string
10from collections.abc import Collection
11from collections.abc import Iterable
12from collections.abc import Iterator
13from collections.abc import Sequence
14from datetime import date
15from datetime import datetime
16from datetime import time
17from datetime import timedelta
18from datetime import tzinfo
19from enum import Enum
20from typing import TYPE_CHECKING
21from typing import Any
22from typing import TypeVar
23from typing import overload
25from tomlkit._compat import PY38
26from tomlkit._compat import decode
27from tomlkit._types import _CustomDict
28from tomlkit._types import _CustomFloat
29from tomlkit._types import _CustomInt
30from tomlkit._types import _CustomList
31from tomlkit._utils import CONTROL_CHARS
32from tomlkit._utils import escape_string
33from tomlkit.exceptions import ConvertError
34from tomlkit.exceptions import InvalidStringError
37if TYPE_CHECKING:
38 from typing import Protocol
40 from tomlkit import container
41 from tomlkit.container import OutOfOrderTableProxy
43 class Encoder(Protocol):
44 def __call__(self, __value: Any, /) -> Item: ...
47ItemT = TypeVar("ItemT", bound="Item")
48CUSTOM_ENCODERS: list[Encoder] = []
49AT = TypeVar("AT", bound="AbstractTable")
52@overload
53def item(value: bool, _parent: Item | None = ..., _sort_keys: bool = ...) -> Bool: ... # type: ignore[overload-overlap]
56@overload
57def item(value: int, _parent: Item | None = ..., _sort_keys: bool = ...) -> Integer: ...
60@overload
61def item(value: float, _parent: Item | None = ..., _sort_keys: bool = ...) -> Float: ...
64@overload
65def item(value: str, _parent: Item | None = ..., _sort_keys: bool = ...) -> String: ...
68@overload
69def item( # type: ignore[overload-overlap]
70 value: datetime, _parent: Item | None = ..., _sort_keys: bool = ...
71) -> DateTime: ...
74@overload
75def item(value: date, _parent: Item | None = ..., _sort_keys: bool = ...) -> Date: ...
78@overload
79def item(value: time, _parent: Item | None = ..., _sort_keys: bool = ...) -> Time: ...
82@overload
83def item(
84 value: Sequence[dict[str, Any]], _parent: Item | None = ..., _sort_keys: bool = ...
85) -> AoT: ...
88@overload
89def item(
90 value: Sequence[Any], _parent: Item | None = ..., _sort_keys: bool = ...
91) -> Array: ...
94@overload
95def item(
96 value: dict[str, Any], _parent: Array = ..., _sort_keys: bool = ...
97) -> InlineTable: ...
100@overload
101def item(
102 value: dict[str, Any], _parent: Item | None = ..., _sort_keys: bool = ...
103) -> Table: ...
106@overload
107def item(value: ItemT, _parent: Item | None = ..., _sort_keys: bool = ...) -> ItemT: ...
110@overload
111def item(value: object, _parent: Item | None = ..., _sort_keys: bool = ...) -> Item: ...
114def item(value: Any, _parent: Item | None = None, _sort_keys: bool = False) -> Item:
115 """Create a TOML item from a Python object.
117 :Example:
119 >>> item(42)
120 42
121 >>> item([1, 2, 3])
122 [1, 2, 3]
123 >>> item({'a': 1, 'b': 2})
124 a = 1
125 b = 2
126 """
128 from tomlkit.container import Container
130 if isinstance(value, Item):
131 return value
133 if isinstance(value, bool):
134 return Bool(value, Trivia())
135 elif isinstance(value, int):
136 return Integer(value, Trivia(), str(value))
137 elif isinstance(value, float):
138 return Float(value, Trivia(), str(value))
139 elif isinstance(value, dict):
140 table_constructor = (
141 InlineTable if isinstance(_parent, (Array, InlineTable)) else Table
142 )
143 val = table_constructor(Container(), Trivia(), False)
144 for k, v in sorted(
145 value.items(),
146 key=lambda i: (isinstance(i[1], dict), i[0]) if _sort_keys else 1,
147 ):
148 val[k] = item(v, _parent=val, _sort_keys=_sort_keys)
150 return val
151 elif isinstance(value, (list, tuple)):
152 a: AoT | Array
153 if (
154 value
155 and all(isinstance(v, dict) for v in value)
156 and (_parent is None or isinstance(_parent, Table))
157 ):
158 a = AoT([])
159 table_constructor = Table
160 else:
161 a = Array([], Trivia())
162 table_constructor = InlineTable
164 if table_constructor is Table:
165 # AoT tables render as ``[[table]]`` headers, so dict-valued keys
166 # must come last to avoid them capturing the following keys.
167 def _sort_key(i: tuple[Any, Any]) -> Any:
168 return (isinstance(i[1], dict), i[0] if _sort_keys else 1)
169 else:
170 # Inline tables cannot capture, so there is no need to put
171 # dict-valued keys last: preserve insertion order, or sort purely
172 # by key when explicitly requested. See #546.
173 def _sort_key(i: tuple[Any, Any]) -> Any:
174 return i[0] if _sort_keys else 1
176 for v in value:
177 if isinstance(v, dict):
178 table = table_constructor(Container(), Trivia(), True)
180 for k, _v in sorted(v.items(), key=_sort_key):
181 i = item(_v, _parent=table, _sort_keys=_sort_keys)
182 if isinstance(table, InlineTable):
183 i.trivia.trail = ""
185 table[k] = i
187 v = table
189 a.append(v)
191 return a
192 elif isinstance(value, str):
193 return String.from_raw(value)
194 elif isinstance(value, datetime):
195 return DateTime(
196 value.year,
197 value.month,
198 value.day,
199 value.hour,
200 value.minute,
201 value.second,
202 value.microsecond,
203 value.tzinfo,
204 Trivia(),
205 value.isoformat().replace("+00:00", "Z"),
206 fold=value.fold,
207 )
208 elif isinstance(value, date):
209 return Date(value.year, value.month, value.day, Trivia(), value.isoformat())
210 elif isinstance(value, time):
211 return Time(
212 value.hour,
213 value.minute,
214 value.second,
215 value.microsecond,
216 value.tzinfo,
217 Trivia(),
218 _format_time(value),
219 fold=value.fold,
220 )
221 else:
222 for encoder in CUSTOM_ENCODERS:
223 try:
224 # Check if encoder accepts keyword arguments for backward compatibility
225 sig = inspect.signature(encoder)
226 if "_parent" in sig.parameters or any(
227 p.kind == p.VAR_KEYWORD for p in sig.parameters.values()
228 ):
229 # New style encoder that can accept additional parameters
230 rv = encoder(value, _parent=_parent, _sort_keys=_sort_keys) # type: ignore[call-arg]
231 else:
232 # Old style encoder that only accepts value
233 rv = encoder(value)
234 except ConvertError:
235 pass
236 else:
237 if not isinstance(rv, Item):
238 raise ConvertError(
239 f"Custom encoder is expected to return an instance of Item, got {type(rv)}"
240 )
241 return rv
243 raise ConvertError(f"Unable to convert an object of {type(value)} to a TOML item")
246def _format_time(value: time) -> str:
247 # TOML local times carry no offset or timezone (the spec's examples
248 # are plain ``07:32:00``); keeping the offset in the rendered raw
249 # value would produce a literal this library's own parser rejects.
250 # Built from the fields (not ``isoformat``/``replace``) so it stays
251 # correct even when called on a ``Time`` from ``Time._new``.
252 s = f"{value.hour:02d}:{value.minute:02d}:{value.second:02d}"
253 if value.microsecond:
254 s += f".{value.microsecond:06d}".rstrip("0")
255 return s
258# A carriage return that is not the CR of a CRLF line ending. Inside a multiline
259# string only a raw line feed or a CRLF pair is a valid line ending; a lone CR is
260# a control character that a basic string must escape.
261_BARE_CR = re.compile(r"\r(?!\n)")
264class StringType(Enum):
265 # Single Line Basic
266 SLB = '"'
267 # Multi Line Basic
268 MLB = '"""'
269 # Single Line Literal
270 SLL = "'"
271 # Multi Line Literal
272 MLL = "'''"
274 @classmethod
275 def select(cls, literal: bool = False, multiline: bool = False) -> StringType:
276 return {
277 (False, False): cls.SLB,
278 (False, True): cls.MLB,
279 (True, False): cls.SLL,
280 (True, True): cls.MLL,
281 }[(literal, multiline)]
283 @property
284 def escaped_sequences(self) -> Collection[str]:
285 # https://toml.io/en/v1.0.0#string
286 escaped_in_basic = CONTROL_CHARS | {"\\"}
287 allowed_in_multiline = {"\n", "\r"}
288 return {
289 StringType.SLB: escaped_in_basic | {'"'},
290 StringType.MLB: (escaped_in_basic | {'"""'}) - allowed_in_multiline,
291 StringType.SLL: (),
292 StringType.MLL: (),
293 }[self]
295 @property
296 def invalid_sequences(self) -> Collection[str]:
297 # https://toml.io/en/v1.0.0#string
298 forbidden_in_literal = CONTROL_CHARS - {"\t"}
299 # CR stays forbidden in multiline: the value is checked with CRLF
300 # pairs collapsed, so any remaining CR is a lone control character
301 return {
302 StringType.SLB: (),
303 StringType.MLB: (),
304 StringType.SLL: forbidden_in_literal | {"'"},
305 StringType.MLL: (forbidden_in_literal | {"'''"}) - {"\n"},
306 }[self]
308 @property
309 def unit(self) -> str:
310 return self.value[0]
312 def is_basic(self) -> bool:
313 return self is StringType.SLB or self is StringType.MLB
315 def is_literal(self) -> bool:
316 return self is StringType.SLL or self is StringType.MLL
318 def is_singleline(self) -> bool:
319 return self is StringType.SLB or self is StringType.SLL
321 def is_multiline(self) -> bool:
322 return self is StringType.MLB or self is StringType.MLL
324 def toggle(self) -> StringType:
325 return {
326 StringType.SLB: StringType.MLB,
327 StringType.MLB: StringType.SLB,
328 StringType.SLL: StringType.MLL,
329 StringType.MLL: StringType.SLL,
330 }[self]
333class BoolType(Enum):
334 TRUE = "true"
335 FALSE = "false"
337 def __bool__(self) -> bool:
338 return {BoolType.TRUE: True, BoolType.FALSE: False}[self]
340 def __iter__(self) -> Iterator[str]:
341 return iter(self.value)
343 def __len__(self) -> int:
344 return len(self.value)
347@dataclasses.dataclass
348class Trivia:
349 """
350 Trivia information (aka metadata).
351 """
353 # Whitespace before a value.
354 indent: str = ""
355 # Whitespace after a value, but before a comment.
356 comment_ws: str = ""
357 # Comment, starting with # character, or empty string if no comment.
358 comment: str = ""
359 # Trailing newline.
360 trail: str = "\n"
362 def copy(self) -> Trivia:
363 return dataclasses.replace(self)
366class KeyType(Enum):
367 """
368 The type of a Key.
370 Keys can be bare (unquoted), or quoted using basic ("), or literal (')
371 quotes following the same escaping rules as single-line StringType.
372 """
374 Bare = ""
375 Basic = '"'
376 Literal = "'"
379class Key(abc.ABC):
380 """Base class for a key"""
382 sep: str
383 _original: str
384 _keys: list[SingleKey]
385 _dotted: bool
386 key: str
388 @abc.abstractmethod
389 def __hash__(self) -> int:
390 pass
392 @abc.abstractmethod
393 def __eq__(self, __o: object) -> bool:
394 pass
396 def is_dotted(self) -> bool:
397 """If the key is followed by other keys"""
398 return self._dotted
400 def __iter__(self) -> Iterator[SingleKey]:
401 return iter(self._keys)
403 def concat(self, other: Key) -> DottedKey:
404 """Concatenate keys into a dotted key"""
405 keys = self._keys + other._keys
406 return DottedKey(keys, sep=self.sep)
408 def is_multi(self) -> bool:
409 """Check if the key contains multiple keys"""
410 return len(self._keys) > 1
412 def as_string(self) -> str:
413 """The TOML representation"""
414 return self._original
416 def __str__(self) -> str:
417 return self.as_string()
419 def __repr__(self) -> str:
420 return f"<Key {self.as_string()}>"
423class SingleKey(Key):
424 """A single key"""
426 def __init__(
427 self,
428 k: str,
429 t: KeyType | None = None,
430 sep: str | None = None,
431 original: str | None = None,
432 ) -> None:
433 if not isinstance(k, str):
434 raise TypeError("Keys must be strings")
436 if t is None:
437 if not k or any(
438 c not in string.ascii_letters + string.digits + "-" + "_" for c in k
439 ):
440 t = KeyType.Basic
441 else:
442 t = KeyType.Bare
444 self.t = t
445 if sep is None:
446 sep = " = "
448 self.sep = sep
449 self.key = k
450 if original is None:
451 key_str = escape_string(k) if t == KeyType.Basic else k
452 original = f"{t.value}{key_str}{t.value}"
454 self._original = original
455 self._keys = [self]
456 self._dotted = False
458 @property
459 def delimiter(self) -> str:
460 """The delimiter: double quote/single quote/none"""
461 return self.t.value
463 def is_bare(self) -> bool:
464 """Check if the key is bare"""
465 return self.t == KeyType.Bare
467 def __hash__(self) -> int:
468 return hash(self.key)
470 def __eq__(self, other: Any) -> bool:
471 if isinstance(other, Key):
472 return isinstance(other, SingleKey) and self.key == other.key
474 return bool(self.key == other)
477class DottedKey(Key):
478 def __init__(
479 self,
480 keys: Iterable[SingleKey],
481 sep: str | None = None,
482 original: str | None = None,
483 ) -> None:
484 self._keys = list(keys)
485 if original is None:
486 original = ".".join(k.as_string() for k in self._keys)
488 self.sep = " = " if sep is None else sep
489 self._original = original
490 self._dotted = False
491 self.key = ".".join(k.key for k in self._keys)
493 def __hash__(self) -> int:
494 return hash(tuple(self._keys))
496 def __eq__(self, __o: object) -> bool:
497 return isinstance(__o, DottedKey) and self._keys == __o._keys
500class Item:
501 """
502 An item within a TOML document.
503 """
505 def __init__(self, trivia: Trivia) -> None:
506 self._trivia = trivia
508 @property
509 def trivia(self) -> Trivia:
510 """The trivia element associated with this item"""
511 return self._trivia
513 @property
514 def discriminant(self) -> int:
515 raise NotImplementedError()
517 def as_string(self) -> str:
518 """The TOML representation"""
519 raise NotImplementedError()
521 @property
522 def value(self) -> Any:
523 return self
525 def unwrap(self) -> Any:
526 """Returns as pure python object (ppo)"""
527 raise NotImplementedError()
529 # Helpers
531 def comment(self, comment: str) -> Item:
532 """Attach a comment to this item"""
533 if "\n" in comment or "\r" in comment:
534 raise ValueError("Comment cannot contain line breaks")
535 if not comment.strip().startswith("#"):
536 comment = "# " + comment
538 self._trivia.comment_ws = " "
539 self._trivia.comment = comment
541 return self
543 def indent(self, indent: int) -> Item:
544 """Indent this item with given number of spaces"""
545 if self._trivia.indent.startswith("\n"):
546 self._trivia.indent = "\n" + " " * indent
547 else:
548 self._trivia.indent = " " * indent
550 return self
552 def is_boolean(self) -> bool:
553 return isinstance(self, Bool)
555 def is_table(self) -> bool:
556 return isinstance(self, Table)
558 def is_inline_table(self) -> bool:
559 return isinstance(self, InlineTable)
561 def is_aot(self) -> bool:
562 return isinstance(self, AoT)
564 def _getstate(self, protocol: int = 3) -> tuple[object, ...]:
565 return (self._trivia,)
567 def __reduce__(self) -> tuple[type, tuple[object, ...]]:
568 return self.__reduce_ex__(2)
570 def __reduce_ex__(self, protocol: int) -> tuple[type, tuple[object, ...]]: # type: ignore[override]
571 return self.__class__, self._getstate(protocol)
574class Whitespace(Item):
575 """
576 A whitespace literal.
577 """
579 def __init__(self, s: str, fixed: bool = False) -> None:
580 self._s = s
581 self._fixed = fixed
583 @property
584 def s(self) -> str:
585 return self._s
587 @property
588 def value(self) -> str:
589 """The wrapped string of the whitespace"""
590 return self._s
592 @property
593 def trivia(self) -> Trivia:
594 raise RuntimeError("Called trivia on a Whitespace variant.")
596 @property
597 def discriminant(self) -> int:
598 return 0
600 def is_fixed(self) -> bool:
601 """If the whitespace is fixed, it can't be merged or discarded from the output."""
602 return self._fixed
604 def as_string(self) -> str:
605 return self._s
607 def __repr__(self) -> str:
608 return f"<{self.__class__.__name__} {self._s!r}>"
610 def _getstate(self, protocol: int = 3) -> tuple[str, bool]:
611 return self._s, self._fixed
614class Comment(Item):
615 """
616 A comment literal.
617 """
619 @property
620 def discriminant(self) -> int:
621 return 1
623 def as_string(self) -> str:
624 return (
625 f"{self._trivia.indent}{decode(self._trivia.comment)}{self._trivia.trail}"
626 )
628 def __str__(self) -> str:
629 return f"{self._trivia.indent}{decode(self._trivia.comment)}"
632class Integer(Item, _CustomInt):
633 """
634 An integer literal.
635 """
637 def __new__(cls, value: int, trivia: Trivia, raw: str) -> Integer:
638 return int.__new__(cls, value)
640 def __init__(self, value: int, trivia: Trivia, raw: str) -> None:
641 super().__init__(trivia)
642 self._original = value
643 self._raw = raw
644 self._sign = False
646 if re.match(r"^[+\-]\d+$", raw):
647 self._sign = True
649 def unwrap(self) -> int:
650 return self._original
652 __int__ = unwrap
654 def __hash__(self) -> int:
655 return hash(self.unwrap())
657 @property
658 def discriminant(self) -> int:
659 return 2
661 @property
662 def value(self) -> int:
663 """The wrapped integer value"""
664 return self
666 def as_string(self) -> str:
667 return self._raw
669 def _new(self, result: int) -> Integer:
670 raw = str(result)
671 if self._sign and result >= 0:
672 raw = f"+{raw}"
674 return Integer(result, self._trivia.copy(), raw)
676 def _getstate(self, protocol: int = 3) -> tuple[int, Trivia, str]:
677 return int(self), self._trivia, self._raw
679 # int methods — explicit typed wrappers
680 def __abs__(self) -> Integer:
681 return self._new(int.__abs__(self))
683 def __add__(self, other: object) -> Integer:
684 result = int.__add__(self, other) # type: ignore[operator]
685 if result is NotImplemented:
686 return result # type: ignore[return-value]
687 return self._new(result)
689 def __and__(self, other: object) -> Integer:
690 result = int.__and__(self, other) # type: ignore[operator]
691 if result is NotImplemented:
692 return result # type: ignore[return-value]
693 return self._new(result)
695 def __ceil__(self) -> Integer:
696 return self._new(int.__ceil__(self))
698 __eq__ = int.__eq__
700 def __floor__(self) -> Integer:
701 return self._new(int.__floor__(self))
703 def __floordiv__(self, other: object) -> Integer:
704 result = int.__floordiv__(self, other) # type: ignore[operator]
705 if result is NotImplemented:
706 return result # type: ignore[return-value]
707 return self._new(result)
709 def __invert__(self) -> Integer:
710 return self._new(int.__invert__(self))
712 __le__ = int.__le__
714 def __lshift__(self, other: object) -> Integer:
715 result = int.__lshift__(self, other) # type: ignore[operator]
716 if result is NotImplemented:
717 return result # type: ignore[return-value]
718 return self._new(result)
720 __lt__ = int.__lt__
722 def __mod__(self, other: object) -> Integer:
723 result = int.__mod__(self, other) # type: ignore[operator]
724 if result is NotImplemented:
725 return result # type: ignore[return-value]
726 return self._new(result)
728 def __mul__(self, other: object) -> Integer:
729 result = int.__mul__(self, other) # type: ignore[operator]
730 if result is NotImplemented:
731 return result # type: ignore[return-value]
732 return self._new(result)
734 def __neg__(self) -> Integer:
735 return self._new(int.__neg__(self))
737 def __or__(self, other: object) -> Integer:
738 result = int.__or__(self, other) # type: ignore[operator]
739 if result is NotImplemented:
740 return result # type: ignore[return-value]
741 return self._new(result)
743 def __pos__(self) -> Integer:
744 return self._new(int.__pos__(self))
746 def __pow__(self, other: int, mod: int | None = None) -> Integer: # type: ignore[override]
747 result = (
748 int.__pow__(self, other) if mod is None else int.__pow__(self, other, mod)
749 )
750 return self._new(result)
752 def __radd__(self, other: object) -> Integer:
753 result = int.__radd__(self, other) # type: ignore[operator]
754 if result is NotImplemented:
755 return result # type: ignore[return-value]
756 return self._new(result)
758 def __rand__(self, other: object) -> Integer:
759 result = int.__rand__(self, other) # type: ignore[operator]
760 if result is NotImplemented:
761 return result # type: ignore[return-value]
762 return self._new(result)
764 def __rfloordiv__(self, other: object) -> Integer:
765 result = int.__rfloordiv__(self, other) # type: ignore[operator]
766 if result is NotImplemented:
767 return result # type: ignore[return-value]
768 return self._new(result)
770 def __rlshift__(self, other: object) -> Integer:
771 result = int.__rlshift__(self, other) # type: ignore[operator]
772 if result is NotImplemented:
773 return result # type: ignore[return-value]
774 return self._new(result)
776 def __rmod__(self, other: object) -> Integer:
777 result = int.__rmod__(self, other) # type: ignore[operator]
778 if result is NotImplemented:
779 return result # type: ignore[return-value]
780 return self._new(result)
782 def __rmul__(self, other: object) -> Integer:
783 result = int.__rmul__(self, other) # type: ignore[operator]
784 if result is NotImplemented:
785 return result # type: ignore[return-value]
786 return self._new(result)
788 def __ror__(self, other: object) -> Integer:
789 result = int.__ror__(self, other) # type: ignore[operator]
790 if result is NotImplemented:
791 return result # type: ignore[return-value]
792 return self._new(result)
794 def __round__(self, ndigits: int = 0) -> Integer: # type: ignore[override]
795 return self._new(int.__round__(self, ndigits))
797 def __rpow__(self, other: int, mod: int | None = None) -> Integer: # type: ignore[misc]
798 result = (
799 int.__rpow__(self, other) if mod is None else int.__rpow__(self, other, mod)
800 )
801 return self._new(result)
803 def __rrshift__(self, other: object) -> Integer:
804 result = int.__rrshift__(self, other) # type: ignore[operator]
805 if result is NotImplemented:
806 return result # type: ignore[return-value]
807 return self._new(result)
809 def __rshift__(self, other: object) -> Integer:
810 result = int.__rshift__(self, other) # type: ignore[operator]
811 if result is NotImplemented:
812 return result # type: ignore[return-value]
813 return self._new(result)
815 def __rxor__(self, other: object) -> Integer:
816 result = int.__rxor__(self, other) # type: ignore[operator]
817 if result is NotImplemented:
818 return result # type: ignore[return-value]
819 return self._new(result)
821 def __sub__(self, other: object) -> Integer:
822 result = int.__sub__(self, other) # type: ignore[operator]
823 if result is NotImplemented:
824 return result # type: ignore[return-value]
825 return self._new(result)
827 def __rsub__(self, other: object) -> Integer:
828 result = int.__rsub__(self, other) # type: ignore[operator]
829 if result is NotImplemented:
830 return result # type: ignore[return-value]
831 return self._new(result)
833 def __trunc__(self) -> Integer:
834 return self._new(int.__trunc__(self))
836 def __xor__(self, other: object) -> Integer:
837 result = int.__xor__(self, other) # type: ignore[operator]
838 if result is NotImplemented:
839 return result # type: ignore[return-value]
840 return self._new(result)
842 def __rtruediv__(self, other: object) -> Float:
843 result = int.__rtruediv__(self, other) # type: ignore[operator]
844 if result is NotImplemented:
845 return result # type: ignore[return-value]
846 return Float._new(self, result) # type: ignore[arg-type]
848 def __truediv__(self, other: object) -> Float:
849 result = int.__truediv__(self, other) # type: ignore[operator]
850 if result is NotImplemented:
851 return result # type: ignore[return-value]
852 return Float._new(self, result) # type: ignore[arg-type]
855class Float(Item, _CustomFloat):
856 """
857 A float literal.
858 """
860 def __new__(cls, value: float, trivia: Trivia, raw: str) -> Float:
861 return float.__new__(cls, value)
863 def __init__(self, value: float, trivia: Trivia, raw: str) -> None:
864 super().__init__(trivia)
865 self._original = value
866 self._raw = raw
867 self._sign = False
869 if re.match(r"^[+\-].+$", raw):
870 self._sign = True
872 def unwrap(self) -> float:
873 return self._original
875 __float__ = unwrap
877 def __hash__(self) -> int:
878 return hash(self.unwrap())
880 @property
881 def discriminant(self) -> int:
882 return 3
884 @property
885 def value(self) -> float:
886 """The wrapped float value"""
887 return self
889 def as_string(self) -> str:
890 return self._raw
892 def _new(self, result: float) -> Float:
893 raw = str(result)
895 # -0.0 >= 0 is True, but str(-0.0) already carries its sign.
896 if self._sign and result >= 0 and not raw.startswith("-"):
897 raw = f"+{raw}"
899 return Float(result, self._trivia.copy(), raw)
901 def _getstate(self, protocol: int = 3) -> tuple[float, Trivia, str]:
902 return float(self), self._trivia, self._raw
904 # float methods — explicit typed wrappers
905 def __abs__(self) -> Float:
906 return self._new(float.__abs__(self))
908 def __add__(self, other: object) -> Float:
909 result = float.__add__(self, other) # type: ignore[operator]
910 if result is NotImplemented:
911 return result # type: ignore[return-value]
912 return self._new(result)
914 __eq__ = float.__eq__
916 def __floordiv__(self, other: object) -> Float:
917 result = float.__floordiv__(self, other) # type: ignore[operator]
918 if result is NotImplemented:
919 return result # type: ignore[return-value]
920 return self._new(result)
922 __le__ = float.__le__
923 __lt__ = float.__lt__
925 def __mod__(self, other: object) -> Float:
926 result = float.__mod__(self, other) # type: ignore[operator]
927 if result is NotImplemented:
928 return result # type: ignore[return-value]
929 return self._new(result)
931 def __mul__(self, other: object) -> Float:
932 result = float.__mul__(self, other) # type: ignore[operator]
933 if result is NotImplemented:
934 return result # type: ignore[return-value]
935 return self._new(result)
937 def __neg__(self) -> Float:
938 return self._new(float.__neg__(self))
940 def __pos__(self) -> Float:
941 return self._new(float.__pos__(self))
943 def __pow__(self, other: object, mod: None = None) -> Float:
944 result = float.__pow__(self, other) # type: ignore[operator]
945 if result is NotImplemented:
946 return result # type: ignore[no-any-return]
947 return self._new(result)
949 def __radd__(self, other: object) -> Float:
950 result = float.__radd__(self, other) # type: ignore[operator]
951 if result is NotImplemented:
952 return result # type: ignore[return-value]
953 return self._new(result)
955 def __rfloordiv__(self, other: object) -> Float:
956 result = float.__rfloordiv__(self, other) # type: ignore[operator]
957 if result is NotImplemented:
958 return result # type: ignore[return-value]
959 return self._new(result)
961 def __rmod__(self, other: object) -> Float:
962 result = float.__rmod__(self, other) # type: ignore[operator]
963 if result is NotImplemented:
964 return result # type: ignore[return-value]
965 return self._new(result)
967 def __rmul__(self, other: object) -> Float:
968 result = float.__rmul__(self, other) # type: ignore[operator]
969 if result is NotImplemented:
970 return result # type: ignore[return-value]
971 return self._new(result)
973 def __round__(self, ndigits: int = 0) -> Float: # type: ignore[override]
974 return self._new(float.__round__(self, ndigits))
976 def __rpow__(self, other: object, mod: None = None) -> Float:
977 result = float.__rpow__(self, other) # type: ignore[operator]
978 if result is NotImplemented:
979 return result # type: ignore[no-any-return]
980 return self._new(result)
982 def __rtruediv__(self, other: object) -> Float:
983 result = float.__rtruediv__(self, other) # type: ignore[operator]
984 if result is NotImplemented:
985 return result # type: ignore[return-value]
986 return self._new(result)
988 def __truediv__(self, other: object) -> Float:
989 result = float.__truediv__(self, other) # type: ignore[operator]
990 if result is NotImplemented:
991 return result # type: ignore[return-value]
992 return self._new(result)
994 def __sub__(self, other: object) -> Float:
995 result = float.__sub__(self, other) # type: ignore[operator]
996 if result is NotImplemented:
997 return result # type: ignore[return-value]
998 return self._new(result)
1000 def __rsub__(self, other: object) -> Float:
1001 result = float.__rsub__(self, other) # type: ignore[operator]
1002 if result is NotImplemented:
1003 return result # type: ignore[return-value]
1004 return self._new(result)
1006 __trunc__ = float.__trunc__
1007 __ceil__ = float.__ceil__
1008 __floor__ = float.__floor__
1011class Bool(Item):
1012 """
1013 A boolean literal.
1014 """
1016 def __init__(self, t: int | BoolType, trivia: Trivia) -> None:
1017 super().__init__(trivia)
1019 self._value = bool(t)
1021 def unwrap(self) -> bool:
1022 return bool(self)
1024 @property
1025 def discriminant(self) -> int:
1026 return 4
1028 @property
1029 def value(self) -> bool:
1030 """The wrapped boolean value"""
1031 return self._value
1033 def as_string(self) -> str:
1034 return str(self._value).lower()
1036 def _getstate(self, protocol: int = 3) -> tuple[bool, Trivia]:
1037 return self._value, self._trivia
1039 def __bool__(self) -> bool:
1040 return self._value
1042 __nonzero__ = __bool__
1044 def __eq__(self, other: object) -> bool:
1045 if isinstance(other, Bool):
1046 other = other.value
1048 if not isinstance(other, bool):
1049 return NotImplemented
1051 return other == self._value
1053 def __hash__(self) -> int:
1054 return hash(self._value)
1056 def __repr__(self) -> str:
1057 return repr(self._value)
1060class DateTime(Item, datetime):
1061 """
1062 A datetime literal.
1063 """
1065 def __new__(
1066 cls,
1067 year: int,
1068 month: int,
1069 day: int,
1070 hour: int,
1071 minute: int,
1072 second: int,
1073 microsecond: int,
1074 tzinfo: tzinfo | None,
1075 trivia: Trivia | None = None,
1076 raw: str | None = None,
1077 fold: int = 0,
1078 **kwargs: object,
1079 ) -> DateTime:
1080 return datetime.__new__(
1081 cls,
1082 year,
1083 month,
1084 day,
1085 hour,
1086 minute,
1087 second,
1088 microsecond,
1089 tzinfo=tzinfo,
1090 fold=fold,
1091 )
1093 def __init__(
1094 self,
1095 year: int,
1096 month: int,
1097 day: int,
1098 hour: int,
1099 minute: int,
1100 second: int,
1101 microsecond: int,
1102 tzinfo: tzinfo | None,
1103 trivia: Trivia | None = None,
1104 raw: str | None = None,
1105 fold: int = 0,
1106 **kwargs: object,
1107 ) -> None:
1108 super().__init__(trivia or Trivia())
1110 self._raw = raw or self.isoformat()
1112 def unwrap(self) -> datetime:
1113 (
1114 year,
1115 month,
1116 day,
1117 hour,
1118 minute,
1119 second,
1120 microsecond,
1121 tzinfo,
1122 _,
1123 _,
1124 ) = self._getstate()
1125 return datetime(
1126 year, month, day, hour, minute, second, microsecond, tzinfo, fold=self.fold
1127 )
1129 @property
1130 def discriminant(self) -> int:
1131 return 5
1133 @property
1134 def value(self) -> datetime:
1135 return self
1137 def as_string(self) -> str:
1138 return self._raw
1140 def __add__(self, other: timedelta) -> DateTime:
1141 if PY38:
1142 result = datetime(
1143 self.year,
1144 self.month,
1145 self.day,
1146 self.hour,
1147 self.minute,
1148 self.second,
1149 self.microsecond,
1150 self.tzinfo,
1151 ).__add__(other)
1152 else:
1153 result = super().__add__(other)
1155 return self._new(result)
1157 @overload # type: ignore[override]
1158 def __sub__(self, other: timedelta) -> DateTime: ...
1160 @overload
1161 def __sub__(self, other: datetime) -> timedelta: ...
1163 def __sub__(self, other: timedelta | datetime) -> DateTime | timedelta:
1164 if PY38:
1165 result = datetime(
1166 self.year,
1167 self.month,
1168 self.day,
1169 self.hour,
1170 self.minute,
1171 self.second,
1172 self.microsecond,
1173 self.tzinfo,
1174 fold=self.fold,
1175 ).__sub__(other)
1176 else:
1177 result = super().__sub__(other) # type: ignore[operator]
1179 if isinstance(result, datetime):
1180 result = self._new(result)
1182 return result
1184 def replace(self, *args: object, **kwargs: object) -> DateTime:
1185 return self._new(super().replace(*args, **kwargs)) # type: ignore[arg-type]
1187 def astimezone(self, tz: tzinfo) -> DateTime: # type: ignore[override]
1188 result = super().astimezone(tz)
1189 if PY38:
1190 return result
1191 return self._new(result)
1193 def _new(self, result: datetime) -> DateTime:
1194 raw = result.isoformat()
1196 return DateTime(
1197 result.year,
1198 result.month,
1199 result.day,
1200 result.hour,
1201 result.minute,
1202 result.second,
1203 result.microsecond,
1204 result.tzinfo,
1205 self._trivia.copy(),
1206 raw,
1207 fold=result.fold,
1208 )
1210 def _getstate(
1211 self, protocol: int = 3
1212 ) -> tuple[int, int, int, int, int, int, int, tzinfo | None, Trivia, str]:
1213 return (
1214 self.year,
1215 self.month,
1216 self.day,
1217 self.hour,
1218 self.minute,
1219 self.second,
1220 self.microsecond,
1221 self.tzinfo,
1222 self._trivia,
1223 self._raw,
1224 )
1227class Date(Item, date):
1228 """
1229 A date literal.
1230 """
1232 def __new__(
1233 cls,
1234 year: int,
1235 month: int,
1236 day: int,
1237 trivia: Trivia | None = None,
1238 raw: str = "",
1239 ) -> Date:
1240 return date.__new__(cls, year, month, day)
1242 def __init__(
1243 self,
1244 year: int,
1245 month: int,
1246 day: int,
1247 trivia: Trivia | None = None,
1248 raw: str = "",
1249 ) -> None:
1250 super().__init__(trivia or Trivia())
1252 self._raw = raw
1254 def unwrap(self) -> date:
1255 (year, month, day, _, _) = self._getstate()
1256 return date(year, month, day)
1258 @property
1259 def discriminant(self) -> int:
1260 return 6
1262 @property
1263 def value(self) -> date:
1264 return self
1266 def as_string(self) -> str:
1267 return self._raw
1269 def __add__(self, other: timedelta) -> Date:
1270 if PY38:
1271 result = date(self.year, self.month, self.day).__add__(other)
1272 else:
1273 result = super().__add__(other)
1275 return self._new(result)
1277 @overload # type: ignore[override]
1278 def __sub__(self, other: timedelta) -> Date: ...
1280 @overload
1281 def __sub__(self, other: date) -> timedelta: ...
1283 def __sub__(self, other: timedelta | date) -> Date | timedelta:
1284 if PY38:
1285 result = date(self.year, self.month, self.day).__sub__(other)
1286 else:
1287 result = super().__sub__(other) # type: ignore[operator]
1289 if isinstance(result, date):
1290 result = self._new(result)
1292 return result
1294 def replace(self, *args: object, **kwargs: object) -> Date:
1295 return self._new(super().replace(*args, **kwargs)) # type: ignore[arg-type]
1297 def _new(self, result: date) -> Date:
1298 raw = result.isoformat()
1300 return Date(result.year, result.month, result.day, self._trivia.copy(), raw)
1302 def _getstate(self, protocol: int = 3) -> tuple[int, int, int, Trivia, str]:
1303 return (self.year, self.month, self.day, self._trivia, self._raw)
1306class Time(Item, time):
1307 """
1308 A time literal.
1309 """
1311 def __new__(
1312 cls,
1313 hour: int,
1314 minute: int,
1315 second: int,
1316 microsecond: int,
1317 tzinfo: tzinfo | None,
1318 trivia: Trivia | None = None,
1319 raw: str = "",
1320 fold: int = 0,
1321 ) -> Time:
1322 return time.__new__(cls, hour, minute, second, microsecond, tzinfo, fold=fold)
1324 def __init__(
1325 self,
1326 hour: int,
1327 minute: int,
1328 second: int,
1329 microsecond: int,
1330 tzinfo: tzinfo | None,
1331 trivia: Trivia | None = None,
1332 raw: str = "",
1333 fold: int = 0,
1334 ) -> None:
1335 super().__init__(trivia or Trivia())
1337 self._raw = raw
1339 def unwrap(self) -> time:
1340 (hour, minute, second, microsecond, tzinfo, _, _) = self._getstate()
1341 return time(hour, minute, second, microsecond, tzinfo, fold=self.fold)
1343 @property
1344 def discriminant(self) -> int:
1345 return 7
1347 @property
1348 def value(self) -> time:
1349 return self
1351 def as_string(self) -> str:
1352 return self._raw
1354 def replace(self, *args: object, **kwargs: object) -> Time:
1355 return self._new(super().replace(*args, **kwargs)) # type: ignore[arg-type]
1357 def _new(self, result: time) -> Time:
1358 raw = _format_time(result)
1360 return Time(
1361 result.hour,
1362 result.minute,
1363 result.second,
1364 result.microsecond,
1365 result.tzinfo,
1366 self._trivia.copy(),
1367 raw,
1368 fold=result.fold,
1369 )
1371 def _getstate(
1372 self, protocol: int = 3
1373 ) -> tuple[int, int, int, int, tzinfo | None, Trivia, str]:
1374 return (
1375 self.hour,
1376 self.minute,
1377 self.second,
1378 self.microsecond,
1379 self.tzinfo,
1380 self._trivia,
1381 self._raw,
1382 )
1385class _ArrayItemGroup:
1386 __slots__ = ("comma", "comment", "indent", "value")
1388 def __init__(
1389 self,
1390 value: Item | None = None,
1391 indent: Whitespace | None = None,
1392 comma: Whitespace | None = None,
1393 comment: Comment | None = None,
1394 ) -> None:
1395 self.value = value
1396 self.indent = indent
1397 self.comma = comma
1398 self.comment = comment
1400 def __iter__(self) -> Iterator[Item]:
1401 return (
1402 x
1403 for x in (self.indent, self.value, self.comma, self.comment)
1404 if x is not None
1405 )
1407 def __repr__(self) -> str:
1408 return repr(tuple(self))
1410 def is_whitespace(self) -> bool:
1411 return self.value is None and self.comment is None
1413 def __bool__(self) -> bool:
1414 try:
1415 next(iter(self))
1416 except StopIteration:
1417 return False
1418 return True
1421class Array(Item, _CustomList): # type: ignore[type-arg]
1422 """
1423 An array literal
1424 """
1426 def __init__(
1427 self, value: list[Item], trivia: Trivia, multiline: bool = False
1428 ) -> None:
1429 super().__init__(trivia)
1430 list.__init__(
1431 self,
1432 [v for v in value if not isinstance(v, (Whitespace, Comment, Null))],
1433 )
1434 self._index_map: dict[int, int] = {}
1435 self._value = self._group_values(value)
1436 self._multiline = multiline
1437 self._reindex()
1439 def _group_values(self, value: list[Item]) -> list[_ArrayItemGroup]:
1440 """Group the values into (indent, value, comma, comment) tuples"""
1441 groups = []
1442 this_group = _ArrayItemGroup()
1443 start_new_group = False
1444 for item in value:
1445 if isinstance(item, Whitespace):
1446 if "," not in item.s or start_new_group:
1447 groups.append(this_group)
1448 this_group = _ArrayItemGroup(indent=item)
1449 start_new_group = False
1450 else:
1451 if this_group.value is None:
1452 # when comma is met and no value is provided, add a dummy Null
1453 this_group.value = Null()
1454 this_group.comma = item
1455 elif isinstance(item, Comment):
1456 if this_group.value is None:
1457 this_group.value = Null()
1458 this_group.comment = item
1459 # Comments are the last item in a group.
1460 start_new_group = True
1461 elif this_group.value is None:
1462 this_group.value = item
1463 else:
1464 groups.append(this_group)
1465 this_group = _ArrayItemGroup(value=item)
1466 groups.append(this_group)
1467 return [group for group in groups if group]
1469 def unwrap(self) -> list[Any]:
1470 unwrapped = []
1471 for v in self:
1472 if hasattr(v, "unwrap"):
1473 unwrapped.append(v.unwrap())
1474 else:
1475 unwrapped.append(v)
1476 return unwrapped
1478 @property
1479 def discriminant(self) -> int:
1480 return 8
1482 @property
1483 def value(self) -> list[Item]:
1484 return self
1486 def _iter_items(self) -> Iterator[Item]:
1487 for v in self._value:
1488 yield from v
1490 def multiline(self, multiline: bool) -> Array:
1491 """Change the array to display in multiline or not.
1493 :Example:
1495 >>> a = item([1, 2, 3])
1496 >>> print(a.as_string())
1497 [1, 2, 3]
1498 >>> print(a.multiline(True).as_string())
1499 [
1500 1,
1501 2,
1502 3,
1503 ]
1504 """
1505 self._multiline = multiline
1507 return self
1509 def as_string(self) -> str:
1510 if not self._multiline or not self._value:
1511 rendered = list(self._iter_items())
1512 inner = "".join(v.as_string() for v in rendered)
1513 if rendered and isinstance(rendered[-1], Comment):
1514 # A trailing comment (e.g. from add_line(comment=...)) carries
1515 # no newline, so keep the closing bracket off the comment line
1516 # to produce parseable output (GH #580).
1517 inner += "\n" + self.trivia.indent
1518 return f"[{inner}]"
1520 s = "[\n"
1521 s += "".join(
1522 self.trivia.indent
1523 + " " * 4
1524 + v.value.as_string()
1525 + ("," if not isinstance(v.value, Null) else "")
1526 + (v.comment.as_string() if v.comment is not None else "")
1527 + "\n"
1528 for v in self._value
1529 if v.value is not None
1530 )
1531 s += self.trivia.indent + "]"
1533 return s
1535 def _reindex(self) -> None:
1536 self._index_map.clear()
1537 index = 0
1538 for i, v in enumerate(self._value):
1539 if v.value is None or isinstance(v.value, Null):
1540 continue
1541 self._index_map[index] = i
1542 index += 1
1544 def add_line(
1545 self,
1546 *items: Any,
1547 indent: str = " ",
1548 comment: str | None = None,
1549 add_comma: bool = True,
1550 newline: bool = True,
1551 ) -> None:
1552 """Add multiple items in a line to control the format precisely.
1553 When add_comma is True, only accept actual values and
1554 ", " will be added between values automatically.
1556 :Example:
1558 >>> a = array()
1559 >>> a.add_line(1, 2, 3)
1560 >>> a.add_line(4, 5, 6)
1561 >>> a.add_line(indent="")
1562 >>> print(a.as_string())
1563 [
1564 1, 2, 3,
1565 4, 5, 6,
1566 ]
1567 """
1568 if comment and ("\n" in comment or "\r" in comment):
1569 raise ValueError("Comment cannot contain line breaks")
1570 new_values: list[Item] = []
1571 first_indent = f"\n{indent}" if newline else indent
1572 if first_indent:
1573 new_values.append(Whitespace(first_indent))
1574 whitespace = ""
1575 data_values = []
1576 for i, el in enumerate(items):
1577 it = item(el, _parent=self)
1578 if isinstance(it, Comment) or (add_comma and isinstance(el, Whitespace)):
1579 raise ValueError(f"item type {type(it)} is not allowed in add_line")
1580 if not isinstance(it, Whitespace):
1581 if whitespace:
1582 new_values.append(Whitespace(whitespace))
1583 whitespace = ""
1584 new_values.append(it)
1585 data_values.append(it.value)
1586 if add_comma:
1587 new_values.append(Whitespace(","))
1588 if i != len(items) - 1:
1589 new_values.append(Whitespace(" "))
1590 elif "," not in it.s:
1591 whitespace += it.s
1592 else:
1593 new_values.append(it)
1594 if whitespace:
1595 new_values.append(Whitespace(whitespace))
1596 if comment:
1597 indent = " " if items else ""
1598 new_values.append(
1599 Comment(Trivia(indent=indent, comment=f"# {comment}", trail=""))
1600 )
1601 list.extend(self, data_values)
1602 if len(self._value) > 0:
1603 last_item = self._value[-1]
1604 last_value_item = next(
1605 (
1606 v
1607 for v in self._value[::-1]
1608 if v.value is not None and not isinstance(v.value, Null)
1609 ),
1610 None,
1611 )
1612 if last_value_item is not None:
1613 last_value_item.comma = Whitespace(",")
1614 if last_item.is_whitespace():
1615 self._value[-1:-1] = self._group_values(new_values)
1616 else:
1617 self._value.extend(self._group_values(new_values))
1618 else:
1619 self._value.extend(self._group_values(new_values))
1620 self._reindex()
1622 def clear(self) -> None:
1623 """Clear the array."""
1624 list.clear(self)
1625 self._index_map.clear()
1626 self._value.clear()
1628 def __len__(self) -> int:
1629 return list.__len__(self)
1631 def item(self, index: int) -> Item:
1632 return list.__getitem__(self, index) # type: ignore[no-any-return]
1634 def __getitem__(self, key: int | slice) -> Any: # type: ignore[override]
1635 return list.__getitem__(self, key)
1637 def __setitem__(self, key: int | slice, value: Any) -> None: # type: ignore[override]
1638 if isinstance(key, slice):
1639 raise ValueError("slice assignment is not supported")
1640 it = item(value, _parent=self)
1641 list.__setitem__(self, key, it)
1642 if key < 0:
1643 key += len(self)
1644 self._value[self._index_map[key]].value = it
1646 def insert(self, pos: int, value: Any) -> None: # type: ignore[override]
1647 it = item(value, _parent=self)
1648 length = len(self)
1649 if not isinstance(it, (Comment, Whitespace)):
1650 list.insert(self, pos, it)
1651 if pos < 0:
1652 pos += length
1653 if pos < 0:
1654 pos = 0
1656 idx = 0 # insert position of the self._value list
1657 default_indent = " "
1658 if pos < length:
1659 try:
1660 idx = self._index_map[pos]
1661 except KeyError as e:
1662 raise IndexError("list index out of range") from e
1663 else:
1664 idx = len(self._value)
1665 if idx >= 1 and self._value[idx - 1].is_whitespace():
1666 # The last item is a pure whitespace(\n ), insert before it
1667 idx -= 1
1668 _indent = self._value[idx].indent
1669 if _indent is not None and "\n" in _indent.s:
1670 default_indent = "\n "
1671 indent: Whitespace | None = None
1672 comma: Whitespace | None = Whitespace(",") if pos < length else None
1673 if idx < len(self._value) and not self._value[idx].is_whitespace():
1674 # Prefer to copy the indentation from the item after
1675 indent = self._value[idx].indent
1676 if idx > 0:
1677 last_item = self._value[idx - 1]
1678 if indent is None:
1679 indent = last_item.indent
1680 if not isinstance(last_item.value, Null) and "\n" in default_indent:
1681 # Copy the comma from the last item if 1) it contains a value and
1682 # 2) the array is multiline
1683 comma = last_item.comma
1684 comma_in_indent = indent is not None and "," in indent.s
1685 if (
1686 last_item.comma is None
1687 and not isinstance(last_item.value, Null)
1688 and not comma_in_indent
1689 ):
1690 # Add comma to the last item to separate it from the following
1691 # items, unless the new item's indent already starts with a
1692 # comma (comma-first style).
1693 last_item.comma = Whitespace(",")
1694 if indent is not None and "," in indent.s:
1695 # Comma-first style: the item that will follow the new one brings
1696 # its own leading comma, so the new item must not add a trailing
1697 # comma of its own.
1698 comma = None
1699 if indent is None and (idx > 0 or "\n" in default_indent):
1700 # apply default indent if it isn't the first item or the array is multiline.
1701 indent = Whitespace(default_indent)
1702 new_item = _ArrayItemGroup(value=it, indent=indent, comma=comma)
1703 self._value.insert(idx, new_item)
1704 self._reindex()
1706 def __delitem__(self, key: int | slice) -> None: # type: ignore[override]
1707 length = len(self)
1708 list.__delitem__(self, key)
1710 if isinstance(key, slice):
1711 indices_to_remove = list(range(*key.indices(length)))
1712 else:
1713 indices_to_remove = [length + key if key < 0 else key]
1714 for i in sorted(indices_to_remove, reverse=True):
1715 try:
1716 idx = self._index_map[i]
1717 except KeyError as e:
1718 if not isinstance(key, slice):
1719 raise IndexError("list index out of range") from e
1720 else:
1721 group_rm = self._value[idx]
1722 del self._value[idx]
1723 if (
1724 idx == 0
1725 and len(self._value) > 0
1726 and (ind := self._value[idx].indent)
1727 and "\n" not in ind.s
1728 ):
1729 # Remove the indentation of the first item if not newline
1730 self._value[idx].indent = None
1731 comma_in_indent = (
1732 group_rm.indent is not None and "," in group_rm.indent.s
1733 )
1734 comma_in_comma = group_rm.comma is not None and "," in group_rm.comma.s
1735 if comma_in_indent and comma_in_comma:
1736 # Removed group had both commas. Add one to the next group.
1737 group = self._value[idx] if len(self._value) > idx else None
1738 if group is not None:
1739 if group.indent is None:
1740 group.indent = Whitespace(",")
1741 elif "," not in group.indent.s:
1742 # Insert the comma after the newline
1743 try:
1744 newline_index = group.indent.s.index("\n")
1745 group.indent._s = (
1746 group.indent.s[: newline_index + 1]
1747 + ","
1748 + group.indent.s[newline_index + 1 :]
1749 )
1750 except ValueError:
1751 group.indent._s = "," + group.indent.s
1752 elif not comma_in_indent and not comma_in_comma:
1753 # Removed group had no commas. Remove the next comma found.
1754 for j in range(idx, len(self._value)):
1755 group = self._value[j]
1756 if group.indent is not None and "," in group.indent.s:
1757 group.indent._s = group.indent.s.replace(",", "", 1)
1758 break
1759 if group_rm.indent is not None and "\n" in group_rm.indent.s:
1760 # Restore the removed group's newline onto the next group
1761 # if the next group does not have a newline.
1762 # i.e. the two were on the same line
1763 group = self._value[idx] if len(self._value) > idx else None
1764 if group is not None and (
1765 group.indent is None or "\n" not in group.indent.s
1766 ):
1767 group.indent = group_rm.indent
1769 if len(self._value) > 0:
1770 v = self._value[-1]
1771 if not v.is_whitespace():
1772 # remove the comma of the last item
1773 v.comma = None
1775 self._reindex()
1777 def _getstate(self, protocol: int = 3) -> tuple[list[Item], Trivia, bool]:
1778 return list(self._iter_items()), self._trivia, self._multiline
1781class AbstractTable(Item, _CustomDict): # type: ignore[type-arg]
1782 """Common behaviour of both :class:`Table` and :class:`InlineTable`"""
1784 def __init__(self, value: container.Container, trivia: Trivia):
1785 Item.__init__(self, trivia)
1787 self._value = value
1789 for k, v in self._value.body:
1790 if k is not None:
1791 dict.__setitem__(self, k.key, v)
1793 def unwrap(self) -> dict[str, Any]:
1794 # Delegate to the inner container's unwrap, which walks its _body
1795 # directly instead of re-resolving every key through items()/__getitem__
1796 # (which rebuilds a SingleKey, and an OutOfOrderTableProxy for
1797 # out-of-order keys, per key just to throw it away).
1798 return self._value.unwrap()
1800 @property
1801 def value(self) -> container.Container:
1802 return self._value
1804 @overload
1805 def append(self: AT, key: None, value: Comment | Whitespace) -> AT: ...
1807 @overload
1808 def append(self: AT, key: Key | str, value: Any) -> AT: ...
1810 def append(self: AT, key: Key | str | None, value: Any) -> AT:
1811 raise NotImplementedError
1813 @overload
1814 def add(self: AT, key: Comment | Whitespace) -> AT: ...
1816 @overload
1817 def add(self: AT, key: Key | str, value: Any = ...) -> AT: ...
1819 def add(
1820 self: AT, key: Key | str | Comment | Whitespace, value: Any | None = None
1821 ) -> AT:
1822 if value is None:
1823 if not isinstance(key, (Comment, Whitespace)):
1824 msg = "Non comment/whitespace items must have an associated key"
1825 raise ValueError(msg)
1827 return self.append(None, key)
1829 if isinstance(key, (Comment, Whitespace)):
1830 raise ValueError("Comment/Whitespace keys must not have a value")
1832 return self.append(key, value)
1834 def remove(self: AT, key: Key | str) -> AT:
1835 self._value.remove(key)
1837 if isinstance(key, Key):
1838 key = key.key
1840 if key is not None:
1841 dict.__delitem__(self, key)
1843 return self
1845 def item(self, key: Key | str) -> Item | OutOfOrderTableProxy:
1846 return self._value.item(key)
1848 def setdefault(self, key: Key | str, default: Any) -> Any: # type: ignore[override]
1849 super().setdefault(key, default)
1850 return self[key]
1852 def __str__(self) -> str:
1853 return str(self.value)
1855 def copy(self: AT) -> AT:
1856 return copy.copy(self)
1858 def __repr__(self) -> str:
1859 return repr(self.value)
1861 def __iter__(self) -> Iterator[str]:
1862 return iter(self._value)
1864 def __len__(self) -> int:
1865 return len(self._value)
1867 def __delitem__(self, key: Key | str) -> None:
1868 self.remove(key)
1870 def __getitem__(self, key: Key | str) -> Any:
1871 return self._value[key]
1873 def __contains__(self, key: object) -> bool:
1874 # Native membership test. The inherited ``MutableMapping.__contains__``
1875 # resolves the value via ``__getitem__`` (``self._value[key]``) -- and
1876 # builds a ``NonExistentKey`` on every absent key -- only to discard it.
1877 # Delegate straight to the inner container instead: its ``__contains__``
1878 # answers from ``_map`` while still building the ``OutOfOrderTableProxy``
1879 # for an out-of-order entry, so validation runs exactly as before.
1880 return key in self._value
1882 def __setitem__(self, key: Key | str, value: Any) -> None:
1883 if not isinstance(value, Item):
1884 value = item(value, _parent=self)
1886 is_replace = key in self
1887 self._value[key] = value
1889 if key is not None:
1890 dict.__setitem__(self, key, value)
1892 if is_replace:
1893 return
1894 m = re.match("(?s)^[^ ]*([ ]+).*$", self._trivia.indent)
1895 if not m:
1896 return
1898 indent = m.group(1)
1900 if not isinstance(value, Whitespace):
1901 m = re.match("(?s)^([^ ]*)(.*)$", value.trivia.indent)
1902 if not m:
1903 value.trivia.indent = indent
1904 else:
1905 value.trivia.indent = m.group(1) + indent + m.group(2)
1908class Table(AbstractTable):
1909 """
1910 A table literal.
1911 """
1913 def __init__(
1914 self,
1915 value: container.Container,
1916 trivia: Trivia,
1917 is_aot_element: bool,
1918 is_super_table: bool | None = None,
1919 name: str | None = None,
1920 display_name: str | None = None,
1921 ) -> None:
1922 super().__init__(value, trivia)
1924 self.name = name
1925 self.display_name = display_name
1926 self._is_aot_element = is_aot_element
1927 self._is_super_table = is_super_table
1929 @property
1930 def discriminant(self) -> int:
1931 return 9
1933 def __copy__(self) -> Table:
1934 return type(self)(
1935 self._value.copy(),
1936 self._trivia.copy(),
1937 self._is_aot_element,
1938 self._is_super_table,
1939 self.name,
1940 self.display_name,
1941 )
1943 def append(self, key: Key | str | None, _item: Any) -> Table:
1944 """
1945 Appends a (key, item) to the table.
1946 """
1947 if not isinstance(_item, Item):
1948 _item = item(_item, _parent=self)
1950 self._value.append(key, _item)
1952 if isinstance(key, Key):
1953 key = next(iter(key)).key
1954 _item = self._value[key]
1956 if key is not None:
1957 dict.__setitem__(self, key, _item)
1959 m = re.match(r"(?s)^[^ ]*([ ]+).*$", self._trivia.indent)
1960 if not m:
1961 return self
1963 indent = m.group(1)
1965 if not isinstance(_item, Whitespace):
1966 m = re.match("(?s)^([^ ]*)(.*)$", _item.trivia.indent)
1967 if not m:
1968 _item.trivia.indent = indent
1969 else:
1970 _item.trivia.indent = m.group(1) + indent + m.group(2)
1972 return self
1974 def raw_append(self, key: Key | str | None, _item: Any) -> Table:
1975 """Similar to :meth:`append` but does not copy indentation."""
1976 if not isinstance(_item, Item):
1977 _item = item(_item)
1979 self._value.append(key, _item, validate=False)
1981 if isinstance(key, Key):
1982 key = next(iter(key)).key
1983 _item = self._value[key]
1985 if key is not None:
1986 dict.__setitem__(self, key, _item)
1988 return self
1990 def is_aot_element(self) -> bool:
1991 """True if the table is the direct child of an AOT element."""
1992 return self._is_aot_element
1994 def is_super_table(self) -> bool:
1995 """A super table is the intermediate parent of a nested table as in [a.b.c].
1996 If true, it won't appear in the TOML representation."""
1997 if self._is_super_table is not None:
1998 return self._is_super_table
1999 if not self:
2000 return False
2001 # If the table has children and all children are tables, then it is a super table.
2002 for k, child in self.items():
2003 if not isinstance(k, Key):
2004 k = SingleKey(k)
2005 index = self.value._map[k]
2006 if isinstance(index, tuple):
2007 return False
2008 real_key = self.value.body[index][0]
2009 if (
2010 not isinstance(child, (Table, AoT))
2011 or real_key is None
2012 or real_key.is_dotted()
2013 ):
2014 return False
2015 return True
2017 def as_string(self) -> str:
2018 return self._value.as_string()
2020 # Helpers
2022 def indent(self, indent: int) -> Table:
2023 """Indent the table with given number of spaces."""
2024 super().indent(indent)
2026 m = re.match("(?s)^[^ ]*([ ]+).*$", self._trivia.indent)
2027 if not m:
2028 indent_str = ""
2029 else:
2030 indent_str = m.group(1)
2032 for _, item in self._value.body:
2033 if not isinstance(item, Whitespace):
2034 item.trivia.indent = indent_str + item.trivia.indent
2036 return self
2038 def invalidate_display_name(self) -> None:
2039 """Call ``invalidate_display_name`` on the contained tables"""
2040 self.display_name = None
2042 for child in self.values():
2043 if hasattr(child, "invalidate_display_name"):
2044 child.invalidate_display_name()
2046 def _getstate(
2047 self, protocol: int = 3
2048 ) -> tuple[container.Container, Trivia, bool, bool | None, str | None, str | None]:
2049 return (
2050 self._value,
2051 self._trivia,
2052 self._is_aot_element,
2053 self._is_super_table,
2054 self.name,
2055 self.display_name,
2056 )
2059class InlineTable(AbstractTable):
2060 """
2061 An inline table literal.
2062 """
2064 def __init__(
2065 self, value: container.Container, trivia: Trivia, new: bool = False
2066 ) -> None:
2067 super().__init__(value, trivia)
2069 self._new = new
2071 @property
2072 def discriminant(self) -> int:
2073 return 10
2075 def append(self, key: Key | str | None, _item: Any) -> InlineTable:
2076 """
2077 Appends a (key, item) to the table.
2078 """
2079 if not isinstance(_item, Item):
2080 _item = item(_item, _parent=self)
2082 self._validate_child(_item)
2084 if not isinstance(_item, (Whitespace, Comment)):
2085 if not _item.trivia.indent and len(self._value) > 0 and not self._new:
2086 _item.trivia.indent = " "
2087 if _item.trivia.comment:
2088 _item.trivia.comment = ""
2090 self._value.append(key, _item)
2092 if isinstance(key, Key):
2093 key = key.key
2095 if key is not None:
2096 dict.__setitem__(self, key, _item)
2098 return self
2100 def _validate_child(self, _item: Item) -> None:
2101 if isinstance(_item, Table):
2102 raise ValueError("Inline tables cannot contain a table")
2104 def as_string(self) -> str:
2105 buf = "{"
2106 emitted_key = False
2107 needs_separator = False
2108 # Single pass over the body to precompute everything the render loop
2109 # needs, instead of rescanning the tail on every separator comma (which
2110 # was O(n^2) on large inline tables): whether any explicit-comma
2111 # whitespace is present, the index of the last real key, and the index
2112 # of the last Null (deleted) element.
2113 has_explicit_commas = False
2114 last_item_idx = None
2115 last_null_idx = -1
2116 for _i, (_k, _v) in enumerate(self._value.body):
2117 if _k is not None:
2118 last_item_idx = _i
2119 elif isinstance(_v, Whitespace) and "," in _v.s:
2120 has_explicit_commas = True
2121 if isinstance(_v, Null):
2122 last_null_idx = _i
2123 pending_separator = False
2124 # Buffer position right after the last rendered value, used to place a
2125 # deferred separator comma after the value rather than after a trailing
2126 # comment/whitespace.
2127 last_value_end = len(buf)
2128 for i, (k, v) in enumerate(self._value.body):
2129 if k is None:
2130 if isinstance(v, Whitespace) and "," in v.s:
2131 if not emitted_key:
2132 buf += v.as_string().replace(",", "", 1)
2133 continue
2135 # Equivalent to scanning body[i + 1 :] for a Null / a real
2136 # key, but O(1) using the indices precomputed above.
2137 has_following_null = last_null_idx > i
2138 has_following_key = last_item_idx is not None and last_item_idx > i
2139 if has_following_null and not has_following_key:
2140 buf += v.as_string().replace(",", "", 1)
2141 continue
2143 if pending_separator:
2144 # A previous key was removed, leaving this comma as a
2145 # duplicate of an already emitted separator. Drop it to
2146 # avoid producing an invalid ``, ,`` sequence.
2147 buf += v.as_string().replace(",", "", 1)
2148 continue
2150 pending_separator = True
2152 if i == len(self._value.body) - 1:
2153 if self._new:
2154 buf = buf.rstrip(", ")
2155 elif not has_explicit_commas or "," in v.as_string():
2156 buf = buf.rstrip(",")
2158 v_string = v.as_string()
2159 buf += v_string
2160 if "," in v_string:
2161 needs_separator = False
2163 continue
2165 if has_explicit_commas and needs_separator:
2166 # Insert the deferred separator right after the previous value,
2167 # not after any trailing comment/whitespace -- otherwise the
2168 # comma is swallowed by a trailing comment (see #512).
2169 # Match the conventional ", " spacing used by the explicit
2170 # separators already in the table. Only add the space when the
2171 # text that will follow the comma (the previous value's trailing
2172 # trivia plus the new key's own indent) does not already start
2173 # with one, so a padded table like ``{ a = 1 }`` -- which
2174 # contributes the space itself -- does not end up double-spaced.
2175 following = buf[last_value_end:] + v.trivia.indent
2176 separator = "," if following.startswith(" ") else ", "
2177 buf = f"{buf[:last_value_end]}{separator}{buf[last_value_end:]}"
2178 needs_separator = False
2180 v_trivia_trail = v.trivia.trail.replace("\n", "")
2181 if k.is_dotted() and isinstance(v, Table):
2182 # A table materialized from a dotted key renders one
2183 # `prefix.child = value` pair per child, so that keys added
2184 # after parsing stay attached to the dotted prefix.
2185 rendered = ", ".join(self._render_dotted(k, v))
2186 else:
2187 rendered = (
2188 f"{k.as_string() + ('.' if k.is_dotted() else '')}"
2189 f"{k.sep}"
2190 f"{v.as_string()}"
2191 )
2192 buf += f"{v.trivia.indent}{rendered}"
2193 last_value_end = len(buf)
2194 buf += f"{v.trivia.comment}{v_trivia_trail}"
2195 emitted_key = True
2196 pending_separator = False
2197 if has_explicit_commas:
2198 needs_separator = True
2200 if (
2201 not has_explicit_commas
2202 and last_item_idx is not None
2203 and i < last_item_idx
2204 ):
2205 buf += ","
2206 if self._new:
2207 buf += " "
2209 buf += "}"
2211 return buf
2213 def _render_dotted(self, key: Key, table: Table) -> list[str]:
2214 """Render a table materialized from a dotted key as a list of
2215 ``prefix.child = value`` strings, recursing into nested dotted
2216 children."""
2217 prefix = f"{key.as_string()}.{key.sep}"
2218 parts = []
2219 for k, v in table.value.body:
2220 if k is None:
2221 continue
2222 if isinstance(v, Table):
2223 parts.extend(f"{prefix}{sub}" for sub in self._render_dotted(k, v))
2224 else:
2225 trail = v.trivia.trail.replace("\n", "")
2226 parts.append(
2227 f"{prefix}{v.trivia.indent}{k.as_string()}{k.sep}{v.as_string()}"
2228 f"{v.trivia.comment_ws}{v.trivia.comment}{trail}"
2229 )
2230 return parts
2232 def __setitem__(self, key: Key | str, value: Any) -> None:
2233 if hasattr(value, "trivia") and value.trivia.comment:
2234 value.trivia.comment = ""
2235 if not isinstance(value, Item):
2236 value = item(value, _parent=self)
2237 self._validate_child(value)
2238 super().__setitem__(key, value)
2240 def __copy__(self) -> InlineTable:
2241 return type(self)(self._value.copy(), self._trivia.copy(), self._new)
2243 def _getstate(self, protocol: int = 3) -> tuple[container.Container, Trivia]:
2244 return (self._value, self._trivia)
2247class String(str, Item):
2248 """
2249 A string literal.
2250 """
2252 def __new__(
2253 cls, t: StringType, value: str, original: str, trivia: Trivia
2254 ) -> String:
2255 return super().__new__(cls, value)
2257 def __init__(self, t: StringType, _: str, original: str, trivia: Trivia) -> None:
2258 super().__init__(trivia)
2260 self._t = t
2261 self._original = original
2263 def unwrap(self) -> str:
2264 return str(self)
2266 @property
2267 def discriminant(self) -> int:
2268 return 11
2270 @property
2271 def value(self) -> str:
2272 return self
2274 def as_string(self) -> str:
2275 return f"{self._t.value}{decode(self._original)}{self._t.value}"
2277 @property
2278 def type(self) -> StringType:
2279 return self._t
2281 def __add__(self, other: str) -> String:
2282 result = super().__add__(other)
2283 if isinstance(other, String):
2284 original = self._original + other._original
2285 else:
2286 escaped = self._t.escaped_sequences
2287 original = self._original + (
2288 escape_string(other, escaped) if escaped else other
2289 )
2291 return self._new(result, original)
2293 def _new(self, result: str, original: str) -> String:
2294 return String(self._t, result, original, self._trivia.copy())
2296 def _getstate(self, protocol: int = 3) -> tuple[StringType, str, str, Trivia]:
2297 return self._t, str(self), self._original, self._trivia
2299 @classmethod
2300 def from_raw(
2301 cls, value: str, type_: StringType = StringType.SLB, escape: bool = True
2302 ) -> String:
2303 value = decode(value)
2305 invalid = type_.invalid_sequences
2306 # collapse CRLF line endings so only a lone CR trips the CR check
2307 checked = value.replace("\r\n", "\n")
2308 if any(c in checked for c in invalid):
2309 raise InvalidStringError(value, invalid, type_.value)
2311 string_value = value
2312 if escape:
2313 string_value = escape_string(string_value, type_.escaped_sequences)
2314 if type_ is StringType.MLB and escape:
2315 string_value = _BARE_CR.sub(r"\\r", string_value)
2316 if type_.is_multiline() and string_value[:1] in ("\n", "\r"):
2317 # A newline immediately following the opening delimiter of a
2318 # multiline string is trimmed by the parser, so a value that
2319 # starts with a newline would otherwise lose it on round-trip.
2320 # Emit an extra leading newline (the trimmed one) to preserve it.
2321 string_value = "\n" + string_value
2323 return cls(type_, value, string_value, Trivia())
2326class AoT(Item, _CustomList): # type: ignore[type-arg]
2327 """
2328 An array of table literal
2329 """
2331 def __init__(
2332 self, body: list[Table], name: str | None = None, parsed: bool = False
2333 ) -> None:
2334 self.name = name
2335 self._body: list[Table] = []
2336 self._parsed = parsed
2338 super().__init__(Trivia(trail=""))
2340 for table in body:
2341 self.append(table)
2343 def unwrap(self) -> list[dict[str, Any]]:
2344 unwrapped = []
2345 for t in self._body:
2346 if hasattr(t, "unwrap"):
2347 unwrapped.append(t.unwrap())
2348 else:
2349 unwrapped.append(t)
2350 return unwrapped
2352 @property
2353 def body(self) -> list[Table]:
2354 return self._body
2356 @property
2357 def discriminant(self) -> int:
2358 return 12
2360 @property
2361 def value(self) -> list[dict[str, Any]]:
2362 return [v.value for v in self._body]
2364 def __len__(self) -> int:
2365 return len(self._body)
2367 @overload # type: ignore[override]
2368 def __getitem__(self, key: slice) -> list[Table]: ...
2370 @overload
2371 def __getitem__(self, key: int) -> Table: ...
2373 def __getitem__(self, key: int | slice) -> Table | list[Table]:
2374 return self._body[key]
2376 def __setitem__(self, key: slice | int, value: Any) -> None: # type: ignore[override]
2377 self._body[key] = item(value, _parent=self)
2379 def __delitem__(self, key: slice | int) -> None: # type: ignore[override]
2380 del self._body[key]
2381 list.__delitem__(self, key)
2383 def insert(self, index: int, value: dict[str, Any]) -> None: # type: ignore[override]
2384 value = item(value, _parent=self)
2385 if not isinstance(value, Table):
2386 raise ValueError(f"Unsupported insert value type: {type(value)}")
2387 length = len(self)
2388 if index < 0:
2389 index += length
2390 if index < 0:
2391 index = 0
2392 elif index >= length:
2393 index = length
2394 m = re.match("(?s)^[^ ]*([ ]+).*$", self._trivia.indent)
2395 if m:
2396 indent = m.group(1)
2398 m = re.match("(?s)^([^ ]*)(.*)$", value.trivia.indent)
2399 if not m:
2400 value.trivia.indent = indent
2401 else:
2402 value.trivia.indent = m.group(1) + indent + m.group(2)
2403 prev_table = self._body[index - 1] if 0 < index and length else None
2404 next_table = self._body[index + 1] if index < length - 1 else None
2405 if not self._parsed:
2406 if prev_table and "\n" not in value.trivia.indent:
2407 value.trivia.indent = "\n" + value.trivia.indent
2408 if next_table and "\n" not in next_table.trivia.indent:
2409 next_table.trivia.indent = "\n" + next_table.trivia.indent
2410 self._body.insert(index, value)
2411 list.insert(self, index, value)
2413 def invalidate_display_name(self) -> None:
2414 """Call ``invalidate_display_name`` on the contained tables"""
2415 for child in self:
2416 if hasattr(child, "invalidate_display_name"):
2417 child.invalidate_display_name()
2419 def as_string(self) -> str:
2420 b = ""
2421 for table in self._body:
2422 b += table.as_string()
2424 return b
2426 def __repr__(self) -> str:
2427 return f"<AoT {self.value}>"
2429 def _getstate(self, protocol: int = 3) -> tuple[list[Table], str | None, bool]:
2430 return self._body, self.name, self._parsed
2433class Null(Item):
2434 """
2435 A null item.
2436 """
2438 def __init__(self) -> None:
2439 super().__init__(Trivia(trail=""))
2441 def unwrap(self) -> None:
2442 return None
2444 @property
2445 def discriminant(self) -> int:
2446 return -1
2448 @property
2449 def value(self) -> None:
2450 return None
2452 def as_string(self) -> str:
2453 return ""
2455 def _getstate(self, protocol: int = 3) -> tuple[()]:
2456 return ()