def test_immutable_traverse_plain_object(): """Test that by traversin the plain object we get that object itself.""" # Check on boolean assert traverse(True) is True # Check on integer assert traverse(5) == 5 # Check non traversable class instance class NonTraversable: def __init__(self): self.value = [1, 2] t1 = NonTraversable() t2 = traverse(t1) # If traverse indeed could not traverse thorugh this # object and did not copy it but returned the original one # then both 't1' and 't2' point to the # same object. assert t1.value == t2.value # Alter the list value in the original object to make sure # the second object has the same change. t1.value.append(3) assert t2.value == [1, 2, 3]
def test_mutable_traverse_copy_dict(): """Test that plain traverse produces deep copy of the Dict.""" duhast_traversed = traverse(duhast) assert duhast_traversed == duhast # Alter traversed version and verify the original # did not change. duhast_traversed["person"]["perks"][-1].append("valorian") len_traversed = len(duhast_traversed["person"]["perks"][-1]) len_original = len(duhast["person"]["perks"][-1]) assert len_traversed == len_original + 1
def test_immutable_traverse(): """Test immutable traverse and visit order. This test verifies that my passing a non-modifying visitor function to the traversal method we are correctly visiting every node and do not alter the initial object. """ def _on_kv(*args, **kwargs): k, v = args if issequence(v): v = "<list>" if isinstance(v, Mapping): v = "<dict>" kwargs["tracking_list"].append((k, v)) visit_order = [] traverse(duhast, visitor=_on_kv, tracking_list=visit_order) assert visit_order == [ ("person", "<dict>"), ("name", "Duhast"), ("parental", "Vyacheslavovich"), ("songs", "<dict>"), ("Du Hast (variations)", "<list>"), (None, "<dict>"), ("Du Hast - Live", "<dict>"), ("Wales", 2007), ("London", 2001), (None, "Du Hast - Remix"), ("perks", "<list>"), (None, "awesome"), (None, "<list>"), (None, "vocal"), (None, "brave"), ("event", b"Babaika Fest"), ]
def to_dict( mapping: Union[Dict[Hashable, Any], Mapping[Hashable, Any]], inplace: bool = False, ) -> Dict[Hashable, Any]: """Convert mapping structure to plain dict. This is useful to convert any subtype of ``dict`` back to its original form. For example, Mapz objects overwrite get/set methods of original dict to add new features. Applying ``to_dict`` to Mapz object will transform it into a simple dict with the same structure. Args: mapping: Mapping structure to convert. inplace (bool): Whether to replace given Dict with converted ``dict`` object. Only works if the passed ``mapping`` is also a mutable Dict-like object. Defaults to False. Returns: dict: Plain dictionary object with the same structure. If the "inplace" is True, then the given "mapping" object is also rebound to newly converted dictionary instead of pointing to the initial given structure. Examples: Transform Mapz object to dict. >>> m = Mapz({"a": 1}) >>> m MapZ{'a': 1} >>> to_dict(m) {'a': 1} """ d = traverse(mapping, mapping_type=dict) if isinstance(mapping, Dict) and inplace: mapping.clear() mapping.update(d) d = mapping # Explicit cast here because otherwise mypy complains that 'traverse' # returns Any, thus 'd' also evaluates to Any, and 'to_dict' # returns Dict. # Somehow, unable to specify just 'dict' as a return type for this # function. return cast(Dict[Hashable, Any], d)
def to_table( mapping: Mapping[Hashable, Any], headers: Iterable[str] = ("Key", "Value"), indentation: str = " ", limit: int = 0, ) -> TableType: """Transform dictionary into a printable table structure. Resulting table always consists of the two columns. The first column contains mapping keys sorted in ascending order from first row to last and indented according to the internal structure of the given mapping. If a key in the mapping represents another mapping or a list then the value across it in the "Value" column will be empty. If a key represents anything different, then its value will be cast to string and truncated if its length is more than 79 characters. In such case first 76 characters of the value are taken and three dots ("...") indicating truncation are added to them. Each next nested level of the mapping is indented using the ``indentation`` string provided in the arguments. If a certain key contains a list of values, then each value printed in the following rows will be accompanied by the dash ("-") in the "Key" column. This also applied to the case when key contains a list of mappings. In such case the mappings will be printed as in YAML. If ``limit`` is > 0, then no more than ``limit`` rows will be converted. An additional row indicating truncation will be added as the last one (["...", "..."]). Args: mapping: Mapping or dictionary to transform to table. headers: Iterable of headers for the table. Defaults to ("Key", "Value"). indentation (str): String that will be used as an indentation of nested keys in the table. Defaults to double-space " ". limit (int): Row limit. Limits the number of rows in the resulting table. Defaults to 0 (no limit). Returns: TableType: A tuple consiting of headers and list of rows. Examples: Below, a structure with nested values, dictionaries, plain lists, and lists of other dictionaries is transformed to a table. Notice how list items each get prepended by a dash and any nested key is indented. >>> m = Mapz({ \ "databases": { "db1": { "host": "localhost", "port": 5432, }, }, "users": [ "Duhast", "Valera", ], "Params": [ {"ttl": 120, "flush": True}, {frozenset({1, 2}): {1, 2}}, ], "name": "Boris", }) >>> to_table(m) ( ['Key', 'Value'], [ ['Params', ''], [' - flush', 'True'], [' ttl', '120'], [' - frozenset({1, 2})', '{1, 2}'], ['databases', ''], [' db1', ''], [' host', 'localhost'], [' port', '5432'], ['name', 'Boris'], ['users', ''], [' -', 'Duhast'], [' -', 'Valera'] ] ) """ def builder(k: Any, v: Any, **kwargs: Any) -> Optional[Tuple[Any, Any]]: """Visit each key and value and collect them into row list. Args: **kwargs: Arguments provided by ``traverse`` function as well as by invoking function. Contains "_depth", "_index", and "_ancestors" values provided by ``traverse``. Must also contain "rows" and "limit" provided by function that invoked traverse. """ # Mutable list of rows. rows = kwargs["rows"] # Limit of rows. limit = kwargs["limit"] # How deeply nested are we. depth = kwargs["_depth"] # Index of the current item (useful for processing lists). index = kwargs["_index"] # List of acenstors of current nested key/value. ancestors = kwargs["_ancestors"] # Render keys by default as: table_key = indentation * (depth - 1) + str(k) if ( len(ancestors) > 1 and ismapping(ancestors[-1]) # noqa: W503 and issequence(ancestors[-2]) # noqa: W503 ): # If node has two or more ancestors, then check if it's a # mapping within a list. Because in that case it must be # rendered as in YAML: # my_list # - key1 value1 # key2 value2 if index: table_key = indentation * (depth - 1) + str(k) else: table_key = indentation * (depth - 2) + "- " + str(k) elif ancestors and issequence(ancestors[-1]): # Render child items of lists as just a dash with proper indent table_key = indentation * (depth - 1) + "-" if not limit or limit and len(rows) < limit: value = "" if not (ismapping(v) or issequence(v)): s = " ".join( str(v).replace("\n", " ").replace("\t", " ").split() ) value = s if len(s) < 80 else f"{s[:76]}..." # Ignore empty lines with no key and no value. # Example: List of Mappings will result in such row. if not k and not value: return None rows.append([table_key, value]) return None rows: List[RowType] = [] traverse( mapping, visitor=builder, key_order=lambda keys: sorted(keys), rows=rows, limit=limit, ) if limit and len(rows) >= limit: rows.append(["...", "..."]) return (list(headers), rows)
def test_mutable_traverse_plain_object(): """Test that modifying visitor function has no effect on plain objects.""" assert traverse(True, lambda k, v, **kwargs: 1 + 2) is True