Repository: sdispater/tomlkit
Pull request #284 (2023-04-27 08:09:38)

fix: Build table with dotted key correctly
Signed-off-by: Frost Ming <me@frostming.com>


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f6b33ce..4972d0a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 22.12.0
+    rev: 23.3.0
     hooks:
       - id: black
 
@@ -37,7 +37,7 @@ repos:
         exclude: ^tomlkit/items\.py
 
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.3.1
+    rev: v3.3.2
     hooks:
       - id: pyupgrade
         args: [--py37-plus]
diff --git a/pyproject.toml b/pyproject.toml
index b54c0fe..edae337 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,7 +2,10 @@
 name = "tomlkit"
 version = "0.11.7"
 description = "Style preserving TOML library"
-authors = ["Sébastien Eustace <sebastien@eustace.io>"]
+authors = [
+    "Sébastien Eustace <sebastien@eustace.io>",
+    "Frost Ming <me@frostming.com>"
+]
 license = "MIT"
 
 readme = "README.md"
diff --git a/tomlkit/_compat.py b/tomlkit/_compat.py
index f1d3bcc..8e76b7f 100644
--- a/tomlkit/_compat.py
+++ b/tomlkit/_compat.py
@@ -1,15 +1,15 @@
+from __future__ import annotations
+
 import contextlib
 import sys
 
 from typing import Any
-from typing import List
-from typing import Optional
 
 
 PY38 = sys.version_info >= (3, 8)
 
 
-def decode(string: Any, encodings: Optional[List[str]] = None):
+def decode(string: Any, encodings: list[str] | None = None):
     if not isinstance(string, bytes):
         return string
 
diff --git a/tomlkit/_utils.py b/tomlkit/_utils.py
index 85958e9..a6ed9c6 100644
--- a/tomlkit/_utils.py
+++ b/tomlkit/_utils.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import re
 
 from collections.abc import Mapping
@@ -7,7 +9,6 @@ from datetime import time
 from datetime import timedelta
 from datetime import timezone
 from typing import Collection
-from typing import Union
 
 from tomlkit._compat import decode
 
@@ -41,7 +42,7 @@ RFC_3339_TIME = re.compile(
 _utc = timezone(timedelta(), "UTC")
 
 
-def parse_rfc3339(string: str) -> Union[datetime, date, time]:
+def parse_rfc3339(string: str) -> datetime | date | time:
     m = RFC_3339_DATETIME.match(string)
     if m:
         year = int(m.group(1))
diff --git a/tomlkit/api.py b/tomlkit/api.py
index c71491a..8ec5653 100644
--- a/tomlkit/api.py
+++ b/tomlkit/api.py
@@ -1,11 +1,10 @@
+from __future__ import annotations
+
 import datetime as _datetime
 
 from collections.abc import Mapping
 from typing import IO
 from typing import Iterable
-from typing import Optional
-from typing import Tuple
-from typing import Union
 
 from tomlkit._utils import parse_rfc3339
 from tomlkit.container import Container
@@ -34,7 +33,7 @@ from tomlkit.parser import Parser
 from tomlkit.toml_document import TOMLDocument
 
 
-def loads(string: Union[str, bytes]) -> TOMLDocument:
+def loads(string: str | bytes) -> TOMLDocument:
     """
     Parses a string into a TOMLDocument.
 
@@ -59,7 +58,7 @@ def dumps(data: Mapping, sort_keys: bool = False) -> str:
         raise TypeError(msg) from ex
 
 
-def load(fp: Union[IO[str], IO[bytes]]) -> TOMLDocument:
+def load(fp: IO[str] | IO[bytes]) -> TOMLDocument:
     """
     Load toml document from a file-like object.
     """
@@ -76,7 +75,7 @@ def dump(data: Mapping, fp: IO[str], *, sort_keys: bool = False) -> None:
     fp.write(dumps(data, sort_keys=sort_keys))
 
 
-def parse(string: Union[str, bytes]) -> TOMLDocument:
+def parse(string: str | bytes) -> TOMLDocument:
     """
     Parses a string or bytes into a TOMLDocument.
     """
@@ -91,12 +90,12 @@ def document() -> TOMLDocument:
 
 
 # Items
-def integer(raw: Union[str, int]) -> Integer:
+def integer(raw: str | int) -> Integer:
     """Create an integer item from a number or string."""
     return item(int(raw))
 
 
-def float_(raw: Union[str, float]) -> Float:
+def float_(raw: str | float) -> Float:
     """Create an float item from a number or string."""
     return item(float(raw))
 
@@ -175,7 +174,7 @@ def array(raw: str = None) -> Array:
     return value(raw)
 
 
-def table(is_super_table: Optional[bool] = None) -> Table:
+def table(is_super_table: bool | None = None) -> Table:
     """Create an empty table.
 
     :param is_super_table: if true, the table is a super table
@@ -224,7 +223,7 @@ def aot() -> AoT:
     return AoT([])
 
 
-def key(k: Union[str, Iterable[str]]) -> Key:
+def key(k: str | Iterable[str]) -> Key:
     """Create a key from a string. When a list of string is given,
     it will create a dotted key.
 
@@ -261,7 +260,7 @@ def value(raw: str) -> _Item:
     return v
 
 
-def key_value(src: str) -> Tuple[Key, _Item]:
+def key_value(src: str) -> tuple[Key, _Item]:
     """Parse a key-value pair from a string.
 
     :Example:
diff --git a/tomlkit/container.py b/tomlkit/container.py
index 302127d..502f69a 100644
--- a/tomlkit/container.py
+++ b/tomlkit/container.py
@@ -1,12 +1,9 @@
+from __future__ import annotations
+
 import copy
 
 from typing import Any
-from typing import Dict
 from typing import Iterator
-from typing import List
-from typing import Optional
-from typing import Tuple
-from typing import Union
 
 from tomlkit._compat import decode
 from tomlkit._utils import merge_dicts
@@ -37,16 +34,16 @@ class Container(_CustomDict):
     """
 
     def __init__(self, parsed: bool = False) -> None:
-        self._map: Dict[Key, int] = {}
-        self._body: List[Tuple[Optional[Key], Item]] = []
+        self._map: dict[SingleKey, int | tuple[int, ...]] = {}
+        self._body: list[tuple[Key | None, Item]] = []
         self._parsed = parsed
         self._table_keys = []
 
     @property
-    def body(self) -> List[Tuple[Optional[Key], Item]]:
+    def body(self) -> list[tuple[Key | None, Item]]:
         return self._body
 
-    def unwrap(self) -> Dict[str, Any]:
+    def unwrap(self) -> dict[str, Any]:
         unwrapped = {}
         for k, v in self.items():
             if k is None:
@@ -66,7 +63,7 @@ class Container(_CustomDict):
         return unwrapped
 
     @property
-    def value(self) -> Dict[str, Any]:
+    def value(self) -> dict[str, Any]:
         d = {}
         for k, v in self._body:
             if k is None:
@@ -95,9 +92,7 @@ class Container(_CustomDict):
                 for t in v.body:
                     t.value.parsing(parsing)
 
-    def add(
-        self, key: Union[Key, Item, str], item: Optional[Item] = None
-    ) -> "Container":
+    def add(self, key: Key | Item | str, item: Item | None = None) -> Container:
         """
         Adds an item to the current Container.
 
@@ -119,64 +114,38 @@ class Container(_CustomDict):
         return self.append(key, item)
 
     def _handle_dotted_key(self, key: Key, value: Item) -> None:
-        names = tuple(iter(key))
-        name = names[0]
+        if isinstance(value, (Table, AoT)):
+            raise TOMLKitError("Can't add a table to a dotted key")
+        name, *mid, last = key
         name._dotted = True
-        if name in self:
-            if not isinstance(value, Table):
-                table = Table(Container(True), Trivia(), False, is_super_table=True)
-                _table = table
-                for i, _name in enumerate(names[1:]):
-                    if i == len(names) - 2:
-                        _name.sep = key.sep
-
-                        _table.append(_name, value)
-                    else:
-                        _name._dotted = True
-                        _table.append(
-                            _name,
-                            Table(
-                                Container(True),
-                                Trivia(),
-                                False,
-                                is_super_table=i < len(names) - 2,
-                            ),
-                        )
-
-                        _table = _table[_name]
-
-                value = table
-
-            self.append(name, value)
-
-            return
-        else:
-            table = Table(Container(True), Trivia(), False, is_super_table=True)
-            self.append(name, table)
+        table = current = Table(Container(True), Trivia(), False, is_super_table=True)
+        for _name in mid:
+            _name._dotted = True
+            new_table = Table(Container(True), Trivia(), False, is_super_table=True)
+            current.append(_name, new_table)
+            current = new_table
+
+        last.sep = key.sep
+        current.append(last, value)
+
+        self.append(name, table)
+        return
+
+    def _get_last_index_before_table(self) -> int:
+        last_index = -1
+        for i, (k, v) in enumerate(self._body):
+            if isinstance(v, Null):
+                continue  # Null elements are inserted after deletion
+
+            if isinstance(v, Whitespace) and not v.is_fixed():
+                continue
 
-        for i, _name in enumerate(names[1:]):
-            if i == len(names) - 2:
-                _name.sep = key.sep
+            if isinstance(v, (Table, AoT)) and not k.is_dotted():
+                break
+            last_index = i
+        return last_index + 1
 
-                table.append(_name, value)
-            else:
-                _name._dotted = True
-                if _name in table.value:
-                    table = table.value[_name]
-                else:
-                    table.append(
-                        _name,
-                        Table(
-                            Container(True),
-                            Trivia(),
-                            False,
-                            is_super_table=i < len(names) - 2,
-                        ),
-                    )
-
-                    table = table[_name]
-
-    def append(self, key: Union[Key, str, None], item: Item) -> "Container":
+    def append(self, key: Key | str | None, item: Item) -> Container:
         """Similar to :meth:`add` but both key and value must be given."""
         if not isinstance(key, Key) and key is not None:
             key = SingleKey(key)
@@ -196,7 +165,11 @@ class Container(_CustomDict):
         if isinstance(item, Table):
             if not self._parsed:
                 item.invalidate_display_name()
-            if self._body and not (self._parsed or item.trivia.indent or prev_ws):
+            if (
+                self._body
+                and not (self._parsed or item.trivia.indent or prev_ws)
+                and not key.is_dotted()
+            ):
                 item.trivia.indent = "\n"
 
         if isinstance(item, AoT) and self._body and not self._parsed:
@@ -244,12 +217,15 @@ class Container(_CustomDict):
                             or key.is_dotted()
                             or current_body_element[0].is_dotted()
                         ):
