Package: plumber
PlumberPlumbing is an alternative to mixin-based extension of classes.  In motivation
an incomplete list of limitations and/or design choices of python’s subclassing
are given along with plumber’s solutions for them. The plumbing system is
described in detail with code examples. Some design choices and ongoing
discussions are explained. Finally, in miscellanea you find nomenclature,
coverage report, list of contributors, changes and some todos.  All
non-experimental features are fully test covered.ContentsPlumberMotivation - Limitations of subclassingThe plumbing systemMiscellaneaChanges1.7 (2022-03-17)1.61.51.41.3.11.31.21.11.0LicenseMotivation - Limitations of subclassingPlumbing is an alternative to mixin-based extension of classes, motivated by
limitations and/or design choice of python’s subclassing:Control of precedence only through order of mixinsImpossible to provide default values to fill gaps on a base classsuper- chains are not verified during class creationNo conditionalsuper- chainsDocstrings are not accumulatedControl of precedence only through order of mixinsMixins are commonly used to extend classes with pre-defined behaviours: an
attribute on the first mixin overwrites attributes with the same name on all
following mixins and the base class being extended.>>>classMixin1(object):...a=1>>>classMixin2(object):...a=2...b=2>>>Base=dict>>>classMixedClass(Mixin1,Mixin2,Base):...pass>>>MixedClass.a1>>>MixedClass.b2>>>MixedClass.keys<method 'keys' of 'dict' objects>There is no way for a mixin later in the chain to take precedence over an
earlier one.Solution: plumber provides 3 decorators to enable finer control of
precedence (default,override,finalize).Impossible to provide default values to fill gaps on a base classA dictionary-like storage at least needs to provide__getitem__,__setitem__,__delitem__and__iter__, all other methods of a
dictionary can be build upon these. A mixin that turns storages into full
dictionaries needs to be able to provide default methods, taken if the base
class does not provide a (more efficient) implementation.Solution: plumber provides thedefaultdecorator to enable such
defaults.super- chains are not verified during class creationIt is possible to build a chain of methods usingsuper:Mixin1turns
the key lowercase before passing it on,Mixin2multiplies the result by 2
before returning it and both are chatty about start/stop.>>>classMixin1(object):...def__getitem__(self,key):...print('Mixin1 start')...key=key.lower()...ret=super(Mixin1,self).__getitem__(key)...print('Mixin1 stop')...returnret>>>classMixin2(object):...def__getitem__(self,key):...print('Mixin2 start')...ret=super(Mixin2,self).__getitem__(key)...ret=2*ret...print('Mixin2 stop')...returnret>>>Base=dict>>>classMixedClass(Mixin1,Mixin2,Base):...pass>>>mc=MixedClass()>>>mc['abc']=6>>>mc['ABC']Mixin1 start
Mixin2 start
Mixin2 stop
Mixin1 stop
12dict.__getitem__forms the endpoint of the chain as it returns a value
without delegating to a method later in the chain (usingsuper). If there
is no endpoint anAttributeErroris raised during runtime, not during class
creation.>>>classMixin1(object):...deffoo(self):...super(Mixin1,self).foo()>>>classMixedClass(Mixin1,Base):...pass>>>mc=MixedClass()>>>mc.foo()Traceback (most recent call last):...AttributeError:'super' object has no attribute 'foo'Solution: Plumber provides theplumbdecorator to build similar chains
using nested closures. These are create and verified during class creation.No conditionalsuper- chainsA mixin with subclassing needs to fit exactly the base class, there is no way
to conditionally hook into method calls depending on whether the base class
provides a method.Solution: Plumber provides theplumbifexistsdecorator that behaves
likeplumb, if there is an endpoint available.Docstrings are not accumulatedA class’ docstring that uses mixins is not build from the docstrings of the
mixins.Solution: Plumber enables plumbing of docstrings using a special marker__plbnext__, which is replaced with the docstring of the next “mixin”
Without the marker, docstrings are concatenated.The plumbing systemTheplumbermetaclass creates plumbing classes according to instructions
found on plumbing behaviors. First, all instructions are gathered, then they are
applied in two stages: stage1: extension and stage2: pipelines, docstrings and
optionalzope.interfaces. There exists a class decoratorplumbingwhich
should be used in favor of setting metaclass directly as of plumber 1.3.Plumbing behaviors provide instructionsThe plumber gathers instructionsStage 1 - ExtensionInteraction -finalize, plumbing declaration and base classesInteraction -override, plumbing declaration and base classesInteraction -default, plumbing declaration and base classInteraction -finalizewins overoverrideInteraction -finalizewins overdefaultSubclassing BehaviorsStage 2 - Pipeline, docstrings andzope.interfaceinstructionsPlumbing Pipelines in generalMethod pipelinesProperty pipelinesSubclassing BehaviorsMixing methods and properties within the same pipeline is not possibleDocstrings of classes, methods and propertiesSlots and plumbingszope.interface(if available)Plumber metaclass hooksPlumbing behaviors provide instructionsPlumbing behaviors correspond to mixins, but are more powerful and flexible. A
plumbing behavior needs to inherit fromplumber.Behaviorand declares
attributes with instructions on how to use them, here by example of thedefaultinstruction (more later).>>>fromplumberimportBehavior>>>fromplumberimportdefault>>>classBehavior1(Behavior):...a=default(True)......@default...deffoo(self):...return42>>>classBehavior2(Behavior):...@default...@property...defbar(self):...return17The instructions are given as behavior of assignments (a = default(None))
or as decorators (@default).A plumbing declaration defines theplumberas metaclass and one or more
plumbing behaviors to be processed from left to right. Further it may declare
attributes like every normal class, they will be treated as implicitfinalizeinstructions (see Stage 1: Extension).>>>fromplumberimportplumbing>>>Base=dict>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):......deffoobar(self):...return5The result is a plumbing class created according to the plumbing declaration.>>>plb=Plumbing()>>>plb.aTrue>>>plb.foo()42>>>plb.bar17>>>plb.foobar()5>>>plb['a']=1>>>plb['a']1A plumbing class can be subclassed like normal classes.>>>classSub(Plumbing):...a='Sub'>>>Sub.a'Sub'>>>Sub().foo()42>>>Sub().bar17>>>Sub().foobar()5The plumber gathers instructionsA plumbing declaration provides a list of behaviors via theplumbingdecorator. Behaviors provide instructions to be applied in two stages:Stage 1Extension viadefault,overrideandfinalize. This happens by
manipulating the class dict before the actual class is created.Stage 2Creation of pipelines viaplumbandplumbifexists.Plumbing of docstrings.Implemented interfaces fromzope.interface, if available.Stage 2 instructions are applied on the class object.The plumber iterates the Behavior list from left to right (behavior order) and
gathers the instructions to apply. A history of all seen instructions is kept.>>>pprint(Plumbing.__plumbing_stacks__.stage1){'a': <default 'a' of <class 'Behavior1'> payload=True>,
 'bar': <default 'bar' of <class 'Behavior2'> payload=<property object at ...>>,
 'foo': <default 'foo' of <class 'Behavior1'> payload=<function Behavior1.foo at ...>>}>>>pprint(Plumbing.__plumbing_stacks__.stage2){'__interfaces__': <_implements '__interfaces__' of None payload=()>}>>>pprint(Plumbing.__plumbing_stacks__.history)[<_implements '__interfaces__' of None payload=()>,
 <default 'a' of <class 'Behavior1'> payload=True>,
 <default 'foo' of <class 'Behavior1'> payload=<function Behavior1.foo at ...>>,
 <_implements '__interfaces__' of None payload=()>,
 <default 'bar' of <class 'Behavior2'> payload=<property object at ...>>]Each instruction belongs to a class member name and gets compared with the
