Skip to content

Commit

Permalink
Merge pull request #60 from pepkit/dev
Browse files Browse the repository at this point in the history
0.12.5
  • Loading branch information
vreuter authored Jun 7, 2019
2 parents f63c9ca + 01fdccb commit 660e279
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 42 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@ Thumbs.db
# Build-related stuff
build/
dist/
site/
attmap.egg-info/
docs/autodoc_build
3 changes: 3 additions & 0 deletions attmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@
__all__ = ["AttMapLike", "AttMap", "AttMapEcho", "AttributeDict",
"AttributeDictEcho", "EchoAttMap", "OrdAttMap", "PathExAttMap",
"get_data_lines"]
__aliases__ = {"AttMap": ["AttributeDict"],
"AttMapEcho": ["AttributeDictEcho"],
"EchoAttMap": ["AttMapEcho"]}
33 changes: 25 additions & 8 deletions attmap/_att_map_like.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ def __len__(self):
return sum(1 for _ in iter(self))

def __repr__(self):
return self._render(self._simplify_keyvalue(
self._data_for_repr(), self._new_empty_basic_map))

def _render(self, data):
base = self.__class__.__name__
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 @@ -94,13 +96,22 @@ def add_entries(self, entries):
else self[k].add_entries(v)
return self

def get_yaml_lines(self):
def get_yaml_lines(self, conversions=(
(lambda obj: isinstance(obj, Mapping) and 0 == len(obj), None), )):
"""
Get collection of lines that define YAML text rep. of this instance.
:param Iterable[(function(object) -> bool, object)] conversions:
collection of pairs in which first component is predicate function
and second is what to replace a value with if it satisfies the predicate
:return list[str]: YAML representation lines
"""
return ["{}"] if 0 == len(self) else repr(self).split("\n")[1:]
if 0 == len(self):
return ["{}"]
data = self._simplify_keyvalue(
self._data_for_repr(), self._new_empty_basic_map,
conversions=conversions)
return self._render(data).split("\n")[1:]

def is_null(self, item):
"""
Expand Down Expand Up @@ -185,7 +196,7 @@ def _new_empty_basic_map(self):
""" Return the empty collection builder for Mapping type simplification. """
pass

def _simplify_keyvalue(self, kvs, build, acc=None):
def _simplify_keyvalue(self, kvs, build, acc=None, conversions=None):
"""
Simplify a collection of key-value pairs, "reducing" to simpler types.
Expand All @@ -200,6 +211,12 @@ def _simplify_keyvalue(self, kvs, build, acc=None):
k, v = next(kvs)
except StopIteration:
return acc
acc[k] = self._simplify_keyvalue(
v.items(), build, build()) if is_custom_map(v) else v
return self._simplify_keyvalue(kvs, build, acc)
if is_custom_map(v):
v = self._simplify_keyvalue(v.items(), build, build())
if isinstance(v, Mapping):
for pred, proxy in (conversions or []):
if pred(v):
v = proxy
break
acc[k] = v
return self._simplify_keyvalue(kvs, build, acc, conversions)
2 changes: 1 addition & 1 deletion attmap/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.12.4"
__version__ = "0.12.5"
40 changes: 16 additions & 24 deletions attmap/pathex_attmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
class PathExAttMap(OrdAttMap):
""" Used in pepkit projects, with Mapping conversion and path expansion """

def __getattribute__(self, item, expand=True):
res = super(PathExAttMap, self).__getattribute__(item)
return _safely_expand(res) if expand else res

