Skip to content

capturegraph.data.containers.dict #

Dict - Dictionary with Attribute Access#

A dict subclass that enables attribute-style access to keys.

Example
from capturegraph.data import Dict

d = Dict({"name": "Alice", "age": 30})

# Attribute access
print(d.name)   # "Alice"
print(d.age)    # 30

# Standard dict access still works
print(d["name"])  # "Alice"

# Missing keys return Missing (not KeyError)
print(d.nonexistent)  # Missing
Notes
  • Keys must be strings to support attribute access.
  • Missing keys return Missing instead of raising KeyError.
  • Use for structured data where dot notation improves readability.

Dict #

Bases: dict[str, V]

A dict that supports attribute access to keys.

Enables cleaner syntax: d.foo instead of d["foo"].

Class Type Parameters:

Name Bound or Constraints Description Default
V

The value type stored in the dictionary.

required
Note
  • Keys must be strings.
  • Missing keys return Missing (for safe chaining).
  • Standard dict operations (len, keys, iteration) work normally.
Example
d = Dict({"name": "Alice", "age": 30})
d.name       # 'Alice'
d["age"]     # 30 (standard access still works)
Source code in capturegraph-lib/capturegraph/data/containers/dict.py
class Dict[V](dict[str, V]):
    """A dict that supports attribute access to keys.

    Enables cleaner syntax: `d.foo` instead of `d["foo"]`.

    Type Parameters:
        V: The value type stored in the dictionary.

    Note:
        - Keys must be strings.
        - Missing keys return `Missing` (for safe chaining).
        - Standard dict operations (`len`, `keys`, iteration) work normally.

    Example:
        ```python
        d = Dict({"name": "Alice", "age": 30})
        d.name       # 'Alice'
        d["age"]     # 30 (standard access still works)
        ```
    """

    # --------------------------------------------------
    # Access
    # --------------------------------------------------

    def __getitem__(self, key: str | int) -> V | MissingType:
        """Get item by key. Returns Missing if key doesn't exist.

        If key is an int, return the value at that index position.
        """
        try:
            if isinstance(key, int):
                key = list(self.keys())[key]
                result = _vectorize(self[key])
                self[key] = result
                return result
            else:
                result = _vectorize(super().__getitem__(key))
                self[key] = result
                return result
        except (KeyError, IndexError) as e:
            return MissingType(e)

    def __getattr__(self, name: str) -> V | MissingType:
        """Get item by attribute name. Returns Missing if key doesn't exist."""
        if _is_dunder(name):
            raise AttributeError(
                f"'{type(self).__name__}' object has no attribute '{name}'"
            )

        try:
            result = _vectorize(self[name])
            self[name] = result
            return result
        except KeyError as e:
            return MissingType(e)

    def __setitem__(self, key: str, value: V) -> None:
        """Set item by key."""
        if not is_missing(value):
            super().__setitem__(key, value)

    def __setattr__(self, name: str, value: V) -> None:
        """Set item by attribute name."""
        if not is_missing(value):
            self[name] = value

    def __delattr__(self, name: str) -> None:
        """Delete item by attribute name. Silently ignores missing keys."""
        try:
            del self[name]
        except KeyError:
            pass

    # --------------------------------------------------
    # Function application
    # --------------------------------------------------

    def map[U](self, function: Callable[[V], U]) -> "Dict[U]":
        """Apply a function to each value in this dict.

        Keys are preserved; values are transformed.

        Args:
            function: A callable to apply to each value.

        Returns:
            Dict with same keys, transformed values.

        Raises:
            TypeError: If function is not callable.

        Example:
            ```python
            d = Dict({"a": 1, "b": 2, "c": 3})
            d.map(lambda x: x * 10)
            # → Dict({'a': 10, 'b': 20, 'c': 30})

            d.map(str)  # Apply built-in function
            # → Dict({'a': '1', 'b': '2', 'c': '3'})
            ```
        """
        if not callable(function):
            raise TypeError(
                f"Dict.map requires a callable (function/lambda).\n"
                f"Got: {type(function).__name__} = {function!r}\n\n"
                f"Usage: d.map(lambda x: x * 2)"
            )

        results = {}
        for key, value in dict.items(self):
            try:
                result = _vectorize(function(value))
                results[key] = result
            except Exception as e:
                results[key] = MissingType(e)

        _check_all_missing(
            list(results.values()),
            f"function '{function.__name__ if hasattr(function, '__name__') else repr(function)}'",
        )

        return Dict(results)

    def map_leaves(self, function: Callable[[Any], Any]) -> "Dict[Any]":
        """Apply a function to each leaf element recursively.

        Unlike ``map()`` which applies to top-level values, ``map_leaves()``
        descends into nested List/Dict structures and applies the function to leaf values.

        Args:
            function: A callable to apply to each leaf element.

        Returns:
            Nested Dict with same structure, function applied to leaves.

        Example:
            ```python
            # Nested structure - map_leaves descends into inner lists/dicts
            d = Dict({"nums": [1, 2, 3], "value": 4})
            d.map_leaves(lambda x: x * 10)
            # → Dict({'nums': [10, 20, 30], 'value': 40})

            # Compare with map which applies to top-level values only
            d.map(sum)  # Would try sum(4) which fails
            ```
        """
        result = _map_leaves(self, function)

        _check_all_missing(
            list(result.values()) if isinstance(result, dict) else result,
            f"function '{function.__name__ if hasattr(function, '__name__') else repr(function)}'",
        )

        return result

    def with_key(self, key: str, value: V) -> "Dict[V]":
        """Return a new Dict with an additional key-value pair.

        Creates a shallow copy with the new key added (or updated if it exists).

        Args:
            key: The key to add or update.
            value: The value to associate with the key.

        Returns:
            A new Dict containing all existing entries plus the new key-value pair.
        """
        return Dict(self | {key: value})

    def get_or_insert(self, key: str, value: V) -> V:
        """Get the value for the given key, or insert the value if the key is not present.

        Args:
            key: The key to look up or insert.
            value: The value to insert if the key is not present.

        Returns:
            The value associated with the key.
        """
        if key in self:
            return self[key]
        else:
            value = _vectorize(value)
            self[key] = value
            return value

    # --------------------------------------------------
    # Properties
    # --------------------------------------------------

    def keys(self):
        """Return a list of keys."""
        from capturegraph.data.containers.list import List

        return List(super().keys())

    def values(self):
        """Return a list of values."""
        from capturegraph.data.containers.list import List

        return List(super().values())

    def items(self):
        """Return a list of key-value pairs."""
        from capturegraph.data.containers.list import List

        return List(super().items())

    def __iter__(self) -> Iterator[V]:
        """Iterate over values (not keys) for tuple-unpacking semantics.

        Unlike standard dicts, Dict iterates over values. This enables
        unpacking syntax when used with ``cg.zip``::

            for src, dst in cg.zip(src=sources, dst=destinations):
                shutil.copy(src, dst)

        This makes Dict behave like a named tuple or record type—access
        fields by name (``row.src``) or unpack positionally.
        """
        return iter(super().values())

    # --------------------------------------------------
    # Exchanging Representations
    # --------------------------------------------------

    def to_dict(self) -> dict[str, V]:
        """Convert to a standard dict."""
        return _unwrap(self, replace_missing=None)

    def __repr__(self) -> str:
        return f"Dict({super().__repr__()})"

