Python Wires: Simple Callable Wiring¶
Python Wires is a library to facilitate callable wiring by decoupling callers from callees. It can be used as a simple callable-based event notification system, as an in-process publish-subscribe like solution, or in any context where 1:N callable decoupling is appropriate.
Installation¶
Python Wires is a pure Python package distributed via PyPI. Install it with:
$ pip install wires
Quick Start¶
Create a Wires
object:
from wires import Wires
w = Wires()
Its attributes are callables, auto-created on first access, that can be wired to other callables:
def say_hello():
print('Hello from wires!')
w.my_callable.wire(say_hello) # Wires `w.my_callable`, auto-created, to `say_hello`.
Calling such callables calls their wired callables:
w.my_callable() # Prints 'Hello from wires!'
More wirings can be added:
def say_welcome():
print('Welcome!')
w.my_callable.wire(say_welcome) # Wires `w.my_callable` to `say_welcome`, as well.
w.my_callable() # Prints 'Hello from wires!' and 'Welcome!'.
Wirings can also be removed:
w.my_callable.unwire(say_hello) # Removes the wiring to `say_hello`.
w.my_callable() # Prints 'Welcome!'
w.my_callable.unwire(say_welcome) # Removes the wiring to `say_welcome`.
w.my_callable() # Does nothing.
To learn more about Python Wires, including passing parameters, setting wiring limits and tuning the call-time coupling behaviour, please refer to the remaining documentation at https://python-wires.readthedocs.org/.
Contents¶
Concepts¶
This section describes the key concepts in Python Wires, purposely omitting sample code and fine grained details. For that, refer to the API Usage Examples and to the API Reference.
Wires Objects¶
Python Wires provides a single, public Wires
class, through which the full API is accessed. Using it revolves mostly around using Wires
object attributes, which are:
- Auto-created on first-access.
- Callable, like regular Python functions.
Being callable, Wires
object attributes will be referred to as wires callables, from here on.
Wires
objects and their wires callables behave differently depending on initialization arguments. These can be overridden on a per-wires callable basis or even at call-time.
Wirings¶
These principles describe the essence of Python Wires:
- Wires callables can be wired to one or more other callables.
- Calling a wires callable calls all its wired callables, in wiring order.
To support the remaining documentation, the following definitions are established:
- wirings
- The list of wires callable’s wired callables, in wiring order.
- number of wirings
- The length of wirings.
- wire actions
- Add a new callable, which becomes wired, to the end of wirings.
- unwire actions
- Remove the first matching wired callable from wirings.
By default there are no limits on the number of wirings a given wires callable has. Those can be enforced at the Wires
object’s level (set at initialization time and applicable to all wires callables on that object), or at the individual wires callable’s level (set at any time), overriding its Wires
object’s settings. With such limits in place:
- Wire actions fail when the current number of wirings is the maximum allowed.
- Unwire actions fail when the current number of wirings is the minimum allowed.
- Calling fails there are less than the minimum allowed number of wirings.
Additionally, changing wires callable’s wiring limits fails when the current number of wirings is greater than zero and does not meet the new limits.
Argument Passing¶
Like regular Python functions, wires callables can be passed positional and keyword arguments at call-time. Additionally, wire-time positional and keyword arguments can be set on each wire action.
At call-time, each wired callable is passed a combination of its wire-time arguments with the call-time arguments:
- Call-time positional arguments are passed by position, after wire-time positional arguments.
- Call-time keyword arguments are passed by name, overriding wire-time keyword arguments named alike.
Handling arguments is up to each individual wired callable. The way failures are handled, including argument mismatching ones is described next.
Call-time Coupling¶
Call-time coupling defines how coupled/decoupled are the callers of wires callables from the respective wired callables.
By default, call-time coupling is fully decoupled:
- Calling a wires callable returns
None
, regardless of what each wired callable returns or whether or not calling a given wired callable raises an exception. - All wired callables will be called, in order, regardless of the fact that calling a given wired callable may raise an exception.
Call-time coupling behaviour can be changed with two independent flags:
Ignore Exceptions
- When on, all wired callables are called, in order, regardless of the fact that calling a given wired callable may raise an exception.
- When off, no further wired callables will be called once calling a given wired callable raises an exception.
Returns
When off, calling a wires callable always returns
None
.When on, calling a wires callable will return a value or raise an exception:
- An exception will be raised when Ignore Exceptions is off and calling a wired callable raises an exception.
- A value is returned in every other case: a list of
(<exception>, <result>)
tuples containing either the raised<exception>
or returned<result>
for each wired callable, in the wiring order.
Call-time coupling flags can be set at Wires
objects initialization time (applicable to all wires callables on that object), defined on a per-wires callable basis, or overridden at call-time.
API Usage Examples¶
Note
Under Python 2.7, some of these examples require the following import statement at the top:
from __future__ import print_function
Getting a Wires object¶
Python Wires ships with a built-in, shared, readily usable Wires
object, called w
.
from wires import w # `w` is a built-in, shared `Wires` object
Alternatively, Wires
objects are created with:
from wires import Wires
w = Wires()
The Wires
class initializer
supports optional arguments to override the default behaviour. For example, here’s a Wires
object requiring its wires callables to have exactly one wiring:
from wires import Wires
w = Wires(min_wirings=1, max_wirings=1)
Wiring and Unwiring¶
The wire
method wires a callable to a wires callable [1]:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable.wire(say_hi) # calling `w.callable` will call `say_hi`
Multiple wirings to the same callable are allowed:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable.wire(say_hi) # calling `w.callable` will call `say_hi`
w.callable.wire(say_hi) # calling `w.callable` will call `say_hi` twice
Wiring a non-callable raises a TypeError
exception:
from wires import w
w.callable.wire(42) # raises TypeError: 42 isn't callable
The unwire
method unwires a given callable:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable.wire(say_hi) # calling `w.callable` will call `say_hi`
w.callable.unwire(say_hi) # calling `w.callable` no longer calls `say_hi`
If multiple wirings exist, unwire
unwires the first matching wiring only:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable.wire(say_hi) # calling `w.callable` will call `say_hi`
w.callable.wire(say_hi) # calling `w.callable` will call `say_hi` twice
w.callable.unwire(say_hi) # calling `w.callable` will call `say_hi` once
w.callable.unwire(say_hi) # calling `w.callable` no longer calls `say_hi`
Unwiring a non-wired callable raises a ValueError
:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable.unwire(say_hi) # raises ValueError: non-wired `say_hi`
Wiring Limits¶
Limiting the number of wirings on wires callables can be done in two different ways.
Using a custom-initialized Wires
object, its wires callables default to its wiring limits:
from wires import Wires
def say_hi():
print('Hello from wires!')
def say_bye():
print('Bye, see you soon.')
w = Wires(min_wirings=1, max_wirings=1)
w.callable.wire(say_hi)
w.callable.wire(say_bye) # raises RuntimeError: max_wirings limit reached
w.callable.unwire(say_hi) # raises RuntimeError: min_wirings limit reached
Overriding wiring limits on a wires callable basis:
from wires import Wires
def say_hi():
print('Hello from wires!')
def say_bye():
print('Bye, see you soon.')
w = Wires() # defaults to no wiring limits
w.callable1.min_wirings = 1 # set `w.callable1`'s min wirings
w.callable1.max_wirings = 1 # set `w.callable1`'s max wirings
w.callable1.wire(say_hi)
w.callable1.wire(say_bye) # raises RuntimeError: max_wirings limit reached
w.callable1.unwire(say_hi) # raises RuntimeError: min_wirings limit reached
w.callable2.wire(say_hi)
w.callable2.wire(say_bye) # works, no limits on `w.callable2`
w.callable2.unwire(say_bye) # works, no limits on `w.callable2`
w.callable2.unwire(say_hi) # works, no limits on `w.callable2`
Clearing wiring limits on a per-wires callable basis:
from wires import Wires
def say_hi():
print('Hello from wires!')
def say_bye():
print('Bye, see you soon.')
w = Wires(min_wirings=1, max_wirings=1)
w.callable1.min_wirings = None # no min wiring limit on `w.callable1`
w.callable1.max_wirings = None # no max wiring limit on `w.callable1`
w.callable1.wire(say_hi)
w.callable1.wire(say_bye) # works, no limits on `w.callable1`
w.callable1.unwire(say_bye) # works, no limits on `w.callable1`
w.callable1.unwire(say_hi) # works, no limits on `w.callable1`
w.callable2.wire(say_hi)
w.callable2.wire(say_bye) # raises RuntimeError: max_wirings limit reached
w.callable2.unwire(say_hi) # raises RuntimeError: min_wirings limit reached
Overriding per-wires callable wiring limits raises a ValueError
when:
- There is at least one wiring.
- The current wirings don’t meet the limit trying to be set.
from wires import w
def say_hi():
print('Hello from wires!')
w.callable.wire(say_hi)
w.callable.min_wirings = 2 # raises ValueError: too few wirings
Calling¶
Calling a just-created, default wires callable works:
from wires import w
w.callable()
Calling a wires callable calls its wired callables, in wiring order:
from wires import w
def say_hi():
print('Hello from wires!')
def say_bye():
print('Bye, see you soon.')
w.callable1.wire(say_hi)
w.callable1.wire(say_bye)
w.callable1() # calls `say_hi` first, then `say_bye`
w.callable2.wire(say_bye)
w.callable2.wire(say_hi)
w.callable2() # calls `say_bye` first, then `say_hi`
Calling a wires callable where the current number of wirings is below the minimum wiring limit raises a ValueError
(set by the Wires
object or overriden at the wires callable level):
from wires import w
w.callable.min_wirings = 1
w.callable() # raises ValueError: less than min_wirings wired
Argument Passing¶
Call-time arguments are passed to each wired callable:
from wires import w
def a_print(*args, **kw):
print('args=%r kw=%r' % (args, kw))
w.callable.wire(a_print)
w.callable() # prints: args=() kw={}
w.callable(42, 24) # prints: args=(42, 24) kw={}
w.callable(a=42, b=24) # prints: args=() kw={'a': 42, 'b': 24}
w.callable(42, a=24) # prints: args=(42,) kw={'a': 24}
Wiring actions can include wire-time arguments, later combined with call-time arguments:
from wires import w
def a_print(*args, **kw):
print('args=%r kw=%r' % (args, kw))
w.callable1.wire(a_print, 'one')
w.callable2.wire(a_print, a='nother')
w.callable1() # prints: args=('one',) kw={}
w.callable1(42, 24) # prints: args=('one', 42, 24) kw={}
w.callable1(a=42, b=24) # prints: args=('one',) kw={'a': 42, 'b': 24}
w.callable1(42, a=24) # prints: args=('one', 42) kw={'a': 24}
w.callable2() # prints: args=() kw={'a': 'nother'}
w.callable2(42, 24) # prints: args=(42, 24) kw={'a': 'nother'}
w.callable2(a=42, b=24) # prints: args=() kw={'a': 42, 'b': 24}
w.callable2(42, a=24) # prints: args=(42,) kw={'a': 24}
Unwiring actions can include wire-time arguments in the unwire
call:
- If no positional/keyword arguments are passed (other than the mandatory callable argument) the first wiring to that callable is removed.
- If positional/keyword arguments are passed, the specific wiring to that callable with the provided wire-time arguments is removed.
In either case, a ValueError
is raised when no matching wiring exists.
from wires import w
def p_arg(arg):
print(arg)
w.callable.wire(p_arg, 'a')
w.callable() # prints 'a'
w.callable.wire(p_arg, 'b')
w.callable() # prints 'a', then prints 'b'
w.callable.unwire(p_arg, 'b')
w.callable() # prints 'a'
w.callable.unwire(p_arg)
w.callable() # does nothing
w.callable.unwire(p_arg, 'c') # raises ValueError: no such wiring
Call-time coupling¶
Note
For a description of possible behaviours, refer to Call-time Coupling Concepts.
By default, calling a wires callable calls all its wirings and returns None
:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires() # Default call coupling.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w.callable() # prints 'about to raise', then 'about to return'
# returns None
Call-time coupling can be:
- Set at the
Wires
object level, applicable to all its wired callables. - Overridden on a wires callable basis.
- Overridden at call-time.
Setting returns at the Wires
object level:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires(returns=True) # Non-default call coupling.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w.callable() # prints 'about to raise', then 'about to return'
# returns [(ZeroDivisionError(), None), (None, 42)]
Overriding returns at the wires callable level:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires() # Default call coupling.
w.callable.returns = True # Override call coupling for `callable`.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w.callable() # prints 'about to raise', then 'about to return'
# returns [(ZeroDivisionError(), None), (None, 42)]
Overriding returns at call-time:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires() # Default call coupling.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w(returns=True).callable() # Override call coupling at call time.
# prints 'about to raise', then 'about to return'
# returns [(ZeroDivisionError(), None), (None, 42)]
Setting ignore exceptions at the Wires
object level:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires(ignore_exceptions=False) # Non-default call coupling.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w.callable() # prints 'about to raise' only
# returns None
Overriding ignore exceptions at the wires callable level:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires() # Default call coupling.
w.callable.ignore_exceptions = False # Override call coupling for `callable`.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w.callable() # prints 'about to raise' only
# returns None
Overriding ignore exceptions at call-time:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires() # Default call coupling.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w(ignore_exceptions=False).callable() # Override call coupling at call time.
# prints 'about to raise' only
# returns None
Setting both returns and ignore exceptions at the Wires
level:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires(returns=True, ignore_exceptions=False) # Non-default call coupling.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w.callable() # prints 'about to raise' only
# raises RuntimeError((ZeroDivisionError(), None),)
Overriding both returns and ignore exceptions at the wires callable level:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires() # Default call coupling.
w.callable.returns = True # Override call coupling for `callable`.
w.callable.ignore_exceptions = False # Override call coupling for `callable`.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w.callable() # prints 'about to raise' only
# raises RuntimeError((ZeroDivisionError(), None),)
Note
Overriding multiple per wires callable settings can also be done with a
single call to set
.
Overriding both returns and ignore exceptions at call-time:
from wires import Wires
def raise_exception():
print('about to raise')
raise ZeroDivisionError()
def return_42():
print('about to return')
return 42
w = Wires() # Default call coupling.
w.callable.wire(raise_exception)
w.callable.wire(return_42)
w(returns=True, ignore_exceptions=False).callable()
# prints 'about to raise' only
# raises RuntimeError((ZeroDivisionError(), None),)
Introspection¶
Wires
objects¶
Using dir()
to obtain all attributes:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable1.wire(say_hi)
w.callable2.wire(say_hi)
dir(w) # 'callable1' and 'callable2' in resulting list
Using len()
to get the wires callables count:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable1.wire(say_hi)
w.callable2.wire(say_hi)
len(w) # returns 2
Iterating over a Wires
object produces each wires callable:
from wires import w
def say_hi():
print('Hello from wires!')
w.callable1.wire(say_hi)
w.callable2.wire(say_hi)
for wcallable in w:
wcallable() # calls `w.callable1` and `w.callable2`
# order not guaranteed
Wires callable objects¶
Each holds a __name__
attribute, like regular Python functions:
from wires import w
w.callable.__name__ # returns 'callable'
Getting min_wirings
and max_wirings
, falling back to the Wires
object settings:
from wires import Wires
w = Wires(min_wirings=1, max_wirings=1)
w.callable1.min_wirings # returns 1
w.callable1.max_wirings # returns 1
w.callable2.min_wirings = None
w.callable2.max_wirings = None
w.callable2.min_wirings # returns None
w.callable2.max_wirings # returns None
Getting returns
and ignore_exceptions
, falling back to the Wires
object settings:
from wires import Wires
w = Wires(returns=True, ignore_exceptions=False)
w.callable1.returns # returns True
w.callable1.ignore_exceptions # returns False
w.callable2.returns = False
w.callable2.ignore_exceptions = True
w.callable2.returns # returns False
w.callable2.ignore_exceptions # returns True
Using len()
to get number wirings:
from wires import w
def say_hi():
print('Hello from wires!')
def say_bye():
print('Bye, see you soon.')
w.callable.wire(say_hi)
w.callable.wire(say_bye)
len(w.callable) # returns 2
Using wirings
to get the current list of wired callables and wire-time arguments:
from wires import w
def a_print(*args, **kw):
print('args=%r kw=%r' % (args, kw))
w.callable.wire(a_print)
w.callable.wire(a_print, 42, 24)
w.callable.wire(a_print, a=42, b=24)
w.callable.wirings # returns [
# (<function a_print at ...>, (), {}),
# (<function a_print at ...>, (42, 24), {}),
# (<function a_print at ...>, (), {'a': 42, 'b': 24}),
# ]
[1] | Per the Concepts section, wires callables are Wires object auto-created attributes. |
API Reference¶
Wires Package¶
Python Wires
Wires Class¶
Python Wires Wires
Class.
Wires
objects hold callables as attributes that spring into existence
on first access; each such callable is a
WiresCallable
object.
>>> w = Wires()
>>> c = w.one_callable # Springs into existence.
>>> callable(c) # Is callable.
True
Callables can also be accessed via indexing:
>>> w['one_callable'] is w.one_callable
True
Wires
objects support dir()
, len()
and iteration:
>>> dir(w) # Get callable name list.
['one_callable']
>>> len(w) # How many callables?
1
>>> for c in w: # Iterating over all callables.
... print(c)
<WiresCallable 'one_callable' at 0x1018d4a90>
Deleting attributes deletes the associated callable:
>>> del w.one_callable # Delete and check it's gone.
>>> len(w)
0
-
class
wires._wires.
Wires
(min_wirings=None, max_wirings=None, returns=False, ignore_exceptions=True)¶ Wires
Class.-
__init__
(min_wirings=None, max_wirings=None, returns=False, ignore_exceptions=True)¶ Initialization arguments determine default settings for this object’s
WiresCallable
s.Optional, per-
WiresCallable
settings override these settings and, single use, call-time overriding is supported via calls to self: see__call__()
.Parameters: - min_wirings (
int
> 0 orNone
) – Minimum wiring count. - max_wirings (
int
> 0 orNone
) – Maximum wiring count. - returns (
bool
) – IfTrue
, calling callables returns results/raises exceptions. - ignore_exceptions (
bool
) – IfTrue
, all wired callables will be called, regardless of raised exceptions; ifFalse
, wired callable calling will stop after the first exception.
- min_wirings (
-
__repr__
()¶ Evaluating creates a new object with the same initialization arguments.
-
__getattr__
(name)¶ Attribute based access to
WiresCallable
s.
-
__getitem__
(name)¶ Index based access to
WiresCallable
s.
-
__delattr__
(name)¶ Deletes
WiresCallable
s or any other attributes.
-
__dir__
()¶ Default dir() implementation.
-
__len__
()¶ Existing
WiresCallable
count.
-
__iter__
()¶ Iterate over existing
WiresCallable
s.
-
__call__
(returns=None, ignore_exceptions=None)¶ Call-time settings override.
Parameters: - returns (
bool
) – IfTrue
, calling callables returns results/raises exceptions. - ignore_exceptions (
bool
) – IfTrue
, all wired callables will be called, regardless of raised exceptions; ifFalse
, wired callable calling will stop after the first exception.
Usage example:
>>> w = Wires(returns=False) >>> w.one_callable() # returns is False >>> w(returns=True).one_callable() # returns is True for this call only [] >>> w.one_callable() # returns is still False
- returns (
-
WiresCallable Class¶
Python Wires WiresCallable
Class.
WiresCallable
s are callable objects used exclusively as
Wires
object attributes.
>>> w = Wires()
>>> callable(w.one_callable)
True
Each WiresCallable
has zero or more wirings: functions/callables
wired to it. Their wire
method is used to add
wirings, including optional wire-time arguments:
>>> w.one_callable.wire(print, 'hi') # Wire `print`: one wire-time positional arg.
>>> w.one_callable.wire(print, 'bye') # 2nd `print` wiring: different wire-time arg.
Calling a WiresCallable
calls its wirings, in wiring order, passing
them a combination of the optional wire-time and optional call-time arguments:
>>> w.one_callable('world', end='!\n') # Call with call-time positional and named args.
hi world!
bye world!
WiresCallable
s have a wirings
attribute, representing all current wirings, and include support for len()
,
returning the wiring count:
>>> w.one_callable.wirings
[(<built-in function print>, ('hi',), {}), (<built-in function print>, ('bye',), {})]
>>> len(w.one_callable)
2
WiresCallable
objects behave differently depending on their
Wires
object’s settings and on their own
min_wirings
,
max_wirings
,
returns
and
ignore_exceptions
attributes.
-
class
wires._callable.
WiresCallable
(_wires, _name, _wires_settings)¶ WiresCallable
Class.-
__init__
(_wires, _name, _wires_settings)¶ IMPORTANT
Do not instantiate
WiresCallable
objects;Wires
objects do that, when needed. The class name and all its initialization arguments are considered private and may change in the future without prior notice.To ensure future compatibility, it should be used strictly in the context of
Wires
objects, along with its public attributes, properties and methods.
-
__repr__
()¶ Return repr(self).
-
min_wirings
¶ Minimum number of wirings or
None
, meaning no limit.Reading returns the per-
WiresCallable
value, if set, falling back to the containingWires
’s setting. Writing assigns a per-WiresCallable
value that, if non-None
, must be:- An
int
> 0. - Less than or equal to
max_wirings
. - Less than or equal to the wiring count, if there are wirings.
Raises: ValueError – When assigned invalid values. - An
-
max_wirings
¶ Maximum number of wirings or
None
, meaning no limit.Reading returns the per-
WiresCallable
value, if set, falling back to the containingWires
’s setting. Writing assigns a per-WiresCallable
value that, if non-None
, must be:- An
int
> 0. - Greater than or equal to
min_wirings
. - Greater than or equal to the wiring count, if there are wirings.
Raises: ValueError – When assigned non-conforming values. - An
-
returns
¶ bool
value defining call-time coupling behaviour: see__call__()
.Reading returns the per-
WiresCallable
value, if set, falling back to the containingWires
’s setting. Writing assigns a per-WiresCallable
value.
-
ignore_exceptions
¶ bool
value defining call-time coupling behaviour: see__call__()
.Reading returns the per-
WiresCallable
value, if set, falling back to the containingWires
’s setting. Writing assigns a per-WiresCallable
value.
-
set
(min_wirings=<object object>, max_wirings=<object object>, returns=<object object>, ignore_exceptions=<object object>, _next_call_only=False)¶ Sets one or more per-
WiresCallable
settings.Parameters: - min_wirings – See
min_wirings
. - max_wirings – See
max_wirings
. - returns – See
returns
. - ignore_exceptions – See
ignore_exceptions
. - _next_call_only – IMPORTANT: This argument is considered private and may be changed or removed in future releases.
Raises: May raise exceptions. Refer to the per-attribute documentation.
The uncommon defaults are used as a guard to identify non-set arguments, given than
None
is a valid value formin_wirings
andmax_wirings
.- min_wirings – See
-
__delattr__
(name)¶ Removes per-
WiresCallable
settings.Parameters: name ( str
) – An existing attribute name.Raises: May raise exceptions if the resulting settings would be invalid.
-
wire
(function, *args, **kwargs)¶ Adds a new wiring to
function
, withargs
andkwargs
as wire-time arguments.Raises: - TypeError – If
function
is notcallable()
. - RuntimeError – If
max_wirings
would be violated.
- TypeError – If
-
unwire
(function, *args, **kwargs)¶ Removes the first wiring to
function
.If
args
orkwargs
are passed, unwires the first wiredfunction
with those wire-time arguments; otherwise, unwires the first wiredfunction
, regardless of wire-time arguments.Raises: - TypeError – If
function
is notcallable()
. - ValueError – If no matching wiring is found.
- RuntimeError – If
min_wirings
would be violated.
- TypeError – If
-
wirings
¶ List of
(<function>, <args>, <kwargs>)
wiring tuples, in wiring order, where<args>
and<kwargs>
are the wire-time arguments passed towire()
.
-
__call__
(*args, **kwargs)¶ Calls wired callables, in wiring order.
Raises: ValueError – If the wiring count is lower than min_wirings
, when set to anint
> 0.Argument passing:
- Wired callables are passed a combination of the wire-time and call-time arguments; wire-time positional arguments will be passed first, followed by the call-time ones; wire-time named arguments may be overridden by call-time ones.
Call-time coupling depends on:
returns
: ifFalse
, calling returnsNone
; otherwise, returns or raises (see below).ignore_exceptions
: ifFalse
, calling wirings stops on the first wiring-raised exception; otherwise, all wirings will be called.
Returns: A list of (<exception>, <result>)
tuples, in wiring order, where:<exception>
isNone
and<result>
holds the returned value from that wiring, if no exception was raised; otherwise,<exception>
is the raised exception and<result>
isNone
.Raises: RuntimeError – Only if returns
isTrue
,ignore_exceptions
isFalse
, and a wiring raises an exception. The exception arguments will be a tuple of(<exception>, <result>)
tuples, much like the returned ones, where only the last one will have<exception>
as a non-None
value.
-
__len__
()¶ Wiring count.
-
Thanks¶
- Hynek Schlawack for the articles Sharing Your Labor of Love: PyPI Quick and Dirty and Testing & Packaging.
- Stuart Colville for the article Including parts of README.rst in your sphinx docs.
About¶
Python Wires was created by Tiago Montes.
License¶
The MIT License (MIT)
Copyright (c) 2018 Tiago Montes.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Support¶
Python Wires support is based on volunteer effort, delivered on an availability and best-effort basis.
Versions and Platforms¶
Following a strict Backwards Compatibility Policy, Python Wires strives to work on as many Python interpreters and underlying platforms as possible. For that, care is taken in crafting and maintaining a clean, documented code base, that respects and uses safe, non-deprecated Python programming APIs and conventions.
There is, however, a limited set of interpreters and platforms where development and automated testing takes place; these are the only ones where, respectively, human based diagnostics are completed, and where “correctness assertions” [1] can be made.
Important
For these motives, the only supported version is the latest PyPI released version, running on an interpreter and platform for which automated testing is in place:
- CPython 2.7 on 64 bit Linux, Windows or macOS systems.
- CPython 3.6 on 64 bit Linux, Windows or macOS systems.
- CPython 3.7 on 64 bit Linux, Windows or macOS systems.
Other interpreters and platforms may become supported in the future.
Python Wires versions follow a Calendar Versioning scheme with YY.MINOR.MICRO tags, where:
YY | Is the two digit year of the release. |
MINOR | Is the release number, starting at 1 every year. |
MICRO | Is the bugfix release, being 0 for non-bugfix only releases. |
Backwards Compatibility Policy¶
- A given release is API compatible with the release preceding it, possibly including new backwards-compatible features: codebases depending on a given Python Wires release should be able to use a later release, with no changes.
- Notable exceptions:
- Bug fixes may change an erroneous behaviour that a codebase oddly depends on.
- Deprecations, as described below.
Deprecation Policy¶
If a non-backwards compatible API change is planned:
A fully backwards compatible release will be made, where uses of API about to break compatibility will issue
WiresDeprecationWarning
s using the Python Standard Library’s warnings module as in:import warnings class WiresDeprecationWarning(DeprecationWarning): pass warnings.simplefilter('always', WiresDeprecationWarning) warnings.warn('message', WiresDeprecationWarning)
A non-backwards compatible release will be made, no earlier than six months after the release including the
WiresDeprecationWarning
s.
Requesting Support¶
Before moving forward, please review the documentation: it may include the answers you’re looking for. After that, if still in need of support, take this guide into consideration, and then open a new issue here, taking care to submit:
- A clear and concise summary.
- A detailed description, including snippets of code, if considered useful.
If something is not working as expected, please also include:
- A short code sample demonstrating the behaviour, along with the actual and expected results.
- The Python Wires version (eg. 18.1.0).
- The Python interpreter version (eg. CPython 3.5.2 64 bit).
- The operating system version (eg. Debian 9 “stretch” 64 bit).
Note
Addressing requests targeting unsupported versions, interpreters or platforms may require additional efforts and non-trivial amounts of time. Besides that, they will be equally welcome.
[1] | Whatever that means, without implying any guarantees other than the ones expressed in the License. |
Development¶
Python Wires is openly developed on GitHub, following a process that strives to be:
- As simple as possible, but not simpler.
- Easy to understand.
- Structured.
- Flexible.
Substantiated contributions to its improvement and related discussions will be welcome.
Environment¶
Setting up a development environment should be done under a Python virtual environment:
$ git clone https://github.com/tmontes/python-wires
$ cd python-wires/
$ pip install -e .[dev]
Running the test suite:
$ python -m unittest discover
Running the test suite with code coverage and branch reporting:
$ coverage run --branch -m unittest discover
$ coverage report
Building the documentation, which will be available under docs/build/html/
:
$ cd docs && make html
Running the test suite with tox:
$ pip install tox tox-venv
$ tox
Process¶
GitHub Issues, Labels, Milestones, and Pull Requests are used to track Python Wires development.
- Issues must be labelled and associated to a milestone.
- Pull Requests must reference, at least, one issue (and preferably only one).
- Pull Requests will not be merged if any of the GitHub checks fails.
- Pull Requests will not necessarily be merged if all of the GitHub checks pass.
Milestones¶
The following GitHub Milestones are tracked:
NEXT | Issues and Pull Requests that will be included in the next release. |
DEFER | Issues and Pull Requests that will be worked on, but will not be included in the next release. |
TBD | Issues and Pull Requests that will not be worked on until future decision. |
Note
Unassigned Issues and Pull Requests will be assigned to the TBD milestone.
At release time:
- The NEXT milestone is renamed to the release version and closed.
- A new NEXT milestone is created, with no associated Issues or Pull Requests.
Issues and Labels¶
All development issues will be labelled one of:
enhancement | Describing a new feature or capability. |
bug | Describing something that isn’t working as documented. |
release | Describing release process issues. |
maintenance | Describing other development related issues: refactors, automation, process, etc. |
Note
The key motivation for having mandatory labels in development issues is to simplify filtering support related ones which submitters will leave unlabelled.
General requirements:
- All issues must describe a single, actionable topic.
- Complex issues should be split into simpler, possibly related, issues.
- enhancement issues:
- Must describe the use-case, benefits and tradeoffs.
- Should include sample code demonstrating the enhancement in action.
- Should take the Checklist for Python library APIs into consideration.
- bug issues must:
- Be explicitly reported against either the latest PyPI released version or the current GitHub master branch.
- Describe the steps to reproduce the bug, ideally with a minimal code sample.
- Describe the expected and actual results.
- Include a reference to where the documentation is inconsistent with the actual results.
- maintenance issues:
- Must describe the purpose, benefits and trade-offs.
Warning
Open development issues not meeting these requirements will be either discarded and closed, or worked on, at the maintainer’s discretion.
Pull Requests¶
Pull Requests are tracked here and:
- Must reference an existing, open issue, and preferably only one.
- May totally or partially contribute to closing the referenced open issue.
- Will not be merged if any of the GitHub checks fails.
- Will not necessarily be merged if all of the GitHub checks pass.
- Must be assigned to the same milestone as the referenced open issue.
- May be labelled.
Release Procedure¶
Confirm that the NEXT milestone contains:
- No open issues.
- One or more closed issues, each associated with one or more merged Pull Requests.
Once confirmed, rename the NEXT milestone to YY.MINOR.MICRO and create a new issue in it, labelled release and named “Release”. Then:
Update
__version__
insrc/wires/__init__.py
toYY.MINOR.MICRO
.Confirm that the documentation builds successfully, making adjustments if needed.
Update the Change Log:
- Run
towncrier --draft
and confirm the output. - If needed, add missing
.deprecate
,.enhancement
,.bug
or.other
news-fragment files underdocs/newsfragments
. - Once the draft output looks correct, run
towncrier
.
- Run
Commit the version, documentation and changelog changes, tagging it YY.MINOR.MICRO.
Create Pull Request against the “Release” issue.
Once all the GitHub checks pass, merge the Pull Request.
Update the local repository with the GitHub merged changes.
Release in PyPI:
Install release dependencies with
pip install -e .[release]
.Build the release artifacts:
$ rm -r build/ dist/ $ python setup.py sdist bdist_wheel
Upload to test PyPI:
$ twine upload -r test dist/wires-*
Test the installation into a freshly created virtual environment:
$ pip install -i https://test.pypi.org/pypi wires
If ok, upload to PyPI:
$ twine upload -r pypi dist/wires-*
Confirm the installation into a freshly created virtual environment:
$ pip install wires
Lastly, cleanup again:
$ rm -r build/ dist/
Confirm the versioned documentation is available at Read the Docs.
Close the YY.MINOR.MICRO milestone.
Lastly, create a new NEXT milestone.