previous instruction for this name if exists. It is then either taken as is,
discarded, merged or aPlumbingCollisionis raised. This is detailed in the
following sections.After all instructions are collected, they are applied taking declarations on
the plumbing class and base classes into account.NoteThe payload of an instruction is the attribute value passed to the
instruction via function call or decoration. An instruction knows the
behavior it is declared on.NoteBehaviors are created bybehaviormetaclass. Ifzope.interfaceis available, it will generate_implementsinstructions for each behavior.
During behavior creation the interfaces are not yet implemented, they are
checked at a later stage. Therefore the_implementsinstructions are
generated even if the behaviors do not implement interfaces, which results in
the empty tuple as payload (see alsozope.interface support.WarningDo not rely on this structure within your programs it might change
at any time. If you need information from the__plumbing_stacks__or lack
information in there, e.g. to create a plumbing inspector and earn yourself
a box of your favorite beverage, please let us know.Stage 1 - ExtensionThe extension stage creates endpoints for the pipelines created in stage 2. If
no pipeline uses the endpoint, it will just live on as a normal attribute in
the plumbing class’ dictionary.The extension decorators:finalizefinalizeis the strongest extension instruction. It will override
declarations on base classes and all other extension instructions
(overrideanddefault). Attributes declared as behavior of the
plumbing declaration are implicitfinalizedeclarations. Twofinalizefor one attribute name will collide and raise aPlumbingCollisionduring class creation.overrideoverrideis weaker thanfinalizeand overrides declarations on base
classes anddefaultdeclarations. Twooverrideinstructions for the
same attribute name do not collide, instead the first one will be used.defaultdefaultis the weakest extension instruction. It will not even override
declarations of base classes. The first default takes precendence over
later defaults.Interaction -finalize, plumbing declaration and base classesInteraction -override, plumbing declaration and base classesInteraction -default, plumbing declaration and base classInteraction -finalizewins overoverrideInteraction -finalizewins overdefaultSubclassing BehaviorsInteraction -finalize, plumbing declaration and base classesIn code.>>>fromplumberimportfinalize>>>classBehavior1(Behavior):...N=finalize('Behavior1')...>>>classBehavior2(Behavior):...M=finalize('Behavior2')>>>classBase(object):...K='Base'>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):...L='Plumbing'>>>forxin['K','L','M','N']:...print('%sfrom%s'%(x,getattr(Plumbing,x)))K from Base
L from Plumbing
M from Behavior2
N from Behavior1summary:K-Q: attributes defined by behaviors, plumbing class and base classesf:finalizedeclarationx: declaration on plumbing class or base class?: base class declaration is irrelevantY: chosen end pointcollision: indicates an invalid combination, that raises aPlumbingCollisionAttrBehavior1Behavior2PlumbingBaseok?KxLx?Mf?Nf?Ofx?collisionPfx?collisionQff?collisioncollisions.>>>classBehavior1(Behavior):...O=finalize(False)>>>@plumbing(Behavior1)...classPlumbing(object):...O=TrueTraceback (most recent call last):...plumber.exceptions.PlumbingCollision:Plumbing class
  with:
    <finalize 'O' of <class 'Behavior1'> payload=False>>>>classBehavior2(Behavior):...P=finalize(False)>>>@plumbing(Behavior2)...classPlumbing(object):...P=TrueTraceback (most recent call last):...plumber.exceptions.PlumbingCollision:Plumbing class
  with:
    <finalize 'P' of <class 'Behavior2'> payload=False>>>>classBehavior1(Behavior):...Q=finalize(False)>>>classBehavior2(Behavior):...Q=finalize(True)>>>@plumbing(Behavior1,Behavior2)...classPlumbing(object):...passTraceback (most recent call last):...plumber.exceptions.PlumbingCollision:<finalize 'Q' of <class 'Behavior1'> payload=False>
  with:
    <finalize 'Q' of <class 'Behavior2'> payload=True>Interaction -override, plumbing declaration and base classesin code.>>>fromplumberimportoverride>>>classBehavior1(Behavior):...K=override('Behavior1')...M=override('Behavior1')>>>classBehavior2(Behavior):...K=override('Behavior2')...L=override('Behavior2')...M=override('Behavior2')>>>classBase(object):...K='Base'...L='Base'...M='Base'>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):...K='Plumbing'>>>forxin['K','L','M']:...print('%sfrom%s'%(x,getattr(Plumbing,x)))K from Plumbing