__getitem__(key) #

Get item by key. Returns Missing if key doesn't exist.

If key is an int, return the value at that index position.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def __getitem__(self, key: str | int) -> V | MissingType:
    """Get item by key. Returns Missing if key doesn't exist.

    If key is an int, return the value at that index position.
    """
    try:
        if isinstance(key, int):
            key = list(self.keys())[key]
            result = _vectorize(self[key])
            self[key] = result
            return result
        else:
            result = _vectorize(super().__getitem__(key))
            self[key] = result
            return result
    except (KeyError, IndexError) as e:
        return MissingType(e)

__getattr__(name) #

Get item by attribute name. Returns Missing if key doesn't exist.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def __getattr__(self, name: str) -> V | MissingType:
    """Get item by attribute name. Returns Missing if key doesn't exist."""
    if _is_dunder(name):
        raise AttributeError(
            f"'{type(self).__name__}' object has no attribute '{name}'"
        )

    try:
        result = _vectorize(self[name])
        self[name] = result
        return result
    except KeyError as e:
        return MissingType(e)

__setitem__(key, value) #

Set item by key.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def __setitem__(self, key: str, value: V) -> None:
    """Set item by key."""
    if not is_missing(value):
        super().__setitem__(key, value)

__setattr__(name, value) #

Set item by attribute name.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def __setattr__(self, name: str, value: V) -> None:
    """Set item by attribute name."""
    if not is_missing(value):
        self[name] = value