-                            if not isinstance(current_idx, tuple):
-                                current_idx = (current_idx,)
+                            if key.is_dotted() and not self._parsed:
+                                idx = self._get_last_index_before_table()
+                            else:
+                                idx = len(self._body)
 
-                            self._map[key] = current_idx + (len(self._body),)
-                            self._body.append((key, item))
-                            self._table_keys.append(key)
+                            if idx < len(self._body):
+                                self._insert_at(idx, key, item)
+                            else:
+                                self._raw_append(key, item)
 
                             # Building a temporary proxy to check for errors
                             OutOfOrderTableProxy(self, self._map[key])
@@ -284,55 +260,42 @@ class Container(_CustomDict):
                 raise KeyAlreadyPresent(key)
 
         is_table = isinstance(item, (Table, AoT))
-        if key is not None and self._body and not self._parsed:
+        if (
+            key is not None
+            and self._body
+            and not self._parsed
+            and (not is_table or key.is_dotted())
+        ):
             # If there is already at least one table in the current container
             # and the given item is not a table, we need to find the last
             # item that is not a table and insert after it
             # If no such item exists, insert at the top of the table
-            key_after = None
-            for i, (k, v) in enumerate(self._body):
-                if isinstance(v, Null):
-                    continue  # Null elements are inserted after deletion
-
-                if isinstance(v, Whitespace) and not v.is_fixed():
-                    continue
-
-                if not is_table and isinstance(v, (Table, AoT)):
-                    break
-
-                key_after = k or i  # last scalar, Array or InlineTable value
+            last_index = self._get_last_index_before_table()
 
-            if key_after is not None:
-                if isinstance(key_after, int):
-                    if key_after + 1 < len(self._body):
-                        return self._insert_at(key_after + 1, key, item)
-                    else:
-                        previous_item = self._body[-1][1]
-                        if not (
-                            isinstance(previous_item, Whitespace)
-                            or ends_with_whitespace(previous_item)
-                            or is_table
-                            or "\n" in previous_item.trivia.trail
-                        ):
-                            previous_item.trivia.trail += "\n"
-                else:
-                    return self._insert_after(key_after, key, item)
+            if last_index < len(self._body):
+                return self._insert_at(last_index, key, item)
             else:
