"""
Dictionary utilities for SmartSeeds.
Provides utilities for dict manipulation used across the library.
"""
from collections.abc import Callable, Mapping
from types import SimpleNamespace
from typing import Any
def filtered_dict(
data: Mapping[str, Any] | None,
filter_fn: Callable[[str, Any], bool] | None = None,
) -> dict[str, Any]:
"""
Return a dict filtered through ``filter_fn``.
Args:
data: Mapping with the original values (can be None).
filter_fn: Optional callable receiving ``(key, value)`` and returning
True if the pair should be kept. When None, the mapping is copied.
"""
if not data:
return {}
if filter_fn is None:
return dict(data)
return {k: v for k, v in data.items() if filter_fn(k, v)}
def make_opts(
incoming: Mapping[str, Any] | None,
defaults: Mapping[str, Any] | None = None,
*,
filter_fn: Callable[[str, Any], bool] | None = None,
ignore_none: bool = False,
ignore_empty: bool = False,
) -> SimpleNamespace:
"""
Merge ``incoming`` kwargs with ``defaults`` and return a SimpleNamespace.
``incoming`` values override defaults after optional filtering steps.
"""
merged_dict = _merge_kwargs(
incoming,
defaults,
filter_fn=filter_fn,
ignore_none=ignore_none,
ignore_empty=ignore_empty,
)
return SimpleNamespace(**merged_dict)
def _merge_kwargs(
incoming: Mapping[str, Any] | None,
defaults: Mapping[str, Any] | None,
*,
filter_fn: Callable[[str, Any], bool] | None = None,
ignore_none: bool = False,
ignore_empty: bool = False,
) -> dict[str, Any]:
combined_filter = _compose_filter(filter_fn, ignore_none, ignore_empty)
merged_defaults = dict(defaults or {})
filtered_incoming = filtered_dict(incoming, combined_filter)
return merged_defaults | filtered_incoming
def _compose_filter(
filter_fn: Callable[[str, Any], bool] | None,
ignore_none: bool,
ignore_empty: bool,
) -> Callable[[str, Any], bool] | None:
if not (filter_fn or ignore_none or ignore_empty):
return None
def predicate(key: str, value: Any) -> bool:
if ignore_none and value is None:
return False
if ignore_empty and _is_empty_value(value):
return False
if filter_fn and not filter_fn(key, value):
return False
return True
return predicate
def _is_empty_value(value: Any) -> bool:
"""Return True for values considered 'empty'."""
empty_sequences = (str, bytes, list, tuple, dict, set, frozenset)
if isinstance(value, empty_sequences):
return len(value) == 0
return False
[docs]
class SmartOptions(SimpleNamespace):
"""
Convenience namespace for option management.
Args:
incoming: Mapping with runtime kwargs.
defaults: Mapping with baseline options.
ignore_none: Skip incoming entries where the value is ``None``.
ignore_empty: Skip empty strings/collections from incoming entries.
"""
[docs]
def __init__(
self,
incoming: Mapping[str, Any] | None = None,
defaults: Mapping[str, Any] | None = None,
*,
ignore_none: bool = False,
ignore_empty: bool = False,
filter_fn: Callable[[str, Any], bool] | None = None,
):
merged = _merge_kwargs(
incoming,
defaults,
filter_fn=filter_fn,
ignore_none=ignore_none,
ignore_empty=ignore_empty,
)
object.__setattr__(self, "_data", dict(merged))
super().__init__(**merged)
[docs]
def as_dict(self) -> dict[str, Any]:
"""Return a copy of current options."""
return dict(self._data)
def __setattr__(self, key: str, value: Any):
if key == "_data":
object.__setattr__(self, key, value)
return
self._data[key] = value
super().__setattr__(key, value)
def __delattr__(self, key: str):
if key == "_data":
raise AttributeError("_data attribute cannot be removed")
self._data.pop(key, None)
super().__delattr__(key)
def dictExtract(mydict, prefix, pop=False, slice_prefix=True, is_list=False):
"""Return a dict of the items with keys starting with prefix.
:param mydict: sourcedict
:param prefix: the prefix of the items you need to extract
:param pop: removes the items from the sourcedict
:param slice_prefix: shortens the keys of the output dict removing the prefix
:param is_list: reserved for future use (currently not used)
:returns: a dict of the items with keys starting with prefix"""
# FIXME: the is_list parameter is never used.
lprefix = len(prefix) if slice_prefix else 0
cb = mydict.pop if pop else mydict.get
reserved_names = ["class"]
return dict(
[
(k[lprefix:] if k[lprefix:] not in reserved_names else f"_{k[lprefix:]}", cb(k))
for k in list(mydict.keys())
if k.startswith(prefix)
]
)