__delattr__(name) #

Delete item by attribute name. Silently ignores missing keys.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def __delattr__(self, name: str) -> None:
    """Delete item by attribute name. Silently ignores missing keys."""
    try:
        del self[name]
    except KeyError:
        pass

map(function) #

Apply a function to each value in this dict.

Keys are preserved; values are transformed.

Parameters:

Name Type Description Default
function Callable[[V], U]

A callable to apply to each value.

required

Returns:

Type Description
Dict[U]

Dict with same keys, transformed values.

Raises:

Type Description
TypeError

If function is not callable.

Example
d = Dict({"a": 1, "b": 2, "c": 3})
d.map(lambda x: x * 10)
# → Dict({'a': 10, 'b': 20, 'c': 30})

d.map(str)  # Apply built-in function
# → Dict({'a': '1', 'b': '2', 'c': '3'})
Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def map[U](self, function: Callable[[V], U]) -> "Dict[U]":
    """Apply a function to each value in this dict.

    Keys are preserved; values are transformed.

    Args:
        function: A callable to apply to each value.

    Returns:
        Dict with same keys, transformed values.

    Raises:
        TypeError: If function is not callable.

    Example:
        ```python
        d = Dict({"a": 1, "b": 2, "c": 3})
        d.map(lambda x: x * 10)
        # → Dict({'a': 10, 'b': 20, 'c': 30})

        d.map(str)  # Apply built-in function
        # → Dict({'a': '1', 'b': '2', 'c': '3'})
        ```
    """
    if not callable(function):
        raise TypeError(
            f"Dict.map requires a callable (function/lambda).\n"
            f"Got: {type(function).__name__} = {function!r}\n\n"
            f"Usage: d.map(lambda x: x * 2)"
        )

    results = {}
    for key, value in dict.items(self):
        try:
            result = _vectorize(function(value))
            results[key] = result
        except Exception as e:
            results[key] = MissingType(e)

    _check_all_missing(
        list(results.values()),
        f"function '{function.__name__ if hasattr(function, '__name__') else repr(function)}'",
    )

    return Dict(results)

map_leaves(function) #

Apply a function to each leaf element recursively.

Unlike map() which applies to top-level values, map_leaves() descends into nested List/Dict structures and applies the function to leaf values.

Parameters:

Name Type Description Default
function Callable[[Any], Any]

A callable to apply to each leaf element.

required

Returns:

Type Description
Dict[Any]

Nested Dict with same structure, function applied to leaves.

Example
# Nested structure - map_leaves descends into inner lists/dicts
d = Dict({"nums": [1, 2, 3], "value": 4})
d.map_leaves(lambda x: x * 10)
# → Dict({'nums': [10, 20, 30], 'value': 40})

