Skip to content

Commit

Permalink
Merge pull request #44 from pepkit/dev
Browse files Browse the repository at this point in the history
0.11
  • Loading branch information
vreuter authored May 16, 2019
2 parents c088cbc + 1ab3ad3 commit 9585745
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 21 deletions.
43 changes: 34 additions & 9 deletions attmap/_att_map_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ def __getattr__(self, item, default=None):
raise AttributeError(item)

@abc.abstractmethod
def __setitem__(self, key, value):
def __delitem__(self, item):
pass

@abc.abstractmethod
def __getitem__(self, item):
pass

@abc.abstractmethod
def __delitem__(self, item):
def __setitem__(self, key, value):
pass

def __iter__(self):
Expand All @@ -60,7 +60,8 @@ def __len__(self):

def __repr__(self):
base = self.__class__.__name__
data = self._simplify_keyvalue(self._data_for_repr())
data = self._simplify_keyvalue(
self._data_for_repr(), self._new_empty_basic_map)
if data:
return base + "\n" + "\n".join(
get_data_lines(data, lambda obj: repr(obj).strip("'")))
Expand Down Expand Up @@ -91,6 +92,14 @@ def add_entries(self, entries):
else self[k].add_entries(v)
return self

def get_yaml_lines(self):
"""
Get collection of lines that define YAML text rep. of this instance.
:return list[str]: YAML representation lines
"""
return ["{}"] if 0 == len(self) else repr(self).split("\n")[1:]

def is_null(self, item):
"""
Conjunction of presence in underlying mapping and value being None
Expand All @@ -115,8 +124,23 @@ def to_map(self):
:return dict[str, object]: this map's data, in a simpler container
"""
return self._simplify_keyvalue(
self.items(), self._new_empty_basic_map())
return self._simplify_keyvalue(self.items(), self._new_empty_basic_map)

def to_dict(self):
"""
Return a builtin dict representation of this instance.
:return dict: builtin dict representation of this instance
"""
return self._simplify_keyvalue(self.items(), dict)

def to_yaml(self):
"""
Get text for YAML representation.
:return str: YAML text representation of this instance.
"""
return "\n".join(self.get_yaml_lines())

def _data_for_repr(self):
"""
Expand Down Expand Up @@ -158,20 +182,21 @@ def _new_empty_basic_map(self):
""" Return the empty collection builder for Mapping type simplification. """
pass

def _simplify_keyvalue(self, kvs, acc=None):
def _simplify_keyvalue(self, kvs, build, acc=None):
"""
Simplify a collection of key-value pairs, "reducing" to simpler types.
:param Iterable[(object, object)] kvs: collection of key-value pairs
:param callable build: how to build an empty collection
:param Iterable acc: accumulating collection of simplified data
:return Iterable: collection of simplified data
"""
acc = acc or self._new_empty_basic_map()
acc = acc or build()
kvs = iter(kvs)
try:
k, v = next(kvs)
except StopIteration:
return acc
acc[k] = self._simplify_keyvalue(
v.items(), self._new_empty_basic_map()) if is_custom_map(v) else v
return self._simplify_keyvalue(kvs, acc)
v.items(), build, build()) if is_custom_map(v) else v
return self._simplify_keyvalue(kvs, build, acc)
2 changes: 1 addition & 1 deletion attmap/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.10"
__version__ = "0.11"
10 changes: 5 additions & 5 deletions attmap/attmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __setitem__(self, key, value):
"""
# TODO: consider enforcement of type constraint, that value of different
# type may not overwrite existing.
self.__dict__[key] = self._finalize_value(value)
self.__dict__[key] = self._final_for_store(value)

def __eq__(self, other):
# TODO: check for equality across classes?
Expand Down Expand Up @@ -78,14 +78,14 @@ def same_type(obj1, obj2, typenames=None):
# have nonidentical labels.
return False

def _finalize_value(self, v):
def _final_for_store(self, v):
"""
Before storing a value, apply any desired transformation.
:param object v: value to potentially transform before storing
:return object: finalized value
"""
for p, f in self._transformations:
for p, f in self._insertion_mutations:
if p(v):
return f(v)
return v
Expand Down Expand Up @@ -124,9 +124,9 @@ def _repr_pretty_(self, p, cycle):
return p.text(repr(self) if not cycle else '...')

