Metadata-Version: 2.1
Name: compose
Version: 1.0.0
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 still be weakly referenced and 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 real signature of the composed function (the signature
    of the "inner-most" function) is exposed in the standard
    Python way (by assigning that function in ``__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.

* ``__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.

* Storing the first function separately from the rest allows
  ``__call__`` to be written more efficiently, simply, and clearly.

* Treating ``compose()`` without any arguments as an error, instead
  of as producing a no-op passthrough identity function, because:

  1. It avoids turning mistakes into silent misbehavior by default.

  2. It is the more flexible way: people can do

     .. code:: python

         compose = partial(compose, identity)

     but going the other way is less trivial.

* Despite ``compose()`` being an error, ``__init__(self, *functions)__``
  is used instead of ``__init__(self, function, *functions)__``
  to produce a reliably nice and clear error message for that error.

* Using ``functools.recursive_repr`` if available because if recursion
  somehow ever 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 for
  that is a separate polyfil, monkey-patched or added in manually.

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

  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.

  This is the same care we see taken in the
  implementation of ``functools.partial``.

* Not using ``__slots__`` because:

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

  2. ``__wrapped__`` can be implemented with ``__getattr__``,
     but this would cause an inconsistent error string or traceback
     when trying to get non-existent attributes relative to other
     typical objects, and did not seem to actually perform better.

  3. Due to the above two reasons, ``__dict__`` will always be created
     and initialized in ``__init__``, so it would not save space.

  4. For what ``compose`` is doing, using ``__slots__`` does not
     seem to significantly increase execution speed anyway.


