From c3e301526e71ce07ac0141cc8c14e8eae2ae683d Mon Sep 17 00:00:00 2001 From: Peter Gaultney Date: Tue, 29 Sep 2020 10:50:58 -0500 Subject: [PATCH] map_tree supports postorder transformation --- .isort.cfg | 3 +++ .pre-commit-config.yaml | 7 +++++- CHANGES.md | 4 ++++ tests/xoto3/utils/tree_map_test.py | 25 ++++++++++++++++---- xoto3/__about__.py | 2 +- xoto3/utils/tree_map.py | 38 ++++++++++++++++++++---------- 6 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..5244d02 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +profile=black +line_length=100 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cee6f3..47ef60e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,8 +19,13 @@ repos: - id: prettier exclude: .*\.html + - repo: https://github.com/PyCQA/isort + rev: 5.4.2 # Use the revision sha / tag you want to point at + hooks: + - id: isort + - repo: https://github.com/ambv/black - rev: stable + rev: 19.10b0 hooks: - id: black language_version: python3.7 diff --git a/CHANGES.md b/CHANGES.md index a91fd72..6323329 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +### 1.5.0 + +`map_tree` now supports postorder transformations via keyword argument. + ### 1.4.0 - Improved DynamoDB Item-related Exceptions for `GetItem`, diff --git a/tests/xoto3/utils/tree_map_test.py b/tests/xoto3/utils/tree_map_test.py index 2eefdd9..fe2ccbd 100644 --- a/tests/xoto3/utils/tree_map_test.py +++ b/tests/xoto3/utils/tree_map_test.py @@ -1,13 +1,15 @@ from decimal import Decimal +import pytest + +from xoto3.utils.dec import decimal_to_number from xoto3.utils.tree_map import ( + compose, + make_path_only_transform, + make_path_stop_transform, map_tree, type_dispatched_transform, - make_path_stop_transform, - make_path_only_transform, - compose, ) -from xoto3.utils.dec import decimal_to_number def test_map_tree_idents(): @@ -76,3 +78,18 @@ def test_compose(): d = dict(p=[1.2, 3.4, 8.8]) assert map_tree(tx, d) == dict(p=["1i", "3i", "8i"]) + + +def test_postorder_transformation(): + def coerce_to_int(o): + return int(o) + + inp = dict(a=2.3, b=4.4) + with pytest.raises(TypeError): + map_tree(coerce_to_int, inp) + + assert dict(a=2, b=4) == map_tree(coerce_to_int, inp, postorder=True) + + assert dict(p=7, g=[2, 3, 4]) == map_tree( + coerce_to_int, dict(p=7.4, g=[2.01, 3.01, 4.01]), postorder=True + ) diff --git a/xoto3/__about__.py b/xoto3/__about__.py index 1ddca1a..070cdff 100644 --- a/xoto3/__about__.py +++ b/xoto3/__about__.py @@ -1,4 +1,4 @@ """xoto3""" -__version__ = "1.4.0" +__version__ = "1.5.0" __author__ = "Peter Gaultney" __author_email__ = "pgaultney@xoi.io" diff --git a/xoto3/utils/tree_map.py b/xoto3/utils/tree_map.py index d3da5aa..fbe7219 100644 --- a/xoto3/utils/tree_map.py +++ b/xoto3/utils/tree_map.py @@ -1,7 +1,6 @@ -from typing import Tuple, Hashable, Callable, Any, Mapping, Set, Union, cast -from functools import singledispatch, wraps import inspect - +from functools import singledispatch, wraps +from typing import Any, Callable, Hashable, Mapping, Set, Tuple, Union, cast SimpleTransform = Callable[[Any], Any] @@ -35,7 +34,7 @@ def coerce_transform(transform: TreeTransform) -> PathTransform: ) -def map_tree(transform: TreeTransform, obj: Any) -> Any: +def map_tree(transform: TreeTransform, obj: Any, *, postorder: bool = False) -> Any: """Maps a tree made of Python general-purpose builtin containers. The tree property of the object is important - technically you may @@ -50,6 +49,10 @@ def map_tree(transform: TreeTransform, obj: Any) -> Any: tuples, and sets, rendering a new corresponding builtin instance for each. + If you want a post-order call to your transform, pass the keyword + argument postorder=True. This will disable the use of the stop + return. + Only applies the first recursive transform that matches the type of the provided object. @@ -86,23 +89,32 @@ def map_tree(transform: TreeTransform, obj: Any) -> Any: paths only are provided. """ - return _map_tree(coerce_transform(transform), obj) + return _map_tree(coerce_transform(transform), obj, postorder=postorder) -def _map_tree(transform: PathTransform, obj: Any, *, path: KeyPath = ()) -> Any: - obj, stop = transform(obj, path) - if stop: - return obj +def _map_tree( + transform: PathTransform, obj: Any, *, path: KeyPath = (), postorder: bool = False +) -> Any: + if not postorder: + obj, stop = transform(obj, path) + if stop: + return obj # then apply the first builtin-type-matching recursive transform. if isinstance(obj, Mapping): - return {k: _map_tree(transform, v, path=path + (k,)) for k, v in obj.items()} + return { + k: _map_tree(transform, v, path=path + (k,), postorder=postorder) + for k, v in obj.items() + } if isinstance(obj, Set): - return {_map_tree(transform, member, path=path) for member in obj} + return {_map_tree(transform, member, path=path, postorder=postorder) for member in obj} if isinstance(obj, list): - return [_map_tree(transform, item, path=path) for item in obj] + return [_map_tree(transform, item, path=path, postorder=postorder) for item in obj] if isinstance(obj, tuple): - return tuple((_map_tree(transform, item, path=path) for item in obj)) + return tuple((_map_tree(transform, item, path=path, postorder=postorder) for item in obj)) + + if postorder: + obj, _stop = transform(obj, path) return obj