diff --git a/.circleci/config.yml b/.circleci/config.yml index a3d72ae..80ddd5b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,9 +15,9 @@ executors: docker: - image: cimg/openjdk:15.0.1 - ubuntu-1604-vm: + ubuntu-2022: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2004:2022.10.1 commands: @@ -36,7 +36,7 @@ commands: - install-tox - run: name: Run unittests & measure code coverage - command: tox -e mypy,clean,build-n-test -vv + command: tox -e type,clean,build-n-test -vv jobs: build-test-report: @@ -114,7 +114,7 @@ jobs: java -jar codacy-coverage-reporter-assembly.jar report -l Python -r coverage.xml send-coverage-to-codecov: - executor: ubuntu-1604-vm + executor: ubuntu-2022 steps: - attach_workspace: at: . @@ -171,7 +171,7 @@ jobs: - install-tox - run: name: Visualize dependency graphs as .svg files - command: tox -e graphs -vv + command: tox -e pydeps -vv - store_artifacts: path: dependency-graphs destination: dep-graphs @@ -193,8 +193,10 @@ workflows: python_version: - "3.6.15" - "3.7.12" + # - "3.8.12" # is the build-test-coverage job - "3.9.9" - "3.10" + - "3.11.0" filters: branches: only: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d155ae8..105216c 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,38 @@ Changelog ========= +2.0.0 (2022-11-19) +================== + +Redesign the Proxy pattern so that it is actually usefull for client code. +Now, to use the pattern one need only to import the Proxy (class) type +`from software_patterns import Proxy` + +Changes +^^^^^^^ + +feature +""""""" +- allow flexible adaptation of Proxy pattern with a single entrypoint + +refactor +"""""""" +- fix type checking with mypy on python3.6 +- remove Handler Prototype Pattern +- static type checking with mypy of test code (along with prod code) + +ci +"" +- update Codecov Job image runner to ubuntu-2004:2022 +- enable CI for Python 3.11 + + +breaking +"""""""" +- Handler Pattern is no longer supported. +- Removed the ProxySubject type. + + 1.3.0 (2022-06-10) ================== diff --git a/README.rst b/README.rst index 854441a..915aaef 100755 --- a/README.rst +++ b/README.rst @@ -133,9 +133,9 @@ Example code to use the `factory` pattern in the form of a `(sub) class registry :alt: PyPI - Python Version :target: https://pypi.org/project/software-patterns -.. |commits_since| image:: https://img.shields.io/github/commits-since/boromir674/software-patterns/v1.3.0/master?color=blue&logo=Github +.. |commits_since| image:: https://img.shields.io/github/commits-since/boromir674/software-patterns/v2.0.0/master?color=blue&logo=Github :alt: GitHub commits since tagged version (branch) - :target: https://github.com/boromir674/software-patterns/compare/v1.3.0..master + :target: https://github.com/boromir674/software-patterns/compare/v2.0.0..master diff --git a/docs/conf.py b/docs/conf.py index 9a38d31..df3729d 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Konstantinos Lampridis' # The full version, including alpha/beta/rc tags -release = '1.3.0' +release = '2.0.0' # -- General configuration --------------------------------------------------- diff --git a/docs/introduction.rst b/docs/introduction.rst index a1c82dd..66beb13 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -3,12 +3,20 @@ Introduction In these documentation pages we present the `software_patterns` Python package and the motivation for publishing it. The package currently host only a handful of software patterns, but they come with -a test suite and full CI for local and remote integrations. +a test suite and full CI for local and remote builds. Why would this project be useful to you? ======================================== +To develop your code, while abstracting common software patterns away and focus on important business logic. +------------------------------------------------------------------------------------------------------------ + +- To promote the DRY prinicipal in your codebase. +- To promote the principal of Single Responsibility in your codebase. +- To promote having Simple Units of Code in your codebase. +- To keep technical debt low in your codebase. + To Learn: --------- @@ -16,15 +24,6 @@ To Learn: - You may wanna learn how to make an open source code contribution and find this codebase a good place to start. -To use the python package and its contained software patterns in your project: ------------------------------------------------------------------------------- - -- To promote good software quality in your python codebase. -- To promote the DRY prinicipal in your client code. -- To promote the principal of Single Responsibility in your client code. -- To promote having Simple Units of Code in your project. - - Why use this Python Patterns library? ===================================== diff --git a/docs/static/classes_software_patterns.svg b/docs/static/classes_software_patterns.svg index 913c77b..c18c757 100644 --- a/docs/static/classes_software_patterns.svg +++ b/docs/static/classes_software_patterns.svg @@ -4,220 +4,202 @@ - + classes_software_patterns - + 0 - -ABC - - - + +ABC + + + 1 - -Generic - - - + +Generic + + + 2 - -InstantiationError - - - + +InstantiationError + + + 3 - -ObjectsPool - -constructor -user_supplied_callback : dict - -get_object() + +ObjectsPool + +constructor +user_supplied_callback : dict + +get_object() 3->1 - - + + 4 - -Observer - - - + +Observer + + + 4->0 - - + + 5 - -ObserverInterface - - -update() + +ObserverInterface + + +update() 4->5 - - + + 5->0 - - + + 6 - -Proxy - - -request() + +Proxy + + + 6->1 - - - - - -8 - -ProxySubjectInterface - - -request() - - - -6->8 - - + + 7 - -ProxySubject - - -request() + +ProxySubjectInterface + + + - - -7->1 - - - - - -7->8 - - + + +6->7 + + - - -8->0 - - + + +7->0 + + - - -8->1 - - + + +8 + +Singleton + + + 9 - -SubclassRegistry - - -create(cls, subclass_identifier) -register_as_subclass(cls, subclass_identifier) + +SubclassRegistry + + +create(cls, subclass_identifier) +register_as_subclass(cls, subclass_identifier) - + 9->1 - - + + 10 - -Subject - -state - -add() -attach(observer) -detach(observer) -notify() + +Subject + +state + +add() +attach(observer) +detach(observer) +notify() - + 10->1 - - + + 11 - -SubjectInterface - - -attach(observer) -detach(observer) -notify() + +SubjectInterface + + +attach(observer) +detach(observer) +notify() - + 10->11 - - + + - + 11->0 - - + + 12 - -UnknownClassError - - - + +UnknownClassError + + + diff --git a/docs/static/software_patterns_deps.svg b/docs/static/software_patterns_deps.svg index 7c4936f..bc5f172 100644 --- a/docs/static/software_patterns_deps.svg +++ b/docs/static/software_patterns_deps.svg @@ -4,61 +4,61 @@ - + G - + abc - -abc + +abc software_patterns_notification - -software_patterns. -notification + +software_patterns. +notification abc->software_patterns_notification - - + + software_patterns_proxy - -software_patterns. -proxy + +software_patterns. +proxy abc->software_patterns_proxy - - - + + + - + typing - + typing abc->typing - - + + software_patterns - -software_patterns + +software_patterns @@ -70,76 +70,95 @@ software_patterns_memoize->software_patterns - - + + software_patterns_notification->software_patterns - - + + software_patterns_proxy->software_patterns - - + + - + -software_patterns_subclass_registry +software_patterns_singleton software_patterns. -subclass_registry +singleton - + +software_patterns_singleton->software_patterns + + + + + +software_patterns_subclass_registry + +software_patterns. +subclass_registry + + + software_patterns_subclass_registry->software_patterns - - + + - + types - -types + +types - + types->software_patterns_memoize - - + + - + types->typing - - + + - + typing->software_patterns_memoize - - - + + + - + typing->software_patterns_notification - - + + - + typing->software_patterns_proxy - - + + + + + +typing->software_patterns_singleton + + - + typing->software_patterns_subclass_registry - - + + diff --git a/setup.cfg b/setup.cfg index 8eb8327..8261ce1 100755 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] ## Setuptools specific information name = software_patterns -version = 1.3.0 +version = 2.0.0 description = Software Design Patterns with types in Python. long_description = file: README.rst long_description_content_type = text/x-rst diff --git a/src/software_patterns/__init__.py b/src/software_patterns/__init__.py index 8cc1b1f..2a14da6 100755 --- a/src/software_patterns/__init__.py +++ b/src/software_patterns/__init__.py @@ -1,8 +1,8 @@ -__version__ = '1.3.0' +__version__ = '2.0.0' from .memoize import ObjectsPool from .notification import Observer, Subject -from .proxy import Proxy, ProxySubject +from .proxy import Proxy from .singleton import Singleton from .subclass_registry import SubclassRegistry @@ -11,7 +11,6 @@ 'Subject', 'ObjectsPool', 'SubclassRegistry', - 'ProxySubject', 'Proxy', 'Singleton', ] diff --git a/src/software_patterns/handler.py b/src/software_patterns/handler.py deleted file mode 100644 index e9d75fc..0000000 --- a/src/software_patterns/handler.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any, Optional - - -class Handler(ABC): - """ - The Handler interface declares a method for building the chain of handlers. - It also declares a method for executing a request. - """ - - @abstractmethod - def set_next(self, handler: Handler) -> Handler: - pass - - @abstractmethod - def handle(self, request) -> Optional[str]: - pass - - -class AbstractHandler(Handler): - """ - The default chaining behavior can be implemented inside a base handler - class. - """ - - _next_handler: Optional[Handler] = None - - def set_next(self, handler: Handler) -> Handler: - self._next_handler = handler - # Returning a handler from here will let us link handlers in a - # convenient way like this: - # monkey.set_next(squirrel).set_next(dog) - return handler - - @abstractmethod - def handle(self, request: Any) -> Optional[str]: - if self._next_handler: - return self._next_handler.handle(request) - - return None diff --git a/src/software_patterns/notification.py b/src/software_patterns/notification.py index a5d9dc5..4da3008 100644 --- a/src/software_patterns/notification.py +++ b/src/software_patterns/notification.py @@ -25,12 +25,13 @@ """ from abc import ABC, abstractmethod -from typing import Generic, List, TypeVar +from typing import Generic, List, TypeVar, Union __all__ = ['Subject', 'Observer'] StateType = TypeVar('StateType') +StateVariableType = Union[StateType, None] class ObserverInterface(ABC): @@ -125,7 +126,7 @@ class Subject(SubjectInterface, Generic[StateType]): def __init__(self, *args, **kwargs): self._observers: List[ObserverInterface] = [] - self._state = None + self._state = StateVariableType def attach(self, observer: ObserverInterface) -> None: self._observers.append(observer) @@ -142,7 +143,7 @@ def add(self, *observers): self._observers.extend(list(observers)) @property - def state(self) -> StateType: + def state(self) -> StateVariableType: """Get the state of the Subject. Returns: diff --git a/src/software_patterns/proxy.py b/src/software_patterns/proxy.py index b73f5b6..d012167 100644 --- a/src/software_patterns/proxy.py +++ b/src/software_patterns/proxy.py @@ -3,60 +3,16 @@ This module contains boiler-plate code to supply the Proxy structural software design pattern, to the client code.""" -from abc import ABC, abstractmethod -from typing import Callable, Generic, TypeVar +from abc import ABC +from typing import Generic, TypeVar T = TypeVar('T') -__all__ = ['ProxySubject', 'Proxy'] +__all__ = ['Proxy'] -class ProxySubjectInterfaceClass(type, Generic[T]): - """Interfacing enabling classes to construct classes (instead of instances). - - Dynamically creates classes that represent a ProxySubjectInterface. - The created classes automatically gain an abstract method with name given at - creation time. The input name can match the desired named selected to a - proxied object. - - For example in a scenario where you proxy a remote web server you might - create ProxySubjectInterface with a 'make_request' abstract method where as - in a scenario where the proxied object is a Tensorflow function you might - name the abstract method as 'tensorflow'. - - Dynamically, creating a class (as this class allows) is useful to adjust to - scenarios like the above. - - Args: - Generic ([type]): [description] - - Raises: - NotImplementedError: [description] - - Returns: - [type]: [description] - """ - - def __new__(mcs, *args, **kwargs): - def __init__(self, proxied_object): - self._proxy_subject = proxied_object - - def object(self, *args, **kwargs) -> T: - return self._proxy_subject - - return super().__new__( - mcs, - 'ProxySubjectInterface', - (ABC,), - { - '__init__': __init__, - args[0]: object, - }, - ) - - -class ProxySubjectInterface(ABC, Generic[T]): +class ProxySubjectInterface(ABC): """Proxy Subject interface holding the important 'request' operation. Declares common operations for both ProxySubject and @@ -64,36 +20,6 @@ class ProxySubjectInterface(ABC, Generic[T]): be passed pass to it, instead of a real subject. """ - @abstractmethod - def request(self, *args, **kwargs) -> T: - raise NotImplementedError - - -class ProxySubject(ProxySubjectInterface, Generic[T]): - """ - The ProxySubject contains some core business logic. Usually, ProxySubject are - capable of doing some useful work which may also be very slow or sensitive - - e.g. correcting input data. A Proxy can solve these issues without any - changes to the ProxySubject's code. - - Example: - - >>> from software_patterns import ProxySubject - >>> proxied_operation = lambda x: x + 1 - >>> proxied_operation(1) - 2 - - >>> proxied_object = ProxySubject(proxied_operation) - >>> proxied_object.request(1) - 2 - """ - - def __init__(self, callback: Callable[..., T]): - self._callback = callback - - def request(self, *args, **kwargs) -> T: - return self._callback(*args, **kwargs) - class Proxy(ProxySubjectInterface, Generic[T]): """ @@ -102,33 +28,29 @@ class Proxy(ProxySubjectInterface, Generic[T]): Example: >>> from software_patterns import Proxy - >>> from software_patterns import ProxySubject - - >>> class ClientProxy(Proxy): - ... def request(self, *args, **kwargs): - ... args = [args[0] + 1] - ... result = super().request(*args, **kwargs) - ... result += 1 + >>> class PlusTen(Proxy): + ... def __call__(self, x: int): + ... result = self._proxy_subject(x + 10) ... return result - >>> proxied_operation = lambda x: x * 2 - >>> proxy_subject = ProxySubject(proxied_operation) - >>> proxy_subject.request(3) - 6 + >>> def plus_one(x: int): + ... return x + 1 + >>> plus_one(2) + 3 - >>> proxy = ClientProxy(proxy_subject) - >>> proxy.request(3) - 9 + >>> proxy = PlusTen(plus_one) + >>> proxy(2) + 13 """ - def __init__(self, proxy_subject: ProxySubject): - self._proxy_subject = proxy_subject - - def request(self, *args, **kwargs) -> T: - """ - The most common applications of the Proxy pattern are lazy loading, - caching, controlling the access, logging, etc. A Proxy can perform one - of these things and then, depending on the result, pass the execution to - the same method in a linked ProxySubject object. - """ - return self._proxy_subject.request(*args, **kwargs) + def __init__(self, runtime_proxy: T): + self._proxy_subject = runtime_proxy + + def __getattr__(self, name: str): + return getattr(self._proxy_subject, name) + + def __str__(self): + return str(self._proxy_subject) + + def __hash__(self): + return hash(self._proxy_subject) diff --git a/src/software_patterns/singleton.py b/src/software_patterns/singleton.py index ea9ed31..cda9e45 100644 --- a/src/software_patterns/singleton.py +++ b/src/software_patterns/singleton.py @@ -13,7 +13,6 @@ class Singleton(type): >>> class ObjectDict(metaclass=Singleton): ... def __init__(self): - ... super().__init__() ... self.objects = {} >>> reg1 = ObjectDict() diff --git a/tests/test_notification.py b/tests/test_notification.py index 3525dd1..b2314fc 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -1,31 +1,22 @@ import pytest +from software_patterns import Observer, Subject -@pytest.fixture -def subject(): - from software_patterns import Subject - return Subject - - -@pytest.fixture -def observer(): - from software_patterns import Observer - - return Observer - - -def test_observers_sanity_test1(subject): - subject1 = subject([]) - subject2 = subject([]) +def test_observers_sanity_test(): + subject1: Subject = Subject([]) + subject2: Subject = Subject([]) assert hasattr(subject1, '_observers') assert hasattr(subject2, '_observers') assert id(subject1._observers) != id(subject2._observers) -def test_observer_as_constructor(observer): +# def test_observer_as_constructor(observer: t.Type[Observer]): +def test_observer_as_constructor(): + observer = Observer + with pytest.raises(TypeError) as instantiation_from_interface_error: - _observer_instance = observer() + _observer_instance = observer() # type: ignore[abstract] import re @@ -38,16 +29,15 @@ def test_observer_as_constructor(observer): ) -def test_scenario(subject, observer): +# def test_scenario(subject: t.Type[Subject], observer: t.Type[Observer]): +def test_scenario(): + # Scenario 1 # The client code. - - print("------ Scenario 1 ------\n") - - class ObserverA(observer): - def update(self, a_subject) -> None: + class ObserverA(Observer): + def update(self, *args, **kwargs) -> None: print("ObserverA: Reacted to the event") - s1 = subject([]) + s1: Subject = Subject([]) o1 = ObserverA() s1.attach(o1) @@ -55,9 +45,8 @@ def update(self, a_subject) -> None: s1.state = 0 s1.notify() - print("------ Scenario 2 ------\n") - # example 2 - class Businessubject(subject): + # Scenario 2 + class Businessubject(Subject): def some_business_logic(self) -> None: """ Usually, the subscription logic is only a fraction of what a Subject can @@ -70,9 +59,10 @@ def some_business_logic(self) -> None: print(f"Subject: My state has just changed to: {self._state}") self.notify() - class ObserverB(observer): - def update(self, a_subject) -> None: - if a_subject.state == 0 or a_subject.state >= 2: + class ObserverB(Observer): + def update(self, *args, **kwargs) -> None: + subject = args[0] + if subject.state == 0 or subject.state >= 2: print("ObserverB: Reacted to the event") s2 = Businessubject([]) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index d2c068d..8cbfa7c 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -1,13 +1,6 @@ import pytest -@pytest.fixture -def proxy_module(): - from software_patterns import proxy - - return proxy - - @pytest.fixture def dummy_handle(): def handle(self, *args, **kwargs): @@ -16,31 +9,29 @@ def handle(self, *args, **kwargs): return handle -def test_proxy_behaviour(proxy_module, dummy_handle, capsys): - prm = proxy_module +def test_proxy_behaviour(dummy_handle, capsys): + from typing import List + + from software_patterns import Proxy # replicate client code that wants to use the proxy pattern + class ClientSubject(object): + """ "A class with a request instance method.""" - # Derive from class RealSubject or use 'python overloading' (use a class - # that has a 'request' method with the same signature as ReadSubject.request) - class ClientSubject(prm.ProxySubject): def request(self, *args, **kwargs): - # delegate handling to the real real subject from client code - # so frankly the request method here is also an adapter print(dummy_handle(self, *args, **kwargs)) - super().request(*args, **kwargs) return type(self).__name__ # Derive from Proxy - class ClientProxy(prm.Proxy): + class ClientProxy(Proxy): def request(self, *args, **kwargs): # run proxy code before sending request to the "proxied" handler before_args = list(['before'] + list(args)) print(dummy_handle(self, *before_args, **kwargs)) - # handle request with the proxied logic - _ = super().request(*args, **kwargs) + # _ = super().request(*args, **kwargs) + _ = self._proxy_subject.request(*args, **kwargs) assert _ == 'ClientSubject' # run proxy code after request to the "proxied" handler @@ -48,16 +39,13 @@ def request(self, *args, **kwargs): print(dummy_handle(self, *after_args, **kwargs)) return _ - def _dummy_callback(*args, **kwargs): - return None - - real_subject = ClientSubject(_dummy_callback) + real_subject = ClientSubject() proxy = ClientProxy(real_subject) # use proxy in a scenario # First test what happens without using proxy - args = [1, 2] + args: List = [1, 2] kwargs = {'k1': 'v1'} result = real_subject.request(*args, **kwargs) @@ -74,6 +62,7 @@ def _dummy_callback(*args, **kwargs): result = proxy.request(*args, **kwargs) captured = capsys.readouterr() + assert ( captured.out == dummy_handle(*list([proxy, 'before'] + args), **kwargs) @@ -91,3 +80,100 @@ def _dummy_callback(*args, **kwargs): ) assert result == type(real_subject).__name__ assert result == 'ClientSubject' + + +def test_simple_proxy(): + from typing import Callable + + from software_patterns import Proxy + + RemoteCall = Callable[[int], int] + remote_call: RemoteCall = lambda x: x + 1 + + # Code that the developer writes + VALUE = 10 + + class ClientProxy(Proxy[RemoteCall]): + def __call__(self, x: int): + return self._proxy_subject(x + VALUE) + + proxy: ClientProxy = ClientProxy(remote_call) + + assert remote_call(2) == 2 + 1 + assert proxy(2) == 2 + VALUE + 1 + + +def test_proxy_as_instance(): + from typing import Callable + + from software_patterns import Proxy + + RemoteCall = Callable[[int], int] + + remote_call: RemoteCall = lambda x: x + 1 + + # Code that the developer writes + VALUE = 10 + + class ClientProxy(Proxy[RemoteCall]): + def __call__(self, x: int): + return self._proxy_subject(x + VALUE) + + proxy: ClientProxy = ClientProxy(remote_call) + + assert remote_call(2) == 2 + 1 + assert proxy(2) == 2 + VALUE + 1 + + assert hash(proxy) == hash(remote_call) + + +def test_mapping_proxy(): + from typing import Mapping + + from software_patterns import Proxy + + # test data + RemoteMapping = Mapping[str, str] + + remote_mapping: RemoteMapping = { + 'id_1': 'a', + 'id_2': 'b', + } + + ## Code that the developer writes + + class ClientProxy(Proxy[Mapping]): + CACHE = { + 'id_1': 'a-cached', + } + + def __getitem__(self, str_id: str): + return self.CACHE.get(str_id, self._proxy_subject[str_id]) + + def __contains__(self, element): + return element in self._proxy_subject + + def wipe_cache(self): + self.CACHE = {} + + proxy: ClientProxy = ClientProxy(remote_mapping) + + # Test code + assert remote_mapping['id_1'] == 'a' + assert remote_mapping['id_2'] == 'b' + assert 'id_1' in remote_mapping + + assert proxy['id_1'] == 'a-cached' + assert proxy['id_2'] == 'b' + assert 'id_1' in proxy + + # Proxy delegates attribute requests (ie 'keys' as below) to subject + assert sorted(remote_mapping.keys()) == sorted(proxy.keys()) + assert str(proxy) == str(remote_mapping) + + proxy.wipe_cache() + + assert proxy['id_1'] == 'a' + assert proxy['id_2'] == 'b' + assert sorted(remote_mapping.keys()) == sorted(proxy.keys()) + assert str(proxy) == str(remote_mapping) diff --git a/tests/test_singleton.py b/tests/test_singleton.py index 0689bd0..99af913 100644 --- a/tests/test_singleton.py +++ b/tests/test_singleton.py @@ -1,17 +1,20 @@ import typing as t +import pytest + from software_patterns import Singleton -def test_singleton(): +def test_singleton(assert_same_objects): class MySingleton(metaclass=Singleton): - def __init__(self, data: t.Mapping): + def __init__(self, data: t.MutableMapping): self.data = data instance_1 = MySingleton({'a': 1}) instance_2 = MySingleton({'b': 2}) - assert id(instance_1) == id(instance_2) + assert_same_objects(instance_1, instance_2) + assert instance_1.data['a'] == instance_2.data['a'] == 1 assert 'b' not in instance_1.data assert 'b' not in instance_2.data @@ -19,3 +22,42 @@ def __init__(self, data: t.Mapping): instance_1.data['c'] = 0 assert instance_2.data['c'] == 0 + + +@pytest.fixture +def assert_same_objects(): + def _assert_same_objects(obj1, obj2): + assert id(obj1) == id(obj2) + attributes_1 = list(dir(obj1)) + attributes_2 = list(dir(obj2)) + assert attributes_1 == attributes_2 + for attr_name in set(attributes_1).difference( + { + '__delattr__', + '__init__', + '__gt__', + '__ne__', + '__dir__', + '__repr__', + '__setattr__', + '__le__', + '__subclasshook__', + '__str__', + '__format__', + '__lt__', + '__eq__', + '__reduce_ex__', + '__getattribute__', + '__reduce__', + '__init_subclass__', + '__hash__', + '__sizeof__', + '__ge__', + '__getstate__', + } + ): + print(attr_name) + assert getattr(obj1, attr_name) == getattr(obj2, attr_name) + assert id(getattr(obj1, attr_name)) == id(getattr(obj2, attr_name)) + + return _assert_same_objects diff --git a/tests/test_subclass_registry.py b/tests/test_subclass_registry.py index 2f62029..78ab33c 100644 --- a/tests/test_subclass_registry.py +++ b/tests/test_subclass_registry.py @@ -1,3 +1,5 @@ +import typing as t + import pytest @@ -21,28 +23,30 @@ def subclass_registry_module(): @pytest.fixture -def register_class(subclass_registry_module): +def register_class(): + from software_patterns import SubclassRegistry + def _register_class(subclass_id: str, inherit=False): - class ParentClass(metaclass=subclass_registry_module.SubclassRegistry): + class ParentClass(metaclass=SubclassRegistry): pass if inherit: @ParentClass.register_as_subclass(subclass_id) - class Child(ParentClass): + class Child1(ParentClass): pass else: @ParentClass.register_as_subclass(subclass_id) - class Child: + class Child2: pass - child_instance = ParentClass.create(subclass_id) + child_instance: t.Any = ParentClass.create(subclass_id) return { 'class_registry': ParentClass, - 'child': Child, + 'child': ParentClass.subclasses[subclass_id], 'child_instance': child_instance, } @@ -61,7 +65,7 @@ def use_metaclass(register_class, assert_correct_metaclass_behaviour): def _use_metaclass_in_scenario(subclass_id: str, inherit=False): classes = register_class(subclass_id, inherit=inherit) assert_correct_metaclass_behaviour(classes, subclass_id) - assert inherited_from_parent[inherit] + assert bool(inherited_from_parent[inherit]) return classes['child_instance'], classes['child'], classes['class_registry'] return _use_metaclass_in_scenario @@ -77,11 +81,13 @@ def assert_metaclass_behaviour(classes, subclass_id): return assert_metaclass_behaviour -def test_metaclass_usage(subclass_registry_module): - class ParentClass(metaclass=subclass_registry_module.SubclassRegistry): +def test_metaclass_usage(): + from software_patterns import SubclassRegistry + + class ParentClass(metaclass=SubclassRegistry): pass - assert type(ParentClass) == subclass_registry_module.SubclassRegistry + assert type(ParentClass) == SubclassRegistry assert hasattr(ParentClass, 'subclasses') assert hasattr(ParentClass, 'create') assert hasattr(ParentClass, 'register_as_subclass') @@ -108,8 +114,11 @@ def test_subclass_registry(use_metaclass, subclass_registry_module): assert ParentClass.subclasses['child1'] == Child1 -def test_create_wrong_input(subclass_registry_module): - class ClassRegistry(metaclass=subclass_registry_module.SubclassRegistry): +def test_create_wrong_input(): + from software_patterns import SubclassRegistry + from software_patterns.subclass_registry import InstantiationError + + class ClassRegistry(metaclass=SubclassRegistry): pass @ClassRegistry.register_as_subclass('id-1') @@ -121,12 +130,12 @@ class ConcreteClassB: def __init__(self, a): self.a = a - with pytest.raises(subclass_registry_module.InstantiationError): + with pytest.raises(InstantiationError): ClassRegistry.create('id-1', 'extra-argument') - with pytest.raises(subclass_registry_module.InstantiationError): + with pytest.raises(InstantiationError): ClassRegistry.create('id-1', extra_key='extra-kwarg') ClassRegistry.create('id-2', 'argument-a-provided') - with pytest.raises(subclass_registry_module.InstantiationError): + with pytest.raises(InstantiationError): ClassRegistry.create('id-2') diff --git a/tox.ini b/tox.ini index 8d6a32f..c352e90 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] isolated_build = True requires = pip >= 21.3.1 -envlist = mypy, clean, dev +envlist = type, clean, dev [testenv] @@ -11,10 +11,8 @@ pasenv = * setenv = PYTHONHASHSEED=2577074909 - MYPYPATH={toxinidir}/src/stubs TEST_RESULTS_DIR={toxinidir}/test-results JUNIT_TEST_RESULTS=junit-test-results.xml - PY_PACKAGE=software_patterns DIST_DIR=dist-dir black,lint,isort: LINT_ARGS = "tests src{/}software_patterns" @@ -26,7 +24,7 @@ commands = pytest {posargs} --cov -vv --junitxml={env:TEST_RESULTS_DIR:test-resu [testenv:dev] basepython = {env:TOXPYTHON:python} -use_develop = true +usedevelop = true ; commands = pytest {posargs} -vv [testenv:build-n-test] @@ -58,13 +56,16 @@ commands = coverage html - ## STATIC TYPE CHECKING -[testenv:mypy] +[testenv:type] description = Python source code type hints (mypy) -deps = mypy -skip_install = true -commands = mypy {posargs:{toxinidir}/src/} +deps = + mypy + pytest +usedevelop = true +changedir = {toxinidir} +commands = mypy {posargs:src tests --show-traceback \ + --check-untyped-defs} # very aggresive: eg it flags 'a = Subject()' requiring 'a: Subject = Subject()' ## PYTHON PACKAGING @@ -124,10 +125,11 @@ commands = ## STATIC ANALYSIS OF CODE +# CODE LINTING, STATIC (STYLE) CHECKING [testenv:lint] -description = test if code conforms with our styles - to check against code style (aka lint check) run: tox -e lint - to apply code style (aka lint apply) run: APPLY_LINT= tox -e lint +description = Code Linting using Black and Isort. + To check against code style (aka lint check) run: `tox -e lint`. + To apply code style (aka lint apply) run: `APPLY_LINT= tox -e lint`. deps = black isort >= 5.0.0 @@ -165,13 +167,13 @@ description = Run the Pylint tool to analyse the Python code and output informat potential problems and convention violations basepython = {env:TOXPYTHON:python} deps = pylint==2.7.4 -use_develop = true +usedevelop = true commands = python -m pylint {posargs:{toxinidir}/src/{env:PY_PACKAGE}} ## GENERATE ARCHITECTURE GRAPHS -[testenv:graphs] +[testenv:pydeps] description = Visualise the dependency graphs (roughly which module imports which), by examining the Python code. The dependency graph(s) are rendered in .svg file(s) and saved on the disk. You can use the DEPS_GRAPHS environment variable to determine the directory location to store the visualisation(s). If @@ -189,7 +191,7 @@ setenv = DEPS_DEFAULT_LOCATION = dependency-graphs deps = pydeps==1.9.13 skip_install = false -use_develop = true +usedevelop = true commands_pre = python -c 'import os; dir_path = os.path.join("{toxinidir}", "{env:DEPS_GRAPHS:{env:DEPS_DEFAULT_LOCATION}}"); exec("if not os.path.exists(dir_path):\n os.mkdir(dir_path)");' commands = @@ -228,13 +230,14 @@ commands = [testenv:uml] description = Generate UML (class and package) diagrams by inspecting the code. The diagrams are stored in the $UML_DIAGRAMS dir. Runs the pyreverse tool to parse the code and generate the files. This is a pretty legacy tool currently integrated in pylint (not available through pip). + Run `tox -e uml -- svg` to update the docs/static. setenv = {[testenv]setenv} # include dirs to pythonpath to solve issue of inspect lib failing with some relative imports PYTHONPATH={toxinidir}/src/{env:PY_PACKAGE}:{toxinidir}/src/{env:PY_PACKAGE}/utils UML_DIAGRAMS=uml-diagrams deps = pylint==2.7.4 -use_develop = true +usedevelop = true commands_pre = python -c 'from glob import glob; import os; dir = os.path.join("{toxinidir}", "{env:UML_DIAGRAMS}"); exec("if not os.path.isdir(dir):\n os.mkdir(dir)\nelse:\n _ = [os.remove(x) for x in glob(dir+\"/*\")]")' commands = @@ -289,7 +292,7 @@ setenv = deps = -rrequirements/docs.txt sphinx-autobuild -use_develop = true +usedevelop = true commands = sphinx-autobuild docs docs/_build/html {posargs} python -c 'import os; my_dir = os.getcwd(); print("View documentation at " + os.path.join(os.getcwd(), "{env:DOCS_BUILD_LOCATION:dist/docs}", "index.html") + "; it is ready to be hosted!")'