{ "info": { "author": "BlueDynamics Alliance", "author_email": "dev@bluedynamics.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Python Software Foundation License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development" ], "description": "Plumber\n=======\n\nPlumbing is an alternative to mixin-based extension of classes. In motivation\nan incomplete list of limitations and/or design choices of python's subclassing\nare given along with plumber's solutions for them. The plumbing system is\ndescribed in detail with code examples. Some design choices and ongoing\ndiscussions are explained. Finally, in miscellanea you find nomenclature,\ncoverage report, list of contributors, changes and some todos. All\nnon-experimental features are fully test covered.\n\n.. contents::\n :depth: 2\n\n\nMotivation: limitations of subclassing\n--------------------------------------\n\nPlumbing is an alternative to mixin-based extension of classes, motivated by\nlimitations and/or design choice of python's subclassing:\n\n.. contents::\n :local:\n\n\nControl of precedence only through order of mixins\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nMixins are commonly used to extend classes with pre-defined behaviours: an\nattribute on the first mixin overwrites attributes with the same name on all\nfollowing mixins and the base class being extended.\n\n.. code-block:: pycon\n\n >>> class Mixin1(object):\n ... a = 1\n\n >>> class Mixin2(object):\n ... a = 2\n ... b = 2\n\n >>> Base = dict\n >>> class MixedClass(Mixin1, Mixin2, Base):\n ... pass\n\n >>> MixedClass.a\n 1\n >>> MixedClass.b\n 2\n >>> MixedClass.keys\n \n\nThere is no way for a mixin later in the chain to take precedence over an\nearlier one.\n\n**Solution**: plumber provides 3 decorators to enable finer control of\nprecedence (``default``, ``override``, ``finalize``).\n\n\nImpossible to provide default values to fill gaps on a base class\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nA dictionary-like storage at least needs to provide ``__getitem__``,\n``__setitem__``, ``__delitem__`` and ``__iter__``, all other methods of a\ndictionary can be build upon these. A mixin that turns storages into full\ndictionaries needs to be able to provide default methods, taken if the base\nclass does not provide a (more efficient) implementation.\n\n**Solution**: plumber provides the ``default`` decorator to enable such\ndefaults.\n\n\n``super``-chains are not verified during class creation\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nIt is possible to build a chain of methods using ``super``: ``Mixin1`` turns\nthe key lowercase before passing it on, ``Mixin2`` multiplies the result by 2\nbefore returning it and both are chatty about start/stop.\n\n.. code-block:: pycon\n\n >>> class Mixin1(object):\n ... def __getitem__(self, key):\n ... print \"Mixin1 start\"\n ... key = key.lower()\n ... ret = super(Mixin1, self).__getitem__(key)\n ... print \"Mixin1 stop\"\n ... return ret\n\n >>> class Mixin2(object):\n ... def __getitem__(self, key):\n ... print \"Mixin2 start\"\n ... ret = super(Mixin2, self).__getitem__(key)\n ... ret = 2 * ret\n ... print \"Mixin2 stop\"\n ... return ret\n\n >>> Base = dict\n >>> class MixedClass(Mixin1, Mixin2, Base):\n ... pass\n\n >>> mc = MixedClass()\n >>> mc['abc'] = 6\n >>> mc['ABC']\n Mixin1 start\n Mixin2 start\n Mixin2 stop\n Mixin1 stop\n 12\n\n``dict.__getitem__`` forms the endpoint of the chain as it returns a value\nwithout delegating to a method later in the chain (using ``super``). If there\nis no endpoint an ``AttributeError`` is raised during runtime, not during class\ncreation.\n\n.. code-block:: pycon\n\n >>> class Mixin1(object):\n ... def foo(self):\n ... super(Mixin1, self).foo()\n\n >>> class MixedClass(Mixin1, Base):\n ... pass\n\n >>> mc = MixedClass()\n >>> mc.foo()\n Traceback (most recent call last):\n ...\n AttributeError: 'super' object has no attribute 'foo'\n\n**Solution**: Plumber provides the ``plumb`` decorator to build similar chains\nusing nested closures. These are create and verified during class creation.\n\n\nNo conditional ``super``-chains\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nA mixin with subclassing needs to fit exactly the base class, there is no way\nto conditionally hook into method calls depending on whether the base class\nprovides a method.\n\n**Solution**: Plumber provides the ``plumbifexists`` decorator that behaves\nlike ``plumb``, if there is an endpoint available.\n\n\nDocstrings are not accumulated\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nA class' docstring that uses mixins is not build from the docstrings of the\nmixins.\n\n**Solution**: Plumber enables plumbing of docstrings using a special marker\n``__plbnext__``, which is replaced with the docstring of the next \"mixin\"\nWithout the marker, docstrings are concatenated.\n\n\nThe plumbing system\n-------------------\n\nThe ``plumber`` metaclass creates plumbing classes according to instructions\nfound on plumbing behaviors. First, all instructions are gathered, then they are\napplied in two stages: stage1: extension and stage2: pipelines, docstrings and\noptional ``zope.interfaces``. There exists a class decorator ``plumbing`` which\nshould be used in favor of setting metaclass directly as of plumber 1.3.\n\n.. contents::\n :local:\n\n\nPlumbing behaviors provide instructions\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nPlumbing behaviors correspond to mixins, but are more powerful and flexible. A\nplumbing behavior needs to inherit from ``plumber.Behavior`` and declares \nattributes with instructions on how to use them, here by example of the \n``default`` instruction (more later).\n\n.. code-block:: pycon\n\n >>> from plumber import Behavior\n >>> from plumber import default\n\n >>> class Behavior1(Behavior):\n ... a = default(True)\n ...\n ... @default\n ... def foo(self):\n ... return 42\n\n >>> class Behavior2(Behavior):\n ... @default\n ... @property\n ... def bar(self):\n ... return 17\n\nThe instructions are given as behavior of assignments (``a = default(None)``) \nor as decorators (``@default``).\n\nA plumbing declaration defines the ``plumber`` as metaclass and one or more\nplumbing behaviors to be processed from left to right. Further it may declare\nattributes like every normal class, they will be treated as implicit\n``finalize`` instructions (see Stage 1: Extension).\n\n.. code-block:: pycon\n\n >>> from plumber import plumbing\n\n >>> Base = dict\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ...\n ... def foobar(self):\n ... return 5\n\nThe result is a plumbing class created according to the plumbing declaration.\n\n.. code-block:: pycon\n\n >>> plb = Plumbing()\n >>> plb.a\n True\n >>> plb.foo()\n 42\n >>> plb.bar\n 17\n >>> plb.foobar()\n 5\n >>> plb['a'] = 1\n >>> plb['a']\n 1\n\nA plumbing class can be subclassed like normal classes.\n\n.. code-block:: pycon\n\n >>> class Sub(Plumbing):\n ... a = 'Sub'\n\n >>> Sub.a\n 'Sub'\n >>> Sub().foo()\n 42\n >>> Sub().bar\n 17\n >>> Sub().foobar()\n 5\n\n\nThe plumber gathers instructions\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nA plumbing declaration provides a list of behaviors via the ``plumbing``\ndecorator. Behaviors provide instructions to be applied in two stages:\n\nstage1\n - extension via ``default``, ``override`` and ``finalize``, the result of this\n stage is the base for stage2.\n\nstage2\n - creation of pipelines via ``plumb`` and ``plumbifexists``\n - plumbing of docstrings\n - implemented interfaces from ``zope.interface``, iff available\n\nThe plumber walks the Behavior list from left to right (behavior order). On its\nway it gathers instructions onto stacks, sorted by stage and attribute name. A \nhistory of all instructions is kept.\n\n.. code-block:: pycon\n\n >>> pprint(Plumbing.__plumbing_stacks__)\n {'history':\n [<_implements '__interfaces__' of None payload=()>,\n payload=True>,\n payload=>,\n <_implements '__interfaces__' of None payload=()>,\n payload=>],\n 'stages':\n {'stage1':\n {'a': [ payload=True>],\n 'bar': [ payload= payload=>> from plumber import finalize\n\n >>> class Behavior1(Behavior):\n ... N = finalize('Behavior1')\n ...\n\n >>> class Behavior2(Behavior):\n ... M = finalize('Behavior2')\n\n >>> class Base(object):\n ... K = 'Base'\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ... L = 'Plumbing'\n\n >>> for x in ['K', 'L', 'M', 'N']:\n ... print \"%s from %s\" % (x, getattr(Plumbing, x))\n K from Base\n L from Plumbing\n M from Behavior2\n N from Behavior1\n\nsummary:\n\n- K-Q: attributes defined by behaviors, plumbing class and base classes\n- f: ``finalize`` declaration\n- x: declaration on plumbing class or base class\n- ?: base class declaration is irrelevant\n- **Y**: chosen end point\n- collision: indicates an invalid combination, that raises a ``PlumbingCollision``\n\n+------+-----------+-----------+----------+-------+-----------+\n| Attr | Behavior1 | Behavior2 | Plumbing | Base | ok? |\n+======+===========+===========+==========+=======+===========+\n| K | | | | **x** | |\n+------+-----------+-----------+----------+-------+-----------+\n| L | | | **x** | ? | |\n+------+-----------+-----------+----------+-------+-----------+\n| M | | **f** | | ? | |\n+------+-----------+-----------+----------+-------+-----------+\n| N | **f** | | | ? | |\n+------+-----------+-----------+----------+-------+-----------+\n| O | f | | x | ? | collision |\n+------+-----------+-----------+----------+-------+-----------+\n| P | | f | x | ? | collision |\n+------+-----------+-----------+----------+-------+-----------+\n| Q | f | f | | ? | collision |\n+------+-----------+-----------+----------+-------+-----------+\n\ncollisions.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... O = finalize(False)\n\n >>> @plumbing(Behavior1)\n ... class Plumbing(object):\n ... O = True\n Traceback (most recent call last):\n ...\n PlumbingCollision:\n Plumbing class\n with:\n payload=False>\n\n >>> class Behavior2(Behavior):\n ... P = finalize(False)\n\n >>> @plumbing(Behavior2)\n ... class Plumbing(object):\n ... P = True\n Traceback (most recent call last):\n ...\n PlumbingCollision:\n Plumbing class\n with:\n payload=False>\n\n >>> class Behavior1(Behavior):\n ... Q = finalize(False)\n\n >>> class Behavior2(Behavior):\n ... Q = finalize(True)\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(object):\n ... pass\n Traceback (most recent call last):\n ...\n PlumbingCollision:\n payload=False>\n with:\n payload=True>\n\n\nInteraction: ``override``, plumbing declaration and base classes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nin code.\n\n.. code-block:: pycon\n\n >>> from plumber import override\n\n >>> class Behavior1(Behavior):\n ... K = override('Behavior1')\n ... M = override('Behavior1')\n\n >>> class Behavior2(Behavior):\n ... K = override('Behavior2')\n ... L = override('Behavior2')\n ... M = override('Behavior2')\n\n >>> class Base(object):\n ... K = 'Base'\n ... L = 'Base'\n ... M = 'Base'\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ... K = 'Plumbing'\n\n >>> for x in ['K', 'L', 'M']:\n ... print \"%s from %s\" % (x, getattr(Plumbing, x))\n K from Plumbing\n L from Behavior2\n M from Behavior1\n\nsummary:\n\n- K-M: attributes defined by behaviors, plumbing class and base classes\n- e: ``override`` declaration\n- x: declaration on plumbing class or base class\n- ?: base class declaration is irrelevant\n- **Y**: chosen end point\n\n+------+-----------+-----------+----------+------+\n| Attr | Behavior1 | Behavior2 | Plumbing | Base |\n+======+===========+===========+==========+======+\n| K | e | e | **x** | ? |\n+------+-----------+-----------+----------+------+\n| L | | **e** | | ? |\n+------+-----------+-----------+----------+------+\n| M | **e** | e | | ? |\n+------+-----------+-----------+----------+------+\n\n\nInteraction: ``default``, plumbing declaration and base class\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nin code.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... N = default('Behavior1')\n\n >>> class Behavior2(Behavior):\n ... K = default('Behavior2')\n ... L = default('Behavior2')\n ... M = default('Behavior2')\n ... N = default('Behavior2')\n\n >>> class Base(object):\n ... K = 'Base'\n ... L = 'Base'\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ... L = 'Plumbing'\n\n >>> for x in ['K', 'L', 'M', 'N']:\n ... print \"%s from %s\" % (x, getattr(Plumbing, x))\n K from Base\n L from Plumbing\n M from Behavior2\n N from Behavior1\n\nsummary:\n\n- K-N: attributes defined by behaviors, plumbing class and base classes\n- d = ``default`` declaration\n- x = declaration on plumbing class or base class\n- ? = base class declaration is irrelevant\n- **Y** = chosen end point\n\n+------+-----------+-----------+----------+-------+\n| Attr | Behavior1 | Behavior2 | Plumbing | Base |\n+======+===========+===========+==========+=======+\n| K | | d | | **x** |\n+------+-----------+-----------+----------+-------+\n| L | | d | **x** | ? |\n+------+-----------+-----------+----------+-------+\n| M | | **d** | | |\n+------+-----------+-----------+----------+-------+\n| N | **d** | d | | |\n+------+-----------+-----------+----------+-------+\n\n\nInteraction: ``finalize`` wins over ``override``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nin code.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... K = override('Behavior1')\n ... L = finalize('Behavior1')\n\n >>> class Behavior2(Behavior):\n ... K = finalize('Behavior2')\n ... L = override('Behavior2')\n\n >>> class Base(object):\n ... K = 'Base'\n ... L = 'Base'\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ... pass\n\n >>> for x in ['K', 'L']:\n ... print \"%s from %s\" % (x, getattr(Plumbing, x))\n K from Behavior2\n L from Behavior1\n\nsummary:\n\n- K-L: attributes defined by behaviors, plumbing class and base classes\n- e = ``override`` declaration\n- f = ``finalize`` declaration\n- ? = base class declaration is irrelevant\n- **Y** = chosen end point\n\n+------+-----------+-----------+----------+------+\n| Attr | Behavior1 | Behavior2 | Plumbing | Base |\n+======+===========+===========+==========+======+\n| K | e | **f** | | ? |\n+------+-----------+-----------+----------+------+\n| L | **f** | e | | ? |\n+------+-----------+-----------+----------+------+\n\n\nInteraction: ``finalize`` wins over ``default``:\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nin code.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... K = default('Behavior1')\n ... L = finalize('Behavior1')\n\n >>> class Behavior2(Behavior):\n ... K = finalize('Behavior2')\n ... L = default('Behavior2')\n\n >>> class Base(object):\n ... K = 'Base'\n ... L = 'Base'\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ... pass\n\n >>> for x in ['K', 'L']:\n ... print \"%s from %s\" % (x, getattr(Plumbing, x))\n K from Behavior2\n L from Behavior1\n\nsummary:\n\n- K-L: attributes defined by behaviors, plumbing class and base classes\n- d = ``default`` declaration\n- f = ``finalize`` declaration\n- ? = base class declaration is irrelevant\n- **Y** = chosen end point\n\n+------+-----------+-----------+----------+------+\n| Attr | Behavior1 | Behavior2 | Plumbing | Base |\n+======+===========+===========+==========+======+\n| K | d | **f** | | ? |\n+------+-----------+-----------+----------+------+\n| L | **f** | d | | ? |\n+------+-----------+-----------+----------+------+\n\n\nInteraction: ``override`` wins over ``default``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nin code.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... K = default('Behavior1')\n ... L = override('Behavior1')\n\n >>> class Behavior2(Behavior):\n ... K = override('Behavior2')\n ... L = default('Behavior2')\n\n >>> class Base(object):\n ... K = 'Base'\n ... L = 'Base'\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ... pass\n\n >>> for x in ['K', 'L']:\n ... print \"%s from %s\" % (x, getattr(Plumbing, x))\n K from Behavior2\n L from Behavior1\n\nsummary:\n\n- K-L: attributes defined by behaviors, plumbing class and base classes\n- d = ``default`` declaration\n- e = ``override`` declaration\n- ? = base class declaration is irrelevant\n- **Y** = chosen end point\n\n+------+-----------+-----------+----------+------+\n| Attr | Behavior1 | Behavior2 | Plumbing | Base |\n+======+===========+===========+==========+======+\n| K | d | **e** | | ? |\n+------+-----------+-----------+----------+------+\n| L | **e** | d | | ? |\n+------+-----------+-----------+----------+------+\n\n\nSubclassing Behaviors\n~~~~~~~~~~~~~~~~~~~~~\n\nin code.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... J = default('Behavior1')\n ... K = default('Behavior1')\n ... M = override('Behavior1')\n\n >>> class Behavior2(Behavior1):\n ... J = default('Behavior2') # overrides ``J`` of ``Behavior1``\n ... L = default('Behavior2')\n ... M = default('Behavior2') # this one wins, even if ``M`` on\n ... # superclass is ``override`` instruction.\n ... # due to ordinary inheritance behavior.\n\n >>> @plumbing(Behavior2)\n ... class Plumbing(object):\n ... pass\n\n >>> plb = Plumbing()\n >>> plb.J\n 'Behavior2'\n\n >>> plb.K\n 'Behavior1'\n\n >>> plb.L\n 'Behavior2'\n\n >>> plb.M\n 'Behavior2'\n\n\nStage 2: Pipeline, docstrings and ``zope.interface`` instructions\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nIn stage1 plumbing class attributes were set, which can serve as endpoints for\nplumbing pipelines that are build in stage2. Plumbing pipelines correspond to\n``super``-chains. Docstrings of behaviors, methods in a pipeline and properties\nin a pipeline are accumulated. Plumber is ``zope.interface`` aware and takes\nimplemeneted interfaces from behaviors, if it can be imported.\n\n.. contents::\n :local:\n\n\nPlumbing Pipelines in general\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nElements for plumbing pipelines are declared with the ``plumb`` and\n``plumbifexists`` decorators:\n\n``plumb``\n Marks a method to be used as behavior of a plumbing pipeline. The signature of\n such a plumbing method is ``def foo(_next, self, *args, **kw)``. Via\n ``_next`` it is passed the next plumbing method to be called. ``self`` is\n an instance of the plumbing class, not the behavior.\n\n``plumbifexists``\n Like ``plumb``, but only used if an endpoint exists.\n\nThe user of a plumbing class does not know which ``_next`` to pass. Therefore,\nafter the pipelines are built, an entrance method is generated for each pipe,\nthat wraps the first plumbing method passing it the correct ``_next``. Each\n``_next`` method is an entrance to the rest of the pipeline.\n\nThe pipelines are build in behavior order, skipping behaviors that do not\ndefine a pipeline element with the same attribute name::\n\n +---+-----------+-----------+-----------+----------+\n | | Behavior1 | Behavior2 | Behavior3 | ENDPOINT |\n +---+-----------+-----------+-----------+----------+\n | | ---------------------------------> |\n | E | x | | | x |\n | N | <--------------------------------- |\n + T +-----------+-----------+-----------+----------+\n | R | ----------> ---------------------> |\n | A | y | y | | y |\n | N | <---------- <--------------------- |\n + C +-----------+-----------+-----------+----------+\n | E | | | ---------> |\n | S | | | z | z |\n | | | | <--------- |\n +---+-----------+-----------+-----------+----------+\n\n\nMethod pipelines\n~~~~~~~~~~~~~~~~\n\nTwo plumbing behaviors and a ``dict`` as base class. ``Behavior1`` lowercases\nkeys before passing them on, ``Behavior2`` multiplies results before returning\nthem.\n\n.. code-block:: pycon\n\n >>> from plumber import plumb\n\n >>> class Behavior1(Behavior):\n ... @plumb\n ... def __getitem__(_next, self, key):\n ... print \"Behavior1 start\"\n ... key = key.lower()\n ... ret = _next(self, key)\n ... print \"Behavior1 stop\"\n ... return ret\n\n >>> class Behavior2(Behavior):\n ... @plumb\n ... def __getitem__(_next, self, key):\n ... print \"Behavior2 start\"\n ... ret = 2 * _next(self, key)\n ... print \"Behavior2 stop\"\n ... return ret\n\n >>> Base = dict\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(Base):\n ... pass\n\n >>> plb = Plumbing()\n >>> plb['abc'] = 6\n >>> plb['AbC']\n Behavior1 start\n Behavior2 start\n Behavior2 stop\n Behavior1 stop\n 12\n\nPlumbing pipelines need endpoints. If no endpoint is available an\n``AttributeError`` is raised.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... @plumb\n ... def foo(_next, self):\n ... pass\n\n >>> @plumbing(Behavior1)\n ... class Plumbing(object):\n ... pass\n Traceback (most recent call last):\n ...\n AttributeError: type object 'Plumbing' has no attribute 'foo'\n\nIf no endpoint is available and a behavior does not care about that,\n``plumbifexists`` can be used to only plumb if an endpoint is available.\n\n.. code-block:: pycon\n\n >>> from plumber import plumbifexists\n\n >>> class Behavior1(Behavior):\n ... @plumbifexists\n ... def foo(_next, self):\n ... pass\n ...\n ... @plumbifexists\n ... def bar(_next, self):\n ... return 2 * _next(self)\n\n >>> @plumbing(Behavior1)\n ... class Plumbing(object):\n ...\n ... def bar(self):\n ... return 6\n\n >>> hasattr(Plumbing, 'foo')\n False\n >>> Plumbing().bar()\n 12\n\nThis enables one implementation of a certain behaviour, e.g. sending events for\ndictionaries, to be used for readwrite dictionaries that implement\n``__getitem__`` and ``__setitem__`` and readonly dictionaries, that only\nimplement ``__getitem__`` but no ``__setitem__``.\n\n\nProperty pipelines\n~~~~~~~~~~~~~~~~~~\n\nPlumbing of read only properties.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... @plumb\n ... @property\n ... def foo(_next, self):\n ... return 2 * _next(self)\n\n >>> @plumbing(Behavior1)\n ... class Plumbing(object):\n ...\n ... @property\n ... def foo(self):\n ... return 3\n\n >>> plb = Plumbing()\n >>> plb.foo\n 6\n\nIt is possible to extend a property with so far unset getter/setter/deleter.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... @plumb\n ... @property\n ... def foo(_next, self):\n ... return 2 * _next(self)\n\n >>> class Behavior2(Behavior):\n ... def set_foo(self, value):\n ... self._foo = value\n ... foo = plumb(property(\n ... None,\n ... override(set_foo),\n ... ))\n\n >>> @plumbing(Behavior1, Behavior2)\n ... class Plumbing(object):\n ...\n ... @property\n ... def foo(self):\n ... return self._foo\n\n >>> plb = Plumbing()\n >>> plb.foo = 4\n >>> plb.foo\n 8\n\n\nSubclassing Behaviors\n~~~~~~~~~~~~~~~~~~~~~\n\nOther than stage 1 instructions, which extend a class with properties\nand functions and thus override each other by the rules of ordinary\nsubclassing, pipeline instructions are aggregated.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... \n ... @plumb\n ... def foo(_next, self):\n ... return 'Behavior1 ' + _next(self)\n ... \n ... @plumb\n ... def bar(_next, self):\n ... return 'Behavior1 ' + _next(self)\n\n >>> class Behavior2(Behavior1):\n ... \n ... @plumb\n ... def foo(_next, self):\n ... return 'Behavior2 ' + _next(self)\n\n >>> @plumbing(Behavior2)\n ... class Plumbing(object):\n ... \n ... def foo(self):\n ... return 'foo'\n ... \n ... def bar(self):\n ... return 'bar'\n\n >>> plb = Plumbing()\n >>> plb.foo()\n 'Behavior2 Behavior1 foo'\n \n >>> plb.bar()\n 'Behavior1 bar'\n\n\nMixing methods and properties within the same pipeline is not possible\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWithin a pipeline all elements need to be of the same type, it is not possible\nto mix properties with methods.\n\n.. code-block:: pycon\n\n >>> class Behavior1(Behavior):\n ... @plumb\n ... def foo(_next, self):\n ... return _next(self)\n\n >>> @plumbing(Behavior1)\n ... class Plumbing(object):\n ...\n ... @property\n ... def foo(self):\n ... return 5\n Traceback (most recent call last):\n ...\n PlumbingCollision:\n payload=>\n with:\n \n\n\ndocstrings of classes, methods and properties\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nNormal docstrings of the plumbing declaration and the behavior classes, plumbed\nmethods and plumbed properties are joined by newlines starting with the\nplumbing declaration and followed by the behaviors in reverse order.\n\n.. code-block:: pycon\n\n >>> class P1(Behavior):\n ... \"\"\"P1\n ... \"\"\"\n ... @plumb\n ... def foo(self):\n ... \"\"\"P1.foo\n ... \"\"\"\n ... bar = plumb(property(None, None, None, \"P1.bar\"))\n\n >>> class P2(Behavior):\n ... @override\n ... def foo(self):\n ... \"\"\"P2.foo\n ... \"\"\"\n ... bar = plumb(property(None, None, None, \"P2.bar\"))\n\n >>> @plumbing(P1, P2)\n ... class Plumbing(object):\n ... \"\"\"Plumbing\n ... \"\"\"\n ... bar = property(None, None, None, \"Plumbing.bar\")\n\n >>> print Plumbing.__doc__\n Plumbing\n \n P1\n \n\n >>> print Plumbing.foo.__doc__\n P2.foo\n \n P1.foo\n \n\n >>> print Plumbing.bar.__doc__\n Plumbing.bar\n \n P2.bar\n \n P1.bar\n\nThe accumulation of docstrings is an experimental feature and will probably\nchange.\n\n\nSlots and plumbings\n~~~~~~~~~~~~~~~~~~~\n\nA plumbing class can have __slots__ like normal classes.\n\n.. code-block:: pycon\n\n >>> class P1(Behavior):\n ... @default\n ... def somewhing_which_writes_to_foo(self, foo_val):\n ... self.foo = foo_val\n\n >>> @plumbing(P1)\n ... class WithSlots(object):\n ... __slots__ = 'foo'\n\n >>> WithSlots.__dict__['foo']\n \n\n >>> ob = WithSlots()\n >>> ob.somewhing_which_writes_to_foo('foo')\n >>> assert(ob.foo == 'foo')\n\n\n``zope.interface`` (if available)\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe plumber does not depend on ``zope.interface`` but is aware of it. That\nmeans it will try to import it and if available will check plumbing behaviors\nfor implemented interfaces and will make the plumbing implement them, too.\n\n.. code-block:: pycon\n\n >>> from zope.interface import Interface\n >>> from zope.interface import implementer\n\nA class with an interface that will serve as base class of a plumbing.\n\n.. code-block:: pycon\n\n >>> class IBase(Interface):\n ... pass\n\n >>> @implementer(IBase)\n ... class Base(object):\n ... pass\n\n >>> IBase.implementedBy(Base)\n True\n\nTwo behaviors with corresponding interfaces, one with a base class that also\nimplements an interface.\n\n.. code-block:: pycon\n\n >>> class IBehavior1(Interface):\n ... pass\n\n >>> @implementer(IBehavior1)\n ... class Behavior1(Behavior):\n ... blub = 1\n\n >>> class IBehavior2Base(Interface):\n ... pass\n\n >>> @implementer(IBehavior2Base)\n ... class Behavior2Base(Behavior):\n ... pass\n\n >>> class IBehavior2(Interface):\n ... pass\n\n >>> @implementer(IBehavior2)\n ... class Behavior2(Behavior2Base):\n ... pass\n\n >>> IBehavior1.implementedBy(Behavior1)\n True\n\n >>> IBehavior2Base.implementedBy(Behavior2Base)\n True\n\n >>> IBehavior2Base.implementedBy(Behavior2)\n True\n\n >>> IBehavior2.implementedBy(Behavior2)\n True\n\nA plumbing based on ``Base`` using ``Behavior1`` and ``Behavior2`` and\nimplementing ``IPlumbingClass``.\n\n.. code-block:: pycon\n\n >>> class IPlumbingClass(Interface):\n ... pass\n\n >>> @implementer(IPlumbingClass)\n ... @plumbing(Behavior1, Behavior2)\n ... class PlumbingClass(Base):\n ... pass\n\nThe directly declared and inherited interfaces are implemented.\n\n.. code-block:: pycon\n\n >>> IPlumbingClass.implementedBy(PlumbingClass)\n True\n\n >>> IBase.implementedBy(PlumbingClass)\n True\n\nThe interfaces implemented by the Behaviors are also implemented.\n\n.. code-block:: pycon\n\n >>> IBehavior1.implementedBy(PlumbingClass)\n True\n\n >>> IBehavior2.implementedBy(PlumbingClass)\n True\n\n >>> IBehavior2Base.implementedBy(PlumbingClass)\n True\n\nAn instance of the class provides the interfaces.\n\n.. code-block:: pycon\n\n >>> plumbing = PlumbingClass()\n\n >>> IPlumbingClass.providedBy(plumbing)\n True\n\n >>> IBase.providedBy(plumbing)\n True\n\n >>> IBehavior1.providedBy(plumbing)\n True\n\n >>> IBehavior2.providedBy(plumbing)\n True\n\n >>> IBehavior2Base.providedBy(plumbing)\n True\n\n\nplumber metaclass hooks\n~~~~~~~~~~~~~~~~~~~~~~~\n\nIn case one writes a plumbing behavior requiring class manipulation at plumber\nmetaclass execution time a decorator is provided for registering callbacks\nwhich are executed at the end of the plumber metaclass ``__init__`` function.\nExecuting at last task ensures stage 1 and stage 2 instructions already have\nbeen executed and we're working on a complete plumbing class.\n\n.. code-block:: pycon\n\n >>> class IBehaviorInterface(Interface):\n ... pass\n\n >>> @plumber.metaclasshook\n ... def test_metclass_hook(cls, name, bases, dct):\n ... if not IBehaviorInterface.implementedBy(cls):\n ... return\n ... cls.hooked = True\n\n >>> @implementer(IBehaviorInterface)\n ... class MetaclassConsideredBehavior(Behavior):\n ... pass\n\n >>> @plumbing(MetaclassConsideredBehavior)\n ... class Plumbing(object):\n ... pass\n\n >>> Plumbing.hooked\n True\n\n\nMiscellanea\n-----------\n\nNomenclature\n^^^^^^^^^^^^\n\n**``plumber``**\n Metaclass that creates a plumbing according to the instructions declared on\n plumbing behaviors. Instructions are given by decorators: ``default``,\n ``override``, ``finalize``, ``plumb`` and ``plumbifexists``.\n\n**plumbing**\n A plumbing is a class decorated by ``plumbing`` decorator which gets passed\n the behviors to apply, e.g. ``@plumbing(Behavior1, Behavior2)``. Apart from\n the behaviors, declarations on base classes and the class asking for the\n plumber are taken into account. Once created, a plumbing looks like any\n other class and can be subclassed as usual.\n\n**plumbing behavior**\n A plumbing behavior provides attributes (functions, properties and plain\n values) along with instructions for how to use them. Instructions are given\n via decorators: ``default``, ``override``, ``finalize``, ``plumb`` and\n ``plumbifexists`` (see Stage 1:... and Stage 2:...).\n\n**plumbing pipeline**\n Plumbing methods/properties with the same name form a pipeline. The\n entrance and end-point have the signature of normal methods: ``def\n foo(self, *args, **kw)``. The plumbing pipelines is a series of nested\n closures (see ``_next``).\n\n**entrance (method)**\n A method with a normal signature. i.e. expecting ``self`` as first\n argument, that is used to enter a pipeline. It is a ``_next`` function. A\n method declared on the class with the same name, will be overwritten, but\n referenced in the pipelines as the innermost method, the endpoint.\n\n**``_next`` function**\n The ``_next`` function is used to call the next method in a pipelines: in\n case of a plumbing method, it is a wrapper of it that passes the correct\n next ``_next`` as first argument and in case of an end-point, just the\n end-point method itself.\n\n**end-point (method)**\n Method retrieved from the plumbing class with ``getattr()``, before setting\n the entrance method on the class.\n\nIf you feel something is missing, please let us now or write a short\ncorresponding text.\n\n\nTest Coverage\n^^^^^^^^^^^^^\n\n.. image:: https://travis-ci.org/bluedynamics/plumber.svg?branch=master\n :target: https://travis-ci.org/bluedynamics/plumber\n\nCoverage report::\n\n Name Stmts Miss Cover\n -------------------------------------------------------------\n src/plumber/__init__.py 10 0 100%\n src/plumber/behavior.py 49 0 100%\n src/plumber/compat.py 9 0 100%\n src/plumber/exceptions.py 6 0 100%\n src/plumber/instructions.py 172 0 100%\n src/plumber/plumber.py 71 0 100%\n src/plumber/tests/__init__.py 574 0 100%\n src/plumber/tests/globalmetaclass.py 15 0 100%\n -------------------------------------------------------------\n TOTAL 1882 0 100%\n\n\nPython Versions\n^^^^^^^^^^^^^^^\n\n- Python 2.6+, 3.3+, pypy\n\n- May work with other versions (untested)\n\n\nContributors\n^^^^^^^^^^^^\n\n- Florian Friesdorf\n\n- Robert Niederreiter\n\n- Jens W. Klein\n\n- Marco Lempen\n\n- Attila Ol\u00e1h\n\n\nChanges\n-------\n\n1.5 (unreleased)\n~~~~~~~~~~~~~~~~\n\n- Introduce ``plumber.metaclasshook`` decorator.\n [rnix, 2017-06-16]\n\n\n1.4\n~~~\n\n- No more \"private\" module names.\n [rnix, 2017-05-21]\n\n- Python 3 support.\n [rnix, 2017-05-18]\n\n\n1.3.1\n~~~~~\n\n- Avoid use of deprecated ``dict.has_key``.\n [rnix, 2015-10-05]\n\n\n1.3\n~~~\n\n- Introduce ``plumbing`` decorator.\n [rnix, 2014-07-31]\n\n- Remove deprecated ``plumber.extend`` and ``plumber.Part``.\n [rnix, 2014-07-31]\n\n\n1.2\n~~~\n\n- Deprecate ``plumber.extend``. Use ``plumber.override`` instead.\n [rnix, 2012-07-28]\n\n- Deprecate ``plumber.Part``. Use ``plumber.Behavior`` instead.\n [rnix, 2012-07-28]\n\n\n1.1\n~~~\n\n- Use ``zope.interface.implementer`` instead of ``zope.interface.implements``.\n [rnix, 2012-05-18]\n\n\n1.0\n~~~\n\n- ``.. plbnext::`` instead of ``.. plb_next::``\n [chaoflow 2011-02-02]\n\n- stage1 in __new__, stage2 in __init__, setting of __name__ now works\n [chaoflow 2011-01-25]\n\n- instructions recognize equal instructions\n [chaoflow 2011-01-24]\n\n- instructions from base classes now like subclass inheritance [chaoflow 2011\n [chaoflow 2011-01-24]\n\n- doctest order now plumbing order: P1, P2, PlumbingClass, was PlumbingClass,\n P1, P2\n [chaoflow 2011-01-24]\n\n- merged docstring instruction into plumb\n [chaoflow 2011-01-24]\n\n- plumber instead of Plumber\n [chaoflow 2011-01-24]\n\n- plumbing methods are not classmethods of part anymore\n [chaoflow 2011-01-24]\n\n- complete rewrite\n [chaoflow 2011-01-22]\n\n- prt instead of cls\n [chaoflow, rnix 2011-01-19\n\n- default, extend, plumb\n [chaoflow, rnix 2011-01-19]\n\n- initial\n [chaoflow, 2011-01-04]\n\n\nLicense / Disclaimer\n--------------------\n\nCopyright (c) 2011-2017, BlueDynamics Alliance, Austria, Germany, Switzerland\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or\n other materials provided with the distribution.\n* Neither the name of the BlueDynamics Alliance nor the names of its\n contributors may be used to endorse or promote products derived from this\n software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY BlueDynamics Alliance ``AS IS`` AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL BlueDynamics Alliance BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://github.com/bluedynamics/plumber", "keywords": "", "license": "Python Software Foundation License", "maintainer": "", "maintainer_email": "", "name": "plumber", "package_url": "https://pypi.org/project/plumber/", "platform": "", "project_url": "https://pypi.org/project/plumber/", "project_urls": { "Homepage": "http://github.com/bluedynamics/plumber" }, "release_url": "https://pypi.org/project/plumber/1.5/", "requires_dist": null, "requires_python": "", "summary": "An alternative to mixin-based extension of classes.", "version": "1.5" }, "last_serial": 3030725, "releases": { "0": [], "1.0": [ { "comment_text": "", "digests": { "md5": "1223521e9baff4619038c630b130cef5", "sha256": "2803c1fb21e910c6caa404044d0c0230b917956ca0f43e42d5b08ec821fcad11" }, "downloads": -1, "filename": "plumber-1.0.tar.gz", "has_sig": false, "md5_digest": "1223521e9baff4619038c630b130cef5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 43168, "upload_time": "2011-02-04T23:23:10", "url": "https://files.pythonhosted.org/packages/52/c8/620a649ffac8899cb81c0cd7a3aeab9fe3fd7a0264f306fe57921ffb966c/plumber-1.0.tar.gz" } ], "1.1": [ { "comment_text": "", "digests": { "md5": "8206685946474c84440164553e6be73b", "sha256": "11d01d7712c10ecc192a7b3d7bd1845cb6fa5e574b884ad524ba7d4ec5ce0248" }, "downloads": -1, "filename": "plumber-1.1.tar.gz", "has_sig": false, "md5_digest": "8206685946474c84440164553e6be73b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 36248, "upload_time": "2012-05-29T17:24:05", "url": "https://files.pythonhosted.org/packages/88/76/f8eccd598cee2e2061fa006cedc8a7390e4e7557f350ba9d9bc9ce333acd/plumber-1.1.tar.gz" } ], "1.2": [ { "comment_text": "", "digests": { "md5": "79c1c5374c15e3f10eefbb898fef6665", "sha256": "2279cad4177fc27b9af8d29d6a7ef2e643af9d8d60216698d8275cc9d3d66fb9" }, "downloads": -1, "filename": "plumber-1.2.tar.gz", "has_sig": false, "md5_digest": "79c1c5374c15e3f10eefbb898fef6665", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34093, "upload_time": "2012-08-01T11:00:14", "url": "https://files.pythonhosted.org/packages/23/71/aaff51f5c7e2717a76e623a1c1e22d1dac94730fe75e13e5cf42363917bf/plumber-1.2.tar.gz" } ], "1.3": [ { "comment_text": "", "digests": { "md5": "f7d13c5fd742a9b96b7c4a5d2b7acc30", "sha256": "efd85ecb834caa1e903d7ba1c689dff7f72e8547a580a4ca1edff2b889374c4c" }, "downloads": -1, "filename": "plumber-1.3.tar.gz", "has_sig": false, "md5_digest": "f7d13c5fd742a9b96b7c4a5d2b7acc30", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 36104, "upload_time": "2014-08-06T18:07:16", "url": "https://files.pythonhosted.org/packages/cd/b4/f190d0cc0421f9629737cfada55487145a41eb77d3d525f3e91417e14f5b/plumber-1.3.tar.gz" } ], "1.3.1": [ { "comment_text": "", "digests": { "md5": "e95c207e79d5c6d9afe2f9a58b2dc578", "sha256": "83201696b04e07cf5f53759f39756a6d3e93e78242de4d9b27c129a888a41e4d" }, "downloads": -1, "filename": "plumber-1.3.1.tar.gz", "has_sig": false, "md5_digest": "e95c207e79d5c6d9afe2f9a58b2dc578", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 36352, "upload_time": "2015-10-08T08:54:33", "url": "https://files.pythonhosted.org/packages/b6/66/f409c66b025fec583ae3af7a39aa4c529f8638c3207c39d658b43d03737e/plumber-1.3.1.tar.gz" } ], "1.4": [ { "comment_text": "", "digests": { "md5": "5aa8d31110b957e4d0a206e1df0a5b24", "sha256": "9629f8cf7d0eef98aa9fa652f7bf577f1a35aeeb4db508049dfa4945fe05b286" }, "downloads": -1, "filename": "plumber-1.4.tar.gz", "has_sig": false, "md5_digest": "5aa8d31110b957e4d0a206e1df0a5b24", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40266, "upload_time": "2017-06-07T10:13:32", "url": "https://files.pythonhosted.org/packages/d8/32/a5d646f582e17635b77d8f96a9d541de6ccc2ed6d1c3b00aa8c13c3e7130/plumber-1.4.tar.gz" } ], "1.5": [ { "comment_text": "", "digests": { "md5": "219c87d8b80fe5f0c6c0801611a60fc6", "sha256": "229d1cb754fdc07c62d831f62eb32c7a71727738d4a6b3a298e587156eea4093" }, "downloads": -1, "filename": "plumber-1.5.tar.gz", "has_sig": false, "md5_digest": "219c87d8b80fe5f0c6c0801611a60fc6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 41313, "upload_time": "2017-07-18T08:25:51", "url": "https://files.pythonhosted.org/packages/a0/55/cad009be81ca45a748a8f3e435171b0e43f276c04a5d5f871ca0b1603511/plumber-1.5.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "219c87d8b80fe5f0c6c0801611a60fc6", "sha256": "229d1cb754fdc07c62d831f62eb32c7a71727738d4a6b3a298e587156eea4093" }, "downloads": -1, "filename": "plumber-1.5.tar.gz", "has_sig": false, "md5_digest": "219c87d8b80fe5f0c6c0801611a60fc6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 41313, "upload_time": "2017-07-18T08:25:51", "url": "https://files.pythonhosted.org/packages/a0/55/cad009be81ca45a748a8f3e435171b0e43f276c04a5d5f871ca0b1603511/plumber-1.5.tar.gz" } ] }