diff --git a/.circleci/config.yml b/.circleci/config.yml index 580b300..b21fbbc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,23 +1,23 @@ version: 2.1 orbs: - python: cjw296/python-ci@1.2 + python: cjw296/python-ci@2.1 common: &common jobs: - python/pip-run-tests: - name: python27 - image: circleci/python:2.7 - - python/pip-run-tests: - name: python37 - image: circleci/python:3.7 + matrix: + parameters: + image: + - circleci/python:3.6 + - circleci/python:3.9 - python/coverage: name: coverage + image: circleci/python:3.9 requires: - - python27 - - python37 + - python/pip-run-tests - python/release: name: release diff --git a/.coveragerc b/.coveragerc index 542e548..23d833b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,5 @@ [run] -source = mush +source = mush,tests [report] exclude_lines = @@ -9,4 +9,8 @@ exclude_lines = # stuff that we don't worry about pass + \.\.\. __name__ == '__main__' + + # circular references needed for type checking: + if TYPE_CHECKING: diff --git a/docs/Makefile b/docs/Makefile index e8d21ce..d5d8d40 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -9,7 +9,7 @@ PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -W --keep-going -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest diff --git a/docs/api.txt b/docs/api.txt index acbfa2b..8a5aff6 100644 --- a/docs/api.txt +++ b/docs/api.txt @@ -12,7 +12,7 @@ API Reference :members: Modifier .. automodule:: mush.declarations - :members: how, nothing, result_type, update_wrapper + :members: how, nothing, result_type, update_wrapper, DeclarationsFrom .. automodule:: mush.plug :members: insert, ignore, append, Plug diff --git a/docs/conf.py b/docs/conf.py index 91eb7a8..3e4b286 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,3 +37,4 @@ 'Simplistix Ltd', 'manual'), ] +default_role = 'any' diff --git a/docs/conftest.py b/docs/conftest.py index 0cc9d3a..187b090 100644 --- a/docs/conftest.py +++ b/docs/conftest.py @@ -5,8 +5,6 @@ from sybil.parsers.codeblock import CodeBlockParser from sybil.parsers.doctest import DocTestParser -from mush.compat import PY2 - sybil_collector = Sybil( parsers=[ DocTestParser(optionflags=REPORT_NDIFF|ELLIPSIS), @@ -17,6 +15,5 @@ ).pytest() -def pytest_collect_file(parent, path): - if not PY2: - return sybil_collector(parent, path) +# def pytest_collect_file(parent, path): +# return sybil_collector(parent, path) diff --git a/docs/use.txt b/docs/use.txt index 9aed676..008a1a3 100755 --- a/docs/use.txt +++ b/docs/use.txt @@ -198,8 +198,6 @@ I made an apple I turned an apple into an orange I made juice out of an apple and an orange -.. _optional-resources: - Optional requirements ~~~~~~~~~~~~~~~~~~~~~ @@ -209,17 +207,17 @@ take this into account. Take the following function: .. code-block:: python - def greet(name='stranger'): + def greet(name: str = 'stranger'): print('Hello ' + name + '!') -If a name is not always be available, it can be added to a runner as follows: +If a name is not always be available, the callable's default will be used: .. code-block:: python - from mush import Runner, optional + from mush import Runner runner = Runner() - runner.add(greet, requires=optional(str)) + runner.add(greet) Now, when this runner is called, the default will be used: @@ -231,13 +229,14 @@ available: .. code-block:: python - from mush import Runner, optional + from mush import Runner, returns + @returns('name') def my_name_is(): return 'Slim Shady' runner = Runner(my_name_is) - runner.add(greet, requires=optional(str)) + runner.add(greet) In this case, the string returned will be used: @@ -276,12 +275,12 @@ of the returns resources through to the :func:`pick` function: .. code-block:: python - from mush import Runner, attr, item, requires + from mush import Runner, requires, Value runner = Runner(some_attributes, some_items) - runner.add(pick, requires(fruit1=attr(Stuff, 'fruit'), - fruit2=item(dict, 'fruit'), - fruit3=item(attr(Stuff, 'tree'), 'fruit'))) + runner.add(pick, requires(fruit1=Value(Stuff).fruit, + fruit2=Value(dict)['fruit'], + fruit3=Value(Stuff).tree['fruit'])) So now we can pick fruit from some interesting places: @@ -376,17 +375,18 @@ I sold vegetables as fruit I made juice out of a tomato and a cucumber Finally, if you have a callable that returns results that you wish to ignore, -you can do so using :attr:`~mush.declarations.nothing`: +you can do so as follows: .. code-block:: python - from mush import Runner, nothing + from mush import Runner + @returns() def spam(): return 'spam' runner = Runner() - runner.add(spam, returns=nothing) + runner.add(spam) .. _named-resources: @@ -538,9 +538,6 @@ I turned an apple into an orange I made juice out of an apple and an orange a refreshing fruit beverage -If an argument has a default, then the requirement will be made -:ref:`optional `. - Configuration Precedence ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -653,10 +650,10 @@ the representation of a runner gives all this information: >>> runner requires() returns_result_type() - requires(Ring) returns_result_type() <-- forged - requires(Ring) returns_result_type() - requires(Ring) returns_result_type() <-- engraved - requires(Ring) returns_result_type() + requires(Value(Ring)) returns_result_type() <-- forged + requires(Value(Ring)) returns_result_type() + requires(Value(Ring)) returns_result_type() <-- engraved + requires(Value(Ring)) returns_result_type() As you can see above, when a callable is inserted at a label, the label @@ -679,7 +676,7 @@ Now, when you add to a specific label, only that label is moved: >>> runner requires() returns_result_type() <-- before_polish - requires('ring') returns_result_type() <-- after_polish + requires(Value('ring')) returns_result_type() <-- after_polish Of course, you can still add to the end of the runner: @@ -689,8 +686,8 @@ Of course, you can still add to the end of the runner: >>> runner requires() returns_result_type() <-- before_polish - requires('ring') returns_result_type() <-- after_polish - requires('ring') returns_result_type() + requires(Value('ring')) returns_result_type() <-- after_polish + requires(Value('ring')) returns_result_type() However, the point modifier returned by getting a label from a runner will @@ -700,9 +697,9 @@ keep on moving the label as more callables are added using it: >>> runner requires() returns_result_type() <-- before_polish - requires('ring') returns_result_type() - requires('ring') returns_result_type() <-- after_polish - requires('ring') returns_result_type() + requires(Value('ring')) returns_result_type() + requires(Value('ring')) returns_result_type() <-- after_polish + requires(Value('ring')) returns_result_type() .. _plugs: @@ -924,11 +921,13 @@ a remote web service: .. code-block:: python + from mush import Runner, Value + def load_config() -> 'config': return json.loads(urllib2.urlopen('...').read()) - def do_stuff(username: item('config', 'username'), - password: item('config', 'password')): + def do_stuff(username: Value('config')['username'], + password: Value('config')['password']): print('doing stuff as ' + username + ' with '+ password) runner = Runner(load_config, do_stuff) @@ -949,6 +948,7 @@ If you have a base runner such as this: .. code-block:: python from argparse import ArgumentParser, Namespace + from mush import Runner, Value def base_args(parser): parser.add_argument('config_url') @@ -956,7 +956,7 @@ If you have a base runner such as this: def parse_args(parser): return parser.parse_args() - def load_config(): + def load_config(config_url): return json.loads(urllib2.urlopen('...').read()) def finalise_things(): @@ -965,7 +965,7 @@ If you have a base runner such as this: base_runner = Runner(ArgumentParser) base_runner.add(base_args, requires=ArgumentParser, label='args') base_runner.add(parse_args, requires=ArgumentParser) - point = base_runner.add(load_config, requires=attr(Namespace, 'config_url'), + point = base_runner.add(load_config, requires=Value(Namespace).config_url, returns='config') point.add_label('body') base_runner.add(finalise_things, label='ending') @@ -977,8 +977,8 @@ That runner might be used for a specific script as follows: def job_args(parser: ArgumentParser): parser.add_argument('--colour') - def do_stuff(username: item('config', 'username'), - colour: attr(Namespace, 'colour')): + def do_stuff(username: Value('config')['username'], + colour: Value(Namespace).colour): print(username + ' is '+ colour) runner = base_runner.clone() @@ -1019,12 +1019,12 @@ For example, consider this runner: .. code-block:: python - from mush import Runner + from mush import Runner, Value def make_config() -> 'config': return {'foo': 'bar'} - def connect(foo: item('config', 'foo')): + def connect(foo = Value('config')['foo']): return 'connection' def process(connection): @@ -1040,8 +1040,8 @@ To see how the configuration panned out, we would look at the :func:`repr`: >>> runner requires() returns('config') - requires(foo='config'['foo']) returns_result_type() <-- config - requires('connection') returns_result_type() + requires(Value('config')['foo']) returns_result_type() <-- config + requires(Value('connection')) returns_result_type() As you can see, there is a problem with this configuration that will be exposed diff --git a/mush/__init__.py b/mush/__init__.py old mode 100755 new mode 100644 index 60fc10e..0cebf2b --- a/mush/__init__.py +++ b/mush/__init__.py @@ -1,14 +1 @@ -from .runner import Runner -from .declarations import ( - requires, - returns_result_type, returns_mapping, returns_sequence, returns, - optional, attr, item, nothing -) -from .plug import Plug - -__all__ = [ - 'Runner', - 'requires', 'optional', - 'returns_result_type', 'returns_mapping', 'returns_sequence', 'returns', - 'attr', 'item', 'Plug', 'nothing' -] +from .context import Context diff --git a/mush/callpoints.py b/mush/callpoints.py deleted file mode 100644 index f1275aa..0000000 --- a/mush/callpoints.py +++ /dev/null @@ -1,33 +0,0 @@ -from .declarations import result_type, nothing, extract_declarations -from .factory import Factory - - -class CallPoint(object): - - next = None - previous = None - requires = nothing - returns = result_type - - def __init__(self, obj, requires=None, returns=None, lazy=None): - requires, returns = extract_declarations(obj, requires, returns) - lazy = lazy or getattr(obj, '__mush_lazy__', False) - requires = requires or nothing - returns = returns or result_type - if lazy: - obj = Factory(obj, requires, returns) - requires = returns = nothing - self.obj = obj - self.requires = requires - self.returns = returns - self.labels = set() - self.added_using = set() - - def __call__(self, context): - return context.extract(self.obj, self.requires, self.returns) - - def __repr__(self): - txt = '%r %r %r' % (self.obj, self.requires, self.returns) - if self.labels: - txt += (' <-- ' + ', '.join(sorted(self.labels))) - return txt diff --git a/mush/compat.py b/mush/compat.py deleted file mode 100644 index 14aa1b5..0000000 --- a/mush/compat.py +++ /dev/null @@ -1,70 +0,0 @@ -# compatibility module for different python versions -import sys -from collections import OrderedDict -from .markers import Marker - - -if sys.version_info[:2] < (3, 0): - PY2 = True - from functools import partial - from inspect import getargspec, ismethod, isclass, isfunction - - class Parameter(object): - POSITIONAL_ONLY = Marker('POSITIONAL_ONLY') - POSITIONAL_OR_KEYWORD = kind = Marker('POSITIONAL_OR_KEYWORD') - KEYWORD_ONLY = Marker('KEYWORD_ONLY') - empty = default = Marker('empty') - - class Signature(object): - __slots__ = 'parameters' - - def signature(obj): - sig = Signature() - sig.parameters = params = OrderedDict() - - bound_args = 0 - extra_kw = {} - if isclass(obj): - obj = obj.__init__ - elif isinstance(obj, partial): - bound_args = len(obj.args) - extra_kw = obj.keywords - obj = obj.func - if not (isfunction(obj) or ismethod(obj)): - obj = obj.__call__ - if not (isfunction(obj) or ismethod(obj)): - return sig - spec = getargspec(obj) - spec_args = spec.args - if callable(obj) and not isfunction(obj): - bound_args += 1 - if bound_args: - spec_args = spec.args[bound_args:] - - defaults_count = 0 if spec.defaults is None else len(spec.defaults) - default_start = len(spec_args) - defaults_count - for i, arg in enumerate(spec_args): - params[arg] = p = Parameter() - p.name = arg - if i >= default_start: - p.default = True - - for name in extra_kw: - p = params[name] - p.default = True - p.kind = p.KEYWORD_ONLY - - seen_keyword_only = False - for p in params.values(): - if p.kind is p.KEYWORD_ONLY: - seen_keyword_only = True - elif seen_keyword_only: - p.kind = p.KEYWORD_ONLY - - return sig - -else: - PY2 = False - from inspect import signature - -NoneType = type(None) diff --git a/mush/context.py b/mush/context.py index 4070cc4..aa06ef4 100644 --- a/mush/context.py +++ b/mush/context.py @@ -1,130 +1,22 @@ -from collections import deque +from typing import Callable -from .declarations import how, nothing -from .factory import Factory -from .markers import missing +from .paradigms import Call, Paradigm, Paradigms, paradigms +from .typing import Calls -NONE_TYPE = None.__class__ +class Context: -class ContextError(Exception): - """ - Errors likely caused by incorrect building of a runner. - """ - def __init__(self, text, point=None, context=None): - self.text = text - self.point = point - self.context = context + paradigms: Paradigms = paradigms - def __str__(self): - rows = [] - if self.point: - point = self.point.previous - while point: - rows.append(repr(point)) - point = point.previous - if rows: - rows.append('Already called:') - rows.append('') - rows.append('') - rows.reverse() - rows.append('') + def __init__(self, paradigm: Paradigm = None): + self.paradigm = paradigm - rows.append('While calling: '+repr(self.point)) - if self.context is not None: - rows.append('with '+repr(self.context)+':') - rows.append('') + def _resolve(self, obj: Callable, target_paradigm: Paradigm) -> Calls: + caller = self.paradigms.find_caller(obj, target_paradigm) + yield Call(caller, (), {}) - rows.append(self.text) + def call(self, obj: Callable, *, paradigm: Paradigm = None): + paradigm = paradigm or self.paradigm or self.paradigms.find_paradigm(obj) + calls = self._resolve(obj, paradigm) + return paradigm.process(calls) - if self.point: - point = self.point.next - if point: - rows.append('') - rows.append('Still to call:') - while point: - rows.append(repr(point)) - point = point.next - - return '\n'.join(rows) - - __repr__ = __str__ - - -def type_key(type_tuple): - type, _ = type_tuple - if isinstance(type, str): - return type - return type.__name__ - - -class Context(dict): - "Stores resources for a particular run." - - def add(self, it, type): - """ - Add a resource to the context. - - Optionally specify the type to use for the object rather than - the type of the object itself. - """ - - if type is NONE_TYPE: - raise ValueError('Cannot add None to context') - if type in self: - raise ContextError('Context already contains %r' % ( - type - )) - self[type] = it - - def __repr__(self): - bits = [] - for type, value in sorted(self.items(), key=type_key): - bits.append('\n %r: %r' % (type, value)) - if bits: - bits.append('\n') - return '' % ''.join(bits) - - def extract(self, obj, requires, returns): - result = self.call(obj, requires) - for type, obj in returns.process(result): - self.add(obj, type) - return result - - def call(self, obj, requires): - - if isinstance(obj, Factory): - self.add(obj, obj.returns.args[0]) - return - - args = [] - kw = {} - - for name, required in requires: - - type = required - ops = deque() - while isinstance(type, how): - ops.appendleft(type.process) - type = type.type - - o = self.get(type, missing) - if isinstance(o, Factory): - o = self.call(o.__wrapped__, o.requires) - self[type] = o - - for op in ops: - o = op(o) - if o is nothing: - break - - if o is nothing: - pass - elif o is missing: - raise ContextError('No %s in context' % repr(required)) - elif name is None: - args.append(o) - else: - kw[name] = o - - return obj(*args, **kw) diff --git a/mush/declarations.py b/mush/declarations.py deleted file mode 100644 index 7d6ec7b..0000000 --- a/mush/declarations.py +++ /dev/null @@ -1,337 +0,0 @@ -import sys -import types -from functools import ( - WRAPPER_UPDATES, - WRAPPER_ASSIGNMENTS as FUNCTOOLS_ASSIGNMENTS -) -from inspect import isclass, isfunction -from .compat import NoneType, signature -from .markers import missing, not_specified - - -def name_or_repr(obj): - return getattr(obj, '__name__', None) or repr(obj) - - -class requires(object): - """ - Represents requirements for a particular callable. - - The passed in `args` and `kw` should map to the types, including - any required :class:`~.declarations.how`, for the matching - arguments or keyword parameters the callable requires. - - String names for resources must be used instead of types where the callable - returning those resources is configured to return the named resource. - """ - - def __init__(self, *args, **kw): - check_type(*args) - check_type(*kw.values()) - self.args = args - self.kw = kw - - def __iter__(self): - """ - 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. - """ - for arg in self.args: - yield None, arg - for k, v in self.kw.items(): - yield k, v - - def __repr__(self): - bits = [] - for arg in self.args: - bits.append(name_or_repr(arg)) - for k, v in sorted(self.kw.items()): - bits.append('%s=%s' % (k, name_or_repr(v))) - txt = 'requires(%s)' % ', '.join(bits) - return txt - - def __call__(self, obj): - obj.__mush_requires__ = self - return obj - - -class ReturnsType(object): - - def __call__(self, obj): - obj.__mush_returns__ = self - return obj - - def __repr__(self): - return self.__class__.__name__ + '()' - - -class returns_result_type(ReturnsType): - """ - Default declaration that indicates a callable's return value - should be used as a resource based on the type of the object returned. - - ``None`` is ignored as a return value. - """ - - def process(self, obj): - if obj is not None: - yield obj.__class__, obj - - -class returns_mapping(ReturnsType): - """ - Declaration that indicates a callable returns a mapping of type or name - to resource. - """ - - def process(self, mapping): - return mapping.items() - - -class returns_sequence(returns_result_type): - """ - Declaration that indicates a callable's returns a sequence of values - that should be used as a resources based on the type of the object returned. - - Any ``None`` values in the sequence are ignored. - """ - - def process(self, sequence): - super_process = super(returns_sequence, self).process - for obj in sequence: - for pair in super_process(obj): - yield pair - - -class returns(returns_result_type): - """ - Declaration that specifies names for returned resources or overrides - the type of a returned resource. - - This declaration can be used to indicate the type or name of a single - returned resource or, if multiple arguments are passed, that the callable - will return a sequence of values where each one should be named or have its - type overridden. - """ - - def __init__(self, *args): - check_type(*args) - self.args = args - - def process(self, obj): - if len(self.args) == 1: - yield self.args[0], obj - else: - for t, o in zip(self.args, obj): - yield t, o - - def __repr__(self): - args_repr = ', '.join(name_or_repr(arg) for arg in self.args) - return self.__class__.__name__ + '(' + args_repr + ')' - - -def lazy(obj): - """ - Declaration that specifies the callable should only be called the first time - it is required. - """ - obj.__mush_lazy__ = True - return obj - - -class how(object): - """ - The base class for type decorators that indicate which part of a - resource is required by a particular callable. - - :param type: The resource type to be decorated. - :param names: Used to identify the part of the resource to extract. - """ - type_pattern = '%(type)s' - name_pattern = '' - - def __init__(self, type, *names): - check_type(type) - self.type = type - self.names = names - - def __repr__(self): - txt = self.type_pattern % dict(type=name_or_repr(self.type)) - for name in self.names: - txt += self.name_pattern % dict(name=name) - return txt - - def process(self, o): - """ - Extract the required part of the object passed in. - :obj:`missing` should be returned if the required part - cannot be extracted. - :obj:`missing` may be passed in and is usually be handled - by returning :obj:`missing` immediately. - """ - return missing - -class optional(how): - """ - A :class:`~.declarations.how` that indicates the callable requires the - wrapped requirement only if it's present in the :class:`~.context.Context`. - """ - type_pattern = 'optional(%(type)s)' - - def process(self, o): - if o is missing: - return nothing - return o - - -class attr(how): - """ - A :class:`~.declarations.how` that indicates the callable requires the named - attribute from the decorated type. - """ - name_pattern = '.%(name)s' - - def process(self, o): - if o is missing: - return o - try: - for name in self.names: - o = getattr(o, name) - except AttributeError: - return missing - else: - return o - - -class item(how): - """ - A :class:`~.declarations.how` that indicates the callable requires the named - item from the decorated type. - """ - name_pattern = '[%(name)r]' - - def process(self, o): - if o is missing: - return o - try: - for name in self.names: - o = o[name] - except KeyError: - return missing - else: - return o - - -if sys.version_info[0] == 2: - ok_types = (type, types.ClassType, str, how) -else: - ok_types = (type, str, how) - - -def check_type(*objs): - for obj in objs: - if not isinstance(obj, ok_types): - raise TypeError( - repr(obj)+" is not a type or label" - ) - - -class Nothing(requires, returns): - - def process(self, result): - return () - -#: A singleton that be used as a :class:`~mush.requires` to indicate that a -#: callable has no required arguments or as a :class:`~mush.returns` to indicate -#: that anything returned from a callable should be ignored. -nothing = Nothing() - -#: A singleton indicating that a callable's return value should be -#: stored based on the type of that return value. -result_type = returns_result_type() - - -def maybe_optional(p): - value = p.name - if p.default is not p.empty: - value = optional(value) - return value - - -def guess_requirements(obj): - args = [] - kw = {} - for name, p in signature(obj).parameters.items(): - if p.kind in {p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD}: - args.append(maybe_optional(p)) - elif p.kind is p.KEYWORD_ONLY: - kw[name] = maybe_optional(p) - if args or kw: - return requires(*args, **kw) - - -def extract_declarations(obj, explicit_requires, explicit_returns, guess=True): - mush_requires = getattr(obj, '__mush_requires__', None) - mush_returns = getattr(obj, '__mush_returns__', None) - annotations = getattr(obj, '__annotations__', None) - annotations = {} if annotations is None else annotations.copy() - annotation_returns = annotations.pop('return', None) - annotation_requires = annotations or None - - requires_ = explicit_requires or mush_requires or annotation_requires - returns_ = explicit_returns or mush_returns or annotation_returns - - if isinstance(requires_, requires): - pass - elif isinstance(requires_, NoneType): - if guess: - requires_ = guess_requirements(obj) - elif isinstance(requires_, (list, tuple)): - requires_ = requires(*requires_) - elif isinstance(requires_, dict): - requires_ = requires(**requires_) - else: - requires_ = requires(requires_) - - if isinstance(returns_, (ReturnsType, NoneType)): - pass - elif isinstance(returns_, (list, tuple)): - returns_ = returns(*returns_) - else: - returns_ = returns(returns_) - - return requires_, returns_ - - -WRAPPER_ASSIGNMENTS = FUNCTOOLS_ASSIGNMENTS + ( - '__mush__requires__', '__mush_returns__' -) - - -def update_wrapper(wrapper, - wrapped, - assigned=WRAPPER_ASSIGNMENTS, - updated=WRAPPER_UPDATES): - """ - An extended version of :func:`functools.update_wrapper` that - also preserves Mush's annotations. - """ - # copied here to backport bugfix from Python 3. - for attr in assigned: - try: - value = getattr(wrapped, attr) - except AttributeError: - pass - else: - setattr(wrapper, attr, value) - for attr in updated: - getattr(wrapper, attr).update(getattr(wrapped, attr, {})) - # Issue #17482: set __wrapped__ last so we don't inadvertently copy it - # from the wrapped function when updating __dict__ - wrapper.__wrapped__ = wrapped - # Return the wrapper so this can be used as a decorator via partial() - return wrapper diff --git a/mush/factory.py b/mush/factory.py deleted file mode 100644 index 5b9d03c..0000000 --- a/mush/factory.py +++ /dev/null @@ -1,16 +0,0 @@ -from .declarations import returns as returns_declaration - - -class Factory(object): - - value = None - - def __init__(self, obj, requires, returns): - if not (type(returns) is returns_declaration and len(returns.args) == 1): - raise TypeError('a single return type must be explicitly specified') - self.__wrapped__ = obj - self.requires = requires - self.returns = returns - - def __repr__(self): - return '' % self.__wrapped__ diff --git a/mush/markers.py b/mush/markers.py deleted file mode 100644 index e45b138..0000000 --- a/mush/markers.py +++ /dev/null @@ -1,11 +0,0 @@ -class Marker(object): - - def __init__(self, name): - self.name = name - - def __repr__(self): - return '' % self.name - - -not_specified = Marker('not_specified') -missing = Marker('missing') \ No newline at end of file diff --git a/mush/modifier.py b/mush/modifier.py deleted file mode 100644 index 06807a6..0000000 --- a/mush/modifier.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -.. currentmodule:: mush -""" - -from .callpoints import CallPoint -from .markers import not_specified - - -class Modifier(object): - """ - Used to make changes at a particular point in a runner. - These are returned by :meth:`Runner.add` and :meth:`Runner.__getitem__`. - """ - def __init__(self, runner, callpoint, label): - self.runner = runner - self.callpoint = callpoint - if label is not_specified: - self.labels = set() - else: - self.labels = {label} - - def add(self, obj, requires=None, returns=None, label=None, lazy=False): - """ - :param obj: The callable to be added. - - :param requires: The resources to required as parameters when calling - `obj`. These can be specified by passing a single - type, a string name or a :class:`requires` object. - - :param returns: The resources that `obj` will return. - These can be specified as a single - type, a string name or a :class:`returns`, - :class:`returns_mapping`, :class:`returns_sequence` - object. - - :param label: If specified, this is a string that adds a label to the - point where `obj` is added that can later be retrieved - with :meth:`Runner.__getitem__`. - - :param lazy: If true, ``obj`` will only be called the first time it - is needed. - - If no label is specified but the point which this - :class:`~.modifier.Modifier` represents has any labels, those labels - will be moved to the newly inserted point. - """ - if label in self.runner.labels: - raise ValueError('%r already points to %r' % ( - label, self.runner.labels[label] - )) - callpoint = CallPoint(obj, requires, returns, lazy) - - if label: - self.add_label(label, callpoint) - - callpoint.previous = self.callpoint - - if self.callpoint: - - callpoint.next = self.callpoint.next - if self.callpoint.next: - self.callpoint.next.previous = callpoint - self.callpoint.next = callpoint - - if not label: - for label in self.labels: - self.add_label(label, callpoint) - callpoint.added_using.add(label) - else: - self.runner.start = callpoint - - if self.callpoint is self.runner.end or self.runner.end is None: - self.runner.end = callpoint - - self.callpoint = callpoint - - def add_label(self, label, callpoint=None): - """ - Add a label to the point represented by this - :class:`~.modifier.Modifier`. - - :param callpoint: For internal use only. - """ - callpoint = callpoint or self.callpoint - callpoint.labels.add(label) - old_callpoint = self.runner.labels.get(label) - if old_callpoint: - old_callpoint.labels.remove(label) - self.runner.labels[label] = callpoint - self.labels.add(label) diff --git a/mush/paradigms/__init__.py b/mush/paradigms/__init__.py new file mode 100644 index 0000000..bd0cc17 --- /dev/null +++ b/mush/paradigms/__init__.py @@ -0,0 +1,14 @@ +from collections import namedtuple + +from .paradigm import Paradigm +from .paradigms import Paradigms + + +Call = namedtuple('Call', ('obj', 'args', 'kw')) + +paradigms = Paradigms() + +normal = paradigms.register_if_possible('mush.paradigms.normal_', 'Normal') +asyncio = paradigms.register_if_possible('mush.paradigms.asyncio_', 'AsyncIO') + +paradigms.add_shifter_if_possible(normal, asyncio, 'mush.paradigms.asyncio_', 'asyncio_to_normal') diff --git a/mush/paradigms/asyncio_.py b/mush/paradigms/asyncio_.py new file mode 100644 index 0000000..4a49c36 --- /dev/null +++ b/mush/paradigms/asyncio_.py @@ -0,0 +1,28 @@ +import asyncio +from functools import partial +from typing import Callable + +from .paradigm import Paradigm +from ..typing import Calls + + +class AsyncIO(Paradigm): + + def claim(self, obj: Callable) -> bool: + if asyncio.iscoroutinefunction(obj): + return True + + async def process(self, calls: Calls): + call = next(calls) + try: + while True: + result = await call.obj(*call.args, **call.kw) + call = calls.send(result) + except StopIteration: + return result + + +async def asyncio_to_normal(obj, *args, **kw): + loop = asyncio.get_event_loop() + obj_ = partial(obj, *args, **kw) + return await loop.run_in_executor(None, obj_) diff --git a/mush/paradigms/normal_.py b/mush/paradigms/normal_.py new file mode 100644 index 0000000..5a30f17 --- /dev/null +++ b/mush/paradigms/normal_.py @@ -0,0 +1,19 @@ +from typing import Callable + +from .paradigm import Paradigm +from ..typing import Calls + + +class Normal(Paradigm): + + def claim(self, obj: Callable) -> bool: + return True + + def process(self, calls: Calls): + call = next(calls) + try: + while True: + result = call.obj(*call.args, **call.kw) + call = calls.send(result) + except StopIteration: + return result diff --git a/mush/paradigms/paradigm.py b/mush/paradigms/paradigm.py new file mode 100644 index 0000000..3e5dd18 --- /dev/null +++ b/mush/paradigms/paradigm.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod +from typing import Callable +from ..typing import Calls + + +class Paradigm(ABC): + + @abstractmethod + def claim(self, obj: Callable) -> bool: + ... + + @abstractmethod + def process(self, calls: Calls): + ... + + +class MissingParadigm(Paradigm): + + def __init__(self, exception): + super().__init__() + self.exception = exception + + def claim(self, obj: Callable) -> bool: + raise self.exception + + def process(self, calls: Calls): + raise self.exception diff --git a/mush/paradigms/paradigms.py b/mush/paradigms/paradigms.py new file mode 100644 index 0000000..193d487 --- /dev/null +++ b/mush/paradigms/paradigms.py @@ -0,0 +1,54 @@ +from functools import partial +from importlib import import_module +from typing import Callable, List, Optional, Dict, Tuple + +from .paradigm import Paradigm, MissingParadigm + + + +def missing_shifter(exception, obj): + raise exception + + +class Paradigms: + + def __init__(self): + self._paradigms: List[Paradigm] = [] + self._shifters: Dict[Tuple['Paradigm', 'Paradigm'], Callable] = {} + + def register(self, paradigm: Paradigm) -> None: + self._paradigms.insert(0, paradigm) + + def register_if_possible(self, module_path: str, class_name: str) -> Paradigm: + try: + module = import_module(module_path) + except ModuleNotFoundError as e: + paradigm = MissingParadigm(e) + else: + paradigm = getattr(module, class_name)() + self.register(paradigm) + return paradigm + + def add_shifter_if_possible( + self, source: Paradigm, target: Paradigm, module_path: str, callable_name: str + ) -> None: + try: + module = import_module(module_path) + except ModuleNotFoundError as e: + shifter = partial(missing_shifter, e) + else: + shifter = getattr(module, callable_name) + self._shifters[source, target] = shifter + + def find_paradigm(self, obj: Callable) -> Paradigm: + for paradigm in self._paradigms: + if paradigm.claim(obj): + return paradigm + raise Exception('No paradigm') + + def find_caller(self, obj: Callable, target_paradigm: Paradigm) -> Callable: + source_paradigm = self.find_paradigm(obj) + if source_paradigm is target_paradigm: + return obj + else: + return partial(self._shifters[source_paradigm, target_paradigm], obj) diff --git a/mush/plug.py b/mush/plug.py deleted file mode 100644 index 0b59d8f..0000000 --- a/mush/plug.py +++ /dev/null @@ -1,68 +0,0 @@ -class ignore(object): - """ - A decorator to explicitly mark that a method of a :class:`~mush.Plug` should - not be added to a runner by :meth:`~mush.Plug.add_to` - """ - def __call__(self, method): - method.__mush_plug__ = self - return method - - def apply(self, runner, obj): - pass - - -class insert(ignore): - """ - A decorator to explicitly mark that a method of a :class:`~mush.Plug` should - be added to a runner by :meth:`~mush.Plug.add_to`. The `label` parameter - can be used to indicate a different label at which to add the method, - instead of using the name of the method. - """ - def __init__(self, label=None): - self.label = label - - def apply(self, runner, obj): - runner[self.label or obj.__name__].add(obj) - -class append(ignore): - """ - A decorator to mark that this method of a :class:`~mush.Plug` should - be added to the end of a runner by :meth:`~mush.Plug.add_to`. - """ - - def apply(self, runner, obj): - runner.add(obj) - - -class Plug(object): - """ - Base class for a 'plug' that can add to several points in a runner. - """ - - #: Control whether methods need to be decorated with :class:`insert` - #: in order to be added by this :class:`~mush.Plug`. - explicit = False - - @ignore() - def add_to(self, runner): - """ - Add methods of the instance to the supplied runner. - By default, all methods will be added and the name of the method will be - used as the label in the runner at which the method will be added. - If no such label exists, a :class:`KeyError` will be raised. - - If :attr:`explicit` is ``True``, then only methods decorated with an - :class:`~mush.plug.insert` will be added. - """ - - if self.explicit: - default_action = ignore() - else: - default_action = insert() - - for name in dir(self): - if not name.startswith('_'): - obj = getattr(self, name) - if callable(obj): - action = getattr(obj, '__mush_plug__', default_action) - action.apply(runner, obj) diff --git a/mush/runner.py b/mush/runner.py deleted file mode 100644 index 0ca0740..0000000 --- a/mush/runner.py +++ /dev/null @@ -1,266 +0,0 @@ -from .callpoints import CallPoint -from .context import Context, ContextError -from .declarations import extract_declarations -from .markers import not_specified -from .modifier import Modifier -from .plug import Plug - - -class Runner(object): - """ - A chain of callables along with declarations of their required and - returned resources along with tools to manage the order in which they - will be called. - """ - - start = end = None - - def __init__(self, *objects): - self.labels = {} - self.extend(*objects) - - def add(self, obj, requires=None, returns=None, label=None, lazy=False): - """ - Add a callable to the runner. - - :param obj: The callable to be added. - - :param requires: The resources to required as parameters when calling - `obj`. These can be specified by passing a single - type, a string name or a :class:`requires` object. - - :param returns: The resources that `obj` will return. - These can be specified as a single - type, a string name or a :class:`returns`, - :class:`returns_mapping`, :class:`returns_sequence` - object. - - :param label: If specified, this is a string that adds a label to the - point where `obj` is added that can later be retrieved - with :meth:`Runner.__getitem__`. - - :param lazy: If true, ``obj`` will only be called the first time it - is needed. - """ - if isinstance(obj, Plug): - obj.add_to(self) - else: - m = Modifier(self, self.end, not_specified) - m.add(obj, requires, returns, label, lazy) - return m - - def add_label(self, label): - """ - Add a label to the the point currently at the end of the runner. - """ - m = Modifier(self, self.end, not_specified) - m.add_label(label) - return m - - def _copy_from(self, start_point, end_point, added_using=None): - previous_cloned_point = self.end - point = start_point - - while point: - if added_using is None or added_using in point.added_using: - cloned_point = CallPoint(point.obj, point.requires, point.returns) - cloned_point.labels = set(point.labels) - for label in cloned_point.labels: - self.labels[label] = cloned_point - - if self.start is None: - self.start = cloned_point - - if previous_cloned_point: - previous_cloned_point.next = cloned_point - cloned_point.previous = previous_cloned_point - - previous_cloned_point = cloned_point - - point = point.next - if point and point.previous is end_point: - break - - self.end = previous_cloned_point - - def extend(self, *objs): - """ - Add the specified callables to this runner. - - If any of the objects passed is a :class:`Runner`, the contents of that - runner will be added to this runner. - """ - for obj in objs: - if isinstance(obj, Runner): - self._copy_from(obj.start, obj.end) - else: - self.add(obj) - - def clone(self, - start_label=None, end_label=None, - include_start=False, include_end=False, - added_using=None): - """ - Return a copy of this :class:`Runner`. - - :param start_label: - An optional string specifying the point at which to start cloning. - - :param end_label: - An optional string specifying the point at which to stop cloning. - - :param include_start: - If ``True``, the point specified in ``start_label`` will be included - in the cloned runner. - - :param include_end: - If ``True``, the point specified in ``end_label`` will be included - in the cloned runner. - - :param added_using: - An optional string specifying that only points added using the - label specified in this option should be cloned. - This filtering is applied in addition to the above options. - """ - runner = Runner() - - if start_label: - start = self.labels[start_label] - if not include_start: - start = start.next - else: - start = self.start - - if end_label: - end = self.labels[end_label] - if not include_end: - end = end.previous - else: - end = self.end - - # check start point is before end_point - point = start.previous - while point: - if point is end: - return runner - point = point.previous - - runner._copy_from(start, end, added_using) - return runner - - def replace(self, original, replacement, requires=None, returns=None): - """ - Replace all instances of one callable with another. - - No changes in requirements or call ordering will be made unless the - replacements have been decorated with requirements, or either - ``requires`` or ``returns`` have been specified. - - :param requires: The resources to required as parameters when calling - `obj`. These can be specified by passing a single - type, a string name or a :class:`requires` object. - - :param returns: The resources that `obj` will return. - These can be specified as a single - type, a string name or a :class:`returns`, - :class:`returns_mapping`, :class:`returns_sequence` - object. - """ - point = self.start - while point: - if point.obj is original: - - new_requirements = extract_declarations( - replacement, requires, returns, guess=False - ) - - if any(new_requirements): - new_point = CallPoint(replacement, *new_requirements) - if point.previous is None: - self.start = new_point - else: - point.previous.next = new_point - if point.next is None: - self.end = new_point - else: - point.next.previous = new_point - new_point.next = point.next - for label in point.labels: - self.labels[label] = new_point - new_point.labels.add(label) - new_point.added_using = set(point.added_using) - - else: - - point.obj = replacement - - point = point.next - - def __getitem__(self, label): - """ - Retrieve a :class:`~.modifier.Modifier` for a previous labelled point in - the runner. - """ - return Modifier(self, self.labels[label], label) - - def __add__(self, other): - """ - Return a new :class:`Runner` containing the contents of the two - :class:`Runner` instances being added together. - """ - runner = Runner() - for r in self, other: - runner._copy_from(r.start, r.end) - return runner - - def __call__(self, 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 - :class:`~.context.Context` will be created meaning that no required - objects are kept between calls and all callables will be - called each time. - - :param context: - Used for passing a context when context managers are used. - You should never need to pass this parameter. - """ - if context is None: - context = Context() - context.point = self.start - - result = None - - while context.point: - - point = context.point - context.point = point.next - - try: - result = point(context) - except ContextError as e: - raise ContextError(str(e), point, context) - - if getattr(result, '__enter__', None): - with result as manager: - if manager not in (None, result): - context.add(manager, manager.__class__) - result = None - result = self(context) - - return result - - def __repr__(self): - bits = [] - point = self.start - while point: - bits.append('\n ' + repr(point)) - point = point.next - if bits: - bits.append('\n') - return '%s' % ''.join(bits) - - diff --git a/mush/tests/configparser.py b/mush/tests/configparser.py deleted file mode 100644 index 8bbd59b..0000000 --- a/mush/tests/configparser.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -if sys.version_info[:2] > (3, 0): - from configparser import RawConfigParser -else: - from ConfigParser import RawConfigParser - - diff --git a/mush/tests/conftest.py b/mush/tests/conftest.py deleted file mode 100644 index d981b98..0000000 --- a/mush/tests/conftest.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from mock import Mock -from testfixtures.comparison import register, compare_simple - -from mush import returns, requires -from mush.declarations import how -from ..compat import PY2 - - -def pytest_ignore_collect(path): - if 'py3' in path.basename and PY2: - return True - - -@pytest.fixture() -def mock(): - return Mock() - - -def compare_requires(x, y, context): - diff_args = context.different(x.args, y.args, '.args') - diff_kw = context.different(x.kw, y.kw, '.args') - if diff_args or diff_kw: # pragma: no cover - return compare_simple(x, y, context) - - -def compare_returns(x, y, context): - diff_args = context.different(x.args, y.args, '.args') - if diff_args: # pragma: no cover - return compare_simple(x, y, context) - - -def compare_how(x, y, context): - diff_args = context.different(x.type, y.type, '.type') - diff_names = context.different(x.type, y.type, '.names') - if diff_args or diff_names: # pragma: no cover - return compare_simple(x, y, context) - - -register(requires, compare_requires) -register(returns, compare_returns) -register(how, compare_how) diff --git a/mush/tests/example_with_mush_clone.py b/mush/tests/example_with_mush_clone.py deleted file mode 100644 index 1d6d02e..0000000 --- a/mush/tests/example_with_mush_clone.py +++ /dev/null @@ -1,70 +0,0 @@ -from argparse import ArgumentParser, Namespace -from .configparser import RawConfigParser -from mush import Runner, requires, attr, item -import logging, os, sqlite3, sys - -log = logging.getLogger() - -def base_options(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') - -def parse_args(parser: ArgumentParser): - return parser.parse_args() - -def parse_config(args: Namespace) -> 'config': - config = RawConfigParser() - config.read(args.config) - return dict(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() - -base_runner = Runner(ArgumentParser) -base_runner.add(base_options, label='args') -base_runner.extend(parse_args, parse_config) -base_runner.add(setup_logging, requires( - log_path = item('config', 'log'), - quiet = attr(Namespace, 'quiet'), - verbose = attr(Namespace, 'verbose') -)) - - -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) - -main = base_runner.clone() -main['args'].add(args, requires=ArgumentParser) -main.add(DatabaseHandler, requires=item('config', 'db')) -main.add(do, - requires(attr(DatabaseHandler, 'conn'), attr(Namespace, 'path'))) - -if __name__ == '__main__': - main() diff --git a/mush/tests/example_with_mush_factory.py b/mush/tests/example_with_mush_factory.py deleted file mode 100644 index 98c36ba..0000000 --- a/mush/tests/example_with_mush_factory.py +++ /dev/null @@ -1,35 +0,0 @@ -from mush import Runner, attr, item, requires -from argparse import ArgumentParser, Namespace - -from .example_with_mush_clone import ( - DatabaseHandler, parse_args, parse_config, do, - setup_logging - ) - - -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, requires=ArgumentParser) - runner.add(parse_args, requires=ArgumentParser) - runner.add(parse_config, requires=Namespace) - runner.add(setup_logging, requires( - log_path = item('config', 'log'), - quiet = attr(Namespace, 'quiet'), - verbose = attr(Namespace, 'verbose') - )) - runner.add(DatabaseHandler, requires=item('config', 'db')) - runner.add( - do, - requires(attr(DatabaseHandler, 'conn'), attr(Namespace, 'path')) - ) - return runner - -main = make_runner(do) diff --git a/mush/tests/example_without_mush.py b/mush/tests/example_without_mush.py deleted file mode 100644 index 387c2b9..0000000 --- a/mush/tests/example_without_mush.py +++ /dev/null @@ -1,44 +0,0 @@ -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) - log.setLevel(logging.DEBUG) - - 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() diff --git a/mush/tests/test_callpoints.py b/mush/tests/test_callpoints.py deleted file mode 100644 index 955bb9e..0000000 --- a/mush/tests/test_callpoints.py +++ /dev/null @@ -1,118 +0,0 @@ -from functools import update_wrapper -from unittest import TestCase - -from mock import Mock, call -from testfixtures import compare - -from mush.callpoints import CallPoint -from mush.declarations import requires, returns, update_wrapper - - -class TestCallPoints(TestCase): - - def setUp(self): - self.context = Mock() - - def test_passive_attributes(self): - # these are managed by Modifiers - point = CallPoint(self.context) - compare(point.previous, None) - compare(point.next, None) - compare(point.labels, set()) - - def test_supplied_explicitly(self): - obj = object() - rq = requires('foo') - rt = returns('bar') - result = CallPoint(obj, rq, rt)(self.context) - compare(result, self.context.extract.return_value) - self.context.extract.assert_called_with(obj, rq, rt) - - def test_extract_from_decorations(self): - rq = requires('foo') - rt = returns('bar') - - @rq - @rt - def foo(): pass - - result = CallPoint(foo)(self.context) - compare(result, self.context.extract.return_value) - self.context.extract.assert_called_with(foo, rq, rt) - - def test_extract_from_decorated_class(self): - - rq = requires('foo') - rt = returns('bar') - - class Wrapper(object): - def __init__(self, func): - self.func = func - def __call__(self): - return 'the '+self.func() - - def my_dec(func): - return update_wrapper(Wrapper(func), func) - - @my_dec - @rq - @rt - def foo(): - return 'answer' - - self.context.extract.side_effect = lambda func, rq, rt: (func(), rq, rt) - result = CallPoint(foo)(self.context) - compare(result, expected=('the answer', rq, rt)) - - def test_explicit_trumps_decorators(self): - @requires('foo') - @returns('bar') - def foo(): pass - - rq = requires('baz') - rt = returns('bob') - - result = CallPoint(foo, requires=rq, returns=rt)(self.context) - compare(result, self.context.extract.return_value) - self.context.extract.assert_called_with(foo, rq, rt) - - def test_repr_minimal(self): - def foo(): pass - point = CallPoint(foo) - compare(repr(foo)+" requires() returns_result_type()", repr(point)) - - def test_repr_maximal(self): - def foo(): pass - point = CallPoint(foo, requires('foo'), returns('bar')) - point.labels.add('baz') - point.labels.add('bob') - compare(repr(foo)+" requires('foo') returns('bar') <-- baz, bob", - repr(point)) - - def test_convert_to_requires_and_returns(self): - def foo(): pass - point = CallPoint(foo, requires='foo', returns='bar') - self.assertTrue(isinstance(point.requires, requires)) - self.assertTrue(isinstance(point.returns, returns)) - compare(repr(foo)+" requires('foo') returns('bar')", - repr(point)) - - def test_convert_to_requires_and_returns_tuple(self): - def foo(): pass - point = CallPoint(foo, - requires=('foo', 'bar'), - returns=('baz', 'bob')) - self.assertTrue(isinstance(point.requires, requires)) - self.assertTrue(isinstance(point.returns, returns)) - compare(repr(foo)+" requires('foo', 'bar') returns('baz', 'bob')", - repr(point)) - - def test_convert_to_requires_and_returns_list(self): - def foo(): pass - point = CallPoint(foo, - requires=['foo', 'bar'], - returns=['baz', 'bob']) - self.assertTrue(isinstance(point.requires, requires)) - self.assertTrue(isinstance(point.returns, returns)) - compare(repr(foo)+" requires('foo', 'bar') returns('baz', 'bob')", - repr(point)) diff --git a/mush/tests/test_context.py b/mush/tests/test_context.py deleted file mode 100644 index c109a6b..0000000 --- a/mush/tests/test_context.py +++ /dev/null @@ -1,268 +0,0 @@ -from unittest import TestCase -from mock import Mock - -from testfixtures import ShouldRaise, compare - -from mush.context import Context, ContextError - -from mush.declarations import ( - nothing, requires, optional, item, - attr, returns, returns_mapping -) - - -class TheType(object): - def __repr__(self): - return '' - - -class TestContext(TestCase): - - def test_simple(self): - obj = TheType() - context = Context() - context.add(obj, TheType) - - self.assertTrue(context[TheType] is obj) - expected = ( - ": \n" - "}>" - ) - self.assertEqual(repr(context), expected) - self.assertEqual(str(context), expected) - - def test_type_as_string(self): - obj = TheType() - context = Context() - context.add(obj, type='my label') - - expected = ("\n" - "}>") - self.assertTrue(context['my label'] is obj) - self.assertEqual(repr(context), expected) - self.assertEqual(str(context), expected) - - def test_explicit_type(self): - class T2(object): pass - obj = TheType() - context = Context() - context.add(obj, T2) - self.assertTrue(context[T2] is obj) - expected = ("\n" - "}>") - compare(repr(context), expected) - compare(str(context), expected) - - def test_clash(self): - obj1 = TheType() - obj2 = TheType() - context = Context() - context.add(obj1, TheType) - with ShouldRaise(ContextError('Context already contains '+repr(TheType))): - context.add(obj2, TheType) - - def test_clash_string_type(self): - obj1 = TheType() - obj2 = TheType() - context = Context() - context.add(obj1, type='my label') - with ShouldRaise(ContextError("Context already contains 'my label'")): - context.add(obj2, type='my label') - - def test_add_none(self): - context = Context() - with ShouldRaise(ValueError('Cannot add None to context')): - context.add(None, None.__class__) - - def test_add_none_with_type(self): - context = Context() - context.add(None, TheType) - self.assertTrue(context[TheType] is None) - - def test_call_basic(self): - def foo(): - return 'bar' - context = Context() - result = context.call(foo, nothing) - compare(result, 'bar') - - def test_call_requires_string(self): - def foo(obj): - return obj - context = Context() - context.add('bar', 'baz') - result = context.call(foo, requires('baz')) - compare(result, 'bar') - compare({'baz': 'bar'}, context) - - def test_call_requires_type(self): - def foo(obj): - return obj - context = Context() - context.add('bar', TheType) - result = context.call(foo, requires(TheType)) - compare(result, 'bar') - compare({TheType: 'bar'}, context) - - def test_call_requires_missing(self): - def foo(obj): return obj - context = Context() - with ShouldRaise(ContextError( - "No in context" - )): - context.call(foo, requires(TheType)) - - def test_call_requires_item_missing(self): - def foo(obj): return obj - context = Context() - context.add({}, TheType) - with ShouldRaise(ContextError( - "No TheType['foo'] in context" - )): - context.call(foo, requires(item(TheType, 'foo'))) - - def test_call_requires_accidental_tuple(self): - def foo(obj): return obj - context = Context() - with ShouldRaise(TypeError( - "(, " - ") " - "is not a type or label" - )): - context.call(foo, requires((TheType, TheType))) - - def test_call_requires_named_parameter(self): - def foo(x, y): - return x, y - context = Context() - context.add('foo', TheType) - context.add('bar', 'baz') - result = context.call(foo, requires(y='baz', x=TheType)) - compare(result, ('foo', 'bar')) - compare({TheType: 'foo', - 'baz': 'bar'}, - actual=context) - - def test_call_requires_optional_present(self): - def foo(x=1): - return x - context = Context() - context.add(2, TheType) - result = context.call(foo, requires(optional(TheType))) - compare(result, 2) - compare({TheType: 2}, context) - - def test_call_requires_optional_ContextError(self): - def foo(x=1): - return x - context = Context() - result = context.call(foo, requires(optional(TheType))) - compare(result, 1) - - def test_call_requires_optional_string(self): - def foo(x=1): - return x - context = Context() - context.add(2, 'foo') - result = context.call(foo, requires(optional('foo'))) - compare(result, 2) - compare({'foo': 2}, context) - - def test_call_requires_item(self): - def foo(x): - return x - context = Context() - context.add(dict(bar='baz'), 'foo') - result = context.call(foo, requires(item('foo', 'bar'))) - compare(result, 'baz') - - def test_call_requires_attr(self): - def foo(x): - return x - m = Mock() - context = Context() - context.add(m, 'foo') - result = context.call(foo, requires(attr('foo', 'bar'))) - compare(result, m.bar) - - def test_call_requires_item_attr(self): - def foo(x): - return x - m = Mock() - m.bar= dict(baz='bob') - context = Context() - context.add(m, 'foo') - result = context.call(foo, requires(item(attr('foo', 'bar'), 'baz'))) - compare(result, 'bob') - - def test_call_requires_optional_item_ContextError(self): - def foo(x=1): - return x - context = Context() - result = context.call(foo, requires(optional(item('foo', 'bar')))) - compare(result, 1) - - def test_call_requires_optional_item_present(self): - def foo(x=1): - return x - context = Context() - context.add(dict(bar='baz'), 'foo') - result = context.call(foo, requires(optional(item('foo', 'bar')))) - compare(result, 'baz') - - def test_call_requires_item_optional_ContextError(self): - def foo(x=1): - return x - context = Context() - result = context.call(foo, requires(item(optional('foo'), 'bar'))) - compare(result, 1) - - def test_call_requires_item_optional_present(self): - def foo(x=1): - return x - context = Context() - context.add(dict(bar='baz'), 'foo') - result = context.call(foo, requires(item(optional('foo'), 'bar'))) - compare(result, 'baz') - - def test_returns_single(self): - def foo(): - return 'bar' - context = Context() - result = context.extract(foo, nothing, returns(TheType)) - compare(result, 'bar') - compare({TheType: 'bar'}, context) - - def test_returns_sequence(self): - def foo(): - return 1, 2 - context = Context() - result = context.extract(foo, nothing, returns('foo', 'bar')) - compare(result, (1, 2)) - compare({'foo': 1, 'bar': 2}, context) - - def test_returns_mapping(self): - def foo(): - return {'foo': 1, 'bar': 2} - context = Context() - result = context.extract(foo, nothing, returns_mapping()) - compare(result, {'foo': 1, 'bar': 2}) - compare({'foo': 1, 'bar': 2}, context) - - def test_ignore_return(self): - def foo(): - return 'bar' - context = Context() - result = context.extract(foo, nothing, nothing) - compare(result, 'bar') - compare({}, context) - - def test_ignore_non_iterable_return(self): - def foo(): pass - context = Context() - result = context.extract(foo, nothing, nothing) - compare(result, expected=None) - compare(context, expected={}) diff --git a/mush/tests/test_declarations.py b/mush/tests/test_declarations.py deleted file mode 100644 index c5d6421..0000000 --- a/mush/tests/test_declarations.py +++ /dev/null @@ -1,323 +0,0 @@ -from functools import partial -from unittest import TestCase -from mock import Mock -from testfixtures import compare, generator, ShouldRaise -from mush.markers import missing -from mush.declarations import ( - requires, optional, returns, - returns_mapping, returns_sequence, returns_result_type, - how, item, attr, nothing, - extract_declarations -) - - -def check_extract(obj, expected_rq, expected_rt): - rq, rt = extract_declarations(obj, None, None) - compare(rq, expected=expected_rq, strict=True) - compare(rt, expected=expected_rt, strict=True) - - -class Type1(object): pass -class Type2(object): pass -class Type3(object): pass -class Type4(object): pass - - -class TestRequires(TestCase): - - def test_empty(self): - r = requires() - compare(repr(r), 'requires()') - compare(generator(), r) - - def test_types(self): - r = requires(Type1, Type2, x=Type3, y=Type4) - compare(repr(r), 'requires(Type1, Type2, x=Type3, y=Type4)') - compare({ - (None, Type1), - (None, Type2), - ('x', Type3), - ('y', Type4), - }, set(r)) - - def test_strings(self): - r = requires('1', '2', x='3', y='4') - compare(repr(r), "requires('1', '2', x='3', y='4')") - compare({ - (None, '1'), - (None, '2'), - ('x', '3'), - ('y', '4'), - }, set(r)) - - def test_tuple_arg(self): - with ShouldRaise(TypeError("('1', '2') is not a type or label")): - requires(('1', '2')) - - def test_tuple_kw(self): - with ShouldRaise(TypeError("('1', '2') is not a type or label")): - requires(foo=('1', '2')) - - def test_decorator_paranoid(self): - @requires(Type1) - def foo(): - return 'bar' - - compare(set(foo.__mush_requires__), {(None, Type1)}) - compare(foo(), 'bar') - - -class TestItem(TestCase): - - def test_single(self): - h = item(Type1, 'foo') - compare(repr(h), "Type1['foo']") - compare(h.process(dict(foo=1)), 1) - - def test_multiple(self): - h = item(Type1, 'foo', 'bar') - compare(repr(h), "Type1['foo']['bar']") - compare(h.process(dict(foo=dict(bar=1))), 1) - - def test_missing_obj(self): - h = item(Type1, 'foo', 'bar') - with ShouldRaise(TypeError): - h.process(object()) - - def test_missing_key(self): - h = item(Type1, 'foo', 'bar') - compare(h.process({}), missing) - - def test_passed_missing(self): - h = item(Type1, 'foo', 'bar') - compare(h.process(missing), missing) - - def test_bad_type(self): - with ShouldRaise(TypeError): - item([], 'foo', 'bar') - - -class TestHow(TestCase): - - def test_process_on_base(self): - compare(how('foo').process('bar'), missing) - - -class TestAttr(TestCase): - - def test_single(self): - h = attr(Type1, 'foo') - compare(repr(h), "Type1.foo") - m = Mock() - compare(h.process(m), m.foo) - - def test_multiple(self): - h = attr(Type1, 'foo', 'bar') - compare(repr(h), "Type1.foo.bar") - m = Mock() - compare(h.process(m), m.foo.bar) - - def test_missing(self): - h = attr(Type1, 'foo', 'bar') - compare(h.process(object()), missing) - - def test_passed_missing(self): - h = attr(Type1, 'foo', 'bar') - compare(h.process(missing), missing) - - -class TestOptional(TestCase): - - def test_type(self): - compare(repr(optional(Type1)), "optional(Type1)") - - def test_string(self): - compare(repr(optional('1')), "optional('1')") - - def test_present(self): - compare(optional(Type1).process(1), 1) - - def test_missing(self): - compare(optional(Type1).process(missing), nothing) - - -class TestReturns(TestCase): - - def test_type(self): - r = returns(Type1) - compare(repr(r), 'returns(Type1)') - compare(dict(r.process('foo')), {Type1: 'foo'}) - - def test_string(self): - r = returns('bar') - compare(repr(r), "returns('bar')") - compare(dict(r.process('foo')), {'bar': 'foo'}) - - def test_sequence(self): - r = returns(Type1, 'bar') - compare(repr(r), "returns(Type1, 'bar')") - compare(dict(r.process(('foo', 'baz'))), - {Type1: 'foo', 'bar': 'baz'}) - - def test_decorator(self): - @returns(Type1) - def foo(): - return 'foo' - r = foo.__mush_returns__ - compare(repr(r), 'returns(Type1)') - compare(dict(r.process(foo())), {Type1: 'foo'}) - - def test_bad_type(self): - with ShouldRaise(TypeError( - '[] is not a type or label' - )): - @returns([]) - def foo(): pass - -class TestReturnsMapping(TestCase): - - def test_it(self): - @returns_mapping() - def foo(): - return {Type1: 'foo', 'bar': 'baz'} - r = foo.__mush_returns__ - compare(repr(r), 'returns_mapping()') - compare(dict(r.process(foo())), - {Type1: 'foo', 'bar': 'baz'}) - - -class TestReturnsSequence(TestCase): - - def test_it(self): - t1 = Type1() - t2 = Type2() - @returns_sequence() - def foo(): - return t1, t2 - r = foo.__mush_returns__ - compare(repr(r), 'returns_sequence()') - compare(dict(r.process(foo())), - {Type1: t1, Type2: t2}) - - -class TestReturnsResultType(TestCase): - - def test_basic(self): - @returns_result_type() - def foo(): - return 'foo' - r = foo.__mush_returns__ - compare(repr(r), 'returns_result_type()') - compare(dict(r.process(foo())), {str: 'foo'}) - - def test_old_style_class(self): - class Type: pass - obj = Type() - r = returns_result_type() - compare(dict(r.process(obj)), {Type: obj}) - - def test_returns_nothing(self): - def foo(): - pass - r = returns_result_type() - compare(dict(r.process(foo())), {}) - - -class TestExtractDeclarations(object): - - def test_default_requirements_for_function(self): - def foo(a, b=None): pass - check_extract(foo, - expected_rq=requires('a', optional('b')), - expected_rt=None) - - def test_default_requirements_for_class(self): - class MyClass(object): - def __init__(self, a, b=None): pass - check_extract(MyClass, - expected_rq=requires('a', optional('b')), - expected_rt=None) - - def test_extract_from_partial(self): - def foo(x, y, z, a=None): pass - p = partial(foo, 1, y=2) - check_extract( - p, - expected_rq=requires(z='z', a=optional('a'), y=optional('y')), - expected_rt=None - ) - - def test_extract_from_partial_default_not_in_partial(self): - def foo(a=None): pass - p = partial(foo) - check_extract( - p, - expected_rq=requires(optional('a')), - expected_rt=None - ) - - def test_extract_from_partial_default_in_partial_arg(self): - def foo(a=None): pass - p = partial(foo, 1) - check_extract( - p, - # since a is already bound by the partial: - expected_rq=None, - expected_rt=None - ) - - def test_extract_from_partial_default_in_partial_kw(self): - def foo(a=None): pass - p = partial(foo, a=1) - check_extract( - p, - expected_rq=requires(a=optional('a')), - expected_rt=None - ) - - def test_extract_from_partial_required_in_partial_arg(self): - def foo(a): pass - p = partial(foo, 1) - check_extract( - p, - # since a is already bound by the partial: - expected_rq=None, - expected_rt=None - ) - - def test_extract_from_partial_required_in_partial_kw(self): - def foo(a): pass - p = partial(foo, a=1) - check_extract( - p, - expected_rq=requires(a=optional('a')), - expected_rt=None - ) - - def test_extract_from_partial_plus_one_default_not_in_partial(self): - def foo(b, a=None): pass - p = partial(foo) - check_extract( - p, - expected_rq=requires('b', optional('a')), - expected_rt=None - ) - - def test_extract_from_partial_plus_one_required_in_partial_arg(self): - def foo(b, a): pass - p = partial(foo, 1) - check_extract( - p, - # since b is already bound: - expected_rq=requires('a'), - expected_rt=None - ) - - def test_extract_from_partial_plus_one_required_in_partial_kw(self): - def foo(b, a): pass - p = partial(foo, a=1) - check_extract( - p, - expected_rq=requires('b', a=optional('a')), - expected_rt=None - ) diff --git a/mush/tests/test_declarations_py3.py b/mush/tests/test_declarations_py3.py deleted file mode 100644 index 12d3cec..0000000 --- a/mush/tests/test_declarations_py3.py +++ /dev/null @@ -1,86 +0,0 @@ -from testfixtures import compare - -from mush.declarations import ( - requires, returns, returns_mapping, returns_sequence, item, update_wrapper, - optional -) -from mush.tests.test_declarations import check_extract - - -class TestExtractDeclarations(object): - - def test_extract_from_annotations(self): - def foo(a: 'foo', b, c: 'bar' = 1, d=2) -> 'bar': pass - check_extract(foo, - expected_rq=requires(a='foo', c='bar'), - expected_rt=returns('bar')) - - def test_requires_only(self): - def foo(a: 'foo'): pass - check_extract(foo, - expected_rq=requires(a='foo'), - expected_rt=None) - - def test_returns_only(self): - def foo() -> 'bar': pass - check_extract(foo, - expected_rq=None, - expected_rt=returns('bar')) - - def test_extract_from_decorated_class(self, mock): - - class Wrapper(object): - def __init__(self, func): - self.func = func - def __call__(self): - return 'the '+self.func() - - def my_dec(func): - return update_wrapper(Wrapper(func), func) - - @my_dec - def foo(a: 'foo'=None) -> 'bar': - return 'answer' - - compare(foo(), expected='the answer') - check_extract(foo, - expected_rq=requires(a='foo'), - expected_rt=returns('bar')) - - def test_decorator_trumps_annotations(self): - @requires('foo') - @returns('bar') - def foo(a: 'x') -> 'y': pass - check_extract(foo, - expected_rq=requires('foo'), - expected_rt=returns('bar')) - - def test_returns_mapping(self): - rt = returns_mapping() - def foo() -> rt: pass - check_extract(foo, - expected_rq=None, - expected_rt=rt) - - def test_returns_sequence(self): - rt = returns_sequence() - def foo() -> rt: pass - check_extract(foo, - expected_rq=None, - expected_rt=rt) - - def test_how_instance_in_annotations(self): - how = item('config', 'db_url') - def foo(a: how): pass - check_extract(foo, - expected_rq=requires(a=how), - expected_rt=None) - - def test_default_requirements(self): - def foo(a, b=1, *, c, d=None): pass - check_extract(foo, - expected_rq=requires('a', - optional('b'), - c='c', - d=optional('d')), - expected_rt=None) diff --git a/mush/tests/test_example_with_mush_clone_py3.py b/mush/tests/test_example_with_mush_clone_py3.py deleted file mode 100644 index eefcc22..0000000 --- a/mush/tests/test_example_with_mush_clone_py3.py +++ /dev/null @@ -1,96 +0,0 @@ -from .example_with_mush_clone import DatabaseHandler, main, do, setup_logging -from unittest import TestCase -from testfixtures import TempDirectory -from testfixtures import Replacer -from testfixtures import LogCapture -from testfixtures import ShouldRaise -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')] - ) - - # coverage.py says no test of branch to log.check call! - def test_do(self): - # setup db - conn = sqlite3.connect(':memory:') - conn.execute('create table notes (filename varchar, text varchar)') - conn.commit() - with TempDirectory() as d: - # setup file to read - source = d.write('test.txt', 'some text', 'ascii') - with LogCapture() as log: - # call the function under test - do(conn, source) # pragma: no branch (coverage.py bug) - # check results - self.assertEqual( - conn.execute('select * from notes').fetchall(), - [('test.txt', 'some text')] - ) - # check logging - log.check(('root', 'INFO', "Successfully added 'test.txt'")) - - def test_setup_logging(self): - with TempDirectory() as dir: - with LogCapture(): - setup_logging(dir.getpath('test.log'), verbose=True) - -class DatabaseHandlerTests(TestCase): - - def setUp(self): - self.dir = TempDirectory() - self.addCleanup(self.dir.cleanup) - 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() - self.addCleanup(self.log.uninstall) - - 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')) diff --git a/mush/tests/test_example_with_mush_factory_py3.py b/mush/tests/test_example_with_mush_factory_py3.py deleted file mode 100644 index 1687ac0..0000000 --- a/mush/tests/test_example_with_mush_factory_py3.py +++ /dev/null @@ -1,30 +0,0 @@ -from .example_with_mush_factory import main -from unittest import TestCase -from testfixtures import TempDirectory, Replacer -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')] - ) diff --git a/mush/tests/test_example_without_mush.py b/mush/tests/test_example_without_mush.py deleted file mode 100644 index afa4df2..0000000 --- a/mush/tests/test_example_without_mush.py +++ /dev/null @@ -1,74 +0,0 @@ -from .example_without_mush import main -from unittest import TestCase -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')] - ) - - def test_main_verbose(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]) - with OutputCapture() as output: - main() - output.compare("Successfully added 'test.txt'") - - def test_main_exception(self): - with TempDirectory() as d: - from testfixtures import OutputCapture - # setup db - db_path = d.getpath('sqlite.db') - conn = sqlite3.connect(db_path) - # don't create the table so we get at exception - 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('bad.txt', 'some text', 'ascii') - with Replacer() as r: - r.replace('sys.argv', ['script.py', config, source]) - with OutputCapture() as output: - main() - self.assertTrue('OperationalError' in output.captured, - output.captured) diff --git a/mush/tests/test_factory.py b/mush/tests/test_factory.py deleted file mode 100644 index 3657132..0000000 --- a/mush/tests/test_factory.py +++ /dev/null @@ -1,12 +0,0 @@ -from testfixtures import compare - -from mush import returns -from mush.factory import Factory -from mush.markers import Marker - -foo = Marker('foo') - - -def test_repr(): - f = Factory(foo, None, returns('foo')) - compare(repr(f), expected='>') diff --git a/mush/tests/test_marker.py b/mush/tests/test_marker.py deleted file mode 100644 index 3a60f74..0000000 --- a/mush/tests/test_marker.py +++ /dev/null @@ -1,6 +0,0 @@ -from mush.markers import Marker -from testfixtures import compare - - -def test_repr(): - compare(repr(Marker('foo')), expected='') diff --git a/mush/tests/test_plug.py b/mush/tests/test_plug.py deleted file mode 100644 index 6179679..0000000 --- a/mush/tests/test_plug.py +++ /dev/null @@ -1,236 +0,0 @@ -from unittest import TestCase - -from mock import Mock, call -from testfixtures import compare, ShouldRaise - -from mush import Plug, Runner, returns, requires -from mush.plug import insert, ignore, append -from mush.tests.test_runner import verify - - -class TestPlug(TestCase): - - def test_simple(self): - m = Mock() - - runner = Runner() - runner.add(m.job1, label='one') - runner.add(m.job2) - runner.add(m.job3, label='three') - runner.add(m.job4) - - class MyPlug(Plug): - - def one(self): - m.plug_one() - - def three(self): - m.plug_two() - - plug = MyPlug() - plug.add_to(runner) - - runner() - - compare([ - call.job1(), call.plug_one(), call.job2(), - call.job3(), call.plug_two(), call.job4() - ], m.mock_calls) - - verify(runner, - (m.job1, set()), - (plug.one, {'one'}), - (m.job2, set()), - (m.job3, set()), - (plug.three, {'three'}), - (m.job4, set()), - ) - - def test_label_not_there(self): - runner = Runner() - - class MyPlug(Plug): - def not_there(self): pass - - with ShouldRaise(KeyError('not_there')): - MyPlug().add_to(runner) - - def test_requirements_and_returns(self): - m = Mock() - - @returns('r1') - def job1(): - m.job1() - return 1 - - @requires('r2') - def job3(r): - m.job3(r) - - runner = Runner() - runner.add(job1, label='point') - runner.add(job3) - - class MyPlug(Plug): - - @requires('r1') - @returns('r2') - def point(self, r): - m.point(r) - return 2 - - plug = MyPlug() - plug.add_to(runner) - - runner() - - compare([ - call.job1(), call.point(1), call.job3(2), - ], m.mock_calls) - - verify(runner, - (job1, set()), - (plug.point, {'point'}), - (job3, set()), - ) - - def test_explict(self): - m = Mock() - - runner = Runner() - runner.add(m.job1, label='one') - - class MyPlug(Plug): - - explicit = True - - def helper(self): - m.plug_one() - - @insert() - def one(self): - self.helper() - - plug = MyPlug() - plug.add_to(runner) - - runner() - - compare([ - call.job1(), - call.plug_one() - ], actual=m.mock_calls) - - verify(runner, - (m.job1, set()), - (plug.one, {'one'}), - ) - - def test_ignore(self): - m = Mock() - - runner = Runner() - runner.add(m.job1, label='one') - - class MyPlug(Plug): - - @ignore() - def helper(self): # pragma: no cover - m.plug_bad() - - def one(self): - m.plug_good() - - plug = MyPlug() - plug.add_to(runner) - - runner() - - compare([ - call.job1(), - call.plug_good() - ], actual=m.mock_calls) - - verify(runner, - (m.job1, set()), - (plug.one, {'one'}), - ) - - def test_remap_name(self): - m = Mock() - - runner = Runner() - runner.add(m.job1, label='one') - - class MyPlug(Plug): - - @insert(label='one') - def run_plug(self): - m.plug_one() - - plug = MyPlug() - plug.add_to(runner) - - runner() - - compare([ - call.job1(), - call.plug_one() - ], m.mock_calls) - - verify(runner, - (m.job1, set()), - (plug.run_plug, {'one'}), - ) - - def test_append(self): - m = Mock() - - runner = Runner() - runner.add(m.job1, label='one') - - class MyPlug(Plug): - - @append() - def run_plug(self): - m.do_it() - - plug = MyPlug() - plug.add_to(runner) - - runner() - - compare([ - call.job1(), - call.do_it() - ], actual=m.mock_calls) - - verify(runner, - (m.job1, {'one'}), - (plug.run_plug, set()), - ) - - def test_add_plug(self): - m = Mock() - - runner = Runner() - runner.add(m.job1, label='one') - - class MyPlug(Plug): - def one(self): - m.plug_one() - - plug = MyPlug() - runner.add(plug) - - runner() - - compare([ - call.job1(), call.plug_one() - ], m.mock_calls) - - verify(runner, - (m.job1, set()), - (plug.one, {'one'}), - ) - diff --git a/mush/tests/test_runner.py b/mush/tests/test_runner.py deleted file mode 100644 index 5591f4e..0000000 --- a/mush/tests/test_runner.py +++ /dev/null @@ -1,1337 +0,0 @@ -from unittest import TestCase - -from mock import Mock, call -from testfixtures import ( - ShouldRaise, - compare -) - -from mush.context import ContextError -from mush.declarations import ( - requires, attr, item, nothing, returns, returns_mapping, lazy -) -from mush.runner import Runner - - -def verify(runner, *expected): - seen_labels = set() - - actual = [] - point = runner.start - while point: - actual.append((point.obj, point.labels)) - for label in point.labels: - if label in seen_labels: # pragma: no cover - raise AssertionError('%s occurs more than once' % label) - seen_labels.add(label) - compare(runner.labels[label], point) - point = point.next - - compare(expected=expected, actual=actual) - - actual_reverse = [] - point = runner.end - while point: - actual_reverse.append((point.obj, point.labels)) - point = point.previous - - compare(actual, reversed(actual_reverse)) - compare(seen_labels, runner.labels.keys()) - - -class RunnerTests(TestCase): - - def test_simple(self): - m = Mock() - def job(): - m.job() - - runner = Runner() - point = runner.add(job).callpoint - - compare(job, point.obj) - compare(runner.start, point) - compare(runner.end, point) - runner() - - compare([ - call.job() - ], m.mock_calls) - - verify(runner, (job, set())) - - def test_constructor(self): - m = Mock() - def job1(): - m.job1() - def job2(): - m.job2() - - runner = Runner(job1, job2) - compare(job1, runner.start.obj) - compare(job2, runner.end.obj) - - runner() - - compare([ - call.job1(), - call.job2(), - ], m.mock_calls) - - verify(runner, - (job1, set()), - (job2, set())) - - def test_return_value(self): - def job(): - return 42 - runner = Runner(job) - compare(runner(), 42) - - def test_return_value_empty(self): - runner = Runner() - compare(runner(), None) - - def test_add_with_label(self): - def job1(): pass - def job2(): pass - - runner = Runner() - - point1 = runner.add(job1, label='1').callpoint - point2 = runner.add(job2, label='2').callpoint - - compare(point1.obj, job1) - compare(point2.obj, job2) - - compare(runner['1'].callpoint, point1) - compare(runner['2'].callpoint, point2) - - compare({'1'}, point1.labels) - compare({'2'}, point2.labels) - - verify(runner, - (job1, {'1'}), - (job2, {'2'})) - - def test_modifier_add_moves_label(self): - def job1(): pass - def job2(): pass - - runner = Runner() - - runner.add(job1, label='the label') - runner['the label'].add(job2) - - verify(runner, - (job1, set()), - (job2, {'the label'})) - - def test_runner_add_does_not_move_label(self): - def job1(): pass - def job2(): pass - - runner = Runner() - - runner.add(job1, label='the label') - runner.add(job2) - - verify(runner, - (job1, {'the label'}), - (job2, set())) - - def test_modifier_moves_only_explicit_label(self): - def job1(): pass - def job2(): pass - - runner = Runner() - - mod = runner.add(job1) - mod.add_label('1') - mod.add_label('2') - - verify(runner, - (job1, {'1', '2'})) - - runner['2'].add(job2) - - verify(runner, - (job1, {'1'}), - (job2, {'2'})) - - def test_modifier_add_with_label(self): - def job1(): pass - def job2(): pass - - runner = Runner() - - mod = runner.add(job1) - mod.add_label('1') - - runner['1'].add(job2, label='2') - - verify(runner, - (job1, {'1'}), - (job2, {'2'})) - - def test_runner_add_label(self): - m = Mock() - - runner = Runner() - runner.add(m.job1) - runner.add_label('label') - runner.add(m.job3) - - runner['label'].add(m.job2) - - verify( - runner, - (m.job1, set()), - (m.job2, {'label'}), - (m.job3, set()) - ) - - cloned = runner.clone(added_using='label') - verify( - cloned, - (m.job2, {'label'}), - ) - - def test_declarative(self): - m = Mock() - class T1(object): pass - class T2(object): pass - - t1 = T1() - t2 = T2() - - def job1(): - m.job1() - return t1 - - @requires(T1) - def job2(obj): - m.job2(obj) - return t2 - - @requires(T2) - def job3(obj): - m.job3(obj) - - runner = Runner(job1, job2, job3) - runner() - - compare([ - call.job1(), - call.job2(t1), - call.job3(t2), - ], m.mock_calls) - - def test_imperative(self): - m = Mock() - class T1(object): pass - class T2(object): pass - - t1 = T1() - t2 = T2() - - def job1(): - m.job1() - return t1 - - def job2(obj): - m.job2(obj) - return t2 - - def job3(t2_): - m.job3(t2_) - - # imperative config trumps declarative - @requires(T1) - def job4(t2_): - m.job4(t2_) - - runner = Runner() - runner.add(job1) - runner.add(job2, requires(T1)) - runner.add(job3, requires(t2_=T2)) - runner.add(job4, requires(T2)) - runner() - - compare([ - call.job1(), - call.job2(t1), - call.job3(t2), - call.job4(t2), - ], m.mock_calls) - - def test_returns_type_mapping(self): - m = Mock() - class T1(object): pass - class T2(object): pass - t = T1() - - @returns_mapping() - def job1(): - m.job1() - return {T2:t} - - @requires(T2) - def job2(obj): - m.job2(obj) - - Runner(job1, job2)() - - compare([ - call.job1(), - call.job2(t), - ], m.mock_calls) - - def test_returns_type_mapping_of_none(self): - m = Mock() - class T2(object): pass - - @returns_mapping() - def job1(): - m.job1() - return {T2:None} - - @requires(T2) - def job2(obj): - m.job2(obj) - - Runner(job1, job2)() - - compare([ - call.job1(), - call.job2(None), - ], m.mock_calls) - - def test_returns_tuple(self): - m = Mock() - class T1(object): pass - class T2(object): pass - - t1 = T1() - t2 = T2() - - @returns(T1, T2) - def job1(): - m.job1() - return t1, t2 - - @requires(T1, T2) - def job2(obj1, obj2): - m.job2(obj1, obj2) - - Runner(job1, job2)() - - compare([ - call.job1(), - call.job2(t1, t2), - ], m.mock_calls) - - def test_returns_list(self): - m = Mock() - class T1(object): pass - class T2(object): pass - - t1 = T1() - t2 = T2() - - def job1(): - m.job1() - return [t1, t2] - - @requires(obj1=T1, obj2=T2) - def job2(obj1, obj2): - m.job2(obj1, obj2) - - runner = Runner() - runner.add(job1, returns=returns(T1, T2)) - runner.add(job2) - runner() - - compare([ - call.job1(), - call.job2(t1, t2), - ], m.mock_calls) - - def test_return_type_specified_decorator(self): - m = Mock() - class T1(object): pass - class T2(object): pass - t = T1() - - @returns(T2) - def job1(): - m.job1() - return t - - @requires(T2) - def job2(obj): - m.job2(obj) - - Runner(job1, job2)() - - compare([ - call.job1(), - call.job2(t), - ], m.mock_calls) - - def test_return_type_specified_imperative(self): - m = Mock() - class T1(object): pass - class T2(object): pass - t = T1() - - def job1(): - m.job1() - return t - - @requires(T2) - def job2(obj): - m.job2(obj) - - runner = Runner() - runner.add(job1, returns=returns(T2)) - runner.add(job2, requires(T2)) - runner() - - compare([ - call.job1(), - call.job2(t), - ], m.mock_calls) - - def test_lazy_decorator(self): - m = Mock() - class T1(object): pass - class T2(object): pass - t = T1() - - @lazy - @returns(T1) - def lazy_used(): - m.lazy_used() - return t - - @lazy - @returns(T2) - def lazy_unused(): - raise AssertionError('should not be called') # pragma: no cover - - @requires(T1) - def job(obj): - m.job(obj) - - runner = Runner(lazy_used, lazy_unused, job) - runner() - - compare(m.mock_calls, expected=[ - call.lazy_used(), - call.job(t), - ], ) - - def test_lazy_imperative(self): - m = Mock() - class T1(object): pass - class T2(object): pass - t = T1() - - def lazy_used(): - m.lazy_used() - return t - - def lazy_unused(): - raise AssertionError('should not be called') # pragma: no cover - - def job(obj): - m.job(obj) - - runner = Runner() - runner.add(lazy_used, returns=returns(T1), lazy=True) - runner.add(lazy_unused, returns=returns(T2), lazy=True) - runner.add(job, requires(T1)) - runner() - - compare(m.mock_calls, expected=[ - call.lazy_used(), - call.job(t), - ], ) - - def test_lazy_no_return_type_specified(self): - runner = Runner() - with ShouldRaise( - TypeError('a single return type must be explicitly specified') - ): - runner.add(lambda: None, lazy=True) - - def test_returns_more_than_one_type(self): - class T1(object): pass - class T2(object): pass - runner = Runner() - with ShouldRaise( - TypeError('a single return type must be explicitly specified') - ): - runner.add(lambda: None, returns=returns(T1, T2), lazy=True) - - def test_lazy_per_context(self): - m = Mock() - class T1(object): pass - t = T1() - - def lazy(): - m.lazy_used() - return t - - def job(obj): - m.job(obj) - - runner = Runner() - runner.add(lazy, returns=returns(T1), lazy=True) - runner.add(job, requires(T1)) - runner() - runner() - - compare(m.mock_calls, expected=[ - call.lazy_used(), - call.job(t), - call.lazy_used(), - call.job(t), - ], ) - - def test_missing_from_context_no_chain(self): - class T(object): pass - - @requires(T) - def job(arg): - pass # pragma: nocover - - runner = Runner(job) - - with ShouldRaise(ContextError) as s: - runner() - - text = '\n'.join(( - 'While calling: '+repr(job)+' requires(T) returns_result_type()', - 'with :', - '', - 'No '+repr(T)+' in context', - )) - compare(text, repr(s.raised)) - compare(text, str(s.raised)) - - def test_missing_from_context_with_chain(self): - class T(object): pass - - def job1(): pass - def job2(): pass - - @requires(T) - def job3(arg): - pass # pragma: nocover - - def job4(): pass - def job5(): pass - - runner = Runner() - runner.add(job1, label='1') - runner.add(job2) - runner.add(job3) - runner.add(job4, label='4') - runner.add(job5, requires('foo', bar='baz'), returns('bob')) - - with ShouldRaise(ContextError) as s: - runner() - - text = '\n'.join(( - '', - '', - 'Already called:', - repr(job1)+' requires() returns_result_type() <-- 1', - repr(job2)+' requires() returns_result_type()', - '', - 'While calling: '+repr(job3)+' requires(T) returns_result_type()', - 'with :', - '', - 'No '+repr(T)+' in context', - '', - 'Still to call:', - repr(job4)+' requires() returns_result_type() <-- 4', - repr(job5)+" requires('foo', bar='baz') returns('bob')", - )) - compare(text, repr(s.raised)) - compare(text, str(s.raised)) - - def test_job_called_badly(self): - def job(arg): - pass # pragma: nocover - runner = Runner(job) - with ShouldRaise(ContextError) as s: - runner() - compare(s.raised.text, expected="No 'arg' in context") - - def test_already_in_context(self): - class T(object): pass - - t1 = T() - - @returns(T, T) - def job(): - return t1, T() - - runner = Runner(job) - - with ShouldRaise(ContextError) as s: - runner() - - text = '\n'.join(( - 'While calling: '+repr(job)+' requires() returns(T, T)', - 'with :', - '', - 'Context already contains '+repr(T), - )) - compare(text, repr(s.raised)) - compare(text, str(s.raised)) - - def test_job_error(self): - def job(): - raise Exception('huh?') - runner = Runner(job) - with ShouldRaise(Exception('huh?')): - runner() - - def test_attr(self): - class T(object): - foo = 'bar' - m = Mock() - def job1(): - m.job1() - return T() - def job2(obj): - m.job2(obj) - runner = Runner() - runner.add(job1) - runner.add(job2, requires(attr(T, 'foo'))) - runner() - - compare([ - call.job1(), - call.job2('bar'), - ], m.mock_calls) - - def test_attr_multiple(self): - class T2: - bar = 'baz' - class T: - foo = T2() - - m = Mock() - def job1(): - m.job1() - return T() - def job2(obj): - m.job2(obj) - runner = Runner() - runner.add(job1) - runner.add(job2, requires(attr(T, 'foo', 'bar'))) - runner() - - compare([ - call.job1(), - call.job2('baz'), - ], m.mock_calls) - - def test_item(self): - class MyDict(dict): pass - m = Mock() - def job1(): - m.job1() - obj = MyDict() - obj['the_thing'] = m.the_thing - return obj - def job2(obj): - m.job2(obj) - runner = Runner() - runner.add(job1) - runner.add(job2, requires(item(MyDict, 'the_thing'))) - runner() - compare([ - call.job1(), - call.job2(m.the_thing), - ], m.mock_calls) - - def test_item_multiple(self): - class MyDict(dict): pass - m = Mock() - def job1(): - m.job1() - obj = MyDict() - obj['the_thing'] = dict(other_thing=m.the_thing) - return obj - def job2(obj): - m.job2(obj) - runner = Runner() - runner.add(job1) - runner.add(job2, requires(item(MyDict, 'the_thing', 'other_thing'))) - runner() - compare([ - call.job1(), - call.job2(m.the_thing), - ], m.mock_calls) - - def test_nested(self): - class T(object): - foo = dict(baz='bar') - m = Mock() - def job1(): - m.job1() - return T() - def job2(obj): - m.job2(obj) - runner = Runner() - runner.add(job1) - runner.add(job2, requires(item(attr(T, 'foo'), 'baz'))) - runner() - - compare([ - call.job1(), - call.job2('bar'), - ], m.mock_calls) - - def test_context_manager(self): - m = Mock() - - class CM1(object): - def __enter__(self): - m.cm1.enter() - return self - def __exit__(self, type, obj, tb): - m.cm1.exit(type, obj) - return True - - class CM2Context(object): pass - - class CM2(object): - def __enter__(self): - m.cm2.enter() - return CM2Context() - - def __exit__(self, type, obj, tb): - m.cm2.exit(type, obj) - - @requires(CM1) - def func1(obj): - m.func1(type(obj)) - - @requires(CM1, CM2, CM2Context) - def func2(obj1, obj2, obj3): - m.func2(type(obj1), - type(obj2), - type(obj3)) - return '2' - - runner = Runner( - CM1, - CM2, - func1, - func2, - ) - - result = runner() - compare(result, '2') - - compare([ - call.cm1.enter(), - call.cm2.enter(), - call.func1(CM1), - call.func2(CM1, CM2, CM2Context), - call.cm2.exit(None, None), - call.cm1.exit(None, None) - ], m.mock_calls) - - # now check with an exception - m.reset_mock() - m.func2.side_effect = e = Exception() - result = runner() - - # if something goes wrong, you get None - compare(None, result) - - compare([ - call.cm1.enter(), - call.cm2.enter(), - call.func1(CM1), - call.func2(CM1, CM2, CM2Context), - call.cm2.exit(Exception, e), - call.cm1.exit(Exception, e) - ], m.mock_calls) - - def test_marker_interfaces(self): - # return {Type:None} - # don't pass when a requirement is for a type but value is None - class Marker(object): pass - - m = Mock() - - def setup(): - m.setup() - return {Marker: nothing} - - @requires(Marker) - def use(): - m.use() - - runner = Runner() - runner.add(setup, returns=returns_mapping(), label='setup') - runner['setup'].add(use) - runner() - - compare([ - call.setup(), - call.use(), - ], m.mock_calls) - - def test_clone(self): - m = Mock() - class T1(object): pass - class T2(object): pass - def f1(): m.f1() - def n1(): - m.n1() - return T1(), T2() - def l1(): m.l1() - def t1(obj): m.t1() - def t2(obj): m.t2() - # original - runner1 = Runner() - runner1.add(f1, label='first') - runner1.add(n1, returns=returns(T1, T2), label='normal') - runner1.add(l1, label='last') - runner1.add(t1, requires(T1)) - runner1.add(t2, requires(T2)) - # now clone and add bits - def f2(): m.f2() - def n2(): m.n2() - def l2(): m.l2() - def tn(obj): m.tn() - runner2 = runner1.clone() - runner2['first'].add(f2) - runner2['normal'].add(n2) - runner2['last'].add(l2) - # make sure types stay in order - runner2.add(tn, requires(T2)) - - # now run both, and make sure we only get what we should - - runner1() - verify(runner1, - (f1, {'first'}), - (n1, {'normal'}), - (l1, {'last'}), - (t1, set()), - (t2, set()), - ) - compare([ - call.f1(), - call.n1(), - call.l1(), - call.t1(), - call.t2(), - ], m.mock_calls) - - m.reset_mock() - - runner2() - verify(runner2, - (f1, set()), - (f2, {'first'}), - (n1, set()), - (n2, {'normal'}), - (l1, set()), - (l2, {'last'}), - (t1, set()), - (t2, set()), - (tn, set()), - ) - compare([ - call.f1(), - call.f2(), - call.n1(), - call.n2(), - call.l1(), - call.l2(), - call.t1(), - call.t2(), - call.tn() - ], m.mock_calls) - - def test_clone_end_label(self): - m = Mock() - runner1 = Runner() - runner1.add(m.f1, label='first') - runner1.add(m.f2, label='second') - runner1.add(m.f3, label='third') - - runner2 = runner1.clone(end_label='third') - verify(runner2, - (m.f1, {'first'}), - (m.f2, {'second'}), - ) - - def test_clone_end_label_include(self): - m = Mock() - runner1 = Runner() - runner1.add(m.f1, label='first') - runner1.add(m.f2, label='second') - runner1.add(m.f3, label='third') - - runner2 = runner1.clone(end_label='second', include_end=True) - verify(runner2, - (m.f1, {'first'}), - (m.f2, {'second'}), - ) - - def test_clone_start_label(self): - m = Mock() - runner1 = Runner() - runner1.add(m.f1, label='first') - runner1.add(m.f2, label='second') - runner1.add(m.f3, label='third') - - runner2 = runner1.clone(start_label='first') - verify(runner2, - (m.f2, {'second'}), - (m.f3, {'third'}), - ) - - def test_clone_start_label_include(self): - m = Mock() - runner1 = Runner() - runner1.add(m.f1, label='first') - runner1.add(m.f2, label='second') - runner1.add(m.f3, label='third') - - runner2 = runner1.clone(start_label='second', include_start=True) - verify(runner2, - (m.f2, {'second'}), - (m.f3, {'third'}), - ) - - def test_clone_between(self): - m = Mock() - runner1 = Runner() - runner1.add(m.f1, label='first') - runner1.add(m.f2, label='second') - runner1.add(m.f3, label='third') - runner1.add(m.f4, label='fourth') - - runner2 = runner1.clone(start_label='first', end_label='fourth') - verify(runner2, - (m.f2, {'second'}), - (m.f3, {'third'}), - ) - - def test_clone_between_one_item(self): - m = Mock() - runner1 = Runner() - runner1.add(m.f1, label='first') - runner1.add(m.f2, label='second') - runner1.add(m.f3, label='third') - - runner2 = runner1.clone(start_label='first', end_label='third') - verify(runner2, - (m.f2, {'second'}), - ) - - def test_clone_between_empty(self): - m = Mock() - runner1 = Runner() - runner1.add(m.f1, label='first') - runner1.add(m.f2, label='second') - - runner2 = runner1.clone(start_label='first', end_label='second') - verify(runner2) - - def test_clone_added_using(self): - runner1 = Runner() - m = Mock() - runner1.add(m.f1) - runner1.add(m.f2, label='the_label') - runner1.add(m.f3) - - runner1['the_label'].add(m.f6) - runner1['the_label'].add(m.f7) - - runner2 = runner1.clone(added_using='the_label') - verify(runner2, - (m.f6, set()), - (m.f7, {'the_label'}), - ) - - def test_extend(self): - m = Mock() - class T1(object): pass - class T2(object): pass - - t1 = T1() - t2 = T2() - - def job1(): - m.job1() - return t1 - - @requires(T1) - def job2(obj): - m.job2(obj) - return t2 - - @requires(T2) - def job3(obj): - m.job3(obj) - - runner = Runner() - runner.extend(job1, job2, job3) - runner() - - compare([ - call.job1(), - call.job2(t1), - call.job3(t2), - ], m.mock_calls) - - def test_addition(self): - m = Mock() - - def job1(): - m.job1() - - def job2(): - m.job2() - - def job3(): - m.job3() - - runner1 = Runner(job1, job2) - runner2 = Runner(job3) - runner = runner1 + runner2 - runner() - - verify(runner, - (job1, set()), - (job2, set()), - (job3, set()), - ) - compare([ - call.job1(), - call.job2(), - call.job3(), - ], m.mock_calls) - - def test_extend_with_runners(self): - m = Mock() - class T1(object): pass - class T2(object): pass - - t1 = T1() - t2 = T2() - - def job1(): - m.job1() - return t1 - - @requires(T1) - def job2(obj): - m.job2(obj) - return t2 - - @requires(T2) - def job3(obj): - m.job3(obj) - - runner1 = Runner(job1) - runner2 = Runner(job2) - runner3 = Runner(job3) - - runner = Runner(runner1) - runner.extend(runner2, runner3) - runner() - - verify(runner, - (job1, set()), - (job2, set()), - (job3, set()), - ) - compare([ - call.job1(), - call.job2(t1), - call.job3(t2), - ], m.mock_calls) - - def test_replace_for_testing(self): - m = Mock() - class T1(object): pass - class T2(object): pass - - t1 = T1() - t2 = T2() - - def job1(): - raise Exception() # pragma: nocover - - @requires(T1) - def job2(obj): - raise Exception() # pragma: nocover - - @requires(T2) - def job3(obj): - raise Exception() # pragma: nocover - - runner = Runner(job1, job2, job3) - runner.replace(job1, m.job1) - m.job1.return_value = t1 - runner.replace(job2, m.job2) - m.job2.return_value = t2 - runner.replace(job3, m.job3) - runner() - - compare([ - call.job1(), - call.job2(t1), - call.job3(t2), - ], m.mock_calls) - - def test_replace_for_behaviour(self): - m = Mock() - class T1(object): pass - class T2(object): pass - class T3(object): pass - class T4(object): pass - - t2 = T2() - def job0(): - return t2 - - @requires(T1) - @returns(T3) - def job1(obj): - raise Exception() # pragma: nocover - - job2 = requires(T4)(m.job2) - runner = Runner(job0, job1, job2) - - runner.replace(job1, requires(T2)(returns(T4)(m.job1))) - runner() - - compare([ - call.job1(t2), - call.job2(m.job1.return_value), - ], actual=m.mock_calls) - - def test_replace_explicit_requires_returns(self): - m = Mock() - class T1(object): pass - class T2(object): pass - class T3(object): pass - class T4(object): pass - - t2 = T2() - def job0(): - return t2 - - @requires(T1) - @returns(T3) - def job1(obj): - raise Exception() # pragma: nocover - - job2 = requires(T4)(m.job2) - runner = Runner(job0, job1, job2) - - runner.replace(job1, m.job1, requires=T2, returns=T4) - runner() - - compare([ - call.job1(t2), - call.job2(m.job1.return_value), - ], actual=m.mock_calls) - - def test_replace_explicit_with_labels(self): - m = Mock() - - runner = Runner(m.job0) - runner.add_label('foo') - runner['foo'].add(m.job1) - runner['foo'].add(m.job2) - - runner.replace(m.job2, m.jobnew, returns='mock') - - runner() - - compare([ - call.job0(), - call.job1(), - call.jobnew() - ], m.mock_calls) - - # check added_using is handled correctly - m.reset_mock() - runner2 = runner.clone(added_using='foo') - runner2() - - compare([ - call.job1(), - call.jobnew() - ], actual=m.mock_calls) - - # check runner's label pointer is sane - m.reset_mock() - runner['foo'].add(m.job3) - runner() - - compare([ - call.job0(), - call.job1(), - call.jobnew(), - call.job3() - ], actual=m.mock_calls) - - def test_replace_explicit_at_start(self): - m = Mock() - runner = Runner(m.job1, m.job2) - - runner.replace(m.job1, m.jobnew, returns='mock') - runner() - - compare([ - call.jobnew(), - call.job2(), - ], actual=m.mock_calls) - - def test_replace_explicit_at_end(self): - m = Mock() - runner = Runner(m.job1, m.job2) - - runner.replace(m.job2, m.jobnew, returns='mock') - runner.add(m.jobnew2) - runner() - - compare([ - call.job1(), - call.jobnew(), - call.jobnew2(), - ], actual=m.mock_calls) - - def test_replace_keep_explicit_requirements(self): - def foo(): - return 'bar' - def barbar(sheep): - return sheep*2 - - runner = Runner() - runner.add(foo, returns='flossy') - runner.add(barbar, requires='flossy') - compare(runner(), expected='barbar') - - runner.replace(barbar, lambda dog: None) - compare(runner(), expected=None) - - def test_modifier_changes_endpoint(self): - m = Mock() - runner = Runner(m.job1) - compare(runner.end.obj, m.job1) - verify(runner, - (m.job1, set()), - ) - - mod = runner.add(m.job2, label='foo') - compare(runner.end.obj, m.job2) - verify(runner, - (m.job1, set()), - (m.job2, {'foo'}), - ) - - mod.add(m.job3) - compare(runner.end.obj, m.job3) - compare(runner.end.labels, {'foo'}) - verify(runner, - (m.job1, set()), - (m.job2, set()), - (m.job3, {'foo'}), - ) - - runner.add(m.job4) - compare(runner.end.obj, m.job4) - compare(runner.end.labels, set()) - verify(runner, - (m.job1, set()), - (m.job2, set()), - (m.job3, {'foo'}), - (m.job4, set()), - ) - - def test_duplicate_label_runner_add(self): - m = Mock() - runner = Runner() - runner.add(m.job1, label='label') - runner.add(m.job2) - with ShouldRaise(ValueError( - "'label' already points to "+repr(m.job1)+" requires() " - "returns_result_type() <-- label" - )): - runner.add(m.job3, label='label') - verify(runner, - (m.job1, {'label'}), - (m.job2, set()), - ) - - def test_duplicate_label_runner_next_add(self): - m = Mock() - runner = Runner() - runner.add(m.job1, label='label') - with ShouldRaise(ValueError( - "'label' already points to "+repr(m.job1)+" requires() " - "returns_result_type() <-- label" - )): - runner.add(m.job2, label='label') - verify(runner, - (m.job1, {'label'}), - ) - - def test_duplicate_label_modifier(self): - m = Mock() - runner = Runner() - runner.add(m.job1, label='label1') - mod = runner['label1'] - mod.add(m.job2, label='label2') - with ShouldRaise(ValueError( - "'label1' already points to "+repr(m.job1)+" requires() " - "returns_result_type() <-- label1" - )): - mod.add(m.job3, label='label1') - verify(runner, - (m.job1, {'label1'}), - (m.job2, {'label2'}), - ) - - def test_repr(self): - class T1: pass - class T2: pass - m = Mock() - runner = Runner() - runner.add(m.job1, label='label1') - runner.add(m.job2, requires('foo', T1), returns(T2), label='label2') - runner.add(m.job3) - - compare('\n'.join(( - '', - ' '+repr(m.job1)+' requires() returns_result_type() <-- label1', - ' '+repr(m.job2)+" requires('foo', T1) returns(T2) <-- label2", - ' '+repr(m.job3)+' requires() returns_result_type()', - '' - - )), repr(runner)) - - def test_repr_empty(self): - compare('', repr(Runner())) diff --git a/mush/typing.py b/mush/typing.py new file mode 100644 index 0000000..95d2f9f --- /dev/null +++ b/mush/typing.py @@ -0,0 +1,7 @@ +from typing import Generator, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from .paradigms import Call + + +Calls = Generator['Call', Any, None] diff --git a/setup.cfg b/setup.cfg index 3648c65..03b03d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[wheel] -universal = 1 - [tool:pytest] addopts = --verbose --strict norecursedirs=functional .git docs/_build diff --git a/setup.py b/setup.py index 994c11c..3c8db51 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='mush', - version='2.8.1', + version='3.0.0a1', author='Chris Withers', author_email='chris@simplistix.co.uk', license='MIT', @@ -21,17 +21,24 @@ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], packages=find_packages(), zip_safe=False, include_package_data=True, + python_requires='>=3.6', extras_require=dict( - test=['pytest', 'pytest-cov', 'mock', 'sybil', 'testfixtures'], + test=[ + 'mock', + 'pytest', + 'pytest-asyncio', + 'pytest-cov', + 'sybil', + 'testfixtures>=6.14.1' + ], build=['sphinx', 'setuptools-git', 'wheel', 'twine'] )) diff --git a/mush/tests/__init__.py b/tests/__init__.py similarity index 100% rename from mush/tests/__init__.py rename to tests/__init__.py diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 0000000..640cfd1 --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,13 @@ +from testfixtures import compare + +from mush.context import Context + + +class TestCall: + + def test_no_params(self): + def foo(): + return 'bar' + context = Context() + result = context.call(foo) + compare(result, 'bar') diff --git a/tests/test_paradigm_asyncio.py b/tests/test_paradigm_asyncio.py new file mode 100644 index 0000000..c405e79 --- /dev/null +++ b/tests/test_paradigm_asyncio.py @@ -0,0 +1,73 @@ +import asyncio +from contextlib import contextmanager +from functools import partial +from unittest.mock import Mock + +import pytest +from testfixtures import compare + +from mush import Context +from mush.paradigms import Call +from mush import paradigms +from mush.paradigms.asyncio_ import AsyncIO + + +@contextmanager +def no_threads(): + loop = asyncio.get_event_loop() + original = loop.run_in_executor + loop.run_in_executor = Mock(side_effect=Exception('threads used when they should not be')) + try: + yield + finally: + loop.run_in_executor = original + + +@contextmanager +def must_run_in_thread(*expected): + seen = set() + loop = asyncio.get_event_loop() + original = loop.run_in_executor + + def recording_run_in_executor(executor, func, *args): + if isinstance(func, partial): + to_record = func.func + else: + # get the underlying method for bound methods: + to_record = getattr(func, '__func__', func) + seen.add(to_record) + return original(executor, func, *args) + + loop.run_in_executor = recording_run_in_executor + try: + yield + finally: + loop.run_in_executor = original + + not_seen = set(expected) - seen + assert not not_seen, f'{not_seen} not run in a thread, seen: {seen}' + + +class TestContext: + + @pytest.mark.asyncio + async def test_call_is_async(self): + context = Context(paradigm=paradigms.asyncio) + + def it(): + return 'bar' + + result = context.call(it) + assert asyncio.iscoroutine(result) + with must_run_in_thread(it): + compare(await result, expected='bar') + + @pytest.mark.asyncio + async def test_call_async(self): + context = Context() + + async def it(): + return 'bar' + + with no_threads(): + compare(await context.call(it), expected='bar') diff --git a/tests/test_paradigm_normal.py b/tests/test_paradigm_normal.py new file mode 100644 index 0000000..0767bd5 --- /dev/null +++ b/tests/test_paradigm_normal.py @@ -0,0 +1,45 @@ +from unittest.mock import Mock + +from testfixtures import compare + +from mush.paradigms import Call +from mush.paradigms.normal_ import Normal + + +class TestParadigm: + + def test_claim(self): + # Since this is the "backstop" paradigm, it always claims things + p = Normal() + assert p.claim(lambda x: None) + + def test_process_single(self): + obj = Mock() + + def calls(): + yield Call(obj, ('a',), {'b': 'c'}) + + p = Normal() + + compare(p.process(calls()), expected=obj.return_value) + + obj.assert_called_with('a', b='c') + + def test_process_multiple(self): + mocks = Mock() + + results = [] + + def calls(): + results.append((yield Call(mocks.obj1, ('a',), {}))) + results.append((yield Call(mocks.obj2, ('b',), {}))) + yield Call(mocks.obj3, ('c',), {}) + + p = Normal() + + compare(p.process(calls()), expected=mocks.obj3.return_value) + + compare(results, expected=[ + mocks.obj1.return_value, + mocks.obj2.return_value, + ]) diff --git a/tests/test_paradigms.py b/tests/test_paradigms.py new file mode 100644 index 0000000..ba3fc5b --- /dev/null +++ b/tests/test_paradigms.py @@ -0,0 +1,63 @@ +from testfixtures import ShouldRaise +from testfixtures.mock import Mock + +from mush.paradigms import Paradigms + + +class TestCollection: + + def test_register_not_importable(self): + p = Paradigms() + obj = p.register_if_possible('mush.badname', 'ParadigmClass') + + with ShouldRaise(ModuleNotFoundError("No module named 'mush.badname'")): + obj.claim(lambda: None) + + with ShouldRaise(ModuleNotFoundError("No module named 'mush.badname'")): + obj.process((o for o in [])) + + def test_register_class_missing(self): + p = Paradigms() + with ShouldRaise(AttributeError( + "module 'mush.paradigms.normal_' has no attribute 'BadName'" + )): + p.register_if_possible('mush.paradigms.normal_', 'BadName') + + def test_shifter_not_importable(self): + p1 = Mock() + p2 = Mock() + p = Paradigms() + p.register(p1) + p.add_shifter_if_possible(p1, p2, 'mush.badname', 'shifter') + + caller = p.find_caller(lambda: None, target_paradigm=p2) + with ShouldRaise(ModuleNotFoundError("No module named 'mush.badname'")): + caller() + + def test_shifter_callable_missing(self): + p = Paradigms() + with ShouldRaise(AttributeError( + "module 'mush.paradigms.normal_' has no attribute 'bad_name'" + )): + p.add_shifter_if_possible(Mock(), Mock(), 'mush.paradigms.normal_', 'bad_name') + + def test_find_paradigm(self): + p1 = Mock() + p2 = Mock() + p = Paradigms() + p.register(p1) + p.register(p2) + + assert p.find_paradigm(lambda: None) is p2 + + p2.claim.return_value = False + + assert p.find_paradigm(lambda: None) is p1 + + def test_no_paradigm_claimed(self): + p_ = Mock() + p_.claim.return_value = False + p = Paradigms() + p.register(p_) + with ShouldRaise(Exception('No paradigm')): + p.find_paradigm(lambda: None)