Skip to content

Commit

Permalink
better repr; pepkit#22
Browse files Browse the repository at this point in the history
  • Loading branch information
vreuter committed May 10, 2019
1 parent bd52ee1 commit 97737bb
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 64 deletions.
3 changes: 2 additions & 1 deletion attmap/__init__.py
Expand Up @@ -2,6 +2,7 @@

from .attmap import AttMap
from .attmap_echo import AttMapEcho
from .helpers import *
from .ordattmap import OrdAttMap
from .ordpathex_attmap import OrdPathExAttMap
from ._version import __version__
Expand All @@ -10,4 +11,4 @@
AttributeDictEcho = AttMapEcho

__all__ = ["AttMap", "AttMapEcho", "AttributeDict", "AttributeDictEcho",
"OrdAttMap", "OrdPathExAttMap"]
"OrdAttMap", "OrdPathExAttMap", "get_data_lines", "is_custom_map"]
100 changes: 41 additions & 59 deletions attmap/_att_map_like.py
@@ -1,13 +1,12 @@
""" The trait defining a multi-access data object """

import abc
import pprint
import sys
if sys.version_info < (3, 3):
from collections import Mapping, MutableMapping
else:
from collections.abc import Mapping, MutableMapping
from .helpers import get_logger
from .helpers import is_custom_map, get_data_lines, get_logger

__author__ = "Vince Reuter"
__email__ = "vreuter@virginia.edu"
Expand Down Expand Up @@ -101,18 +100,11 @@ def __len__(self):

def __repr__(self):
base = self.__class__.__name__
data = self._wrap_data_repr(self._data_for_repr())
data_text = "({})".format(pprint.PrettyPrinter(indent=2).pformat(data)) if data else "({})"
return base + data_text

def _data_for_repr(self):
return self._wrap_data_repr(
filter(lambda kv: not self._excl_from_repr(kv[0], self.__class__),
self.items()))

@staticmethod
def _wrap_data_repr(data):
return dict(data)
data = self._wrap_data_repr(self._simplify_keyvalue(self._data_for_repr()))
if data:
return base + ": {\n" + ",\n".join(get_data_lines(data, repr)) + "\n}"
else:
return base + ": {}"