# Compare with map which applies to top-level values only
d.map(sum)  # Would try sum(4) which fails
Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def map_leaves(self, function: Callable[[Any], Any]) -> "Dict[Any]":
    """Apply a function to each leaf element recursively.

    Unlike ``map()`` which applies to top-level values, ``map_leaves()``
    descends into nested List/Dict structures and applies the function to leaf values.

    Args:
        function: A callable to apply to each leaf element.

    Returns:
        Nested Dict with same structure, function applied to leaves.

    Example:
        ```python
        # Nested structure - map_leaves descends into inner lists/dicts
        d = Dict({"nums": [1, 2, 3], "value": 4})
        d.map_leaves(lambda x: x * 10)
        # → Dict({'nums': [10, 20, 30], 'value': 40})

        # Compare with map which applies to top-level values only
        d.map(sum)  # Would try sum(4) which fails
        ```
    """
    result = _map_leaves(self, function)

    _check_all_missing(
        list(result.values()) if isinstance(result, dict) else result,
        f"function '{function.__name__ if hasattr(function, '__name__') else repr(function)}'",
    )

    return result

with_key(key, value) #

Return a new Dict with an additional key-value pair.

Creates a shallow copy with the new key added (or updated if it exists).

Parameters:

Name Type Description Default
key str

The key to add or update.

required
value V

The value to associate with the key.

required

Returns:

Type Description
Dict[V]

A new Dict containing all existing entries plus the new key-value pair.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def with_key(self, key: str, value: V) -> "Dict[V]":
    """Return a new Dict with an additional key-value pair.

    Creates a shallow copy with the new key added (or updated if it exists).

    Args:
        key: The key to add or update.
        value: The value to associate with the key.

    Returns:
        A new Dict containing all existing entries plus the new key-value pair.
    """
    return Dict(self | {key: value})

get_or_insert(key, value) #

Get the value for the given key, or insert the value if the key is not present.

Parameters:

Name Type Description Default
key str

The key to look up or insert.

required
value V

The value to insert if the key is not present.

required

Returns:

Type Description
V

The value associated with the key.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def get_or_insert(self, key: str, value: V) -> V:
    """Get the value for the given key, or insert the value if the key is not present.

    Args:
        key: The key to look up or insert.
        value: The value to insert if the key is not present.

    Returns:
        The value associated with the key.
    """
    if key in self:
        return self[key]
    else:
        value = _vectorize(value)
        self[key] = value
        return value

keys() #

Return a list of keys.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def keys(self):
    """Return a list of keys."""
    from capturegraph.data.containers.list import List

    return List(super().keys())

values() #

Return a list of values.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def values(self):
    """Return a list of values."""
    from capturegraph.data.containers.list import List

    return List(super().values())

items() #

Return a list of key-value pairs.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def items(self):
    """Return a list of key-value pairs."""
    from capturegraph.data.containers.list import List

    return List(super().items())

__iter__() #

Iterate over values (not keys) for tuple-unpacking semantics.

Unlike standard dicts, Dict iterates over values. This enables unpacking syntax when used with cg.zip::

for src, dst in cg.zip(src=sources, dst=destinations):
    shutil.copy(src, dst)

This makes Dict behave like a named tuple or record type—access fields by name (row.src) or unpack positionally.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def __iter__(self) -> Iterator[V]:
    """Iterate over values (not keys) for tuple-unpacking semantics.

    Unlike standard dicts, Dict iterates over values. This enables
    unpacking syntax when used with ``cg.zip``::

        for src, dst in cg.zip(src=sources, dst=destinations):
            shutil.copy(src, dst)

    This makes Dict behave like a named tuple or record type—access
    fields by name (``row.src``) or unpack positionally.
    """
    return iter(super().values())

to_dict() #

Convert to a standard dict.

Source code in capturegraph-lib/capturegraph/data/containers/dict.py
def to_dict(self) -> dict[str, V]:
    """Convert to a standard dict."""
    return _unwrap(self, replace_missing=None)