@property
def _transformations(self):
def _insertion_mutations(self):
"""
Add path expansion behavior to more general attmap.
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
Expand Down
16 changes: 15 additions & 1 deletion attmap/ordattmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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()))
Expand All @@ -30,13 +31,21 @@ def __reversed__(self):
return iter(reversed(list(self.keys())))

def __getitem__(self, item):
"""
Attempt ordinary access, then access to attributes.
:param hashable item: key/attr for which to fetch value
:return object: value to which given key maps, perhaps modifed
according to the instance's finalization of retrieved values
"""
try:
return super(OrdAttMap, self).__getitem__(item)
except KeyError:
return AttMap.__getitem__(self, item)

def __setitem__(self, key, value):
super(OrdAttMap, self).__setitem__(key, self._finalize_value(value))
""" Support hook for value transformation before storage. """
super(OrdAttMap, self).__setitem__(key, self._final_for_store(value))

def __delitem__(self, key):
""" Make unmapped key deletion unexceptional. """
Expand All @@ -46,13 +55,15 @@ def __delitem__(self, key):
_LOGGER.debug(safedel_message(key))

def __eq__(self, other):
""" Leverage base AttMap eq check, and check key order. """
return AttMap.__eq__(self, other) and \
list(self.keys()) == list(other.keys())

def __ne__(self, other):
return not self == other

def __repr__(self):
""" Leverage base AttMap text representation. """
return AttMap.__repr__(self)

def keys(self):
Expand Down Expand Up @@ -87,11 +98,14 @@ def popitem(self, last=True):

@staticmethod
def _is_od_member(name):
""" Assess whether name appears to be a protected OrderedDict member. """
return name.startswith("_OrderedDict")

def _new_empty_basic_map(self):
""" For ordered maps, OrderedDict is the basic building block. """
return OrderedDict()

@property
def _lower_type_bound(self):
""" OrdAttMap is the type to which nested maps are converted. """
return OrdAttMap
105 changes: 100 additions & 5 deletions attmap/pathex_attmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,103 @@
class PathExAttMap(OrdAttMap):
""" Used in pepkit projects, with Mapping conversion and path expansion """

@property
def _transformations(self):
""" Add path expansion behavior to more general attmap. """
return super(PathExAttMap, self)._transformations + \
[(lambda obj: isinstance(obj, str), expandpath)]
def __getattr__(self, item, default=None, expand=True):
"""
Get attribute, accessing stored key-value pairs as needed.
:param str item: name of attribute/key
:param object default: value to return if requested attr/key is missing
:param bool expand: whether to attempt path expansion of string value
:return object: value bound to requested name
:raise AttributeError: if requested item is unavailable
"""
try:
v = super(PathExAttMap, self).__getattribute__(item)
except AttributeError:
try:
return self.__getitem__(item, expand)
except KeyError:
# Requested item is unknown, but request was made via
# __getitem__ syntax, not attribute-access syntax.
raise AttributeError(item)
else:
return expandpath(v) if expand else v

def __getitem__(self, item, expand=True):
"""
Fetch the value of given key.
:param hashable item: key for which to fetch value
:param bool expand: whether to expand string value as path
:return object: value mapped to given key, if available
:raise KeyError: if the requested key is unmapped.
"""
v = super(PathExAttMap, self).__getitem__(item)
return self._finalize_value(v) if expand else v

def items(self, expand=False):
"""
Produce list of key-value pairs, optionally expanding paths.
:param bool expand: whether to expand paths
:return Iterable[object]: stored key-value pairs, optionally expanded
"""
return [(k, self.__getitem__(k, expand)) for k in self]

def values(self, expand=False):
"""
Produce list of values, optionally expanding paths.
:param bool expand: whether to expand paths
:return Iterable[object]: stored values, optionally expanded
"""
return [self.__getitem__(k, expand) for k in self]

def _data_for_repr(self, expand=False):
"""
Hook for extracting the data used in the object's text representation.
:param bool expand: whether to expand paths
:return Iterable[(hashable, object)]: collection of key-value pairs
to include in object's text representation
"""
return filter(lambda kv: not self._excl_from_repr(kv[0], self.__class__),
self.items(expand))

def to_map(self, expand=False):
"""
Convert this instance to a dict.
:return dict[str, object]: this map's data, in a simpler container
"""
return self._simplify_keyvalue(self.items(expand), self._new_empty_basic_map)

def to_dict(self, expand=False):
"""
Return a builtin dict representation of this instance.
:return dict: builtin dict representation of this instance
"""
return self._simplify_keyvalue(self.items(expand), dict)

def _finalize_value(self, v):
"""
Make any modifications to a retrieved value before returning it.
This hook accesses an instance's declaration of value mutations, or
transformations. That sequence may be empty (the base case), in which
case any value is simply always returned as-is.
If an instances does declare retrieval modifications, though, the
declaration should be an iterable of pairs, in which each element's
first component is a single-argument predicate function evaluated on
the given value, and the second component of each element is the
modification to apply if the predicate is satisfied.
At most one modification function will be called, and it would be the
first one for which the predicate was satisfied.
:param object v: a value to consider for modification
:return object: the finalized value
"""
return expandpath(v) if isinstance(v, str) else v
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.11] - 2019-05-16
### Added
- `get_yaml_lines` to get collection of YAML-ready lines from any attmap
- `to_dict` to convert any attmap (and nested maps) to base `dict`
- `to_yaml` to represent any attmap as raw YAML text
### Changed
- `PathExAttMap` defers expansion behavior to retrieval time.

## [0.10] - 2019-05-15
### Fixed
- `OrdAttMap` and descendants now have data updated via `__setattr__` syntax.
Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions tests/test_path_expansion.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ def test_PathExAttMap_substitution_is_selective(path, pres, repl, env, pam, fetc
with TmpEnv(**env):
pam[k] = path
res = fetch(pam, k)
print("Inserted path: {}".format(path))
print("Retrieved path: {}".format(res))
assert all(map(lambda s: s in res, pres))
assert all(map(lambda s: s not in res, repl))

Expand Down
24 changes: 24 additions & 0 deletions tests/test_to_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
""" Tests for conversion to base/builtin dict type """

import pytest
from tests.helpers import get_att_map
import numpy as np
from pandas import Series


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


@pytest.mark.para
@pytest.mark.parametrize("entries", [
{}, {"a": 1}, {"b": {"c": 3}}, {"A": [1, 2]},
{"B": 1, "C": np.arange(3)}, {"E": Series(["a", "b"])}])
def test_to_dict_type(attmap_type, entries):
""" Validate to_dict result is a base dict and that contents match. """
m = get_att_map(attmap_type, entries)
assert type(m) is not dict
d = m.to_dict()
assert type(d) is dict
assert d == entries
assert entries == d
Loading

0 comments on commit 9585745

Please sign in to comment.