Python Wires: Simple Callable Wiring

PyPI CI Status Test Coverage Documentation

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

class wires.Wires(min_wirings=None, max_wirings=None, returns=False, ignore_exceptions=True)

Wires Class.

wires.w = Shared Wires instance.

Wires Class.

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

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 or None) – Minimum wiring count.
  • max_wirings (int > 0 or None) – Maximum wiring count.
  • returns (bool) – If True, calling callables returns results/raises exceptions.
  • ignore_exceptions (bool) – If True, all wired callables will be called, regardless of raised exceptions; if False, wired callable calling will stop after the first exception.
__repr__()

Evaluating creates a new object with the same initialization arguments.

__getattr__(name)

Attribute based access to WiresCallables.

__getitem__(name)

Index based access to WiresCallables.

__delattr__(name)

Deletes WiresCallables or any other attributes.

__dir__()

Default dir() implementation.

__len__()

Existing WiresCallable count.

__iter__()

Iterate over existing WiresCallables.

__call__(returns=None, ignore_exceptions=None)

Call-time settings override.

Parameters:
  • returns (bool) – If True, calling callables returns results/raises exceptions.
  • ignore_exceptions (bool) – If True, all wired callables will be called, regardless of raised exceptions; if False, 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

WiresCallable Class

Python Wires WiresCallable Class.

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

WiresCallables 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 containing Wires’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.
max_wirings

Maximum number of wirings or None, meaning no limit.

Reading returns the per-WiresCallable value, if set, falling back to the containing Wires’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.
returns

bool value defining call-time coupling behaviour: see __call__().

Reading returns the per-WiresCallable value, if set, falling back to the containing Wires’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 containing Wires’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_onlyIMPORTANT: 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 for min_wirings and max_wirings.

__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, with args and kwargs as wire-time arguments.

Raises:
  • TypeError – If function is not callable().
  • RuntimeError – If max_wirings would be violated.
unwire(function, *args, **kwargs)

Removes the first wiring to function.

If args or kwargs are passed, unwires the first wired function with those wire-time arguments; otherwise, unwires the first wired function, regardless of wire-time arguments.

Raises:
  • TypeError – If function is not callable().
  • ValueError – If no matching wiring is found.
  • RuntimeError – If min_wirings would be violated.
wirings

List of (<function>, <args>, <kwargs>) wiring tuples, in wiring order, where <args> and <kwargs> are the wire-time arguments passed to wire().

__call__(*args, **kwargs)

Calls wired callables, in wiring order.

Raises:ValueError – If the wiring count is lower than min_wirings, when set to an int > 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: if False, calling returns None; otherwise, returns or raises (see below).
  • ignore_exceptions: if False, 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> is None and <result> holds the returned value from that wiring, if no exception was raised; otherwise, <exception> is the raised exception and <result> is None.
Raises:RuntimeError – Only if returns is True, ignore_exceptions is False, 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

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.

Change Log

Wires 19.2.0

Bug Fixes

  • Fixed release process documentation and its packaging dependencies. (#112)

Other Changes

  • 64 bit Windows now supported. Automated tests in place. (#94)
  • macOS now supported. Automated tests in place. (#95)

Wires 19.1.0

Bug Fixes

  • Spelling review in comments, docstrings and documentation. (#103)

Other Changes

  • Links to PyPI updated to reflect the new pypi.org domain. (#102)
  • Python 3.7 support. (#105)

Wires 18.1.0

Enhancements

  • Initial public release. (#100)

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

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__ in src/wires/__init__.py to YY.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 under docs/newsfragments.
    • Once the draft output looks correct, run towncrier.
  • 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:

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

Indices and tables