#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: "2025-11-10 22:40:05 (ywatanabe)"
import json
import pprint as _pprint
[docs]
class DotDict:
"""
A dictionary-like object that allows attribute-like access (for valid identifier keys)
and standard item access for all keys (including integers, etc.).
"""
[docs]
def __init__(self, dictionary=None):
# Use a private attribute to store the actual data
# Avoids conflicts with keys named like standard methods ('keys', 'items', etc.)
super().__setattr__("_data", {})
if dictionary is not None:
if isinstance(dictionary, DotDict):
dictionary = dictionary._data
elif not isinstance(dictionary, dict):
raise TypeError("Input must be a dictionary.")
for key, value in dictionary.items():
if isinstance(value, dict) and not isinstance(value, DotDict):
value = DotDict(value)
self[key] = value
# --- Attribute Access (for valid identifiers) ---
def __getattr__(self, key):
# Called for obj.key when key is not found by normal attribute lookup
# Allow access to internal methods/attributes starting with '_'
if key.startswith("_"):
return super().__getattribute__(
key
) # Use superclass method for internal attrs
try:
return self._data[key]
except KeyError:
# Mimic standard attribute access behavior
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{key}'"
)
def __setattr__(self, key, value):
# Called for obj.key = value
# Protect internal attributes
if key == "_data" or key.startswith("_"):
super().__setattr__(key, value)
else:
if isinstance(value, dict) and not isinstance(value, DotDict):
value = DotDict(value)
self._data[key] = value
def __delattr__(self, key):
# Called for del obj.key
if key.startswith("_"):
super().__delattr__(key)
else:
try:
del self._data[key]
except KeyError:
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{key}'"
)
# --- Item Access (for any key type) ---
def __getitem__(self, key):
# Called for obj[key]
return self._data[key] # Raises KeyError if not found
def __setitem__(self, key, value):
# Called for obj[key] = value
if isinstance(value, dict) and not isinstance(value, DotDict):
value = DotDict(value)
self._data[key] = value
def __delitem__(self, key):
# Called for del obj[key]
del self._data[key] # Raises KeyError if not found
# --- Standard Dictionary Methods (operating on _data) ---
[docs]
def get(self, key, default=None):
# Use _data's get method
return self._data.get(key, default)
[docs]
def to_dict(self, include_private=False):
"""
Recursively converts DotDict and nested DotDict objects back to ordinary dictionaries.
Args:
include_private: If False, exclude keys starting with '_' (default: False)
"""
result = {}
for key, value in self._data.items():
# Skip private keys (starting with _) unless explicitly requested
if not include_private and isinstance(key, str) and key.startswith("_"):
continue
if isinstance(value, DotDict):
value = value.to_dict(include_private=include_private)
result[key] = value
return result
[docs]
def __str__(self):
"""
Returns a string representation, handling non-JSON-serializable objects.
"""
def default_handler(obj):
# Handle DotDict specifically during serialization if needed,
# otherwise represent non-serializable items as strings.
if isinstance(obj, DotDict):
return obj.to_dict()
try:
# Attempt standard JSON serialization first
json.dumps(obj)
return obj # Let json.dumps handle it if possible
except (TypeError, OverflowError):
return str(obj) # Fallback for non-serializable types
try:
# Use the internal _data for representation
return json.dumps(self.to_dict(), indent=4, default=default_handler)
except TypeError as e:
# Fallback if default_handler still fails (e.g., complex recursion)
return f"<DotDict object at {hex(id(self))}, contains: {list(self._data.keys())}> Error: {e}"
[docs]
def __repr__(self):
"""
Returns a string representation suitable for debugging.
Returns a nicely formatted representation that pprint can display properly.
Private keys (starting with '_') are hidden by default.
"""
# Use pprint.pformat for nice formatting
# Convert to regular dict recursively, hiding private keys
return _pprint.pformat(
self.to_dict(include_private=False), indent=2, width=80, compact=False
)
[docs]
def _repr_pretty_(self, p, cycle):
"""
IPython/Jupyter pretty printing support.
This method is called by IPython's pprint when displaying DotDict objects.
Private keys (starting with '_') are hidden by default.
Args:
p: The pretty printer object
cycle: Boolean indicating if we're in a reference cycle
"""
if cycle:
p.text("DotDict(...)")
else:
with p.group(8, "DotDict(", ")"):
if self._data:
# Filter out private keys for display
public_data = self.to_dict(include_private=False)
p.pretty(public_data)
else:
p.text("{}")
[docs]
def __len__(self):
"""
Returns the number of key-value pairs in the dictionary.
"""
return len(self._data)
[docs]
def keys(self):
"""
Returns a view object displaying a list of all the keys.
"""
return self._data.keys()
[docs]
def values(self):
"""
Returns a view object displaying a list of all the values.
"""
return self._data.values()
[docs]
def items(self):
"""
Returns a view object displaying a list of all the items (key, value pairs).
"""
return self._data.items()
[docs]
def update(self, dictionary):
"""
Updates the dictionary with the key-value pairs from another dictionary or iterable.
"""
if isinstance(dictionary, dict):
iterator = dictionary.items()
# Allow updating from iterables of key-value pairs
elif hasattr(dictionary, "__iter__"):
iterator = dictionary
else:
raise TypeError(
"Input must be a dictionary or an iterable of key-value pairs."
)
for key, value in iterator:
# Use __setitem__ to handle potential DotDict conversion
self[key] = value
[docs]
def setdefault(self, key, default=None):
"""
Returns the value of the given key. If the key does not exist, insert the key
with the specified default value and return the default value.
"""
if key not in self._data:
# Use __setitem__ for potential DotDict conversion of default
self[key] = default
return default
else:
return self._data[key]
[docs]
def pop(self, key, *args):
"""
Removes the specified key and returns the corresponding value.
If key is not found, default is returned if given, otherwise KeyError is raised.
Accepts optional default value like dict.pop.
"""
# Mimic dict.pop behavior with optional default
if len(args) > 1:
raise TypeError(f"pop expected at most 2 arguments, got {1 + len(args)}")
if key not in self._data:
if args:
return args[0] # Return default if provided
else:
raise KeyError(key)
return self._data.pop(key) # Use internal dict's pop
[docs]
def __contains__(self, key):
"""
Checks if the dotdict contains the specified key.
"""
return key in self._data
[docs]
def __iter__(self):
"""
Returns an iterator over the keys of the dictionary.
"""
return iter(self._data)
[docs]
def copy(self):
"""
Creates a shallow copy of the DotDict object.
Nested DotDicts/dicts/lists will be references, not copies.
Use deepcopy for a fully independent copy.
"""
# Create a new instance using a copy of the internal data
return DotDict(self._data.copy()) # Shallow copy of internal dict
[docs]
def __dir__(self):
"""
Provides attribute suggestions for dir() and tab completion.
Includes both standard methods/attributes and the keys stored in _data.
"""
# Get standard attributes/methods from the object's structure
standard_attrs = set(super().__dir__())
# Get the keys from the internal data dictionary
# Filter for keys that are valid identifiers for attribute access
data_keys = set(
key
for key in self._data.keys()
if isinstance(key, str) and key.isidentifier()
)
# Return a sorted list of the combined unique names
return sorted(list(standard_attrs.union(data_keys)))
# --- Comparison Methods ---
[docs]
def __eq__(self, other):
"""Check equality. Supports comparison with dict, DotDict, and scalar values."""
if isinstance(other, DotDict):
return self._data == other._data
elif isinstance(other, dict):
return self._data == other
else:
# For scalar comparison, compare against empty/falsy
return False
[docs]
def __ne__(self, other):
"""Check inequality."""
return not self.__eq__(other)
[docs]
def __lt__(self, other):
"""Less than comparison - delegate to _data or handle scalars."""
if isinstance(other, (int, float)):
# If comparing to scalar, treat empty as 0
if len(self._data) == 0:
return 0 < other
# If single numeric value in total field, use it
if "total" in self._data and isinstance(self._data["total"], (int, float)):
return self._data["total"] < other
# Otherwise not comparable
return NotImplemented
return NotImplemented
[docs]
def __le__(self, other):
"""Less than or equal."""
return self.__lt__(other) or self.__eq__(other)
[docs]
def __gt__(self, other):
"""Greater than comparison - delegate to _data or handle scalars."""
if isinstance(other, (int, float)):
# If comparing to scalar, treat empty as 0
if len(self._data) == 0:
return 0 > other
# If single numeric value in total field, use it
if "total" in self._data and isinstance(self._data["total"], (int, float)):
return self._data["total"] > other
# Otherwise not comparable
return NotImplemented
return NotImplemented
[docs]
def __ge__(self, other):
"""Greater than or equal."""
return self.__gt__(other) or self.__eq__(other)
[docs]
def __bool__(self):
"""Truth value testing. Empty DotDict is False, non-empty is True."""
return len(self._data) > 0
# Example Usage:
if __name__ == "__main__":
data = {
"name": "example",
"version": 1,
100: "integer key",
"nested": {"value1": True, 200: False},
"list_val": [1, {"a": 2}],
"invalid-key": "hyphenated",
}
dd = DotDict(data)
# Access via attribute (for valid identifiers)
print(f"dd.name: {dd.name}")
print(f"dd.version: {dd.version}")
print(f"dd.nested.value1: {dd.nested.value1}")
# print(dd.100) # This would be a SyntaxError, as expected
# Access via item (for any key)
print(f"dd[100]: {dd[100]}")
print(f"dd['nested'][200]: {dd['nested'][200]}")
print(f"dd['invalid-key']: {dd['invalid-key']}")
# Modify values
dd.name = "updated example"
dd[100] = "new integer value"
dd.nested[200] = "updated nested int key"
dd[300] = "new top-level int key" # Add new int key
dd["new-key"] = "another invalid id key"
print("\n--- After Modifications ---")
print(f"dd.name: {dd.name}")
print(f"dd[100]: {dd[100]}")
print(f"dd.nested[200]: {dd.nested[200]}")
print(f"dd[300]: {dd[300]}")
print(f"dd['new-key']: {dd['new-key']}")
print("\n--- Representation ---")
print(f"repr(dd): {repr(dd)}")
print("\n--- String (JSON) Representation ---")
print(f"str(dd):\n{str(dd)}")
print("\n--- Convert back to dict ---")
plain_dict = dd.to_dict()
print(f"plain_dict: {plain_dict}")
print(f"plain_dict[100]: {plain_dict[100]}")
print(f"plain_dict['nested'][200]: {plain_dict['nested'][200]}")
print("\n--- Iteration ---")
for k in dd:
print(f"Key: {k}, Value: {dd[k]}")
print("\n--- Contains ---")
print(f"100 in dd: {100 in dd}")
print(f"'name' in dd: {'name' in dd}")
print(f"999 in dd: {999 in dd}")
print("\n--- Copy ---")
dd_copy = dd.copy()
dd_copy[100] = "value in copy"
dd_copy.name = "name in copy"
print(f"Original dd[100]: {dd[100]}")
print(f"Copy dd_copy[100]: {dd_copy[100]}")
print(f"Original dd.name: {dd.name}")
print(f"Copy dd_copy.name: {dd_copy.name}")
# EOF