Skip to content

Commit

Permalink
Reimplement init behavior with merge (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
okomestudio committed Apr 29, 2020
1 parent 150acce commit ef5955c
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 19 deletions.
75 changes: 56 additions & 19 deletions src/resconfig/ondict.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class ONDict(OrderedDict):
_create = None

def __init__(self, *args, **kwargs):
args, kwargs = _expand_args(args, kwargs)
super().__init__(*args, **kwargs)
super().__init__()
self.merge(*args, **kwargs)

def __repr__(self):
items = []
Expand Down Expand Up @@ -78,14 +78,37 @@ def setdefault(self, key, default=None):
return (super() if ref is self else ref).setdefault(lastkey, default)

def update(self, *args, **kwargs):
args, kwargs = _expand_args(args, kwargs)
"""Update from dict and/or iterable.
This method takes in the same argument(s) as :meth:`dict.update`. Compared to
the built-in :class:`dict` object, the update behavior is expanded to allow
nested key notation.
Note that update happens only on the top-level keys, just like built-in
:class:`dict`, to supply consistent behavior. If you desire a full merging
behavior, use :meth:`ONDict.merge`.
Raises:
TypeError: When more than one positional argument is supplied.
ValueError: When the iterable does not hold two-element items.
"""
if args:
if len(args) != 1:
raise TypeError(f"update expected at most 1 argument, got {len(args)}")
for k, v in args[0].items():
self[k] = v
arg = args[0]
if hasattr(arg, "keys"):
super().update(normalize(arg))
else:
try:
for k, v in arg:
super().update(normalize({k: v}))
except Exception:
raise ValueError(
"dictionary update sequence element #0 has length "
f"{ len(arg[0]) }; 2 is required"
)
for k in kwargs:
self[k] = kwargs[k]
super().update(normalize({k: kwargs[k]}))

@classmethod
def fromkeys(cls, iterable, value=None):
Expand All @@ -110,21 +133,35 @@ def allkeys(self, as_str=False):
for key in self.__allkeys(("__ROOT__",), {"__ROOT__": self}):
yield ".".join(key) if as_str else key

def merge(self, d):
merge(self, d)
def merge(self, *args, **kwargs):
"""Merge from dict and/or iterable.
This method takes in the same argument(s) as :meth:`dict.update`, but merge the
input instead of :class:`dict`-like update. Merging extends the update behavior
to allow nested updates and to support nested key notation.
def _expand_args(args, kwargs):
if args:
arg = args[0]
if hasattr(arg, "keys"):
new = normalize(arg)
else:
new = ONDict()
for key, val in arg:
new = merge(new, normalize({key: val}))
args = [new] + list(args[1:])
return args, normalize(kwargs)
Raises:
TypeError: When more than one positional argument is supplied.
ValueError: When the iterable does not hold two-element items.
"""
if args:
if len(args) != 1:
raise TypeError(f"update expected at most 1 argument, got {len(args)}")
arg = args[0]
if hasattr(arg, "keys"):
for k, v in arg.items():
merge(self, normalize({k: v}))
else:
try:
for k, v in arg:
merge(self, normalize({k: v}))
except Exception:
raise ValueError(
"dictionary update sequence element #0 has length "
f"{ len(arg[0]) }; 2 is required"
)
for k in kwargs:
merge(self, normalize({k: kwargs[k]}))


def _key_error(obj, key):
Expand Down
48 changes: 48 additions & 0 deletions tests/resconfig/test_ondict.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ class TestONDict:
def d(self):
yield ONDict(deepcopy(self.default))

def test_init_default(self):
d = ONDict()
assert d == {}

def test_init_mapping(self):
d = ONDict(self.default)
assert d == self.default

def test_init_iterable(self):
d = ONDict(((k, v) for k, v in self.default.items()))
assert d == self.default

def test_init_kwargs(self):
d = ONDict(**self.default)
assert d == self.default

@pytest.mark.parametrize(
"key, expected",
[
Expand Down Expand Up @@ -164,6 +180,7 @@ def test_setdefault_error(self, d, key, expected):
],
)
def test_update(self, d, args, kwargs, expected):
# d = {"foo": {"bar": {"baz": 0}, "qux": "quux"}}
d.update(*args, **kwargs)
assert d == expected

Expand Down Expand Up @@ -195,6 +212,37 @@ def test_allkeys(self, d):
assert list(d.allkeys()) == [("foo", "bar", "baz"), ("foo", "qux")]
assert list(d.allkeys(as_str=True)) == ["foo.bar.baz", "foo.qux"]

@pytest.mark.parametrize(
"args, kwargs, expected",
[
(({"bar": 3},), {}, {**default, **{"bar": 3}}),
(({"foo": 1, "bar.baz": 3},), {}, {"foo": 1, "bar": {"baz": 3}}),
(
({"foo.baz.bar": 1},),
{},
{"foo": {"bar": {"baz": 0}, "qux": "quux", "baz": {"bar": 1}}},
),
([(("bar", 3),)], {}, {**default, **{"bar": 3}}),
([(("foo", 1), ("bar.baz", 3))], {}, {"foo": 1, "bar": {"baz": 3}}),
(
[(("foo.baz.bar", 1),)],
{},
{"foo": {"bar": {"baz": 0}, "qux": "quux", "baz": {"bar": 1}}},
),
([], {"bar": 3}, {**default, **{"bar": 3}}),
([], {"foo": 1, "bar.baz": 3}, {"foo": 1, "bar": {"baz": 3}}),
(
[],
{"foo.baz.bar": 1},
{"foo": {"bar": {"baz": 0}, "qux": "quux", "baz": {"bar": 1}}},
),
],
)
def test_merge(self, d, args, kwargs, expected):
# d = {"foo": {"bar": {"baz": 0}, "qux": "quux"}}
d.merge(*args, **kwargs)
assert d == expected


class TestMerge:
@pytest.mark.parametrize(
Expand Down

0 comments on commit ef5955c

Please sign in to comment.