L from Behavior2
M from Behavior1summary:K-M: attributes defined by behaviors, plumbing class and base classese:overridedeclarationx: declaration on plumbing class or base class?: base class declaration is irrelevantY: chosen end pointAttrBehavior1Behavior2PlumbingBaseKeex?Le?Mee?Interaction -default, plumbing declaration and base classin code.>>>classBehavior1(Behavior):...N=default('Behavior1')>>>classBehavior2(Behavior):...K=default('Behavior2')...L=default('Behavior2')...M=default('Behavior2')...N=default('Behavior2')>>>classBase(object):...K='Base'...L='Base'>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):...L='Plumbing'>>>forxin['K','L','M','N']:...print('%sfrom%s'%(x,getattr(Plumbing,x)))K from Base
L from Plumbing
M from Behavior2
N from Behavior1summary:K-N: attributes defined by behaviors, plumbing class and base classesd =defaultdeclarationx = declaration on plumbing class or base class? = base class declaration is irrelevantY= chosen end pointAttrBehavior1Behavior2PlumbingBaseKdxLdx?MdNddInteraction -finalizewins overoverridein code.>>>classBehavior1(Behavior):...K=override('Behavior1')...L=finalize('Behavior1')>>>classBehavior2(Behavior):...K=finalize('Behavior2')...L=override('Behavior2')>>>classBase(object):...K='Base'...L='Base'>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):...pass>>>forxin['K','L']:...print('%sfrom%s'%(x,getattr(Plumbing,x)))K from Behavior2
L from Behavior1summary:K-L: attributes defined by behaviors, plumbing class and base classese =overridedeclarationf =finalizedeclaration? = base class declaration is irrelevantY= chosen end pointAttrBehavior1Behavior2PlumbingBaseKef?Lfe?Interaction -finalizewins overdefaultin code.>>>classBehavior1(Behavior):...K=default('Behavior1')...L=finalize('Behavior1')>>>classBehavior2(Behavior):...K=finalize('Behavior2')...L=default('Behavior2')>>>classBase(object):...K='Base'...L='Base'>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):...pass>>>forxin['K','L']:...print('%sfrom%s'%(x,getattr(Plumbing,x)))K from Behavior2
L from Behavior1summary:K-L: attributes defined by behaviors, plumbing class and base classesd =defaultdeclarationf =finalizedeclaration? = base class declaration is irrelevantY= chosen end pointAttrBehavior1Behavior2PlumbingBaseKdf?Lfd?Interaction -overridewins overdefault~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~in code.>>>classBehavior1(Behavior):...K=default('Behavior1')...L=override('Behavior1')>>>classBehavior2(Behavior):...K=override('Behavior2')...L=default('Behavior2')>>>classBase(object):...K='Base'...L='Base'>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):...pass>>>forxin['K','L']:...print('%sfrom%s'%(x,getattr(Plumbing,x)))K from Behavior2
L from Behavior1summary:K-L: attributes defined by behaviors, plumbing class and base classesd =defaultdeclaratione =overridedeclaration? = base class declaration is irrelevantY= chosen end pointAttrBehavior1Behavior2PlumbingBaseKde?Led?Subclassing Behaviorsin code.>>>classBehavior1(Behavior):...J=default('Behavior1')...K=default('Behavior1')...M=override('Behavior1')>>>classBehavior2(Behavior1):...J=default('Behavior2')# overrides ``J`` of ``Behavior1``...L=default('Behavior2')...M=default('Behavior2')# this one wins, even if ``M`` on...# superclass is ``override`` instruction....# due to ordinary inheritance behavior.>>>@plumbing(Behavior2)...classPlumbing(object):...pass>>>plb=Plumbing()>>>plb.J'Behavior2'>>>plb.K'Behavior1'>>>plb.L'Behavior2'>>>plb.M'Behavior2'Stage 2 - Pipeline, docstrings andzope.interfaceinstructionsIn stage1 plumbing class attributes were set, which can serve as endpoints for
plumbing pipelines that are build in stage2. Plumbing pipelines correspond tosuper-chains. Docstrings of behaviors, methods in a pipeline and properties
in a pipeline are accumulated. Plumber iszope.interfaceaware and takes
implemeneted interfaces from behaviors, if it can be imported.Plumbing Pipelines in generalMethod pipelinesProperty pipelinesSubclassing BehaviorsMixing methods and properties within the same pipeline is not possibleDocstrings of classes, methods and propertiesSlots and plumbingszope.interface(if available)Plumbing Pipelines in generalElements for plumbing pipelines are declared with theplumbandplumbifexistsdecorators:plumbMarks a method to be used as behavior of a plumbing pipeline.  The signature of
such a plumbing method isdef foo(next_, self, *args, **kw).  Vianext_it is passed the next plumbing method to be called.selfis
an instance of the plumbing class, not the behavior.plumbifexistsLikeplumb, but only used if an endpoint exists.The user of a plumbing class does not know whichnext_to pass. Therefore,
after the pipelines are built, an entrance method is generated for each pipe,
that wraps the first plumbing method passing it the correctnext_. Eachnext_method is an entrance to the rest of the pipeline.The pipelines are build in behavior order, skipping behaviors that do not
define a pipeline element with the same attribute name:+---+-----------+-----------+-----------+----------+
|   | Behavior1 | Behavior2 | Behavior3 | ENDPOINT |
+---+-----------+-----------+-----------+----------+
|   |      --------------------------------->      |
| E |     x     |           |           |    x     |
| N |      <---------------------------------      |
+ T +-----------+-----------+-----------+----------+
| R |      ----------> --------------------->      |
| A |     y     |     y     |           |    y     |
| N |      <---------- <---------------------      |
+ C +-----------+-----------+-----------+----------+
| E |           |           |      --------->      |
| S |           |           |     z     |    z     |
|   |           |           |      <---------      |
+---+-----------+-----------+-----------+----------+Method pipelinesTwo plumbing behaviors and adictas base class.Behavior1lowercases
keys before passing them on,Behavior2multiplies results before returning
them.>>>fromplumberimportplumb>>>classBehavior1(Behavior):...@plumb...def__getitem__(next_,self,key):...print('Behavior1 start')...key=key.lower()...ret=next_(self,key)...print('Behavior1 stop')...returnret>>>classBehavior2(Behavior):...@plumb...def__getitem__(next_,self,key):...print('Behavior2 start')...ret=2*next_(self,key)...print('Behavior2 stop')...returnret>>>Base=dict>>>@plumbing(Behavior1,Behavior2)...classPlumbing(Base):...pass>>>plb=Plumbing()>>>plb['abc']=6>>>plb['AbC']Behavior1 start
Behavior2 start
Behavior2 stop
Behavior1 stop
12Plumbing pipelines need endpoints. If no endpoint is available anAttributeErroris raised.>>>classBehavior1(Behavior):...@plumb...deffoo(next_,self):...pass>>>@plumbing(Behavior1)...classPlumbing(object):...passTraceback (most recent call last):...AttributeError:type object 'Plumbing' has no attribute 'foo'If no endpoint is available and a behavior does not care about that,plumbifexistscan be used to only plumb if an endpoint is available.>>>fromplumberimportplumbifexists>>>classBehavior1(Behavior):...@plumbifexists...deffoo(next_,self):...pass......@plumbifexists...defbar(next_,self):...return2*next_(self)>>>@plumbing(Behavior1)...classPlumbing(object):......defbar(self):...return6>>>hasattr(Plumbing,'foo')False>>>Plumbing().bar()12This enables one implementation of a certain behaviour, e.g. sending events for
dictionaries, to be used for readwrite dictionaries that implement__getitem__and__setitem__and readonly dictionaries, that only
implement__getitem__but no__setitem__.Property pipelinesPlumbing of read only properties.>>>classBehavior1(Behavior):...@plumb...@property...deffoo(next_,self):...return2*next_(self)>>>@plumbing(Behavior1)...classPlumbing(object):......@property...deffoo(self):...return3>>>plb=Plumbing()>>>plb.foo6It is possible to extend a property with so far unset getter/setter/deleter.>>>classBehavior1(Behavior):...@plumb...@property...deffoo(next_,self):...return2*next_(self)>>>classBehavior2(Behavior):...defset_foo(self,value):...self._foo=value...foo=plumb(property(...None,...override(set_foo),...))>>>@plumbing(Behavior1,Behavior2)...classPlumbing(object):......@property...deffoo(self):...returnself._foo>>>plb=Plumbing()>>>plb.foo=4>>>plb.foo8Subclassing BehaviorsOther than stage 1 instructions, which extend a class with properties
and functions and thus override each other by the rules of ordinary
subclassing, pipeline instructions are aggregated.>>>classBehavior1(Behavior):......@plumb...deffoo(next_,self):...return'Behavior1 '+next_(self)......@plumb...defbar(next_,self):...return'Behavior1 '+next_(self)>>>classBehavior2(Behavior1):......@plumb...deffoo(next_,self):...return'Behavior2 '+next_(self)>>>@plumbing(Behavior2)...classPlumbing(object):......deffoo(self):...return'foo'......defbar(self):...return'bar'>>>plb=Plumbing()>>>plb.foo()'Behavior2 Behavior1 foo'>>>plb.bar()'Behavior1 bar'Mixing methods and properties within the same pipeline is not possibleWithin a pipeline all elements need to be of the same type, it is not possible
to mix properties with methods.>>>classBehavior1(Behavior):...@plumb...deffoo(next_,self):...returnnext_(self)>>>@plumbing(Behavior1)...classPlumbing(object):......@property...deffoo(self):...return5Traceback (most recent call last):...plumber.exceptions.PlumbingCollision:<plumb 'foo' of <class 'Behavior1'> payload=<function Behavior1.foo at 0x...>>
  with:
    <class 'Plumbing'>Docstrings of classes, methods and propertiesNormal docstrings of the plumbing declaration and the behavior classes, plumbed
