compose


Namecompose JSON
Version 1.6.2 PyPI version JSON
download
home_pagehttps://github.com/mentalisttraceur/python-compose
SummaryThe classic ``compose``, with all the Pythonic features.
upload_time2024-09-26 18:02:53
maintainerNone
docs_urlNone
authorAlexander Kozhevnikov
requires_pythonNone
license0BSD
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            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).

For ``async``/``await`` support, different variants of
``compose`` are included.


Versioning
----------

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


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

::

    pip install compose

For static type checking, also install `the type hint
stubs <https://pypi.org/project/compose-stubs>`_:

::

    pip install compose-stubs


Usage
-----

Basics
~~~~~~

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(double, double, double)
    >>> times_16 = compose(double, 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 0x...>, <function g at 0x...>)

``compose`` instances flatten when nested:

.. code:: python

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

.. code:: python

    >>> isinstance(g_of_f, compose)
    True

``compose`` raises a ``TypeError`` when called with
no arguments or with any non-callable arguments:

.. code:: python

    >>> compose()
    Traceback (most recent call last):
        ...
    TypeError: compose() needs at least one argument

.. code:: python

    >>> compose(increment, 'oops', increment)
    Traceback (most recent call last):
        ...
    TypeError: compose() arguments must be callable


``async``/``await``
~~~~~~~~~~~~~~~~~~~

We can compose ``async`` code by using ``acompose``:

.. code:: python

    >>> 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`` can compose any number of ``async``
and regular functions, in any order:

.. code:: python

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

``acompose`` instances always return awaitable values,
even if none of the composed functions are ``async``:

.. code:: python

    >>> awaitable_times_16 = acompose(double, double, double, double)
    >>> asyncio.run(awaitable_times_16(1))
    16

``sacompose`` is like ``acompose``, but ``sacompose``
instances return an awaitable value only if any of
the composed functions return an awaitable value:

.. code:: python

    >>> from compose import sacompose
    >>>
    >>> regular_times_4 = sacompose(double, double)
    >>> async_times_4 = sacompose(double, async_double)
    >>>
    >>> regular_times_4(1)
    4
    >>> asyncio.run(async_times_4(1))
    4

If |markcoroutinefunction|_ is available,
``acompose`` and ``sacompose`` instances
will be correctly detected as coroutine functions:

.. |markcoroutinefunction| replace:: ``inspect.markcoroutinefunction``
.. _markcoroutinefunction:  https://docs.python.org/3/library/inspect.html#inspect.markcoroutinefunction

.. code:: python

    >>> inspect.iscoroutinefunction(async_times_16)
    True
    >>> inspect.iscoroutinefunction(awaitable_times_16)
    True
    >>> inspect.iscoroutinefunction(regular_times_4)
    False
    >>> inspect.iscoroutinefunction(async_times_4)
    True

``acompose`` and ``sacompose`` instances flatten when nested:

.. code:: python

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

.. code:: python

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

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

