From b9398b7f55cc223741dd0f8f3b01de93076b4c86 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Sun, 7 Jul 2019 09:16:11 +0200 Subject: [PATCH 01/17] :see_no_evil: Ignore python virtual environment directory. --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index af1b6b7..10b2523 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,14 @@ __pycache__/* *.pyc +# Virtual Environment +env/* + # Packaging build/* dist/* scalpl.egg-info/* - # Mypy .mypy_cache/* From 7a61f1236cf6796625d1c3a046c29d9851509ece Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Sun, 7 Jul 2019 10:47:35 +0200 Subject: [PATCH 02/17] :label: Improve type annotations. --- scalpl.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/scalpl.py b/scalpl.py index 72b4a5c..ef3c38e 100644 --- a/scalpl.py +++ b/scalpl.py @@ -1,8 +1,21 @@ """ A lightweight wrapper to operate on nested dictionaries seamlessly. """ -from typing import Any, Optional from itertools import chain +from typing import ( + Any, + ItemsView, + Iterable, + Iterator, + KeysView, + Optional, + Type, + TypeVar, + ValuesView, +) + + +TLightCut = TypeVar("TLightCut", bound="LightCut") class LightCut: @@ -57,7 +70,7 @@ def __getitem__(self, key: str) -> Any: except IndexError: raise IndexError('index out of range in key "' + key + '".') - def __iter__(self): + def __iter__(self) -> Iterator: return iter(self.data) def __len__(self) -> int: @@ -85,7 +98,7 @@ def _traverse(self, parent, key: str): parent = parent[key] return parent, last_key - def all(self, key: str): + def all(self: TLightCut, key: str) -> Iterator[TLightCut]: """Wrap each item of an Iterable.""" items = self[key] cls = self.__class__ @@ -98,19 +111,21 @@ def copy(self) -> dict: return self.data.copy() @classmethod - def fromkeys(cls, seq, value=None): + def fromkeys( + cls: Type[TLightCut], seq: Iterable, value: Optional[Iterable] = None + ) -> TLightCut: return cls(dict.fromkeys(seq, value)) - def get(self, key: str, default: Any = None) -> Any: + def get(self, key: str, default: Optional = None) -> Any: try: return self[key] except (KeyError, IndexError): return default - def keys(self): + def keys(self) -> KeysView: return self.data.keys() - def items(self): + def items(self) -> ItemsView: return self.data.items() def pop(self, key: str, default: Any = None) -> Any: @@ -141,7 +156,7 @@ def update(self, data=None, **kwargs): for key, value in pairs: self.__setitem__(key, value) - def values(self): + def values(self) -> ValuesView: return self.data.values() @@ -161,7 +176,7 @@ class Cut(LightCut): __slots__ = () - def setdefault(self, key: str, default: Any = None) -> Any: + def setdefault(self, key: str, default: Optional = None) -> Any: try: parent = self.data *parent_keys, last_key = key.split(self.sep) @@ -171,7 +186,7 @@ def setdefault(self, key: str, default: Any = None) -> Any: try: parent = parent[_key] except KeyError: - child = {} + child: dict = {} parent[_key] = child parent = child parent, last_key = self._traverse_list(parent, last_key) From 7f189aa7a6a835180230f0d214da48d2aa6ddc78 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Sun, 7 Jul 2019 10:58:30 +0200 Subject: [PATCH 03/17] :truck: Move the scalpl module into its own package. --- scalpl/__init__.py | 1 + scalpl.py => scalpl/scalpl.py | 0 2 files changed, 1 insertion(+) create mode 100644 scalpl/__init__.py rename scalpl.py => scalpl/scalpl.py (100%) diff --git a/scalpl/__init__.py b/scalpl/__init__.py new file mode 100644 index 0000000..3cc5a70 --- /dev/null +++ b/scalpl/__init__.py @@ -0,0 +1 @@ +from .scalpl import LightCut, Cut diff --git a/scalpl.py b/scalpl/scalpl.py similarity index 100% rename from scalpl.py rename to scalpl/scalpl.py From 3a5a935ef754bc4aa5a493219de0b856c4c68e3a Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Sun, 7 Jul 2019 10:59:30 +0200 Subject: [PATCH 04/17] :truck: Create an assets directory for the logo. --- README.rst | 2 +- scalpl.png => assets/scalpl.png | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename scalpl.png => assets/scalpl.png (100%) diff --git a/README.rst b/README.rst index 34c7221..50855f9 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://raw.githubusercontent.com/ducdetronquito/scalpl/master/scalpl.png +.. image:: https://raw.githubusercontent.com/ducdetronquito/scalpl/master/assets/scalpl.png :target: https://github.com/ducdetronquito/scalpl Scalpl diff --git a/scalpl.png b/assets/scalpl.png similarity index 100% rename from scalpl.png rename to assets/scalpl.png From ab0a0564f4a2404fc1c817f8df9663d503849148 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Sun, 7 Jul 2019 10:59:50 +0200 Subject: [PATCH 05/17] :truck: Move tests into their own package. --- pytest.ini | 2 ++ tests/__init__.py | 0 tests.py => tests/tests.py | 0 3 files changed, 2 insertions(+) create mode 100644 pytest.ini create mode 100644 tests/__init__.py rename tests.py => tests/tests.py (100%) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..be57201 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = tests/* diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests.py b/tests/tests.py similarity index 100% rename from tests.py rename to tests/tests.py From 25473e832c244ebc41796e2fd1052593fd83e1ac Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Sun, 7 Jul 2019 11:08:52 +0200 Subject: [PATCH 06/17] :truck: Benchmarks have their own directory. --- README.rst | 2 +- .../perfomance_comparison.py | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) rename performance_tests.py => benchmarks/perfomance_comparison.py (92%) diff --git a/README.rst b/README.rst index 50855f9..6bb5c7a 100644 --- a/README.rst +++ b/README.rst @@ -227,7 +227,7 @@ able to perform on the JSON dump of the `Python subreddit main page Date: Sun, 7 Jul 2019 12:24:01 +0200 Subject: [PATCH 07/17] :recycle: Use f-string instead of string concatenation. --- scalpl/scalpl.py | 14 +++++++------- tests/tests.py | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/scalpl/scalpl.py b/scalpl/scalpl.py index ef3c38e..166d669 100644 --- a/scalpl/scalpl.py +++ b/scalpl/scalpl.py @@ -54,9 +54,9 @@ def __delitem__(self, key: str) -> None: parent, last_key = self._traverse(self.data, key) del parent[last_key] except KeyError: - raise KeyError('key "' + key + '" not found.') + raise KeyError(f'Key "{key}" not found.') except IndexError: - raise IndexError('index out of range in key "' + key + '".') + raise IndexError(f'Index out of range in key "{key}".') def __eq__(self, other: Any) -> bool: return self.data == other @@ -66,9 +66,9 @@ def __getitem__(self, key: str) -> Any: parent, last_key = self._traverse(self.data, key) return parent[last_key] except KeyError: - raise KeyError('key "' + key + '" not found.') + raise KeyError(f'Key "{key}" not found.') except IndexError: - raise IndexError('index out of range in key "' + key + '".') + raise IndexError(f'Index out of range in key "{key}".') def __iter__(self) -> Iterator: return iter(self.data) @@ -84,9 +84,9 @@ def __setitem__(self, key: str, value: Any) -> None: parent, last_key = self._traverse(self.data, key) parent[last_key] = value except KeyError: - raise KeyError('key "' + key + '" not found.') + raise KeyError(f'Key "{key}" not found.') except IndexError: - raise IndexError('index out of range in key "' + key + '".') + raise IndexError(f'Index out of range in key "{key}".') def __str__(self) -> str: return str(self.data) @@ -195,7 +195,7 @@ def setdefault(self, key: str, default: Optional = None) -> Any: parent[last_key] = default return default except IndexError: - raise IndexError('index out of range in key "' + key + '".') + raise IndexError(f'Index out of range in key "{key}".') def _traverse_list(self, parent, key): key, *str_indexes = key.split("[") diff --git a/tests/tests.py b/tests/tests.py index ffab430..24030cb 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -159,7 +159,7 @@ def test_delitem_undefined_key(self): del self.data["trainer.bicycle"] self.fail() except KeyError as error: - expected = "'key \"trainer.bicycle\" not found.'" + expected = "'Key \"trainer.bicycle\" not found.'" assert str(error) == expected def test_get(self): @@ -182,7 +182,7 @@ def test_getitem_undefined_key(self): self.data["trainer.badges.Thunder"] self.fail() except KeyError as error: - assert str(error) == "'key \"trainer.badges.Thunder\" not found.'" + assert str(error) == "'Key \"trainer.badges.Thunder\" not found.'" def test_getitem_with_custom_separator(self): self.data = self.Wrapper(deepcopy(BASE), sep="+") @@ -220,7 +220,7 @@ def test_settitem_on_undefined_key(self): self.data["trainer.bicycle.size"] = 180 self.fail() except KeyError as error: - assert str(error) == "'key \"trainer.bicycle.size\" not found.'" + assert str(error) == "'Key \"trainer.bicycle.size\" not found.'" def test_setdefault(self): assert self.data.setdefault("trainer", "Not Found") == ASH @@ -326,7 +326,7 @@ def test_delitem_undefined_list_item(self): del self.data["pokemons[42].type[1]"] self.fail() except IndexError as error: - expected = 'index out of range in key "pokemons[42].type[1]".' + expected = 'Index out of range in key "pokemons[42].type[1]".' assert str(error) == expected def test_delitem_list_item_through_nested_list(self): @@ -339,7 +339,7 @@ def test_delitem_list_item_through_undefined_nested_list(self): del self.data["team_sets[42][0]"] self.fail() except IndexError as error: - expected = 'index out of range in key "team_sets[42][0]".' + expected = 'Index out of range in key "team_sets[42][0]".' assert str(error) == expected def test_get_through_list(self): @@ -367,7 +367,7 @@ def test_getitem_through_undefined_list_item(self): self.data["pokemons[42].type[1]"] self.fail() except IndexError as error: - expected = 'index out of range in key "pokemons[42].type[1]".' + expected = 'Index out of range in key "pokemons[42].type[1]".' assert str(error) == expected def test_in_through_list(self): @@ -419,14 +419,14 @@ def test_setdefault_on_undefined_first_item(self): self.data.setdefault("pokemons[666]", BULBASAUR) self.fail() except IndexError as error: - assert str(error) == 'index out of range in key "pokemons[666]".' + assert str(error) == 'Index out of range in key "pokemons[666]".' def test_setdefault_on_undefined_list_item(self): try: self.data.setdefault("pokemons[0].type[2]", "Funny") self.fail() except IndexError as error: - expected = 'index out of range in key "pokemons[0].type[2]".' + expected = 'Index out of range in key "pokemons[0].type[2]".' assert str(error) == expected def test_setdefault_through_list(self): @@ -439,7 +439,7 @@ def test_setdefault_undefined_key_through_list(self): self.data.setdefault("pokemons[42].sex", "Unknown") self.fail() except IndexError as error: - expected = 'index out of range in key "pokemons[42].sex".' + expected = 'Index out of range in key "pokemons[42].sex".' assert str(error) == expected def test_setdefault_through_nested_list(self): @@ -453,7 +453,7 @@ def test_setdefault_undefined_first_item_in_nested_list_item(self): self.data.setdefault("team_sets[42][0]", BULBASAUR) self.fail() except IndexError as error: - expected = 'index out of range in key "team_sets[42][0]".' + expected = 'Index out of range in key "team_sets[42][0]".' assert str(error) == expected def test_setdefault_undefined_nested_list_item(self): @@ -461,7 +461,7 @@ def test_setdefault_undefined_nested_list_item(self): self.data.setdefault("team_sets[0][42]", BULBASAUR) self.failt() except IndexError as error: - expected = 'index out of range in key "team_sets[0][42]".' + expected = 'Index out of range in key "team_sets[0][42]".' assert str(error) == expected def test_setitem_through_list(self): @@ -482,7 +482,7 @@ def test_setitem_undefined_list_item(self): self.data["pokemons[42].type[1]"] = "Fire" self.fail() except IndexError as error: - expected = 'index out of range in key "pokemons[42].type[1]".' + expected = 'Index out of range in key "pokemons[42].type[1]".' assert str(error) == expected def test_settitem_nested_list_item(self): @@ -495,7 +495,7 @@ def test_settitem_nested_undefined_list_item(self): self.data["team_sets[42][0]"] = CHARMANDER self.fail() except IndexError as error: - expected = 'index out of range in key "team_sets[42][0]".' + expected = 'Index out of range in key "team_sets[42][0]".' assert str(error) == expected def test_update_from_dict_through_list(self): From 95ffc100f9d3d214cb259e772b4f99d4ca30d216 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Sun, 7 Jul 2019 12:39:16 +0200 Subject: [PATCH 08/17] :bug: Fix optional types syntax error. --- scalpl/scalpl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scalpl/scalpl.py b/scalpl/scalpl.py index 166d669..310757b 100644 --- a/scalpl/scalpl.py +++ b/scalpl/scalpl.py @@ -116,7 +116,7 @@ def fromkeys( ) -> TLightCut: return cls(dict.fromkeys(seq, value)) - def get(self, key: str, default: Optional = None) -> Any: + def get(self, key: str, default: Optional[Any] = None) -> Any: try: return self[key] except (KeyError, IndexError): @@ -176,7 +176,7 @@ class Cut(LightCut): __slots__ = () - def setdefault(self, key: str, default: Optional = None) -> Any: + def setdefault(self, key: str, default: Optional[Any] = None) -> Any: try: parent = self.data *parent_keys, last_key = key.split(self.sep) From 400aa4cfa83ccd0e914cc5e1764441fb10803362 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Mon, 15 Jul 2019 11:27:01 +0200 Subject: [PATCH 09/17] :green_heart: Fix commands of the Travis build. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c831bec..f912f7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,5 @@ install: - pip3 install black script: - - pytest tests.py - - black --check scalpl.py tests.py setup.py + - pytest + - black --check scalpl tests setup.py From fe33611518f25afeab670010ee927d14f70890cd Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 10 Jul 2019 14:41:07 +0200 Subject: [PATCH 10/17] :sparkles: New take on Errors and test parametrization. --- scalpl/scalpl.py | 174 ++++++--- tests/fixtures.py | 90 +++++ tests/tests.py | 968 +++++++++++++++++++++++++--------------------- 3 files changed, 727 insertions(+), 505 deletions(-) create mode 100644 tests/fixtures.py diff --git a/scalpl/scalpl.py b/scalpl/scalpl.py index 310757b..17cd212 100644 --- a/scalpl/scalpl.py +++ b/scalpl/scalpl.py @@ -18,6 +18,24 @@ TLightCut = TypeVar("TLightCut", bound="LightCut") +def key_error( + failing_key: str, original_path: str, raised_error: Exception +) -> KeyError: + return KeyError( + f"Cannot access key '{failing_key}' in path '{original_path}'," + f" because of error: {repr(raised_error)}." + ) + + +def index_error( + failing_key: str, original_path: str, raised_error: Exception +) -> IndexError: + return IndexError( + f"Cannot access index '{failing_key}' in path '{original_path}'," + f" because of error: {repr(raised_error)}." + ) + + class LightCut: """ LightCut is a simple wrapper over the built-in dict class. @@ -42,33 +60,35 @@ def __bool__(self) -> bool: return bool(self.data) def __contains__(self, key: str) -> bool: + parent, last_key = self._traverse(self.data, key) try: - parent, last_key = self._traverse(self.data, key) parent[last_key] return True except (IndexError, KeyError): return False - def __delitem__(self, key: str) -> None: + def __delitem__(self, path: str) -> None: + parent, last_key = self._traverse(self.data, path) + try: - parent, last_key = self._traverse(self.data, key) del parent[last_key] - except KeyError: - raise KeyError(f'Key "{key}" not found.') - except IndexError: - raise IndexError(f'Index out of range in key "{key}".') + except KeyError as error: + raise key_error(last_key, path, error) + except IndexError as error: + raise index_error(last_key, path, error) def __eq__(self, other: Any) -> bool: return self.data == other - def __getitem__(self, key: str) -> Any: + def __getitem__(self, path: str) -> Any: + parent, last_key = self._traverse(self.data, path) + try: - parent, last_key = self._traverse(self.data, key) return parent[last_key] - except KeyError: - raise KeyError(f'Key "{key}" not found.') - except IndexError: - raise IndexError(f'Index out of range in key "{key}".') + except KeyError as error: + raise key_error(last_key, path, error) + except IndexError as error: + raise index_error(last_key, path, error) def __iter__(self) -> Iterator: return iter(self.data) @@ -79,23 +99,28 @@ def __len__(self) -> int: def __ne__(self, other: Any) -> bool: return self.data != other - def __setitem__(self, key: str, value: Any) -> None: + def __setitem__(self, path: str, value: Any) -> None: + parent, last_key = self._traverse(self.data, path) + try: - parent, last_key = self._traverse(self.data, key) parent[last_key] = value - except KeyError: - raise KeyError(f'Key "{key}" not found.') - except IndexError: - raise IndexError(f'Index out of range in key "{key}".') + except IndexError as error: + raise index_error(last_key, path, error) def __str__(self) -> str: return str(self.data) - def _traverse(self, parent, key: str): - *parent_keys, last_key = key.split(self.sep) - if parent_keys: - for key in parent_keys: - parent = parent[key] + def _traverse(self, parent, path: str): + *parent_keys, last_key = path.split(self.sep) + if len(parent_keys) == 0: + return parent, last_key + + try: + for sub_key in parent_keys: + parent = parent[sub_key] + except KeyError as error: + raise key_error(sub_key, path, error) + return parent, last_key def all(self: TLightCut, key: str) -> Iterator[TLightCut]: @@ -116,11 +141,13 @@ def fromkeys( ) -> TLightCut: return cls(dict.fromkeys(seq, value)) - def get(self, key: str, default: Optional[Any] = None) -> Any: + def get(self, path: str, default: Optional[Any] = None) -> Any: try: - return self[key] - except (KeyError, IndexError): - return default + return self[path] + except (KeyError, IndexError) as error: + if default is not None: + return default + raise error def keys(self) -> KeysView: return self.data.keys() @@ -128,12 +155,18 @@ def keys(self) -> KeysView: def items(self) -> ItemsView: return self.data.items() - def pop(self, key: str, default: Any = None) -> Any: + def pop(self, path: str, default: Any = None) -> Any: + parent, last_key = self._traverse(self.data, path) try: - parent, last_key = self._traverse(self.data, key) return parent.pop(last_key) - except (KeyError, IndexError): - return default + except IndexError as error: + if default is not None: + return default + raise index_error(last_key, path, error) + except KeyError as error: + if default is not None: + return default + raise key_error(last_key, path, error) def popitem(self) -> Any: return self.data.popitem() @@ -153,6 +186,7 @@ def update(self, data=None, **kwargs): pairs = data.items() except AttributeError: pairs = chain(data, kwargs.items()) + for key, value in pairs: self.__setitem__(key, value) @@ -177,42 +211,64 @@ class Cut(LightCut): __slots__ = () def setdefault(self, key: str, default: Optional[Any] = None) -> Any: + parent = self.data + *parent_keys, last_key = key.split(self.sep) + + if parent_keys: + for _key in parent_keys: + parent, _key = self._traverse_list(parent, _key, key) + try: + parent = parent[_key] + except KeyError: + child: dict = {} + parent[_key] = child + parent = child + except IndexError as error: + raise index_error(_key, key, error) + + parent, last_key = self._traverse_list(parent, last_key, key) + try: - parent = self.data - *parent_keys, last_key = key.split(self.sep) - if parent_keys: - for _key in parent_keys: - parent, _key = self._traverse_list(parent, _key) - try: - parent = parent[_key] - except KeyError: - child: dict = {} - parent[_key] = child - parent = child - parent, last_key = self._traverse_list(parent, last_key) return parent[last_key] except KeyError: parent[last_key] = default return default - except IndexError: - raise IndexError(f'Index out of range in key "{key}".') + except IndexError as error: + raise index_error(last_key, key, error) - def _traverse_list(self, parent, key): + def _traverse_list(self, parent, key, original_path: str): key, *str_indexes = key.split("[") - if str_indexes: + if not str_indexes: + return parent, key + + try: parent = parent[key] + except KeyError as error: + raise key_error(key, original_path, error) + try: for str_index in str_indexes[:-1]: index = int(str_index[:-1]) parent = parent[index] - return parent, int(str_indexes[-1][:-1]) - else: - return parent, key - - def _traverse(self, parent, key): - *parent_keys, last_key = key.split(self.sep) - if parent_keys: - for _key in parent_keys: - parent, _key = self._traverse_list(parent, _key) - parent = parent[_key] - parent, last_key = self._traverse_list(parent, last_key) + except IndexError as error: + raise index_error(index, original_path, error) + try: + last_index = int(str_indexes[-1][:-1]) + except ValueError as error: + raise index_error(str_indexes[-1][:-1], original_path, error) + + return parent, last_index + + def _traverse(self, parent, path: str): + *parent_keys, last_key = path.split(self.sep) + if len(parent_keys) > 0: + try: + for sub_key in parent_keys: + parent, sub_key = self._traverse_list(parent, sub_key, path) + parent = parent[sub_key] + except KeyError as error: + raise key_error(sub_key, path, error) + except IndexError as error: + raise index_error(sub_key, path, error) + + parent, last_key = self._traverse_list(parent, last_key, path) return parent, last_key diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..318f13c --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,90 @@ +from collections import defaultdict, OrderedDict +from copy import deepcopy +from functools import partial +import pytest +from scalpl import Cut, LightCut + + +ASH = { + "name": "Ash", + "age": 666, + "sex": None, + "badges": {"Boulder": True, "Cascade": False}, +} + +BULBASAUR = { + "name": "Bulbasaur", + "types": ["Grass", "Poison"], + "category": "Seed", + "ability": "Overgrow", +} + +CHARMANDER = { + "name": "Charmander", + "types": ["Fire"], + "category": "Lizard", + "ability": "Blaze", +} + +SQUIRTLE = { + "name": "Squirtle", + "types": ["Water"], + "category": "Tiny Turtle", + "ability": "Torrent", +} + +FIRST_SET = [CHARMANDER, SQUIRTLE, BULBASAUR] +SECOND_SET = [CHARMANDER, BULBASAUR, SQUIRTLE] +TEAM_SETS = [FIRST_SET, SECOND_SET] +POKEMONS = [BULBASAUR, CHARMANDER, SQUIRTLE] + + +BASE = {"trainer": ASH, "pokemons": POKEMONS, "team_sets": TEAM_SETS} + + +@pytest.fixture( + params=[ + (LightCut, dict), + (LightCut, OrderedDict), + (LightCut, partial(defaultdict, None)), + (Cut, dict), + (Cut, OrderedDict), + (Cut, partial(defaultdict, None)), + ] +) +def proxy(request): + scalpl_class = request.param[0] + underlying_dict_class = request.param[1] + return scalpl_class(underlying_dict_class(deepcopy(BASE))) + + +@pytest.fixture( + params=[(Cut, dict), (Cut, OrderedDict), (Cut, partial(defaultdict, None))] +) +def cut_proxy(request): + scalpl_class = request.param[0] + underlying_dict_class = request.param[1] + return scalpl_class(underlying_dict_class(deepcopy(BASE))) + + +@pytest.fixture( + params=[ + (LightCut, dict), + (LightCut, OrderedDict), + (LightCut, partial(defaultdict, None)), + ] +) +def lightcut_proxy(request): + scalpl_class = request.param[0] + underlying_dict_class = request.param[1] + return scalpl_class(underlying_dict_class(deepcopy(BASE))) + + +@pytest.fixture(params=[LightCut, Cut]) +def scalpl_class(request): + return request.param + + +@pytest.fixture(params=[dict, OrderedDict, partial(defaultdict, None)]) +def underlying_dict_class(request): + return request.param diff --git a/tests/tests.py b/tests/tests.py index 24030cb..b882737 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,102 +1,191 @@ from collections import defaultdict, OrderedDict from copy import deepcopy +from .fixtures import ( + BULBASAUR, + CHARMANDER, + SQUIRTLE, + ASH, + FIRST_SET, + SECOND_SET, + BASE, + cut_proxy, + lightcut_proxy, + proxy, + scalpl_class, + underlying_dict_class, +) +import pytest from types import GeneratorType -from scalpl import Cut, LightCut +class TestLightCutTraverse: + def test_traverse_single_key(self, lightcut_proxy): + parent, last_key = lightcut_proxy._traverse(ASH, "name") + assert parent == ASH + assert last_key == "name" + + def test_traverse_single_undefined_key(self, lightcut_proxy): + parent, last_key = lightcut_proxy._traverse(ASH, "undefined_key") + assert parent == ASH + assert last_key == "undefined_key" + + def test_traverse_nested_keys(self, lightcut_proxy): + parent, last_key = lightcut_proxy._traverse(ASH, "badges.Boulder") + assert parent == ASH["badges"] + assert last_key == "Boulder" + + def test_traverse_undefined_nested_keys(self, lightcut_proxy): + with pytest.raises(KeyError) as error: + parent, last_key = lightcut_proxy._traverse(ASH, "undefined_key.name") -ASH = { - "name": "Ash", - "age": 666, - "sex": None, - "badges": {"Boulder": True, "Cascade": False}, -} + expected_error = KeyError( + "Cannot access key 'undefined_key' in path 'undefined_key.name', " + "because of error: KeyError('undefined_key',)." + ) + assert str(error.value) == str(expected_error) -BULBASAUR = { - "name": "Bulbasaur", - "type": ["Grass", "Poison"], - "category": "Seed", - "ability": "Overgrow", -} -CHARMANDER = { - "name": "Charmander", - "type": "Fire", - "category": "Lizard", - "ability": "Blaze", -} +class TestCutTraverse: + def test_traverse_single_key(self, cut_proxy): + parent, last_key = cut_proxy._traverse(ASH, "name") + assert parent == ASH + assert last_key == "name" -SQUIRTLE = { - "name": "Squirtle", - "type": "Water", - "category": "Tiny Turtle", - "ability": "Torrent", -} + def test_traverse_single_undefined_key(self, cut_proxy): + parent, last_key = cut_proxy._traverse(ASH, "undefined_key") + assert parent == ASH + assert last_key == "undefined_key" -FIRST_SET = [CHARMANDER, SQUIRTLE, BULBASAUR] -SECOND_SET = [CHARMANDER, BULBASAUR, SQUIRTLE] + def test_traverse_nested_keys(self, cut_proxy): + parent, last_key = cut_proxy._traverse(ASH, "badges.Boulder") + assert parent == ASH["badges"] + assert last_key == "Boulder" -BASE = { - "trainer": ASH, - "pokemons": [BULBASAUR, CHARMANDER, SQUIRTLE], - "team_sets": [FIRST_SET, SECOND_SET], -} + def test_traverse_undefined_nested_keys(self, cut_proxy): + with pytest.raises(KeyError) as error: + parent, last_key = cut_proxy._traverse(ASH, "undefined_key.name") + expected_error = KeyError( + "Cannot access key 'undefined_key' in path 'undefined_key.name', " + "because of error: KeyError('undefined_key',)." + ) + assert str(error.value) == str(expected_error) -class TestLightCutProxiedMethods: - """ - Here lives tests of dict methods that are simply proxied by scalpl. - """ + def test_traverse_single_list_item(self, cut_proxy): + data = {"types": ["Fire"]} + parent, last_key = cut_proxy._traverse(data, "types[0]") + assert parent == data["types"] + assert last_key == 0 - Dict = dict - Wrapper = LightCut + def test_traverse_undefined_list_item_do_not_throw_error(self, cut_proxy): + data = {"types": ["Fire"]} + parent, last_key = cut_proxy._traverse(data, "types[1]") + assert parent == data["types"] + assert last_key == 1 - def setup(self): - self.data = self.Wrapper(deepcopy(self.Dict(BASE))) + def test_traverse_undefined_list_raises_exception(self, cut_proxy): + data = {"types": ["Fire"]} - def test_bool(self): - assert bool(self.data) is True + with pytest.raises(KeyError) as error: + cut_proxy._traverse(data, "colors[0]") - def test_clear(self): - self.data.clear() - assert len(self.data) == 0 + expected_error = KeyError( + "Cannot access key 'colors' in path 'colors[0]', " + "because of error: KeyError('colors',)." + ) + assert str(error.value) == str(expected_error) - def test_copy(self): - assert self.data.copy() == BASE + def test_traverse_nested_list_item(self, cut_proxy): + data = {"types": [["Fire", "Water"]]} + parent, last_key = cut_proxy._traverse(BULBASAUR, "types[0][1]") + assert parent == BULBASAUR["types"][0] + assert last_key == 1 - def test_equalty_with_Cut(self): - other = self.Wrapper(self.data.data) - assert self.data == other + def test_traverse_undefined_nested_list_item_raises_exception(self, cut_proxy): + data = {"types": [["Fire", "Water"]]} - def test_equalty_with_dict(self): - other = self.data.data - assert self.data == other + with pytest.raises(IndexError) as error: + cut_proxy._traverse(BULBASAUR, "types[42][0]") - def test_fromkeys(self): - expected = self.Wrapper({"Catz": "Lulz", "Dogz": "Lulz", "Fishz": "Lulz"}) + expected_error = IndexError( + "Cannot access index '42' in path 'types[42][0]', " + "because of error: IndexError('list index out of range',)." + ) + assert str(error.value) == str(expected_error) - seq = ["Catz", "Dogz", "Fishz"] - value = "Lulz" - assert self.Wrapper.fromkeys(seq, value) == expected + def test_traverse_with_non_integer_index_raises_exception(self, cut_proxy): + data = {"types": [["Fire", "Water"]]} - def test_fromkeys_default(self): - expected = self.Wrapper({"Catz": None, "Dogz": None, "Fishz": None}) + with pytest.raises(IndexError) as error: + cut_proxy._traverse(BULBASAUR, "types[toto]") - seq = ["Catz", "Dogz", "Fishz"] - assert self.Wrapper.fromkeys(seq) == expected + expected_error = IndexError( + "Cannot access index 'toto' in path 'types[toto]', " + "because of error: ValueError(\"invalid literal for int() with base 10: 'toto'\",)." + ) - def test_inequalty_with_Cut(self): - other = self.Wrapper(deepcopy(BASE)) - other["trainer.name"] = "Giovanni" - assert self.data != other + assert str(error.value) == str(expected_error) - def test_inequalty_with_dict(self): - other = self.Wrapper(deepcopy(BASE)) - other["trainer.name"] = "Giovanni" - assert self.data != other.data - def test_items(self): - result = sorted(self.data.items()) +class TestBool: + def test_evaluate_to_true_for_existing_data(self, proxy): + assert bool(proxy) is True + + def test_evaluate_to_false_for_empty_data(self, proxy): + proxy.clear() + assert bool(proxy) is False + + +class TestClear: + def test_clear(self, proxy): + proxy.clear() + assert len(proxy) == 0 + + +class TestCopy: + def test_copy(self, proxy): + assert proxy.copy() == BASE + + +class TestEquals: + def test_against_another_scalpl_class(self, proxy): + assert proxy == deepcopy(proxy) + + def test_against_another_dict(self, proxy): + other = dict(proxy.data) + assert proxy == other + + def test_against_another_ordered_dict(self, proxy): + other = OrderedDict(proxy.data) + assert proxy == other + + def test_against_another_default_dict(self, proxy): + other = defaultdict(None, proxy.data) + assert proxy == other + + +class TestFromKeys: + def test_scalpl_class_from_keys(self, scalpl_class, underlying_dict_class): + expected = scalpl_class( + {"Bulbasaur": "captured", "Charmander": "captured", "Squirtle": "captured"} + ) + seq = ["Bulbasaur", "Charmander", "Squirtle"] + value = "captured" + + assert scalpl_class.fromkeys(seq, value) == expected + + def test_with_default_values(self, scalpl_class, underlying_dict_class): + expected = scalpl_class( + {"Bulbasaur": None, "Charmander": None, "Squirtle": None} + ) + seq = ["Bulbasaur", "Charmander", "Squirtle"] + + assert scalpl_class.fromkeys(seq) == expected + + +class TestItems: + def test_items(self, proxy): + result = sorted(proxy.items()) expected = [ ("pokemons", [BULBASAUR, CHARMANDER, SQUIRTLE]), ("team_sets", [FIRST_SET, SECOND_SET]), @@ -104,468 +193,455 @@ def test_items(self): ] assert result == expected - def test_iter(self): - keys = sorted([key for key in self.data]) - assert keys[0] == "pokemons" - assert keys[1] == "team_sets" - assert keys[2] == "trainer" - def test_keys(self): - result = sorted(self.data.keys()) - expected = ["pokemons", "team_sets", "trainer"] - assert result == expected +class TestIter: + def test_iter(self, proxy): + keys = sorted([key for key in proxy]) + assert keys == ["pokemons", "team_sets", "trainer"] - def test_len(self): - assert len(self.data) == 3 - def test_popitem(self): - self.data.popitem() - assert len(self.data) == 2 +class TestKeys: + def test_keys(self, proxy): + result = sorted(proxy.keys()) + assert result == ["pokemons", "team_sets", "trainer"] - def test_str(self): - result = str(self.Wrapper(OrderedDict(deepcopy(BASE)))) - expected = str(OrderedDict(deepcopy(BASE))) - assert result == expected - def test_values(self): - result = list(self.Wrapper(OrderedDict(deepcopy(BASE))).values()) - expected = list(OrderedDict(deepcopy(BASE)).values()) - assert result == expected +class TestLen: + def test_len(self, proxy): + assert len(proxy) == 3 -class TestLightCutCustomLogicMethods: - """ - Here lives tests of dict methods where scalpl adds its custom logic - to handle operate on nested dictionnaries. - """ +class TestPopitem: + def test_popitem(self, proxy): + proxy.popitem() + assert len(proxy) == 2 - Dict = dict - Wrapper = LightCut - def setup(self): - self.data = self.Wrapper(deepcopy(self.Dict(BASE))) +class TestDelitem: + def test_delitem(self, proxy): + del proxy["trainer"] + assert "trainer" not in proxy - def test_delitem(self): - del self.data["trainer"] - assert "trainer" not in self.data + def test_on_nested_key(self, proxy): + del proxy["trainer.name"] + assert "name" not in proxy["trainer"] + assert "trainer" in proxy - def test_delitem_nested_key(self): - del self.data["trainer.name"] - assert "name" not in self.data["trainer"] - assert "trainer" in self.data + def test_on_undefined_key(self, proxy): + with pytest.raises(KeyError) as error: + del proxy["trainer.bicycle"] - def test_delitem_undefined_key(self): - try: - del self.data["trainer.bicycle"] - self.fail() - except KeyError as error: - expected = "'Key \"trainer.bicycle\" not found.'" - assert str(error) == expected + expected_error = KeyError( + "Cannot access key 'bicycle' in path 'trainer.bicycle', " + "because of error: KeyError('bicycle',)." + ) + assert str(error.value) == str(expected_error) - def test_get(self): - assert self.data.get("trainer") == ASH - assert self.data.get("trainer.badges") == {"Boulder": True, "Cascade": False} - assert self.data.get("trainer.badges.Boulder") is True - - def test_get_undefined_key(self): - assert self.data.get("game", "Pokemon Blue") == "Pokemon Blue" - assert self.data.get("trainer.hometown", "Pallet Town") == "Pallet Town" - assert self.data.get("trainer.badges.Thunder", False) is False - - def test_getitem(self): - assert self.data["trainer"] == ASH - assert self.data["trainer.badges"] == {"Boulder": True, "Cascade": False} - assert self.data["trainer.badges.Cascade"] is False - - def test_getitem_undefined_key(self): - try: - self.data["trainer.badges.Thunder"] - self.fail() - except KeyError as error: - assert str(error) == "'Key \"trainer.badges.Thunder\" not found.'" - - def test_getitem_with_custom_separator(self): - self.data = self.Wrapper(deepcopy(BASE), sep="+") - assert self.data["trainer"] == ASH - assert self.data["trainer+badges"] == {"Boulder": True, "Cascade": False} - assert self.data["trainer+badges+Boulder"] is True - - def test_in(self): - assert "trainer" in self.data - assert "trainer.badges" in self.data - assert "trainer.badges.Boulder" in self.data - - def test_not_in(self): - assert "game" not in self.data - assert "trainer.hometown" not in self.data - assert "trainer.badges.Thunder" not in self.data - - def test_pop(self): - assert self.data.pop("trainer") == ASH - assert "trainer" not in self.data - - def test_pop_nested_key(self): - assert self.data.pop("trainer.badges.Cascade") is False - assert "trainer.badges.Cascade" not in self.data - - def test_pop_undefined_key(self): - assert self.data.pop("trainer.bicycle", "Not Found") == "Not Found" - - def test_setitem(self): - self.data["trainer.badges.Boulder"] = False - assert self.data["trainer"]["badges"]["Boulder"] is False - - def test_settitem_on_undefined_key(self): - try: - self.data["trainer.bicycle.size"] = 180 - self.fail() - except KeyError as error: - assert str(error) == "'Key \"trainer.bicycle.size\" not found.'" - - def test_setdefault(self): - assert self.data.setdefault("trainer", "Not Found") == ASH - - def test_setdefault_nested_key(self): - result = self.data.setdefault("trainer.badges.Boulder", "Undefined") - assert result is True + def test_through_list(self, cut_proxy): + del cut_proxy["pokemons[1].category"] + assert "category" not in cut_proxy.data["pokemons"][1] - def test_setdefault_on_undefined_key(self): - result = self.data.setdefault("trainer.bicycle.size", 180) - assert result == 180 - assert self.data.data["trainer"]["bicycle"]["size"] == 180 + def test_undefined_key_through_list_(self, cut_proxy): + with pytest.raises(KeyError) as error: + del cut_proxy["pokemons[1].has_been_seen"] - def test_update_from_dict(self): - from_other = {"trainer.friend": "Brock"} - self.data.update(from_other) - assert self.data["trainer"]["friend"] == "Brock" + expected_error = KeyError( + "Cannot access key 'has_been_seen' in path 'pokemons[1].has_been_seen', " + "because of error: KeyError('has_been_seen',)." + ) + assert str(error.value) == str(expected_error) - def test_update_from_dict_and_keyword_args(self): - from_other = {"trainer.friend": "Brock"} - self.data.update(from_other, game="Pokemon Blue") - assert self.data["trainer"]["friend"] == "Brock" - assert self.data["game"] == "Pokemon Blue" + def test_through_nested_list(self, cut_proxy): + del cut_proxy["team_sets[0][0].name"] + assert "name" not in cut_proxy.data["team_sets"][0][0] - def test_update_from_list(self): - from_other = [("trainer.friend", "Brock")] - self.data.update(from_other) - assert self.data["trainer"]["friend"] == "Brock" + def test_undefined_key_through_nested_list(self, cut_proxy): + with pytest.raises(KeyError) as error: + del cut_proxy["team_sets[0][0].can_fly"] - def test_update_from_list_and_keyword_args(self): - from_other = [("trainer.friend", "Brock")] - self.data.update(from_other, game="Pokemon Blue") - assert self.data["trainer"]["friend"] == "Brock" - assert self.data["game"] == "Pokemon Blue" + expected_error = KeyError( + "Cannot access key 'can_fly' in path 'team_sets[0][0].can_fly', " + "because of error: KeyError('can_fly',)." + ) + assert str(error.value) == str(expected_error) - def test_all_returns_generator(self): - result = self.data.all("pokemons") - assert isinstance(result, GeneratorType) is True - result = list(result) - assert result[0].data == BULBASAUR - assert result[1].data == CHARMANDER - assert result[2].data == SQUIRTLE + def test_list_item(self, cut_proxy): + del cut_proxy["pokemons[0].types[1]"] + assert len(cut_proxy.data["pokemons"][0]["types"]) == 1 - def test_all_with_custom_separator(self): - self.data.sep = "/" - result = self.data.all("pokemons") - result = list(result) - assert result[0].sep == "/" - assert result[1].sep == "/" - assert result[2].sep == "/" + def test_undefined_list_item(self, cut_proxy): + with pytest.raises(IndexError) as error: + del cut_proxy["pokemons[0].types[42]"] + expected_error = IndexError( + "Cannot access index '42' in path 'pokemons[0].types[42]', " + "because of error: IndexError('list assignment index out of range',)." + ) + assert str(error.value) == str(expected_error) -class TestLightCutWithOrderedDictPM(TestLightCutProxiedMethods): - Dict = OrderedDict - Wrapper = LightCut + def test_undefined_list_index(self, cut_proxy): + with pytest.raises(IndexError) as error: + del cut_proxy["pokemons[42].types[1]"] + expected = IndexError( + "Cannot access index '42' in path 'pokemons[42].types[1]', " + "because of error: IndexError('list index out of range',)." + ) + assert str(error.value) == str(expected) -class TestLightCutWithOrderedDictCLM(TestLightCutCustomLogicMethods): - Dict = OrderedDict - Wrapper = LightCut + def test_list_item_through_nested_list(self, cut_proxy): + del cut_proxy["team_sets[0][0]"] + assert len(cut_proxy.data["team_sets"][0]) == 2 -class TestLightCutWithDefaultDictPM(TestLightCutProxiedMethods): - Dict = defaultdict - Wrapper = LightCut +class TestGet: + def test_get(self, proxy): + assert proxy.get("trainer") == ASH + assert proxy.get("trainer.badges") == {"Boulder": True, "Cascade": False} + assert proxy.get("trainer.badges.Boulder") is True + + def test_get_undefined_key_raises_exception(self, proxy): + with pytest.raises(KeyError) as error: + proxy.get("trainer.hometown") + + expected = KeyError( + "Cannot access key 'hometown' in path 'trainer.hometown', " + "because of error: KeyError('hometown',)." + ) + assert str(error.value) == str(expected) + + def test_get_undefined_key_when_default_is_provided(self, proxy): + assert proxy.get("game", "Pokemon Blue") == "Pokemon Blue" + assert proxy.get("trainer.hometown", "Pallet Town") == "Pallet Town" + assert proxy.get("trainer.badges.Thunder", False) is False + + def test_get_through_list(self, cut_proxy): + assert cut_proxy.get("pokemons[0].types[1]") == "Poison" + + def test_get_through_nested_list(self, cut_proxy): + result = cut_proxy.get("team_sets[0][0].name", "Unknwown") + assert result == "Charmander" - def setup(self): - self.data = self.Wrapper(deepcopy(self.Dict(None, BASE))) + def test_get_undefined_key_through_list(self, cut_proxy): + assert cut_proxy.get("pokemons[0].sex", "Unknown") == "Unknown" + def test_get_undefined_list_item(self, cut_proxy): + assert cut_proxy.get("pokemons[0].types[42]", "Unknown") == "Unknown" -class TestLightCutWithDefaultDictCLM(TestLightCutCustomLogicMethods): - Dict = defaultdict - Wrapper = LightCut - def setup(self): - self.data = self.Wrapper(deepcopy(self.Dict(None, BASE))) +class TestGetitem: + def test_getitem(self, proxy): + assert proxy["trainer"] == ASH + assert proxy["trainer.badges"] == {"Boulder": True, "Cascade": False} + assert proxy["trainer.badges.Cascade"] is False + def test_getitem_undefined_key(self, proxy): + with pytest.raises(KeyError) as error: + proxy["trainer.badges.Thunder"] -class TestCutWithDictPM(TestLightCutProxiedMethods): - Dict = dict - Wrapper = Cut + expected_error = KeyError( + "Cannot access key 'Thunder' in path 'trainer.badges.Thunder', " + "because of error: KeyError('Thunder',)." + ) + assert str(error.value) == str(expected_error) + def test_getitem_with_custom_separator(self, proxy): + proxy.sep = "+" + assert proxy["trainer"] == ASH + assert proxy["trainer+badges"] == {"Boulder": True, "Cascade": False} + assert proxy["trainer+badges+Boulder"] is True -class TestCutWithDictCLM(TestLightCutCustomLogicMethods): - Dict = dict - Wrapper = Cut + def test_getitem_through_list(self, cut_proxy): + assert cut_proxy["pokemons[0].types[1]"] == "Poison" - def test_delitem_through_list(self): - del self.data["pokemons[1].category"] - assert "category" not in self.data.data["pokemons"][1] + def test_getitem_through_nested_list(self, cut_proxy): + assert cut_proxy["team_sets[0][0].name"] == "Charmander" - def test_delitem_through_nested_list(self): - del self.data["team_sets[0][0].name"] - assert "name" not in self.data.data["team_sets"][0][0] + def test_getitem_through_undefined_list_item_raises_exception(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy["pokemons[42].types[1]"] - def test_delitem_list_item(self): - assert len(self.data.data["pokemons"][0]["type"]) == 2 - del self.data["pokemons[0].type[1]"] - assert len(self.data.data["pokemons"][0]["type"]) == 1 + expected = IndexError( + "Cannot access index '42' in path 'pokemons[42].types[1]', " + "because of error: IndexError('list index out of range',)." + ) + assert str(error.value) == str(expected) - def test_delitem_undefined_list_item(self): - try: - del self.data["pokemons[42].type[1]"] - self.fail() - except IndexError as error: - expected = 'Index out of range in key "pokemons[42].type[1]".' - assert str(error) == expected + def test_getitem_through_undefined_list_item_raises_exception(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy["pokemons[0].types[42]"] - def test_delitem_list_item_through_nested_list(self): - assert len(self.data.data["team_sets"][0]) == 3 - del self.data["team_sets[0][0]"] - assert len(self.data.data["team_sets"][0]) == 2 - - def test_delitem_list_item_through_undefined_nested_list(self): - try: - del self.data["team_sets[42][0]"] - self.fail() - except IndexError as error: - expected = 'Index out of range in key "team_sets[42][0]".' - assert str(error) == expected + expected = IndexError( + "Cannot access index '42' in path 'pokemons[0].types[42]', " + "because of error: IndexError('list index out of range',)." + ) + assert str(error.value) == str(expected) - def test_get_through_list(self): - assert self.data.get("pokemons[0].type[1]") == "Poison" - def test_get_through_nested_list(self): - result = self.data.get("team_sets[0][0].name", "Unknwown") - assert result == "Charmander" +class TestIn: + def test_in(self, proxy): + assert "trainer" in proxy + assert "trainer.badges" in proxy + assert "trainer.badges.Boulder" in proxy - def test_get_undefined_key_through_list(self): - assert self.data.get("pokemons[0].sex", "Unknown") == "Unknown" + def test_on_undefined_nested_key_raises_exception(self, proxy): + with pytest.raises(KeyError) as error: + "trainer.hometown.size" not in proxy - def test_get_undefined_list_item(self): - assert self.data.get("pokemons[0].type[42]", "Unknown") == "Unknown" + expected = KeyError( + "Cannot access key 'hometown' in path 'trainer.hometown.size', " + "because of error: KeyError('hometown',)." + ) + assert str(error.value) == str(expected) - def test_getitem_through_list(self): - assert self.data["pokemons[0].type[1]"] == "Poison" + def test_through_list(self, cut_proxy): + assert "pokemons[0].types[1]" in cut_proxy - def test_getitem_through_nested_list(self): - result = self.data["team_sets[0][0].name"] - assert result == "Charmander" + def test_through_nested_list(self, cut_proxy): + assert "team_sets[0][0].name" in cut_proxy - def test_getitem_through_undefined_list_item(self): - try: - self.data["pokemons[42].type[1]"] - self.fail() - except IndexError as error: - expected = 'Index out of range in key "pokemons[42].type[1]".' - assert str(error) == expected + def test_through_undefined_list_item(self, cut_proxy): + with pytest.raises(IndexError) as error: + "pokemons[42].favorite_meal" in cut_proxy - def test_in_through_list(self): - assert "pokemons[0].type[1]" in self.data + expected = IndexError( + "Cannot access index '42' in path 'pokemons[42].favorite_meal', " + "because of error: IndexError('list index out of range',)." + ) + assert str(error.value) == str(expected) - def test_in_through_nested_list(self): - assert "team_sets[0][0].name" in self.data - def test_not_in_through_list(self): - assert "pokemons[1].favorite_meal" not in self.data +class TestPop: + def test_pop(self, proxy): + assert proxy.pop("trainer") == ASH + assert "trainer" not in proxy - def test_not_in_through_nested_list(self): - assert "team_sets[0][0].favorite_meal" not in self.data + def test_with_nested_key(self, proxy): + assert proxy.pop("trainer.badges.Cascade") is False + assert "trainer.badges.Cascade" not in proxy - def test_not_in_through_undefined_list_item(self): - assert "pokemons[42].favorite_meal" not in self.data + def test_when_key_is_undefined_and_default_value_is_provided(self, proxy): + assert proxy.pop("trainer.bicycle", "Not Found") == "Not Found" - def test_pop_list_item(self): - assert len(self.data.data["pokemons"][0]["type"]) == 2 - result = self.data.pop("pokemons[0].type[1]") - assert result == "Poison" - assert len(self.data.data["pokemons"][0]["type"]) == 1 + def test_when_key_is_undefined(self, proxy): + with pytest.raises(KeyError) as error: + proxy.pop("trainer.bicycle") - def test_pop_list_item_through_nested_list(self): - assert len(self.data.data["team_sets"][0]) == 3 - result = self.data.pop("team_sets[0][0]") - assert result == CHARMANDER - assert len(self.data.data["team_sets"][0]) == 2 + expected_error = KeyError( + "Cannot access key 'bicycle' in path 'trainer.bicycle', " + "because of error: KeyError('bicycle',)." + ) + + assert str(error.value) == str(expected_error) + + def test_when_index_is_undefined(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy.pop("pokemons[42]") + + expected_error = IndexError( + "Cannot access index '42' in path 'pokemons[42]', " + "because of error: IndexError('pop index out of range',)." + ) + assert str(error.value) == str(expected_error) + + def test_pop_list_item(self, cut_proxy): + assert cut_proxy.pop("pokemons[0].types[1]") == "Poison" + assert len(cut_proxy.data["pokemons"][0]["types"]) == 1 + + def test_pop_list_item_through_nested_list(self, cut_proxy): + assert cut_proxy.pop("team_sets[0][0]") == CHARMANDER + assert len(cut_proxy.data["team_sets"][0]) == 2 - def test_pop_through_list(self): - result = self.data.pop("pokemons[0].type") - assert result == ["Grass", "Poison"] - assert "type" not in self.data.data["pokemons"][0] + def test_pop_through_list(self, cut_proxy): + assert cut_proxy.pop("pokemons[0].types") == ["Grass", "Poison"] + assert "types" not in cut_proxy.data["pokemons"][0] - def test_pop_through_nested_list(self): - result = self.data.pop("team_sets[0][0].type") - assert result == "Fire" - assert "type" not in self.data.data["team_sets"][0][0] + def test_pop_through_nested_list(self, cut_proxy): + assert cut_proxy.pop("team_sets[0][0].types") == ["Fire"] + assert "types" not in cut_proxy.data["team_sets"][0][0] - def test_setdefault_on_list_item(self): - assert self.data.setdefault("pokemons[0].type[1]", "Funny") == "Poison" - def test_setdefault_on_list_item_through_nested_list(self): - result = self.data.setdefault("team_sets[0][0]", "MissingNo") +class TestSetitem: + def test_setitem(self, proxy): + proxy["trainer.badges.Boulder"] = False + assert proxy["trainer"]["badges"]["Boulder"] is False + + def test_set_a_value_through_list(self, cut_proxy): + cut_proxy["pokemons[0].category"] = "Onion" + assert cut_proxy.data["pokemons"][0]["category"] == "Onion" + + def test_set_a_value_through_nested_list(self, cut_proxy): + cut_proxy["team_sets[0][0].category"] = "Lighter" + assert cut_proxy.data["team_sets"][0][0]["category"] == "Lighter" + + def test_set_a_value_through_a_nested_item(self, cut_proxy): + cut_proxy["pokemons[0].types[1]"] = "Fire" + assert cut_proxy["pokemons"][0]["types"][1] == "Fire" + + def test_set_value_on_an_undefined_list_item(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy["pokemons[42].types[1]"] = "Fire" + + expected = 'Index out of range in key "pokemons[42].types[1]".' + assert str(error) == expected + + def test_set_a_value_in_nested_list_item(self, cut_proxy): + cut_proxy["team_sets[0][2]"] = CHARMANDER + assert cut_proxy.data["team_sets"][0][2] == CHARMANDER + + def test_settitem_nested_undefined_list_item_raises_exception(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy["team_sets[0][42]"] = CHARMANDER + + expected_error = IndexError( + "Cannot access index '42' in path 'team_sets[0][42]', " + "because of error: IndexError('list assignment index out of range',)." + ) + assert str(error.value) == str(expected_error) + + +class TestSetdefault: + def test_setdefault(self, proxy): + assert proxy.setdefault("trainer", "Not Found") == ASH + + def test_with_nested_key(self, proxy): + result = proxy.setdefault("trainer.badges.Boulder", "Undefined") + assert result is True + + def test_when_key_is_undefined(self, proxy): + result = proxy.setdefault("trainer.bicycle.size", 180) + assert result == 180 + assert proxy.data["trainer"]["bicycle"]["size"] == 180 + + def test_on_list_item(self, cut_proxy): + assert cut_proxy.setdefault("pokemons[0].types[1]", "Funny") == "Poison" + + def test_on_list_item_through_nested_list(self, cut_proxy): + result = cut_proxy.setdefault("team_sets[0][0]", "MissingNo") assert result == CHARMANDER - def test_setdefault_on_undefined_first_item(self): - try: - self.data.setdefault("pokemons[666]", BULBASAUR) - self.fail() - except IndexError as error: + def test_on_undefined_first_item(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy.setdefault("pokemons[666]", BULBASAUR) + assert str(error) == 'Index out of range in key "pokemons[666]".' - def test_setdefault_on_undefined_list_item(self): - try: - self.data.setdefault("pokemons[0].type[2]", "Funny") - self.fail() - except IndexError as error: - expected = 'Index out of range in key "pokemons[0].type[2]".' + def test_on_undefined_list_item(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy.setdefault("pokemons[0].types[2]", "Funny") + + expected = 'Index out of range in key "pokemons[0].types[2]".' assert str(error) == expected - def test_setdefault_through_list(self): - assert "sex" not in self.data["pokemons"][0] - assert self.data.setdefault("pokemons[0].sex", "Unknown") == "Unknown" - assert self.data.data["pokemons"][0]["sex"] == "Unknown" + def test_through_list(self, cut_proxy): + assert cut_proxy.setdefault("pokemons[0].sex", "Unknown") == "Unknown" + assert cut_proxy.data["pokemons"][0]["sex"] == "Unknown" + + def test_undefined_key_through_list(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy.setdefault("pokemons[42].sex", "Unknown") - def test_setdefault_undefined_key_through_list(self): - try: - self.data.setdefault("pokemons[42].sex", "Unknown") - self.fail() - except IndexError as error: expected = 'Index out of range in key "pokemons[42].sex".' assert str(error) == expected - def test_setdefault_through_nested_list(self): - assert "sex" not in self.data["team_sets"][0][0] - result = self.data.setdefault("team_sets[0][0].sex", "Unknown") + def test_through_nested_list(self, cut_proxy): + result = cut_proxy.setdefault("team_sets[0][0].sex", "Unknown") assert result == "Unknown" - assert self.data.data["team_sets"][0][0]["sex"] == "Unknown" - - def test_setdefault_undefined_first_item_in_nested_list_item(self): - try: - self.data.setdefault("team_sets[42][0]", BULBASAUR) - self.fail() - except IndexError as error: - expected = 'Index out of range in key "team_sets[42][0]".' - assert str(error) == expected + assert cut_proxy.data["team_sets"][0][0]["sex"] == "Unknown" - def test_setdefault_undefined_nested_list_item(self): - try: - self.data.setdefault("team_sets[0][42]", BULBASAUR) - self.failt() - except IndexError as error: - expected = 'Index out of range in key "team_sets[0][42]".' - assert str(error) == expected + def test_undefined_list_item(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy.setdefault("pokemons[42]", BULBASAUR) - def test_setitem_through_list(self): - self.data["pokemons[0].category"] = "Onion" - assert self.data.data["pokemons"][0]["category"] == "Onion" - - def test_setitem_through_nested_list(self): - self.data["team_sets[0][0].category"] = "Lighter" - assert self.data.data["team_sets"][0][0]["category"] == "Lighter" - - def test_setitem_list_item(self): - assert self.data.data["pokemons"][0]["type"][1] == "Poison" - self.data["pokemons[0].type[1]"] = "Fire" - assert self.data["pokemons"][0]["type"][1] == "Fire" - - def test_setitem_undefined_list_item(self): - try: - self.data["pokemons[42].type[1]"] = "Fire" - self.fail() - except IndexError as error: - expected = 'Index out of range in key "pokemons[42].type[1]".' + expected_error = IndexError( + "Cannot access index '42' in path 'pokemons[42]', " + "because of error: IndexError('list index out of range',)." + ) assert str(error) == expected - def test_settitem_nested_list_item(self): - assert self.data.data["team_sets"][0][2] == BULBASAUR - self.data["team_sets[0][2]"] = CHARMANDER - assert self.data.data["team_sets"][0][2] == CHARMANDER - - def test_settitem_nested_undefined_list_item(self): - try: - self.data["team_sets[42][0]"] = CHARMANDER - self.fail() - except IndexError as error: - expected = 'Index out of range in key "team_sets[42][0]".' + def test_undefined_nested_list_item(self, cut_proxy): + with pytest.raises(IndexError) as error: + cut_proxy.setdefault("team_sets[0][42]", BULBASAUR) + + expected_error = IndexError( + "Cannot access index '42' in path 'team_sets[0][42]', " + "because of error: IndexError('list index out of range',)." + ) assert str(error) == expected - def test_update_from_dict_through_list(self): - assert self.data.data["pokemons"][1]["category"] == "Lizard" + +class TestUpdate: + def test_from_dict(self, proxy): + from_other = {"trainer.friend": "Brock"} + proxy.update(from_other) + assert proxy["trainer"]["friend"] == "Brock" + + def test_from_dict_and_keyword_args(self, proxy): + from_other = {"trainer.friend": "Brock"} + proxy.update(from_other, game="Pokemon Blue") + assert proxy["trainer"]["friend"] == "Brock" + assert proxy["game"] == "Pokemon Blue" + + def test_from_list(self, proxy): + from_other = [("trainer.friend", "Brock")] + proxy.update(from_other) + assert proxy["trainer"]["friend"] == "Brock" + + def test_from_list_and_keyword_args(self, proxy): + from_other = [("trainer.friend", "Brock")] + proxy.update(from_other, game="Pokemon Blue") + assert proxy["trainer"]["friend"] == "Brock" + assert proxy["game"] == "Pokemon Blue" + + def test_update_from_dict_through_list(self, cut_proxy): from_other = {"pokemons[1].category": "Lighter"} - self.data.update(from_other) - assert self.data.data["pokemons"][1]["category"] == "Lighter" + cut_proxy.update(from_other) + assert cut_proxy.data["pokemons"][1]["category"] == "Lighter" - def test_update_from_dict_through_nested_list(self): - assert self.data.data["team_sets"][0][0]["category"] == "Lizard" + def test_update_from_dict_through_nested_list(self, cut_proxy): from_other = {"team_sets[0][0].category": "Lighter"} - self.data.update(from_other) - assert self.data.data["team_sets"][0][0]["category"] == "Lighter" + cut_proxy.update(from_other) + assert cut_proxy.data["team_sets"][0][0]["category"] == "Lighter" - def test_update_list_item_from_dict(self): - assert self.data["pokemons"][0]["type"][1] == "Poison" - from_other = {"pokemons[0].type[1]": "Onion"} - self.data.update(from_other) - assert self.data["pokemons"][0]["type"][1] == "Onion" + def test_update_list_item_from_dict(self, cut_proxy): + from_other = {"pokemons[0].types[1]": "Onion"} + cut_proxy.update(from_other) + assert cut_proxy["pokemons"][0]["types"][1] == "Onion" - def test_update_nested_list_item_from_dict(self): - assert self.data.data["team_sets"][0][0] == CHARMANDER + def test_update_nested_list_item_from_dict(self, cut_proxy): from_other = {"team_sets[0][0]": SQUIRTLE} - self.data.update(from_other) - assert self.data.data["team_sets"][0][0] == SQUIRTLE + cut_proxy.update(from_other) + assert cut_proxy.data["team_sets"][0][0] == SQUIRTLE - def test_update_from_dict_and_keyword_args_through_list(self): - assert "game" not in self.data - assert self.data["pokemons"][1]["category"] == "Lizard" + def test_update_from_dict_and_keyword_args_through_list(self, cut_proxy): from_other = {"pokemons[1].category": "Lighter"} - self.data.update(from_other, game="Pokemon Blue") - assert self.data["pokemons"][1]["category"] == "Lighter" - assert self.data["game"] == "Pokemon Blue" + cut_proxy.update(from_other, game="Pokemon Blue") + assert cut_proxy["pokemons"][1]["category"] == "Lighter" + assert cut_proxy["game"] == "Pokemon Blue" - def test_update_from_list_through_list(self): - assert self.data["pokemons"][1]["category"] == "Lizard" + def test_update_from_list_through_list(self, cut_proxy): from_other = [("pokemons[1].category", "Lighter")] - self.data.update(from_other) - assert self.data["pokemons"][1]["category"] == "Lighter" + cut_proxy.update(from_other) + assert cut_proxy["pokemons"][1]["category"] == "Lighter" - def test_update_from_list_and_keyword_args_through_list(self): - assert "game" not in self.data - assert self.data["pokemons"][1]["category"] == "Lizard" + def test_update_from_list_and_keyword_args_through_list(self, cut_proxy): from_other = [("pokemons[1].category", "Lighter")] - self.data.update(from_other, game="Pokemon Blue") - assert self.data["pokemons"][1]["category"] == "Lighter" - assert self.data["game"] == "Pokemon Blue" - - -class TestCutWithOrderedDictPM(TestCutWithDictPM): - Dict = OrderedDict - Wrapper = Cut + cut_proxy.update(from_other, game="Pokemon Blue") + assert cut_proxy["pokemons"][1]["category"] == "Lighter" + assert cut_proxy["game"] == "Pokemon Blue" -class TestCutWithOrderedDictCLM(TestCutWithDictCLM): - Dict = OrderedDict - Wrapper = Cut - - -class TestCutWithDefaultDictPM(TestCutWithDictPM): - Dict = defaultdict - Wrapper = Cut - - def setup(self): - self.data = self.Wrapper(deepcopy(self.Dict(None, BASE))) +class TestAll: + def test_all_returns_generator(self, proxy): + result = proxy.all("pokemons") + assert isinstance(result, GeneratorType) is True + def test_generated_values(self, proxy): + values = [item.data for item in proxy.all("pokemons")] + assert values == [BULBASAUR, CHARMANDER, SQUIRTLE] -class TestCutWithDefaultDictCLM(TestCutWithDictCLM): - Dict = defaultdict - Wrapper = Cut + def test_all_with_custom_separator(self, proxy): + proxy.sep = "/" + separators = [item.sep for item in proxy.all("pokemons")] - def setup(self): - self.data = self.Wrapper(deepcopy(self.Dict(None, BASE))) + assert all(sep == "/" for sep in separators) From e1b4020a2e69132863a9727356445fb6948dc078 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 7 Aug 2019 13:20:51 +0200 Subject: [PATCH 11/17] :see_no_evil: Ignore VSCode settings. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 10b2523..e6bfa66 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ htmlcov/* # Tests reddit.json .cache/* + +# IDE +.vscode/ From f1aeeb21580f9581f5d93659e7f6e778b79eafd8 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 7 Aug 2019 13:29:47 +0200 Subject: [PATCH 12/17] :rotating_light: Do not type check exception helpers. --- scalpl/scalpl.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scalpl/scalpl.py b/scalpl/scalpl.py index 17cd212..36e718d 100644 --- a/scalpl/scalpl.py +++ b/scalpl/scalpl.py @@ -18,18 +18,14 @@ TLightCut = TypeVar("TLightCut", bound="LightCut") -def key_error( - failing_key: str, original_path: str, raised_error: Exception -) -> KeyError: +def key_error(failing_key, original_path, raised_error): return KeyError( f"Cannot access key '{failing_key}' in path '{original_path}'," f" because of error: {repr(raised_error)}." ) -def index_error( - failing_key: str, original_path: str, raised_error: Exception -) -> IndexError: +def index_error(failing_key, original_path, raised_error): return IndexError( f"Cannot access index '{failing_key}' in path '{original_path}'," f" because of error: {repr(raised_error)}." @@ -245,12 +241,14 @@ def _traverse_list(self, parent, key, original_path: str): parent = parent[key] except KeyError as error: raise key_error(key, original_path, error) + try: for str_index in str_indexes[:-1]: index = int(str_index[:-1]) parent = parent[index] except IndexError as error: raise index_error(index, original_path, error) + try: last_index = int(str_indexes[-1][:-1]) except ValueError as error: From 31bfa25986c22a2e8f1b0e24f915a5751bd7d496 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 7 Aug 2019 13:34:09 +0200 Subject: [PATCH 13/17] :recycle: Unify the API by renaming all 'key' parameter to 'path'. --- scalpl/scalpl.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/scalpl/scalpl.py b/scalpl/scalpl.py index 36e718d..b87793f 100644 --- a/scalpl/scalpl.py +++ b/scalpl/scalpl.py @@ -55,8 +55,8 @@ def __init__(self, data: Optional[dict] = None, sep: str = ".") -> None: def __bool__(self) -> bool: return bool(self.data) - def __contains__(self, key: str) -> bool: - parent, last_key = self._traverse(self.data, key) + def __contains__(self, path: str) -> bool: + parent, last_key = self._traverse(self.data, path) try: parent[last_key] return True @@ -119,9 +119,9 @@ def _traverse(self, parent, path: str): return parent, last_key - def all(self: TLightCut, key: str) -> Iterator[TLightCut]: + def all(self: TLightCut, path: str) -> Iterator[TLightCut]: """Wrap each item of an Iterable.""" - items = self[key] + items = self[path] cls = self.__class__ return (cls(_dict, self.sep) for _dict in items) @@ -167,9 +167,9 @@ def pop(self, path: str, default: Any = None) -> Any: def popitem(self) -> Any: return self.data.popitem() - def setdefault(self, key: str, default: Any = None) -> Any: + def setdefault(self, path: str, default: Any = None) -> Any: parent = self.data - *parent_keys, last_key = key.split(self.sep) + *parent_keys, last_key = path.split(self.sep) if parent_keys: for key in parent_keys: parent = parent.setdefault(key, {}) @@ -206,13 +206,13 @@ class Cut(LightCut): __slots__ = () - def setdefault(self, key: str, default: Optional[Any] = None) -> Any: + def setdefault(self, path: str, default: Optional[Any] = None) -> Any: parent = self.data - *parent_keys, last_key = key.split(self.sep) + *parent_keys, last_key = path.split(self.sep) if parent_keys: for _key in parent_keys: - parent, _key = self._traverse_list(parent, _key, key) + parent, _key = self._traverse_list(parent, _key, path) try: parent = parent[_key] except KeyError: @@ -220,9 +220,9 @@ def setdefault(self, key: str, default: Optional[Any] = None) -> Any: parent[_key] = child parent = child except IndexError as error: - raise index_error(_key, key, error) + raise index_error(_key, path, error) - parent, last_key = self._traverse_list(parent, last_key, key) + parent, last_key = self._traverse_list(parent, last_key, path) try: return parent[last_key] @@ -230,7 +230,7 @@ def setdefault(self, key: str, default: Optional[Any] = None) -> Any: parent[last_key] = default return default except IndexError as error: - raise index_error(last_key, key, error) + raise index_error(last_key, path, error) def _traverse_list(self, parent, key, original_path: str): key, *str_indexes = key.split("[") From 76d47b27c74cb5c8f6caa5a945e9bec8643433d7 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 7 Aug 2019 13:50:36 +0200 Subject: [PATCH 14/17] :fire: Remove the LightCut class to simplify the API. --- scalpl/__init__.py | 2 +- scalpl/scalpl.py | 88 ++++--------- tests/fixtures.py | 37 +----- tests/tests.py | 299 ++++++++++++++++++++------------------------- 4 files changed, 165 insertions(+), 261 deletions(-) diff --git a/scalpl/__init__.py b/scalpl/__init__.py index 3cc5a70..ae43857 100644 --- a/scalpl/__init__.py +++ b/scalpl/__init__.py @@ -1 +1 @@ -from .scalpl import LightCut, Cut +from .scalpl import Cut diff --git a/scalpl/scalpl.py b/scalpl/scalpl.py index b87793f..36131e2 100644 --- a/scalpl/scalpl.py +++ b/scalpl/scalpl.py @@ -15,9 +15,6 @@ ) -TLightCut = TypeVar("TLightCut", bound="LightCut") - - def key_error(failing_key, original_path, raised_error): return KeyError( f"Cannot access key '{failing_key}' in path '{original_path}'," @@ -32,18 +29,21 @@ def index_error(failing_key, original_path, raised_error): ) -class LightCut: +TCut = TypeVar("TCut", bound="Cut") + + +class Cut: """ - LightCut is a simple wrapper over the built-in dict class. + Cut is a simple wrapper over the built-in dict class. It enables the standard dict API to operate on nested dictionnaries - by using dot-separated string keys. + and cut accross list item by using dot-separated string keys. ex: query = {...} # Any dict structure proxy = Cut(query) - proxy['pokemons.charmander.level'] - proxy['pokemons.charmander.level'] = 666 + proxy['pokemons[0].level'] + proxy['pokemons[0].level'] = 666 """ __slots__ = ("data", "sep") @@ -106,20 +106,7 @@ def __setitem__(self, path: str, value: Any) -> None: def __str__(self) -> str: return str(self.data) - def _traverse(self, parent, path: str): - *parent_keys, last_key = path.split(self.sep) - if len(parent_keys) == 0: - return parent, last_key - - try: - for sub_key in parent_keys: - parent = parent[sub_key] - except KeyError as error: - raise key_error(sub_key, path, error) - - return parent, last_key - - def all(self: TLightCut, path: str) -> Iterator[TLightCut]: + def all(self: TCut, path: str) -> Iterator[TCut]: """Wrap each item of an Iterable.""" items = self[path] cls = self.__class__ @@ -133,8 +120,8 @@ def copy(self) -> dict: @classmethod def fromkeys( - cls: Type[TLightCut], seq: Iterable, value: Optional[Iterable] = None - ) -> TLightCut: + cls: Type[TCut], seq: Iterable, value: Optional[Iterable] = None + ) -> TCut: return cls(dict.fromkeys(seq, value)) def get(self, path: str, default: Optional[Any] = None) -> Any: @@ -167,45 +154,6 @@ def pop(self, path: str, default: Any = None) -> Any: def popitem(self) -> Any: return self.data.popitem() - def setdefault(self, path: str, default: Any = None) -> Any: - parent = self.data - *parent_keys, last_key = path.split(self.sep) - if parent_keys: - for key in parent_keys: - parent = parent.setdefault(key, {}) - return parent.setdefault(last_key, default) - - def update(self, data=None, **kwargs): - data = data or {} - try: - data.update(kwargs) - pairs = data.items() - except AttributeError: - pairs = chain(data, kwargs.items()) - - for key, value in pairs: - self.__setitem__(key, value) - - def values(self) -> ValuesView: - return self.data.values() - - -class Cut(LightCut): - """ - Cut is a simple wrapper over the built-in dict class. - - It enables the standard dict API to operate on nested dictionnaries - and cut accross list item by using dot-separated string keys. - - ex: - query = {...} # Any dict structure - proxy = Cut(query) - proxy['pokemons[0].level'] - proxy['pokemons[0].level'] = 666 - """ - - __slots__ = () - def setdefault(self, path: str, default: Optional[Any] = None) -> Any: parent = self.data *parent_keys, last_key = path.split(self.sep) @@ -232,6 +180,20 @@ def setdefault(self, path: str, default: Optional[Any] = None) -> Any: except IndexError as error: raise index_error(last_key, path, error) + def update(self, data=None, **kwargs): + data = data or {} + try: + data.update(kwargs) + pairs = data.items() + except AttributeError: + pairs = chain(data, kwargs.items()) + + for key, value in pairs: + self.__setitem__(key, value) + + def values(self) -> ValuesView: + return self.data.values() + def _traverse_list(self, parent, key, original_path: str): key, *str_indexes = key.split("[") if not str_indexes: diff --git a/tests/fixtures.py b/tests/fixtures.py index 318f13c..0110c18 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import partial import pytest -from scalpl import Cut, LightCut +from scalpl import Cut ASH = { @@ -42,47 +42,18 @@ BASE = {"trainer": ASH, "pokemons": POKEMONS, "team_sets": TEAM_SETS} -@pytest.fixture( - params=[ - (LightCut, dict), - (LightCut, OrderedDict), - (LightCut, partial(defaultdict, None)), - (Cut, dict), - (Cut, OrderedDict), - (Cut, partial(defaultdict, None)), - ] -) -def proxy(request): - scalpl_class = request.param[0] - underlying_dict_class = request.param[1] - return scalpl_class(underlying_dict_class(deepcopy(BASE))) - - @pytest.fixture( params=[(Cut, dict), (Cut, OrderedDict), (Cut, partial(defaultdict, None))] ) -def cut_proxy(request): - scalpl_class = request.param[0] - underlying_dict_class = request.param[1] - return scalpl_class(underlying_dict_class(deepcopy(BASE))) - - -@pytest.fixture( - params=[ - (LightCut, dict), - (LightCut, OrderedDict), - (LightCut, partial(defaultdict, None)), - ] -) -def lightcut_proxy(request): +def proxy(request): scalpl_class = request.param[0] underlying_dict_class = request.param[1] return scalpl_class(underlying_dict_class(deepcopy(BASE))) -@pytest.fixture(params=[LightCut, Cut]) +@pytest.fixture() def scalpl_class(request): - return request.param + return Cut @pytest.fixture(params=[dict, OrderedDict, partial(defaultdict, None)]) diff --git a/tests/tests.py b/tests/tests.py index b882737..96124cc 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -8,8 +8,6 @@ FIRST_SET, SECOND_SET, BASE, - cut_proxy, - lightcut_proxy, proxy, scalpl_class, underlying_dict_class, @@ -18,52 +16,25 @@ from types import GeneratorType -class TestLightCutTraverse: - def test_traverse_single_key(self, lightcut_proxy): - parent, last_key = lightcut_proxy._traverse(ASH, "name") - assert parent == ASH - assert last_key == "name" - - def test_traverse_single_undefined_key(self, lightcut_proxy): - parent, last_key = lightcut_proxy._traverse(ASH, "undefined_key") - assert parent == ASH - assert last_key == "undefined_key" - - def test_traverse_nested_keys(self, lightcut_proxy): - parent, last_key = lightcut_proxy._traverse(ASH, "badges.Boulder") - assert parent == ASH["badges"] - assert last_key == "Boulder" - - def test_traverse_undefined_nested_keys(self, lightcut_proxy): - with pytest.raises(KeyError) as error: - parent, last_key = lightcut_proxy._traverse(ASH, "undefined_key.name") - - expected_error = KeyError( - "Cannot access key 'undefined_key' in path 'undefined_key.name', " - "because of error: KeyError('undefined_key',)." - ) - assert str(error.value) == str(expected_error) - - class TestCutTraverse: - def test_traverse_single_key(self, cut_proxy): - parent, last_key = cut_proxy._traverse(ASH, "name") + def test_traverse_single_key(self, proxy): + parent, last_key = proxy._traverse(ASH, "name") assert parent == ASH assert last_key == "name" - def test_traverse_single_undefined_key(self, cut_proxy): - parent, last_key = cut_proxy._traverse(ASH, "undefined_key") + def test_traverse_single_undefined_key(self, proxy): + parent, last_key = proxy._traverse(ASH, "undefined_key") assert parent == ASH assert last_key == "undefined_key" - def test_traverse_nested_keys(self, cut_proxy): - parent, last_key = cut_proxy._traverse(ASH, "badges.Boulder") + def test_traverse_nested_keys(self, proxy): + parent, last_key = proxy._traverse(ASH, "badges.Boulder") assert parent == ASH["badges"] assert last_key == "Boulder" - def test_traverse_undefined_nested_keys(self, cut_proxy): + def test_traverse_undefined_nested_keys(self, proxy): with pytest.raises(KeyError) as error: - parent, last_key = cut_proxy._traverse(ASH, "undefined_key.name") + parent, last_key = proxy._traverse(ASH, "undefined_key.name") expected_error = KeyError( "Cannot access key 'undefined_key' in path 'undefined_key.name', " @@ -71,23 +42,23 @@ def test_traverse_undefined_nested_keys(self, cut_proxy): ) assert str(error.value) == str(expected_error) - def test_traverse_single_list_item(self, cut_proxy): + def test_traverse_single_list_item(self, proxy): data = {"types": ["Fire"]} - parent, last_key = cut_proxy._traverse(data, "types[0]") + parent, last_key = proxy._traverse(data, "types[0]") assert parent == data["types"] assert last_key == 0 - def test_traverse_undefined_list_item_do_not_throw_error(self, cut_proxy): + def test_traverse_undefined_list_item_do_not_throw_error(self, proxy): data = {"types": ["Fire"]} - parent, last_key = cut_proxy._traverse(data, "types[1]") + parent, last_key = proxy._traverse(data, "types[1]") assert parent == data["types"] assert last_key == 1 - def test_traverse_undefined_list_raises_exception(self, cut_proxy): + def test_traverse_undefined_list_raises_exception(self, proxy): data = {"types": ["Fire"]} with pytest.raises(KeyError) as error: - cut_proxy._traverse(data, "colors[0]") + proxy._traverse(data, "colors[0]") expected_error = KeyError( "Cannot access key 'colors' in path 'colors[0]', " @@ -95,17 +66,17 @@ def test_traverse_undefined_list_raises_exception(self, cut_proxy): ) assert str(error.value) == str(expected_error) - def test_traverse_nested_list_item(self, cut_proxy): + def test_traverse_nested_list_item(self, proxy): data = {"types": [["Fire", "Water"]]} - parent, last_key = cut_proxy._traverse(BULBASAUR, "types[0][1]") + parent, last_key = proxy._traverse(BULBASAUR, "types[0][1]") assert parent == BULBASAUR["types"][0] assert last_key == 1 - def test_traverse_undefined_nested_list_item_raises_exception(self, cut_proxy): + def test_traverse_undefined_nested_list_item_raises_exception(self, proxy): data = {"types": [["Fire", "Water"]]} with pytest.raises(IndexError) as error: - cut_proxy._traverse(BULBASAUR, "types[42][0]") + proxy._traverse(BULBASAUR, "types[42][0]") expected_error = IndexError( "Cannot access index '42' in path 'types[42][0]', " @@ -113,11 +84,11 @@ def test_traverse_undefined_nested_list_item_raises_exception(self, cut_proxy): ) assert str(error.value) == str(expected_error) - def test_traverse_with_non_integer_index_raises_exception(self, cut_proxy): + def test_traverse_with_non_integer_index_raises_exception(self, proxy): data = {"types": [["Fire", "Water"]]} with pytest.raises(IndexError) as error: - cut_proxy._traverse(BULBASAUR, "types[toto]") + proxy._traverse(BULBASAUR, "types[toto]") expected_error = IndexError( "Cannot access index 'toto' in path 'types[toto]', " @@ -237,13 +208,13 @@ def test_on_undefined_key(self, proxy): ) assert str(error.value) == str(expected_error) - def test_through_list(self, cut_proxy): - del cut_proxy["pokemons[1].category"] - assert "category" not in cut_proxy.data["pokemons"][1] + def test_through_list(self, proxy): + del proxy["pokemons[1].category"] + assert "category" not in proxy.data["pokemons"][1] - def test_undefined_key_through_list_(self, cut_proxy): + def test_undefined_key_through_list_(self, proxy): with pytest.raises(KeyError) as error: - del cut_proxy["pokemons[1].has_been_seen"] + del proxy["pokemons[1].has_been_seen"] expected_error = KeyError( "Cannot access key 'has_been_seen' in path 'pokemons[1].has_been_seen', " @@ -251,13 +222,13 @@ def test_undefined_key_through_list_(self, cut_proxy): ) assert str(error.value) == str(expected_error) - def test_through_nested_list(self, cut_proxy): - del cut_proxy["team_sets[0][0].name"] - assert "name" not in cut_proxy.data["team_sets"][0][0] + def test_through_nested_list(self, proxy): + del proxy["team_sets[0][0].name"] + assert "name" not in proxy.data["team_sets"][0][0] - def test_undefined_key_through_nested_list(self, cut_proxy): + def test_undefined_key_through_nested_list(self, proxy): with pytest.raises(KeyError) as error: - del cut_proxy["team_sets[0][0].can_fly"] + del proxy["team_sets[0][0].can_fly"] expected_error = KeyError( "Cannot access key 'can_fly' in path 'team_sets[0][0].can_fly', " @@ -265,13 +236,13 @@ def test_undefined_key_through_nested_list(self, cut_proxy): ) assert str(error.value) == str(expected_error) - def test_list_item(self, cut_proxy): - del cut_proxy["pokemons[0].types[1]"] - assert len(cut_proxy.data["pokemons"][0]["types"]) == 1 + def test_list_item(self, proxy): + del proxy["pokemons[0].types[1]"] + assert len(proxy.data["pokemons"][0]["types"]) == 1 - def test_undefined_list_item(self, cut_proxy): + def test_undefined_list_item(self, proxy): with pytest.raises(IndexError) as error: - del cut_proxy["pokemons[0].types[42]"] + del proxy["pokemons[0].types[42]"] expected_error = IndexError( "Cannot access index '42' in path 'pokemons[0].types[42]', " @@ -279,9 +250,9 @@ def test_undefined_list_item(self, cut_proxy): ) assert str(error.value) == str(expected_error) - def test_undefined_list_index(self, cut_proxy): + def test_undefined_list_index(self, proxy): with pytest.raises(IndexError) as error: - del cut_proxy["pokemons[42].types[1]"] + del proxy["pokemons[42].types[1]"] expected = IndexError( "Cannot access index '42' in path 'pokemons[42].types[1]', " @@ -289,9 +260,9 @@ def test_undefined_list_index(self, cut_proxy): ) assert str(error.value) == str(expected) - def test_list_item_through_nested_list(self, cut_proxy): - del cut_proxy["team_sets[0][0]"] - assert len(cut_proxy.data["team_sets"][0]) == 2 + def test_list_item_through_nested_list(self, proxy): + del proxy["team_sets[0][0]"] + assert len(proxy.data["team_sets"][0]) == 2 class TestGet: @@ -315,18 +286,18 @@ def test_get_undefined_key_when_default_is_provided(self, proxy): assert proxy.get("trainer.hometown", "Pallet Town") == "Pallet Town" assert proxy.get("trainer.badges.Thunder", False) is False - def test_get_through_list(self, cut_proxy): - assert cut_proxy.get("pokemons[0].types[1]") == "Poison" + def test_get_through_list(self, proxy): + assert proxy.get("pokemons[0].types[1]") == "Poison" - def test_get_through_nested_list(self, cut_proxy): - result = cut_proxy.get("team_sets[0][0].name", "Unknwown") + def test_get_through_nested_list(self, proxy): + result = proxy.get("team_sets[0][0].name", "Unknwown") assert result == "Charmander" - def test_get_undefined_key_through_list(self, cut_proxy): - assert cut_proxy.get("pokemons[0].sex", "Unknown") == "Unknown" + def test_get_undefined_key_through_list(self, proxy): + assert proxy.get("pokemons[0].sex", "Unknown") == "Unknown" - def test_get_undefined_list_item(self, cut_proxy): - assert cut_proxy.get("pokemons[0].types[42]", "Unknown") == "Unknown" + def test_get_undefined_list_item(self, proxy): + assert proxy.get("pokemons[0].types[42]", "Unknown") == "Unknown" class TestGetitem: @@ -351,15 +322,15 @@ def test_getitem_with_custom_separator(self, proxy): assert proxy["trainer+badges"] == {"Boulder": True, "Cascade": False} assert proxy["trainer+badges+Boulder"] is True - def test_getitem_through_list(self, cut_proxy): - assert cut_proxy["pokemons[0].types[1]"] == "Poison" + def test_getitem_through_list(self, proxy): + assert proxy["pokemons[0].types[1]"] == "Poison" - def test_getitem_through_nested_list(self, cut_proxy): - assert cut_proxy["team_sets[0][0].name"] == "Charmander" + def test_getitem_through_nested_list(self, proxy): + assert proxy["team_sets[0][0].name"] == "Charmander" - def test_getitem_through_undefined_list_item_raises_exception(self, cut_proxy): + def test_getitem_through_undefined_list_item_raises_exception(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy["pokemons[42].types[1]"] + proxy["pokemons[42].types[1]"] expected = IndexError( "Cannot access index '42' in path 'pokemons[42].types[1]', " @@ -367,9 +338,9 @@ def test_getitem_through_undefined_list_item_raises_exception(self, cut_proxy): ) assert str(error.value) == str(expected) - def test_getitem_through_undefined_list_item_raises_exception(self, cut_proxy): + def test_getitem_through_undefined_list_item_raises_exception(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy["pokemons[0].types[42]"] + proxy["pokemons[0].types[42]"] expected = IndexError( "Cannot access index '42' in path 'pokemons[0].types[42]', " @@ -394,15 +365,15 @@ def test_on_undefined_nested_key_raises_exception(self, proxy): ) assert str(error.value) == str(expected) - def test_through_list(self, cut_proxy): - assert "pokemons[0].types[1]" in cut_proxy + def test_through_list(self, proxy): + assert "pokemons[0].types[1]" in proxy - def test_through_nested_list(self, cut_proxy): - assert "team_sets[0][0].name" in cut_proxy + def test_through_nested_list(self, proxy): + assert "team_sets[0][0].name" in proxy - def test_through_undefined_list_item(self, cut_proxy): + def test_through_undefined_list_item(self, proxy): with pytest.raises(IndexError) as error: - "pokemons[42].favorite_meal" in cut_proxy + "pokemons[42].favorite_meal" in proxy expected = IndexError( "Cannot access index '42' in path 'pokemons[42].favorite_meal', " @@ -434,9 +405,9 @@ def test_when_key_is_undefined(self, proxy): assert str(error.value) == str(expected_error) - def test_when_index_is_undefined(self, cut_proxy): + def test_when_index_is_undefined(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy.pop("pokemons[42]") + proxy.pop("pokemons[42]") expected_error = IndexError( "Cannot access index '42' in path 'pokemons[42]', " @@ -444,21 +415,21 @@ def test_when_index_is_undefined(self, cut_proxy): ) assert str(error.value) == str(expected_error) - def test_pop_list_item(self, cut_proxy): - assert cut_proxy.pop("pokemons[0].types[1]") == "Poison" - assert len(cut_proxy.data["pokemons"][0]["types"]) == 1 + def test_pop_list_item(self, proxy): + assert proxy.pop("pokemons[0].types[1]") == "Poison" + assert len(proxy.data["pokemons"][0]["types"]) == 1 - def test_pop_list_item_through_nested_list(self, cut_proxy): - assert cut_proxy.pop("team_sets[0][0]") == CHARMANDER - assert len(cut_proxy.data["team_sets"][0]) == 2 + def test_pop_list_item_through_nested_list(self, proxy): + assert proxy.pop("team_sets[0][0]") == CHARMANDER + assert len(proxy.data["team_sets"][0]) == 2 - def test_pop_through_list(self, cut_proxy): - assert cut_proxy.pop("pokemons[0].types") == ["Grass", "Poison"] - assert "types" not in cut_proxy.data["pokemons"][0] + def test_pop_through_list(self, proxy): + assert proxy.pop("pokemons[0].types") == ["Grass", "Poison"] + assert "types" not in proxy.data["pokemons"][0] - def test_pop_through_nested_list(self, cut_proxy): - assert cut_proxy.pop("team_sets[0][0].types") == ["Fire"] - assert "types" not in cut_proxy.data["team_sets"][0][0] + def test_pop_through_nested_list(self, proxy): + assert proxy.pop("team_sets[0][0].types") == ["Fire"] + assert "types" not in proxy.data["team_sets"][0][0] class TestSetitem: @@ -466,32 +437,32 @@ def test_setitem(self, proxy): proxy["trainer.badges.Boulder"] = False assert proxy["trainer"]["badges"]["Boulder"] is False - def test_set_a_value_through_list(self, cut_proxy): - cut_proxy["pokemons[0].category"] = "Onion" - assert cut_proxy.data["pokemons"][0]["category"] == "Onion" + def test_set_a_value_through_list(self, proxy): + proxy["pokemons[0].category"] = "Onion" + assert proxy.data["pokemons"][0]["category"] == "Onion" - def test_set_a_value_through_nested_list(self, cut_proxy): - cut_proxy["team_sets[0][0].category"] = "Lighter" - assert cut_proxy.data["team_sets"][0][0]["category"] == "Lighter" + def test_set_a_value_through_nested_list(self, proxy): + proxy["team_sets[0][0].category"] = "Lighter" + assert proxy.data["team_sets"][0][0]["category"] == "Lighter" - def test_set_a_value_through_a_nested_item(self, cut_proxy): - cut_proxy["pokemons[0].types[1]"] = "Fire" - assert cut_proxy["pokemons"][0]["types"][1] == "Fire" + def test_set_a_value_through_a_nested_item(self, proxy): + proxy["pokemons[0].types[1]"] = "Fire" + assert proxy["pokemons"][0]["types"][1] == "Fire" - def test_set_value_on_an_undefined_list_item(self, cut_proxy): + def test_set_value_on_an_undefined_list_item(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy["pokemons[42].types[1]"] = "Fire" + proxy["pokemons[42].types[1]"] = "Fire" expected = 'Index out of range in key "pokemons[42].types[1]".' assert str(error) == expected - def test_set_a_value_in_nested_list_item(self, cut_proxy): - cut_proxy["team_sets[0][2]"] = CHARMANDER - assert cut_proxy.data["team_sets"][0][2] == CHARMANDER + def test_set_a_value_in_nested_list_item(self, proxy): + proxy["team_sets[0][2]"] = CHARMANDER + assert proxy.data["team_sets"][0][2] == CHARMANDER - def test_settitem_nested_undefined_list_item_raises_exception(self, cut_proxy): + def test_settitem_nested_undefined_list_item_raises_exception(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy["team_sets[0][42]"] = CHARMANDER + proxy["team_sets[0][42]"] = CHARMANDER expected_error = IndexError( "Cannot access index '42' in path 'team_sets[0][42]', " @@ -513,45 +484,45 @@ def test_when_key_is_undefined(self, proxy): assert result == 180 assert proxy.data["trainer"]["bicycle"]["size"] == 180 - def test_on_list_item(self, cut_proxy): - assert cut_proxy.setdefault("pokemons[0].types[1]", "Funny") == "Poison" + def test_on_list_item(self, proxy): + assert proxy.setdefault("pokemons[0].types[1]", "Funny") == "Poison" - def test_on_list_item_through_nested_list(self, cut_proxy): - result = cut_proxy.setdefault("team_sets[0][0]", "MissingNo") + def test_on_list_item_through_nested_list(self, proxy): + result = proxy.setdefault("team_sets[0][0]", "MissingNo") assert result == CHARMANDER - def test_on_undefined_first_item(self, cut_proxy): + def test_on_undefined_first_item(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy.setdefault("pokemons[666]", BULBASAUR) + proxy.setdefault("pokemons[666]", BULBASAUR) assert str(error) == 'Index out of range in key "pokemons[666]".' - def test_on_undefined_list_item(self, cut_proxy): + def test_on_undefined_list_item(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy.setdefault("pokemons[0].types[2]", "Funny") + proxy.setdefault("pokemons[0].types[2]", "Funny") expected = 'Index out of range in key "pokemons[0].types[2]".' assert str(error) == expected - def test_through_list(self, cut_proxy): - assert cut_proxy.setdefault("pokemons[0].sex", "Unknown") == "Unknown" - assert cut_proxy.data["pokemons"][0]["sex"] == "Unknown" + def test_through_list(self, proxy): + assert proxy.setdefault("pokemons[0].sex", "Unknown") == "Unknown" + assert proxy.data["pokemons"][0]["sex"] == "Unknown" - def test_undefined_key_through_list(self, cut_proxy): + def test_undefined_key_through_list(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy.setdefault("pokemons[42].sex", "Unknown") + proxy.setdefault("pokemons[42].sex", "Unknown") expected = 'Index out of range in key "pokemons[42].sex".' assert str(error) == expected - def test_through_nested_list(self, cut_proxy): - result = cut_proxy.setdefault("team_sets[0][0].sex", "Unknown") + def test_through_nested_list(self, proxy): + result = proxy.setdefault("team_sets[0][0].sex", "Unknown") assert result == "Unknown" - assert cut_proxy.data["team_sets"][0][0]["sex"] == "Unknown" + assert proxy.data["team_sets"][0][0]["sex"] == "Unknown" - def test_undefined_list_item(self, cut_proxy): + def test_undefined_list_item(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy.setdefault("pokemons[42]", BULBASAUR) + proxy.setdefault("pokemons[42]", BULBASAUR) expected_error = IndexError( "Cannot access index '42' in path 'pokemons[42]', " @@ -559,9 +530,9 @@ def test_undefined_list_item(self, cut_proxy): ) assert str(error) == expected - def test_undefined_nested_list_item(self, cut_proxy): + def test_undefined_nested_list_item(self, proxy): with pytest.raises(IndexError) as error: - cut_proxy.setdefault("team_sets[0][42]", BULBASAUR) + proxy.setdefault("team_sets[0][42]", BULBASAUR) expected_error = IndexError( "Cannot access index '42' in path 'team_sets[0][42]', " @@ -593,42 +564,42 @@ def test_from_list_and_keyword_args(self, proxy): assert proxy["trainer"]["friend"] == "Brock" assert proxy["game"] == "Pokemon Blue" - def test_update_from_dict_through_list(self, cut_proxy): + def test_update_from_dict_through_list(self, proxy): from_other = {"pokemons[1].category": "Lighter"} - cut_proxy.update(from_other) - assert cut_proxy.data["pokemons"][1]["category"] == "Lighter" + proxy.update(from_other) + assert proxy.data["pokemons"][1]["category"] == "Lighter" - def test_update_from_dict_through_nested_list(self, cut_proxy): + def test_update_from_dict_through_nested_list(self, proxy): from_other = {"team_sets[0][0].category": "Lighter"} - cut_proxy.update(from_other) - assert cut_proxy.data["team_sets"][0][0]["category"] == "Lighter" + proxy.update(from_other) + assert proxy.data["team_sets"][0][0]["category"] == "Lighter" - def test_update_list_item_from_dict(self, cut_proxy): + def test_update_list_item_from_dict(self, proxy): from_other = {"pokemons[0].types[1]": "Onion"} - cut_proxy.update(from_other) - assert cut_proxy["pokemons"][0]["types"][1] == "Onion" + proxy.update(from_other) + assert proxy["pokemons"][0]["types"][1] == "Onion" - def test_update_nested_list_item_from_dict(self, cut_proxy): + def test_update_nested_list_item_from_dict(self, proxy): from_other = {"team_sets[0][0]": SQUIRTLE} - cut_proxy.update(from_other) - assert cut_proxy.data["team_sets"][0][0] == SQUIRTLE + proxy.update(from_other) + assert proxy.data["team_sets"][0][0] == SQUIRTLE - def test_update_from_dict_and_keyword_args_through_list(self, cut_proxy): + def test_update_from_dict_and_keyword_args_through_list(self, proxy): from_other = {"pokemons[1].category": "Lighter"} - cut_proxy.update(from_other, game="Pokemon Blue") - assert cut_proxy["pokemons"][1]["category"] == "Lighter" - assert cut_proxy["game"] == "Pokemon Blue" + proxy.update(from_other, game="Pokemon Blue") + assert proxy["pokemons"][1]["category"] == "Lighter" + assert proxy["game"] == "Pokemon Blue" - def test_update_from_list_through_list(self, cut_proxy): + def test_update_from_list_through_list(self, proxy): from_other = [("pokemons[1].category", "Lighter")] - cut_proxy.update(from_other) - assert cut_proxy["pokemons"][1]["category"] == "Lighter" + proxy.update(from_other) + assert proxy["pokemons"][1]["category"] == "Lighter" - def test_update_from_list_and_keyword_args_through_list(self, cut_proxy): + def test_update_from_list_and_keyword_args_through_list(self, proxy): from_other = [("pokemons[1].category", "Lighter")] - cut_proxy.update(from_other, game="Pokemon Blue") - assert cut_proxy["pokemons"][1]["category"] == "Lighter" - assert cut_proxy["game"] == "Pokemon Blue" + proxy.update(from_other, game="Pokemon Blue") + assert proxy["pokemons"][1]["category"] == "Lighter" + assert proxy["game"] == "Pokemon Blue" class TestAll: From 002491bf82b5dea1159297f1e78645e54fc93d5c Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 7 Aug 2019 13:56:38 +0200 Subject: [PATCH 15/17] :pencil: Remove mentions of the LightCut class. --- README.rst | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 6bb5c7a..1d6b28c 100644 --- a/README.rst +++ b/README.rst @@ -54,34 +54,28 @@ such as `Addict `_ or `Box `_ , but if you give **Scalpl** a try, you will find it: -* ⚡ Fast * 🚀 Powerful as the standard dict API +* ⚡ Lightweight * 👌 Well tested Installation ~~~~~~~~~~~~ -**Scalpl** is a Python3-only module that you can install via ``pip`` +**Scalpl** is a Python3 library that you can install via ``pip`` .. code:: sh pip3 install scalpl + Usage ~~~~~ -**Scalpl** provides two classes that can wrap around your dictionaries: - -- **LightCut**: a wrapper that handles operations on nested ``dict``. -- **Cut**: a wrapper that handles operations on nested ``dict`` and - that can cut accross ``list`` item. - -Usually, you will only need to use the ``Cut`` wrapper, but if you do -not need to operate through lists, you should work with the ``LightCut`` -wrapper as its computation overhead is a bit smaller. +**Scalpl** provides a simple classe named **Cut** that wraps around your dictionary +and handles operations on nested ``dict`` and that can cut accross ``list`` item. -These two wrappers strictly follow the standard ``dict`` +This wrapper strictly follow the standard ``dict`` `API `_, that means you can operate seamlessly on ``dict``, ``collections.defaultdict`` or ``collections.OrderedDict``. From d9e5d80c5bdd38cafc178e08a20aae3d0fee11d2 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 7 Aug 2019 14:27:29 +0200 Subject: [PATCH 16/17] :zap: Update the benchmark. --- README.rst | 60 +++++++++++++++-------------- benchmarks/perfomance_comparison.py | 41 ++++++++++++++++++-- 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index 1d6b28c..8d1c381 100644 --- a/README.rst +++ b/README.rst @@ -217,7 +217,7 @@ of `Scalpl `_ compared to `Addict `_ and the built-in ``dict``. It will summarize the *number of operations per second* that each library is -able to perform on the JSON dump of the `Python subreddit main page `_. +able to perform on a portion of the JSON dump of the `Python subreddit main page `_. You can run this benchmark on your machine with the following command: @@ -225,48 +225,49 @@ You can run this benchmark on your machine with the following command: Here are the results obtained on an Intel Core i5-7500U CPU (2.50GHz) with **Python 3.6.4**. -**Addict**:: - instanciate:-------- 18,485 ops per second. - get:---------------- 18,806 ops per second. - get through list:--- 18,599 ops per second. - set:---------------- 18,797 ops per second. - set through list:--- 18,129 ops per second. +**Addict** 2.2.1:: + instanciate:-------- 271,132 ops per second. + get:---------------- 276,090 ops per second. + get through list:--- 293,773 ops per second. + set:---------------- 300,324 ops per second. + set through list:--- 282,149 ops per second. -**Box**:: - instanciate:--------- 4,150,396 ops per second. - get:----------------- 1,424,529 ops per second. - get through list:---- 110,926 ops per second. - set:----------------- 1,332,435 ops per second. - set through list:---- 110,833 ops per second. +**Box** 3.4.2:: + instanciate:--------- 4,093,439 ops per second. + get:----------------- 957,069 ops per second. + get through list:---- 164,013 ops per second. + set:----------------- 900,466 ops per second. + set through list:---- 165,522 ops per second. -**Scalpl**:: - instanciate:-------- 136,517,371 ops per second. - get:---------------- 24,918,648 ops per second. - get through list:--- 12,624,630 ops per second. - set:---------------- 26,409,542 ops per second. - set through list:--- 13,765,265 ops per second. +**Scalpl** latest:: + + instanciate:-------- 183,879,865 ops per second. + get:---------------- 14,941,355 ops per second. + get through list:--- 14,175,349 ops per second. + set:---------------- 11,320,968 ops per second. + set through list:--- 11,956,001 ops per second. **dict**:: - instanciate:--------- 92,119,547 ops per second. - get:----------------- 186,290,996 ops per second. - get through list:---- 178,747,154 ops per second. - set:----------------- 159,224,669 ops per second. - set through list :--- 79,294,520 ops per second. + instanciate:--------- 37,816,714 ops per second. + get:----------------- 84,317,032 ops per second. + get through list:---- 62,480,474 ops per second. + set:----------------- 146,484,375 ops per second. + set through list :--- 122,473,974 ops per second. -As a conclusion and despite being ~10 times slower than the built-in -``dict``, **Scalpl** is ~20 times faster than Box on simple read/write -operations, and ~100 times faster when it traverse lists. **Scalpl** is -also ~1300 times faster than Addict. +As a conclusion and despite being an order of magniture slower than the built-in +``dict``, **Scalpl** is faster than Box and Addict by an order of magnitude for any operations. +Besides, the gap increase in favor of **Scalpl** when wrapping large dictionaries. -However, do not trust benchmarks and test it on a real use-case. +Keeping in mind that this benchmark may vary depending on your use-case, it is very unlikely that +**Scalpl** will become a bottleneck of your application. Frequently Asked Questions: @@ -289,6 +290,7 @@ Frequently Asked Questions: proxy = Cut(data) proxy['it works perfectly'] = 'fine' + How to Contribute ~~~~~~~~~~~~~~~~~ diff --git a/benchmarks/perfomance_comparison.py b/benchmarks/perfomance_comparison.py index e3a55ab..ffc2755 100644 --- a/benchmarks/perfomance_comparison.py +++ b/benchmarks/perfomance_comparison.py @@ -16,10 +16,43 @@ class TestDictPerformance(unittest.TestCase): dict wrapper regarding insertion and lookup. """ - # We use the JSON dump of the Python Reddit page. - # We only collect it once. - with open('benchmarks/reddit.json', 'r') as f: - PYTHON_REDDIT = json.loads(f.read()) + # We use a portion of the JSON dump of the Python Reddit page. + + PYTHON_REDDIT = { + "kind": "Listing", + "data": { + "modhash": "", + "dist": 27, + "children": [ + { + "kind": "t3", + "data": { + "approved_at_utc": None, "subreddit": "Python", + "selftext": "Top Level comments must be **Job Opportunities.**\n\nPlease include **Location** or any other **Requirements** in your comment. If you require people to work on site in San Francisco, *you must note that in your post.* If you require an Engineering degree, *you must note that in your post*.\n\nPlease include as much information as possible.\n\nIf you are looking for jobs, send a PM to the poster.", + "author_fullname": "t2_628u", "saved": False, + "mod_reason_title": None, "gilded": 0, "clicked": False, "title": "r/Python Job Board", "link_flair_richtext": [], + "subreddit_name_prefixed": "r/Python", "hidden": False, "pwls": 6, "link_flair_css_class": None, "downs": 0, "hide_score": False, "name": "t3_cmq4jj", + "quarantine": False, "link_flair_text_color": "dark", "author_flair_background_color": "", "subreddit_type": "public", "ups": 11, "total_awards_received": 0, + "media_embed": {}, "author_flair_template_id": None, "is_original_content": False, "user_reports": [], "secure_media": None, + "is_reddit_media_domain": False, "is_meta": False, "category": None, "secure_media_embed": {}, "link_flair_text": None, "can_mod_post": False, + "score": 11, "approved_by": None, "thumbnail": "", "edited": False, "author_flair_css_class": "", "author_flair_richtext": [], "gildings": {}, + "content_categories": None, "is_self": True, "mod_note": None, "created": 1565124336.0, "link_flair_type": "text", "wls": 6, "banned_by": None, + "author_flair_type": "text", "domain": "self.Python", + "allow_live_comments": False, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>Top Level comments must be <strong>Job Opportunities.</strong></p>\n\n<p>Please include <strong>Location</strong> or any other <strong>Requirements</strong> in your comment. If you require people to work on site in San Francisco, <em>you must note that in your post.</em> If you require an Engineering degree, <em>you must note that in your post</em>.</p>\n\n<p>Please include as much information as possible.</p>\n\n<p>If you are looking for jobs, send a PM to the poster.</p>\n</div><!-- SC_ON -->", + "likes": None, "suggested_sort": None, "banned_at_utc": None, "view_count": None, "archived": False, "no_follow": False, "is_crosspostable": False, "pinned": False, + "over_18": False, "all_awardings": [], "media_only": False, "can_gild": False, "spoiler": False, "locked": False, "author_flair_text": "reticulated", + "visited": False, "num_reports": None, "distinguished": None, "subreddit_id": "t5_2qh0y", "mod_reason_by": None, "removal_reason": None, "link_flair_background_color": "", + "id": "cmq4jj", "is_robot_indexable": True, "report_reasons": None, "author": "aphoenix", "num_crossposts": 0, "num_comments": 2, "send_replies": False, "whitelist_status": "all_ads", + "contest_mode": False, "mod_reports": [], "author_patreon_flair": False, "author_flair_text_color": "dark", + "permalink": "/r/Python/comments/cmq4jj/rpython_job_board/", "parent_whitelist_status": "all_ads", "stickied": True, + "url": "https://www.reddit.com/r/Python/comments/cmq4jj/rpython_job_board/", "subreddit_subscribers": 399170, "created_utc": 1565095536.0, + "discussion_type": None, "media": None, "is_video": False + } + } + ] + } + } namespace = { 'Wrapper': dict From b9a4903387fc05ee698870cae013b6b75094ebe2 Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 7 Aug 2019 21:41:01 +0200 Subject: [PATCH 17/17] :tada: Bump to version 0.3.0 ! --- README.rst | 15 ++++++++------- scalpl/__init__.py | 2 ++ setup.py | 11 ++++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 8d1c381..32dbb2b 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ Scalpl .. image:: https://img.shields.io/badge/coverage-100%25-green.svg :target: # -.. image:: https://img.shields.io/badge/pypi-v0.2.6-blue.svg +.. image:: https://img.shields.io/badge/pypi-v0.3.0-blue.svg :target: https://pypi.python.org/pypi/scalpl/ .. image:: https://travis-ci.org/ducdetronquito/scalpl.svg?branch=master @@ -72,16 +72,16 @@ Installation Usage ~~~~~ -**Scalpl** provides a simple classe named **Cut** that wraps around your dictionary +**Scalpl** provides a simple class named **Cut** that wraps around your dictionary and handles operations on nested ``dict`` and that can cut accross ``list`` item. -This wrapper strictly follow the standard ``dict`` -`API `_, that +This wrapper strictly follows the standard ``dict`` +`API `_, which means you can operate seamlessly on ``dict``, -``collections.defaultdict`` or ``collections.OrderedDict``. - +``collections.defaultdict`` or ``collections.OrderedDict`` by using their methods +with dot-separated keys. -Let's see what it looks like with a toy dictionary ! 👇 +Let's see what it looks like with an example ! 👇 .. code:: python @@ -209,6 +209,7 @@ remove all keys. proxy.clear() + Benchmark ~~~~~~~~~ diff --git a/scalpl/__init__.py b/scalpl/__init__.py index ae43857..6c92532 100644 --- a/scalpl/__init__.py +++ b/scalpl/__init__.py @@ -1 +1,3 @@ from .scalpl import Cut + +__version__ = "0.3.0" diff --git a/setup.py b/setup.py index 957fba2..c38ac7c 100644 --- a/setup.py +++ b/setup.py @@ -5,17 +5,15 @@ setup( name="scalpl", - py_modules=["scalpl"], - version="0.2.6", - description=( - "A lightweight wrapper to operate on nested " "dictionaries seamlessly." - ), + packages=["scalpl"], + version="0.3.0", + description=("A lightweight wrapper to operate on nested dictionaries seamlessly."), long_description=readme, author="Guillaume Paulet", author_email="guillaume.paulet@giome.fr", license="Public Domain", url="https://github.com/ducdetronquito/scalpl", - download_url=("https://github.com/ducdetronquito/scalpl/archive/" "0.2.6.tar.gz"), + download_url=("https://github.com/ducdetronquito/scalpl/archive/" "0.3.0.tar.gz"), tests_require=[ "addict", "mypy", @@ -39,7 +37,6 @@ "wrapper", ], classifiers=[ - "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: Public Domain", "Operating System :: OS Independent",