-                return self._insert_at(0, key, item)
+                previous_item = self._body[-1][1]
+                if not (
+                    isinstance(previous_item, Whitespace)
+                    or ends_with_whitespace(previous_item)
+                    or "\n" in previous_item.trivia.trail
+                ):
+                    previous_item.trivia.trail += "\n"
+
+        self._raw_append(key, item)
+        return self
 
+    def _raw_append(self, key: Key, item: Item) -> None:
         if key in self._map:
             current_idx = self._map[key]
-            if isinstance(current_idx, tuple):
-                current_idx = current_idx[-1]
+            if not isinstance(current_idx, tuple):
+                current_idx = (current_idx,)
 
-            current = self._body[current_idx][1]
+            current = self._body[current_idx[-1]][1]
             if key is not None and not isinstance(current, Table):
                 raise KeyAlreadyPresent(key)
 
-            # Adding sub tables to a currently existing table
-            if not isinstance(current_idx, tuple):
-                current_idx = (current_idx,)
-
             self._map[key] = current_idx + (len(self._body),)
         else:
             self._map[key] = len(self._body)
@@ -365,7 +328,7 @@ class Container(_CustomDict):
             dict.__delitem__(self, key.key)
             self._map.pop(key)
 
-    def remove(self, key: Union[Key, str]) -> "Container":
+    def remove(self, key: Key | str) -> Container:
         """Remove a key from the container."""
         if not isinstance(key, Key):
             key = SingleKey(key)
@@ -385,8 +348,8 @@ class Container(_CustomDict):
         return self
 
     def _insert_after(
-        self, key: Union[Key, str], other_key: Union[Key, str], item: Any
-    ) -> "Container":
+        self, key: Key | str, other_key: Key | str, item: Any
+    ) -> Container:
         if key is None:
             raise ValueError("Key cannot be null in insert_after()")
 
@@ -431,7 +394,7 @@ class Container(_CustomDict):
 
         return self
 
-    def _insert_at(self, idx: int, key: Union[Key, str], item: Any) -> "Container":
+    def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container:
         if idx > len(self._body) - 1:
             raise ValueError(f"Unable to insert at position {idx}")
 
@@ -464,15 +427,20 @@ class Container(_CustomDict):
             elif v >= idx:
                 self._map[k] = v + 1
 
-        self._map[key] = idx
+        if key in self._map:
+            current_idx = self._map[key]
+            if not isinstance(current_idx, tuple):
+                current_idx = (current_idx,)
+            self._map[key] = current_idx + (idx,)
+        else:
+            self._map[key] = idx
         self._body.insert(idx, (key, item))
 
-        if key is not None:
-            dict.__setitem__(self, key.key, item.value)
+        dict.__setitem__(self, key.key, item.value)
 
         return self
 
-    def item(self, key: Union[Key, str]) -> Item:
+    def item(self, key: Key | str) -> Item:
         """Get an item for the given key."""
         if not isinstance(key, Key):
             key = SingleKey(key)
@@ -489,7 +457,7 @@ class Container(_CustomDict):
 
         return self._body[idx][1]
 
-    def last_item(self) -> Optional[Item]:
+    def last_item(self) -> Item | None:
         """Get the last item."""
         if self._body:
             return self._body[-1][1]
@@ -510,9 +478,7 @@ class Container(_CustomDict):
 
         return s
 
-    def _render_table(
-        self, key: Key, table: Table, prefix: Optional[str] = None
-    ) -> str:
+    def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
         cur = ""
 
         if table.display_name is not None:
@@ -581,7 +547,7 @@ class Container(_CustomDict):
 
         return cur
 
-    def _render_aot_table(self, table: Table, prefix: Optional[str] = None) -> str:
+    def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
         cur = ""
 
         _key = prefix or ""
@@ -641,7 +607,7 @@ class Container(_CustomDict):
         return iter(dict.keys(self))
 
     # Dictionary methods
-    def __getitem__(self, key: Union[Key, str]) -> Union[Item, "Container"]:
+    def __getitem__(self, key: Key | str) -> Item | Container:
         if not isinstance(key, Key):
             key = SingleKey(key)
 
@@ -661,23 +627,21 @@ class Container(_CustomDict):
 
         return item
 
-    def __setitem__(self, key: Union[Key, str], value: Any) -> None:
+    def __setitem__(self, key: Key | str, value: Any) -> None:
         if key is not None and key in self:
             old_key = next(filter(lambda k: k == key, self._map))
             self._replace(old_key, key, value)
         else:
             self.append(key, value)
 
-    def __delitem__(self, key: Union[Key, str]) -> None:
+    def __delitem__(self, key: Key | str) -> None:
         self.remove(key)
 
-    def setdefault(self, key: Union[Key, str], default: Any) -> Any:
+    def setdefault(self, key: Key | str, default: Any) -> Any:
         super().setdefault(key, default=default)
         return self[key]
 
-    def _replace(
-        self, key: Union[Key, str], new_key: Union[Key, str], value: Item
-    ) -> None:
+    def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None:
         if not isinstance(key, Key):
             key = SingleKey(key)
 
@@ -688,7 +652,7 @@ class Container(_CustomDict):
         self._replace_at(idx, new_key, value)
 
     def _replace_at(
-        self, idx: Union[int, Tuple[int]], new_key: Union[Key, str], value: Item
+        self, idx: int | tuple[int], new_key: Key | str, value: Item
     ) -> None:
         value = _item(value)
 
@@ -784,10 +748,10 @@ class Container(_CustomDict):
             if key is not None:
                 dict.__setitem__(self, key.key, item.value)
 
-    def copy(self) -> "Container":
+    def copy(self) -> Container:
         return copy.copy(self)
 
-    def __copy__(self) -> "Container":
+    def __copy__(self) -> Container:
         c = self.__class__(self._parsed)
         for k, v in dict.items(self):
             dict.__setitem__(c, k, v)
@@ -798,8 +762,8 @@ class Container(_CustomDict):
         return c
 
     def _previous_item_with_index(
-        self, idx: Optional[int] = None, ignore=(Null,)
-    ) -> Optional[Tuple[int, Item]]:
+        self, idx: int | None = None, ignore=(Null,)
+    ) -> tuple[int, Item] | None:
         """Find the immediate previous item before index ``idx``"""
         if idx is None or idx > len(self._body):
             idx = len(self._body)
@@ -809,9 +773,7 @@ class Container(_CustomDict):
                 return i, v
         return None
 
