Metadata-Version: 2.1
Name: compose
Version: 1.1.1
Summary: The classic ``compose``, with all the Pythonic features.
Home-page: https://github.com/mentalisttraceur/python-compose
Author: Alexander Kozhevnikov
Author-email: mentalisttraceur@gmail.com
License: 0BSD (BSD Zero Clause License)
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.5
Classifier: Programming Language :: Python :: 2.4
Classifier: Programming Language :: Python :: 2.3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: IronPython
Classifier: Programming Language :: Python :: Implementation :: Jython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: Implementation :: Stackless
Classifier: Operating System :: OS Independent

compose
=======

The classic ``compose``, with all the Pythonic features.

This ``compose`` follows the lead of ``functools.partial``
and returns callable ``compose`` objects which:

* have a regular and unambiguous ``repr``,
* retain correct signature introspection,
* allow introspection of the composed callables,
* can be type-checked,
* can be weakly referenced,
* can have attributes,
* will merge when nested, and
* can be pickled (if all composed callables can be pickled).

This ``compose`` also fails fast with a ``TypeError`` if any
argument is not callable, or when called with no arguments.


Versioning
----------

This library's version numbers follow the `SemVer 2.0.0 specification
<https://semver.org/spec/v2.0.0.html>`_.

The current version number is available in the variable ``__version__``,
as is normal for Python modules.


Installation
------------

::

    pip install compose


Usage
-----

Import ``compose``:

.. code:: python

    from compose import compose

All the usual function composition you know and love:

.. code:: python

    >>> def double(x):
    ...     return x * 2
    ...
    >>> def increment(x):
    ...     return x + 1
    ...
    >>> double_then_increment = compose(increment, double)
    >>> double_then_increment(1)
    3

Of course any number of functions can be composed:

.. code:: python

    >>> def double(x):
    ...     return x * 2
    ...
    >>> times_eight = compose(douple, double, double)
    >>> times_16 = compose(douple, double, double, double)

We still get the correct signature introspection:

.. code:: python

    >>> def f(a, b, c=0, **kwargs):
    ...     pass
    ...
    >>> def g(x):
    ...     pass
    ...
    >>> g_of_f = compose(g, f)
    >>> import inspect
    >>> inspect.signature(g_of_f)
    <Signature (a, b, c=0, **kwargs)>

And we can inspect all the composed callables:

.. code:: python

    >>> g_of_f.functions  # in order of execution:
    (<function f at 0x4048e6f0>, <function g at 0x405228e8>)

When programmatically inspecting arbitrary callables, we
can check if we are looking at a ``compose`` instance:

.. code:: python

    >>> isinstance(g_of_f, compose)
    True


Design Decisions
----------------

* The result of ``compose`` should be a drop-in replacement to
  functions in as many code paths as possible. Therefore:

  * The proper signature of the composed function is exposed
    in the standard Python way (by exposing the "inner-most"
    function as the attribute ``__wrapped__``).

  * Arbitrary attribute assignment (``__dict__``) should work,
    because Python allows people to do that to functions.

  * Weak references (``__weakref__``) are supported,
    because Python allows weakly referencing functions.

* Failing-fast as much as possible because that is important
  to help debugging by keeping errors local to their causes.

* Treating ``compose()`` with no arguments as an error, instead
  of as implicitly composing with an identity function, because:

  * It avoids turning mistakes into silent misbehavior by default.

  * People who want the other behavior can more trivially build
    it on top of this behavior than the other way around:

    .. code:: python

        compose = partial(compose, identity)

* Doing ``__init__(self, *functions)`` instead of
  ``__init__(self, function, *functions)`` because:

  * It makes the signature and docstring more correctly hint that the
    first function argument is not special or different from the rest.

  * It allows manually raising an error with a clearer and more
    helpful message if ``compose()`` is called with no arguments.

* Using ``functools.recursive_repr`` if available because if recursion
  happens, having a working and recursion-safe ``__repr__`` would
  likely be extremely helpful for debugging and code robustness.

  Not going beyond that because the code involved would be complex and
  not portable across Python implementations, and the right place to
  solve that is a separate polyfil if at all possible.

