From f5eb0e9ac0c46c20f581b04222c3bcb84142285a Mon Sep 17 00:00:00 2001 From: Vince Reuter Date: Thu, 16 May 2019 14:47:37 -0400 Subject: [PATCH 01/12] fixes for ordattmap intuitive behaviors and mixed insertion modes --- attmap/_version.py | 2 +- attmap/ordattmap.py | 27 +++++++++++++++++---------- docs/changelog.md | 5 +++++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/attmap/_version.py b/attmap/_version.py index f56f403..b9a7efa 100644 --- a/attmap/_version.py +++ b/attmap/_version.py @@ -1 +1 @@ -__version__ = "0.12" +__version__ = "0.12.1dev" diff --git a/attmap/ordattmap.py b/attmap/ordattmap.py index eedbd95..5cdd552 100644 --- a/attmap/ordattmap.py +++ b/attmap/ordattmap.py @@ -2,6 +2,7 @@ from collections import OrderedDict import itertools +import sys from .attmap import AttMap from .helpers import get_logger, safedel_message @@ -12,24 +13,18 @@ _LOGGER = get_logger(__name__) +_SUB_PY3 = sys.version_info.major < 3 class OrdAttMap(OrderedDict, AttMap): """ Insertion-ordered mapping with dot notation access """ + def __contains__(self, item): + return super(OrdAttMap, self).__contains__(item) or item in self.__dict__ + def __init__(self, entries=None): super(OrdAttMap, self).__init__(entries or {}) - def __iter__(self): - """ Include in the iteration keys/atts added with setattr style. """ - return itertools.chain( - super(OrdAttMap, self).__iter__(), - filter(lambda k: not self._is_od_member(k), self.__dict__.keys())) - - def __reversed__(self): - _LOGGER.warning("Reverse iteration as implemented may be inefficient") - return iter(reversed(list(self.keys()))) - def __getitem__(self, item): """ Attempt ordinary access, then access to attributes. @@ -66,6 +61,18 @@ def __repr__(self): """ Leverage base AttMap text representation. """ return AttMap.__repr__(self) + def __iter__(self): + """ Include in the iteration keys/atts added with setattr style. """ + from_item = list(super(OrdAttMap, self).__iter__()) + dat_set = set(from_item) + from_attr = [k for k in self.__dict__.keys() + if not self._is_od_member(k) and k not in dat_set] + return itertools.chain(from_item, from_attr) + + def __reversed__(self): + _LOGGER.warning("Reverse iteration as implemented may be inefficient") + return iter(reversed(list(self.keys()))) + def keys(self): return [k for k in self] diff --git a/docs/changelog.md b/docs/changelog.md index e844858..e16c6ab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # Changelog +## [0.12.1] - 2019-05-16 +### Fixed +- In any `OrdAttMap, for membership (`__contains__`) consider items added via attribute syntax. +- Prevent duplicate key/attr iteration in any `OrdAttMap`. + ## [0.12] - 2019-05-16 ### Added - Export base `AttMapLike`. From 0c3984aec3982cb4f2fb31ffa77131b91c52dc33 Mon Sep 17 00:00:00 2001 From: Vince Reuter Date: Thu, 16 May 2019 14:48:00 -0400 Subject: [PATCH 02/12] add note about superclass call --- docs/howto.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/howto.md b/docs/howto.md index 300512f..983f8a1 100644 --- a/docs/howto.md +++ b/docs/howto.md @@ -24,4 +24,6 @@ def _excl_from_repr(self, k, cls): where `BaseOmissionType` is a proxy for the name of some type of values that may be stored in your mapping but that you prefer to not display in its text representation. -The two kinds of exclusion criteria may be combined as desired. +The two kinds of exclusion criteria may be combined as desired. +Note that it's often advisable to invoke the superclass version of the method, +but to achieve the intended effect this may be skipped. From 177bd4b6d58d3391581aeaaea78758df36d8abf0 Mon Sep 17 00:00:00 2001 From: Vince Date: Thu, 16 May 2019 15:41:32 -0400 Subject: [PATCH 03/12] alias AttMapEcho as EchoAttMap; close #38 --- attmap/__init__.py | 8 ++++---- attmap/attmap_echo.py | 15 +++++++++++++-- docs/changelog.md | 2 ++ tests/conftest.py | 4 ++-- tests/test_packaging.py | 6 +++++- tests/test_special_mutability.py | 6 +++--- 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/attmap/__init__.py b/attmap/__init__.py index 6344bc7..6efa393 100644 --- a/attmap/__init__.py +++ b/attmap/__init__.py @@ -2,7 +2,7 @@ from ._att_map_like import AttMapLike from .attmap import AttMap -from .attmap_echo import AttMapEcho +from .attmap_echo import * from .helpers import * from .ordattmap import OrdAttMap from .pathex_attmap import PathExAttMap @@ -11,6 +11,6 @@ AttributeDict = AttMap AttributeDictEcho = AttMapEcho -__all__ = ["AttMapLike", "AttMap", "AttMapEcho", - "AttributeDict", "AttributeDictEcho", - "OrdAttMap", "PathExAttMap", "get_data_lines"] +__all__ = ["AttMapLike", "AttMap", "AttMapEcho", "AttributeDict", + "AttributeDictEcho", "EchoAttMap", "OrdAttMap", "PathExAttMap", + "get_data_lines"] diff --git a/attmap/attmap_echo.py b/attmap/attmap_echo.py index 9f2ef03..4c41599 100644 --- a/attmap/attmap_echo.py +++ b/attmap/attmap_echo.py @@ -2,15 +2,23 @@ from .pathex_attmap import PathExAttMap +__author__ = "Vince Reuter" +__email__ = "vreuter@virginia.edu" + +__all__ = ["AttMapEcho", "EchoAttMap"] + class AttMapEcho(PathExAttMap): """ An AttMap that returns key/attr if it has no set value. """ - def __getattr__(self, item, default=None): + def __getattr__(self, item, default=None, expand=True): """ Fetch the value associated with the provided identifier. :param int | str item: identifier for value to fetch + :param object default: default return value + :param bool expand: whether to attempt variable expansion of string + value, in case it's a path :return object: whatever value corresponds to the requested key/item :raises AttributeError: if the requested item has not been set, no default value is provided, and this instance is not configured @@ -21,7 +29,7 @@ def __getattr__(self, item, default=None): to be indicative of the intent of protection. """ try: - return super(self.__class__, self).__getattr__(item, default) + return super(self.__class__, self).__getattr__(item, default, expand) except (AttributeError, TypeError): # If not, triage and cope accordingly. if self._is_od_member(item) or \ @@ -35,3 +43,6 @@ def __getattr__(self, item, default=None): def _lower_type_bound(self): """ Most specific type to which an inserted value may be converted """ return AttMapEcho + + +EchoAttMap = AttMapEcho diff --git a/docs/changelog.md b/docs/changelog.md index e16c6ab..01bb9e0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,8 @@ # Changelog ## [0.12.1] - 2019-05-16 +### Added +- `EchoAttMap` as alias for `AttMapEcho`; see [Issue 38](https://github.com/pepkit/attmap/issues/38) ### Fixed - In any `OrdAttMap, for membership (`__contains__`) consider items added via attribute syntax. - Prevent duplicate key/attr iteration in any `OrdAttMap`. diff --git a/tests/conftest.py b/tests/conftest.py index 3454ef4..2e80ec0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,8 +7,8 @@ __email__ = "vreuter@virginia.edu" -ALL_ATTMAPS = [AttributeDict, AttributeDictEcho, AttMap, OrdAttMap, AttMapEcho, - PathExAttMap] +ALL_ATTMAPS = [AttributeDict, AttributeDictEcho, AttMap, AttMapEcho, + EchoAttMap, OrdAttMap, PathExAttMap] @pytest.fixture(scope="function", params=ALL_ATTMAPS) diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 360c343..c2ff17c 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -23,12 +23,16 @@ def get_base_check(*bases): return lambda obj: obj.__bases__ == bases +ECHO_TEST_FUNS = [isclass, get_base_check(PathExAttMap)] + + @pytest.mark.parametrize(["obj_name", "typecheck"], itertools.chain(*[ [("AttMapLike", f) for f in [isclass, lambda obj: obj.__metaclass__ == ABCMeta]], [("AttMap", f) for f in [isclass, get_base_check(AttMapLike)]], [("OrdAttMap", f) for f in [isclass, get_base_check(OrderedDict, AttMap)]], [("PathExAttMap", f) for f in [isclass, get_base_check(OrdAttMap)]], - [("AttMapEcho", f) for f in [isclass, get_base_check(PathExAttMap)]], + [("AttMapEcho", f) for f in ECHO_TEST_FUNS], + [("EchoAttMap", f) for f in ECHO_TEST_FUNS], [("get_data_lines", isfunction)] ])) def test_top_level_exports(obj_name, typecheck): diff --git a/tests/test_special_mutability.py b/tests/test_special_mutability.py index 69bc6a1..455ef37 100644 --- a/tests/test_special_mutability.py +++ b/tests/test_special_mutability.py @@ -8,8 +8,8 @@ __email__ = "vreuter@virginia.edu" -ALL_ATTMAPS = [AttributeDict, AttributeDictEcho] -NULLABLE_ATTMAPS = ALL_ATTMAPS +OLD_ATTMAPS = [AttributeDict, AttributeDictEcho] +NULLABLE_ATTMAPS = OLD_ATTMAPS @pytest.fixture(scope="function", params=["arbitrary", "random"]) @@ -22,7 +22,7 @@ class UniversalMutabilityTests: """ Tests of attmap behavior with respect to mutability """ @staticmethod - @pytest.fixture(scope="function", params=ALL_ATTMAPS) + @pytest.fixture(scope="function", params=OLD_ATTMAPS) def m(request): """ Provide a test case with a fresh empty data object. """ return get_att_map(request.param) From 0f2cdfd5d1a54ba0fdc395026e90f77a153bf914 Mon Sep 17 00:00:00 2001 From: Vince Date: Thu, 16 May 2019 15:57:12 -0400 Subject: [PATCH 04/12] doc map types; #38 --- docs/types.md | 6 ++++++ mkdocs.yml | 1 + 2 files changed, 7 insertions(+) create mode 100644 docs/types.md diff --git a/docs/types.md b/docs/types.md new file mode 100644 index 0000000..66d87b4 --- /dev/null +++ b/docs/types.md @@ -0,0 +1,6 @@ +# Types of maps +- `AttMapLike` (abstract) + - `AttMap` + - `OrdAttMap` + - `PathExAttMap` + - `EchoAttMap` diff --git a/mkdocs.yml b/mkdocs.yml index fe9ab9e..ab7f745 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,7 @@ nav: - Introduction: - Home: README.md - Reference: + - Maptypes: types.md - API: autodoc_build/attmap.md - Howto: howto.md - Support: support.md From 8c998bd1f06c00335e279142a39903f3a14883cf Mon Sep 17 00:00:00 2001 From: Vince Date: Thu, 16 May 2019 16:01:15 -0400 Subject: [PATCH 05/12] invert the alias syntax --- attmap/attmap_echo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/attmap/attmap_echo.py b/attmap/attmap_echo.py index 4c41599..e0ff1ea 100644 --- a/attmap/attmap_echo.py +++ b/attmap/attmap_echo.py @@ -8,7 +8,7 @@ __all__ = ["AttMapEcho", "EchoAttMap"] -class AttMapEcho(PathExAttMap): +class EchoAttMap(PathExAttMap): """ An AttMap that returns key/attr if it has no set value. """ def __getattr__(self, item, default=None, expand=True): @@ -45,4 +45,4 @@ def _lower_type_bound(self): return AttMapEcho -EchoAttMap = AttMapEcho +AttMapEcho = EchoAttMap From bf4804976b3484c76e2adfbabf301d4687ef6e8a Mon Sep 17 00:00:00 2001 From: Vince Date: Thu, 16 May 2019 16:14:44 -0400 Subject: [PATCH 06/12] try linking to type headers --- docs/types.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/types.md b/docs/types.md index 66d87b4..4a5e22e 100644 --- a/docs/types.md +++ b/docs/types.md @@ -1,6 +1,6 @@ # Types of maps -- `AttMapLike` (abstract) - - `AttMap` - - `OrdAttMap` - - `PathExAttMap` - - `EchoAttMap` +- [`AttMapLike`](autodoc_build/attmap.md#Class-AttMapLike) (abstract) + - [`AttMap`](autodoc_build/attmap.md#Class-AttMap) + - [`OrdAttMap`](autodoc_build/attmap.md#Class-OrdAttMap) + - [`PathExAttMap`](autodoc_build/attmap.md#Class-PathExAttMap) + - [`EchoAttMap`](autodoc_build/attmap.md#Class-EchoAttMap) From f5aa3407dee55af7f9c2a16ab7cdd1478181bb05 Mon Sep 17 00:00:00 2001 From: Vince Reuter Date: Thu, 16 May 2019 17:33:11 -0400 Subject: [PATCH 07/12] moving toward attr/item uniformity; leverage super iter --- attmap/ordattmap.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/attmap/ordattmap.py b/attmap/ordattmap.py index 5cdd552..b75e768 100644 --- a/attmap/ordattmap.py +++ b/attmap/ordattmap.py @@ -19,12 +19,14 @@ class OrdAttMap(OrderedDict, AttMap): """ Insertion-ordered mapping with dot notation access """ - def __contains__(self, item): - return super(OrdAttMap, self).__contains__(item) or item in self.__dict__ - def __init__(self, entries=None): super(OrdAttMap, self).__init__(entries or {}) + def __setattr__(self, name, value): + super(OrdAttMap, self).__setattr__(name, value) + if not name.startswith("__"): + self.__setitem__(name, value, finalize=False) + def __getitem__(self, item): """ Attempt ordinary access, then access to attributes. @@ -38,9 +40,10 @@ def __getitem__(self, item): except KeyError: return AttMap.__getitem__(self, item) - def __setitem__(self, key, value): + def __setitem__(self, key, value, finalize=True): """ Support hook for value transformation before storage. """ - super(OrdAttMap, self).__setitem__(key, self._final_for_store(value)) + super(OrdAttMap, self).__setitem__( + key, self._final_for_store(value) if finalize else value) def __delitem__(self, key): """ Make unmapped key deletion unexceptional. """ @@ -61,14 +64,6 @@ def __repr__(self): """ Leverage base AttMap text representation. """ return AttMap.__repr__(self) - def __iter__(self): - """ Include in the iteration keys/atts added with setattr style. """ - from_item = list(super(OrdAttMap, self).__iter__()) - dat_set = set(from_item) - from_attr = [k for k in self.__dict__.keys() - if not self._is_od_member(k) and k not in dat_set] - return itertools.chain(from_item, from_attr) - def __reversed__(self): _LOGGER.warning("Reverse iteration as implemented may be inefficient") return iter(reversed(list(self.keys()))) From 99e88bf445f588861c8296c894fe9010eb37f825 Mon Sep 17 00:00:00 2001 From: Vince Reuter Date: Thu, 16 May 2019 17:33:51 -0400 Subject: [PATCH 08/12] simplify insertion modification; hook for value finalization --- attmap/attmap.py | 28 +++++++--------------------- tests/helpers.py | 4 +--- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/attmap/attmap.py b/attmap/attmap.py index 1340b72..d57c20c 100644 --- a/attmap/attmap.py +++ b/attmap/attmap.py @@ -31,17 +31,19 @@ def __delitem__(self, key): def __getitem__(self, item): return self.__dict__[item] - def __setitem__(self, key, value): + def __setitem__(self, key, value, finalize=True): """ This is the key to making this a unique data type. :param str key: name of the key/attribute for which to establish value :param object value: value to which set the given key; if the value is a mapping-like object, other keys' values may be combined. + :param bool finalize: whether to attempt a transformation of the value + to store before storing it """ # TODO: consider enforcement of type constraint, that value of different # type may not overwrite existing. - self.__dict__[key] = self._final_for_store(value) + self.__dict__[key] = self._final_for_store(value) if finalize else value def __eq__(self, other): # TODO: check for equality across classes? @@ -85,9 +87,8 @@ def _final_for_store(self, v): :param object v: value to potentially transform before storing :return object: finalized value """ - for p, f in self._insertion_mutations: - if p(v): - return f(v) + if isinstance(v, Mapping) and not isinstance(v, self._lower_type_bound): + v = self._metamorph_maplike(v) return v @property @@ -105,9 +106,7 @@ def _metamorph_maplike(self, m): if not isinstance(m, Mapping): raise TypeError("Cannot integrate a non-Mapping: {}\nType: {}". format(m, type(m))) - m_prime = self._lower_type_bound.__new__(self._lower_type_bound) - m_prime.__init__(m) - return m_prime + return m.to_map() if isinstance(m, AttMapLike) else self._lower_type_bound(m.items()) def _new_empty_basic_map(self): """ Return the empty collection builder for Mapping type simplification. """ @@ -122,16 +121,3 @@ def _repr_pretty_(self, p, cycle): :return str: text representation of the instance """ return p.text(repr(self) if not cycle else '...') - - @property - def _insertion_mutations(self): - """ - Hook for item transformation(s) to be applied upon insertion. - - :return list[(function, function)]: pairs in which first component is a - predicate and second is a function to apply to a value if it - satisfies the predicate - """ - newmap = lambda obj: isinstance(obj, Mapping) and \ - not isinstance(obj, self._lower_type_bound) - return [(newmap, self._metamorph_maplike)] diff --git a/tests/helpers.py b/tests/helpers.py index 7a44d4c..8e8fc68 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -21,9 +21,7 @@ def get_att_map(cls, entries=None): """ Create a fresh, empty data object. """ assert issubclass(cls, AttMapLike) - m = cls.__new__(cls) - m.__init__(entries or {}) - return m + return cls(entries or {}) def raises_keyerr(k, m): From 8f422eb55cb991ae9cdd393369142bd9f367e3e8 Mon Sep 17 00:00:00 2001 From: Vince Reuter Date: Thu, 16 May 2019 17:49:57 -0400 Subject: [PATCH 09/12] py2 OrderedDict implementation compat --- attmap/ordattmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attmap/ordattmap.py b/attmap/ordattmap.py index b75e768..b5e48ab 100644 --- a/attmap/ordattmap.py +++ b/attmap/ordattmap.py @@ -24,7 +24,7 @@ def __init__(self, entries=None): def __setattr__(self, name, value): super(OrdAttMap, self).__setattr__(name, value) - if not name.startswith("__"): + if not (self._is_od_member(name) or name.startswith("__")): self.__setitem__(name, value, finalize=False) def __getitem__(self, item): From c399e91889cab64412582008b1cfb866c05f5cff Mon Sep 17 00:00:00 2001 From: Vince Reuter Date: Thu, 16 May 2019 17:55:16 -0400 Subject: [PATCH 10/12] version prep --- attmap/_version.py | 2 +- docs/changelog.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/attmap/_version.py b/attmap/_version.py index b9a7efa..def467e 100644 --- a/attmap/_version.py +++ b/attmap/_version.py @@ -1 +1 @@ -__version__ = "0.12.1dev" +__version__ = "0.12.1" diff --git a/docs/changelog.md b/docs/changelog.md index 01bb9e0..a862784 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,7 @@ ### Fixed - In any `OrdAttMap, for membership (`__contains__`) consider items added via attribute syntax. - Prevent duplicate key/attr iteration in any `OrdAttMap`. +- Allow item and attribute syntax to equivalently mutate a map; see [Issue 50](https://github.com/pepkit/attmap/issues/50) ## [0.12] - 2019-05-16 ### Added From d459d7b0522f4f2737590f9e9a9090b81fc215c9 Mon Sep 17 00:00:00 2001 From: Vince Date: Fri, 17 May 2019 01:14:19 -0400 Subject: [PATCH 11/12] simplify signatures --- attmap/ordattmap.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/attmap/ordattmap.py b/attmap/ordattmap.py index b5e48ab..e2e5a45 100644 --- a/attmap/ordattmap.py +++ b/attmap/ordattmap.py @@ -25,7 +25,7 @@ def __init__(self, entries=None): def __setattr__(self, name, value): super(OrdAttMap, self).__setattr__(name, value) if not (self._is_od_member(name) or name.startswith("__")): - self.__setitem__(name, value, finalize=False) + self[name] = value def __getitem__(self, item): """ @@ -40,10 +40,9 @@ def __getitem__(self, item): except KeyError: return AttMap.__getitem__(self, item) - def __setitem__(self, key, value, finalize=True): + def __setitem__(self, key, value): """ Support hook for value transformation before storage. """ - super(OrdAttMap, self).__setitem__( - key, self._final_for_store(value) if finalize else value) + super(OrdAttMap, self).__setitem__(key, self._final_for_store(value)) def __delitem__(self, key): """ Make unmapped key deletion unexceptional. """ From b78ac59927daa36825d9e53e4c7764e409e43ef7 Mon Sep 17 00:00:00 2001 From: Vince Date: Fri, 17 May 2019 01:35:03 -0400 Subject: [PATCH 12/12] changelog --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index a862784..ed8821b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -## [0.12.1] - 2019-05-16 +## [0.12.1] - 2019-05-17 ### Added - `EchoAttMap` as alias for `AttMapEcho`; see [Issue 38](https://github.com/pepkit/attmap/issues/38) ### Fixed