You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
9.0 KiB
321 lines
9.0 KiB
4 years ago
|
# -*- coding: utf-8 -*-
|
||
|
"""
|
||
|
Method chaining interface.
|
||
|
|
||
|
.. versionadded:: 1.0.0
|
||
|
"""
|
||
|
|
||
|
from __future__ import absolute_import, print_function
|
||
|
|
||
|
import pydash as pyd
|
||
|
|
||
|
from .helpers import NoValue
|
||
|
|
||
|
|
||
|
__all__ = (
|
||
|
"chain",
|
||
|
"tap",
|
||
|
"thru",
|
||
|
)
|
||
|
|
||
|
|
||
|
class Chain(object):
|
||
|
"""Enables chaining of :attr:`module` functions."""
|
||
|
|
||
|
#: Object that contains attribute references to available methods.
|
||
|
module = pyd
|
||
|
|
||
|
def __init__(self, value=NoValue):
|
||
|
self._value = value
|
||
|
|
||
|
def value(self):
|
||
|
"""
|
||
|
Return current value of the chain operations.
|
||
|
|
||
|
Returns:
|
||
|
mixed: Current value of chain operations.
|
||
|
"""
|
||
|
return self(self._value)
|
||
|
|
||
|
def to_string(self):
|
||
|
"""
|
||
|
Return current value as string.
|
||
|
|
||
|
Returns:
|
||
|
str: Current value of chain operations casted to ``str``.
|
||
|
"""
|
||
|
return self.module.to_string(self.value())
|
||
|
|
||
|
def commit(self):
|
||
|
"""
|
||
|
Executes the chained sequence and returns the wrapped result.
|
||
|
|
||
|
Returns:
|
||
|
Chain: New instance of :class:`Chain` with resolved value from
|
||
|
previous :class:`Class`.
|
||
|
"""
|
||
|
return Chain(self.value())
|
||
|
|
||
|
def plant(self, value):
|
||
|
"""
|
||
|
Return a clone of the chained sequence planting `value` as the wrapped value.
|
||
|
|
||
|
Args:
|
||
|
value (mixed): Value to plant as the initial chain value.
|
||
|
"""
|
||
|
# pylint: disable=no-member,maybe-no-member
|
||
|
wrapper = self._value
|
||
|
wrappers = []
|
||
|
|
||
|
if hasattr(wrapper, "_value"):
|
||
|
wrappers = [wrapper]
|
||
|
|
||
|
while isinstance(wrapper._value, ChainWrapper):
|
||
|
wrapper = wrapper._value
|
||
|
wrappers.insert(0, wrapper)
|
||
|
|
||
|
clone = Chain(value)
|
||
|
|
||
|
for wrap in wrappers:
|
||
|
clone = ChainWrapper(clone._value, wrap.method)(*wrap.args, **wrap.kwargs)
|
||
|
|
||
|
return clone
|
||
|
|
||
|
@classmethod
|
||
|
def get_method(cls, name):
|
||
|
"""
|
||
|
Return valid :attr:`module` method.
|
||
|
|
||
|
Args:
|
||
|
name (str): Name of pydash method to get.
|
||
|
|
||
|
Returns:
|
||
|
function: :attr:`module` callable.
|
||
|
|
||
|
Raises:
|
||
|
InvalidMethod: Raised if `name` is not a valid :attr:`module` method.
|
||
|
"""
|
||
|
# Python 3.5 issue with pytest doctest call where inspect module tries
|
||
|
# to unwrap this class. If we don't return here, we get an
|
||
|
# InvalidMethod exception.
|
||
|
if name in ("__wrapped__",): # pragma: no cover
|
||
|
return cls
|
||
|
|
||
|
method = getattr(cls.module, name, None)
|
||
|
|
||
|
if not callable(method) and not name.endswith("_"):
|
||
|
# Alias method names not ending in underscore to their underscore
|
||
|
# counterpart. This allows chaining of functions like "map_()"
|
||
|
# using "map()" instead.
|
||
|
method = getattr(cls.module, name + "_", None)
|
||
|
|
||
|
if not callable(method):
|
||
|
raise cls.module.InvalidMethod(("Invalid pydash method: {0}".format(name)))
|
||
|
|
||
|
return method
|
||
|
|
||
|
def __getattr__(self, attr):
|
||
|
"""
|
||
|
Proxy attribute access to :attr:`module`.
|
||
|
|
||
|
Args:
|
||
|
attr (str): Name of :attr:`module` function to chain.
|
||
|
|
||
|
Returns:
|
||
|
ChainWrapper: New instance of :class:`ChainWrapper` with value passed on.
|
||
|
|
||
|
Raises:
|
||
|
InvalidMethod: Raised if `attr` is not a valid function.
|
||
|
"""
|
||
|
return ChainWrapper(self._value, self.get_method(attr))
|
||
|
|
||
|
def __call__(self, value):
|
||
|
"""
|
||
|
Return result of passing `value` through chained methods.
|
||
|
|
||
|
Args:
|
||
|
value (mixed): Initial value to pass through chained methods.
|
||
|
|
||
|
Returns:
|
||
|
mixed: Result of method chain evaluation of `value`.
|
||
|
"""
|
||
|
if isinstance(self._value, ChainWrapper):
|
||
|
# pylint: disable=maybe-no-member
|
||
|
value = self._value.unwrap(value)
|
||
|
return value
|
||
|
|
||
|
|
||
|
class ChainWrapper(object):
|
||
|
"""Wrap :class:`Chain` method call within a :class:`ChainWrapper` context."""
|
||
|
|
||
|
def __init__(self, value, method):
|
||
|
self._value = value
|
||
|
self.method = method
|
||
|
self.args = ()
|
||
|
self.kwargs = {}
|
||
|
|
||
|
def _generate(self):
|
||
|
"""Generate a copy of this instance."""
|
||
|
# pylint: disable=attribute-defined-outside-init
|
||
|
new = self.__class__.__new__(self.__class__)
|
||
|
new.__dict__ = self.__dict__.copy()
|
||
|
return new
|
||
|
|
||
|
def unwrap(self, value=NoValue):
|
||
|
"""
|
||
|
Execute :meth:`method` with :attr:`_value`, :attr:`args`, and :attr:`kwargs`.
|
||
|
|
||
|
If :attr:`_value` is an instance of :class:`ChainWrapper`, then unwrap it before calling
|
||
|
:attr:`method`.
|
||
|
"""
|
||
|
# Generate a copy of ourself so that we don't modify the chain wrapper
|
||
|
# _value directly. This way if we are late passing a value, we don't
|
||
|
# "freeze" the chain wrapper value when a value is first passed.
|
||
|
# Otherwise, we'd locked the chain wrapper value permanently and not be
|
||
|
# able to reuse it.
|
||
|
wrapper = self._generate()
|
||
|
|
||
|
if isinstance(wrapper._value, ChainWrapper):
|
||
|
# pylint: disable=no-member,maybe-no-member
|
||
|
wrapper._value = wrapper._value.unwrap(value)
|
||
|
elif not isinstance(value, ChainWrapper) and value is not NoValue:
|
||
|
# Override wrapper's initial value.
|
||
|
wrapper._value = value
|
||
|
|
||
|
if wrapper._value is not NoValue:
|
||
|
value = wrapper._value
|
||
|
|
||
|
return wrapper.method(value, *wrapper.args, **wrapper.kwargs)
|
||
|
|
||
|
def __call__(self, *args, **kwargs):
|
||
|
"""
|
||
|
Invoke the :attr:`method` with :attr:`value` as the first argument and return a new
|
||
|
:class:`Chain` object with the return value.
|
||
|
|
||
|
Returns:
|
||
|
Chain: New instance of :class:`Chain` with the results of :attr:`method` passed in as
|
||
|
value.
|
||
|
"""
|
||
|
self.args = args
|
||
|
self.kwargs = kwargs
|
||
|
return Chain(self)
|
||
|
|
||
|
|
||
|
class _Dash(object):
|
||
|
"""Class that provides attribute access to valid :mod:`pydash` methods and callable access to
|
||
|
:mod:`pydash` method chaining."""
|
||
|
|
||
|
def __getattr__(self, attr):
|
||
|
"""Proxy to :meth:`Chain.get_method`."""
|
||
|
return Chain.get_method(attr)
|
||
|
|
||
|
def __call__(self, value=NoValue):
|
||
|
"""Return a new instance of :class:`Chain` with `value` as the seed."""
|
||
|
return Chain(value)
|
||
|
|
||
|
|
||
|
def chain(value=NoValue):
|
||
|
"""
|
||
|
Creates a :class:`Chain` object which wraps the given value to enable intuitive method chaining.
|
||
|
Chaining is lazy and won't compute a final value until :meth:`Chain.value` is called.
|
||
|
|
||
|
Args:
|
||
|
value (mixed): Value to initialize chain operations with.
|
||
|
|
||
|
Returns:
|
||
|
:class:`Chain`: Instance of :class:`Chain` initialized with `value`.
|
||
|
|
||
|
Example:
|
||
|
|
||
|
>>> chain([1, 2, 3, 4]).map(lambda x: x * 2).sum().value()
|
||
|
20
|
||
|
>>> chain().map(lambda x: x * 2).sum()([1, 2, 3, 4])
|
||
|
20
|
||
|
|
||
|
>>> summer = chain([1, 2, 3, 4]).sum()
|
||
|
>>> new_summer = summer.plant([1, 2])
|
||
|
>>> new_summer.value()
|
||
|
3
|
||
|
>>> summer.value()
|
||
|
10
|
||
|
|
||
|
>>> def echo(item): print(item)
|
||
|
>>> summer = chain([1, 2, 3, 4]).for_each(echo).sum()
|
||
|
>>> committed = summer.commit()
|
||
|
1
|
||
|
2
|
||
|
3
|
||
|
4
|
||
|
>>> committed.value()
|
||
|
10
|
||
|
>>> summer.value()
|
||
|
1
|
||
|
2
|
||
|
3
|
||
|
4
|
||
|
10
|
||
|
|
||
|
.. versionadded:: 1.0.0
|
||
|
|
||
|
.. versionchanged:: 2.0.0
|
||
|
Made chaining lazy.
|
||
|
|
||
|
.. versionchanged:: 3.0.0
|
||
|
|
||
|
- Added support for late passing of `value`.
|
||
|
- Added :meth:`Chain.plant` for replacing initial chain value.
|
||
|
- Added :meth:`Chain.commit` for returning a new :class:`Chain` instance initialized with
|
||
|
the results from calling :meth:`Chain.value`.
|
||
|
"""
|
||
|
return Chain(value)
|
||
|
|
||
|
|
||
|
def tap(value, interceptor):
|
||
|
"""
|
||
|
Invokes `interceptor` with the `value` as the first argument and then returns `value`. The
|
||
|
purpose of this method is to "tap into" a method chain in order to perform operations on
|
||
|
intermediate results within the chain.
|
||
|
|
||
|
Args:
|
||
|
value (mixed): Current value of chain operation.
|
||
|
interceptor (callable): Function called on `value`.
|
||
|
|
||
|
Returns:
|
||
|
mixed: `value` after `interceptor` call.
|
||
|
|
||
|
Example:
|
||
|
|
||
|
>>> data = []
|
||
|
>>> def log(value): data.append(value)
|
||
|
>>> chain([1, 2, 3, 4]).map(lambda x: x * 2).tap(log).value()
|
||
|
[2, 4, 6, 8]
|
||
|
>>> data
|
||
|
[[2, 4, 6, 8]]
|
||
|
|
||
|
.. versionadded:: 1.0.0
|
||
|
"""
|
||
|
interceptor(value)
|
||
|
return value
|
||
|
|
||
|
|
||
|
def thru(value, interceptor):
|
||
|
"""
|
||
|
Returns the result of calling `interceptor` on `value`. The purpose of this method is to pass
|
||
|
`value` through a function during a method chain.
|
||
|
|
||
|
Args:
|
||
|
value (mixed): Current value of chain operation.
|
||
|
interceptor (callable): Function called with `value`.
|
||
|
|
||
|
Returns:
|
||
|
mixed: Results of ``interceptor(value)``.
|
||
|
|
||
|
Example:
|
||
|
|
||
|
>>> chain([1, 2, 3, 4]).thru(lambda x: x * 2).value()
|
||
|
[1, 2, 3, 4, 1, 2, 3, 4]
|
||
|
|
||
|
.. versionadded:: 2.0.0
|
||
|
"""
|
||
|
return interceptor(value)
|