Source code for magic_dot
"""Top-level package for Magic Dot."""
__author__ = """Mark Eklund"""
__email__ = "magic_dot@patnan.com"
__version__ = "0.2.0"
__all__ = ["MagicDot", "NOT_FOUND"]
from typing import Optional, Any
from .exceptions import NotFound
class _NotFound:
def __bool__(self):
raise RuntimeError("NOT_FOUND does not support truthy operations.")
def __repr__(self):
return "magic_dot.NOT_FOUND"
NOT_FOUND = _NotFound()
"""The default return value when an item is not found."""
[docs]class MagicDot:
"""A wrapper that allows data extraction without all the `try` and `except` overhead.
Args:
data: The data to be wrapped by MagicDot
exception: Selects if a NotFound exception to be raised when NOT_FOUND happens during extraction.
iter_nf_as_empty: Selects if iterating over NOT_FOUND should return an empty iterator.
Note:
The MagicDot class has no public attributes. Any access to an attribute will be
mapped to the underlying data and used to extract a new MagicDot structure.
"""
def __init__(self, data: Any, exception: bool = False, iter_nf_as_empty: bool = False):
self.__data = data
self.__exception = exception
self.__iter_nf_as_empty = iter_nf_as_empty
def __create_child(
self, data, exception: Optional[bool] = None, iter_nf_as_empty: Optional[bool] = None
):
exception = self.__exception if exception is None else exception
iter_nf_as_empty = self.__iter_nf_as_empty if iter_nf_as_empty is None else iter_nf_as_empty
return MagicDot(data, exception=exception, iter_nf_as_empty=iter_nf_as_empty)
def __bool__(self):
raise RuntimeError(
"MagicDot does not support truthy operations. Extract data with .get() first."
)
def __getattr__(self, name):
data = NOT_FOUND
try:
data = getattr(self.__data, name)
except AttributeError:
try:
return self.__getitem__(name)
except NotFound:
raise NotFound from None
return self.__create_child(data)
def __getitem__(self, key):
try:
data = self.__data[key]
except (AttributeError, KeyError, TypeError):
if self.__exception:
raise NotFound from None
else:
data = NOT_FOUND
return self.__create_child(data)
def __iter__(self):
"""Contains iterator creatiion support for MagicDot.
If an attempt is made to iterate over a MagicDot instance and the data
is iterable, this supports walking those contents where the contents
will also be wrapped in the MagicDot structure.
If the data contained is NOT_FOUND, the current iter_nf_as_empty state
of the MagicDot instance is checked. If not set, a TypeError will be
raised. If set, an empty iterator will be returned.
Returns:
The data held withing this MagicDot instance.
"""
if self.__data is NOT_FOUND and self.__iter_nf_as_empty:
return iter(())
else:
return (self.__create_child(x) for x in self.__data)
[docs] def exception(self, exception: bool = True) -> "MagicDot":
"""Enable or disable exceptions when NOT_FOUND is encountered.
NOT_FOUND is encountered whenever code extracts additional data
from the encapsulated data. Some examples would be: :::
>>> from magic_dot import MagicDot
>>> md = MagicDot([1])
>>> md = md.exception()
>>> md['nonexistent']
(raises magic_dot.exceptions.NotFound)
>>> md.nonexistent
(raises magic_dot.exceptions.NotFound)
>>> md.pluck('nonexistent')
(raises magic_dot.exceptions.NotFound)
Without exceptions enabled, the above would respectively return
MagicDot(NOT_FOUND), MagicDot(NOT_FOUND) and MagicDot([NOT_FOUND]).
Args:
exception: Enable or disable exceptions when NOT_FOUND is encountered.
Returns:
If the exception changes, a new MagicDot will be returned. Otherwise,
this returns self.
"""
if self.__exception == exception:
return self
else:
return self.__create_child(self.__data, exception=exception)
[docs] def iter_nf_as_empty(self, iter_nf_as_empty: bool = True) -> "MagicDot":
"""Enable or disable empty iterator support for NOT_FOUND data.
By default, if a request is made to iterate over a NOT_FOUND, a
TypeError is raised. With this set, the iteration will act as
if this was an empty array. :::
>>> from magic_dot import MagicDot
>>> x = MagicDot(1)
>>> for y in x.nonexistent:
>>> ...
(Raises TypeError)
>>> x = MagicDot(1).iter_nf_as_empty()
>>> for y in x.nonexistent:
>>> ...
(Skips the contents of the for loop.)
Args:
iter_nf_as_empty: Enable or Disable
Returns:
If the iter_nf_as_empty changes, a new MagicDot will be returned. Otherwise,
this returns self.
"""
if self.__iter_nf_as_empty == iter_nf_as_empty:
return self
else:
return self.__create_child(self.__data, iter_nf_as_empty=iter_nf_as_empty)
[docs] def get(self, not_found: Any = NOT_FOUND):
"""Gets the encapsulated data in this MagicDot.
This is either the encapsulated data or NOT_FOUND if extraction
failed.
Args:
default: Selects what to return if the encapsulated data was NOT_FOUND.
Returns:
The encaapsulated data in this MagicDot instance.
"""
if self.__data is NOT_FOUND:
return not_found
else:
return self.__data
[docs] def pluck(self, name: str):
"""Extracts **name** from an encapsulated data (which must be iterable) into an array.
For each item in the encapsulated data iterable, extract the named attribute
or key. If they are not available, either extract NOT_FOUND or raise an exception
depending on the current .exception() configuration.
If the encapsulated date is not iterable and not NOT_FOUND, a TypeError will be raised.
If the encapsulated data is NOT_FOUND, either raise a TypeError, or return an empty list
depending on the .iter_nf_as_empty() setting.
Args:
text: The attribute or key to pluck from the array.
Returns:
A new MagicDot containing the resulting array with plucked data.
"""
data = []
if not (self.__data == NOT_FOUND and self.__iter_nf_as_empty):
for x in self.__data:
try:
d = getattr(x, name)
except AttributeError:
try:
d = x[name]
except (AttributeError, KeyError, TypeError):
if self.__exception:
raise NotFound from None
else:
d = NOT_FOUND
data.append(d)
return self.__create_child(data)