-    def _previous_item(
-        self, idx: Optional[int] = None, ignore=(Null,)
-    ) -> Optional[Item]:
+    def _previous_item(self, idx: int | None = None, ignore=(Null,)) -> Item | None:
         """Find the immediate previous item before index ``idx``.
         If ``idx`` is not given, the last item is returned.
         """
@@ -820,7 +782,7 @@ class Container(_CustomDict):
 
 
 class OutOfOrderTableProxy(_CustomDict):
-    def __init__(self, container: Container, indices: Tuple[int]) -> None:
+    def __init__(self, container: Container, indices: tuple[int]) -> None:
         self._container = container
         self._internal_container = Container(True)
         self._tables = []
@@ -845,13 +807,13 @@ class OutOfOrderTableProxy(_CustomDict):
     def value(self):
         return self._internal_container.value
 
-    def __getitem__(self, key: Union[Key, str]) -> Any:
+    def __getitem__(self, key: Key | str) -> Any:
         if key not in self._internal_container:
             raise NonExistentKey(key)
 
         return self._internal_container[key]
 
-    def __setitem__(self, key: Union[Key, str], item: Any) -> None:
+    def __setitem__(self, key: Key | str, item: Any) -> None:
         if key in self._tables_map:
             table = self._tables[self._tables_map[key]]
             table[key] = item
@@ -873,7 +835,7 @@ class OutOfOrderTableProxy(_CustomDict):
                 self._container._remove_at(idx)
                 break
 
-    def __delitem__(self, key: Union[Key, str]) -> None:
+    def __delitem__(self, key: Key | str) -> None:
         if key in self._tables_map:
             table = self._tables[self._tables_map[key]]
             del table[key]
@@ -893,7 +855,7 @@ class OutOfOrderTableProxy(_CustomDict):
     def __len__(self) -> int:
         return dict.__len__(self)
 
-    def setdefault(self, key: Union[Key, str], default: Any) -> Any:
+    def setdefault(self, key: Key | str, default: Any) -> Any:
         super().setdefault(key, default=default)
         return self[key]
 
diff --git a/tomlkit/exceptions.py b/tomlkit/exceptions.py
index 3147ca2..30d0d85 100644
--- a/tomlkit/exceptions.py
+++ b/tomlkit/exceptions.py
@@ -1,9 +1,9 @@
+from __future__ import annotations
+
 from typing import Collection
-from typing import Optional
 
 
 class TOMLKitError(Exception):
-
     pass
 
 
@@ -14,7 +14,7 @@ class ParseError(ValueError, TOMLKitError):
     location within the line where the error was encountered.
     """
 
-    def __init__(self, line: int, col: int, message: Optional[str] = None) -> None:
+    def __init__(self, line: int, col: int, message: str | None = None) -> None:
         self._line = line
         self._col = col
 
@@ -169,7 +169,7 @@ class InternalParserError(ParseError):
     An error that indicates a bug in the parser.
     """
 
-    def __init__(self, line: int, col: int, message: Optional[str] = None) -> None:
+    def __init__(self, line: int, col: int, message: str | None = None) -> None:
         msg = "Internal parser error"
         if message:
             msg += f" ({message})"
diff --git a/tomlkit/items.py b/tomlkit/items.py
index d6a9224..77f4986 100644
--- a/tomlkit/items.py
+++ b/tomlkit/items.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import abc
 import copy
 import re
@@ -11,14 +13,10 @@ from enum import Enum
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Collection
-from typing import Dict
 from typing import Iterable
 from typing import Iterator
-from typing import List
-from typing import Optional
 from typing import Sequence
 from typing import TypeVar
-from typing import Union
 from typing import cast
 from typing import overload
 
@@ -61,90 +59,70 @@ ItemT = TypeVar("ItemT", bound="Item")
 
 
 @overload