* ``self`` has to be a positional-only argument of ``__call__``
  to make ``__call__`` properly transparent in all cases.

  If the user makes a typo, ``**``-splats arguments, or otherwise
  ends up passing ``self`` in ``kwargs``, maybe even intentionally,
  function composition should still work correctly - in this case,
  silent seemingly-successful unintended misbehavior would be awful.

  If the user uses ``compose`` to implement methods, the ``self``
  argument to that method going through ``compose`` will normally
  be a positional argument, but ideally should be passed through
  transparently even if not, to match how normal methods work.

* Manually getting ``self`` from ``*args`` in ``__call__``
  **portably** makes ``self`` a positional-only argument.

* Optimization priorities are:

  1. "Optimize for optimization": implementing the essential logic
     of the intended behavior in as clearly and simply as possible,
     because that helps optimizers.

  2. ``__call__``, because that is the code path which can only be
     extracted from hot loops or other spots where performance
     matters by not using ``compose`` at all.

  3. ``__init__``, because composing callables together is also
     essential to actually using this, and in some cases cannot
     be pulled out of performance-sensitive code paths.

  4. Not storing data redundantly, because memory-constrained
     systems are a thing, and it is much easier to add redundant
     data on top of an implementation than it is to remove it.

* Flattening nested instances of ``compose`` because

  * It makes the repr much more helpful for debugging and
    interactive usage. It is more common to want to know
    what the actual composed callable does, than to know
    the tree of nested ``compose`` calls that created it.

  * ``__call__`` performance is more important in typical cases
    than runspace efficiency (see above performance priorities).

  * Intermediate composed functions that are never used
    after composing them with something else can just
    be deleted so that they don't take up memory.

  * It is more trivial to prevent the flattening by using a
    simple wrapper function or class on this implementation
    than flattening on top of a not-flattening one.

* Using tuples and a read-only ``@property`` for storing
  and exposing the composed functions because:

  * Immutability helps reasoning about and validating code.

  * Immutable types provide more optimisation opportunities
    that a Python implementation could take advantage of.

  * Discouraging mutations encourages optimizer-friendly code.

  * Mutability is normally not needed for composed functions.

  * ``functools.partial`` also only exposes read-only attributes.

  * Immutability now is forward-compatible with mutability later;
    changing mutability into immutability is a breaking change.

  * A simple mutable variant can be implemented trivially
    on top of the current immutable ``compose``:

    .. code:: python

        class compose(compose):
            def __init__(self, *functions):
                super().__init__(*functions)
                self._wrappers = list(self._wrappers)

* Generating the ``functions`` attribute tuple every time instead
  of caching it, because:

  * This implementation prevents *accidental* inconsistencies
    if someone intentionally bypasses the immutability.

    (Intentional inconsistencies that can only be introduced *by
    deliberately modifying the implementation* are fine. What's
    important is minimizing the surface area for errors and
    debugging difficulty being introduced by merely *forgetting*
    or *not realizing* the need to keep things consistent.)

  * The performance priority of not storing data redundantly as
    part of composing and calling is usually more important
    than introspection performance, *especially* because the
    caching can be implemented much more trivially on top of
    this implementation than preventing caching would be if
    it was implemented in ``compose``.

  * A caching variant can be implemented fairly easily
    on top of the current non-caching ``compose``:

    .. code:: python

        import functools

        class compose(compose):
            @property
            @functools.lru_cache(maxsize=1)
            def functions(self):
                return super().functions

* Storing the first function separately from the rest allows
  ``__call__`` to be more efficient, simpler, and clearer.

* ``__wrapped__`` cannot be a ``@property`` because several
  functions in the standard library cannot handle that.

  As a minor point, "portability conservatism": it is safer
  to bet on the most conservative feature-set possible.