methods and plumbed properties are joined by newlines starting with the
plumbing declaration and followed by the behaviors in reverse order.>>>classP1(Behavior):..."""P1..."""...@plumb...deffoo(self):..."""P1.foo..."""...bar=plumb(property(None,None,None,'P1.bar'))>>>classP2(Behavior):...@override...deffoo(self):..."""P2.foo..."""...bar=plumb(property(None,None,None,'P2.bar'))>>>@plumbing(P1,P2)...classPlumbing(object):..."""Plumbing..."""...bar=property(None,None,None,'Plumbing.bar')>>>print(Plumbing.__doc__)Plumbing
<BLANKLINE>
P1
<BLANKLINE>>>>print(Plumbing.foo.__doc__)P2.foo
<BLANKLINE>
P1.foo
<BLANKLINE>>>>print(Plumbing.bar.__doc__)Plumbing.bar
<BLANKLINE>
P2.bar
<BLANKLINE>
P1.barThe accumulation of docstrings is an experimental feature and will probably
change.Slots and plumbingsA plumbing class can have __slots__ like normal classes.>>>classP1(Behavior):...@default...defsomewhing_which_writes_to_foo(self,foo_val):...self.foo=foo_val>>>@plumbing(P1)...classWithSlots(object):...__slots__='foo'>>>WithSlots.__dict__['foo']<member 'foo' of 'WithSlots' objects>>>>ob=WithSlots()>>>ob.somewhing_which_writes_to_foo('foo')>>>assert(ob.foo=='foo')zope.interface(if available)The plumber does not depend onzope.interfacebut is aware of it. That
means it will try to import it and if available will check plumbing behaviors
for implemented interfaces and will make the plumbing implement them, too.>>>fromzope.interfaceimportInterface>>>fromzope.interfaceimportimplementerA class with an interface that will serve as base class of a plumbing.>>>classIBase(Interface):...pass>>>@implementer(IBase)...classBase(object):...pass>>>IBase.implementedBy(Base)TrueTwo behaviors with corresponding interfaces, one with a base class that also
implements an interface.>>>classIBehavior1(Interface):...pass>>>@implementer(IBehavior1)...classBehavior1(Behavior):...blub=1>>>classIBehavior2Base(Interface):...pass>>>@implementer(IBehavior2Base)...classBehavior2Base(Behavior):...pass>>>classIBehavior2(Interface):...pass>>>@implementer(IBehavior2)...classBehavior2(Behavior2Base):...pass>>>IBehavior1.implementedBy(Behavior1)True>>>IBehavior2Base.implementedBy(Behavior2Base)True>>>IBehavior2Base.implementedBy(Behavior2)True>>>IBehavior2.implementedBy(Behavior2)TrueA plumbing based onBaseusingBehavior1andBehavior2and
implementingIPlumbingClass.>>>classIPlumbingClass(Interface):...pass>>>@implementer(IPlumbingClass)...@plumbing(Behavior1,Behavior2)...classPlumbingClass(Base):...passThe directly declared and inherited interfaces are implemented.>>>IPlumbingClass.implementedBy(PlumbingClass)True>>>IBase.implementedBy(PlumbingClass)TrueThe interfaces implemented by the Behaviors are also implemented.>>>IBehavior1.implementedBy(PlumbingClass)True>>>IBehavior2.implementedBy(PlumbingClass)True>>>IBehavior2Base.implementedBy(PlumbingClass)TrueAn instance of the class provides the interfaces.>>>plb=PlumbingClass()>>>IPlumbingClass.providedBy(plb)True>>>IBase.providedBy(plb)True>>>IBehavior1.providedBy(plb)True>>>IBehavior2.providedBy(plb)True>>>IBehavior2Base.providedBy(plb)TruePlumber metaclass hooksIn case one writes a plumbing behavior requiring class manipulation at creation
time, a decorator is provided for registering callbacks which are executed
after stage 1 and stage 2 instructions have been applied.>>>fromplumberimportplumber>>>classIBehaviorInterface(Interface):...pass>>>@plumber.metaclasshook...deftest_metclass_hook(cls,name,bases,dct):...ifnotIBehaviorInterface.implementedBy(cls):...return...cls.hooked=True>>>@implementer(IBehaviorInterface)...classMetaclassConsideredBehavior(Behavior):...pass>>>@plumbing(MetaclassConsideredBehavior)...classPlumbing(object):...pass>>>Plumbing.hookedTrueMiscellaneaNomenclatureplumberMetaclass that creates a plumbing according to the instructions declared on
plumbing behaviors. Instructions are given by decorators:default,override,finalize,plumbandplumbifexists.plumbingA plumbing is a class decorated byplumbingdecorator which gets passed
the behviors to apply, e.g.@plumbing(Behavior1, Behavior2). Apart from
the behaviors, declarations on base classes and the class asking for the
plumber are taken into account. Once created, a plumbing looks like any
other class and can be subclassed as usual.plumbing behaviorA plumbing behavior provides attributes (functions, properties and plain
values) along with instructions for how to use them. Instructions are given
via decorators:default,override,finalize,plumbandplumbifexists(see Stage 1:… and Stage 2:…).plumbing pipelinePlumbing methods/properties with the same name form a pipeline. The
entrance and end-point have the signature of normal methods:def foo(self, *args, **kw). The plumbing pipelines is a series of nested
closures (seenext_).entrance (method)A method with a normal signature. i.e. expectingselfas first
argument, that is used to enter a pipeline. It is anext_function. A
method declared on the class with the same name, will be overwritten, but
referenced in the pipelines as the innermost method, the endpoint.``next_`` functionThenext_function is used to call the next method in a pipelines: in
case of a plumbing method, it is a wrapper of it that passes the correct
nextnext_as first argument and in case of an end-point, just the
end-point method itself.end-point (method)Method retrieved from the plumbing class withgetattr(), before setting
the entrance method on the class.If you feel something is missing, please let us now or write a short
corresponding text.Python VersionsPython 2.7, 3.7+May work with other versions (untested)ContributorsFlorian FriesdorfRobert NiederreiterJens W. KleinMarco LempenAttila OláhChanges1.7 (2022-03-17)Do not run test suites for Python 2.6 and Python < 3.7 any more.
[rnix, 2022-03-16]ReplaceBaseswrapper with asetcontainingderived_membersto avoid multiple base class iteration.
[rnix, 2022-02-13]Only keep most recent instructions onStackswhen parsing behaviors.
[rnix, 2022-02-13]SimplifyStacksobject.
[rnix, 2022-02-13]Apply stage 1 and stage 2 instructions inplumber.__new__instead of
applying stage 2 instructions inplumber.__init__.
[rnix, 2022-02-13]1.6Use raw string for regular expression.
[rnix, 2020-05-28]Drop python 2.6 support.
[rnix, 2019-03-25]1.5Introduceplumber.metaclasshookdecorator.
[rnix, 2017-06-16]1.4No more “private” module names.
[rnix, 2017-05-21]Python 3 support.
[rnix, 2017-05-18]1.3.1Avoid use of deprecateddict.has_key.
[rnix, 2015-10-05]1.3Introduceplumbingdecorator.
[rnix, 2014-07-31]Remove deprecatedplumber.extendandplumber.Part.
[rnix, 2014-07-31]1.2Deprecateplumber.extend. Useplumber.overrideinstead.
[rnix, 2012-07-28]Deprecateplumber.Part. Useplumber.Behaviorinstead.
[rnix, 2012-07-28]1.1Usezope.interface.implementerinstead ofzope.interface.implements.
[rnix, 2012-05-18]1.0.. plbnext::instead of.. plb_next::[chaoflow 2011-02-02]stage1 in __new__, stage2 in __init__, setting of __name__ now works
[chaoflow 2011-01-25]instructions recognize equal instructions
[chaoflow 2011-01-24]instructions from base classes now like subclass inheritance [chaoflow 2011
[chaoflow 2011-01-24]doctest order now plumbing order: P1, P2, PlumbingClass, was PlumbingClass,
P1, P2
[chaoflow 2011-01-24]merged docstring instruction into plumb
[chaoflow 2011-01-24]plumber instead of Plumber
[chaoflow 2011-01-24]plumbing methods are not classmethods of part anymore
[chaoflow 2011-01-24]complete rewrite
[chaoflow 2011-01-22]prt instead of cls
[chaoflow, rnix 2011-01-19default, extend, plumb
[chaoflow, rnix 2011-01-19]initial
[chaoflow, 2011-01-04]LicenseCopyright (c) 2011-2021, BlueDynamics Alliance, Austria, Germany, Switzerland
Copyright (c) 2021-2022, Node Contributors
All rights reserved.Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.