Source code for lenses.hooks.hook_funcs

"""This module contains functions that you can hook into to allow various
lenses to operate on your own custom data structures.

You can hook into them by defining a method that starts with
``_lens_`` followed by the name of the hook function. So, for
example: the hook for ``lenses.hooks.contains_add`` is a method called
``_lens_contains_add``. This is the preferred way of hooking into this
library because it does not require you to have the lenses library as
a hard dependency.

These functions are all decorated with ``singledispatch``, allowing
you to customise the behaviour of types that you did not write. Be
warned that single dispatch functions are registered globally across
your program and that your function also needs to be able to deal with
subclasses of any types you register (or else register separate functions
for each subclass).

All of these hooks operate in the following order:

* Use an implementation registered with ``singledispatch.register``
  for the relevant type, if one exists.
* Use the relevant ``_lens_*`` method on the first object that was passed
  in, if it exists.
* Use a default implementation that is likely to work for most python
  objects, if one exists.
* Raise ``NotImplementedError``.
"""

import copy
import dataclasses
from builtins import setattr as builtin_setattr
from functools import singledispatch
from typing import (
    Any,
    Dict,
    FrozenSet,
    Iterable,
    Iterator,
    List,
    NamedTuple,
    Set,
    Tuple,
    TypeVar,
)

A = TypeVar("A")
B = TypeVar("B")


