Skip to content

Commit

Permalink
Implement __contains__ and __delitem__. (partially) fixes #4
Browse files Browse the repository at this point in the history
  • Loading branch information
elpekenin committed Dec 21, 2023
1 parent 4b0b5cb commit fd16879
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 31 deletions.
96 changes: 89 additions & 7 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,34 @@ def assertEqual(a, b):
raise TestError(f"{a} doesn't match {b}")


class ExceptionManager:
def __init__(self, *exceptions, msg):
def assertTrue(a):
assertEqual(a, True)


def assertFalse(a):
assertEqual(a, False)


def assertIn(a, b):
if a not in b:
raise TestError(f"{a} is not contained in {b}")


def assertNotIn(a, b):
if a in b:
raise TestError(f"{a} is contained in {b}")


class AssertRaises:
def __init__(
self,
*exceptions: list[Exception],
msg: Optional[str] = None
):
self.exc = exceptions
self.msg = msg

def __enter__(self) -> "ExceptionManager":
def __enter__(self) -> "AssertRaises":
return self

def __exit__(self, exc_t: type, exc_v: Exception, _exc_tb):
Expand Down Expand Up @@ -76,7 +98,7 @@ def run(self):
else None
)

with ExceptionManager(expected_exc, msg=self.message):
with AssertRaises(expected_exc, msg=self.message):
assertEqual(toml.loads(self.input), Dotty(self.output))


Expand Down Expand Up @@ -174,8 +196,8 @@ def run(self):
assertEqual(toml.loads(toml.dumps(data)), Dotty(data))


# https://github.com/elpekenin/circuitpython_toml/issues/3
# Lets check that empty dicts dont break things
# https://github.com/elpekenin/circuitpython_toml/issues/3
print("dumping empty dict")
toml.dumps({"y": {}})

Expand All @@ -194,10 +216,70 @@ def run(self):


print("wrong type")
with ExceptionManager(toml.TOMLError, msg="Not a file?"):
with AssertRaises(toml.TOMLError, msg="Not a file?"):
toml.load(42)

print("wrong mode")
with ExceptionManager(toml.TOMLError, msg="File open in wrong mode?"):
with AssertRaises(toml.TOMLError, msg="File open in wrong mode?"):
with open(TEST_FILE, "a") as f:
toml.load(f)

# Lets check manually implemented dunders
# https://github.com/elpekenin/circuitpython_toml/issues/4
print("__contains__")
data = toml.load(TEST_FILE)
assertTrue("foo" in data)
assertFalse("__wrong__" in data)

print("__delitem__")
# ======
# case 1
# ======
# deleting a table causes its child to be deleted too
data = Dotty(
{
"nested": {
"foo": {
"bar": {
"baz": {
"value": 42
}
}
}
}
},
fill_tables=True
)
del data["nested.foo"]

# target table deleted
assertNotIn("nested.foo", data)
assertNotIn("nested.foo.bar", data.tables)
# child table deleted
assertNotIn("nested.foo.bar.baz", data)
assertNotIn("nested.foo.bar.baz", data.tables)
# child item deleted
assertNotIn("nested.foo.bar.baz.value", data)
with AssertRaises(KeyError):
data["nested.foo.bar.bar.value"]

# ======
# case 2
# ======
# deleting the only element on a table deletes the table itself
data = Dotty(
{
"nested": {
"value": 42
}
},
fill_tables=True
)
del data["nested.value"]

# --- item deleted
with AssertRaises(KeyError):
data["nested.value"]

# --- would-be-empty table deleted
assertNotIn("nested", data.tables)
2 changes: 1 addition & 1 deletion toml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__author__ = "elpekenin"
__version__ = (0, 1, 5)
__version__ = (0, 1, 6)

from ._toml import *
101 changes: 78 additions & 23 deletions toml/_dotty.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

try:
# types are needed on compyter
from typing import Any, Optional
from typing import Optional
except ImportError:
pass

