{ "info": { "author": "Mikhail Korobov", "author_email": "kmike84@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "====\nandi\n====\n\n.. image:: https://img.shields.io/pypi/v/andi.svg\n :target: https://pypi.python.org/pypi/andi\n :alt: PyPI Version\n\n.. image:: https://img.shields.io/pypi/pyversions/andi.svg\n :target: https://pypi.python.org/pypi/andi\n :alt: Supported Python Versions\n\n.. image:: https://travis-ci.com/scrapinghub/andi.svg?branch=master\n :target: https://travis-ci.com/scrapinghub/andi\n :alt: Build Status\n\n.. image:: https://codecov.io/github/scrapinghub/andi/coverage.svg?branch=master\n :target: https://codecov.io/gh/scrapinghub/andi\n :alt: Coverage report\n\n.. warning::\n Current status is \"experimental\".\n\n``andi`` tells which kwargs should to be passed to a callable, based\non callable arguments' type annotations.\n\n``andi`` is useful as a building block for frameworks, or as a library\nwhich helps to implement dependency injection (thus the name -\nANnotation-based Dependency Injection).\n\nLicense is BSD 3-clause.\n\nInstallation\n============\n\n::\n\n pip install andi\n\nandi requires Python >= 3.5.3.\n\nIdea\n====\n\n``andi`` does a simple thing, but it requires some explanation why\nthis thing is useful.\n\nYou're building a framework. This framework has code which calls some\nuser-defined function (callback). Callback receives arguments\n``foo`` and ``bar``:\n\n.. code-block:: python\n\n def my_framework(callback):\n # ... compute foo and bar somehow\n result = callback(foo=foo, bar=bar)\n # ...\n\nThen you decide that you want the framework to be flexible,\nand support callbacks which take\n\n* both ``foo`` and ``bar``,\n* only ``foo``,\n* only ``bar``,\n* nothing.\n\nIf a callback only takes ``foo``, it may be unnecessary to compute ``bar``.\n\nIn addition to that, you realize that there can be environments\nor implementations where ``foo`` is available for the framework,\nbut ``bar`` isn't, but you still want to reuse the callbacks which\nwork without ``bar``, and disable (or error) those who need ``bar``.\n\nSo, the logic is the following:\n\n1. Framework defines which inputs are available, or can be possibly computed\n (e.g. ``foo`` and ``bar``).\n2. Callback declares which inputs it receives (e.g. ``bar``).\n3. Framework inspects the callback, finds arguments the callback needs.\n4. Optional: if there are some arguments which callback needs,\n but framework doesn't provide, an error is raised (or callback is disabled).\n5. Framework computes argument values (``bar`` in this case).\n6. Framework calls the callback.\n\nDepending on implementation, steps 1-5 may happen iteratevely - e.g.\nmiddlewares may be populating different parts of callback kwargs.\nIn this case step (4 - raising an error) can be skipped.\n\n``andi`` is a library which helps to support this workflow.\n\nUsage\n=====\n\n``andi`` usage looks like this:\n\n.. code-block:: python\n\n import andi\n\n class Foo:\n pass\n\n class Bar:\n pass\n\n class Baz:\n pass\n\n\n # use type annotations to declare which inputs a callback wants\n def my_callback1(foo: Foo):\n pass\n\n\n def my_callback2(bar: Bar, foo: Foo):\n pass\n\n\n def my_framework(callback):\n kwargs_to_provide = andi.to_provide(callable,\n can_provide={Foo, Bar, None})\n # for my_callback: kwargs_to_provide == {'foo': Foo}\n\n # Create all the dependencies - implementation is framework-specific,\n # and can be organized in different ways. Code below is an example.\n kwargs = {}\n for name, cls in kwargs_to_provide.items():\n if cls is Foo:\n kwargs[name] = Foo()\n elif cls is Bar:\n kwargs[name] = fetch_bar()\n elif cls is None:\n kwargs[name] = None\n else:\n raise Exception(\"Unexpected type\") # shouldn't really happen\n\n # everything is ready, call the callback\n result = callback(**kwargs)\n # ...\n\n my_framework(my_callback1) # Foo instance is passed to my_callback1\n my_framework(my_callback2) # Bar and Foo instances are passed to my_callback2\n\n\nIf a callback wants some input which framework can't provide,\nthen some arguments are going to be missing in kwargs,\nand Python can raise TypeError, as usual.\nIt is possible to check it explicitly, to avoid doing unnecessary\nwork creating values for other arguments:\n\n.. code-block:: python\n\n arguments = andi.inspect(callable)\n kwargs_to_provide = andi.to_provide(arguments,\n can_provide={Foo, Bar, None})\n cant_provide = arguments.keys() - kwargs_to_provide.keys()\n if cant_provide:\n raise Exception(\"Can't provide arguments: %s\" % cant_provide)\n\n\n``andi`` support typing.Union. If an argument is annotated\nas ``Union[Foo, Bar]``, it means \"both Foo and Bar objects are fine,\nbut callable prefers Foo\":\n\n.. code-block:: python\n\n def callback4(x: Union[Baz, Bar, Foo]):\n pass\n\n # Bar is preferred to Foo, and Baz is not available, so my_framework passes\n # Bar instance to ``x`` argument (``x = fetch_bar()``)\n my_framework(callback4)\n\n``andi`` also supports typing.Optional types. If an argument is annotated\nas optional, it means ``Union[, None]``. So usually framework\nspecifies that None is OK, and provides it; None has the least priority:\n\n.. code-block:: python\n\n def callback4(foo: Optional[Foo], baz: Optional[Baz]):\n pass\n\n # foo=Foo(), baz=None is passed, because my_framework\n # supports Foo, but not Baz\n my_framework(callback4)\n\n``andi`` only checks type-annotated arguments; arguments without annotations\nare ignored.\n\nConstructor Dependency Injection\n--------------------------------\n\nIt is common for frameworks to ask users to define classes with a certain\ninterface, not just callbacks. ``andi`` can be used like this:\n\n.. code-block:: python\n\n class UserClass:\n def __init__(self, foo: Foo):\n self.foo = foo\n # ...\n\n class MyFramework:\n # ...\n def create_instance(self, user_cls):\n kwargs_to_provide = andi.to_provide(user_cls.__init__,\n can_provide={Foo, Bar})\n # ... fill kwargs, based on ``kwargs_to_provide``\n return user_cls(**kwargs)\n\n obj = framework.create_instance(UserClass)\n\nPattern is the following:\n\n1) ask user classes to declare all dependencies in ``__init__`` method,\n2) then framework creates instances of these classes, passing all the\n required dependencies.\n\nInstead of ``__init__`` you can also use a classmethod.\n\nRecursive dependencies\n----------------------\n\n``andi`` can be used on different levels in a framework. For example,\nframework supports callbacks which receive instances of\nsome BaseClass subclasses:\n\n.. code-block:: python\n\n class UserClass(framework.BaseClass):\n def __init__(self, foo: Foo):\n self.foo = foo\n\n def callback(user: UserClass):\n # ...\n\n class MyFramework:\n # ...\n def create_instance(self, user_cls):\n kwargs_to_provide = andi.to_provide(user_cls.__init__,\n can_provide={Foo, Bar})\n # ... fill kwargs, based on ``kwargs_to_provide``, i.e.\n # create Foo and Bar objects somehow\n return user_cls(**kwargs)\n\n def call_callback(self, callback):\n kwargs_to_provide = andi.to_provide(\n callback,\n can_provide=self.is_allowed_callback_argument\n )\n kwargs = {}\n for name, user_cls in kwargs_to_provide.items():\n kwargs[name] = self.create_instance(user_cls)\n return callback(**kwargs)\n\n def is_allowed_callback_argument(self, cls):\n return issubclass(cls, framework.BaseClass)\n\nIn this example callback needs a dependency (UserClass object), and UserClass\nobject on itself has a dependency (Foo). So ``andi`` is used to find out these\ndependencies, and then framework creates Foo object first, then\nUserClass object, and then finally calls the callback.\n\nImplementation can be recursive as well, e.g. Foo may need some dependencies\nas well.\n\nWhy type annotations?\n---------------------\n\n``andi`` uses type annotations to declare dependencies (inputs).\nIt has several advantages, and some limitations as well.\n\nAdvantages:\n\n1. Built-in language feature.\n2. You're not lying when specifying a type - these\n annotations still work as usual type annotations.\n3. In many projects you'd annotate arguments anyways, so ``andi`` support\n is \"for free\".\n\nLimitations:\n\n1. Callable can't have two arguments of the same type.\n2. This feature could possibly conflict with regular type annotation usages.\n\nIf your callable has two arguments of the same type, consider making them\ndifferent types. For example, a callable may receive url and html of\na web page:\n\n.. code-block:: python\n\n def parse(html: str, url: str):\n # ...\n\nTo make it play well with ``andi``, you may define separate types for url\nand for html:\n\n.. code-block:: python\n\n class HTML(str):\n pass\n\n class URL(str):\n pass\n\n def parse(html: HTML, url: URL):\n # ...\n\nThis is more boilerplate though.\n\nYou can also refactor ``parse`` to have a single argument:\n\n.. code-block:: python\n\n @dataclass\n class Response:\n url: str\n html: str\n\n def parse(response: Response):\n # ...\n\nWhy doesn't andi handle creation of objects?\n--------------------------------------------\n\nCurrently ``andi`` just inspects callable and chooses best concrete types\na framework needs to create and pass to a callable, without prescribing how\nto create them. This makes ``andi`` useful in various contexts - e.g.\n\n* creation of some objects may require asynchronous funnctions, and it\n may depend on libraries used (asyncio, twisted, etc.)\n* in streaming architectures (e.g. based on Kafka) inspection may happen\n on one machine, while creation of objects may happen on different nodes\n in a distributed system, and then actually running a callable may happen on\n yet another machine.\n\nIt is hard to design API with enough flexibility for all such use cases.\nThat said, ``andi`` may provide more helpers in future,\nonce patterns emerge, even if they're useful only in certain contexts.\n\nContributing\n============\n\n* Source code: https://github.com/scrapinghub/andi\n* Issue tracker: https://github.com/scrapinghub/andi/issues\n\nUse tox_ to run tests with different Python versions::\n\n tox\n\nThe command above also runs type checks; we use mypy.\n\n.. _tox: https://tox.readthedocs.io\n\n\nChanges\n=======\n\nTBA\n---\n\nInitial release.\n\n", "description_content_type": "text/x-rst", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/scrapinghub/andi", "keywords": "", "license": "", "maintainer": "", "maintainer_email": "", "name": "andi", "package_url": "https://pypi.org/project/andi/", "platform": "", "project_url": "https://pypi.org/project/andi/", "project_urls": { "Homepage": "https://github.com/scrapinghub/andi" }, "release_url": "https://pypi.org/project/andi/0.1/", "requires_dist": null, "requires_python": "", "summary": "Library for annotation-based dependency injection", "version": "0.1" }, "last_serial": 5744654, "releases": { "0.1": [ { "comment_text": "", "digests": { "md5": "3cda79495f7401996460781dc0e0d903", "sha256": "6f5435a510ca487b683783934ac1db67ba75d5e682c2eb426946ef19b2de2e8c" }, "downloads": -1, "filename": "andi-0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "3cda79495f7401996460781dc0e0d903", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 7491, "upload_time": "2019-08-28T18:06:13", "url": "https://files.pythonhosted.org/packages/c0/12/00abded0948a6245b9e2d549a74800da3fa8fc3250884c3fbc91107f967b/andi-0.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "05b527572312f5b863ffbe2415062e9e", "sha256": "b78e51b238c0c99b3ce5ac12f90c3bdb652cc2b6bdc40c8c3dc9c8f296b77f3b" }, "downloads": -1, "filename": "andi-0.1.tar.gz", "has_sig": false, "md5_digest": "05b527572312f5b863ffbe2415062e9e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9688, "upload_time": "2019-08-28T18:06:15", "url": "https://files.pythonhosted.org/packages/68/c5/b862a8890cb01620cbec939ade48b7c96fa38354854eeb63d16d07a39edb/andi-0.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "3cda79495f7401996460781dc0e0d903", "sha256": "6f5435a510ca487b683783934ac1db67ba75d5e682c2eb426946ef19b2de2e8c" }, "downloads": -1, "filename": "andi-0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "3cda79495f7401996460781dc0e0d903", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 7491, "upload_time": "2019-08-28T18:06:13", "url": "https://files.pythonhosted.org/packages/c0/12/00abded0948a6245b9e2d549a74800da3fa8fc3250884c3fbc91107f967b/andi-0.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "05b527572312f5b863ffbe2415062e9e", "sha256": "b78e51b238c0c99b3ce5ac12f90c3bdb652cc2b6bdc40c8c3dc9c8f296b77f3b" }, "downloads": -1, "filename": "andi-0.1.tar.gz", "has_sig": false, "md5_digest": "05b527572312f5b863ffbe2415062e9e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9688, "upload_time": "2019-08-28T18:06:15", "url": "https://files.pythonhosted.org/packages/68/c5/b862a8890cb01620cbec939ade48b7c96fa38354854eeb63d16d07a39edb/andi-0.1.tar.gz" } ] }