[docs] @singledispatch def setitem(self: Any, key: Any, value: Any) -> Any: """Takes an object, a key, and a value and produces a new object that is a copy of the original but with ``value`` as the new value of ``key``. The following equality should hold for your definition: .. code-block:: python setitem(obj, key, obj[key]) == obj This function is used by many lenses (particularly GetitemLens) to set items on states even when those states do not ordinarily support ``setitem``. This function is designed to have a similar signature as python's built-in ``setitem`` except that it returns a new object that has the item set rather than mutating the object in place. It's what enables the ``lens[some_key]`` functionality. The corresponding method call for this hook is ``obj._lens_setitem(key, value)``. The default implementation makes a copy of the object using ``copy.copy`` and then mutates the new object by setting the item on it in the conventional way. """ try: self._lens_setitem except AttributeError: selfcopy = copy.copy(self) selfcopy[key] = value return selfcopy else: return self._lens_setitem(key, value)
@setitem.register(bytes) def _bytes_setitem(self: bytes, key: int, value: int) -> bytes: data = bytearray(self) data[key] = value return bytes(data) @setitem.register(str) def _str_setitem(self: str, key: int, value: str) -> str: data = list(self) data[key] = value return "".join(data) @setitem.register(tuple) def _tuple_setitem(self: Tuple[A, ...], key: int, value: A) -> Tuple[A, ...]: return tuple(value if i == key else item for i, item in enumerate(self))
[docs] @singledispatch def setattr(self: Any, name: Any, value: Any) -> Any: """Takes an object, a string, and a value and produces a new object that is a copy of the original but with the attribute called ``name`` set to ``value``. The following equality should hold for your definition: .. code-block:: python setattr(obj, 'attr', obj.attr) == obj This function is used by many lenses (particularly GetattrLens) to set attributes on states even when those states do not ordinarily support ``setattr``. This function is designed to have a similar signature as python's built-in ``setattr`` except that it returns a new object that has the attribute set rather than mutating the object in place. It's what enables the ``lens.some_attribute`` functionality. The corresponding method call for this hook is ``obj._lens_setattr(name, value)``. The default implementation makes a copy of the object using ``copy.copy`` and then mutates the new object by calling python's built in ``setattr`` on it. """ if dataclasses.is_dataclass(self) and not isinstance(self, type): return dataclasses.replace(self, **{name: value}) try: self._lens_setattr except AttributeError: selfcopy = copy.copy(self) builtin_setattr(selfcopy, name, value) return selfcopy else: return self._lens_setattr(name, value)
@setattr.register(tuple) def _tuple_setattr_immutable(self: NamedTuple, name: str, value: A) -> NamedTuple: # setting attributes on a tuple probably means we really have a # namedtuple so we can use self._fields to understand the names data = (value if field == name else item for field, item in zip(self._fields, self)) return type(self)(*data)
[docs] @singledispatch def contains_add(self: Any, item: Any) -> Any: """Takes a collection and an item and returns a new collection of the same type that contains the item. The notion of "contains" is defined by the object itself; The following must be ``True``: .. code-block:: python item in contains_add(obj, item) This function is used by some lenses (particularly ContainsLens) to add new items to containers when necessary. The corresponding method call for this hook is ``obj._lens_contains_add(item)``. There is no default implementation. """ try: self._lens_contains_add except AttributeError: message = "Don't know how to add an item to {}" raise NotImplementedError(message.format(type(self))) else: return self._lens_contains_add(item)
@contains_add.register(list) def _list_contains_add(self: List[A], item: A) -> List[A]: return self + [item] @contains_add.register(tuple) def _tuple_contains_add(self: Tuple[A, ...], item: A) -> Tuple[A, ...]: return self + (item,) @contains_add.register(dict) def _dict_contains_add(self: Dict[A, Any], item: A) -> Dict[A, Any]: result = self.copy() result[item] = None return result @contains_add.register(set) def _set_contains_add(self: Set[A], item: A) -> Set[A]: return self | {item}
[docs] @singledispatch def contains_remove(self: Any, item: Any) -> Any: """Takes a collection and an item and returns a new collection of the same type with that item removed. The notion of "contains" is defined by the object itself; the following must be ``True``: .. code-block:: python item not in contains_remove(obj, item) This function is used by some lenses (particularly ContainsLens) to remove items from containers when necessary. The corresponding method call for this hook is ``obj._lens_contains_remove(item)``. There is no default implementation. """ try: self._lens_contains_remove except AttributeError: message = "Don't know how to remove an item from {}" raise NotImplementedError(message.format(type(self))) else: return self._lens_contains_remove(item)
@contains_remove.register(list) def _list_contains_remove(self: List[A], item: A) -> List[A]: return [x for x in self if x != item] @contains_remove.register(tuple) def _tuple_contains_remove(self: Tuple[A, ...], item: A) -> Tuple[A, ...]: return tuple(x for x in self if x != item) @contains_remove.register(dict) def _dict_contains_remove(self: Dict[A, B], item: A) -> Dict[A, B]: result = self.copy() del result[item] return result @contains_remove.register(set) def _set_contains_remove(self: Set[A], item: A) -> Set[A]: return self - {item}
[docs] @singledispatch def to_iter(self: Any) -> Any: """Takes an object and produces an iterable. It is intended as the inverse of the ``from_iter`` function. The reason this hook exists is to customise how dictionaries are iterated. In order to properly reconstruct a dictionary from an iterable you need access to both the keys and the values. So this function iterates over dictionaries by thier items instead. The corresponding method call for this hook is ``obj._lens_to_iter()``. The default implementation is to call python's built in ``iter`` function. """ try: self._lens_to_iter except AttributeError: return iter(self) else: return self._lens_to_iter()
@to_iter.register(dict) def _dict_to_iter(self: Dict[A, B]) -> Iterator[Tuple[A, B]]: return iter(self.items())
[docs] @singledispatch def from_iter(self: Any, iterable: Any) -> Any: """Takes an object and an iterable and produces a new object that is a copy of the original with data from ``iterable`` reincorporated. It is intended as the inverse of the ``to_iter`` function. Any state in ``self`` that is not modelled by the iterable should remain unchanged. The following equality should hold for your definition: .. code-block:: python from_iter(self, to_iter(self)) == self This function is used by EachLens to synthesise states from iterables, allowing it to focus every element of an iterable state. The corresponding method call for this hook is ``obj._lens_from_iter(iterable)``. There is no default implementation. """ try: self._lens_from_iter except AttributeError: message = "Don't know how to create instance of {} from iterable" raise NotImplementedError(message.format(type(self))) else: return self._lens_from_iter(iterable)
@from_iter.register(bytes) def _bytes_from_iter(self: bytes, iterable: Iterable[int]) -> bytes: return bytes(iterable) @from_iter.register(str) def _str_from_iter(self: str, iterable: Iterable[str]) -> str: return "".join(iterable) @from_iter.register(dict) def _dict_from_iter(self: Dict, iterable: Iterable[Tuple[A, B]]) -> Dict[A, B]: new = self.copy() new.clear() new.update(iterable) return new @from_iter.register(list) def _list_from_iter(self: List, iterable: Iterable[A]) -> List[A]: return list(iterable) @from_iter.register(set) def _set_from_iter(self: Set, iterable: Iterable[A]) -> Set[A]: return set(iterable) @from_iter.register(frozenset) def _frozenset_from_iter(self: FrozenSet, iterable: Iterable[A]) -> FrozenSet[A]: return frozenset(iterable) @from_iter.register(tuple) def _tuple_from_iter(self, iterable): if type(self) is tuple: return tuple(iterable) elif hasattr(self, "_make"): # this is probably a namedtuple return self._make(iterable) else: message = "Don't know how to create instance of {} from iterable" raise NotImplementedError(message.format(type(self)))