Filesystem-like pathing and searching for dictionaries
—
Low-level utilities for custom path manipulation and advanced use cases, providing direct access to the underlying path walking, matching, and transformation algorithms that power dpath's high-level functions.
Core traversal functions that provide the foundation for all dpath operations, offering fine-grained control over how nested structures are navigated.
from dpath.segments import walk, make_walkable, leaf, leafy
def walk(obj, location=()):
"""
Walk object yielding (segments, value) pairs in breadth-first order.
Parameters:
- obj: Object to walk (dict, list, or other nested structure)
- location (tuple): Current location tuple (used internally for recursion)
Yields:
Tuple[Tuple[PathSegment, ...], Any]: (path_segments, value) pairs
Note:
Walks right-to-left for sequences to support safe deletion patterns.
"""
def make_walkable(node):
"""
Create iterator for (key, value) pairs regardless of node type.
Parameters:
- node: Dict, list, tuple, or other container
Returns:
Iterator[Tuple[PathSegment, Any]]: Iterator of (key, value) pairs
"""
def leaf(thing):
"""
Check if object is a leaf (primitive type: str, int, float, bool, None, bytes).
Parameters:
- thing: Any object to test
Returns:
bool: True if object is a primitive leaf value
"""
def leafy(thing):
"""
Check if object is leaf-like (primitive or empty container).
Parameters:
- thing: Any object to test
Returns:
bool: True if object is primitive or empty sequence/mapping
"""from dpath.segments import walk, make_walkable, leaf, leafy
data = {
"users": {
"john": {"age": 30, "hobbies": ["reading", "gaming"]},
"jane": {"age": 25, "hobbies": []}
}
}
# Walk entire structure
for path_segments, value in walk(data):
path_str = "/".join(str(seg) for seg in path_segments)
print(f"{path_str}: {value} (leaf: {leaf(value)})")
# Output includes:
# users: {'john': {...}, 'jane': {...}} (leaf: False)
# users/john: {'age': 30, 'hobbies': [...]} (leaf: False)
# users/john/age: 30 (leaf: True)
# users/john/hobbies: ['reading', 'gaming'] (leaf: False)
# users/john/hobbies/0: reading (leaf: True)
# etc.
# Make any container walkable
mixed_data = [{"key": "value"}, "string", 42]
for key, value in make_walkable(mixed_data):
print(f"Index {key}: {value}")
# Test leaf conditions
print(leaf("string")) # True
print(leaf(42)) # True
print(leaf([])) # False
print(leafy([])) # True (empty containers are leafy)
print(leafy({"a": 1})) # False (non-empty containers)Advanced pattern matching utilities for implementing custom search and filter logic.
from dpath.segments import match, view, has, get
def match(segments, glob):
"""
Check if path segments match glob pattern.
Parameters:
- segments (Path): Path segments as tuple or list
- glob (Glob): Glob pattern as tuple or list
Returns:
bool: True if segments match the glob pattern
Note:
Supports *, **, character classes, and complex patterns.
Converts integers to strings for comparison.
"""
def view(obj, glob):
"""
Create filtered deep copy view of object matching glob.
Parameters:
- obj (MutableMapping): Object to create view from
- glob (Glob): Glob pattern to match
Returns:
MutableMapping: New object containing only matching paths (deep copied)
"""
def has(obj, segments):
"""
Check if path exists in object.
Parameters:
- obj: Object to check
- segments: Path segments as sequence
Returns:
bool: True if path exists
"""
def get(obj, segments):
"""
Get value at path segments (lower-level than dpath.get).
Parameters:
- obj: Object to navigate
- segments: Path segments as sequence
Returns:
Any: Value at path
Raises:
- PathNotFound: If path doesn't exist
"""from dpath.segments import match, view, has, get
data = {
"api": {
"v1": {"users": ["john", "jane"]},
"v2": {"users": ["bob"], "admin": ["alice"]}
}
}
# Test pattern matching directly
segments = ("api", "v1", "users")
pattern = ("api", "*", "users")
print(match(segments, pattern)) # True
pattern2 = ("api", "v2", "admin")
print(match(segments, pattern2)) # False
# Create views with deep copying
api_v1_view = view(data, ("api", "v1", "**"))
# Returns: {"api": {"v1": {"users": ["john", "jane"]}}}
users_view = view(data, ("**", "users"))
# Returns: {"api": {"v1": {"users": ["john", "jane"]}, "v2": {"users": ["bob"]}}}
# Path existence checking
print(has(data, ("api", "v1", "users"))) # True
print(has(data, ("api", "v3", "users"))) # False
print(has(data, ("api", "v1", "users", 0))) # True (first user exists)
# Direct path access
users = get(data, ("api", "v1", "users")) # Returns: ["john", "jane"]
first_user = get(data, ("api", "v1", "users", 0)) # Returns: "john"Low-level path construction and manipulation utilities for implementing custom creators and transformers.
from dpath.segments import set, extend, types, expand, int_str, _default_creator
def set(obj, segments, value, creator=_default_creator, hints=()):
"""
Set value at path, optionally creating missing components.
Parameters:
- obj (MutableMapping): Object to modify
- segments: Path segments sequence
- value: Value to set
- creator (Creator): Function to create missing path components (default _default_creator)
- hints (Hints): Type hints for creator function
Returns:
MutableMapping: Modified object
"""
def _default_creator(current, segments, i, hints=()):
"""
Default creator function that creates dicts or lists based on next segment type.
Parameters:
- current: Current container being modified
- segments: Full path segments
- i (int): Current segment index
- hints (Hints): Type hints for creation
Creates:
- List if next segment is numeric
- Dict otherwise
"""
def extend(thing, index, value=None):
"""
Extend sequence to contain at least index+1 elements.
Parameters:
- thing (MutableSequence): Sequence to extend
- index (int): Required minimum index
- value: Fill value for new elements (default None)
Returns:
MutableSequence: Extended sequence
"""
def types(obj, segments):
"""
Get type information for each level of path.
Parameters:
- obj: Object to analyze
- segments: Path segments
Returns:
Tuple[Tuple[PathSegment, type], ...]: (segment, type) pairs for each level
"""
def expand(segments):
"""
Generate progressively longer path prefixes.
Parameters:
- segments: Full path segments
Yields:
Tuple: Path prefixes from shortest to full length
"""
def int_str(segment):
"""
Convert integer segments to strings.
Parameters:
- segment (PathSegment): Path segment to convert
Returns:
PathSegment: String if input was int, otherwise unchanged
"""from dpath.segments import set, extend, types, expand, int_str, _default_creator
# Use default creator (automatic dict/list selection)
data = {}
set(data, ("users", 0, "name"), "john") # Uses _default_creator
# Creates: {"users": [{"name": "john"}]} - list because next segment is 0
# Custom creator functions
def always_list_creator(current, segments, i, hints=()):
"""Creator that always makes lists instead of dicts"""
segment = segments[i]
if isinstance(current, list):
extend(current, segment)
current[segment] = []
# Use custom creator
data2 = {}
set(data2, ("items", 0, "name"), "first", creator=always_list_creator)
# Creates: {"items": [{"name": "first"}]} but with all list structure
# Extend sequences manually
my_list = ["a", "b"]
extend(my_list, 5, "empty") # Extends to ["a", "b", "empty", "empty", "empty", "empty"]
# Analyze path types
data = {"users": [{"name": "john", "age": 30}]}
path_types = types(data, ("users", 0, "name"))
# Returns: (("users", <class 'dict'>), (0, <class 'list'>), ("name", <class 'dict'>))
# Generate path prefixes
for prefix in expand(("a", "b", "c", "d")):
print(prefix)
# Output:
# ("a",)
# ("a", "b")
# ("a", "b", "c")
# ("a", "b", "c", "d")
# Convert path segments for display
mixed_path = ("users", 0, "profile", 1, "email")
display_path = "/".join(int_str(seg) for seg in mixed_path)
# Returns: "users/0/profile/1/email"Higher-order functions for implementing custom operations over nested structures.
from dpath.segments import fold, foldm, leaves
def fold(obj, f, acc):
"""
Fold (reduce) over all paths in object with read-only access.
Parameters:
- obj: Object to fold over
- f: Function(obj, (path_segments, value), accumulator) -> bool|Any
- acc: Initial accumulator value
Returns:
Any: Final accumulator value
Note:
If f returns False (exactly), folding stops early.
"""
def foldm(obj, f, acc):
"""
Fold with mutation support - allows modifying obj during iteration.
Parameters:
- obj: Object to fold over (may be modified)
- f: Function(obj, (path_segments, value), accumulator) -> bool|Any
- acc: Initial accumulator value
Returns:
Any: Final accumulator value
Note:
Loads all paths into memory first to support safe mutations.
"""
def leaves(obj):
"""
Generate only leaf (path, value) pairs.
Parameters:
- obj: Object to traverse
Yields:
Tuple[Tuple[PathSegment, ...], Any]: (path_segments, leaf_value) pairs
"""from dpath.segments import fold, foldm, leaves
data = {
"users": {
"john": {"age": 30, "active": True},
"jane": {"age": 25, "active": False}
},
"settings": {"theme": "dark"}
}
# Count all paths
def counter(obj, pair, count):
count[0] += 1
return True
total_paths = fold(data, counter, [0])
print(f"Total paths: {total_paths[0]}")
# Find first match with early termination
def find_age_30(obj, pair, result):
path_segments, value = pair
if value == 30:
result.append(path_segments)
return False # Stop folding
return True
result = fold(data, find_age_30, [])
if result:
print(f"Found age 30 at: {result[0]}")
# Mutating fold - deactivate all users
def deactivate_users(obj, pair, count):
path_segments, value = pair
if (isinstance(value, dict) and
"active" in value and
len(path_segments) >= 2 and
path_segments[0] == "users"):
value["active"] = False
count[0] += 1
return True
changes = foldm(data, deactivate_users, [0])
print(f"Deactivated {changes[0]} users")
# Process only leaf values
for path_segments, value in leaves(data):
path_str = "/".join(str(seg) for seg in path_segments)
print(f"Leaf {path_str}: {value}")
# Custom aggregations
def sum_numeric_leaves(obj, pair, acc):
path_segments, value = pair
if isinstance(value, (int, float)):
acc["sum"] += value
acc["count"] += 1
return True
result = fold(data, sum_numeric_leaves, {"sum": 0, "count": 0})
if result["count"] > 0:
average = result["sum"] / result["count"]
print(f"Average numeric value: {average}")# Implement custom search with transformation
def transform_search(obj, pattern, transformer):
"""Search and transform matching values"""
results = {}
def transform_match(obj, pair, results):
path_segments, value = pair
if match(path_segments, pattern):
transformed = transformer(value)
set(results, path_segments, transformed)
return True
fold(obj, transform_match, results)
return results
# Usage
def uppercase_strings(value):
return value.upper() if isinstance(value, str) else value
transformed = transform_search(data, ("**",), uppercase_strings)# Early termination patterns
def find_first_matching_path(obj, condition):
"""Find first path matching condition"""
def finder(obj, pair, result):
path_segments, value = pair
if condition(value):
result.append(path_segments)
return False # Stop immediately
return True
result = fold(obj, finder, [])
return result[0] if result else None
# Memory-efficient processing
def process_large_structure(obj, processor):
"""Process structure without loading all paths"""
for path_segments, value in walk(obj):
if leaf(value):
processor(path_segments, value)Install with Tessl CLI
npx tessl i tessl/pypi-dpath