From 4154d7e3d839456c047fe9b8ce109cfbc6663715 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 May 2017 18:27:27 +0200 Subject: [PATCH 1/6] Starting to work on #47 a.k.a the context processor mess. This is a first implementation removing all the uggly function calls, but further work must work on less things called "context", as it is bad for readability. Concerns better separated now. --- bonobo/basics.py | 5 +- bonobo/config/configurables.py | 21 +++-- bonobo/config/processors.py | 79 ++++++++++++++++--- bonobo/execution/base.py | 55 +++++-------- bonobo/execution/node.py | 2 +- bonobo/ext/opendatasoft.py | 1 - bonobo/io/csv.py | 2 - bonobo/io/file.py | 2 - bonobo/io/json.py | 1 - tests/test_basics.py | 27 +++++++ ...rocessors.py => test_config_processors.py} | 27 ++++--- tests/test_execution.py | 5 +- 12 files changed, 152 insertions(+), 75 deletions(-) create mode 100644 tests/test_basics.py rename tests/{context/test_processors.py => test_config_processors.py} (62%) diff --git a/bonobo/basics.py b/bonobo/basics.py index 97282ab..03a1232 100644 --- a/bonobo/basics.py +++ b/bonobo/basics.py @@ -3,7 +3,7 @@ from pprint import pprint as _pprint from colorama import Fore, Style -from bonobo.config.processors import contextual +from bonobo.config.processors import ContextProcessor from bonobo.structs.bags import Bag from bonobo.util.objects import ValueHolder from bonobo.util.term import CLEAR_EOL @@ -49,12 +49,11 @@ def Tee(f): return wrapped -@contextual def count(counter, *args, **kwargs): counter += 1 -@count.add_context_processor +@ContextProcessor.decorate(count) def _count_counter(self, context): counter = ValueHolder(0) yield counter diff --git a/bonobo/config/configurables.py b/bonobo/config/configurables.py index 6be2b29..c1486ee 100644 --- a/bonobo/config/configurables.py +++ b/bonobo/config/configurables.py @@ -1,3 +1,4 @@ +from bonobo.config.processors import ContextProcessor from bonobo.config.options import Option __all__ = [ @@ -15,16 +16,24 @@ class ConfigurableMeta(type): super().__init__(what, bases, dict) cls.__options__ = {} cls.__positional_options__ = [] + cls.__processors__ = [] for typ in cls.__mro__: for name, value in typ.__dict__.items(): if isinstance(value, Option): - if not value.name: - value.name = name - if not name in cls.__options__: - cls.__options__[name] = value - if value.positional: - cls.__positional_options__.append(name) + if isinstance(value, ContextProcessor): + cls.__processors__.append(value) + else: + if not value.name: + value.name = name + if not name in cls.__options__: + cls.__options__[name] = value + if value.positional: + cls.__positional_options__.append(name) + + # This can be done before, more efficiently. Not so bad neither as this is only done at type() creation time + # (aka class Xxx(...) time) and there should not be hundreds of processors. Still not very elegant. + cls.__processors__ = sorted(cls.__processors__, key=lambda v: v._creation_counter) class Configurable(metaclass=ConfigurableMeta): diff --git a/bonobo/config/processors.py b/bonobo/config/processors.py index 9dac6d0..2d3de8f 100644 --- a/bonobo/config/processors.py +++ b/bonobo/config/processors.py @@ -2,24 +2,23 @@ import functools import types -from bonobo.util.compat import deprecated_alias +from bonobo.util.compat import deprecated_alias, deprecated + +from bonobo.config.options import Option +from bonobo.util.iterators import ensure_tuple _CONTEXT_PROCESSORS_ATTR = '__processors__' -class ContextProcessor: - _creation_counter = 0 - +class ContextProcessor(Option): @property def __name__(self): return self.func.__name__ def __init__(self, func): self.func = func - - # This hack is necessary for python3.5 - self._creation_counter = ContextProcessor._creation_counter - ContextProcessor._creation_counter += 1 + super(ContextProcessor, self).__init__(required=False, default=self.__name__) + self.name = self.__name__ def __repr__(self): return repr(self.func).replace(' int: return i**2 -@contextual def push_result(results, i: int): results.append(i) -@push_result.__processors__.append +@ContextProcessor.decorate(push_result) def results(f, context): results = [] yield results From 743de17c4c7a27e6c64ed1203b50c4d06361e0cb Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 May 2017 18:35:12 +0200 Subject: [PATCH 2/6] mock is in unittest --- tests/test_basicusage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basicusage.py b/tests/test_basicusage.py index 1cd84a2..a43831d 100644 --- a/tests/test_basicusage.py +++ b/tests/test_basicusage.py @@ -2,7 +2,7 @@ import pytest import bonobo from bonobo.execution import GraphExecutionContext -from mock import patch +from unittest.mock import patch @pytest.mark.timeout(2) From 5111b2932e19f92ccc5d9092f6accf7af4e3ed62 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 May 2017 18:37:23 +0200 Subject: [PATCH 3/6] mocks work this way under py3.5, apparently. --- tests/test_basics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index f6dccfa..4243cf2 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,4 +1,3 @@ -import pprint from unittest.mock import MagicMock import bonobo @@ -19,7 +18,7 @@ def test_count(): currified() currified.teardown() - context.send.assert_called_once() + assert len(context.method_calls) == 1 bag = context.send.call_args[0][0] assert isinstance(bag, bonobo.Bag) assert 0 == len(bag.kwargs) From da4ba2603c25e4e658e3ba2026c71df85f176e27 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 May 2017 18:50:06 +0200 Subject: [PATCH 4/6] Update basic tests. --- tests/test_basics.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_basics.py b/tests/test_basics.py index 4243cf2..7a9f317 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock import bonobo import pytest from bonobo.config.processors import ContextCurrifier +from bonobo.constants import NOT_MODIFIED def test_count(): @@ -24,3 +25,38 @@ def test_count(): assert 0 == len(bag.kwargs) assert 1 == len(bag.args) assert bag.args[0] == 42 + + +def test_identity(): + assert bonobo.identity(42) == 42 + + +def test_limit(): + limit = bonobo.Limit(2) + results = [] + for i in range(42): + results += list(limit()) + assert results == [NOT_MODIFIED] * 2 + + +def test_limit_not_there(): + limit = bonobo.Limit(42) + results = [] + for i in range(10): + results += list(limit()) + assert results == [NOT_MODIFIED] * 10 + + +def test_tee(): + inner = MagicMock(side_effect=bonobo.identity) + tee = bonobo.Tee(inner) + results = [] + for i in range(10): + results.append(tee('foo')) + + assert results == [NOT_MODIFIED] * 10 + assert len(inner.mock_calls) == 10 + + +def test_noop(): + assert bonobo.noop(1, 2, 3, 4, foo='bar') == NOT_MODIFIED From 06e7875c89ab6b936a9e7b86b01fde72b6294a49 Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 May 2017 18:51:46 +0200 Subject: [PATCH 5/6] Removes dead code. My pleasure. --- bonobo/util/lifecycle.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 bonobo/util/lifecycle.py diff --git a/bonobo/util/lifecycle.py b/bonobo/util/lifecycle.py deleted file mode 100644 index c497f41..0000000 --- a/bonobo/util/lifecycle.py +++ /dev/null @@ -1,34 +0,0 @@ -from bonobo.basics import noop - - -def _create_lifecycle_functions(noun, verb): - getter = lambda c: getattr(c, verb, noop) - getter.__name__ = 'get_' + noun - - def setter(f): - nonlocal noun, verb - assert callable(f), 'You must provide a callable to decorate with {}.'.format(noun) - - def wrapper(c): - nonlocal verb, f - setattr(f, verb, c) - return f - - return wrapper - - setter.__name__ = 'set_' + noun - - return getter, setter - - -get_initializer, set_initializer = _create_lifecycle_functions('initializer', 'initialize') -get_finalizer, set_finalizer = _create_lifecycle_functions('finalizer', 'finalize') - - -class Contextual: - _with_context = True - - -def with_context(cls_or_func): - cls_or_func._with_context = True - return cls_or_func From 4f9b2081ca5a978a49ed464dbee36522ae29c44f Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Mon, 1 May 2017 19:03:56 +0200 Subject: [PATCH 6/6] draft of release manual --- docs/contribute/release.rst | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/contribute/release.rst diff --git a/docs/contribute/release.rst b/docs/contribute/release.rst new file mode 100644 index 0000000..8e40f08 --- /dev/null +++ b/docs/contribute/release.rst @@ -0,0 +1,38 @@ +Releases +======== + +WORK IN PROGRESS, THIS DOC IS UNFINISHED AND JUST RAW NOTES TO HELP ME RELEASING STUFF. + +How to make a patch release? +:::::::::::::::::::::::::::: + +For now, reference at http://rdc.li/r + +Additional checklist: + +* make format + +How to make a minor or major release? +::::::::::::::::::::::::::::::::::::: + +Releases above patch level are more complex, because we did not find a way not to hardcode the version number in a bunch +of files, and because a few dependant services (source control, continuous integration, code coverage, documentation +builder ...) also depends on version numbers. + +Checklist: + +* Files +* Github + + +Recipes +::::::: + +Get current minor:: + + git semver | python -c 'import sys; print(".".join(sys.stdin.read().strip().split(".")[0:2]))' + +Open git with all files containing current minor:: + + ack `git semver | python -c 'import sys; print("\\\\.".join(sys.stdin.read().strip().split(".")[0:2]))'` | vim - +