From 9e56735f2273c2341399932b76c0d717059db61b Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Mon, 15 Nov 2021 12:32:51 +0100 Subject: [PATCH] Error when dotted keys define values outside current table (#125) * Add failing test cases * Add implementation plan * Implementation * Fix performance regression * Add changelog --- CHANGELOG.md | 5 +++ .../dotted-keys/extend-defined-aot.toml | 3 ++ .../extend-defined-table-with-subtable.toml | 4 ++ .../dotted-keys/extend-defined-table.toml | 4 ++ tests/test_flags.py | 13 ------- tomli/_parser.py | 37 +++++++++++-------- 6 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 tests/data/extras/invalid/dotted-keys/extend-defined-aot.toml create mode 100644 tests/data/extras/invalid/dotted-keys/extend-defined-table-with-subtable.toml create mode 100644 tests/data/extras/invalid/dotted-keys/extend-defined-table.toml delete mode 100644 tests/test_flags.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 583e52d..37dcebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - Removed - Python 3.6 support - Support for text file objects as `load` input. Use binary file objects instead. +- Improved + - Raise an error when dotted keys define values outside the "current table". + Technically speaking TOML v1.0.0 does allow such assignments + but that isn't intended by specification writers, + and will change in a future specification version (see the [pull request](https://github.com/toml-lang/toml/pull/848)). ## 1.2.2 diff --git a/tests/data/extras/invalid/dotted-keys/extend-defined-aot.toml b/tests/data/extras/invalid/dotted-keys/extend-defined-aot.toml new file mode 100644 index 0000000..1c3c34b --- /dev/null +++ b/tests/data/extras/invalid/dotted-keys/extend-defined-aot.toml @@ -0,0 +1,3 @@ +[[tab.arr]] +[tab] +arr.val1=1 diff --git a/tests/data/extras/invalid/dotted-keys/extend-defined-table-with-subtable.toml b/tests/data/extras/invalid/dotted-keys/extend-defined-table-with-subtable.toml new file mode 100644 index 0000000..70e2ac5 --- /dev/null +++ b/tests/data/extras/invalid/dotted-keys/extend-defined-table-with-subtable.toml @@ -0,0 +1,4 @@ +[a.b.c.d] + z = 9 +[a] + b.c.d.k.t = 8 diff --git a/tests/data/extras/invalid/dotted-keys/extend-defined-table.toml b/tests/data/extras/invalid/dotted-keys/extend-defined-table.toml new file mode 100644 index 0000000..c88c179 --- /dev/null +++ b/tests/data/extras/invalid/dotted-keys/extend-defined-table.toml @@ -0,0 +1,4 @@ +[a.b.c] + z = 9 +[a] + b.c.t = 9 diff --git a/tests/test_flags.py b/tests/test_flags.py deleted file mode 100644 index 64194d9..0000000 --- a/tests/test_flags.py +++ /dev/null @@ -1,13 +0,0 @@ -from tomli._parser import Flags - - -def test_set_for_relative_key(): - flags = Flags() - head_key = ("a", "b") - rel_key = ("c", "d") - flags.set_for_relative_key(head_key, rel_key, Flags.EXPLICIT_NEST) - assert not flags.is_(("a",), flags.EXPLICIT_NEST) - assert not flags.is_(("a", "b"), flags.EXPLICIT_NEST) - assert flags.is_(("a", "b", "c"), flags.EXPLICIT_NEST) - assert flags.is_(("a", "b", "c", "d"), flags.EXPLICIT_NEST) - assert not flags.is_(("a", "b", "c", "d", "e"), flags.EXPLICIT_NEST) diff --git a/tomli/_parser.py b/tomli/_parser.py index ec7c749..0cf1b23 100644 --- a/tomli/_parser.py +++ b/tomli/_parser.py @@ -95,6 +95,7 @@ def loads(s: str, *, parse_float: ParseFloat = float) -> dict[str, Any]: # noqa second_char: str | None = src[pos + 1] except IndexError: second_char = None + out.flags.finalize_pending() if second_char == "[": pos, header = create_list_rule(src, pos, out) else: @@ -131,6 +132,15 @@ class Flags: def __init__(self) -> None: self._flags: dict[str, dict] = {} + self._pending_flags: set[tuple[Key, int]] = set() + + def add_pending(self, key: Key, flag: int) -> None: + self._pending_flags.add((key, flag)) + + def finalize_pending(self) -> None: + for key, flag in self._pending_flags: + self.set(key, flag, recursive=False) + self._pending_flags.clear() def unset_all(self, key: Key) -> None: cont = self._flags @@ -140,19 +150,6 @@ def unset_all(self, key: Key) -> None: cont = cont[k]["nested"] cont.pop(key[-1], None) - def set_for_relative_key(self, head_key: Key, rel_key: Key, flag: int) -> None: - cont = self._flags - for k in head_key: - if k not in cont: - cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}} - cont = cont[k]["nested"] - for k in rel_key: - if k in cont: - cont[k]["flags"].add(flag) - else: - cont[k] = {"flags": {flag}, "recursive_flags": set(), "nested": {}} - cont = cont[k]["nested"] - def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003 cont = self._flags key_parent, key_stem = key[:-1], key[-1] @@ -320,12 +317,20 @@ def key_value_rule( key_parent, key_stem = key[:-1], key[-1] abs_key_parent = header + key_parent + relative_path_cont_keys = (header + key[:i] for i in range(1, len(key))) + for cont_key in relative_path_cont_keys: + # Check that dotted key syntax does not redefine an existing table + if out.flags.is_(cont_key, Flags.EXPLICIT_NEST): + raise suffixed_err(src, pos, f"Cannot redefine namespace {cont_key}") + # Containers in the relative path can't be opened with the table syntax or + # dotted key/value syntax in following table sections. + out.flags.add_pending(cont_key, Flags.EXPLICIT_NEST) + if out.flags.is_(abs_key_parent, Flags.FROZEN): raise suffixed_err( - src, pos, f"Can not mutate immutable namespace {abs_key_parent}" + src, pos, f"Cannot mutate immutable namespace {abs_key_parent}" ) - # Containers in the relative path can't be opened with the table syntax after this - out.flags.set_for_relative_key(header, key, Flags.EXPLICIT_NEST) + try: nest = out.data.get_or_create_nest(abs_key_parent) except KeyError: