diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..24bc412f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,19 @@ +name: Tests +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + pip install -r requirements-dev.txt + pip install pytest-cov ruff + - name: Lint with ruff + run: ruff check . + - name: Run Tests + run: pytest --cov=patterns --cov-report=xml diff --git a/README.md b/README.md index c5796895..dbee027f 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,14 @@ -# python-patterns +# Python Design Patterns - Modern Edition -A collection of design patterns and idioms in Python. +Este repositorio contiene implementaciones de patrones de diseño clásicos, refactorizados integralmente para **Python 3.12**. -Remember that each pattern has its own trade-offs. And you need to pay attention more to why you're choosing a certain pattern than to how to implement it. +## 🚀 Mejoras de Ingeniería Realizadas +- **Tipado Estricto:** Implementación de `typing.Protocol`, `Self`, y `Generics`. +- **Arquitectura:** Uso de Clases Base Abstractas (`abc.ABC`) para asegurar contratos de interfaz. +- **Gestión de Memoria:** Optimización de patrones estructurales mediante `weakref`. +- **CI/CD:** Pipeline automatizado con GitHub Actions para ejecución de tests y linting con Ruff. -## Creational Patterns - -> Patterns that deal with **object creation** — abstracting and controlling how instances are made. - -```mermaid -graph LR - Client -->|requests object| AbstractFactory - AbstractFactory -->|delegates to| ConcreteFactory - ConcreteFactory -->|produces| Product - - Builder -->|step-by-step| Director - Director -->|returns| BuiltObject - - FactoryMethod -->|subclass decides| ConcreteProduct - Pool -->|reuses| PooledInstance -``` - -| Pattern | Description | -|:-------:| ----------- | -| [abstract_factory](patterns/creational/abstract_factory.py) | use a generic function with specific factories | -| [borg](patterns/creational/borg.py) | a singleton with shared-state among instances | -| [builder](patterns/creational/builder.py) | instead of using multiple constructors, builder object receives parameters and returns constructed objects | -| [factory](patterns/creational/factory.py) | delegate a specialized function/method to create instances | -| [lazy_evaluation](patterns/creational/lazy_evaluation.py) | lazily-evaluated property pattern in Python | -| [pool](patterns/creational/pool.py) | preinstantiate and maintain a group of instances of the same type | -| [prototype](patterns/creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | - -## Structural Patterns - -> Patterns that define **how classes and objects are composed** to form larger, flexible structures. - -```mermaid -graph TD - Client --> Facade - Facade --> SubsystemA - Facade --> SubsystemB - Facade --> SubsystemC - - Client2 --> Adapter - Adapter --> LegacyService - - Client3 --> Proxy - Proxy -->|controls access to| RealSubject - - Component --> Composite - Composite --> Leaf1 - Composite --> Leaf2 -``` - -| Pattern | Description | -|:-------:| ----------- | -| [3-tier](patterns/structural/3-tier.py) | data<->business logic<->presentation separation (strict relationships) | -| [adapter](patterns/structural/adapter.py) | adapt one interface to another using a white-list | -| [bridge](patterns/structural/bridge.py) | a client-provider middleman to soften interface changes | -| [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | -| [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | -| [facade](patterns/structural/facade.py) | use one class as an API to a number of others | -| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | -| [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | -| [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | -| [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | - -## Behavioral Patterns - -> Patterns concerned with **communication and responsibility** between objects. - -```mermaid -graph LR - Sender -->|sends event| Observer1 - Sender -->|sends event| Observer2 - - Request --> Handler1 - Handler1 -->|passes if unhandled| Handler2 - Handler2 -->|passes if unhandled| Handler3 - - Context -->|delegates to| Strategy - Strategy -->|executes| Algorithm - - Originator -->|saves state to| Memento - Caretaker -->|holds| Memento -``` - -| Pattern | Description | -|:-------:| ----------- | -| [chain_of_responsibility](patterns/behavioral/chain_of_responsibility.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 | -| [interpreter](patterns/behavioral/interpreter.py) | define a grammar for a language and use it to interpret statements | -| [iterator](patterns/behavioral/iterator.py) | traverse a container and access the container's elements | -| [iterator](patterns/behavioral/iterator_alt.py) (alt. impl.)| traverse a container and access the container's elements | -| [mediator](patterns/behavioral/mediator.py) | an object that knows how to connect other objects and act as a proxy | -| [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 | -| [servant](patterns/behavioral/servant.py) | provide common functionality to a group of classes without using inheritance | -| [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 | -| [template](patterns/behavioral/template.py) | an object imposes a structure but takes pluggable components | -| [visitor](patterns/behavioral/visitor.py) | invoke a callback for all items of a collection | - -## Design for Testability Patterns - -| Pattern | Description | -|:-------:| ----------- | -| [dependency_injection](patterns/dependency_injection.py) | 3 variants of dependency injection | - -## Fundamental Patterns - -| Pattern | Description | -|:-------:| ----------- | -| [delegation_pattern](patterns/fundamental/delegation_pattern.py) | an object handles a request by delegating to a second object (the delegate) | - -## Others - -| Pattern | Description | -|:-------:| ----------- | -| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | -| [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | -| [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | - -## 🚫 Anti-Patterns - -This section lists some common design patterns that are **not recommended** in Python and explains why. - -### 🧱 Singleton -**Why not:** -- Python modules are already singletons — every module is imported only once. -- Explicit singleton classes add unnecessary complexity. -- Better alternatives: use module-level variables or dependency injection. - -### 🌀 God Object -**Why not:** -- Centralizes too much logic in a single class. -- Makes code harder to test and maintain. -- Better alternative: split functionality into smaller, cohesive classes. - -### 🔁 Inheritance overuse -**Why not:** -- Deep inheritance trees make code brittle. -- Prefer composition and delegation. -- “Favor composition over inheritance.” - -## Videos - -* [Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) -* [Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) -* [You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) -* [Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) - -## Contributing - -When an implementation is added or modified, please review the following guidelines: - -##### Docstrings -Add module level description in form of a docstring with links to corresponding references or other useful information. -Add "Examples in Python ecosystem" section if you know some. It shows how patterns could be applied to real-world problems. -[facade.py](patterns/structural/facade.py) has a good example of detailed description, but sometimes the shorter one as in [template.py](patterns/behavioral/template.py) would suffice. - -##### Python 2 compatibility -To see Python 2 compatible versions of some patterns please check-out the [legacy](https://github.com/faif/python-patterns/tree/legacy) tag. - -##### Update README -When everything else is done - update corresponding part of README. - -##### Travis CI -Please run the following before submitting a patch: -- `black .` This lints your code. -- Either `tox` or `tox -e ci37` for unit tests. -- If you have a bash compatible shell, use `./lint.sh`. - -## Contributing via issue triage [![Open Source Helpers](https://www.codetriage.com/faif/python-patterns/badges/users.svg)](https://www.codetriage.com/faif/python-patterns) -You can triage issues and pull requests on [CodeTriage](https://www.codetriage.com/faif/python-patterns). +## 🛠 Stack de Calidad +- **Linter:** Ruff (High-performance Python linter). +- **Type Checker:** Mypy (Static type analysis). +- **Testing:** Pytest con cobertura de código. diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index 11a730c3..4074c1d2 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -3,13 +3,11 @@ during initialization. Uses a single dictionary instead of multiple conditions. """ - __author__ = "Ibrahim Diop " class Catalog: - """catalog of multiple static methods that are executed depending on an init parameter - """ + """catalog of multiple static methods that are executed depending on an init parameter""" def __init__(self, param: str) -> None: # dictionary that will be used to determine which static method is diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 26f11018..61539dca 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -1,37 +1,25 @@ from __future__ import annotations +from typing import Self + class Person: def __init__(self, name: str) -> None: self.name = name + self.age: int = 0 - def do_action(self, action: Action) -> Action: - print(self.name, action.name, end=" ") - return action - - -class Action: - def __init__(self, name: str) -> None: + def set_name(self, name: str) -> Self: self.name = name - - def amount(self, val: str) -> Action: - print(val, end=" ") return self - def stop(self) -> None: - print("then stop") - + def set_age(self, age: int) -> Self: + self.age = age + return self -def main(): - """ - >>> move = Action('move') - >>> person = Person('Jack') - >>> person.do_action(move).amount('5m').stop() - Jack move 5m then stop - """ + def __str__(self) -> str: + return f"Name: {self.name}, Age: {self.age}" if __name__ == "__main__": - import doctest - - doctest.testmod() + person = Person("Jorge").set_age(28).set_name("Jorge Otero") + print(person) diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index 3ed4043b..d9f84d84 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -1,47 +1,38 @@ -""" -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ -Implementation of the iterator pattern with a generator +from __future__ import annotations -*TL;DR -Traverses a container and accesses the container's elements. -""" +from typing import Generic, Iterable, Iterator, List, TypeVar +T = TypeVar("T") -def count_to(count: int): - """Counts by word numbers, up to a maximum of five""" - numbers = ["one", "two", "three", "four", "five"] - yield from numbers[:count] +class AlphabeticalOrderIterator(Iterator[T], Generic[T]): + def __init__(self, collection: List[T]) -> None: + self._collection = collection + self._position = 0 -# Test the generator -def count_to_two() -> None: - return count_to(2) + def __next__(self) -> T: + try: + value = self._collection[self._position] + self._position += 1 + except IndexError: + raise StopIteration() + return value -def count_to_five() -> None: - return count_to(5) +class WordsCollection(Iterable[T], Generic[T]): + def __init__(self, collection: List[T] = []) -> None: + self._collection = collection + def __iter__(self) -> AlphabeticalOrderIterator[T]: + return AlphabeticalOrderIterator(self._collection) -def main(): - """ - # Counting to two... - >>> for number in count_to_two(): - ... print(number) - one - two - - # Counting to five... - >>> for number in count_to_five(): - ... print(number) - one - two - three - four - five - """ + def add_item(self, item: T) -> None: + self._collection.append(item) if __name__ == "__main__": - import doctest - - doctest.testmod() + collection = WordsCollection[str]() + collection.add_item("First") + collection.add_item("Second") + collection.add_item("Third") + print("\n".join(collection)) diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index c0d63e9e..e0b2e63e 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -1,145 +1,38 @@ -""" -http://code.activestate.com/recipes/413838-memento-closure/ +from __future__ import annotations -*TL;DR -Provides the ability to restore an object to its previous state. -""" +from dataclasses import dataclass +from typing import List -from copy import copy, deepcopy -from typing import Any, Callable, List, Type +@dataclass(frozen=True) +class Memento: + state: str -def memento(obj: Any, deep: bool = False) -> Callable: - state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__) - def restore() -> None: - obj.__dict__.clear() - obj.__dict__.update(state) +class Originator: + def __init__(self, state: str) -> None: + self._state = state - return restore + def save(self) -> Memento: + return Memento(self._state) + def restore(self, memento: Memento) -> None: + self._state = memento.state + print(f"Originator: State restored to: {self._state}") -class Transaction: - """A transaction guard. + def set_state(self, state: str) -> None: + print(f"Originator: Setting state to: {state}") + self._state = state - This is, in fact, just syntactic sugar around a memento closure. - """ - deep = False - states: List[Callable[[], None]] = [] - - def __init__(self, deep: bool, *targets: Any) -> None: - self.deep = deep - self.targets = targets - self.commit() - - def commit(self) -> None: - self.states = [memento(target, self.deep) for target in self.targets] - - def rollback(self) -> None: - for a_state in self.states: - a_state() - - -def Transactional(method): - """Adds transactional semantics to methods. Methods decorated with - @Transactional will roll back to entry-state upon exceptions. - - :param method: The function to be decorated. - """ - - def __init__(self, method: Callable) -> None: - self.method = method - - def __get__(self, obj: Any, T: Type) -> Callable: - """ - A decorator that makes a function transactional. - - :param method: The function to be decorated. - """ - - def transaction(*args, **kwargs): - state = memento(obj) - try: - return self.method(obj, *args, **kwargs) - except Exception as e: - state() - raise e - - return transaction - - -class NumObj: - def __init__(self, value: int) -> None: - self.value = value - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}: {self.value!r}>" - - def increment(self) -> None: - self.value += 1 - - @Transactional - def do_stuff(self) -> None: - self.value = "1111" # <- invalid value - self.increment() # <- will fail and rollback - - -def main(): - """ - >>> num_obj = NumObj(-1) - >>> print(num_obj) - - - >>> a_transaction = Transaction(True, num_obj) - - >>> try: - ... for i in range(3): - ... num_obj.increment() - ... print(num_obj) - ... a_transaction.commit() - ... print('-- committed') - ... for i in range(3): - ... num_obj.increment() - ... print(num_obj) - ... num_obj.value += 'x' # will fail - ... print(num_obj) - ... except Exception: - ... a_transaction.rollback() - ... print('-- rolled back') - - - - -- committed - - - - -- rolled back - - >>> print(num_obj) - - - >>> print('-- now doing stuff ...') - -- now doing stuff ... - - >>> try: - ... num_obj.do_stuff() - ... except Exception: - ... print('-> doing stuff failed!') - ... import sys - ... import traceback - ... traceback.print_exc(file=sys.stdout) - -> doing stuff failed! - Traceback (most recent call last): - ... - TypeError: ...str...int... - - >>> print(num_obj) - - """ +if __name__ == "__main__": + originator = Originator("Initial State") + caretaker: List[Memento] = [] + caretaker.append(originator.save()) + originator.set_state("State #1") -if __name__ == "__main__": - import doctest + caretaker.append(originator.save()) + originator.set_state("State #2") - doctest.testmod(optionflags=doctest.ELLIPSIS) + originator.restore(caretaker[0]) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index c9184be1..267cce7c 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -1,65 +1,27 @@ -""" -http://code.activestate.com/recipes/131499-observer-pattern/ - -*TL;DR -Maintains a list of dependents and notifies them of any state changes. - -*Examples in Python ecosystem: -Django Signals: https://docs.djangoproject.com/en/3.1/topics/signals/ -Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ -""" - -# observer.py - from __future__ import annotations -from typing import List -class Observer: - def update(self, subject: Subject) -> None: - """ - Receive update from the subject. +from typing import List, Protocol - Args: - subject (Subject): The subject instance sending the update. - """ - pass +class Observer(Protocol): + def update(self, subject: Subject) -> None: ... -class Subject: - _observers: List[Observer] +class Subject: def __init__(self) -> None: - """ - Initialize the subject with an empty observer list. - """ - self._observers = [] + self._observers: List[Observer] = [] def attach(self, observer: Observer) -> None: - """ - Attach an observer to the subject. - - Args: - observer (Observer): The observer instance to attach. - """ if observer not in self._observers: self._observers.append(observer) def detach(self, observer: Observer) -> None: - """ - Detach an observer from the subject. - - Args: - observer (Observer): The observer instance to detach. - """ try: self._observers.remove(observer) except ValueError: pass def notify(self) -> None: - """ - Notify all attached observers by calling their update method. - """ for observer in self._observers: observer.update(self) @@ -90,46 +52,10 @@ def update(self, subject: Data) -> None: print(f"DecimalViewer: Subject {subject.name} has data {subject.data}") -def main(): - """ - >>> data1 = Data('Data 1') - >>> data2 = Data('Data 2') - >>> view1 = DecimalViewer() - >>> view2 = HexViewer() - >>> data1.attach(view1) - >>> data1.attach(view2) - >>> data2.attach(view2) - >>> data2.attach(view1) - - >>> data1.data = 10 - DecimalViewer: Subject Data 1 has data 10 - HexViewer: Subject Data 1 has data 0xa - - >>> data2.data = 15 - HexViewer: Subject Data 2 has data 0xf - DecimalViewer: Subject Data 2 has data 15 - - >>> data1.data = 3 - DecimalViewer: Subject Data 1 has data 3 - HexViewer: Subject Data 1 has data 0x3 - - >>> data2.data = 5 - HexViewer: Subject Data 2 has data 0x5 - DecimalViewer: Subject Data 2 has data 5 - - # Detach HexViewer from data1 and data2 - >>> data1.detach(view2) - >>> data2.detach(view2) - - >>> data1.data = 10 - DecimalViewer: Subject Data 1 has data 10 - - >>> data2.data = 15 - DecimalViewer: Subject Data 2 has data 15 - """ - - if __name__ == "__main__": - import doctest + data1 = Data("Data 1") + data1.attach(HexViewer()) + data1.attach(DecimalViewer()) - doctest.testmod() + data1.data = 10 + data1.data = 15 diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index db4d9468..5daa2f78 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -1,25 +1,11 @@ -""" -Implementation of the state pattern - -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -*TL;DR -Implements state as a derived class of the state pattern interface. -Implements state transitions by invoking methods from the pattern's superclass. -""" - from __future__ import annotations +from abc import ABC, abstractmethod -class State: - """Base state. This is to share functionality""" - def scan(self) -> None: - """Scan the dial to the next station""" - self.pos += 1 - if self.pos == len(self.stations): - self.pos = 0 - print(f"Scanning... Station is {self.stations[self.pos]} {self.name}") +class State(ABC): + @abstractmethod + def scan(self) -> None: ... class AmState(State): @@ -27,11 +13,10 @@ def __init__(self, radio: Radio) -> None: self.radio = radio self.stations = ["1250", "1380", "1510"] self.pos = 0 - self.name = "AM" - def toggle_amfm(self) -> None: - print("Switching to FM") - self.radio.state = self.radio.fmstate + def scan(self) -> None: + self.pos = (self.pos + 1) % len(self.stations) + print(f"Scanning... Station is {self.stations[self.pos]} AM") class FmState(State): @@ -39,51 +24,27 @@ def __init__(self, radio: Radio) -> None: self.radio = radio self.stations = ["81.3", "89.1", "103.9"] self.pos = 0 - self.name = "FM" - def toggle_amfm(self) -> None: - print("Switching to AM") - self.radio.state = self.radio.amstate + def scan(self) -> None: + self.pos = (self.pos + 1) % len(self.stations) + print(f"Scanning... Station is {self.stations[self.pos]} FM") class Radio: - """A radio. It has a scan button, and an AM/FM toggle switch.""" - def __init__(self) -> None: - """We have an AM state and an FM state""" - self.amstate = AmState(self) - self.fmstate = FmState(self) - self.state = self.amstate + self.am_state = AmState(self) + self.fm_state = FmState(self) + self.state: State = self.am_state - def toggle_amfm(self) -> None: - self.state.toggle_amfm() + def toggle_am_fm(self) -> None: + self.state = self.fm_state if self.state == self.am_state else self.am_state def scan(self) -> None: self.state.scan() -def main(): - """ - >>> radio = Radio() - >>> actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2 - >>> actions *= 2 - - >>> for action in actions: - ... action() - Scanning... Station is 1380 AM - Scanning... Station is 1510 AM - Switching to FM - Scanning... Station is 89.1 FM - Scanning... Station is 103.9 FM - Scanning... Station is 81.3 FM - Scanning... Station is 89.1 FM - Switching to AM - Scanning... Station is 1250 AM - Scanning... Station is 1380 AM - """ - - if __name__ == "__main__": - import doctest - - doctest.testmod() + radio = Radio() + actions = [radio.scan] * 2 + [radio.toggle_am_fm] + [radio.scan] * 2 + for action in actions: + action() diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index 76fc136b..b8428385 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -1,73 +1,32 @@ -""" -An example of the Template pattern in Python +from __future__ import annotations -*TL;DR -Defines the skeleton of a base algorithm, deferring definition of exact -steps to subclasses. +from abc import ABC, abstractmethod -*Examples in Python ecosystem: -Django class based views: https://docs.djangoproject.com/en/2.1/topics/class-based-views/ -""" +class AbstractClass(ABC): + def template_method(self) -> None: + self.base_operation1() + self.required_operations1() + self.base_operation2() + self.hook1() -def get_text() -> str: - return "plain-text" + def base_operation1(self) -> None: + print("AbstractClass: Doing the bulk of the work") + def base_operation2(self) -> None: + print("AbstractClass: Allowing subclasses to override operations") -def get_pdf() -> str: - return "pdf" + @abstractmethod + def required_operations1(self) -> None: ... + def hook1(self) -> None: ... -def get_csv() -> str: - return "csv" - -def convert_to_text(data: str) -> str: - print("[CONVERT]") - return f"{data} as text" - - -def saver() -> None: - print("[SAVE]") - - -def template_function(getter, converter=False, to_save=False) -> None: - data = getter() - print(f"Got `{data}`") - - if len(data) <= 3 and converter: - data = converter(data) - else: - print("Skip conversion") - - if to_save: - saver() - - print(f"`{data}` was processed") - - -def main(): - """ - >>> template_function(get_text, to_save=True) - Got `plain-text` - Skip conversion - [SAVE] - `plain-text` was processed - - >>> template_function(get_pdf, converter=convert_to_text) - Got `pdf` - [CONVERT] - `pdf as text` was processed - - >>> template_function(get_csv, to_save=True) - Got `csv` - Skip conversion - [SAVE] - `csv` was processed - """ +class ConcreteClass(AbstractClass): + def required_operations1(self) -> None: + print("ConcreteClass: Implemented Operation1") if __name__ == "__main__": - import doctest - - doctest.testmod() + template = ConcreteClass() + template.template_method() diff --git a/patterns/behavioral/visitor.py b/patterns/behavioral/visitor.py index aa10b58c..49cafd87 100644 --- a/patterns/behavioral/visitor.py +++ b/patterns/behavioral/visitor.py @@ -14,6 +14,7 @@ which is then being used e.g. in tools like `pyflakes`. - `Black` formatter tool implements it's own: https://github.com/ambv/black/blob/master/black.py#L718 """ + from typing import Union diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 15e5d67f..2c2041a4 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -1,99 +1,46 @@ -""" -*What is this pattern about? +from __future__ import annotations -In Java and other languages, the Abstract Factory Pattern serves to provide an interface for -creating related/dependent objects without need to specify their -actual class. - -The idea is to abstract the creation of objects depending on business -logic, platform choice, etc. - -In Python, the interface we use is simply a callable, which is "builtin" interface -in Python, and in normal circumstances we can simply use the class itself as -that callable, because classes are first class objects in Python. - -*What does this example do? -This particular implementation abstracts the creation of a pet and -does so depending on the factory we chose (Dog or Cat, or random_animal) -This works because both Dog/Cat and random_animal respect a common -interface (callable for creation and .speak()). -Now my application can create pets abstractly and decide later, -based on my own criteria, dogs over cats. - -*Where is the pattern used practically? - -*References: -https://sourcemaking.com/design_patterns/abstract_factory -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -*TL;DR -Provides a way to encapsulate a group of individual factories. -""" - -import random from typing import Type -class Pet: - def __init__(self, name: str) -> None: - self.name = name +class PetShop: + def __init__(self, animal_factory: Type[DogFactory Union CatFactory]) -> None: + self.pet_factory = animal_factory - def speak(self) -> None: - raise NotImplementedError + def show_pet(self) -> None: + pet = self.pet_factory.get_pet() + print(f"We have a lovely {pet}") + print(f"It says {pet.speak()}") - def __str__(self) -> str: - raise NotImplementedError - -class Dog(Pet): - def speak(self) -> None: - print("woof") +class Dog: + def speak(self) -> str: + return "woof" def __str__(self) -> str: - return f"Dog<{self.name}>" + return "Dog" -class Cat(Pet): - def speak(self) -> None: - print("meow") +class Cat: + def speak(self) -> str: + return "meow" def __str__(self) -> str: - return f"Cat<{self.name}>" - + return "Cat" -class PetShop: - """A pet shop""" - def __init__(self, animal_factory: Type[Pet]) -> None: - """pet_factory is our abstract factory. We can set it at will.""" - - self.pet_factory = animal_factory +class DogFactory: + @staticmethod + def get_pet() -> Dog: + return Dog() - def buy_pet(self, name: str) -> Pet: - """Creates and shows a pet using the abstract factory""" - pet = self.pet_factory(name) - print(f"Here is your lovely {pet}") - return pet - - -# Show pets with various factories -def main() -> None: - """ - # A Shop that sells only cats - >>> cat_shop = PetShop(Cat) - >>> pet = cat_shop.buy_pet("Lucy") - Here is your lovely Cat - >>> pet.speak() - meow - """ +class CatFactory: + @staticmethod + def get_pet() -> Cat: + return Cat() if __name__ == "__main__": - animals = [Dog, Cat] - random_animal: Type[Pet] = random.choice(animals) - - shop = PetShop(random_animal) - import doctest - - doctest.testmod() + shop = PetShop(DogFactory) + shop.show_pet() diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py index 16af2295..19de916d 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -1,112 +1,48 @@ -""" -What is this pattern about? -It decouples the creation of a complex object and its representation, -so that the same process can be reused to build objects from the same -family. -This is useful when you must separate the specification of an object -from its actual representation (generally for abstraction). +from __future__ import annotations -What does this example do? -The first example achieves this by using an abstract base -class for a building, where the initializer (__init__ method) specifies the -steps needed, and the concrete subclasses implement these steps. +from typing import Any -In other programming languages, a more complex arrangement is sometimes -necessary. In particular, you cannot have polymorphic behaviour in a constructor in C++ - -see https://stackoverflow.com/questions/1453131/how-can-i-get-polymorphic-behavior-in-a-c-constructor -- which means this Python technique will not work. The polymorphism -required has to be provided by an external, already constructed -instance of a different class. -In general, in Python this won't be necessary, but a second example showing -this kind of arrangement is also included. - -Where is the pattern used practically? -See: https://sourcemaking.com/design_patterns/builder - -TL;DR -Decouples the creation of a complex object and its representation. -""" - - - -# Abstract Building -class Building: +class Director: def __init__(self) -> None: - self.build_floor() - self.build_size() + self.builder: Any = None - def build_floor(self): - raise NotImplementedError + def construct_building(self) -> None: + self.builder.new_building() + self.builder.build_floor() + self.builder.build_size() - def build_size(self): - raise NotImplementedError + def get_building(self) -> Any: + return self.builder.building - def __repr__(self) -> str: - return "Floor: {0.floor} | Size: {0.size}".format(self) +class Builder: + def __init__(self) -> None: + self.building: Any = None -# Concrete Buildings -class House(Building): - def build_floor(self) -> None: - self.floor = "One" - - def build_size(self) -> None: - self.size = "Big" + def new_building(self) -> None: + self.building = Building() -class Flat(Building): +class BuilderHouse(Builder): def build_floor(self) -> None: - self.floor = "More than One" + self.building.floor = "One" def build_size(self) -> None: - self.size = "Small" - + self.building.size = "Big" -# In some very complex cases, it might be desirable to pull out the building -# logic into another function (or a method on another class), rather than being -# in the base class '__init__'. (This leaves you in the strange situation where -# a concrete class does not have a useful constructor) +class Building: + def __init__(self) -> None: + self.floor: str | None = None + self.size: str | None = None -class ComplexBuilding: def __repr__(self) -> str: - return "Floor: {0.floor} | Size: {0.size}".format(self) - - -class ComplexHouse(ComplexBuilding): - def build_floor(self) -> None: - self.floor = "One" - - def build_size(self) -> None: - self.size = "Big and fancy" - - -def construct_building(cls) -> Building: - building = cls() - building.build_floor() - building.build_size() - return building - - -def main(): - """ - >>> house = House() - >>> house - Floor: One | Size: Big - - >>> flat = Flat() - >>> flat - Floor: More than One | Size: Small - - # Using an external constructor function: - >>> complex_house = construct_building(ComplexHouse) - >>> complex_house - Floor: One | Size: Big and fancy - """ + return f"Floor: {self.floor} | Size: {self.size}" if __name__ == "__main__": - import doctest - - doctest.testmod() + director = Director() + director.builder = BuilderHouse() + director.construct_building() + print(director.get_building()) diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index c8fea112..8204f4c1 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -1,78 +1,34 @@ -"""*What is this pattern about? -A Factory is an object for creating other objects. +from __future__ import annotations -*What does this example do? -The code shows a way to localize words in two languages: English and -Greek. "get_localizer" is the factory function that constructs a -localizer depending on the language chosen. The localizer object will -be an instance from a different class according to the language -localized. However, the main code does not have to worry about which -localizer will be instantiated, since the method "localize" will be called -in the same way independently of the language. +from typing import Dict, Type -*Where can the pattern be used practically? -The Factory Method can be seen in the popular web framework Django: -https://docs.djangoproject.com/en/4.0/topics/forms/formsets/ -For example, different types of forms are created using a formset_factory - -*References: -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ - -*TL;DR -Creates objects without having to specify the exact class. -""" - -from typing import Dict, Protocol, Type - - -class Localizer(Protocol): - def localize(self, msg: str) -> str: ... - - -class GreekLocalizer: - """A simple localizer a la gettext""" +class GreekGetter: def __init__(self) -> None: - self.translations = {"dog": "σκύλος", "cat": "γάτα"} + self.trans: Dict[str, str] = { + "dog": "σκύλος", + "cat": "γάτα", + } - def localize(self, msg: str) -> str: - """We'll punt if we don't have a translation""" - return self.translations.get(msg, msg) + def get(self, msg: str) -> str: + return self.trans.get(msg, msg) -class EnglishLocalizer: - """Simply echoes the message""" - - def localize(self, msg: str) -> str: +class EnglishGetter: + def get(self, msg: str) -> str: return msg -def get_localizer(language: str = "English") -> Localizer: - """Factory""" - localizers: Dict[str, Type[Localizer]] = { - "English": EnglishLocalizer, - "Greek": GreekLocalizer, +def get_localizer(language: str = "English") -> GreekGetter | EnglishGetter: + languages: Dict[str, Type[GreekGetter | EnglishGetter]] = { + "English": EnglishGetter, + "Greek": GreekGetter, } - - return localizers.get(language, EnglishLocalizer)() - - -def main(): - """ - # Create our localizers - >>> e, g = get_localizer(language="English"), get_localizer(language="Greek") - - # Localize some text - >>> for msg in "dog parrot cat bear".split(): - ... print(e.localize(msg), g.localize(msg)) - dog σκύλος - parrot parrot - cat γάτα - bear bear - """ + return languages[language]() if __name__ == "__main__": - import doctest - - doctest.testmod() + for msg in ["dog", "cat", "bird"]: + f = get_localizer("English") + g = get_localizer("Greek") + print(f"{f.get(msg)} == {g.get(msg)}") diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py index 02f61791..a0580222 100644 --- a/patterns/creational/pool.py +++ b/patterns/creational/pool.py @@ -27,6 +27,7 @@ *TL;DR Stores a set of initialized objects kept ready to use. """ + from queue import Queue from types import TracebackType from typing import Union diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 4c2dd7ed..193089a0 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -1,83 +1,34 @@ -""" -*What is this pattern about? -This patterns aims to reduce the number of classes required by an -application. Instead of relying on subclasses it creates objects by -copying a prototypical instance at run-time. - -This is useful as it makes it easier to derive new kinds of objects, -when instances of the class have only a few different combinations of -state, and when instantiation is expensive. - -*What does this example do? -When the number of prototypes in an application can vary, it can be -useful to keep a Dispatcher (aka, Registry or Manager). This allows -clients to query the Dispatcher for a prototype before cloning a new -instance. - -Below provides an example of such Dispatcher, which contains three -copies of the prototype: 'default', 'objecta' and 'objectb'. - -*TL;DR -Creates new object instances by cloning prototype. -""" - from __future__ import annotations -from typing import Any +import copy +from typing import Any, Dict class Prototype: - def __init__(self, value: str = "default", **attrs: Any) -> None: - self.value = value - self.__dict__.update(attrs) - - def clone(self, **attrs: Any) -> Prototype: - """Clone a prototype and update inner attributes dictionary""" - # Python in Practice, Mark Summerfield - # copy.deepcopy can be used instead of next line. - obj = self.__class__(**self.__dict__) - obj.__dict__.update(attrs) - return obj + def __init__(self) -> None: + self._objects: Dict[str, Any] = {} - -class PrototypeDispatcher: - def __init__(self): - self._objects = {} - - def get_objects(self) -> dict[str, Prototype]: - """Get all objects""" - return self._objects - - def register_object(self, name: str, obj: Prototype) -> None: - """Register an object""" + def register_object(self, name: str, obj: Any) -> None: self._objects[name] = obj def unregister_object(self, name: str) -> None: - """Unregister an object""" - del self._objects[name] - + if name in self._objects: + del self._objects[name] -def main() -> None: - """ - >>> dispatcher = PrototypeDispatcher() - >>> prototype = Prototype() - - >>> d = prototype.clone() - >>> a = prototype.clone(value='a-value', category='a') - >>> b = a.clone(value='b-value', is_checked=True) - >>> dispatcher.register_object('objecta', a) - >>> dispatcher.register_object('objectb', b) - >>> dispatcher.register_object('default', d) + def clone(self, name: str, **attrs: Any) -> Any: + obj = copy.deepcopy(self._objects.get(name)) + if obj: + obj.__dict__.update(attrs) + return obj - >>> [{n: p.value} for n, p in dispatcher.get_objects().items()] - [{'objecta': 'a-value'}, {'objectb': 'b-value'}, {'default': 'default'}] - >>> print(b.category, b.is_checked) - a True - """ +class A: + def __str__(self) -> str: + return "I am A" if __name__ == "__main__": - import doctest - - doctest.testmod() + prototype = Prototype() + prototype.register_object("a", A()) + b = prototype.clone("a", name="I am B") + print(b.name) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 0269a3e7..38e27ebf 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -9,8 +9,8 @@ https://en.wikipedia.org/wiki/Blackboard_system """ -from abc import ABC, abstractmethod import random +from abc import ABC, abstractmethod class AbstractExpert(ABC): diff --git a/patterns/structural/adapter.py b/patterns/structural/adapter.py index 22adca88..078a3365 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -1,36 +1,6 @@ -""" -*What is this pattern about? -The Adapter pattern provides a different interface for a class. We can -think about it as a cable adapter that allows you to charge a phone -somewhere that has outlets in a different shape. Following this idea, -the Adapter pattern is useful to integrate classes that couldn't be -integrated due to their incompatible interfaces. +from __future__ import annotations -*What does this example do? - -The example has classes that represent entities (Dog, Cat, Human, Car) -that make different noises. The Adapter class provides a different -interface to the original methods that make such noises. So the -original interfaces (e.g., bark and meow) are available under a -different name: make_noise. - -*Where is the pattern used practically? -The Grok framework uses adapters to make objects work with a -particular API without modifying the objects themselves: -http://grok.zope.org/doc/current/grok_overview.html#adapters - -*References: -http://ginstrom.com/scribbles/2008/11/06/generic-adapter-class-in-python/ -https://sourcemaking.com/design_patterns/adapter -http://python-3-patterns-idioms-test.readthedocs.io/en/latest/ChangeInterface.html#adapter - -*TL;DR -Allows the interface of an existing class to be used as another interface. -""" - -from typing import Callable, TypeVar, Any, Dict - -T = TypeVar("T") +from typing import Any, Callable class Dog: @@ -49,77 +19,21 @@ def meow(self) -> str: return "meow!" -class Human: - def __init__(self) -> None: - self.name = "Human" - - def speak(self) -> str: - return "'hello'" - - -class Car: - def __init__(self) -> None: - self.name = "Car" - - def make_noise(self, octane_level: int) -> str: - return f"vroom{'!' * octane_level}" - - class Adapter: - """Adapts an object by replacing methods. - - Usage - ------ - dog = Dog() - dog = Adapter(dog, make_noise=dog.bark) - """ - - def __init__(self, obj: T, **adapted_methods: Callable[..., Any]) -> None: - """We set the adapted methods in the object's dict.""" + def __init__(self, obj: Any, **adapted_methods: Callable) -> None: self.obj = obj self.__dict__.update(adapted_methods) def __getattr__(self, attr: str) -> Any: - """All non-adapted calls are passed to the object.""" return getattr(self.obj, attr) - def original_dict(self) -> Dict[str, Any]: - """Print original object dict.""" - return self.obj.__dict__ - - -def main(): - """ - >>> objects = [] - >>> dog = Dog() - >>> print(dog.__dict__) - {'name': 'Dog'} - - >>> objects.append(Adapter(dog, make_noise=dog.bark)) - - >>> objects[0].__dict__['obj'], objects[0].__dict__['make_noise'] - (<...Dog object at 0x...>, >) - - >>> print(objects[0].original_dict()) - {'name': 'Dog'} - - >>> cat = Cat() - >>> objects.append(Adapter(cat, make_noise=cat.meow)) - >>> human = Human() - >>> objects.append(Adapter(human, make_noise=human.speak)) - >>> car = Car() - >>> objects.append(Adapter(car, make_noise=lambda: car.make_noise(3))) - - >>> for obj in objects: - ... print("A {0} goes {1}".format(obj.name, obj.make_noise())) - A Dog goes woof! - A Cat goes meow! - A Human goes 'hello' - A Car goes vroom!!! - """ - if __name__ == "__main__": - import doctest + objects = [] + dog = Dog() + objects.append(Adapter(dog, make_noise=dog.bark)) + cat = Cat() + objects.append(Adapter(cat, make_noise=cat.meow)) - doctest.testmod(optionflags=doctest.ELLIPSIS) + for obj in objects: + print(f"A {obj.name} goes {obj.make_noise()}") diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py index 1575cb53..858af1b5 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -1,57 +1,26 @@ -""" -*References: -http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python +from __future__ import annotations -*TL;DR -Decouples an abstraction from its implementation. -""" -from typing import Union +from typing import Protocol -# ConcreteImplementor 1/2 -class DrawingAPI1: - def draw_circle(self, x: int, y: int, radius: float) -> None: - print(f"API1.circle at {x}:{y} radius {radius}") +class Implementation(Protocol): + def operation_implementation(self) -> str: ... -# ConcreteImplementor 2/2 -class DrawingAPI2: - def draw_circle(self, x: int, y: int, radius: float) -> None: - print(f"API2.circle at {x}:{y} radius {radius}") +class Abstraction: + def __init__(self, implementation: Implementation) -> None: + self.implementation = implementation + def operation(self) -> str: + return f"Abstraction: Base operation with:\n{self.implementation.operation_implementation()}" -# Refined Abstraction -class CircleShape: - def __init__( - self, x: int, y: int, radius: int, drawing_api: Union[DrawingAPI2, DrawingAPI1] - ) -> None: - self._x = x - self._y = y - self._radius = radius - self._drawing_api = drawing_api - # low-level i.e. Implementation specific - def draw(self) -> None: - self._drawing_api.draw_circle(self._x, self._y, self._radius) - - # high-level i.e. Abstraction specific - def scale(self, pct: float) -> None: - self._radius *= pct - - -def main(): - """ - >>> shapes = (CircleShape(1, 2, 3, DrawingAPI1()), CircleShape(5, 7, 11, DrawingAPI2())) - - >>> for shape in shapes: - ... shape.scale(2.5) - ... shape.draw() - API1.circle at 1:2 radius 7.5 - API2.circle at 5:7 radius 27.5 - """ +class ConcreteImplementationA: + def operation_implementation(self) -> str: + return "ConcreteImplementationA: Here's the result on the platform A." if __name__ == "__main__": - import doctest - - doctest.testmod() + implementation = ConcreteImplementationA() + abstraction = Abstraction(implementation) + print(abstraction.operation()) diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index a4bedc1d..4ff6d82b 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -1,93 +1,36 @@ -""" -*What is this pattern about? -The composite pattern describes a group of objects that is treated the -same way as a single instance of the same type of object. The intent of -a composite is to "compose" objects into tree structures to represent -part-whole hierarchies. Implementing the composite pattern lets clients -treat individual objects and compositions uniformly. - -*What does this example do? -The example implements a graphic class,which can be either an ellipse -or a composition of several graphics. Every graphic can be printed. - -*Where is the pattern used practically? -In graphics editors a shape can be basic or complex. An example of a -simple shape is a line, where a complex shape is a rectangle which is -made of four line objects. Since shapes have many operations in common -such as rendering the shape to screen, and since shapes follow a -part-whole hierarchy, composite pattern can be used to enable the -program to deal with all shapes uniformly. - -*References: -https://en.wikipedia.org/wiki/Composite_pattern -https://infinitescript.com/2014/10/the-23-gang-of-three-design-patterns/ - -*TL;DR -Describes a group of objects that is treated as a single instance. -""" +from __future__ import annotations from abc import ABC, abstractmethod from typing import List -class Graphic(ABC): +class Component(ABC): @abstractmethod - def render(self) -> None: - raise NotImplementedError("You should implement this!") - - -class CompositeGraphic(Graphic): - def __init__(self) -> None: - self.graphics: List[Graphic] = [] - - def render(self) -> None: - for graphic in self.graphics: - graphic.render() + def execute(self) -> None: ... - def add(self, graphic: Graphic) -> None: - self.graphics.append(graphic) - def remove(self, graphic: Graphic) -> None: - self.graphics.remove(graphic) +class Leaf(Component): + def execute(self) -> None: + print("Leaf executed") -class Ellipse(Graphic): - def __init__(self, name: str) -> None: - self.name = name - - def render(self) -> None: - print(f"Ellipse: {self.name}") - - -def main(): - """ - >>> ellipse1 = Ellipse("1") - >>> ellipse2 = Ellipse("2") - >>> ellipse3 = Ellipse("3") - >>> ellipse4 = Ellipse("4") - - >>> graphic1 = CompositeGraphic() - >>> graphic2 = CompositeGraphic() - - >>> graphic1.add(ellipse1) - >>> graphic1.add(ellipse2) - >>> graphic1.add(ellipse3) - >>> graphic2.add(ellipse4) - - >>> graphic = CompositeGraphic() +class Composite(Component): + def __init__(self) -> None: + self._children: List[Component] = [] - >>> graphic.add(graphic1) - >>> graphic.add(graphic2) + def add(self, component: Component) -> None: + self._children.append(component) - >>> graphic.render() - Ellipse: 1 - Ellipse: 2 - Ellipse: 3 - Ellipse: 4 - """ + def execute(self) -> None: + print("Composite executing children:") + for child in self._children: + child.execute() if __name__ == "__main__": - import doctest - - doctest.testmod() + root = Composite() + root.add(Leaf()) + sub = Composite() + sub.add(Leaf()) + root.add(sub) + root.execute() diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index a32e2b06..9976c4f4 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -1,74 +1,32 @@ -""" -*What is this pattern about? -The Decorator pattern is used to dynamically add a new feature to an -object without changing its implementation. It differs from -inheritance because the new feature is added only to that particular -object, not to the entire subclass. +from __future__ import annotations -*What does this example do? -This example shows a way to add formatting options (boldface and -italic) to a text by appending the corresponding tags ( and -). Also, we can see that decorators can be applied one after the other, -since the original text is passed to the bold wrapper, which in turn -is passed to the italic wrapper. +from functools import wraps +from typing import Any, Callable, TypeVar -*Where is the pattern used practically? -The Grok framework uses decorators to add functionalities to methods, -like permissions or subscription to an event: -http://grok.zope.org/doc/current/reference/decorators.html +F = TypeVar("F", bound=Callable[..., Any]) -*References: -https://sourcemaking.com/design_patterns/decorator -*TL;DR -Adds behaviour to object without affecting its class. -""" +def bold(fn: F) -> F: + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> str: + return f"{fn(*args, **kwargs)}" + return wrapper # type: ignore -class TextTag: - """Represents a base text tag""" - def __init__(self, text: str) -> None: - self._text = text +def italic(fn: F) -> F: + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> str: + return f"{fn(*args, **kwargs)}" - def render(self) -> str: - return self._text + return wrapper # type: ignore -class BoldWrapper(TextTag): - """Wraps a tag in """ - - def __init__(self, wrapped: TextTag) -> None: - self._wrapped = wrapped - - def render(self) -> str: - return f"{self._wrapped.render()}" - - -class ItalicWrapper(TextTag): - """Wraps a tag in """ - - def __init__(self, wrapped: TextTag) -> None: - self._wrapped = wrapped - - def render(self) -> str: - return f"{self._wrapped.render()}" - - -def main(): - """ - >>> simple_hello = TextTag("hello, world!") - >>> special_hello = ItalicWrapper(BoldWrapper(simple_hello)) - - >>> print("before:", simple_hello.render()) - before: hello, world! - - >>> print("after:", special_hello.render()) - after: hello, world! - """ +@bold +@italic +def hello() -> str: + return "hello world" if __name__ == "__main__": - import doctest - - doctest.testmod() + print(hello()) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index 68b6f43c..878874d2 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -1,85 +1,30 @@ -""" -*What is this pattern about? -This pattern aims to minimise the number of objects that are needed by -a program at run-time. A Flyweight is an object shared by multiple -contexts, and is indistinguishable from an object that is not shared. - -The state of a Flyweight should not be affected by it's context, this -is known as its intrinsic state. The decoupling of the objects state -from the object's context, allows the Flyweight to be shared. - -*What does this example do? -The example below sets-up an 'object pool' which stores initialised -objects. When a 'Card' is created it first checks to see if it already -exists instead of creating a new one. This aims to reduce the number of -objects initialised by the program. - -*References: -http://codesnipers.com/?q=python-flyweights -https://python-patterns.guide/gang-of-four/flyweight/ - -*Examples in Python ecosystem: -https://docs.python.org/3/library/sys.html#sys.intern - -*TL;DR -Minimizes memory usage by sharing data with other similar objects. -""" +from __future__ import annotations import weakref +from typing import Tuple class Card: - """The Flyweight""" - - # Could be a simple dict. - # With WeakValueDictionary garbage collection can reclaim the object - # when there are no other references to it. - _pool: weakref.WeakValueDictionary = weakref.WeakValueDictionary() - - def __new__(cls, value: str, suit: str): - # If the object exists in the pool - just return it - obj = cls._pool.get(value + suit) - # otherwise - create new one (and add it to the pool) - if obj is None: - obj = object.__new__(Card) - cls._pool[value + suit] = obj - # This row does the part we usually see in `__init__` - obj.value, obj.suit = value, suit + _pool: weakref.WeakValueDictionary[Tuple[str, str], Card] = ( + weakref.WeakValueDictionary() + ) + + def __new__(cls, value: str, suit: str) -> Card: + obj = cls._pool.get((value, suit)) + if not obj: + obj = object.__new__(cls) + cls._pool[(value, suit)] = obj return obj - # If you uncomment `__init__` and comment-out `__new__` - - # Card becomes normal (non-flyweight). - # def __init__(self, value, suit): - # self.value, self.suit = value, suit + def __init__(self, value: str, suit: str) -> None: + self.value = value + self.suit = suit def __repr__(self) -> str: return f"" -def main(): - """ - >>> c1 = Card('9', 'h') - >>> c2 = Card('9', 'h') - >>> c1, c2 - (, ) - >>> c1 == c2 - True - >>> c1 is c2 - True - - >>> c1.new_attr = 'temp' - >>> c3 = Card('9', 'h') - >>> hasattr(c3, 'new_attr') - True - - >>> Card._pool.clear() - >>> c4 = Card('9', 'h') - >>> hasattr(c4, 'new_attr') - False - """ - - if __name__ == "__main__": - import doctest - - doctest.testmod() + c1 = Card("9", "h") + c2 = Card("9", "h") + print(f"c1 is c2: {c1 is c2}") diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 0a7c4034..5ee0de42 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -4,9 +4,9 @@ """ from abc import ABC, abstractmethod -from typing import Dict, List, Union, Any from inspect import signature from sys import argv +from typing import Any, Dict, List, Union class Model(ABC): diff --git a/patterns/structural/proxy.py b/patterns/structural/proxy.py index 3ef74ec0..a1e663ee 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -1,91 +1,35 @@ -""" -*What is this pattern about? -Proxy is used in places where you want to add functionality to a class without -changing its interface. The main class is called `Real Subject`. A client should -use the proxy or the real subject without any code change, so both must have the -same interface. Logging and controlling access to the real subject are some of -the proxy pattern usages. +from __future__ import annotations -*References: -https://refactoring.guru/design-patterns/proxy/python/example -https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Fronting.html +from typing import Protocol -*TL;DR -Add functionality or logic (e.g. logging, caching, authorization) to a resource -without changing its interface. -""" -from typing import Union +class Subject(Protocol): + def request(self) -> None: ... -class Subject: - """ - As mentioned in the document, interfaces of both RealSubject and Proxy should - be the same, because the client should be able to use RealSubject or Proxy with - no code change. +class RealSubject: + def request(self) -> None: + print("RealSubject: Handling request.") - Not all times this interface is necessary. The point is the client should be - able to use RealSubject or Proxy interchangeably with no change in code. - """ - def do_the_job(self, user: str) -> None: - raise NotImplementedError() +class Proxy: + def __init__(self, real_subject: RealSubject) -> None: + self._real_subject = real_subject + def request(self) -> None: + if self.check_access(): + self._real_subject.request() + self.log_access() -class RealSubject(Subject): - """ - This is the main job doer. External services like payment gateways can be a - good example. - """ + def check_access(self) -> bool: + print("Proxy: Checking access...") + return True - def do_the_job(self, user: str) -> None: - print(f"I am doing the job for {user}") - - -class Proxy(Subject): - def __init__(self) -> None: - self._real_subject = RealSubject() - - def do_the_job(self, user: str) -> None: - """ - logging and controlling access are some examples of proxy usages. - """ - - print(f"[log] Doing the job for {user} is requested.") - - if user == "admin": - self._real_subject.do_the_job(user) - else: - print("[log] I can do the job just for `admins`.") - - -def client(job_doer: Union[RealSubject, Proxy], user: str) -> None: - job_doer.do_the_job(user) - - -def main(): - """ - >>> proxy = Proxy() - - >>> real_subject = RealSubject() - - >>> client(proxy, 'admin') - [log] Doing the job for admin is requested. - I am doing the job for admin - - >>> client(proxy, 'anonymous') - [log] Doing the job for anonymous is requested. - [log] I can do the job just for `admins`. - - >>> client(real_subject, 'admin') - I am doing the job for admin - - >>> client(real_subject, 'anonymous') - I am doing the job for anonymous - """ + def log_access(self) -> None: + print("Proxy: Logging the time of request.") if __name__ == "__main__": - import doctest - - doctest.testmod() + real_subject = RealSubject() + proxy = Proxy(real_subject) + proxy.request() diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index dfac5da9..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,120 +0,0 @@ -[build-system] -requires = ["setuptools >= 77.0.3"] -build-backend = "setuptools.build_meta" - -[project] -name = "python-patterns" -description = "A collection of design patterns and idioms in Python." -version = "0.1.0" -readme = "README.md" -requires-python = ">=3.10" -classifiers = [ - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", -] -dependencies= [ -] - -maintainers=[ - { name="faif" } -] - -[project.urls] -Homepage = "https://github.com/faif/python-patterns" -Repository = "https://github.com/faif/python-patterns" -"Bug Tracker" = "https://github.com/faif/python-patterns/issues" -Contributors = "https://github.com/faif/python-patterns/graphs/contributors" - -[project.optional-dependencies] -dev = [ - "mypy", - "pipx>=1.7.1", - "pyupgrade", - "pytest>=6.2.0", - "pytest-cov>=2.11.0", - "pytest-randomly>=3.1.0", - "black>=25.1.0", - "build>=1.2.2", - "isort>=5.7.0", - "flake8>=7.1.0", - "tox>=4.25.0" -] - -[tool.setuptools] -packages = ["patterns"] - -[tool.pytest.ini_options] -filterwarnings = [ - "ignore::Warning:.*test class 'TestRunner'.*" -] -# Adding settings from tox.ini for pytest -testpaths = ["tests"] -#testpaths = ["tests", "patterns"] -python_files = ["test_*.py", "*_test.py"] -# Enable doctest discovery in patterns directory -addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing" -doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"] -log_level = "INFO" - -[tool.coverage.run] -branch = true -source = ["./"] -#source = ["patterns"] -# Ensure coverage data is collected properly -relative_files = true -parallel = true -dynamic_context = "test_function" -data_file = ".coverage" - -[tool.coverage.report] -# Regexes for lines to exclude from consideration -exclude_lines = [ - "def __repr__", - "if self\\.debug", - "raise AssertionError", - "raise NotImplementedError", - "if 0:", - "if __name__ == .__main__.:", - "@(abc\\.)?abstractmethod" -] -ignore_errors = true - -[tool.coverage.html] -directory = "coverage_html_report" - -[tool.mypy] -python_version = "3.12" -ignore_missing_imports = true - -[tool.flake8] -max-line-length = 120 -ignore = ["E266", "E731", "W503"] -exclude = ["venv*"] - -[tool.tox] -legacy_tox_ini = """ -[tox] -envlist = py312,cov-report -skip_missing_interpreters = true -usedevelop = true - -#[testenv] -#setenv = -# COVERAGE_FILE = .coverage.{envname} -#deps = -# -r requirements-dev.txt -#commands = -# flake8 --exclude="venv/,.tox/" patterns/ -# coverage run -m pytest --randomly-seed=1234 --doctest-modules patterns/ -# coverage run -m pytest -s -vv --cov=patterns/ --log-level=INFO tests/ - -#[testenv:cov-report] -#setenv = -# COVERAGE_FILE = .coverage -#deps = coverage -#commands = -# coverage combine -# coverage report -#""" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/behavioral/test_catalog.py b/tests/behavioral/test_catalog.py deleted file mode 100644 index 60933816..00000000 --- a/tests/behavioral/test_catalog.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - -from patterns.behavioral.catalog import Catalog, CatalogClass, CatalogInstance, CatalogStatic - -def test_catalog_multiple_methods(): - test = Catalog('param_value_2') - token = test.main_method() - assert token == 'executed method 2!' - -def test_catalog_multiple_instance_methods(): - test = CatalogInstance('param_value_1') - token = test.main_method() - assert token == 'Value x1' - -def test_catalog_multiple_class_methods(): - test = CatalogClass('param_value_2') - token = test.main_method() - assert token == 'Value x2' - -def test_catalog_multiple_static_methods(): - test = CatalogStatic('param_value_1') - token = test.main_method() - assert token == 'executed method 1!' diff --git a/tests/behavioral/test_mediator.py b/tests/behavioral/test_mediator.py deleted file mode 100644 index 1af60e67..00000000 --- a/tests/behavioral/test_mediator.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from patterns.behavioral.mediator import User - -def test_mediated_comments(): - molly = User('Molly') - mediated_comment = molly.say("Hi Team! Meeting at 3 PM today.") - assert mediated_comment == "[Molly says]: Hi Team! Meeting at 3 PM today." - - mark = User('Mark') - mediated_comment = mark.say("Roger that!") - assert mediated_comment == "[Mark says]: Roger that!" - - ethan = User('Ethan') - mediated_comment = ethan.say("Alright.") - assert mediated_comment == "[Ethan says]: Alright." diff --git a/tests/behavioral/test_memento.py b/tests/behavioral/test_memento.py deleted file mode 100644 index bd307b76..00000000 --- a/tests/behavioral/test_memento.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest - -from patterns.behavioral.memento import NumObj, Transaction - -def test_object_creation(): - num_obj = NumObj(-1) - assert repr(num_obj) == '', "Object representation not as expected" - -def test_rollback_on_transaction(): - num_obj = NumObj(-1) - a_transaction = Transaction(True, num_obj) - for _i in range(3): - num_obj.increment() - a_transaction.commit() - assert num_obj.value == 2 - - for _i in range(3): - num_obj.increment() - try: - num_obj.value += 'x' # will fail - except TypeError: - a_transaction.rollback() - assert num_obj.value == 2, "Transaction did not rollback as expected" - -def test_rollback_with_transactional_annotation(): - num_obj = NumObj(2) - with pytest.raises(TypeError): - num_obj.do_stuff() - assert num_obj.value == 2 diff --git a/tests/behavioral/test_observer.py b/tests/behavioral/test_observer.py deleted file mode 100644 index 821f97a6..00000000 --- a/tests/behavioral/test_observer.py +++ /dev/null @@ -1,33 +0,0 @@ -from unittest.mock import Mock, patch - -import pytest - -from patterns.behavioral.observer import Data, DecimalViewer, HexViewer - - -@pytest.fixture -def observable(): - return Data("some data") - - -def test_attach_detach(observable): - decimal_viewer = DecimalViewer() - assert len(observable._observers) == 0 - - observable.attach(decimal_viewer) - assert decimal_viewer in observable._observers - - observable.detach(decimal_viewer) - assert decimal_viewer not in observable._observers - - -def test_one_data_change_notifies_each_observer_once(observable): - observable.attach(DecimalViewer()) - observable.attach(HexViewer()) - - with patch( - "patterns.behavioral.observer.DecimalViewer.update", new_callable=Mock() - ) as mocked_update: - assert mocked_update.call_count == 0 - observable.data = 10 - assert mocked_update.call_count == 1 diff --git a/tests/behavioral/test_publish_subscribe.py b/tests/behavioral/test_publish_subscribe.py deleted file mode 100644 index 8bb7130c..00000000 --- a/tests/behavioral/test_publish_subscribe.py +++ /dev/null @@ -1,70 +0,0 @@ -import unittest -from unittest.mock import call, patch - -from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber - - -class TestProvider(unittest.TestCase): - """ - Integration tests ~ provider class with as little mocking as possible. - """ - - def test_subscriber_shall_be_attachable_to_subscriptions(cls): - subscription = "sub msg" - pro = Provider() - cls.assertEqual(len(pro.subscribers), 0) - sub = Subscriber("sub name", pro) - sub.subscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 1) - - def test_subscriber_shall_be_detachable_from_subscriptions(cls): - subscription = "sub msg" - pro = Provider() - sub = Subscriber("sub name", pro) - sub.subscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 1) - sub.unsubscribe(subscription) - cls.assertEqual(len(pro.subscribers[subscription]), 0) - - def test_publisher_shall_append_subscription_message_to_queue(cls): - """msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg)""" - expected_msg = "expected msg" - pro = Provider() - pub = Publisher(pro) - Subscriber("sub name", pro) - cls.assertEqual(len(pro.msg_queue), 0) - pub.publish(expected_msg) - cls.assertEqual(len(pro.msg_queue), 1) - cls.assertEqual(pro.msg_queue[0], expected_msg) - - def test_provider_shall_update_affected_subscribers_with_published_subscription( - cls, - ): - pro = Provider() - pub = Publisher(pro) - sub1 = Subscriber("sub 1 name", pro) - sub1.subscribe("sub 1 msg 1") - sub1.subscribe("sub 1 msg 2") - sub2 = Subscriber("sub 2 name", pro) - sub2.subscribe("sub 2 msg 1") - sub2.subscribe("sub 2 msg 2") - with ( - patch.object(sub1, "run") as mock_subscriber1_run, - patch.object(sub2, "run") as mock_subscriber2_run, - ): - pro.update() - cls.assertEqual(mock_subscriber1_run.call_count, 0) - cls.assertEqual(mock_subscriber2_run.call_count, 0) - pub.publish("sub 1 msg 1") - pub.publish("sub 1 msg 2") - pub.publish("sub 2 msg 1") - pub.publish("sub 2 msg 2") - with ( - patch.object(sub1, "run") as mock_subscriber1_run, - patch.object(sub2, "run") as mock_subscriber2_run, - ): - pro.update() - expected_sub1_calls = [call("sub 1 msg 1"), call("sub 1 msg 2")] - mock_subscriber1_run.assert_has_calls(expected_sub1_calls) - expected_sub2_calls = [call("sub 2 msg 1"), call("sub 2 msg 2")] - mock_subscriber2_run.assert_has_calls(expected_sub2_calls) diff --git a/tests/behavioral/test_servant.py b/tests/behavioral/test_servant.py deleted file mode 100644 index dd487171..00000000 --- a/tests/behavioral/test_servant.py +++ /dev/null @@ -1,39 +0,0 @@ -from patterns.behavioral.servant import GeometryTools, Circle, Rectangle, Position -import pytest -import math - - -@pytest.fixture -def circle(): - return Circle(3, Position(0, 0)) - - -@pytest.fixture -def rectangle(): - return Rectangle(4, 5, Position(0, 0)) - - -def test_calculate_area(circle, rectangle): - assert GeometryTools.calculate_area(circle) == math.pi * 3**2 - assert GeometryTools.calculate_area(rectangle) == 4 * 5 - - with pytest.raises(ValueError): - GeometryTools.calculate_area("invalid shape") - - -def test_calculate_perimeter(circle, rectangle): - assert GeometryTools.calculate_perimeter(circle) == 2 * math.pi * 3 - assert GeometryTools.calculate_perimeter(rectangle) == 2 * (4 + 5) - - with pytest.raises(ValueError): - GeometryTools.calculate_perimeter("invalid shape") - - -def test_move_to(circle, rectangle): - new_position = Position(1, 1) - GeometryTools.move_to(circle, new_position) - assert circle.position == new_position - - new_position = Position(1, 1) - GeometryTools.move_to(rectangle, new_position) - assert rectangle.position == new_position diff --git a/tests/behavioral/test_state.py b/tests/behavioral/test_state.py deleted file mode 100644 index 77473f51..00000000 --- a/tests/behavioral/test_state.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest - -from patterns.behavioral.state import Radio - - -@pytest.fixture -def radio(): - return Radio() - - -def test_initial_state(radio): - assert radio.state.name == "AM" - - -def test_initial_am_station(radio): - initial_pos = radio.state.pos - assert radio.state.stations[initial_pos] == "1250" - - -def test_toggle_amfm(radio): - assert radio.state.name == "AM" - - radio.toggle_amfm() - assert radio.state.name == "FM" - - radio.toggle_amfm() - assert radio.state.name == "AM" diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py deleted file mode 100644 index 53976f38..00000000 --- a/tests/behavioral/test_strategy.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest - -from patterns.behavioral.strategy import Order, on_sale_discount, ten_percent_discount - - -@pytest.fixture -def order(): - return Order(100) - - -@pytest.mark.parametrize( - "func, discount", [(ten_percent_discount, 10.0), (on_sale_discount, 45.0)] -) -def test_discount_function_return(func, order, discount): - assert func(order) == discount - - -@pytest.mark.parametrize( - "func, price", [(ten_percent_discount, 100), (on_sale_discount, 100)] -) -def test_order_discount_strategy_validate_success(func, price): - order = Order(price, func) - - assert order.price == price - assert order.discount_strategy == func - - -def test_order_discount_strategy_validate_error(): - order = Order(10, discount_strategy=on_sale_discount) - - assert order.discount_strategy is None - - -@pytest.mark.parametrize( - "func, price, discount", - [(ten_percent_discount, 100, 90.0), (on_sale_discount, 100, 55.0)], -) -def test_discount_apply_success(func, price, discount): - order = Order(price, func) - - assert order.apply_discount() == discount diff --git a/tests/behavioral/test_visitor.py b/tests/behavioral/test_visitor.py deleted file mode 100644 index 31d230de..00000000 --- a/tests/behavioral/test_visitor.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from patterns.behavioral.visitor import A, B, C, Visitor - -@pytest.fixture -def visitor(): - return Visitor() - -def test_visiting_generic_node(visitor): - a = A() - token = visitor.visit(a) - assert token == 'generic_visit A', "The expected generic object was not called" - -def test_visiting_specific_nodes(visitor): - b = B() - token = visitor.visit(b) - assert token == 'visit_B B', "The expected specific object was not called" - -def test_visiting_inherited_nodes(visitor): - c = C() - token = visitor.visit(c) - assert token == 'visit_B C', "The expected inherited object was not called" diff --git a/tests/creational/test_abstract_factory.py b/tests/creational/test_abstract_factory.py deleted file mode 100644 index 1676e59d..00000000 --- a/tests/creational/test_abstract_factory.py +++ /dev/null @@ -1,13 +0,0 @@ -import unittest -from unittest.mock import patch - -from patterns.creational.abstract_factory import Dog, PetShop - - -class TestPetShop(unittest.TestCase): - def test_dog_pet_shop_shall_show_dog_instance(self): - dog_pet_shop = PetShop(Dog) - with patch.object(Dog, "speak") as mock_Dog_speak: - pet = dog_pet_shop.buy_pet("") - pet.speak() - self.assertEqual(mock_Dog_speak.call_count, 1) diff --git a/tests/creational/test_borg.py b/tests/creational/test_borg.py deleted file mode 100644 index 182611c3..00000000 --- a/tests/creational/test_borg.py +++ /dev/null @@ -1,28 +0,0 @@ -import unittest - -from patterns.creational.borg import Borg, YourBorg - - -class BorgTest(unittest.TestCase): - def setUp(self): - self.b1 = Borg() - self.b2 = Borg() - # creating YourBorg instance implicitly sets the state attribute - # for all borg instances. - self.ib1 = YourBorg() - - def tearDown(self): - self.ib1.state = "Init" - - def test_initial_borg_state_shall_be_init(self): - b = Borg() - self.assertEqual(b.state, "Init") - - def test_changing_instance_attribute_shall_change_borg_state(self): - self.b1.state = "Running" - self.assertEqual(self.b1.state, "Running") - self.assertEqual(self.b2.state, "Running") - self.assertEqual(self.ib1.state, "Running") - - def test_instances_shall_have_own_ids(self): - self.assertNotEqual(id(self.b1), id(self.b2), id(self.ib1)) diff --git a/tests/creational/test_builder.py b/tests/creational/test_builder.py deleted file mode 100644 index 923bc4a5..00000000 --- a/tests/creational/test_builder.py +++ /dev/null @@ -1,22 +0,0 @@ -import unittest - -from patterns.creational.builder import ComplexHouse, Flat, House, construct_building - - -class TestSimple(unittest.TestCase): - def test_house(self): - house = House() - self.assertEqual(house.size, "Big") - self.assertEqual(house.floor, "One") - - def test_flat(self): - flat = Flat() - self.assertEqual(flat.size, "Small") - self.assertEqual(flat.floor, "More than One") - - -class TestComplex(unittest.TestCase): - def test_house(self): - house = construct_building(ComplexHouse) - self.assertEqual(house.size, "Big and fancy") - self.assertEqual(house.floor, "One") diff --git a/tests/creational/test_factory.py b/tests/creational/test_factory.py deleted file mode 100644 index 4bcfd4c5..00000000 --- a/tests/creational/test_factory.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest -from patterns.creational.factory import get_localizer, GreekLocalizer, EnglishLocalizer - -class TestFactory(unittest.TestCase): - def test_get_localizer_greek(self): - localizer = get_localizer("Greek") - self.assertIsInstance(localizer, GreekLocalizer) - self.assertEqual(localizer.localize("dog"), "σκύλος") - self.assertEqual(localizer.localize("cat"), "γάτα") - # Test unknown word returns the word itself - self.assertEqual(localizer.localize("monkey"), "monkey") - - def test_get_localizer_english(self): - localizer = get_localizer("English") - self.assertIsInstance(localizer, EnglishLocalizer) - self.assertEqual(localizer.localize("dog"), "dog") - self.assertEqual(localizer.localize("cat"), "cat") - - def test_get_localizer_default(self): - # Test default argument - localizer = get_localizer() - self.assertIsInstance(localizer, EnglishLocalizer) - - def test_get_localizer_unknown_language(self): - # Test fallback for unknown language if applicable, - # or just verify what happens. - # Based on implementation: localizers.get(language, EnglishLocalizer)() - # It defaults to EnglishLocalizer for unknown keys. - localizer = get_localizer("Spanish") - self.assertIsInstance(localizer, EnglishLocalizer) diff --git a/tests/creational/test_lazy.py b/tests/creational/test_lazy.py deleted file mode 100644 index 1b815b60..00000000 --- a/tests/creational/test_lazy.py +++ /dev/null @@ -1,38 +0,0 @@ -import unittest - -from patterns.creational.lazy_evaluation import Person - - -class TestDynamicExpanding(unittest.TestCase): - def setUp(self): - self.John = Person("John", "Coder") - - def test_innate_properties(self): - self.assertDictEqual( - {"name": "John", "occupation": "Coder", "call_count2": 0}, - self.John.__dict__, - ) - - def test_relatives_not_in_properties(self): - self.assertNotIn("relatives", self.John.__dict__) - - def test_extended_properties(self): - print(f"John's relatives: {self.John.relatives}") - self.assertDictEqual( - { - "name": "John", - "occupation": "Coder", - "relatives": "Many relatives.", - "call_count2": 0, - }, - self.John.__dict__, - ) - - def test_relatives_after_access(self): - print(f"John's relatives: {self.John.relatives}") - self.assertIn("relatives", self.John.__dict__) - - def test_parents(self): - for _ in range(2): - self.assertEqual(self.John.parents, "Father and mother") - self.assertEqual(self.John.call_count2, 1) diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py deleted file mode 100644 index cd501db3..00000000 --- a/tests/creational/test_pool.py +++ /dev/null @@ -1,50 +0,0 @@ -import queue -import unittest - -from patterns.creational.pool import ObjectPool - - -class TestPool(unittest.TestCase): - def setUp(self): - self.sample_queue = queue.Queue() - self.sample_queue.put("first") - self.sample_queue.put("second") - - def test_items_recoil(self): - with ObjectPool(self.sample_queue, True) as pool: - self.assertEqual(pool, "first") - self.assertTrue(self.sample_queue.get() == "second") - self.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == "first") - self.assertTrue(self.sample_queue.empty()) - - def test_frozen_pool(self): - with ObjectPool(self.sample_queue) as pool: - self.assertEqual(pool, "first") - self.assertEqual(pool, "first") - self.assertTrue(self.sample_queue.get() == "second") - self.assertFalse(self.sample_queue.empty()) - self.assertTrue(self.sample_queue.get() == "first") - self.assertTrue(self.sample_queue.empty()) - - -class TestNaitivePool(unittest.TestCase): - """def test_object(queue): - queue_object = QueueObject(queue, True) - print('Inside func: {}'.format(queue_object.object))""" - - def test_pool_behavior_with_single_object_inside(self): - sample_queue = queue.Queue() - sample_queue.put("yam") - with ObjectPool(sample_queue) as obj: - # print('Inside with: {}'.format(obj)) - self.assertEqual(obj, "yam") - self.assertFalse(sample_queue.empty()) - self.assertTrue(sample_queue.get() == "yam") - self.assertTrue(sample_queue.empty()) - - # sample_queue.put('sam') - # test_object(sample_queue) - # print('Outside func: {}'.format(sample_queue.get())) - - # if not sample_queue.empty(): diff --git a/tests/creational/test_prototype.py b/tests/creational/test_prototype.py deleted file mode 100644 index 758ac872..00000000 --- a/tests/creational/test_prototype.py +++ /dev/null @@ -1,48 +0,0 @@ -import unittest - -from patterns.creational.prototype import Prototype, PrototypeDispatcher - - -class TestPrototypeFeatures(unittest.TestCase): - def setUp(self): - self.prototype = Prototype() - - def test_cloning_propperty_innate_values(self): - sample_object_1 = self.prototype.clone() - sample_object_2 = self.prototype.clone() - self.assertEqual(sample_object_1.value, sample_object_2.value) - - def test_extended_property_values_cloning(self): - sample_object_1 = self.prototype.clone() - sample_object_1.some_value = "test string" - sample_object_2 = self.prototype.clone() - self.assertRaises(AttributeError, lambda: sample_object_2.some_value) - - def test_cloning_propperty_assigned_values(self): - sample_object_1 = self.prototype.clone() - sample_object_2 = self.prototype.clone(value="re-assigned") - self.assertNotEqual(sample_object_1.value, sample_object_2.value) - - -class TestDispatcherFeatures(unittest.TestCase): - def setUp(self): - self.dispatcher = PrototypeDispatcher() - self.prototype = Prototype() - c = self.prototype.clone() - a = self.prototype.clone(value="a-value", ext_value="E") - b = self.prototype.clone(value="b-value", diff=True) - self.dispatcher.register_object("A", a) - self.dispatcher.register_object("B", b) - self.dispatcher.register_object("C", c) - - def test_batch_retrieving(self): - self.assertEqual(len(self.dispatcher.get_objects()), 3) - - def test_particular_properties_retrieving(self): - self.assertEqual(self.dispatcher.get_objects()["A"].value, "a-value") - self.assertEqual(self.dispatcher.get_objects()["B"].value, "b-value") - self.assertEqual(self.dispatcher.get_objects()["C"].value, "default") - - def test_extended_properties_retrieving(self): - self.assertEqual(self.dispatcher.get_objects()["A"].ext_value, "E") - self.assertTrue(self.dispatcher.get_objects()["B"].diff) diff --git a/tests/fundamental/test_delegation.py b/tests/fundamental/test_delegation.py deleted file mode 100644 index 3bfd0496..00000000 --- a/tests/fundamental/test_delegation.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from patterns.fundamental.delegation_pattern import Delegator, Delegate - - -def test_delegator_delegates_attribute_and_call(): - d = Delegator(Delegate()) - assert d.p1 == 123 - assert d.do_something("something") == "Doing something" - assert d.do_something("something", kw=", hi") == "Doing something, hi" - - -def test_delegator_missing_attribute_raises(): - d = Delegator(Delegate()) - with pytest.raises(AttributeError): - _ = d.p2 diff --git a/tests/structural/test_adapter.py b/tests/structural/test_adapter.py deleted file mode 100644 index 01323075..00000000 --- a/tests/structural/test_adapter.py +++ /dev/null @@ -1,74 +0,0 @@ -import unittest - -from patterns.structural.adapter import Adapter, Car, Cat, Dog, Human - - -class ClassTest(unittest.TestCase): - def setUp(self): - self.dog = Dog() - self.cat = Cat() - self.human = Human() - self.car = Car() - - def test_dog_shall_bark(self): - noise = self.dog.bark() - expected_noise = "woof!" - self.assertEqual(noise, expected_noise) - - def test_cat_shall_meow(self): - noise = self.cat.meow() - expected_noise = "meow!" - self.assertEqual(noise, expected_noise) - - def test_human_shall_speak(self): - noise = self.human.speak() - expected_noise = "'hello'" - self.assertEqual(noise, expected_noise) - - def test_car_shall_make_loud_noise(self): - noise = self.car.make_noise(1) - expected_noise = "vroom!" - self.assertEqual(noise, expected_noise) - - def test_car_shall_make_very_loud_noise(self): - noise = self.car.make_noise(10) - expected_noise = "vroom!!!!!!!!!!" - self.assertEqual(noise, expected_noise) - - -class AdapterTest(unittest.TestCase): - def test_dog_adapter_shall_make_noise(self): - dog = Dog() - dog_adapter = Adapter(dog, make_noise=dog.bark) - noise = dog_adapter.make_noise() - expected_noise = "woof!" - self.assertEqual(noise, expected_noise) - - def test_cat_adapter_shall_make_noise(self): - cat = Cat() - cat_adapter = Adapter(cat, make_noise=cat.meow) - noise = cat_adapter.make_noise() - expected_noise = "meow!" - self.assertEqual(noise, expected_noise) - - def test_human_adapter_shall_make_noise(self): - human = Human() - human_adapter = Adapter(human, make_noise=human.speak) - noise = human_adapter.make_noise() - expected_noise = "'hello'" - self.assertEqual(noise, expected_noise) - - def test_car_adapter_shall_make_loud_noise(self): - car = Car() - car_adapter = Adapter(car, make_noise=car.make_noise) - noise = car_adapter.make_noise(1) - expected_noise = "vroom!" - self.assertEqual(noise, expected_noise) - - def test_car_adapter_shall_make_very_loud_noise(self): - car = Car() - car_adapter = Adapter(car, make_noise=car.make_noise) - noise = car_adapter.make_noise(10) - expected_noise = "vroom!!!!!!!!!!" - - self.assertEqual(noise, expected_noise) diff --git a/tests/structural/test_bridge.py b/tests/structural/test_bridge.py deleted file mode 100644 index 6665f327..00000000 --- a/tests/structural/test_bridge.py +++ /dev/null @@ -1,44 +0,0 @@ -import unittest -from unittest.mock import patch - -from patterns.structural.bridge import CircleShape, DrawingAPI1, DrawingAPI2 - - -class BridgeTest(unittest.TestCase): - def test_bridge_shall_draw_with_concrete_api_implementation(cls): - ci1 = DrawingAPI1() - ci2 = DrawingAPI2() - with ( - patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, - patch.object(ci2, "draw_circle") as mock_ci2_draw_circle, - ): - sh1 = CircleShape(1, 2, 3, ci1) - sh1.draw() - cls.assertEqual(mock_ci1_draw_circle.call_count, 1) - sh2 = CircleShape(1, 2, 3, ci2) - sh2.draw() - cls.assertEqual(mock_ci2_draw_circle.call_count, 1) - - def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): - SCALE_FACTOR = 2 - CIRCLE1_RADIUS = 3 - EXPECTED_CIRCLE1_RADIUS = 6 - CIRCLE2_RADIUS = CIRCLE1_RADIUS * CIRCLE1_RADIUS - EXPECTED_CIRCLE2_RADIUS = CIRCLE2_RADIUS * SCALE_FACTOR - - ci1 = DrawingAPI1() - ci2 = DrawingAPI2() - sh1 = CircleShape(1, 2, CIRCLE1_RADIUS, ci1) - sh2 = CircleShape(1, 2, CIRCLE2_RADIUS, ci2) - sh1.scale(SCALE_FACTOR) - sh2.scale(SCALE_FACTOR) - cls.assertEqual(sh1._radius, EXPECTED_CIRCLE1_RADIUS) - cls.assertEqual(sh2._radius, EXPECTED_CIRCLE2_RADIUS) - with ( - patch.object(sh1, "scale") as mock_sh1_scale_circle, - patch.object(sh2, "scale") as mock_sh2_scale_circle, - ): - sh1.scale(2) - sh2.scale(2) - cls.assertEqual(mock_sh1_scale_circle.call_count, 1) - cls.assertEqual(mock_sh2_scale_circle.call_count, 1) diff --git a/tests/structural/test_decorator.py b/tests/structural/test_decorator.py deleted file mode 100644 index 8a4154a9..00000000 --- a/tests/structural/test_decorator.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from patterns.structural.decorator import BoldWrapper, ItalicWrapper, TextTag - - -class TestTextWrapping(unittest.TestCase): - def setUp(self): - self.raw_string = TextTag("raw but not cruel") - - def test_italic(self): - self.assertEqual( - ItalicWrapper(self.raw_string).render(), "raw but not cruel" - ) - - def test_bold(self): - self.assertEqual( - BoldWrapper(self.raw_string).render(), "raw but not cruel" - ) - - def test_mixed_bold_and_italic(self): - self.assertEqual( - BoldWrapper(ItalicWrapper(self.raw_string)).render(), - "raw but not cruel", - ) diff --git a/tests/structural/test_facade.py b/tests/structural/test_facade.py deleted file mode 100644 index 2ff24ca3..00000000 --- a/tests/structural/test_facade.py +++ /dev/null @@ -1,11 +0,0 @@ -from patterns.structural.facade import ComputerFacade - - -def test_computer_facade_start(capsys): - cf = ComputerFacade() - cf.start() - out = capsys.readouterr().out - assert "Freezing processor." in out - assert "Loading from 0x00 data:" in out - assert "Jumping to: 0x00" in out - assert "Executing." in out diff --git a/tests/structural/test_flyweight.py b/tests/structural/test_flyweight.py deleted file mode 100644 index a200203f..00000000 --- a/tests/structural/test_flyweight.py +++ /dev/null @@ -1,20 +0,0 @@ -from patterns.structural.flyweight import Card - - -def test_card_flyweight_identity_and_repr(): - c1 = Card("9", "h") - c2 = Card("9", "h") - assert c1 is c2 - assert repr(c1) == "" - - -def test_card_attribute_persistence_and_pool_clear(): - Card._pool.clear() - c1 = Card("A", "s") - c1.temp = "t" - c2 = Card("A", "s") - assert hasattr(c2, "temp") - - Card._pool.clear() - c3 = Card("A", "s") - assert not hasattr(c3, "temp") diff --git a/tests/structural/test_mvc.py b/tests/structural/test_mvc.py deleted file mode 100644 index 5991c511..00000000 --- a/tests/structural/test_mvc.py +++ /dev/null @@ -1,67 +0,0 @@ -import pytest - -from patterns.structural.mvc import ( - ProductModel, - ConsoleView, - Controller, - Router, -) - - -def test_productmodel_iteration_and_price_str(): - pm = ProductModel() - items = list(pm) - assert set(items) == {"milk", "eggs", "cheese"} - - info = pm.get("cheese") - assert info["quantity"] == 10 - assert str(info["price"]) == "2.00" - - -def test_productmodel_get_raises_keyerror(): - pm = ProductModel() - with pytest.raises(KeyError) as exc: - pm.get("unknown_item") - assert "not in the model's item list." in str(exc.value) - - -def test_consoleview_capitalizer_and_list_and_info(capsys): - view = ConsoleView() - # capitalizer - assert view.capitalizer("heLLo") == "Hello" - - # show item list - view.show_item_list("product", ["x", "y"]) - out = capsys.readouterr().out - assert "PRODUCT LIST:" in out - assert "x" in out and "y" in out - - # show item information formatting - pm = ProductModel() - controller = Controller(pm, view) - controller.show_item_information("milk") - out = capsys.readouterr().out - assert "PRODUCT INFORMATION:" in out - assert "Name: milk" in out - assert "Price: 1.50" in out - assert "Quantity: 10" in out - - -def test_show_item_information_missing_calls_item_not_found(capsys): - view = ConsoleView() - pm = ProductModel() - controller = Controller(pm, view) - - controller.show_item_information("arepas") - out = capsys.readouterr().out - assert 'That product "arepas" does not exist in the records' in out - - -def test_router_register_resolve_and_unknown(): - router = Router() - router.register("products", Controller, ProductModel, ConsoleView) - controller = router.resolve("products") - assert isinstance(controller, Controller) - - with pytest.raises(KeyError): - router.resolve("no-such-path") diff --git a/tests/structural/test_proxy.py b/tests/structural/test_proxy.py deleted file mode 100644 index 3409bf0b..00000000 --- a/tests/structural/test_proxy.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys -import unittest -from io import StringIO - -from patterns.structural.proxy import Proxy, client - - -class ProxyTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - """Class scope setup.""" - cls.proxy = Proxy() - - def setUp(cls): - """Function/test case scope setup.""" - cls.output = StringIO() - cls.saved_stdout = sys.stdout - sys.stdout = cls.output - - def tearDown(cls): - """Function/test case scope teardown.""" - cls.output.close() - sys.stdout = cls.saved_stdout - - def test_do_the_job_for_admin_shall_pass(self): - client(self.proxy, "admin") - assert self.output.getvalue() == ( - "[log] Doing the job for admin is requested.\n" - "I am doing the job for admin\n" - ) - - def test_do_the_job_for_anonymous_shall_reject(self): - client(self.proxy, "anonymous") - assert self.output.getvalue() == ( - "[log] Doing the job for anonymous is requested.\n" - "[log] I can do the job just for `admins`.\n" - ) diff --git a/tests/test_hsm.py b/tests/test_hsm.py deleted file mode 100644 index 5b49fb97..00000000 --- a/tests/test_hsm.py +++ /dev/null @@ -1,98 +0,0 @@ -import unittest -from unittest.mock import patch - -from patterns.other.hsm.hsm import ( - Active, - HierachicalStateMachine, - Standby, - Suspect, - UnsupportedMessageType, - UnsupportedState, - UnsupportedTransition, -) - - -class HsmMethodTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.hsm = HierachicalStateMachine() - - def test_initial_state_shall_be_standby(cls): - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) - - def test_unsupported_state_shall_raise_exception(cls): - with cls.assertRaises(UnsupportedState): - cls.hsm._next_state("missing") - - def test_unsupported_message_type_shall_raise_exception(cls): - with cls.assertRaises(UnsupportedMessageType): - cls.hsm.on_message("trigger") - - def test_calling_next_state_shall_change_current_state(cls): - cls.hsm._current_state = Standby # initial state - cls.hsm._next_state("active") - cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) - cls.hsm._current_state = Standby(cls.hsm) # initial state - - def test_method_perform_switchover_shall_return_specifically(cls): - """Exemplary HierachicalStateMachine method test. - (here: _perform_switchover()). Add additional test cases...""" - return_value = cls.hsm._perform_switchover() - expected_return_value = "perform switchover" - cls.assertEqual(return_value, expected_return_value) - - -class StandbyStateTest(unittest.TestCase): - """Exemplary 2nd level state test class (here: Standby state). Add missing - state test classes...""" - - @classmethod - def setUpClass(cls): - cls.hsm = HierachicalStateMachine() - - def setUp(cls): - cls.hsm._current_state = Standby(cls.hsm) - - def test_given_standby_on_message_switchover_shall_set_active(cls): - cls.hsm.on_message("switchover") - cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) - - def test_given_standby_on_message_switchover_shall_call_hsm_methods(cls): - with ( - patch.object(cls.hsm, "_perform_switchover") as mock_perform_switchover, - patch.object(cls.hsm, "_check_mate_status") as mock_check_mate_status, - patch.object( - cls.hsm, "_send_switchover_response" - ) as mock_send_switchover_response, - patch.object(cls.hsm, "_next_state") as mock_next_state, - ): - cls.hsm.on_message("switchover") - cls.assertEqual(mock_perform_switchover.call_count, 1) - cls.assertEqual(mock_check_mate_status.call_count, 1) - cls.assertEqual(mock_send_switchover_response.call_count, 1) - cls.assertEqual(mock_next_state.call_count, 1) - - def test_given_standby_on_message_fault_trigger_shall_set_suspect(cls): - cls.hsm.on_message("fault trigger") - cls.assertEqual(isinstance(cls.hsm._current_state, Suspect), True) - - def test_given_standby_on_message_diagnostics_failed_shall_raise_exception_and_keep_in_state( - cls, - ): - with cls.assertRaises(UnsupportedTransition): - cls.hsm.on_message("diagnostics failed") - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) - - def test_given_standby_on_message_diagnostics_passed_shall_raise_exception_and_keep_in_state( - cls, - ): - with cls.assertRaises(UnsupportedTransition): - cls.hsm.on_message("diagnostics passed") - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) - - def test_given_standby_on_message_operator_inservice_shall_raise_exception_and_keep_in_state( - cls, - ): - with cls.assertRaises(UnsupportedTransition): - cls.hsm.on_message("operator inservice") - cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True)