Filesystem-like pathing and searching for dictionaries
—
Operations for modifying dictionary structures including selective deletion and sophisticated deep merging with configurable behavior for different data integration scenarios.
Remove elements from nested dictionaries using glob patterns with support for filtering and preservation of data structure integrity.
def delete(obj, glob, separator="/", afilter=None):
"""
Delete all elements matching glob pattern.
Parameters:
- obj (MutableMapping): Target dictionary to modify
- glob (Glob): Path pattern as string or sequence
- separator (str): Path separator character (default "/")
- afilter (Filter): Optional function to filter which elements to delete
Returns:
int: Number of elements deleted
Raises:
- PathNotFound: If no matching paths found to delete
"""import dpath
from dpath.exceptions import PathNotFound
data = {
"users": {
"john": {"age": 30, "active": True, "temp_data": "delete_me"},
"jane": {"age": 25, "active": False, "temp_data": "also_delete"},
"bob": {"age": 35, "active": True}
},
"temp_settings": {"cache": True},
"permanent_settings": {"theme": "dark"}
}
# Delete exact path
count = dpath.delete(data, "users/john/temp_data") # Returns: 1
# Removes temp_data from john's record
# Delete with wildcards - removes matching fields from all users
count = dpath.delete(data, "users/*/temp_data") # Returns: 2 (john and jane)
# Delete entire user records
count = dpath.delete(data, "users/jane") # Returns: 1
# Removes jane's entire record
# Delete with filter - only delete inactive users
def inactive_filter(user):
return isinstance(user, dict) and not user.get("active", True)
count = dpath.delete(data, "users/*", afilter=inactive_filter)
# Only deletes users where active=False
# Delete all temporary data across entire structure
count = dpath.delete(data, "**/temp_*") # Matches any key starting with "temp_"
# Handle deletion errors
try:
dpath.delete(data, "nonexistent/path")
except PathNotFound as e:
print(f"Nothing to delete: {e}")
# Delete list elements
list_data = {"items": ["a", "b", "c", "d"]}
count = dpath.delete(list_data, "items/1") # Removes "b"
# Note: List behavior depends on position - middle elements become NoneDeeply merge dictionaries with sophisticated control over how conflicts are resolved and data is combined.
def merge(dst, src, separator="/", afilter=None, flags=MergeType.ADDITIVE):
"""
Deep merge source into destination dictionary.
Parameters:
- dst (MutableMapping): Destination dictionary (modified in place)
- src (MutableMapping): Source dictionary to merge from
- separator (str): Path separator for filtered merging (default "/")
- afilter (Filter): Optional function to filter which parts of src to merge
- flags (MergeType): Merge behavior flags (default MergeType.ADDITIVE)
Returns:
MutableMapping: The modified destination dictionary
Note:
Creates references, not deep copies. Source objects may be modified
by subsequent operations on the destination.
"""from dpath.types import MergeType
class MergeType(IntFlag):
ADDITIVE = auto() # Combine lists by concatenation (default)
REPLACE = auto() # Replace destination lists with source lists
TYPESAFE = auto() # Raise TypeError when merging incompatible typesimport dpath
from dpath.types import MergeType
# Basic merge - combines dictionaries recursively
dst = {
"users": {"john": {"age": 30}},
"settings": {"theme": "light"}
}
src = {
"users": {"jane": {"age": 25}, "john": {"city": "NYC"}},
"settings": {"lang": "en"}
}
dpath.merge(dst, src)
# Result: {
# "users": {"john": {"age": 30, "city": "NYC"}, "jane": {"age": 25}},
# "settings": {"theme": "light", "lang": "en"}
# }
# List merging with ADDITIVE (default)
dst = {"tags": ["python", "data"]}
src = {"tags": ["analysis", "ml"]}
dpath.merge(dst, src) # tags becomes ["python", "data", "analysis", "ml"]
# List merging with REPLACE
dst = {"tags": ["python", "data"]}
src = {"tags": ["analysis", "ml"]}
dpath.merge(dst, src, flags=MergeType.REPLACE) # tags becomes ["analysis", "ml"]
# Type-safe merging
dst = {"count": 10}
src = {"count": "ten"} # String instead of int
try:
dpath.merge(dst, src, flags=MergeType.TYPESAFE)
except TypeError as e:
print(f"Type mismatch: {e}")
# Filtered merging - only merge specific parts
def settings_filter(value):
# Only merge settings, not user data
return True # Apply filter logic here
filtered_src = dpath.search(src, "settings/**")
dpath.merge(dst, filtered_src)
# Combined flags
dpath.merge(dst, src, flags=MergeType.REPLACE | MergeType.TYPESAFE)# List deletion preserves order for end elements
data = {"items": ["a", "b", "c", "d", "e"]}
# Deleting last element truly removes it
dpath.delete(data, "items/-1") # or "items/4"
# Result: ["a", "b", "c", "d"]
# Deleting middle elements sets to None to preserve indices
dpath.delete(data, "items/1")
# Result: ["a", None, "c", "d", "e"]
# Delete multiple list elements
for i in reversed(range(1, 4)): # Delete backwards to maintain indices
dpath.delete(data, f"items/{i}")# Delete based on value properties
data = {
"products": {
"item1": {"price": 10, "discontinued": True},
"item2": {"price": 25, "discontinued": False},
"item3": {"price": 15, "discontinued": True}
}
}
# Delete discontinued products
def discontinued_filter(product):
return isinstance(product, dict) and product.get("discontinued", False)
count = dpath.delete(data, "products/*", afilter=discontinued_filter)
# Removes item1 and item3
# Delete based on value ranges
def expensive_filter(product):
return isinstance(product, dict) and product.get("price", 0) > 20
count = dpath.delete(data, "products/*", afilter=expensive_filter)import copy
# Problem: Merge creates references
dst = {"data": {"list": [1, 2]}}
src = {"data": {"list": [3, 4]}}
dpath.merge(dst, src) # dst["data"]["list"] now references src's list
src["data"]["list"].append(5) # This also affects dst!
# Solution: Deep copy source before merging
src_copy = copy.deepcopy(src)
dpath.merge(dst, src_copy) # Now changes to src won't affect dst# Multi-source merging with different behaviors
base_config = {"database": {"host": "localhost"}}
# Merge environment-specific overrides
env_config = {"database": {"port": 5432}}
dpath.merge(base_config, env_config)
# Merge user preferences with replacement for UI settings
user_config = {"ui": {"theme": "dark", "panels": ["editor", "terminal"]}}
dpath.merge(base_config, user_config, flags=MergeType.REPLACE)
# Merge runtime settings type-safely
runtime_config = {"database": {"timeout": 30}}
dpath.merge(base_config, runtime_config, flags=MergeType.TYPESAFE)# Merge only specific branches
def merge_branch(dst, src, branch_pattern):
"""Merge only specific branches of source into destination"""
branch_data = dpath.search(src, branch_pattern)
dpath.merge(dst, branch_data)
# Usage
merge_branch(dst, src, "settings/**") # Only merge settings branch
merge_branch(dst, src, "users/*/profile") # Only merge user profilesfrom dpath.exceptions import PathNotFound, InvalidKeyName
# Handle empty string keys (requires option)
import dpath.options
dpath.options.ALLOW_EMPTY_STRING_KEYS = True
data = {"": "empty key value"} # Now allowed
# Handle deletion of non-existent paths
try:
dpath.delete(data, "path/that/does/not/exist")
except PathNotFound:
print("Nothing to delete")
# Handle invalid key names
try:
dpath.new(data, "path/with//empty/segment", "value")
except InvalidKeyName as e:
print(f"Invalid key: {e}")
# Merge type conflicts
dst = {"value": [1, 2, 3]}
src = {"value": {"nested": "dict"}}
# This will replace the list with dict (default behavior)
dpath.merge(dst, src) # dst["value"] becomes {"nested": "dict"}Install with Tessl CLI
npx tessl i tessl/pypi-dpath