Skip to content

Commit

Permalink
Error when dotted keys define values outside current table (#125)
Browse files Browse the repository at this point in the history
* Add failing test cases

* Add implementation plan

* Implementation

* Fix performance regression

* Add changelog
  • Loading branch information
hukkin committed Nov 15, 2021
1 parent 486c156 commit 9e56735
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 29 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions tests/data/extras/invalid/dotted-keys/extend-defined-aot.toml
@@ -0,0 +1,3 @@
[[tab.arr]]
[tab]
arr.val1=1
@@ -0,0 +1,4 @@
[a.b.c.d]
z = 9
[a]
b.c.d.k.t = 8
@@ -0,0 +1,4 @@
[a.b.c]
z = 9
[a]
b.c.t = 9
13 changes: 0 additions & 13 deletions tests/test_flags.py

This file was deleted.

37 changes: 21 additions & 16 deletions tomli/_parser.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 9e56735

Please sign in to comment.