Giter Site home page Giter Site logo

python-wires's Introduction

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

Thanks

About

Python Wires was created by Tiago Montes.

python-wires's People

Contributors

tmontes avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

python-wires's Issues

Support min/max callee count on callables.

Would it be useful somehow limiting the min/max number of wired callees?

Maybe it could help ensure some "conditions" are always met. Examples:

  • Wiring can raise an exception if the maximum wired callees is reached; (use case: ensure no more than one wiring is in place, preventing coding mistakes, for example)
  • Unwiring can raise an exception if the minimum wired callees is reached.
  • Calling a wired callable with less callees than the minimum callees can also raise an exception.

Food for thought.

If moving forward:

  • A simple and flexible API must be created that works with both Wiring instances and the wiring singleton.
  • Should it be set at the Wiring level? Or at the callable level? Both?

Deleting Wiring attributes needs fixing.

Current behaviour example #1:

w = Wiring()
w.a = 2
del w.a

Raises KeyError, should cleanly delete the attribute such that, on next w.a access a "normal" callable attribute is auto-created.

Current behaviour example #2:

w = Wiring()
del w.a

Raises KeyError when it should raise AttributeError.

Context manager to override call-time settings?

Idea from #35.

When completing several calls with the same call-time override settings instead of:

w(returns=<bool>, ignore_failures=<bool>).first_callable()
w(returns=<bool>, ignore_failures=<bool>).second_callable()

We go for a context manager as in:

with w(returns=<bool>, ignore_failures=<bool>) as w:
    w.first_callable()
    w.second_callable()

PS: Not sure if the proposed w rebinding in the with statement is achievable/desirable. Maybe.

Add .wirings and .wiring_count attributes to WiringCallables?

Following up on #3 which brought up this idea but was closed via #27 with no such implementation.

Thoughts:

  • How useful would these be?
  • Do we want to "keep polluting" the namespace under a WiringShell?
  • What should the .wirings return?
    • A reference to the WiringInstance._callees?
    • A copy?
    • A deep copy?

Split "coupling" into: return/raise or not vs. ignore callee failures or not.

Current state

Currently these concepts are intertwined:

  • Coupled calls:
    • Stop on first wired callee exception and raise exception with previous callee results.
  • Decoupled calls:
    • Call all wired callees, regardless of exceptions, returning list of (<exception>, <result>).

Idea

Separating these concepts is a sound idea:

coupling = False coupling = True
ignore_failures = True calls all wired callees, absorbing exceptions + returns None calls all wired callees, absorbing exceptions + returns (<exception>, <result>) list
ignore_failures = False stops calling wired callees at the 1st exception + returns None stops calling wired callees at the 1st exception + raises exception wrapping (<exception>, <result>) list

PS: This approach can also support setting ignore_failures to a non-negative integer value, defining the number of failures to ignore, before stopping.

API thoughts

Instance creation API is easily adaptable; it's a matter of using separate initialisation arguments:

Wiring(coupling=..., ignore_failures=...)

Call-time overriding is not so obvious. Currently call-time overriding is done via:

w.coupled.this()
w.decoupled.this()

How to go about and combine coupling and ignore_failures overriding? What about possible future settings/overrides?

Ideas

Context manager?

with w.override(coupling=..., ignore_failures=...) as w:
    w.this()

Or even shorter:

with w(coupling=..., ignore_failures=...) as w
    w.this()

Parameterized attribute?

w.override(coupling=..., ignore_failures=...).this()

Or, again, shorter:

w(coupling=..., ignore_failures=...).this()

WiringCallable should implement __delattr__

Such that "resetting" per-callable settings works:

>>> w = Wiring()
>>> w.callable.returns = True
>>> w.callable.wire(print)
>>> w.callable('hi')
'hi'
[(None, None)]
>>> del w.callable.returns     # resetting to the Wiring instance default (which is False)
>>> w.callable('hi')
'hi'
>>>

Support wire action chaining?

Instead of:

w = Wires()
w.one_callable.wire(do_this)
w.one_callable.wire(do_that)

Wire action chaining could be supported:

w = Wires()
w.one_callable.wire(do_this).wire(do_that)

Notes:

  • Trivial to implement.
  • Not sure if readability improves or not.
  • If implemented, unwire actions should also be chainable.

Refactor tests

Current tests are:

  • Duplicated at the instance- vs. singleton-level.
  • All bundled into common test modules.

Let's improve that such that future testing is made easier.

Add automated testing on macOS?

Travis may be suitable.

Benefit:

  • Automatically assert code runs on the macOS platform.

Trade-off:

  • From past experience Travis tends to be on the slowish side, with macOS.
  • Could this lead to minor frustration?

Support per wiring coupling?