def add_entries(self, entries):
"""
Expand All @@ -133,11 +125,9 @@ def add_entries(self, entries):
except AttributeError:
entries_iter = entries
for k, v in entries_iter:
if k not in self or not \
(isinstance(v, Mapping) and isinstance(self[k], AttMapLike)):
self[k] = v
else:
self[k] = self[k].add_entries(v)
self[k] = v if k not in self or not \
(isinstance(v, Mapping) and isinstance(self[k], AttMapLike)) \
else self[k].add_entries(v)
return self

def is_null(self, item):
Expand All @@ -162,47 +152,9 @@ def to_map(self):
"""
Convert this instance to a dict.
:return dict[str, object]:
:return dict[str, object]: this map's data, in a simpler container
"""
def go(kvs, acc):
try:
h, t = kvs[0], kvs[1:]
except IndexError:
return acc
k, v = h
acc[k] = go(list(v.items()), {}) \
if isinstance(v, Mapping) and type(v) is not dict else v
return go(t, acc)
return go(list(self.items()), {})

@staticmethod
def _simplify(data, empty, update, transform):
"""
Simplify an overall data structure and types of contained values.
:param Mapping data: the data collection to simplify
:param callable empty: a no-arg callable that builds an empty collection,
e.g. a type object like list
:param function(Iterable, object, callable) update: a function that
accepts as argument a single value to either simplify or return
unchanged
:param function(object) -> object transform: a function that
accepts as argument a single value to either simplify or return
unchanged
:return Iterable: a simplified version of original container
"""
def go(kvs, acc):
try:
h, t = kvs[0], kvs[1:]
except IndexError:
return acc
k, v = h
update(acc, k, transform(v))
return go(t, acc)
#acc[k] = go(list(v.items()), {}) \
# if isinstance(v, Mapping) and type(v) is not dict else v
#return go(t, acc)
return go(list(data.items()), empty())
return self._simplify_keyvalue(self.items(), {})

def _excl_from_eq(self, k):
"""
Expand All @@ -223,3 +175,33 @@ def _excl_from_repr(self, k, cls):
text representation
"""
return False

@staticmethod
def _new_empty_basic_map():
""" Return the empty collection builder for Mapping type simplification. """
return dict()

def _data_for_repr(self):
return filter(lambda kv: not self._excl_from_repr(kv[0], self.__class__),
self.items())

def _simplify_keyvalue(self, kvs, acc=None):
"""
Simplify a collection of key-value pairs, "reducing" to simpler types.
:param Iterable[(object, object)] kvs: collection of key-value pairs
:param Iterable acc: accumulating collection of simplified data
:return Iterable: collection of simplified data
"""
acc = acc or self._new_empty_basic_map()
kvs = iter(kvs)
try:
k, v = next(kvs)
except StopIteration:
return acc
acc[k] = self._simplify_keyvalue(v.items()) if is_custom_map(v) else v
return self._simplify_keyvalue(kvs, acc)

@staticmethod
def _wrap_data_repr(data):
return dict(data)
60 changes: 59 additions & 1 deletion attmap/helpers.py
Expand Up @@ -2,7 +2,16 @@

from copy import deepcopy
import logging
import os
import sys
if sys.version_info < (3, 3):
from collections import Mapping
else:
from collections.abc import Mapping

__author__ = "Vince Reuter"
__email__ = "vreuter@virginia.edu"

__all__ = ["get_data_lines", "is_custom_map"]


def copy(obj):
Expand All @@ -17,6 +26,45 @@ def copy(self):
return obj


def get_data_lines(data, fun_key, space_per_level=2, fun_val=None):
"""
Get text representation lines for a mapping's data.
:param Mapping data: collection of data for which to get repr lines
:param function(object) -> str fun_key: function to render key as text
:param function(object) -> str fun_val: function to render value as text
:param int space_per_level: number of spaces per level of nesting
:return Iterable[str]: collection of lines
"""

# If no specific value-render function, use key-render function
fun_val = fun_val or fun_key

def space(lev):
return " " * lev * space_per_level

def render(lev, key, val):
if key is None:
return space(lev) + val
valtext = val if val in ["{", "}"] else fun_val(val)
return space(lev) + fun_key(key) + ": " + valtext

def go(kvs, curr_lev, acc):
try:
k, v = next(kvs)
except StopIteration:
return acc
if not isinstance(v, Mapping) or len(v) == 0:
acc.append(render(curr_lev, k, v))
else:
acc.append(render(curr_lev, k, "{"))
acc.append(",\n".join(go(iter(v.items()), curr_lev + 1, [])))
acc.append(render(curr_lev, None, "}"))
return go(kvs, curr_lev, acc)

return go(iter(data.items()), 1, [])


def get_logger(name):
"""
Return a logger equipped with a null handler.
Expand All @@ -27,3 +75,13 @@ def get_logger(name):
log = logging.getLogger(name)
log.addHandler(logging.NullHandler())
return log


def is_custom_map(obj):
"""
Determine whether an object is a Mapping other than dict.
:param object obj: object to examine
:return bool: whether the object is a Mapping other than dict
"""
return isinstance(obj, Mapping) and type(obj) is not dict
2 changes: 2 additions & 0 deletions docs/changelog.md
Expand Up @@ -3,6 +3,8 @@
## Unreleased
### Added
- `OrdAttMap` to create maps that preserve insertion order and otherwise behave like ordinary `AttMap`
- `get_data_lines` utility, supporting nice instance `repr`
- `is_custom_map` utility

## [0.7] - 2019-04-24
### Changed
Expand Down
2 changes: 1 addition & 1 deletion tests/test_AttMap.py
Expand Up @@ -480,7 +480,7 @@ def _yaml_data(sample, filepath, section_to_change=None,


@pytest.mark.parametrize(
["func", "exp"], [(repr, "AttMap({})"), (str, "AttMap({})")])
["func", "exp"], [(repr, "AttMap: {}"), (str, "AttMap: {}")])
def test_text_repr_empty(func, exp):
""" Empty AttMap is correctly represented as text. """
assert exp == func(AttMap())
5 changes: 3 additions & 2 deletions tests/test_packaging.py
@@ -1,6 +1,6 @@
""" Validate what's available directly on the top-level import. """

from inspect import isclass
from inspect import isclass, isfunction
import pytest

__author__ = "Vince Reuter"
Expand All @@ -10,7 +10,8 @@
@pytest.mark.parametrize(
["obj_name", "typecheck"],
[("AttMap", isclass), ("OrdAttMap", isclass), ("OrdPathExAttMap", isclass),
("AttMapEcho", isclass)])
("AttMapEcho", isclass), ("is_custom_map", isfunction),
("get_data_lines", isfunction)])
def test_top_level_exports(obj_name, typecheck):
""" At package level, validate object availability and type. """
import attmap
Expand Down

0 comments on commit 97737bb

Please sign in to comment.