* Not using ``__slots__`` because of many reasons adding up:

  * ``__call__`` performance is basically the same, at best
    only marginally better, when using ``__slots__``.

    (``__init__`` sees a better but still small improvement.)

    On PyPy, ``__call__`` ends up getting optimized to the
    same blazingly performant code with or without
    ``__slots__`` - makes no difference. On CPython, the
    no-``__slots__`` variant actually performs better once
    ``__wrapped__`` is supported (see below).

  * ``__slots__`` forces more code to support older
    pickle protocols for those who might need that.

    (But one-liner ``__getstate__`` and ``__setstate__`` that
    just handle the 3-tuple of ``_wrapped``, ``_wrappers``,
    and ``__dict__`` would work, and are probably optimal.)

  * ``__wrapped__`` cannot be in ``__slots__`` because that has
    the same problem as making it a ``@property`` (see above).

  * ``__wrapped__`` can be implemented with ``__getattribute__``
    redirecting to a slotted ``_wrapped``, but implementing the
    ``__getattribute__`` function is much slower than just not
    using ``__slots__`` at all, since it proxies all attribute
    access.

  * ``__wrapped__`` can be implemented with ``__getattr__``
    redirecting to a slotted ``_wrapped``, although once
    upon a type Transcrypt didn't support ``__getattr__``,
    which is a great example for portability conservatism.

    Moreover, testing shows that adding ``__getattr__`` to
    a class still makes the whole slotted implementation
    slower somehow (merely removing ``__getattr__`` from
    the class definition makes tests which never use
    ``__getattr__`` go faster, although there is no
    reason at the level of Python semantics for why this
    should be the case). Once PyPy warms up, this is
    negligible, and on CPython it is relatively minor,
    but it is still strictly worse on most systems tested.

  * ``__wrapped__`` can be just a *copy* of a slotted attribute,
    but the same reasons apply against this as against making
    ``functions`` a cached copy.

  * If ``__wrapped__`` is stored in ``__dict__`` and is always
    set in ``__init__``, a lot of the memory savings from
    using ``__slots__`` are negated too.

* When flattening composed ``compose`` instances in ``__init__``,
  ``__wrapped__`` and ``_wrapped`` attributes are used instead
  of the ``functions`` attribute, because:

  * Speed of composition significantly increases, given
    that ``functions`` is generated every time.

  * The loss of symmetry between this and the public interface
    of the ``functions`` attribute is unfortunate, because it
    forces any subclasses to use ``_wrappers`` consistently
    with ``compose`` instead of just ``functions``, but the
    advantage seems to be worthwhile.

* The ``functions`` generation uses ``tuple(self._wrappers)``
  instead of just ``self._wrappers`` to enable subclasses
  that make ``_wrappers`` something other than a tuple to
  still work properly.

  A subclass which wants ``functions`` itself to be something
  other than a tuple would need to provide that themselves,
  but this should cover at least some cases.

  Importantly, because tuples are immutable, calling ``tuple``
  on a tuple just returns the same tuple instead of copying in
  CPython, and other Pythons can do that optimization too.

* Not providing a separate ``rcompose`` (which would compose
  its arguments in reverse order) for now, because it is
  trivial to implement on top of ``compose`` if needed:

  .. code:: python

      def rcompose(*functions):
          return compose(*reversed(functions))

* Not providing a separate "just a normal function" variant for now,
  because it is trivial to implement on top of ``compose`` if needed:

  .. code:: python

      def fcompose(*functions):
          composed = compose(*functions)
          return lambda *args, **kwargs: composed(*args, **kwargs)

* Not providing descriptor support like ``functools.partialmethod``
  for now, until a need for it becomes apparent which a "normal
  function" variant (see last point) does not satisfy well enough.

* Not providing an ``async``/``await`` variant for now, because
  it is not yet clear if it is useful enough or if the best
  place for it is this package, and in the meantime it can be
  implemented on top of ``compose`` if needed:

  .. code:: python

      import inspect

      class acompose(compose):
          async def __call__(self, /, *args, **kwargs):
              result = self.__wrapped__(*args, **kwargs)
              if inspect.isawaitable(result):
                  result = await result
              for function in self._wrappers:
                  result = function(result)
                  if inspect.isawaitable(result):
                      result = await result
              return result