Current status:

  • Wiring instances set a default caller/callee coupling via its coupling initialisation argument.
  • Calls default to the instance's default coupling.
  • Caller/callee coupling can be overridden at call time via Wiring.couple / Wiring.decouple.

Possibility:

  • Would it be valuable to allow wire-time caller/callee coupling definition?
  • If so:
    • The mixed coupled/decoupled caller/callee scenario behaviour needs to be defined.
    • An API to set wire-time caller/callee coupling, overriding the default Wiring instance coupling, needs to be created.

Add automated testing on Windows?

AppVeyor may be suitable.

Benefit:

  • Automatically assert code runs on the Windows platform.

Trade-off:

  • From past experience AppVeyor tends to be on the slowish side.
  • Could this lead to minor frustration?

Support caller/callee coupling/decoupling

Example:

def foo():
    raise Exception('foo failed')

def bar():
    return 42

w = Wires()
w.wire.this.calls_to(foo)
w.wire.this.calls_to(bar)

w.this()

It should be possible to define the coupling behaviour of the w.this call:

Fully decoupled

  • Call goes through.
  • foo called first, exception absorbed (currently logged or output to sys.stderr)
  • bar called second.
  • No return value.

Half coupled

  • Call goes through.
  • foo called first, exception absorbed.
  • bar called second.
  • Returns a (<exception>, <return-value>) tuple list with the exception or return value of each callee; the example above would have w.this() return [(Exception('foo failed'), None), (None, 42)].

Fully coupled

  • Calling the wired callees stops on the first callee raised exception.
  • Raises an exception wrapping the caught exception.
  • Subsequent wired callees are not called, like bar in the example.

More:

  • Maybe the Fully decoupled and Half coupled cases above can be simplified to the Half coupled case; removing the logging/output handling responsibility which simplifies the code and lets users decide how to handle callee failures.
  • Maybe the Fully coupled case could be configured to stop on the N-th callee raised exception; it would then raise an exception wrapping a (<exception>, <return-value>) tuple list with all the intermediate callee raised exceptions or results.

This sounds like a valuable capability.

Next:

  • Define an API for such configuration.
  • Should support:
    • Wires instance-level configuration.
    • Call-time overriding.
  • Should work both with the wires singleton and arbitrary instances..

Create documentation

Tasks:

  • Improve README.
  • Concepts documentation.
  • Howto-like documentation.
  • Reference documentation.
  • Support documentation.
  • Development documentation.
  • Create a change log.
  • Publish on https://readthedocs.org.

Set multiple per-callable settings in a single call.

Idea from #43, instead of having to do:

w.this_callable.min_wirings = <n>
w.this_callable.max_wirings = <m>
w.this_callable.returns = <bool>
w.this_callable.ignore_failures = <bool>

Could we have something like:

w.this_callable.set(min_wirings=<n>, max_wirings=<m>, returns=<bool>, ignore_failures=<bool>)

Maybe.

Rename Wiring to Wires all around.

This feels a bit like a last-minute change, however, while working in the documentation, the noun/verb duality of the "wiring" word constantly showed up as a barrier to clear concept phrasing.

"Wires" is also both a noun and a verb. It is, nonetheless:

  • Easier to use in the context of the documentation.
  • More consistent with the library name.
  • Shorter and still to the point.

Tradeoff:

  • "Wiring" is a better name in the sense that it defines a "particular way wires are organised", a concept that "Wires" by itself does not convey.

Rename ignore_failures to ignore_exceptions

Fact:

  • The name will have to stick, given the Backwards Compatibility Policy.

Benefit:

  • ignore_exceptions is more descriptive (exceptions are ignored! failures may be subjective).

Trade-off:

  • Typing 2 extra letters (shorter while still descriptive would be better).
  • Doesn't sound as good (at least to me, but that's highly subjective).

Spell check docs, comments and docstrings.

Found the need for s/deatils/details/ in the module source header comments, at least. There may be more typos and spelling mistakes.

PS: Tagging as [bug], of course. Wrong/mistakes in docs are bugs.

Review / revamp the API

Fact:

  • Current API has grown somewhat organically.
  • It may be inconsistent, unintuitive, too verbose, etc.

Objective:

  • Consistency.
  • Terse enough as long as readability and clarity is not compromised.
  • Open to future additions.

EDIT after ponderation and feedback - tasks:

  • Change the wiring / unwiring API.
  • Change the call-time coupling override API.
  • Change the min/max_wiring API.

Support buffered/delayed calls?

Example:

w = Wiring()
w.this()

def my_callable():
    print('called!')

w.wire.this.calls_to(my_callable)

