Skip to main content

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

Project description

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 throws a TypeError when called with no arguments or with any non-callable arguments.

For async/await support, the right behavior of function composition depends on what you are doing, so variants of compose are included for those cases.

Versioning

This library’s version numbers follow the SemVer 2.0.0 specification.

Installation

pip install compose

Usage

Basics

Import compose:

from compose import compose

All the usual function composition you know and love:

>>> 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:

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

We still get the correct signature introspection:

>>> 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:

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

compose instances flatten when nested:

>>> times_eight_times_two = compose(double, times_eight)
>>> times_eight_times_two.functions == times_16.functions
True

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

>>> isinstance(g_of_f, compose)
True

async/await

We can compose async code by using acompose or sacompose (they are mostly the same):

>>> import asyncio
>>> from compose import acompose
>>>
>>> async def get_data():
...     # pretend this data is fetched from some async API
...     await asyncio.sleep(0)
...     return 42
...
>>> get_and_double_data = acompose(double, get_data)
>>> asyncio.run(get_and_double_data())
84

acompose and sacompose can compose any number of async and regular functions, in any order:

>>> async def async_double(x):
...     await asyncio.sleep(0)
...     return x * 2
...
>>> async_times_16 = acompose(async_double, double, async_double, double)
>>> asyncio.run(async_times_16(1))
16

sacompose provides a different way of handling a corner case that arises when composing functions that we get from users or other code: what if every function we receive to compose is regular, not async, but we want to support async?

  • acompose handles that case by returning an awaitable anyway - so we can just write simple code that calls await in all cases. This is the best choice for function composition that we know will be used in async code.

  • sacompose handles that case by returning a callable which will sometimes behave in an async way, by returning an awaitable only if any of the composed functions return an awaitable. This is needed to simplify reusable helper code that can’t know if it is composing for regular or async code:

    >>> from compose import sacompose
    >>>
    >>> regular_times_4 = sacompose(double, double)
    >>> awaitable_times_4 = sacompose(double, async_double)
    >>>
    >>> # Right:
    >>> regular_times_4(1) == 4
    >>> await awaitable_times_4(1) == 4
    >>>
    >>> # Wrong (TypeError from the `==`, and coroutine not awaited):
    >>> awaitable_times_4(1) == 4
    >>> # Wrong (TypeError from the `await`):
    >>> await regular_times_4(1) == 4

acompose and sacompose instances flatten when nested:

>>> acompose(f, acompose(f, f)).functions == (f, f, f)
True
>>> acompose(sacompose(f, f), f).functions == (f, f, f)
True
>>> sacompose(acompose(f, f), f).functions == (f, f, f)
True
>>> sacompose(f, sacompose(f, f)).functions == (f, f, f)
True

But compose instances don’t flatten when nested into acompose and sacompose, and vice versa:

>>> acompose(g_of_f).functions
(compose(<function f at 0x4048e6f0>, <function g at 0x405228e8>),)
>>> sacompose(g_of_f).functions
(compose(<function f at 0x4048e6f0>, <function g at 0x405228e8>),)
>>> compose(acompose(g, f)).functions
(acompose(<function f at 0x4048e6f0>, <function g at 0x405228e8>),)
>>> compose(sacompose(g, f)).functions
(sacompose(<function f at 0x4048e6f0>, <function g at 0x405228e8>),)

compose, acompose, and sacompose instances are all distinct types:

>>> isinstance(g_of_f, compose)
True
>>> isinstance(g_of_f, (acompose, sacompose))
False
>>> isinstance(async_times_16, acompose)
True
>>> isinstance(async_times_16, (compose, sacompose))
False
>>> isinstance(awaitable_times_4, sacompose)
True
>>> isinstance(awaitable_times_4, (compose, acompose))
False

Recipes

  • If you want composing zero functions to be the identity function:

    from functools import partial
    
    def identity(x):
        return x
    
    icompose = partial(compose, identity)
  • To compose arguments in reverse order:

    def rcompose(*functions):
        return compose(*reversed(functions))
  • When you need composition to return a normal function:

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

compose-1.3.1.tar.gz (5.8 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

compose-1.3.1-py38-none-any.whl (5.1 kB view details)

Uploaded Python 3.8

compose-1.3.1-py35-none-any.whl (5.2 kB view details)

Uploaded Python 3.5

compose-1.3.1-py2.py30-none-any.whl (4.8 kB view details)

Uploaded Python 2Python 3.0

File details

Details for the file compose-1.3.1.tar.gz.

File metadata

  • Download URL: compose-1.3.1.tar.gz
  • Upload date:
  • Size: 5.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.10.4

File hashes

Hashes for compose-1.3.1.tar.gz
Algorithm Hash digest
SHA256 c1bb422ffbba4fd4760e4185c0a09977ae719866e5dfa3b0bd06337aa081ee41
MD5 764ec2badf39b761b12d1293d29ef5aa
BLAKE2b-256 bd365d770a95c232f6b7d5e28c32672a64c61949ae5bca957a4e75e7f3c03773

See more details on using hashes here.

File details

Details for the file compose-1.3.1-py38-none-any.whl.

File metadata

  • Download URL: compose-1.3.1-py38-none-any.whl
  • Upload date:
  • Size: 5.1 kB
  • Tags: Python 3.8
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.10.4

File hashes

Hashes for compose-1.3.1-py38-none-any.whl
Algorithm Hash digest
SHA256 63f69528c3d4476e38d8f3eaabe2eb22d90b13c8cb4295ecc51ece8ddd3330d2
MD5 cb2794a58f483d12af108e0bb53b6a8d
BLAKE2b-256 5d5019eaaba9432e42c560a1f020bd4b6ec1235889b546fa87d37975a82cd198

See more details on using hashes here.

File details

Details for the file compose-1.3.1-py35-none-any.whl.

File metadata

  • Download URL: compose-1.3.1-py35-none-any.whl
  • Upload date:
  • Size: 5.2 kB
  • Tags: Python 3.5
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.10.4

File hashes

Hashes for compose-1.3.1-py35-none-any.whl
Algorithm Hash digest
SHA256 3c0b6862e24bc5f77532b5108acac50809d2e96bbc24ed6b39cde2cfd46d18da
MD5 7237b1989a0d45ab6bff0d29a5976a15
BLAKE2b-256 5547dc5a231d89f7f9e17ef3c224bf0445ee89b4d620b31013145fb299f60c6c

See more details on using hashes here.

File details

Details for the file compose-1.3.1-py2.py30-none-any.whl.

File metadata

  • Download URL: compose-1.3.1-py2.py30-none-any.whl
  • Upload date:
  • Size: 4.8 kB
  • Tags: Python 2, Python 3.0
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.10.4

File hashes

Hashes for compose-1.3.1-py2.py30-none-any.whl
Algorithm Hash digest
SHA256 5b004946d53e3b2ae32cf7f4ddf2569a99a09efb6b8a2604cda5be9aac895ce2
MD5 6c6f9b2aa5f284a79aa88f5f3862e4cf
BLAKE2b-256 03295108b731e81bb8d6f787d4140a48a12ec0a9631950ab771fd0e9f90b2789

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page