-def item(
-    value: bool, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "Bool":
+def item(value: bool, _parent: Item | None = ..., _sort_keys: bool = ...) -> Bool:
     ...
 
 
 @overload
-def item(
-    value: int, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "Integer":
+def item(value: int, _parent: Item | None = ..., _sort_keys: bool = ...) -> Integer:
     ...
 
 
 @overload
-def item(
-    value: float, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "Float":
+def item(value: float, _parent: Item | None = ..., _sort_keys: bool = ...) -> Float:
     ...
 
 
 @overload
-def item(
-    value: str, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "String":
+def item(value: str, _parent: Item | None = ..., _sort_keys: bool = ...) -> String:
     ...
 
 
 @overload
 def item(
-    value: datetime, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "DateTime":
+    value: datetime, _parent: Item | None = ..., _sort_keys: bool = ...
+) -> DateTime:
     ...
 
 
 @overload
-def item(
-    value: date, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "Date":
+def item(value: date, _parent: Item | None = ..., _sort_keys: bool = ...) -> Date:
     ...
 
 
 @overload
-def item(
-    value: time, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "Time":
+def item(value: time, _parent: Item | None = ..., _sort_keys: bool = ...) -> Time:
     ...
 
 
 @overload
 def item(
-    value: Sequence[dict], _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "AoT":
+    value: Sequence[dict], _parent: Item | None = ..., _sort_keys: bool = ...
+) -> AoT:
     ...
 
 
 @overload
-def item(
-    value: Sequence, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "Array":
+def item(value: Sequence, _parent: Item | None = ..., _sort_keys: bool = ...) -> Array:
     ...
 
 
 @overload
-def item(value: dict, _parent: "Array" = ..., _sort_keys: bool = ...) -> "InlineTable":
+def item(value: dict, _parent: Array = ..., _sort_keys: bool = ...) -> InlineTable:
     ...
 
 
 @overload
-def item(
-    value: dict, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> "Table":
+def item(value: dict, _parent: Item | None = ..., _sort_keys: bool = ...) -> Table:
     ...
 
 
 @overload
-def item(
-    value: ItemT, _parent: Optional["Item"] = ..., _sort_keys: bool = ...
-) -> ItemT:
+def item(value: ItemT, _parent: Item | None = ..., _sort_keys: bool = ...) -> ItemT:
     ...
 
 
-def item(
-    value: Any, _parent: Optional["Item"] = None, _sort_keys: bool = False
-) -> "Item":
+def item(value: Any, _parent: Item | None = None, _sort_keys: bool = False) -> Item:
     """Create a TOML item from a Python object.
 
     :Example:
@@ -254,7 +232,7 @@ class StringType(Enum):
     MLL = "'''"
 
     @classmethod
-    def select(cls, literal=False, multiline=False) -> "StringType":
+    def select(cls, literal=False, multiline=False) -> StringType:
         return {
             (False, False): cls.SLB,
             (False, True): cls.MLB,
@@ -302,7 +280,7 @@ class StringType(Enum):
     def is_multiline(self) -> bool:
         return self in {StringType.MLB, StringType.MLL}
 
-    def toggle(self) -> "StringType":
+    def toggle(self) -> StringType:
         return {
             StringType.SLB: StringType.MLB,
             StringType.MLB: StringType.SLB,
@@ -349,7 +327,7 @@ class Trivia:
 
         self.trail = trail
 
-    def copy(self) -> "Trivia":
+    def copy(self) -> Trivia:
         return type(self)(self.indent, self.comment_ws, self.comment, self.trail)
 
 
@@ -371,7 +349,7 @@ class Key(abc.ABC):
 
     sep: str
     _original: str
-    _keys: List["SingleKey"]
+    _keys: list[SingleKey]
     _dotted: bool
     key: str
 
@@ -387,10 +365,10 @@ class Key(abc.ABC):
         """If the key is followed by other keys"""
         return self._dotted
 
-    def __iter__(self) -> Iterator["SingleKey"]:
+    def __iter__(self) -> Iterator[SingleKey]:
         return iter(self._keys)
 
-    def concat(self, other: "Key") -> "DottedKey":
+    def concat(self, other: Key) -> DottedKey:
         """Concatenate keys into a dotted key"""
         keys = self._keys + other._keys
         return DottedKey(keys, sep=self.sep)
@@ -416,9 +394,9 @@ class SingleKey(Key):
     def __init__(
         self,
         k: str,
-        t: Optional[KeyType] = None,
-        sep: Optional[str] = None,
-        original: Optional[str] = None,
+        t: KeyType | None = None,
+        sep: str | None = None,
+        original: str | None = None,
     ) -> None:
         if t is None:
             if not k or any(
@@ -465,8 +443,8 @@ class DottedKey(Key):
     def __init__(
         self,
         keys: Iterable[Key],
-        sep: Optional[str] = None,
-        original: Optional[str] = None,
+        sep: str | None = None,
+        original: str | None = None,
     ) -> None:
         self._keys = list(keys)
         if original is None:
@@ -515,7 +493,7 @@ class Item:
 
     # Helpers
 
-    def comment(self, comment: str) -> "Item":
+    def comment(self, comment: str) -> Item:
         """Attach a comment to this item"""
         if not comment.strip().startswith("#"):
             comment = "# " + comment
@@ -525,7 +503,7 @@ class Item:
 
         return self
 
-    def indent(self, indent: int) -> "Item":
+    def indent(self, indent: int) -> Item:
         """Indent this item with given number of spaces"""
         if self._trivia.indent.startswith("\n"):
             self._trivia.indent = "\n" + " " * indent
@@ -619,7 +597,7 @@ class Integer(int, Item):
     An integer literal.
     """
 
-    def __new__(cls, value: int, trivia: Trivia, raw: str) -> "Integer":
+    def __new__(cls, value: int, trivia: Trivia, raw: str) -> Integer:
         return super().__new__(cls, value)
 
     def __init__(self, _: int, trivia: Trivia, raw: str) -> None:
@@ -813,7 +791,7 @@ class DateTime(Item, datetime):
         minute: int,
         second: int,
         microsecond: int,
-        tzinfo: Optional[tzinfo],
+        tzinfo: tzinfo | None,
         *_: Any,
         **kwargs: Any,
     ) -> datetime:
@@ -839,9 +817,9 @@ class DateTime(Item, datetime):
         minute: int,
         second: int,
         microsecond: int,
-        tzinfo: Optional[tzinfo],
-        trivia: Optional[Trivia] = None,
-        raw: Optional[str] = None,
+        tzinfo: tzinfo | None,
+        trivia: Trivia | None = None,
+        raw: str | None = None,
         **kwargs: Any,
     ) -> None:
         super().__init__(trivia or Trivia())
@@ -920,7 +898,7 @@ class DateTime(Item, datetime):
             return result
         return self._new(result)
 
-    def _new(self, result) -> "DateTime":
+    def _new(self, result) -> DateTime:
         raw = result.isoformat()
 
         return DateTime(
@@ -1023,7 +1001,7 @@ class Time(Item, time):
         minute: int,
         second: int,
         microsecond: int,
-        tzinfo: Optional[tzinfo],
+        tzinfo: tzinfo | None,
         *_: Any,
     ) -> time:
         return time.__new__(cls, hour, minute, second, microsecond, tzinfo)
@@ -1034,7 +1012,7 @@ class Time(Item, time):
         minute: int,
         second: int,
         microsecond: int,
-        tzinfo: Optional[tzinfo],
+        tzinfo: tzinfo | None,
         trivia: Trivia,
         raw: str,
     ) -> None:
@@ -1090,10 +1068,10 @@ class _ArrayItemGroup:
 
     def __init__(
         self,
-        value: Optional[Item] = None,
-        indent: Optional[Whitespace] = None,
-        comma: Optional[Whitespace] = None,
-        comment: Optional[Comment] = None,
+        value: Item | None = None,
+        indent: Whitespace | None = None,
+        comma: Whitespace | None = None,
+        comment: Comment | None = None,
     ) -> None:
         self.value = value
         self.indent = indent
@@ -1125,19 +1103,19 @@ class Array(Item, _CustomList):
     """
 
     def __init__(
-        self, value: List[Item], trivia: Trivia, multiline: bool = False
+        self, value: list[Item], trivia: Trivia, multiline: bool = False
     ) -> None:
         super().__init__(trivia)
         list.__init__(
             self,
             [v for v in value if not isinstance(v, (Whitespace, Comment, Null))],
         )
-        self._index_map: Dict[int, int] = {}
+        self._index_map: dict[int, int] = {}
         self._value = self._group_values(value)
         self._multiline = multiline
         self._reindex()
 
-    def _group_values(self, value: List[Item]) -> List[_ArrayItemGroup]:
+    def _group_values(self, value: list[Item]) -> list[_ArrayItemGroup]:
         """Group the values into (indent, value, comma, comment) tuples"""
         groups = []
         this_group = _ArrayItemGroup()
@@ -1163,7 +1141,7 @@ class Array(Item, _CustomList):
         groups.append(this_group)
         return [group for group in groups if group]
 
-    def unwrap(self) -> List[Any]:
+    def unwrap(self) -> list[Any]:
         unwrapped = []
         for v in self:
             if hasattr(v, "unwrap"):
@@ -1184,7 +1162,7 @@ class Array(Item, _CustomList):
         for v in self._value:
             yield from v
 
-    def multiline(self, multiline: bool) -> "Array":
+    def multiline(self, multiline: bool) -> Array:
         """Change the array to display in multiline or not.
 
         :Example:
@@ -1235,7 +1213,7 @@ class Array(Item, _CustomList):
         self,
         *items: Any,
         indent: str = "    ",
-        comment: Optional[str] = None,
+        comment: str | None = None,
         add_comma: bool = True,
         newline: bool = True,
     ) -> None:
@@ -1255,7 +1233,7 @@ class Array(Item, _CustomList):
             4, 5, 6,
         ]
         """
-        new_values: List[Item] = []
+        new_values: list[Item] = []
         first_indent = f"\n{indent}" if newline else indent
         if first_indent:
             new_values.append(Whitespace(first_indent))
@@ -1316,13 +1294,13 @@ class Array(Item, _CustomList):
     def __len__(self) -> int:
         return list.__len__(self)
 
-    def __getitem__(self, key: Union[int, slice]) -> Any:
+    def __getitem__(self, key: int | slice) -> Any:
         rv = cast(Item, list.__getitem__(self, key))
         if rv.is_boolean():
             return bool(rv)
         return rv
 
-    def __setitem__(self, key: Union[int, slice], value: Any) -> Any:
+    def __setitem__(self, key: int | slice, value: Any) -> Any:
         it = item(value, _parent=self)
         list.__setitem__(self, key, it)
         if isinstance(key, slice):
@@ -1358,8 +1336,8 @@ class Array(Item, _CustomList):
                     and "\n" in self._value[idx].indent.s
                 ):
                     default_indent = "\n    "
-        indent: Optional[Item] = None
-        comma: Optional[Item] = Whitespace(",") if pos < length else None
+        indent: Item | None = None
+        comma: Item | None = Whitespace(",") if pos < length else None
         if idx < len(self._value) and not self._value[idx].is_whitespace():
             # Prefer to copy the indentation from the item after
             indent = self._value[idx].indent
@@ -1381,7 +1359,7 @@ class Array(Item, _CustomList):
         self._value.insert(idx, new_item)
         self._reindex()
 
-    def __delitem__(self, key: Union[int, slice]):
+    def __delitem__(self, key: int | slice):
         length = len(self)
         list.__delitem__(self, key)
 
@@ -1424,7 +1402,7 @@ AT = TypeVar("AT", bound="AbstractTable")
 class AbstractTable(Item, _CustomDict):
     """Common behaviour of both :class:`Table` and :class:`InlineTable`"""
 
-    def __init__(self, value: "container.Container", trivia: Trivia):
+    def __init__(self, value: container.Container, trivia: Trivia):
         Item.__init__(self, trivia)
 
         self._value = value
@@ -1433,7 +1411,7 @@ class AbstractTable(Item, _CustomDict):
             if k is not None:
                 dict.__setitem__(self, k.key, v)
 
-    def unwrap(self) -> Dict[str, Any]:
+    def unwrap(self) -> dict[str, Any]:
         unwrapped = {}
         for k, v in self.items():
             if isinstance(k, Key):
@@ -1445,26 +1423,26 @@ class AbstractTable(Item, _CustomDict):
         return unwrapped
 
     @property
-    def value(self) -> "container.Container":
+    def value(self) -> container.Container:
         return self._value
 
     @overload
-    def append(self: AT, key: None, value: Union[Comment, Whitespace]) -> AT:
+    def append(self: AT, key: None, value: Comment | Whitespace) -> AT:
         ...
 
     @overload
-    def append(self: AT, key: Union[Key, str], value: Any) -> AT:
+    def append(self: AT, key: Key | str, value: Any) -> AT:
         ...
 
     def append(self, key, value):
         raise NotImplementedError
 
     @overload
-    def add(self: AT, value: Union[Comment, Whitespace]) -> AT:
+    def add(self: AT, value: Comment | Whitespace) -> AT:
         ...
 
     @overload
-    def add(self: AT, key: Union[Key, str], value: Any) -> AT:
+    def add(self: AT, key: Key | str, value: Any) -> AT:
         ...
 
     def add(self, key, value=None):
@@ -1477,7 +1455,7 @@ class AbstractTable(Item, _CustomDict):
 
         return self.append(key, value)
 
-    def remove(self: AT, key: Union[Key, str]) -> AT:
+    def remove(self: AT, key: Key | str) -> AT:
         self._value.remove(key)
 
         if isinstance(key, Key):
@@ -1488,7 +1466,7 @@ class AbstractTable(Item, _CustomDict):
 
         return self
 
-    def setdefault(self, key: Union[Key, str], default: Any) -> Any:
+    def setdefault(self, key: Key | str, default: Any) -> Any:
         super().setdefault(key, default)
         return self[key]
 
@@ -1507,13 +1485,13 @@ class AbstractTable(Item, _CustomDict):
     def __len__(self) -> int:
         return len(self._value)
 
-    def __delitem__(self, key: Union[Key, str]) -> None:
+    def __delitem__(self, key: Key | str) -> None:
         self.remove(key)
 
-    def __getitem__(self, key: Union[Key, str]) -> Item:
+    def __getitem__(self, key: Key | str) -> Item:
         return cast(Item, self._value[key])
 
-    def __setitem__(self, key: Union[Key, str], value: Any) -> None:
+    def __setitem__(self, key: Key | str, value: Any) -> None:
         if not isinstance(value, Item):
             value = item(value, _parent=self)
 
@@ -1546,12 +1524,12 @@ class Table(AbstractTable):
 
     def __init__(
         self,
-        value: "container.Container",
+        value: container.Container,
         trivia: Trivia,
         is_aot_element: bool,
-        is_super_table: Optional[bool] = None,
-        name: Optional[str] = None,
-        display_name: Optional[str] = None,
+        is_super_table: bool | None = None,
+        name: str | None = None,
+        display_name: str | None = None,
     ) -> None:
         super().__init__(value, trivia)
 
@@ -1564,7 +1542,7 @@ class Table(AbstractTable):
     def discriminant(self) -> int:
         return 9
 
-    def __copy__(self) -> "Table":
+    def __copy__(self) -> Table:
         return type(self)(
             self._value.copy(),
             self._trivia.copy(),
@@ -1574,7 +1552,7 @@ class Table(AbstractTable):
             self.display_name,
         )
 
-    def append(self, key: Union[Key, str, None], _item: Any) -> "Table":
+    def append(self, key: Key | str | None, _item: Any) -> Table:
         """
         Appends a (key, item) to the table.
         """
@@ -1605,7 +1583,7 @@ class Table(AbstractTable):
 
         return self
 
-    def raw_append(self, key: Union[Key, str, None], _item: Any) -> "Table":
+    def raw_append(self, key: Key | str | None, _item: Any) -> Table:
         """Similar to :meth:`append` but does not copy indentation."""
         if not isinstance(_item, Item):
             _item = item(_item)
@@ -1641,7 +1619,7 @@ class Table(AbstractTable):
 
     # Helpers
 
-    def indent(self, indent: int) -> "Table":
+    def indent(self, indent: int) -> Table:
         """Indent the table with given number of spaces."""
         super().indent(indent)
 
@@ -1681,7 +1659,7 @@ class InlineTable(AbstractTable):
     """
 
     def __init__(
-        self, value: "container.Container", trivia: Trivia, new: bool = False
+        self, value: container.Container, trivia: Trivia, new: bool = False
     ) -> None:
         super().__init__(value, trivia)
 
@@ -1691,7 +1669,7 @@ class InlineTable(AbstractTable):
     def discriminant(self) -> int:
         return 10
 
-    def append(self, key: Union[Key, str, None], _item: Any) -> "InlineTable":
+    def append(self, key: Key | str | None, _item: Any) -> InlineTable:
         """
         Appends a (key, item) to the table.
         """
@@ -1755,12 +1733,12 @@ class InlineTable(AbstractTable):
 
         return buf
 
-    def __setitem__(self, key: Union[Key, str], value: Any) -> None:
+    def __setitem__(self, key: Key | str, value: Any) -> None:
         if hasattr(value, "trivia") and value.trivia.comment:
             value.trivia.comment = ""
         super().__setitem__(key, value)
 
-    def __copy__(self) -> "InlineTable":
+    def __copy__(self) -> InlineTable:
         return type(self)(self._value.copy(), self._trivia.copy(), self._new)
 
     def _getstate(self, protocol: int = 3) -> tuple:
@@ -1803,14 +1781,14 @@ class String(str, Item):
 
         return self._new(result, original)
 
-    def _new(self, result: str, original: str) -> "String":
+    def _new(self, result: str, original: str) -> String:
         return String(self._t, result, original, self._trivia)
 
     def _getstate(self, protocol=3):
         return self._t, str(self), self._original, self._trivia
 
     @classmethod
-    def from_raw(cls, value: str, type_=StringType.SLB, escape=True) -> "String":
+    def from_raw(cls, value: str, type_=StringType.SLB, escape=True) -> String:
         value = decode(value)
 
         invalid = type_.invalid_sequences
@@ -1829,10 +1807,10 @@ class AoT(Item, _CustomList):
     """
 
     def __init__(
-        self, body: List[Table], name: Optional[str] = None, parsed: bool = False
+        self, body: list[Table], name: str | None = None, parsed: bool = False
     ) -> None:
         self.name = name
-        self._body: List[Table] = []
+        self._body: list[Table] = []
         self._parsed = parsed
 
         super().__init__(Trivia(trail=""))
@@ -1840,7 +1818,7 @@ class AoT(Item, _CustomList):
         for table in body:
             self.append(table)
 
-    def unwrap(self) -> List[Dict[str, Any]]:
+    def unwrap(self) -> list[dict[str, Any]]:
         unwrapped = []
         for t in self._body:
             if hasattr(t, "unwrap"):
@@ -1850,7 +1828,7 @@ class AoT(Item, _CustomList):
         return unwrapped
 
     @property
-    def body(self) -> List[Table]:
+    def body(self) -> list[Table]:
         return self._body
 
     @property
@@ -1858,14 +1836,14 @@ class AoT(Item, _CustomList):
         return 12
 
     @property
-    def value(self) -> List[Dict[Any, Any]]:
+    def value(self) -> list[dict[Any, Any]]:
         return [v.value for v in self._body]
 
     def __len__(self) -> int:
         return len(self._body)
 
     @overload
-    def __getitem__(self, key: slice) -> List[Table]:
+    def __getitem__(self, key: slice) -> list[Table]:
         ...
 
     @overload
@@ -1875,10 +1853,10 @@ class AoT(Item, _CustomList):
     def __getitem__(self, key):
         return self._body[key]
 
-    def __setitem__(self, key: Union[slice, int], value: Any) -> None:
+    def __setitem__(self, key: slice | int, value: Any) -> None:
         raise NotImplementedError
 
-    def __delitem__(self, key: Union[slice, int]) -> None:
+    def __delitem__(self, key: slice | int) -> None:
         del self._body[key]
         list.__delitem__(self, key)
 
diff --git a/tomlkit/parser.py b/tomlkit/parser.py
index 097aceb..6a2ee51 100644
--- a/tomlkit/parser.py
+++ b/tomlkit/parser.py
@@ -1,13 +1,9 @@
+from __future__ import annotations
+
 import datetime
 import re
 import string
 
-from typing import List
-from typing import Optional
-from typing import Tuple
-from typing import Type
-from typing import Union
-
 from tomlkit._compat import decode
 from tomlkit._utils import RFC_3339_LOOSE
 from tomlkit._utils import _escaped
@@ -68,7 +64,7 @@ class Parser:
         # Input to parse
         self._src = Source(decode(string))
 
-        self._aot_stack: List[Key] = []
+        self._aot_stack: list[Key] = []
 
     @property
     def _state(self):
@@ -92,14 +88,14 @@ class Parser:
         """
         return self._src.extract()
 
-    def inc(self, exception: Optional[Type[ParseError]] = None) -> bool:
+    def inc(self, exception: type[ParseError] | None = None) -> bool:
         """
         Increments the parser if the end of the input has not been reached.
         Returns whether or not it was able to advance.
         """
         return self._src.inc(exception=exception)
 
-    def inc_n(self, n: int, exception: Optional[Type[ParseError]] = None) -> bool:
+    def inc_n(self, n: int, exception: type[ParseError] | None = None) -> bool:
         """
         Increments the parser by n characters
         if the end of the input has not been reached.
@@ -205,7 +201,7 @@ class Parser:
 
         return parent_parts == child_parts[: len(parent_parts)]
 
-    def _parse_item(self) -> Optional[Tuple[Optional[Key], Item]]:
+    def _parse_item(self) -> tuple[Key | None, Item] | None:
         """
         Attempts to parse the next item and returns it, along with its key
         if the item is value-like.
@@ -241,7 +237,7 @@ class Parser:
 
         return self._parse_key_value(True)
 
-    def _parse_comment_trail(self, parse_trail: bool = True) -> Tuple[str, str, str]:
+    def _parse_comment_trail(self, parse_trail: bool = True) -> tuple[str, str, str]:
         """
         Returns (comment_ws, comment, trail)
         If there is no comment, comment_ws and comment will
@@ -302,7 +298,7 @@ class Parser:
 
         return comment_ws, comment, trail
 
-    def _parse_key_value(self, parse_comment: bool = False) -> Tuple[Key, Item]:
+    def _parse_key_value(self, parse_comment: bool = False) -> tuple[Key, Item]:
         # Leading indent
         self.mark()
 
@@ -564,7 +560,7 @@ class Parser:
         # Consume opening bracket, EOF here is an issue (middle of array)
         self.inc(exception=UnexpectedEofError)
 
-        elems: List[Item] = []
+        elems: list[Item] = []
         prev_value = None
         while True:
             # consume whitespace
@@ -671,7 +667,7 @@ class Parser:
 
         return InlineTable(elems, Trivia())
 
-    def _parse_number(self, raw: str, trivia: Trivia) -> Optional[Item]:
+    def _parse_number(self, raw: str, trivia: Trivia) -> Item | None:
         # Leading zeros are not allowed
         sign = ""
         if raw.startswith(("+", "-")):
@@ -881,8 +877,8 @@ class Parser:
                 self.inc(exception=UnexpectedEofError)
 
     def _parse_table(
-        self, parent_name: Optional[Key] = None, parent: Optional[Table] = None
-    ) -> Tuple[Key, Union[Table, AoT]]:
+        self, parent_name: Key | None = None, parent: Table | None = None
+    ) -> tuple[Key, Table | AoT]:
         """
         Parses a table element.
         """
@@ -1035,7 +1031,7 @@ class Parser:
 
         return key, result
 
-    def _peek_table(self) -> Tuple[bool, Key]:
+    def _peek_table(self) -> tuple[bool, Key]:
         """
         Peeks ahead non-intrusively by cloning then restoring the
         initial state of the parser.
@@ -1099,7 +1095,7 @@ class Parser:
                 break
             return buf
 
-    def _peek_unicode(self, is_long: bool) -> Tuple[Optional[str], Optional[str]]:
+    def _peek_unicode(self, is_long: bool) -> tuple[str | None, str | None]:
         """
         Peeks ahead non-intrusively by cloning then restoring the
         initial state of the parser.
diff --git a/tomlkit/source.py b/tomlkit/source.py
index d1a53cd..ab29854 100644
--- a/tomlkit/source.py
+++ b/tomlkit/source.py
@@ -1,8 +1,7 @@
+from __future__ import annotations
+
 from copy import copy
 from typing import Any
-from typing import Optional
-from typing import Tuple
-from typing import Type
 
 from tomlkit.exceptions import ParseError
 from tomlkit.exceptions import UnexpectedCharError
@@ -12,15 +11,15 @@ from tomlkit.toml_char import TOMLChar
 class _State:
     def __init__(
         self,
-        source: "Source",
-        save_marker: Optional[bool] = False,
-        restore: Optional[bool] = False,
+        source: Source,
+        save_marker: bool | None = False,
+        restore: bool | None = False,
     ) -> None:
         self._source = source
         self._save_marker = save_marker
         self.restore = restore
 
-    def __enter__(self) -> "_State":
+    def __enter__(self) -> _State:
         # Entering this context manager - save the state
         self._chars = copy(self._source._chars)
         self._idx = self._source._idx
@@ -44,7 +43,7 @@ class _StateHandler:
     State preserver for the Parser.
     """
 
-    def __init__(self, source: "Source") -> None:
+    def __init__(self, source: Source) -> None:
         self._source = source
         self._states = []
 
@@ -107,7 +106,7 @@ class Source(str):
         """
         return self[self._marker : self._idx]
 
-    def inc(self, exception: Optional[Type[ParseError]] = None) -> bool:
+    def inc(self, exception: type[ParseError] | None = None) -> bool:
         """
         Increments the parser if the end of the input has not been reached.
         Returns whether or not it was able to advance.
@@ -124,7 +123,7 @@ class Source(str):
 
             return False
 
-    def inc_n(self, n: int, exception: Optional[Type[ParseError]] = None) -> bool:
+    def inc_n(self, n: int, exception: type[ParseError] | None = None) -> bool:
         """
         Increments the parser by n characters
         if the end of the input has not been reached.
@@ -159,7 +158,7 @@ class Source(str):
 
     def parse_error(
         self,
-        exception: Type[ParseError] = ParseError,
+        exception: type[ParseError] = ParseError,
         *args: Any,
         **kwargs: Any,
     ) -> ParseError:
@@ -170,7 +169,7 @@ class Source(str):
 
         return exception(line, col, *args, **kwargs)
 
-    def _to_linecol(self) -> Tuple[int, int]:
+    def _to_linecol(self) -> tuple[int, int]:
         cur = 0
         for i, line in enumerate(self.splitlines()):
             if cur + len(line) + 1 > self.idx:


Test patch:
diff --git a/tests/test_toml_document.py b/tests/test_toml_document.py
index b5a6f62..6f26e20 100644
--- a/tests/test_toml_document.py
+++ b/tests/test_toml_document.py
@@ -1030,3 +1030,32 @@ def test_nested_table_update_display_name():
     z = 3
     """
     assert doc.as_string() == dedent(expected)
+
+
+def test_build_table_with_dotted_key():
+    doc = tomlkit.document()
+    data = {
+        "a.b.c": 1,
+        "a.b.d": 2,
+        "a": {"c": {"foo": "bar"}},
+        "a.d.e": 3,
+    }
+
+    for key, value in data.items():
+        if "." not in key:
+            doc.append(key, value)
+        else:
+            doc.append(tomlkit.key(key.split(".")), value)
+
+    expected = """\
+a.b.c = 1
+a.b.d = 2
+a.d.e = 3
+
+[a.c]
+foo = "bar"
+"""
+    assert doc.as_string() == expected
+    assert json.loads(json.dumps(doc)) == {
+        "a": {"b": {"c": 1, "d": 2}, "d": {"e": 3}, "c": {"foo": "bar"}}
+    }