In the example above, the unwired call w.this() has no effect. Would it be useful to support "buffering" of calls such that the previous example would "remember" the unwired call in w.this() and later call my_callable at wiring time?

Thoughts:

  • If possible, Wiring instances should default to "not doing this", behaving like "event systems" / "pub-sub systems" that just throw away "unhandled "events" / "unsubscribed messages".
  • Probably related to #3 (optional min/max callee setting) in the sense that delayed callee calling should be triggered, if activated, only when the min callee count is reached (or on the first wiring, if min is unset).
  • If possible, activating this behaviour should be configurable in "how many" buffer/delayed calls to keep track of.
  • Configurable at the Wiring instance level and at the individual callable level?

API possibility:

  • Have a track_unwired_calls Wires initialisation argument:
    • Defaults to 0.
    • If set to a positive integer n: will track that amount of unwired calls.
    • If set to True: will track an unlimited number of unwired calls.
    • If set to False: raise an exception if an unwired callable is called (conflict with idea in #3, needs thinking).

Preliminary support for Python 3.8

  • Python 3.8b4 is out (and 3.8.0, as of this update!).
  • Tests pass on local macOS dev system.
  • Let's:
    • Add that to Travis CI, testing on Linux. (failed horribly in #118)
    • Add 3.8.0 to Azure Pipelines, testing on Windows and macOS.
    • Include Linux testing on Azure Pipelines.

Support wire-time min/max_wirings override?

Idea from #43:

For full instantiation-time, per-callable and wire/call-time settings consistency, this could be supported:

w = Wiring(max_wirings=1)

w.some_callable.wire(...)
w(max_wirings=None).some_callable.wire(...)         # would otherwise fail

Other than consistency, I fail to find how this will be useful. Would it?

WiringCallable.unwire should support args and kwargs.

Purpose:

  • Disambiguate unwiring different wirings to the same callable with different call-time args/kwargs.

Signature:

def unwire(self, function, *args, **kwargs):

Behaviour:

  • If no args or kwargs are passed, unwires the first wiring to function.
  • Otherwise, unwires the first wiring to function with the supplied wire-time args and kwargs.
  • Will raise ValueError if no matching wiring is found.

Support per callable call-time coupling defaults?

Status

With #35, Wiring instances provide two interfaces to define the call-time coupling behaviour.

Instance initialisation time.

w = Wiring(returns=<bool>, ignore_failures=<bool>)

Where all calls made via w will obey the behaviour defined by returns and ignore_failures.

Instance level call-time override

w = Wiring(...)
w.some_callable.wire(<callable>)

w(returns=<bool>, ignore_failures=<bool>).some_callable()

Where the original w instantiation call-time coupling parameters are overridden at call-time.

Idea

Would it be valuable to provide a per callable call-time behaviour where, for example, a given callable would behave one way by default, while another one would behave differently, again by default, without the need to use the existing call-time overriding API?

How complex would it be to implement? What would a possible API look like?

For consistency, given that it's currently possible to set per-callable min/max_wirings with w.some_callable.min/max_wirings = <n>, it could probably look like:

w = Wiring(returns=<bool>, ignore_failures=<bool>)

w.callable_one.coupling(returns=<bool>, ignore_failures=<bool>)
w.callable_two.coupling(returns=<bool>, ignore_failures=<bool>)

This sounds reasonable but creates a .coupling attribute on callables, side-by-side with the .min/max_wirings: yet another thing to remember, somewhat inconsistent with Wiring instance initialisation.

POST EDIT: See updates to this below.

Alternatively, we could go for a more consistent approach, combining any/all instantiation level, per-callable level, and call-time parameter overrides via something like:

Instantiation

w = Wiring(min_wirings=<int>, max_wirings=<n>, returns=<bool>, ignore_failures=<bool>)

Just as it is now: simple and extendable.

Per callable defaults

Implement a generic, per-callable .set() method:

w.some_callable.set(min_wirings=<int>, max_wirings=<n>, returns=<bool>, ignore_failures=<bool>)

Maybe we could even have a w.some_callable.get(...) method?

Call-time override

Calling wiring instances, like in #35, allows for call-time overriding. This approach leads to a usage feeling like "re-initializing" the instance which, while not common (that I know of), quickly becomes intuitive.

Extending it to support all the same arguments as the initialiser would result in the following code for wire-time overriding:

w(min_wirings=<int>, max_wirings=<int>, ...).some_callable.wire/unwire(<callable>

And for call-time overriding, again, as is already implemented in #35.

w(..., returns=<bool>, ignore_failures=<bool>).some_callable()

Wrap-up

The idea of having a consistent and extendable API for instantiation-time parameters, per-callable defaults parameters and call-time parameter overriding sounds good.

Let's sleep on this for a while and get some feedback.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.