Source code for elasticai.creator.ir.attribute

from collections.abc import Iterable, Iterator, Mapping
from typing import Any, Self, cast, overload

type AttributeBaseData = float | int | str | bool

type AttributeTuple = tuple["Attribute", ...]

type Attribute = AttributeBaseData | "AttributeTuple" | "AttributeMapping"


[docs] class AttributeMapping(Mapping[str, Attribute]): def __init__(self, **kwargs: Attribute) -> None: self._mapping = dict(kwargs)
[docs] def __getitem__(self, key: str) -> Any: # There is no feasible way to type this properly. # As values are highly dynamic. TypedDict would not # work here because they are mutable and would # require all keys to be known at runtime adding # unreasonable overhead for users. Alternatively, # we could return Attribute, but that would will # result in many false positives in type checking. # Users would have to cast the result almost everytime. return self._mapping[key]
[docs] def __iter__(self) -> Iterator[str]: return iter(self._mapping)
[docs] def __len__(self) -> int: return len(self._mapping)
[docs] def __eq__(self, other: object) -> bool: if isinstance(other, AttributeMapping): return self._mapping == other._mapping return False
[docs] def __repr__(self) -> str: return f"AttributeMapping({repr(self._mapping)})"
[docs] def drop(self, key: str) -> Self: new_dict = {k: v for k, v in self._mapping.items() if k != key} return type(self)(**new_dict)
[docs] def __or__(self, other: object) -> Self: if not isinstance(other, Mapping): return NotImplemented if len(other) == 0: return self return type(self)( **(self._mapping | dict(cast(Mapping[str, Attribute], other))) )
[docs] def update_path(self, path: tuple[str, ...], value: Attribute) -> Self: """update the entry found when following the path into nested Mappings""" def path_to_dict(path): if len(path) == 1: return {path[0]: value} return {path[0]: path_to_dict(path[1:])} return self.merge(path_to_dict(path))
[docs] def merge(self, other: Mapping) -> Self: """merge over nested mappings recursively So instead of replacing a value found under a key, this checks wether that value is again an AttributeMapping and if so, updates it by the corresponding Mapping found in other. This happens recursively. Opposed to that `new_with` and the `|` operator only allow to update values in the most outer AttributeMapping. Therefore using these to update a value in a nested structure, users would have to recreate the whole outer mapping structure until they reach the mapping they want to update. Use Case: If you would want to write ```python data = dict(a=dict(b=1, c=2)) data["a"]["b"] = 3 assert data["a"]["c"] == 2 ``` You can instead ```python data = AttributeMapping(a=AttributeMapping(b=1, c=2)) data = data.merge(dict(a=dict(b=3))) assert data["a"]["c"] == 2 ``` If you want to update a single value, you can use the `update_path()` function instead to avoid having to build all the nested dictionaries. """ mapping = dict((k, v) for k, v in self._mapping.items()) for k in mapping: if k in other: item = mapping[k] if isinstance(item, AttributeMapping): mapping[k] = item.merge(other[k]) else: mapping[k] = other[k] return type(self)(**mapping)
[docs] def new_with(self, **kwargs: Attribute) -> "AttributeMapping": """replace key, value pairs in self by key, value pairs in kwargs""" return self | kwargs
[docs] @classmethod def from_dict(cls, data: dict) -> "AttributeMapping": @overload def to_attribute(data: dict) -> AttributeMapping: ... @overload def to_attribute(data: Attribute | list) -> Attribute: ... def to_attribute(data: Attribute | dict | list) -> Attribute: if isinstance(data, dict): return cls(**{k: to_attribute(v) for k, v in data.items()}) elif isinstance(data, list): return tuple(to_attribute(v) for v in data) else: return data return to_attribute(data)
type AttributeConvertable = ( Attribute | Iterable[AttributeConvertable] | Mapping[str, AttributeConvertable] )
[docs] def is_attribute(obj: object) -> bool: if isinstance(obj, AttributeMapping): return True if isinstance(obj, tuple): if len(obj) == 0: return True else: return is_attribute(obj[0]) if isinstance(obj, float | int | str | bool): return True return False
@overload def attribute(**kwargs: AttributeConvertable) -> AttributeMapping: ... @overload def attribute( mapping: Mapping[str, AttributeConvertable], /, **kwargs: AttributeConvertable ) -> AttributeMapping: ... @overload def attribute(attribute: Attribute, /) -> Attribute: ...
[docs] def attribute( arg: Mapping[str, AttributeConvertable] | AttributeMapping | Attribute | None = None, /, **kwargs: AttributeConvertable, ) -> AttributeMapping | tuple[Attribute] | Attribute: """Create AttributeMapping from other (native) data types recursively. The implementation assumes that any encountered AttributeMapping objects are correct, ie. they only contain Attributes themselves. """ def convert(arg): if not isinstance(arg, AttributeMapping): if isinstance(arg, Mapping): return AttributeMapping(**{k: attribute(arg[k]) for k in arg}) if isinstance(arg, Iterable) and not isinstance(arg, str): return tuple(convert(item) for item in arg) # type: ignore return arg if isinstance(arg, Mapping): if len(kwargs) > 0: arg = kwargs | dict(arg) return convert(arg) elif arg is None: return convert(kwargs) else: if len(kwargs) > 0: raise TypeError( "unsupported call of attribute, only use kwargs if you are creating a mapping" ) return convert(arg)