Skip to content

Commit

Permalink
Merge pull request #51 from pepkit/dev
Browse files Browse the repository at this point in the history
0.12.1
  • Loading branch information
vreuter authored May 17, 2019
2 parents ecfb2f8 + b78ac59 commit 53e8008
Show file tree
Hide file tree
Showing 13 changed files with 65 additions and 48 deletions.
8 changes: 4 additions & 4 deletions attmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
2 changes: 1 addition & 1 deletion attmap/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.12"
__version__ = "0.12.1"
28 changes: 7 additions & 21 deletions attmap/attmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -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. """
Expand All @@ -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)]
17 changes: 14 additions & 3 deletions attmap/attmap_echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

from .pathex_attmap import PathExAttMap

__author__ = "Vince Reuter"
__email__ = "[email protected]"

class AttMapEcho(PathExAttMap):
__all__ = ["AttMapEcho", "EchoAttMap"]


class EchoAttMap(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
Expand All @@ -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 \
Expand All @@ -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


AttMapEcho = EchoAttMap
19 changes: 10 additions & 9 deletions attmap/ordattmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections import OrderedDict
import itertools
import sys
from .attmap import AttMap
from .helpers import get_logger, safedel_message

Expand All @@ -12,6 +13,7 @@


_LOGGER = get_logger(__name__)
_SUB_PY3 = sys.version_info.major < 3


class OrdAttMap(OrderedDict, AttMap):
Expand All @@ -20,15 +22,10 @@ class OrdAttMap(OrderedDict, AttMap):
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 __setattr__(self, name, value):
super(OrdAttMap, self).__setattr__(name, value)
if not (self._is_od_member(name) or name.startswith("__")):
self[name] = value

def __getitem__(self, item):
"""
Expand Down Expand Up @@ -66,6 +63,10 @@ def __repr__(self):
""" Leverage base AttMap text representation. """
return AttMap.__repr__(self)

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]

Expand Down
8 changes: 8 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [0.12.1] - 2019-05-17
### 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`.
- 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
- Export base `AttMapLike`.
Expand Down
4 changes: 3 additions & 1 deletion docs/howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions docs/types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Types of maps
- [`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)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ nav:
- Introduction:
- Home: README.md
- Reference:
- Maptypes: types.md
- API: autodoc_build/attmap.md
- Howto: howto.md
- Support: support.md
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
__email__ = "[email protected]"


ALL_ATTMAPS = [AttributeDict, AttributeDictEcho, AttMap, OrdAttMap, AttMapEcho,
PathExAttMap]
ALL_ATTMAPS = [AttributeDict, AttributeDictEcho, AttMap, AttMapEcho,
EchoAttMap, OrdAttMap, PathExAttMap]


@pytest.fixture(scope="function", params=ALL_ATTMAPS)
Expand Down
4 changes: 1 addition & 3 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 5 additions & 1 deletion tests/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions tests/test_special_mutability.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
__email__ = "[email protected]"


ALL_ATTMAPS = [AttributeDict, AttributeDictEcho]
NULLABLE_ATTMAPS = ALL_ATTMAPS
OLD_ATTMAPS = [AttributeDict, AttributeDictEcho]
NULLABLE_ATTMAPS = OLD_ATTMAPS


@pytest.fixture(scope="function", params=["arbitrary", "random"])
Expand All @@ -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)
Expand Down

0 comments on commit 53e8008

Please sign in to comment.