.. code:: python

    >>> 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(async_times_4, sacompose)
    True
    >>> isinstance(async_times_4, (compose, acompose))
    False

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/mentalisttraceur/python-compose",
    "name": "compose",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": null,
    "author": "Alexander Kozhevnikov",
    "author_email": "mentalisttraceur@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/d8/32/bac364dddb79b58d5df429bb4f14a217e5d679f272eb16df8f68143ec1e0/compose-1.6.2.tar.gz",
    "platform": null,
    "description": "compose\n=======\n\nThe classic ``compose``, with all the Pythonic features.\n\nThis ``compose`` follows the lead of ``functools.partial``\nand returns callable ``compose`` objects which:\n\n* have a regular and unambiguous ``repr``,\n* retain correct signature introspection,\n* allow introspection of the composed callables,\n* can be type-checked,\n* can be weakly referenced,\n* can have attributes,\n* will merge when nested, and\n* can be pickled (if all composed callables can be pickled).\n\nFor ``async``/``await`` support, different variants of\n``compose`` are included.\n\n\nVersioning\n----------\n\nThis library's version numbers follow the `SemVer 2.0.0\nspecification <https://semver.org/spec/v2.0.0.html>`_.\n\n\nInstallation\n------------\n\n::\n\n    pip install compose\n\nFor static type checking, also install `the type hint\nstubs <https://pypi.org/project/compose-stubs>`_:\n\n::\n\n    pip install compose-stubs\n\n\nUsage\n-----\n\nBasics\n~~~~~~\n\nImport ``compose``:\n\n.. code:: python\n\n    >>> from compose import compose\n\nAll the usual function composition you know and love:\n\n.. code:: python\n\n    >>> def double(x):\n    ...     return x * 2\n    ...\n    >>> def increment(x):\n    ...     return x + 1\n    ...\n    >>> double_then_increment = compose(increment, double)\n    >>> double_then_increment(1)\n    3\n\nOf course any number of functions can be composed:\n\n.. code:: python\n\n    >>> def double(x):\n    ...     return x * 2\n    ...\n    >>> times_eight = compose(double, double, double)\n    >>> times_16 = compose(double, double, double, double)\n\nWe still get the correct signature introspection:\n\n.. code:: python\n\n    >>> def f(a, b, c=0, **kwargs):\n    ...     pass\n    ...\n    >>> def g(x):\n    ...     pass\n    ...\n    >>> g_of_f = compose(g, f)\n    >>> import inspect\n    >>> inspect.signature(g_of_f)\n    <Signature (a, b, c=0, **kwargs)>\n\nAnd we can inspect all the composed callables:\n\n.. code:: python\n\n    >>> g_of_f.functions  # in order of execution:\n    (<function f at 0x...>, <function g at 0x...>)\n\n``compose`` instances flatten when nested:\n\n.. code:: python\n\n   >>> times_eight_times_two = compose(double, times_eight)\n   >>> times_eight_times_two.functions == times_16.functions\n   True\n\nWhen programmatically inspecting arbitrary callables, we\ncan check if we are looking at a ``compose`` instance:\n\n.. code:: python\n\n    >>> isinstance(g_of_f, compose)\n    True\n\n``compose`` raises a ``TypeError`` when called with\nno arguments or with any non-callable arguments:\n\n.. code:: python\n\n    >>> compose()\n    Traceback (most recent call last):\n        ...\n    TypeError: compose() needs at least one argument\n\n.. code:: python\n\n    >>> compose(increment, 'oops', increment)\n    Traceback (most recent call last):\n        ...\n    TypeError: compose() arguments must be callable\n\n\n``async``/``await``\n~~~~~~~~~~~~~~~~~~~\n\nWe can compose ``async`` code by using ``acompose``:\n\n.. code:: python\n\n    >>> import asyncio\n    >>> from compose import acompose\n    >>>\n    >>> async def get_data():\n    ...     # pretend this data is fetched from some async API\n    ...     await asyncio.sleep(0)\n    ...     return 42\n    ...\n    >>> get_and_double_data = acompose(double, get_data)\n    >>> asyncio.run(get_and_double_data())\n    84\n\n``acompose`` can compose any number of ``async``\nand regular functions, in any order:\n\n.. code:: python\n\n    >>> async def async_double(x):\n    ...     await asyncio.sleep(0)\n    ...     return x * 2\n    ...\n    >>> async_times_16 = acompose(async_double, double, async_double, double)\n    >>> asyncio.run(async_times_16(1))\n    16\n\n``acompose`` instances always return awaitable values,\neven if none of the composed functions are ``async``:\n\n.. code:: python\n\n    >>> awaitable_times_16 = acompose(double, double, double, double)\n    >>> asyncio.run(awaitable_times_16(1))\n    16\n\n``sacompose`` is like ``acompose``, but ``sacompose``\ninstances return an awaitable value only if any of\nthe composed functions return an awaitable value:\n\n.. code:: python\n\n    >>> from compose import sacompose\n    >>>\n    >>> regular_times_4 = sacompose(double, double)\n    >>> async_times_4 = sacompose(double, async_double)\n    >>>\n    >>> regular_times_4(1)\n    4\n    >>> asyncio.run(async_times_4(1))\n    4\n\nIf |markcoroutinefunction|_ is available,\n``acompose`` and ``sacompose`` instances\nwill be correctly detected as coroutine functions:\n\n.. |markcoroutinefunction| replace:: ``inspect.markcoroutinefunction``\n.. _markcoroutinefunction:  https://docs.python.org/3/library/inspect.html#inspect.markcoroutinefunction\n\n.. code:: python\n\n    >>> inspect.iscoroutinefunction(async_times_16)\n    True\n    >>> inspect.iscoroutinefunction(awaitable_times_16)\n    True\n    >>> inspect.iscoroutinefunction(regular_times_4)\n    False\n    >>> inspect.iscoroutinefunction(async_times_4)\n    True\n\n``acompose`` and ``sacompose`` instances flatten when nested:\n\n.. code:: python\n\n    >>> acompose(f, acompose(f, f)).functions == (f, f, f)\n    True\n    >>> acompose(sacompose(f, f), f).functions == (f, f, f)\n    True\n    >>> sacompose(acompose(f, f), f).functions == (f, f, f)\n    True\n    >>> sacompose(f, sacompose(f, f)).functions == (f, f, f)\n    True\n\nBut ``compose`` instances *don't* flatten when nested \ninto ``acompose`` and ``sacompose``, and vice versa:\n\n.. code:: python\n\n    >>> acompose(g_of_f).functions\n    (compose(<function g at 0x...>, <function f at 0x...>),)\n    >>> sacompose(g_of_f).functions\n    (compose(<function g at 0x...>, <function f at 0x...>),)\n    >>> compose(acompose(g, f)).functions\n    (acompose(<function g at 0x...>, <function f at 0x...>),)\n    >>> compose(sacompose(g, f)).functions\n    (sacompose(<function g at 0x...>, <function f at 0x...>),)\n\n``compose``, ``acompose``, and ``sacompose``\ninstances are all distinct types:\n\n.. code:: python\n\n    >>> isinstance(g_of_f, compose)\n    True\n    >>> isinstance(g_of_f, (acompose, sacompose))\n    False\n    >>> isinstance(async_times_16, acompose)\n    True\n    >>> isinstance(async_times_16, (compose, sacompose))\n    False\n    >>> isinstance(async_times_4, sacompose)\n    True\n    >>> isinstance(async_times_4, (compose, acompose))\n    False\n",
    "bugtrack_url": null,
    "license": "0BSD",
    "summary": "The classic ``compose``, with all the Pythonic features.",
    "version": "1.6.2",
    "project_urls": {
        "Homepage": "https://github.com/mentalisttraceur/python-compose"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0b9fe65ede617a93729dd35ceddfe798a6edbf8ed5036df2a552e0e46c7e78c2",
                "md5": "0a2e4d3fdcc9fe6e97406309b5cebcab",
                "sha256": "e704e66df7cb8888d6c30e46491f94da80d38b2d45511d138dafaa865386fd7d"
            },
            "downloads": -1,
            "filename": "compose-1.6.2-py2.py30-none-any.whl",
            "has_sig": false,
            "md5_digest": "0a2e4d3fdcc9fe6e97406309b5cebcab",
            "packagetype": "bdist_wheel",
            "python_version": "py2.py30",
            "requires_python": null,
            "size": 4574,
            "upload_time": "2024-09-26T18:02:49",
            "upload_time_iso_8601": "2024-09-26T18:02:49.294765Z",
            "url": "https://files.pythonhosted.org/packages/0b/9f/e65ede617a93729dd35ceddfe798a6edbf8ed5036df2a552e0e46c7e78c2/compose-1.6.2-py2.py30-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "2308f510f8522265766b988293ef4ddbd0e4396e51c920b08bcd3b268acac644",
                "md5": "6d445e3f78699e7b4a3c574a4a36da94",
                "sha256": "c8b58b563594199ca40c864bd44e094392eaa1c4f4492ca8591bd54582b1f093"
            },
            "downloads": -1,
            "filename": "compose-1.6.2-py35-none-any.whl",
            "has_sig": false,
            "md5_digest": "6d445e3f78699e7b4a3c574a4a36da94",
            "packagetype": "bdist_wheel",
            "python_version": "py35",
            "requires_python": null,
            "size": 4956,
            "upload_time": "2024-09-26T18:02:50",
            "upload_time_iso_8601": "2024-09-26T18:02:50.784908Z",
            "url": "https://files.pythonhosted.org/packages/23/08/f510f8522265766b988293ef4ddbd0e4396e51c920b08bcd3b268acac644/compose-1.6.2-py35-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "ccb2166a868ceb985939320afd32f5b8c3911bb0f56fb190dfc97a721bfb5c4a",
                "md5": "00527e1e7b1c64d5d0b5dd7e09799fa1",
                "sha256": "5074c7c36bea8d073130398b905410e606a1f75e7f7bd8754f73980028509d83"
            },
            "downloads": -1,
            "filename": "compose-1.6.2-py38-none-any.whl",
            "has_sig": false,
            "md5_digest": "00527e1e7b1c64d5d0b5dd7e09799fa1",
            "packagetype": "bdist_wheel",
            "python_version": "py38",
            "requires_python": null,
            "size": 5031,
            "upload_time": "2024-09-26T18:02:52",
            "upload_time_iso_8601": "2024-09-26T18:02:52.105676Z",
            "url": "https://files.pythonhosted.org/packages/cc/b2/166a868ceb985939320afd32f5b8c3911bb0f56fb190dfc97a721bfb5c4a/compose-1.6.2-py38-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "d832bac364dddb79b58d5df429bb4f14a217e5d679f272eb16df8f68143ec1e0",
                "md5": "a46e982c0c887ff5cc0893697e1f3ef4",
                "sha256": "c943fa8284c1cb3892925388862e203c9888484e4ce3fd5171506769690d8b4d"
            },
            "downloads": -1,
            "filename": "compose-1.6.2.tar.gz",
            "has_sig": false,
            "md5_digest": "a46e982c0c887ff5cc0893697e1f3ef4",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 5709,
            "upload_time": "2024-09-26T18:02:53",
            "upload_time_iso_8601": "2024-09-26T18:02:53.500896Z",
            "url": "https://files.pythonhosted.org/packages/d8/32/bac364dddb79b58d5df429bb4f14a217e5d679f272eb16df8f68143ec1e0/compose-1.6.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-09-26 18:02:53",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "mentalisttraceur",
    "github_project": "python-compose",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "compose"
}
        
Elapsed time: 1.10719s