diff --git a/.travis.yml b/.travis.yml index bff54dd2..8e5abf9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,11 +16,12 @@ install: - pip install -r requirements-dev.txt script: - - pytest --doctest-modules patterns/ + - if [ "${TRAVIS_PYTHON_VERSION:0:1}" = 2 ]; then export PYEXCLUDE=3; else export PYEXCLUDE=2; fi + - flake8 --exclude="*__py${PYEXCLUDE}.py" patterns/ + - pytest --doctest-modules --ignore-glob="*__py${PYEXCLUDE}.py" patterns/ - pytest -s -vv --cov=. --log-level=INFO tests/ # Actually run all the scripts, contributing to coverage - PYTHONPATH=. ./run_all.sh - - flake8 patterns/ after_success: - codecov diff --git a/README.md b/README.md index 20739846..e23b56fb 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ __Behavioral Patterns__: | Pattern | Description | |:-------:| ----------- | -| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.py) | apply a chain of successive handlers to try and process the data | +| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility__py3.py) | apply a chain of successive handlers to try and process the data | | [catalog](patterns/behavioral/catalog.py) | general methods will call different specialized methods based on construction parameter | | [chaining_method](patterns/behavioral/chaining_method.py) | continue callback next object method | | [command](patterns/behavioral/command.py) | bundle a command and arguments to call later | @@ -46,7 +46,7 @@ __Behavioral Patterns__: | [memento](patterns/behavioral/memento.py) | generate an opaque token that can be used to go back to a previous state | | [observer](patterns/behavioral/observer.py) | provide a callback for notification of events/changes to data | | [publish_subscribe](patterns/behavioral/publish_subscribe.py) | a source syndicates events/data to 0+ registered listeners | -| [registry](patterns/behavioral/registry.py) | keep track of all subclasses of a given class | +| [registry](patterns/behavioral/registry__py3.py) | keep track of all subclasses of a given class | | [specification](patterns/behavioral/specification.py) | business rules can be recombined by chaining the business rules together using boolean logic | | [state](patterns/behavioral/state.py) | logic is organized into a discrete number of potential states and the next state that can be transitioned to | | [strategy](patterns/behavioral/strategy.py) | selectable operations over the same data | diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility__py2.py similarity index 78% rename from patterns/behavioral/chain_of_responsibility.py rename to patterns/behavioral/chain_of_responsibility__py2.py index 8e374d5e..af6d4e35 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility__py2.py @@ -92,29 +92,28 @@ def check_range(request): def main(): - h0 = ConcreteHandler0() - h1 = ConcreteHandler1() - h2 = ConcreteHandler2(FallbackHandler()) - h0.successor = h1 - h1.successor = h2 - - requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] - for request in requests: - h0.handle(request) + """ + >>> h0 = ConcreteHandler0() + >>> h1 = ConcreteHandler1() + >>> h2 = ConcreteHandler2(FallbackHandler()) + >>> h0.successor = h1 + >>> h1.successor = h2 + + >>> requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] + >>> for request in requests: + ... h0.handle(request) + request 2 handled in handler 0 + request 5 handled in handler 0 + request 14 handled in handler 1 + request 22 handled in handler 2 + request 18 handled in handler 1 + request 3 handled in handler 0 + end of chain, no handler for 35 + request 27 handled in handler 2 + request 20 handled in handler 2 + """ if __name__ == "__main__": - main() - - -OUTPUT = """ -request 2 handled in handler 0 -request 5 handled in handler 0 -request 14 handled in handler 1 -request 22 handled in handler 2 -request 18 handled in handler 1 -request 3 handled in handler 0 -end of chain, no handler for 35 -request 27 handled in handler 2 -request 20 handled in handler 2 -""" + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/chain_of_responsibility__py3.py b/patterns/behavioral/chain_of_responsibility__py3.py new file mode 100644 index 00000000..56899043 --- /dev/null +++ b/patterns/behavioral/chain_of_responsibility__py3.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +*What is this pattern about? + +The Chain of responsibility is an object oriented version of the +`if ... elif ... elif ... else ...` idiom, with the +benefit that the condition–action blocks can be dynamically rearranged +and reconfigured at runtime. + +This pattern aims to decouple the senders of a request from its +receivers by allowing request to move through chained +receivers until it is handled. + +Request receiver in simple form keeps a reference to a single successor. +As a variation some receivers may be capable of sending requests out +in several directions, forming a `tree of responsibility`. + +*TL;DR80 +Allow a request to pass down a chain of receivers until it is handled. +""" + +import abc + + +class Handler(metaclass=abc.ABCMeta): + + def __init__(self, successor=None): + self.successor = successor + + def handle(self, request): + """ + Handle request and stop. + If can't - call next handler in chain. + + As an alternative you might even in case of success + call the next handler. + """ + res = self.check_range(request) + if not res and self.successor: + self.successor.handle(request) + + @abc.abstractmethod + def check_range(self, request): + """Compare passed value to predefined interval""" + + +class ConcreteHandler0(Handler): + """Each handler can be different. + Be simple and static... + """ + + @staticmethod + def check_range(request): + if 0 <= request < 10: + print("request {} handled in handler 0".format(request)) + return True + + +class ConcreteHandler1(Handler): + """... With it's own internal state""" + + start, end = 10, 20 + + def check_range(self, request): + if self.start <= request < self.end: + print("request {} handled in handler 1".format(request)) + return True + + +class ConcreteHandler2(Handler): + """... With helper methods.""" + + def check_range(self, request): + start, end = self.get_interval_from_db() + if start <= request < end: + print("request {} handled in handler 2".format(request)) + return True + + @staticmethod + def get_interval_from_db(): + return (20, 30) + + +class FallbackHandler(Handler): + @staticmethod + def check_range(request): + print("end of chain, no handler for {}".format(request)) + return False + + +def main(): + """ + >>> h0 = ConcreteHandler0() + >>> h1 = ConcreteHandler1() + >>> h2 = ConcreteHandler2(FallbackHandler()) + >>> h0.successor = h1 + >>> h1.successor = h2 + + >>> requests = [2, 5, 14, 22, 18, 3, 35, 27, 20] + >>> for request in requests: + ... h0.handle(request) + request 2 handled in handler 0 + request 5 handled in handler 0 + request 14 handled in handler 1 + request 22 handled in handler 2 + request 18 handled in handler 1 + request 3 handled in handler 0 + end of chain, no handler for 35 + request 27 handled in handler 2 + request 20 handled in handler 2 + """ + + +if __name__ == "__main__": + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py deleted file mode 100644 index 9c31b834..00000000 --- a/patterns/behavioral/registry.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -class RegistryHolder(type): - - REGISTRY = {} - - def __new__(cls, name, bases, attrs): - new_cls = type.__new__(cls, name, bases, attrs) - """ - Here the name of the class is used as key but it could be any class - parameter. - """ - cls.REGISTRY[new_cls.__name__] = new_cls - return new_cls - - @classmethod - def get_registry(cls): - return dict(cls.REGISTRY) - - -class BaseRegisteredClass(object): - __metaclass__ = RegistryHolder - """ - Any class that will inherits from BaseRegisteredClass will be included - inside the dict RegistryHolder.REGISTRY, the key being the name of the - class and the associated value, the class itself. - """ - pass - - -if __name__ == "__main__": - print("Before subclassing: ") - for k in RegistryHolder.REGISTRY: - print(k) - - class ClassRegistree(BaseRegisteredClass): - def __init__(self, *args, **kwargs): - pass - - print("After subclassing: ") - for k in RegistryHolder.REGISTRY: - print(k) - -### OUTPUT ### -# Before subclassing: -# BaseRegisteredClass -# After subclassing: -# BaseRegisteredClass -# ClassRegistree diff --git a/patterns/behavioral/registry__py2.py b/patterns/behavioral/registry__py2.py new file mode 100644 index 00000000..7b856f12 --- /dev/null +++ b/patterns/behavioral/registry__py2.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +class RegistryHolder(type): + + REGISTRY = {} + + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + """ + Here the name of the class is used as key but it could be any class + parameter. + """ + cls.REGISTRY[new_cls.__name__] = new_cls + return new_cls + + @classmethod + def get_registry(cls): + return dict(cls.REGISTRY) + + +class BaseRegisteredClass(object): + """ + Any class that will inherits from BaseRegisteredClass will be included + inside the dict RegistryHolder.REGISTRY, the key being the name of the + class and the associated value, the class itself. + """ + __metaclass__ = RegistryHolder + + +def main(): + """ + Before subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass'] + + >>> class ClassRegistree(BaseRegisteredClass): + ... def __init__(self, *args, **kwargs): + ... pass + + After subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass', 'ClassRegistree'] + """ + + +if __name__ == "__main__": + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/registry__py3.py b/patterns/behavioral/registry__py3.py new file mode 100644 index 00000000..82c4eaa2 --- /dev/null +++ b/patterns/behavioral/registry__py3.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +class RegistryHolder(type): + + REGISTRY = {} + + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + """ + Here the name of the class is used as key but it could be any class + parameter. + """ + cls.REGISTRY[new_cls.__name__] = new_cls + return new_cls + + @classmethod + def get_registry(cls): + return dict(cls.REGISTRY) + + +class BaseRegisteredClass(metaclass=RegistryHolder): + """ + Any class that will inherits from BaseRegisteredClass will be included + inside the dict RegistryHolder.REGISTRY, the key being the name of the + class and the associated value, the class itself. + """ + + +def main(): + """ + Before subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass'] + + >>> class ClassRegistree(BaseRegisteredClass): + ... def __init__(self, *args, **kwargs): + ... pass + + After subclassing + >>> sorted(RegistryHolder.REGISTRY) + ['BaseRegisteredClass', 'ClassRegistree'] + """ + + +if __name__ == "__main__": + import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/requirements-dev.txt b/requirements-dev.txt index c33c1dbc..d8da0dbd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,8 @@ -e . -pytest~=4.1 -pytest-cov~=2.6 -flake8~=3.6 -codecov~=2.0 -mock~=2.0 + +pytest~=4.3.0 +pytest-cov~=2.6.0 +flake8~=3.7.0 +codecov~=2.0.0 + +mock~=2.0.0; python_version < "3.*" diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_outputs.py b/tests/test_outputs.py index eaa87997..49b3d81d 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -10,8 +10,6 @@ from patterns.behavioral.catalog import main as catalog_main from patterns.behavioral.catalog import OUTPUT as catalog_output -from patterns.behavioral.chain_of_responsibility import main as chain_main -from patterns.behavioral.chain_of_responsibility import OUTPUT as chain_output from patterns.behavioral.chaining_method import main as chaining_method_main from patterns.behavioral.chaining_method import OUTPUT as chaining_method_output from patterns.behavioral.command import main as command_main @@ -38,7 +36,6 @@ reason="requires python3.4 or higher") @pytest.mark.parametrize("main,output", [ (catalog_main, catalog_output), - (chain_main, chain_output), (chaining_method_main, chaining_method_output), (command_main, command_output), (iterator_main, iterator_output),