Mush Documentation¶
Mush is a light weight type-based dependency injection framework aimed at enabling the easy testing and re-use of chunks of code that make up scripts.
How Mush works¶
Note
This documentation explains how Mush works using fairly abstract examples. If you’d prefer more “real world” examples please see the Example Usage documentation.
Constructing runners¶
Mush works by assembling a number of callables into a Runner
:
from mush import Runner
def func1():
print('func1')
def func2():
print('func2')
runner = Runner(func1, func2)
Once assembled, a runner can be called any number of times. Each time it is called, it will call each of its callables in turn:
>>> runner()
func1
func2
More callables can be added to a runner:
def func3():
print('func3')
runner.add(func3)
If you want to add several callables in one go, you can use the
runner’s extend()
method:
def func4():
print('func4')
def func5():
print('func5')
runner.extend(func4, func5)
Now, when called, the runner will call all five functions:
>>> runner()
func1
func2
func3
func4
func5
Runners can also be added together to create a new runner:
runner1 = Runner(func1)
runner2 = Runner(func2)
runner3 = runner1 + runner2
This addition does not modify the existing runners, but does give the result you’d expect:
>>> runner1()
func1
>>> runner2()
func2
>>> runner3()
func1
func2
This can also be done by passing runners in when creating a new runner or calling the extend method on a runner, for example:
runner1 = Runner(func1)
runner2 = Runner(func2)
runner4_1 = Runner(runner1, runner2)
runner4_2 = Runner()
runner4_2.extend(runner1, runner2)
In both cases, the results are as you would expect:
>>> runner4_1()
func1
func2
>>> runner4_2()
func1
func2
Finally, runners can be cloned, providing a way to encapsulate commonly used base runners that can then be extended for each specific use case:
runner5 = runner3.clone()
runner5.add(func4)
The existing runner is not modified, while the new runner behaves as expected:
>>> runner3()
func1
func2
>>> runner5()
func1
func2
func4
Configuring Resources¶
Where Mush becomes useful is when the callables in a runner either produce or require objects of a certain type. Given the right configuration, Mush will wire these together enabling you to write easily testable and reusable callables that encapsulate specific pieces of functionality. This configuration is done either imperatively, declaratively or using a combination of the two styles as described in the sections below.
For the examples, we’ll assume we have three types of resources:
class Apple:
def __str__(self):
return 'an apple'
__repr__ = __str__
class Orange:
def __str__(self):
return 'an orange'
__repr__ = __str__
class Juice:
def __str__(self):
return 'a refreshing fruit beverage'
__repr__ = __str__
Imperative configuration¶
Imperative configuration requires no decorators and is great when working with callables that come from another package or the standard library:
def apple_tree():
print('I made an apple')
return Apple()
def magician(fruit):
print('I turned {0} into an orange'.format(fruit))
return Orange()
def juicer(fruit1, fruit2):
print('I made juice out of {0} and {1}'.format(fruit1, fruit2))
The requirements are passed to the add()
method of a runner which can express requirements for both arguments
and keyword parameters:
runner = Runner()
runner.add(apple_tree)
runner.add(magician, Apple)
runner.add(juicer, fruit1=Apple, fruit2=Orange)
Calling this runner will now manage the resources, collecting them and passing them in as configured:
>>> runner()
I made an apple
I turned an apple into an orange
I made juice out of an apple and an orange
Declarative configuration¶
This is done using the requires()
decorator to mark the
callables with their requirements, which can specify the types
required for either arguments or keyword parameters:
from mush import requires
def apple_tree():
print('I made an apple')
return Apple()
@requires(Apple)
def magician(fruit):
print('I turned {0} into an orange'.format(fruit))
return Orange()
@requires(fruit1=Apple, fruit2=Orange)
def juicer(fruit1, fruit2):
print('I made juice out of {0} and {1}'.format(fruit1, fruit2))
return Juice()
These can now be combined into a runner and executed. The runner will extract the requirements stored by the decorator and will use them to map the parameters as appropriate:
>>> runner = Runner(apple_tree, magician, juicer)
>>> runner()
I made an apple
I turned an apple into an orange
I made juice out of an apple and an orange
Hybrid configuration¶
The two styles of configuration are entirely interchangeable, with
declarative requirements being inspected whenever a callable is added
to a runner, and imperative requirements being taken whenever they are
passed via the add()
method:
@requires(Juice)
def packager(juice):
print('I put {0} in a bottle'.format(juice))
def orange_tree():
print('I made an orange')
return Orange()
trees = Runner(apple_tree, orange_tree)
runner = trees.clone()
runner.extend(juicer, packager)
This runner now ends up with bottled juice:
>>> runner()
I made an apple
I made an orange
I made juice out of an apple and an orange
I put a refreshing fruit beverage in a bottle
It’s useful to note that imperative configuration will be used in preference to declarative configuration where both are present:
runner = trees.clone()
runner.add(juicer, Orange, Apple)
This runner will give us juice made in a different order:
>>> runner()
I made an apple
I made an orange
I made juice out of an orange and an apple
Resource usage periods¶
It can be important for a callable to have either first access or last access to a particular resource. For this reason, configuration can specify one of three periods during which a callable needs to be called with a particular resource; the default is normal and a decorator can be used to indicate either first or last. Within these periods, callables are called in the order they are added to the runner.
As an example, consider a ring and some things that can be done to it:
class Ring:
def __str__(self):
return 'a ring'
def forge():
return Ring()
def polish(ring):
print('polishing {0}'.format(ring))
def more_polish(ring):
print('polishing {0} again'.format(ring))
def engrave(ring):
print('engraving {0}'.format(ring))
def package(ring):
print('packaging {0}'.format(ring))
These can now be added to a runner with configuration expressing the correct periods:
from mush import Runner, first, last
runner = Runner(forge)
runner.add(package, last(Ring))
runner.add(polish, first(Ring))
runner.add(more_polish, first(Ring))
runner.add(engrave, Ring)
Even though the callables were added out order, they will be executed correctly:
>>> runner()
polishing a ring
polishing a ring again
engraving a ring
packaging a ring
The configuration of periods works identically with both the imperative and declarative forms.
Waiting for a resource¶
Sometimes, a callable needs to wait for some other callable to do its work but does not need or cannot accept objects of the type returned by that callable. For example, and miss-using the fruit types from above:
def func1():
return Apple()
def func2(apple):
print('func2 got {0}'.format(apple))
return Orange()
def func3(apple):
print('func3 got {0}'.format(apple))
def func4(orange):
print('func4 processed {0}'.format(orange))
If we want func3()
only to get called once func4()
has
processed the Orange
but, for reasons of abstraction,
we want to add the callables in the order defined above, the simplest
runner will not give us what we want:
>>> runner = Runner(func1)
>>> runner.add(func2, Apple)
>>> runner.add(func3, Apple)
>>> runner.add(func4, Orange)
>>> runner()
func2 got an apple
func3 got an apple
func4 processed an orange
The problem is that the runner hasn’t been told that func3()
has a dependency on Orange
. This can be done using the
after()
type wrapper to specify that func2()
requires an
Orange
to exist, and for any other callables added to the
runner that need an Orange
to have been called first, but
that it must not be passed that orange:
from mush import Runner, after
runner = Runner(func1)
runner.add(func2, Apple)
runner.add(func3, after(Orange), Apple)
runner.add(func4, Orange)
Now, even though we’ve added the callables in the order we want, we get the order of calling that we need:
>>> runner()
func2 got an apple
func4 processed an orange
func3 got an apple
Special return types¶
There are certain types that can be returned from a callable that have special meaning. While these are provided in case of their specific need, you should think twice when you find yourself wanting to use them as it is often a sign that your code can be better structured differently.
For the examples below, we’ll use the fruit classes from above.
Returning multiple resources¶
A callable can return multiple resources by returning either a list or a tuple:
def orchard():
return Apple(), Orange()
This can be used to provide both types of fruit:
>>> runner = Runner(orchard, juicer)
>>> runner()
I made juice out of an apple and an orange
Overriding the type of a resource¶
Sometimes you may need to force a returned resource to be of a particular type. When this is the case, a callable can return a dictionary mapping the forced type to the required type:
class Pear:
def __str__(self):
return 'a pear'
def desperation():
print('oh well, a pear will have to do')
return {Apple: Pear()}
We can now make juice even though we don’t have apples:
>>> runner = Runner(orange_tree, desperation, juicer)
>>> runner()
I made an orange
oh well, a pear will have to do
I made juice out of a pear and an orange
If you have no control over a callable that returns an object of the ‘wrong’ type, you have two options. You can either decorate it:
from mush import returns
@returns(Apple)
def make_pears():
print('I made a pear')
return Pear()
Now you can use the pear maker to get juice:
>>> runner = Runner(orange_tree, make_pears, juicer)
>>> runner()
I made an orange
I made a pear
I made juice out of a pear and an orange
If you can’t even decorate the callable, then you can also imperatively indicate that the return type should be overridden:
from mush import returns
def someone_elses_pears():
print('I made a pear')
return Pear()
Now you can use the pear maker to get juice:
>>> runner = Runner(orange_tree, juicer)
>>> runner.add_returning(someone_elses_pears, returns=Apple)
>>> runner()
I made an orange
I made a pear
I made juice out of a pear and an orange
Returning marker types¶
In some circumstances, you may need to ensure that some callables are used only after another callable has done its work, even though that work does not return a resource. This can be achieved using a marker type as follows:
from mush import nothing, marker
@returns(marker('SetupComplete'))
def setup():
print('setting things up')
@requires(after(marker('SetupComplete')))
def body():
print('doing stuff')
Note that the body()
callable does not take any arguments or
parameters; because after()
is used, it is not passed and is used
purely to affect the order of calling:
>>> runner = Runner(body, setup)
>>> runner()
setting things up
doing stuff
Using parts of a resource¶
When pieces of functionality use settings provided either by command
line arguments or from configuration files, it’s often cleaner to
structure that code to recieve the specific setting value rather than
the setting’s container. It’s certainly easier to test. Mush can take
care of the needed wiring when configured to do so using the
attr
and item
helpers:
from mush import Runner, attr, item, requires
class Config(dict): pass
class Args(object):
fruit = 'apple'
tree = dict(fruit='pear')
def parse_args():
return Args()
def read_config():
return Config(fruit='orange')
@requires(attr(Args, 'fruit'),
item(Config, 'fruit'),
item(attr(Args, 'tree'), 'fruit'))
def pick(fruit1, fruit2, fruit3):
print('I picked {0}, {1} and {2}'.format(fruit1, fruit2, fruit3))
picked = []
for fruit in fruit1, fruit2:
if fruit=='apple':
picked.append(Apple())
elif fruit=='orange':
picked.append(Orange())
else:
raise TypeError('You have made a poor fruit choice')
return picked
While the pick()
function remains usable and testable on its
own:
>>> pick('apple', 'orange', 'pear')
I picked apple, orange and pear
[an apple, an orange]
It can also be added to a runner with the other necessary functions and Mush will do the hard work:
>>> runner = Runner(parse_args, read_config, pick, juicer)
>>> runner()
I picked apple, orange and pear
I made juice out of an apple and an orange
Context manager resources¶
A frequent requirement when writing scripts is to make sure that when unexpected things happen they are logged, transactions are aborted, and other necessary cleanup is done. Mush supports this pattern by allowing context managers to be added as callables:
from mush import Runner, requires
class Transactions(object):
def __enter__(self):
print('starting transaction')
def __exit__(self, type, obj, tb):
if type:
print(obj)
print('aborting transaction')
else:
print('committing transaction')
return True
def a_func():
print('doing my thing')
def good_func():
print('I have done my thing')
def bad_func():
raise Exception("I don't want to do my thing")
The context manager is wrapped around all callables that are called after it:
>>> runner = Runner(Transactions, a_func, good_func)
>>> runner()
starting transaction
doing my thing
I have done my thing
committing transaction
This gives it a chance to clear up when things go wrong:
>>> runner = Runner(Transactions, a_func, bad_func)
>>> runner()
starting transaction
doing my thing
I don't want to do my thing
aborting transaction
Debugging¶
Mush makes some heuristic decisions about the order in which to call objects added to a runner. If your expectations of the call order don’t match that used by Mush, it can be confusing to figure out where the difference comes from.
For this reason, when constructing a Runner
you can pass an
optional debug parameter which can be either a boolean True
or a
file-like object. If passed, debug information will be generated
whenever an object is added to the runner.
If True
, this information will be written to
stderr
. If a file-like object is passed instead, the
information will be written to that object.
As an example, consider this code:
from mush import Runner, requires
class T1(object): pass
class T2(object): pass
class T3(object): pass
def makes_t1():
return T1()
@requires(T1)
def makes_t2(obj):
return T2()
@requires(T2)
def makes_t3(obj):
return T3()
@requires(T3, T1)
def user(obj1, obj2):
m.user(type(obj1), type(obj2))
The debug information that would be written looks like this:
>>> import sys
>>> runner = Runner(makes_t1, makes_t2, makes_t3, user, debug=sys.stdout)
Added <function makes_t1 ...> to 'normal' period for <... 'NoneType'> with Requirements()
Current call order:
For <... 'NoneType'>:
normal: <function makes_t1 ...> requires Requirements()
Added <function makes_t2 ...> to 'normal' period for <class 'T1'> with Requirements(T1)
Current call order:
For <... 'NoneType'>:
normal: <function makes_t1 ...> requires Requirements()
For <class 'T1'>:
normal: <function makes_t2 ...> requires Requirements(T1)
Added <function makes_t3 ...> to 'normal' period for <class 'T2'> with Requirements(T2)
Current call order:
For <... 'NoneType'>:
normal: <function makes_t1 ...> requires Requirements()
For <class 'T1'>:
normal: <function makes_t2 ...> requires Requirements(T1)
For <class 'T2'>:
normal: <function makes_t3 ...> requires Requirements(T2)
Added <function user ...> to 'normal' period for <class 'T3'> with Requirements(T3, T1)
Current call order:
For <... 'NoneType'>:
normal: <function makes_t1 ...> requires Requirements()
For <class 'T1'>:
normal: <function makes_t2 ...> requires Requirements(T1)
For <class 'T2'>:
normal: <function makes_t3 ...> requires Requirements(T2)
For <class 'T3'>:
normal: <function user ...> requires Requirements(T3, T1)
Example Usage¶
To show how Mush works from a more practical point of view, let’s start by looking at a simple script that covers several common patterns:
from argparse import ArgumentParser
from .configparser import RawConfigParser
import logging, os, sqlite3, sys
log = logging.getLogger()
def main():
parser = ArgumentParser()
parser.add_argument('config', help='Path to .ini file')
parser.add_argument('--quiet', action='store_true',
help='Log less to the console')
parser.add_argument('--verbose', action='store_true',
help='Log more to the console')
parser.add_argument('path', help='Path to the file to process')
args = parser.parse_args()
config = RawConfigParser()
config.read(args.config)
handler = logging.FileHandler(config.get('main', 'log'))
handler.setLevel(logging.DEBUG)
log.addHandler(handler)
if not args.quiet:
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.DEBUG if args.verbose else logging.INFO)
log.addHandler(handler)
conn = sqlite3.connect(config.get('main', 'db'))
try:
filename = os.path.basename(args.path)
with open(args.path) as source:
conn.execute('insert into notes values (?, ?)',
(filename, source.read()))
conn.commit()
log.info('Successfully added %r', filename)
except:
log.exception('Something went wrong')
if __name__ == '__main__':
main()
As you can see, the script above takes some command line arguments,
loads some configuration from a file, sets up log handling and then
loads a file into a database. While simple and effective, this script
is hard to test. Even using the TempDirectory
and Replacer
helpers from the TestFixtures
package, the only way to do so is to write one or more high level
tests such as the following:
from testfixtures import TempDirectory, Replacer, OutputCapture
import sqlite3
class Tests(TestCase):
def test_main(self):
with TempDirectory() as d:
# setup db
db_path = d.getpath('sqlite.db')
conn = sqlite3.connect(db_path)
conn.execute('create table notes (filename varchar, text varchar)')
conn.commit()
# setup config
config = d.write('config.ini', '''
[main]
db = %s
log = %s
''' % (db_path, d.getpath('script.log')), 'ascii')
# setup file to read
source = d.write('test.txt', 'some text', 'ascii')
with Replacer() as r:
r.replace('sys.argv', ['script.py', config, source, '--quiet'])
main()
# check results
self.assertEqual(
conn.execute('select * from notes').fetchall(),
[('test.txt', 'some text')]
)
The problem is that, in order to test the different paths through the small piece of logic at the end of the script, we have to work around all the set up and handling done by the rest of the script.
This also makes it hard to re-use parts of the script. It’s common for a project to have several scripts, all of which get some config from the same file, have the same logging options and often use the same database connection.
Encapsulating the re-usable parts of scripts¶
So, let’s start by looking at how these common sections of code can be extracted into re-usable functions that can be assembled by Mush into scripts:
from argparse import ArgumentParser, Namespace
from .configparser import RawConfigParser
from mush import Runner, requires, first, last, attr, item
import logging, os, sqlite3, sys
log = logging.getLogger()
@requires(ArgumentParser)
def base_options(parser):
parser.add_argument('config', help='Path to .ini file')
parser.add_argument('--quiet', action='store_true',
help='Log less to the console')
parser.add_argument('--verbose', action='store_true',
help='Log more to the console')
@requires(last(ArgumentParser))
def parse_args(parser):
return parser.parse_args()
class Config(dict): pass
@requires(first(Namespace))
def parse_config(args):
config = RawConfigParser(dict_type=Config)
config.read(args.config)
return Config(config.items('main'))
def setup_logging(log_path, quiet=False, verbose=False):
handler = logging.FileHandler(log_path)
handler.setLevel(logging.DEBUG)
log.addHandler(handler)
if not quiet:
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.DEBUG if verbose else logging.INFO)
log.addHandler(handler)
class DatabaseHandler:
def __init__(self, db_path):
self.conn = sqlite3.connect(db_path)
def __enter__(self):
return self
def __exit__(self, type, obj, tb):
if type:
log.exception('Something went wrong')
self.conn.rollback()
We start with a function that adds the options needed by all our
scripts to an argparse
parser. This uses the requires()
decorator to tell Mush that it must be called with an
ArgumentParser
instance. See
Configuring Resources for more details.
Next, we have a function that calls
parse_args()
on the parser and returns
the resulting Namespace
. It uses the last()
decorator to indicate that it should be the last callable to use the
ArgumentParser
instance. See Resource usage periods
for more details.
The parse_config()
function uses the first()
decorator to
indicate that it needs first use of the command line arguments. It
uses those to read the specified configuration and return the
configuration as a dictionary. A sub-class of dict
is used so
that other callables can easily indicate that they require the
configuration to be passed in.
Finally, there is a function that configures log handling and a context manager that provides a database connection and handles exceptions that occur by logging them and aborting the transaction. Context managers like this are handled by Mush in a specific way, see Context manager resources for more details.
Each of these components can be separately and thoroughly tested. The
Mush decorations are inert to all but the Mush Runner
,
meaning that they can be used independently of Mush in whatever way is
convenient. As an example, the following tests use a
TempDirectory
and a
LogCapture
to fully test the database-handling
context manager:
def setUp(self):
self.dir = TempDirectory()
self.db_path = self.dir.getpath('test.db')
self.conn = sqlite3.connect(self.db_path)
self.conn.execute('create table notes '
'(filename varchar, text varchar)')
self.conn.commit()
self.log = LogCapture()
def tearDown(self):
self.log.uninstall()
self.dir.cleanup()
def test_normal(self):
with DatabaseHandler(self.db_path) as handler:
handler.conn.execute('insert into notes values (?, ?)',
('test.txt', 'a note'))
handler.conn.commit()
# check the row was inserted and committed
curs = self.conn.cursor()
curs.execute('select * from notes')
self.assertEqual(curs.fetchall(), [('test.txt', 'a note')])
# check there was no logging
self.log.check()
def test_exception(self):
with ShouldRaise(Exception('foo')):
with DatabaseHandler(self.db_path) as handler:
handler.conn.execute('insert into notes values (?, ?)',
('test.txt', 'a note'))
raise Exception('foo')
# check the row not inserted and the transaction was rolled back
curs = handler.conn.cursor()
curs.execute('select * from notes')
self.assertEqual(curs.fetchall(), [])
# check the error was logged
self.log.check(('root', 'ERROR', 'Something went wrong'))
Writing the specific parts of your script¶
Now that all the re-usable parts of the script have been abstracted, writing a specific script becomes a case of just writing two functions:
def args(parser):
parser.add_argument('path', help='Path to the file to process')
def do(conn, path):
filename = os.path.basename(path)
with open(path) as source:
conn.execute('insert into notes values (?, ?)',
(filename, source.read()))
conn.commit()
log.info('Successfully added %r', filename)
As you can imagine, this much smaller set of code is simpler to test and easier to maintain.
Assembling the components into a script¶
So, we now have a library of re-usable components and the specific callables we require for the current script. All we need to do now is assemble these parts into the final script. The full details of this are covered in Constructing runners but two common patterns are covered below.
Cloning¶
With this pattern, a “base runner” is created, usually in the same place that other re-usable parts of the original script are located:
from mush import Runner, requires, first, last, attr, item
base_runner = Runner(ArgumentParser, base_options, parse_args, parse_config)
base_runner.add(setup_logging,
log_path = item(first(Config), 'log'),
quiet = attr(first(Namespace), 'quiet'),
verbose = attr(first(Namespace), 'verbose'))
The code above shows some different ways of getting Mush to pass parts of an object returned from a previous callable to the parameters of another callable. See Using parts of a resource for full details.
Now, for each specific script, the base runner is cloned and the script-specific parts added to the clone leaving a callable that can be put in the usual block at the bottom of the script:
main = base_runner.clone()
main.add(args, ArgumentParser)
main.add(DatabaseHandler, item(Config, 'db'))
main.add(do,
attr(DatabaseHandler, 'conn'),
attr(Namespace, 'path'))
if __name__ == '__main__':
main()
Using a factory¶
This pattern is most useful when you have several or more scripts that all follow a similar pattern when it comes to assembling the runner from the common parts and the specific parts. For example, if all your scripts take a path to a config file and a path to a file that needs processing, you can write a factory function that returns a runner based on the callable that does the work as follows:
def options(parser):
parser.add_argument('config', help='Path to .ini file')
parser.add_argument('--quiet', action='store_true',
help='Log less to the console')
parser.add_argument('--verbose', action='store_true',
help='Log more to the console')
parser.add_argument('path', help='Path to the file to process')
def make_runner(do):
runner = Runner(ArgumentParser)
runner.add(options, ArgumentParser)
runner.add(parse_args, last(ArgumentParser))
runner.add(parse_config, first(Namespace))
runner.add(setup_logging,
log_path = item(first(Config), 'log'),
quiet = attr(first(Namespace), 'quiet'),
verbose = attr(first(Namespace), 'verbose'))
runner.add(DatabaseHandler, item(Config, 'db'))
runner.add(do,
attr(DatabaseHandler, 'conn'),
attr(Namespace, 'path'))
return runner
With this in place, the specific script becomes the do()
function we abstracted above and a very short call to the factory:
main = make_runner(do)
A combination of the clone and factory patterns can also be used to get the best of both worlds. Setting up several base runners and are clones of a parent runner and having factories that take common callable patterns and return complete runners can be very powerful.
Testing¶
The examples above have shown how using Mush can make it easier to have smaller components that are easier to re-use and test, however care should still be taken with testing. In particular, it’s a good idea to have some intergration tests that excercise the whole runner checking that it behaves as expected when all command line options are specified and when just the defaults are used.
When using the factory pattern, the factories themselves should be
unit tested. It can also make tests easier to write by having a
“testing runner” that sets up the required resources, such as database
connections, while maybe doing some things differently such as not
reading a configuration file from disk or using a
LogCapture
instead of file or stream log
handlers.
API Reference¶
-
class
mush.
Context
¶ Stores requirements, callables and resources for a particular run.
-
__iter__
()¶ When iterated over, the context will yield tuples containing the requirements for a callable and the callable itself in the form
(requirements, object)
.This can only be done once for a given context. A context that has been partially iterated over will remember where it had got to and pick up from there when iteration begins again.
-
add
(it, type=None)¶ Add a resource to the context.
Optionally specify the type to use for the object rather than the type of the object itself.
-
-
class
mush.
Marker
¶ Type for Marker classes
-
class
mush.
Periods
¶ A collection of lists used to store the callables that require a particular resource type.
-
__iter__
()¶ Yields callables in the order in which they require the resource this instance is used for.
-
first
= None¶ The callables that require first use of a particular resource.
-
last
= None¶ The callables that require last use of a particular resource.
-
-
class
mush.
Requirements
(*args, **kw)¶ Represents requirements for a particular callable
The passed in args and kw should map to the types, including any required
when
orhow
, for the matching arguments or keyword parameters the callable requires.-
__iter__
()¶ When iterated over, yields tuples representing individual types required by arguments or keyword parameters in the form
(keyword_name, decorated_type)
.If the keyword name is
None
, then the type is for a positional argument.
-
returns
¶ An override for the type that this callable will return.
alias of
not_specified
-
-
class
mush.
Runner
(*objs, **debug)¶ Used to run callables in the order in which they require particular resources and then, having taken that into account, in the order they are added to the runner.
Parameters: - objs – The callables to add to the runner as it is created.
- debug – If passed, debug information will be written whenever an object
is added to the runner. If
True
, it will be written tostderr
. A file-like object can also be passed, in which case the information will be written to that object.
-
__add__
(other)¶ Concatenate two runners, returning a new runner.
The order of the new runner is as if the callables had been added in order from runner on the left-hand side of expression and then in order from the runner on the right-hand side of the expression.
-
__call__
(context=None)¶ Execute the callables in this runner in the required order storing objects that are returned and providing them as arguments or keyword parameters when required.
A runner may be called multiple times. Each time a new
Context
will be created meaning that no required objects are kept between calls and all callables will be called each time.Parameters: context – Used for passing a context when context managers are used. You should never need to pass this parameter.
-
add
(obj, *args, **kw)¶ Add a callable to the runner.
If either
args
orkw
are specified, they will be used to create theRequirements
in this runner for the callable added in favour of any decoration done withrequires
.
-
add_returning
(obj, returns, *args, **kw)¶ Add a callable to the runner and specify that it should be treated as returning the type specified in
returns
, regardless of the actual type returned by callingobj
.If either
args
orkw
are specified, they will be used to create theRequirements
in this runner for the callable added in favour of any decoration done withrequires
.
-
clone
()¶ Return a copy of this runner.
-
extend
(*objs)¶ Add the specified callables to this runner.
If any of the objects passed is a
Runner
, the contents of that runner will be added to this runner.
-
replace
(original, replacement)¶ Replace all instances of one callable with another.
No changes in requirements or call ordering will be made.
-
mush.
after
(type)¶ A type wrapper that specifies the callable marked as requiring this type should not be passed an object of this type but should only be called once an object of that type is available, and should be done so in the
last
period.
-
class
mush.
attr
(type, *names)¶ A
how
that indicates the callable requires the named attribute from the decorated type.
-
class
mush.
first
(type=<class 'NoneType'>)¶ A
when
that indicates the callable requires first use of the decorated type.
-
class
mush.
how
(type, *names)¶ The base class for type decorators that indicate which part of a resource is required by a particular callable.
Parameters: - type – The type to be decorated.
- name – The part of the type required by the callable.
-
class
mush.
ignore
(type, *names)¶ A
how
that indicates the callable should not be passed an object of the decorated type.
-
class
mush.
item
(type, *names)¶ A
how
that indicates the callable requires the named item from the decorated type.
-
class
mush.
last
(type=<class 'NoneType'>)¶ A
when
that indicates the callable requires last use of the decorated type.
-
mush.
nothing
= Requirements()¶ A singleton
Requirements
indicating that a callable requires no resources.
-
class
mush.
requires
(*args, **kw)¶ A decorator used for marking a callable with the
Requirements
it needs.These are stored in an attribute called
__requires__
on the callable meaning that the callable can be used in its original form after decoration.If you need to specify requirements for a callable that cannot have attributes added to it, then use the
add()
method to do so.
-
class
mush.
returns
(type)¶ A decorator to indicate that a callable should be treated as returning the type passed to
returns()
rather than the type of the actual return value.
-
class
mush.
when
(type=<class 'NoneType'>)¶ The base class for type decorators that indicate when a callable requires a particular type.
Parameters: type – The type to be decorated.
For details of how to install the package or get involved in its development, please see the sections below:
Installation Instructions¶
The easyiest way to install Mush is:
easy_install mush
Or, if you’re using zc.buildout, just specify mush
as a
requirement.
Python version requirements
This package has been tested with Python 2.6, 2.7, 3.2 and 3.3 on Linux, Mac OS X and Windows.
Development¶
This package is developed using continuous integration which can be found here:
https://travis-ci.org/Simplistix/mush
The latest development version of the documentation can be found here:
http://mush.readthedocs.org/en/latest/
If you wish to contribute to this project, then you should fork the repository found here:
https://github.com/Simplistix/mush/
Once that has been done and you have a checkout, you can follow these instructions to perform various development tasks:
Setting up a virtualenv¶
The recommended way to set up a development environment is to turn your checkout into a virtualenv and then install the package in editable form as follows:
$ virtualenv .
$ bin/pip install -U -e .[test,build]
Building the documentation¶
The Sphinx documentation is built by doing the following from the directory containg setup.py:
$ cd docs
$ make html
Making a release¶
To make a release, just update the version in setup.py
,
update the change log, tag it
and push to https://github.com/Simplistix/mush
and Travis CI should take care of the rest.
Once Travis CI is done, make sure to go to https://readthedocs.org/projects/mush/versions/ and make sure the new release is marked as an Active Version.
Changes¶
1.3 (21 October 2015)¶
- Official support for Python 3.
- Drop official support for Windows, although things should still work.
- Move to Travis CI, Read The Docs and Coveralls for development.
- ‘How’ decorators like
attr()
anditem()
can now be nested as well as individually performing nested actions. - Add
returns()
andadd_returning()
as new ways to override the type of a returned value. - A better pattern for marker types.
1.2 (11 December 2013)¶
1.1 (27 November 2013)¶
- Allow runners to be instantiated using other runners.
- Allow
Runner.extend()
to be passedRunner
instances. - Allow
requires()
decorations to be stacked. - Add a
Runner.replace()
method to aid with testing assembled runners.
1.0 (29 October 2013)¶
- Initial Release
License¶
Copyright (c) 2013 Simplistix Ltd, 2015 Chris Withers
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.