def __getattr__(self, item, default=None, expand=True):
"""
Get attribute, accessing stored key-value pairs as needed.
Expand All @@ -33,7 +37,7 @@ def __getattr__(self, item, default=None, expand=True):
# __getitem__ syntax, not attribute-access syntax.
raise AttributeError(item)
else:
return expandpath(v) if expand else v
return _safely_expand(v) if expand else v

def __getitem__(self, item, expand=True):
"""
Expand All @@ -45,7 +49,13 @@ def __getitem__(self, item, expand=True):
:raise KeyError: if the requested key is unmapped.
"""
v = super(PathExAttMap, self).__getitem__(item)
return self._finalize_value(v) if expand else v
return _safely_expand(v) if expand else v

def get(self, k, default=None, expand=True):
try:
return self.__getitem__(k, expand)
except KeyError:
return default

def items(self, expand=False):
"""
Expand Down Expand Up @@ -92,28 +102,10 @@ def to_dict(self, expand=False):
"""
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

@property
def _lower_type_bound(self):
return PathExAttMap


def _safely_expand(x):
return expandpath(x) if isinstance(x, str) else x
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.12.5] - 2019-06-06
### Changed
- By default, represent empty `Mapping` value as `null` in YAML rendition.
### Fixed
- Expand paths (in `PathExAttMap`) for text values stored and fetched with attribute` syntax or `.get`.

## [0.12.4] - 2019-05-30
### Fixed
- Avoid infinite recursion when an `EchoAttMap` subtype calls up to the superclass constructor: [Issue 55](https://github.com/pepkit/attmap/issues/55).
Expand Down
File renamed without changes.
9 changes: 9 additions & 0 deletions docs/maptypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Attmap class inheritance hierarchy

Attmap is organized into a series of related objects with slightly different behavior. This document shows the class relationships. Classes underneath others in this tree indicate parent-child relationships of the classes.

- [`AttMapLike`](autodoc_build/attmap.md#AttMapLike) (abstract)
- [`AttMap`](autodoc_build/attmap.md#AttMap)
- [`OrdAttMap`](autodoc_build/attmap.md#OrdAttMap)
- [`PathExAttMap`](autodoc_build/attmap.md#PathExAttMap)
- [`EchoAttMap`](autodoc_build/attmap.md#EchoAttMap)
6 changes: 0 additions & 6 deletions docs/types.md

This file was deleted.

4 changes: 2 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ pypi_name: attmap
nav:
- Introduction:
- Home: README.md
- Excluding attributes: exclude_from_repr.md
- Reference:
- Maptypes: types.md
- Class inheritance hierarchy: maptypes.md
- API: autodoc_build/attmap.md
- Howto: howto.md
- Support: support.md
- Contributing: contributing.md
- Changelog: changelog.md
Expand Down
28 changes: 28 additions & 0 deletions tests/test_path_expansion.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
""" Tests for path expansion behavior """

import copy
import itertools
import os
import random
Expand All @@ -8,6 +9,7 @@
from attmap import *
from ubiquerg import expandpath, TmpEnv


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

Expand Down Expand Up @@ -115,6 +117,32 @@ def test_non_PathExAttMap_preserves_all_variables(path, fetch, env):
("http://lh/$HOME/page.html", "http://lh/{}/page.html".format(os.environ["HOME"]))])
@pytest.mark.parametrize("fetch", [lambda m, k: m[k], lambda m, k: getattr(m, k)])
def test_url_expansion(path, expected, fetch):
""" URL expansion considers env vars but doesn't ruin slashes. """
key = "arbitrary"
m = PathExAttMap({key: path})
assert expected == fetch(m, key)


@pytest.mark.parametrize(
"varname", ["THIS_SHOULD_NOT_BE_SET", "REALLY_IMPROBABLE_ENV_VAR"])
@pytest.mark.parametrize(["var_idx", "path_parts"], itertools.chain(*[
[(i, list(p)) for p in itertools.permutations(c) for i in range(k + 1)]
for k in range(0, 4) for c in itertools.combinations(["a", "b", "c"], k)]))
@pytest.mark.parametrize("store", [setattr, lambda m, k, v: m.__setitem__(k, v)])
@pytest.mark.parametrize("fetch", [getattr, lambda m, k: m[k], lambda m, k: m.get(k)])
def test_multiple_syntax_path_expansion(varname, path_parts, var_idx, tmpdir, store, fetch):
""" Test the different combinations of setting and retrieving an env var path. """
key = "arbitrary"
parts = copy.copy(path_parts)
env_var = "$" + varname
env_var_val = "set-via-env-var"
parts.insert(var_idx, env_var)
final_parts = [tmpdir.strpath] + parts
print("FINAL PARTS: {}".format(final_parts))
path = os.path.join(*final_parts)
m = PathExAttMap()
store(m, key, path)
with TmpEnv(**{varname: env_var_val}):
assert env_var_val == os.getenv(varname)
assert path != expandpath(path)
assert expandpath(path) == fetch(m, key)
1 change: 0 additions & 1 deletion tests/test_to_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
__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"])}])
Expand Down

0 comments on commit 660e279

Please sign in to comment.