Expand Down Expand Up @@ -35,7 +35,7 @@ def __init__(self, __data: Optional[dict] = None, *, fill_tables: bool = False):
self.tables.add(self._BASE)

if fill_tables:
def _fill(key: str, value: Any) -> None:
def _fill(key: str, value: object) -> None:
"""Helper to iterate nested dicts"""

if isinstance(value, dict):
Expand Down Expand Up @@ -64,7 +64,8 @@ def __repr__(self):

@staticmethod
def split(key: str) -> tuple[list[str], str]:
"""Splits a key into last element and rest of them.
"""
Splits a key into last element and rest of them.
>>> split("foo")
>>> [], "foo"
Expand All @@ -76,55 +77,72 @@ def split(key: str) -> tuple[list[str], str]:
>>> ["foo", "bar"], "baz"
"""

# dont try to split non-str keys
if not isinstance(key, str):
return [], key

parts = key.split(".")
return parts[:-1], parts[-1]

def __getitem__(self, key: str) -> Any:
def __getitem__(self, __key: object) -> object:
"""Syntactic sugar to get a nested item."""

# special case, return base dict
if key == self._BASE:
if __key == self._BASE:
return self._data

keys, last = self.split(key)
keys, last = self.split(__key)

item = self._data
table = self._data
for k in keys:
item = item[k]
table = table[k]

return item[last]
return table[last]

def _get_or_create(self, item: dict, k: str, global_key: str) -> dict:
"""Helper function that creates the nested dict if not present."""

if k not in item:
# Add to tables v get rid of heading dot
self.tables.add(global_key[1:])
if k not in item and isinstance(item, dict):
# Add to tables v get rid of heading dot(s)
self.tables.add(global_key.lstrip("."))

item[k] = {}

return item[k]

def __setitem__(self, key: str, value: Any):
"""Syntactic sugar to set a nested item."""
def __setitem__(self, __key: str, __value: object):
"""
Syntactic sugar to set a nested item.
Known limitation, setting dicts doesn't update `self.tables`.
ie, expect issues with code like:
>>> dotty["foo"] = {"bar": baz}
"""

keys, last = self.split(key)
keys, last = self.split(__key)
global_key = ""

item = self._data
table = self._data
for k in keys:
global_key += "." + k
item = self._get_or_create(item, k, global_key)
table = self._get_or_create(table, k, global_key)

item[last] = value
table[last] = __value

def __getattr__(self, key: str) -> Any:
"""Redirect some methods to dict's builtin ones. Perhaps not too useful."""
# === Main logic of Dotty ends here ===
# Below this point, it's mainly convenience for some operator/builtins

if hasattr(self._data, key):
return getattr(self._data, key)
def __getattr__(self, __key: str) -> object:
"""
Redirect some methods to dict's builtin ones. Perhaps not too useful.
Apparently not too useful on CP
> https://github.com/elpekenin/circuitpython_toml/issues/4
"""
if hasattr(self._data, __key):
return getattr(self._data, __key)

raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{key}'")
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{__key}'")

def __eq__(self, __value: object) -> bool:
klass = self.__class__
Expand All @@ -135,3 +153,40 @@ def __eq__(self, __value: object) -> bool:
)

return self._data == __value._data

def __contains__(self, __key: object) -> bool:
try:
self.__getitem__(__key)
return True
except KeyError:
return False

def __delitem__(self, __key: object):
keys, last = self.split(__key)

parent_table = None
table = self._data
for k in keys:
parent_table = table
table = table[k]

# remove item from its table
del table[last]

# if table is empty after that, remove it too
if len(table) == 0:
if parent_table:
parent_table.pop(keys[-1])

self.tables.remove(".".join(keys))

# if key was a table itself, remove it (and children) from set
if __key in self.tables:
self.tables.remove(__key)

for table in self.tables.copy():
if (
table != self._BASE
and table.startswith(f"{__key}.")
):
self.tables.remove(table)

0 comments on commit fd16879

Please sign in to comment.