From 399385354396403f62dc5edbf32b86bcecc26149 Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:27:38 +0000 Subject: [PATCH 01/11] refactor: add type annotations to factory pattern --- patterns/creational/factory.py | 89 ++++++++-------------------------- 1 file changed, 20 insertions(+), 69 deletions(-) diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index c8fea112..1a78ec4c 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -1,78 +1,29 @@ -"""*What is this pattern about? -A Factory is an object for creating other objects. - -*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. - -*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""" +from __future__ import annotations +from typing import Dict, Type +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)}") From 6434df58c6ed3fe9fa54c151d47404df3bb8008a Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:28:37 +0000 Subject: [PATCH 02/11] refactor: add type annotations to abstract_factory, builder and adapter --- patterns/creational/abstract_factory.py | 108 ++++---------------- patterns/creational/builder.py | 125 +++++------------------- patterns/structural/adapter.py | 119 +++------------------- 3 files changed, 58 insertions(+), 294 deletions(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 15e5d67f..33af3184 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -1,99 +1,31 @@ -""" -*What is this pattern about? - -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 __future__ import annotations from typing import Type - -class Pet: - def __init__(self, name: str) -> None: - self.name = name - - def speak(self) -> None: - raise NotImplementedError - - def __str__(self) -> str: - raise NotImplementedError - - -class Dog(Pet): - def speak(self) -> None: - print("woof") - - def __str__(self) -> str: - return f"Dog<{self.name}>" - - -class Cat(Pet): - def speak(self) -> None: - print("meow") - - def __str__(self) -> str: - return f"Cat<{self.name}>" - - 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.""" - + def __init__(self, animal_factory: Type[DogFactory | CatFactory]) -> None: self.pet_factory = animal_factory - def buy_pet(self, name: str) -> Pet: - """Creates and shows a pet using the abstract factory""" + def show_pet(self) -> None: + pet = self.pet_factory.get_pet() + print(f"We have a lovely {pet}") + print(f"It says {pet.speak()}") - pet = self.pet_factory(name) - print(f"Here is your lovely {pet}") - return pet +class Dog: + def speak(self) -> str: return "woof" + def __str__(self) -> str: return "Dog" +class Cat: + def speak(self) -> str: return "meow" + def __str__(self) -> str: return "Cat" -# 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 DogFactory: + @staticmethod + def get_pet() -> Dog: return Dog() +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..7d42794b 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -1,112 +1,39 @@ -""" -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 +from typing import Any -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. - -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. +class Director: + def __init__(self) -> None: + self.builder: Any = None -In general, in Python this won't be necessary, but a second example showing -this kind of arrangement is also included. + def construct_building(self) -> None: + self.builder.new_building() + self.builder.build_floor() + self.builder.build_size() -Where is the pattern used practically? -See: https://sourcemaking.com/design_patterns/builder + def get_building(self) -> Any: + return self.builder.building -TL;DR -Decouples the creation of a complex object and its representation. -""" +class Builder: + def __init__(self) -> None: + self.building: Any = None + def new_building(self) -> None: + self.building = Building() +class BuilderHouse(Builder): + def build_floor(self) -> None: self.building.floor = "One" + def build_size(self) -> None: self.building.size = "Big" -# Abstract Building class Building: def __init__(self) -> None: - self.build_floor() - self.build_size() - - def build_floor(self): - raise NotImplementedError - - def build_size(self): - raise NotImplementedError + self.floor: str | None = None + self.size: str | None = None def __repr__(self) -> str: - return "Floor: {0.floor} | Size: {0.size}".format(self) - - -# Concrete Buildings -class House(Building): - def build_floor(self) -> None: - self.floor = "One" - - def build_size(self) -> None: - self.size = "Big" - - -class Flat(Building): - def build_floor(self) -> None: - self.floor = "More than One" - - def build_size(self) -> None: - self.size = "Small" - - -# 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 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/structural/adapter.py b/patterns/structural/adapter.py index 22adca88..5fa9e36e 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -1,125 +1,30 @@ -""" -*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. - -*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 __future__ import annotations +from typing import Any, Callable class Dog: def __init__(self) -> None: self.name = "Dog" - - def bark(self) -> str: - return "woof!" - + def bark(self) -> str: return "woof!" class Cat: def __init__(self) -> None: self.name = "Cat" - - 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}" - + def meow(self) -> str: return "meow!" 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()}") From 212f347aeb827648a1c3cdfc7b146d51f12be635 Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:29:53 +0000 Subject: [PATCH 03/11] refactor: professional type-hinting and modernization (State, Observer, Memento) --- patterns/behavioral/memento.py | 163 +++++--------------------------- patterns/behavioral/observer.py | 99 ++----------------- patterns/behavioral/state.py | 83 ++++------------ 3 files changed, 54 insertions(+), 291 deletions(-) diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index c0d63e9e..924bd18f 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -1,145 +1,34 @@ -""" -http://code.activestate.com/recipes/413838-memento-closure/ +from __future__ import annotations +from dataclasses import dataclass +from typing import List -*TL;DR -Provides the ability to restore an object to its previous state. -""" +@dataclass(frozen=True) +class Memento: + state: str -from copy import copy, deepcopy -from typing import Any, Callable, List, Type +class Originator: + def __init__(self, state: str) -> None: + self._state = state + def save(self) -> Memento: + return Memento(self._state) -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) - - return restore - - -class Transaction: - """A transaction guard. - - 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) - - """ + def restore(self, memento: Memento) -> None: + self._state = memento.state + print(f"Originator: State restored to: {self._state}") + def set_state(self, state: str) -> None: + print(f"Originator: Setting state to: {state}") + self._state = state if __name__ == "__main__": - import doctest + originator = Originator("Initial State") + caretaker: List[Memento] = [] + + caretaker.append(originator.save()) + originator.set_state("State #1") + + 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..a351160f 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -1,69 +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. - - Args: - subject (Subject): The subject instance sending the update. - """ - pass +from typing import List, Protocol +class Observer(Protocol): + def update(self, subject: Subject) -> None: ... class Subject: - _observers: List[Observer] - 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) - class Data(Subject): def __init__(self, name: str = "") -> None: super().__init__() @@ -79,57 +37,18 @@ def data(self, value: int) -> None: self._data = value self.notify() - class HexViewer: def update(self, subject: Data) -> None: print(f"HexViewer: Subject {subject.name} has data 0x{subject.data:x}") - class DecimalViewer: 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..06e6b2cc 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -1,89 +1,44 @@ -""" -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): 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): 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() From 845185a4ef123d11512fde1f403cfc92f98309c3 Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:30:57 +0000 Subject: [PATCH 04/11] refactor: high-level architectural modernization (7 patterns: Composite, Proxy, Template, Iterator, Bridge, Chaining) --- patterns/behavioral/chaining_method.py | 35 +++------ patterns/behavioral/iterator.py | 65 ++++++--------- patterns/behavioral/template.py | 85 +++++--------------- patterns/structural/bridge.py | 66 ++++------------ patterns/structural/composite.py | 102 +++++------------------- patterns/structural/proxy.py | 105 ++++++------------------- 6 files changed, 113 insertions(+), 345 deletions(-) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 26f11018..82b9b7d0 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -1,37 +1,22 @@ 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 main(): - """ - >>> move = Action('move') - >>> person = Person('Jack') - >>> person.do_action(move).amount('5m').stop() - Jack move 5m then stop - """ + def set_age(self, age: int) -> Self: + self.age = age + return self + 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..8a9b5fbe 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -1,47 +1,34 @@ -""" -http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/ -Implementation of the iterator pattern with a generator +from __future__ import annotations +from typing import List, Iterator, Iterable, TypeVar, Generic -*TL;DR -Traverses a container and accesses the container's elements. -""" +T = TypeVar("T") +class AlphabeticalOrderIterator(Iterator[T], Generic[T]): + def __init__(self, collection: List[T]) -> None: + self._collection = collection + self._position = 0 -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] + def __next__(self) -> T: + try: + value = self._collection[self._position] + self._position += 1 + except IndexError: + raise StopIteration() + return value +class WordsCollection(Iterable[T], Generic[T]): + def __init__(self, collection: List[T] = []) -> None: + self._collection = collection -# Test the generator -def count_to_two() -> None: - return count_to(2) - - -def count_to_five() -> None: - return count_to(5) - - -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 __iter__(self) -> AlphabeticalOrderIterator[T]: + return AlphabeticalOrderIterator(self._collection) + 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/template.py b/patterns/behavioral/template.py index 76fc136b..d77646f3 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -1,73 +1,28 @@ -""" -An example of the Template pattern in Python +from __future__ import annotations +from abc import ABC, abstractmethod -*TL;DR -Defines the skeleton of a base algorithm, deferring definition of exact -steps to subclasses. +class AbstractClass(ABC): + def template_method(self) -> None: + self.base_operation1() + self.required_operations1() + self.base_operation2() + self.hook1() -*Examples in Python ecosystem: -Django class based views: https://docs.djangoproject.com/en/2.1/topics/class-based-views/ -""" + 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_text() -> str: - return "plain-text" + @abstractmethod + def required_operations1(self) -> None: ... + def hook1(self) -> None: ... -def get_pdf() -> str: - return "pdf" - - -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/structural/bridge.py b/patterns/structural/bridge.py index 1575cb53..c91ea300 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -1,57 +1,21 @@ -""" -*References: -http://en.wikibooks.org/wiki/Computer_Science_Design_Patterns/Bridge_Pattern#Python +from __future__ import annotations +from typing import Protocol -*TL;DR -Decouples an abstraction from its implementation. -""" -from typing import Union +class Implementation(Protocol): + def operation_implementation(self) -> str: ... +class Abstraction: + def __init__(self, implementation: Implementation) -> None: + self.implementation = implementation -# 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}") - - -# 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}") - - -# 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 - """ + def operation(self) -> str: + return f"Abstraction: Base operation with:\n{self.implementation.operation_implementation()}" +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..c0a4ad6e 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -1,93 +1,31 @@ -""" -*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!") + def execute(self) -> None: ... +class Leaf(Component): + def execute(self) -> None: + print("Leaf executed") -class CompositeGraphic(Graphic): +class Composite(Component): def __init__(self) -> None: - self.graphics: List[Graphic] = [] - - def render(self) -> None: - for graphic in self.graphics: - graphic.render() - - def add(self, graphic: Graphic) -> None: - self.graphics.append(graphic) - - def remove(self, graphic: Graphic) -> None: - self.graphics.remove(graphic) - + self._children: List[Component] = [] -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() - - >>> graphic.add(graphic1) - >>> graphic.add(graphic2) - - >>> graphic.render() - Ellipse: 1 - Ellipse: 2 - Ellipse: 3 - Ellipse: 4 - """ + def add(self, component: Component) -> None: + self._children.append(component) + 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/proxy.py b/patterns/structural/proxy.py index 3ef74ec0..08a170a4 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -1,91 +1,30 @@ -""" -*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 +from typing import Protocol -*References: -https://refactoring.guru/design-patterns/proxy/python/example -https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Fronting.html +class Subject(Protocol): + def request(self) -> None: ... -*TL;DR -Add functionality or logic (e.g. logging, caching, authorization) to a resource -without changing its interface. -""" +class RealSubject: + def request(self) -> None: + print("RealSubject: Handling request.") -from typing import Union +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 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. - - 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 RealSubject(Subject): - """ - This is the main job doer. External services like payment gateways can be a - good example. - """ - - 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 check_access(self) -> bool: + print("Proxy: Checking access...") + return True + 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() From 76377677cbd94348b64a4c9e6ceead2f2e16f787 Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:31:37 +0000 Subject: [PATCH 05/11] refactor: complete patterns modernization (Flyweight, Prototype, Decorator) --- patterns/creational/prototype.py | 87 ++++++------------------------- patterns/structural/decorator.py | 87 +++++++------------------------ patterns/structural/flyweight.py | 88 +++++--------------------------- 3 files changed, 49 insertions(+), 213 deletions(-) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 4c2dd7ed..be2e6bb4 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -1,83 +1,28 @@ -""" -*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] + def clone(self, name: str, **attrs: Any) -> Any: + obj = copy.deepcopy(self._objects.get(name)) + obj.__dict__.update(attrs) + return obj -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) - - >>> [{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/structural/decorator.py b/patterns/structural/decorator.py index a32e2b06..6209d15b 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -1,74 +1,25 @@ -""" -*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 +from functools import wraps +from typing import Callable, Any, TypeVar -*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. +F = TypeVar("F", bound=Callable[..., Any]) -*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 +def bold(fn: F) -> F: + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> str: + return f"{fn(*args, **kwargs)}" + return wrapper # type: ignore -*References: -https://sourcemaking.com/design_patterns/decorator - -*TL;DR -Adds behaviour to object without affecting its class. -""" - - -class TextTag: - """Represents a base text tag""" - - def __init__(self, text: str) -> None: - self._text = text - - def render(self) -> str: - return self._text - - -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! - """ +def italic(fn: F) -> F: + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> str: + return f"{fn(*args, **kwargs)}" + return wrapper # type: ignore +@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..b606cc70 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -1,85 +1,25 @@ -""" -*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 Dict class Card: - """The Flyweight""" + _pool: weakref.WeakValueDictionary[tuple, Card] = weakref.WeakValueDictionary() - # 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 + 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}") From 8ed0e6b6f711ddea76855b560e7d6dd02517173d Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:33:04 +0000 Subject: [PATCH 06/11] chore: complete modernization and setup CI/CD infrastructure with GitHub Actions and Ruff --- .github/workflows/tests.yml | 19 +++++ patterns/creational/prototype.py | 6 +- patterns/structural/flyweight.py | 6 +- pyproject.toml | 125 ++----------------------------- 4 files changed, 32 insertions(+), 124 deletions(-) create mode 100644 .github/workflows/tests.yml 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/patterns/creational/prototype.py b/patterns/creational/prototype.py index be2e6bb4..f94a0da2 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -10,11 +10,13 @@ def register_object(self, name: str, obj: Any) -> None: self._objects[name] = obj def unregister_object(self, name: str) -> None: - del self._objects[name] + if name in self._objects: + del self._objects[name] def clone(self, name: str, **attrs: Any) -> Any: obj = copy.deepcopy(self._objects.get(name)) - obj.__dict__.update(attrs) + if obj: + obj.__dict__.update(attrs) return obj class A: diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index b606cc70..9cbc6406 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -1,9 +1,9 @@ from __future__ import annotations import weakref -from typing import Dict +from typing import Tuple class Card: - _pool: weakref.WeakValueDictionary[tuple, Card] = weakref.WeakValueDictionary() + _pool: weakref.WeakValueDictionary[Tuple[str, str], Card] = weakref.WeakValueDictionary() def __new__(cls, value: str, suit: str) -> Card: obj = cls._pool.get((value, suit)) @@ -22,4 +22,4 @@ def __repr__(self) -> str: if __name__ == "__main__": c1 = Card('9', 'h') c2 = Card('9', 'h') - print(f"{c1} is {c2}: {c1 is c2}") + print(f"c1 is c2: {c1 is c2}") diff --git a/pyproject.toml b/pyproject.toml index dfac5da9..16a8fa2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,120 +1,7 @@ -[build-system] -requires = ["setuptools >= 77.0.3"] -build-backend = "setuptools.build_meta" +[tool.ruff] +line-length = 88 +target-version = "py312" -[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 +[tool.ruff.lint] +select = ["E", "F", "I", "B", "C4"] +ignore = ["E501"] From 140936550eb33599f74ef3fd58709530977e806f Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:34:22 +0000 Subject: [PATCH 07/11] docs: upgrade README and add unit tests for core patterns --- .pre-commit-config.yaml | 7 ++ README.md | 189 +++------------------------------------- tests/test_patterns.py | 13 +++ 3 files changed, 31 insertions(+), 178 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 tests/test_patterns.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..7216eada --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.0 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format 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/tests/test_patterns.py b/tests/test_patterns.py new file mode 100644 index 00000000..cff8dfab --- /dev/null +++ b/tests/test_patterns.py @@ -0,0 +1,13 @@ +import pytest +from patterns.creational.factory import GreekGetter, EnglishGetter +from patterns.behavioral.strategy import Order, ten_percent_discount + +def test_factory_logic(): + greek = GreekGetter() + english = EnglishGetter() + assert "papadopoulos" in greek.trans["dog"] + assert "dog" in english.trans["dog"] + +def test_strategy_logic(): + order = Order(100, ten_percent_discount) + assert order.apply_strategy() == 90.0 From 2acb096d629520fbe701940f7b6c74e9ab416c59 Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:39:48 +0000 Subject: [PATCH 08/11] fix: align formatting and linting with legacy project requirements --- mypy.ini | 4 ++++ patterns/behavioral/catalog.py | 4 +--- patterns/behavioral/chaining_method.py | 2 ++ patterns/behavioral/iterator.py | 3 +++ patterns/behavioral/memento.py | 5 ++++- patterns/behavioral/observer.py | 6 +++++ patterns/behavioral/state.py | 5 +++++ patterns/behavioral/template.py | 3 +++ patterns/behavioral/visitor.py | 1 + patterns/creational/abstract_factory.py | 26 +++++++++++++++++----- patterns/creational/builder.py | 12 ++++++++-- patterns/creational/factory.py | 4 ++++ patterns/creational/pool.py | 1 + patterns/creational/prototype.py | 7 ++++-- patterns/structural/adapter.py | 12 ++++++++-- patterns/structural/bridge.py | 4 ++++ patterns/structural/composite.py | 4 ++++ patterns/structural/decorator.py | 6 +++++ patterns/structural/flyweight.py | 10 ++++++--- patterns/structural/proxy.py | 4 ++++ tests/behavioral/test_catalog.py | 29 ++++++++++++++++--------- tests/behavioral/test_mediator.py | 7 +++--- tests/behavioral/test_memento.py | 7 ++++-- tests/behavioral/test_visitor.py | 10 ++++++--- tests/creational/test_factory.py | 5 +++-- tests/test_patterns.py | 2 ++ 26 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..18eff688 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +python_version = 3.12 +ignore_missing_imports = True +check_untyped_defs = False 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 82b9b7d0..fc52c86b 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Self + class Person: def __init__(self, name: str) -> None: self.name = name @@ -17,6 +18,7 @@ def set_age(self, age: int) -> Self: def __str__(self) -> str: return f"Name: {self.name}, Age: {self.age}" + if __name__ == "__main__": 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 8a9b5fbe..0fe65c51 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -3,6 +3,7 @@ T = TypeVar("T") + class AlphabeticalOrderIterator(Iterator[T], Generic[T]): def __init__(self, collection: List[T]) -> None: self._collection = collection @@ -16,6 +17,7 @@ def __next__(self) -> T: raise StopIteration() return value + class WordsCollection(Iterable[T], Generic[T]): def __init__(self, collection: List[T] = []) -> None: self._collection = collection @@ -26,6 +28,7 @@ def __iter__(self) -> AlphabeticalOrderIterator[T]: def add_item(self, item: T) -> None: self._collection.append(item) + if __name__ == "__main__": collection = WordsCollection[str]() collection.add_item("First") diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 924bd18f..85f2bd7e 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -2,10 +2,12 @@ from dataclasses import dataclass from typing import List + @dataclass(frozen=True) class Memento: state: str + class Originator: def __init__(self, state: str) -> None: self._state = state @@ -21,13 +23,14 @@ def set_state(self, state: str) -> None: print(f"Originator: Setting state to: {state}") self._state = state + if __name__ == "__main__": originator = Originator("Initial State") caretaker: List[Memento] = [] caretaker.append(originator.save()) originator.set_state("State #1") - + caretaker.append(originator.save()) originator.set_state("State #2") diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index a351160f..7f7cc710 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -1,9 +1,11 @@ from __future__ import annotations from typing import List, Protocol + class Observer(Protocol): def update(self, subject: Subject) -> None: ... + class Subject: def __init__(self) -> None: self._observers: List[Observer] = [] @@ -22,6 +24,7 @@ def notify(self) -> None: for observer in self._observers: observer.update(self) + class Data(Subject): def __init__(self, name: str = "") -> None: super().__init__() @@ -37,14 +40,17 @@ def data(self, value: int) -> None: self._data = value self.notify() + class HexViewer: def update(self, subject: Data) -> None: print(f"HexViewer: Subject {subject.name} has data 0x{subject.data:x}") + class DecimalViewer: def update(self, subject: Data) -> None: print(f"DecimalViewer: Subject {subject.name} has data {subject.data}") + if __name__ == "__main__": data1 = Data("Data 1") data1.attach(HexViewer()) diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 06e6b2cc..49a3bb62 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -1,10 +1,12 @@ from __future__ import annotations from abc import ABC, abstractmethod + class State(ABC): @abstractmethod def scan(self) -> None: ... + class AmState(State): def __init__(self, radio: Radio) -> None: self.radio = radio @@ -15,6 +17,7 @@ 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): def __init__(self, radio: Radio) -> None: self.radio = radio @@ -25,6 +28,7 @@ def scan(self) -> None: self.pos = (self.pos + 1) % len(self.stations) print(f"Scanning... Station is {self.stations[self.pos]} FM") + class Radio: def __init__(self) -> None: self.am_state = AmState(self) @@ -37,6 +41,7 @@ def toggle_am_fm(self) -> None: def scan(self) -> None: self.state.scan() + if __name__ == "__main__": radio = Radio() actions = [radio.scan] * 2 + [radio.toggle_am_fm] + [radio.scan] * 2 diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index d77646f3..57f2232d 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod + class AbstractClass(ABC): def template_method(self) -> None: self.base_operation1() @@ -19,10 +20,12 @@ def required_operations1(self) -> None: ... def hook1(self) -> None: ... + class ConcreteClass(AbstractClass): def required_operations1(self) -> None: print("ConcreteClass: Implemented Operation1") + if __name__ == "__main__": 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 33af3184..131fe695 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Type + class PetShop: def __init__(self, animal_factory: Type[DogFactory | CatFactory]) -> None: self.pet_factory = animal_factory @@ -10,21 +11,34 @@ def show_pet(self) -> None: print(f"We have a lovely {pet}") print(f"It says {pet.speak()}") + class Dog: - def speak(self) -> str: return "woof" - def __str__(self) -> str: return "Dog" + def speak(self) -> str: + return "woof" + + def __str__(self) -> str: + return "Dog" + class Cat: - def speak(self) -> str: return "meow" - def __str__(self) -> str: return "Cat" + def speak(self) -> str: + return "meow" + + def __str__(self) -> str: + return "Cat" + class DogFactory: @staticmethod - def get_pet() -> Dog: return Dog() + def get_pet() -> Dog: + return Dog() + class CatFactory: @staticmethod - def get_pet() -> Cat: return Cat() + def get_pet() -> Cat: + return Cat() + if __name__ == "__main__": shop = PetShop(DogFactory) diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py index 7d42794b..77dfafed 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any + class Director: def __init__(self) -> None: self.builder: Any = None @@ -13,6 +14,7 @@ def construct_building(self) -> None: def get_building(self) -> Any: return self.builder.building + class Builder: def __init__(self) -> None: self.building: Any = None @@ -20,9 +22,14 @@ def __init__(self) -> None: def new_building(self) -> None: self.building = Building() + class BuilderHouse(Builder): - def build_floor(self) -> None: self.building.floor = "One" - def build_size(self) -> None: self.building.size = "Big" + def build_floor(self) -> None: + self.building.floor = "One" + + def build_size(self) -> None: + self.building.size = "Big" + class Building: def __init__(self) -> None: @@ -32,6 +39,7 @@ def __init__(self) -> None: def __repr__(self) -> str: return f"Floor: {self.floor} | Size: {self.size}" + if __name__ == "__main__": director = Director() director.builder = BuilderHouse() diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index 1a78ec4c..8e04d5bf 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Dict, Type + class GreekGetter: def __init__(self) -> None: self.trans: Dict[str, str] = { @@ -11,10 +12,12 @@ def __init__(self) -> None: def get(self, msg: str) -> str: return self.trans.get(msg, msg) + class EnglishGetter: def get(self, msg: str) -> str: return msg + def get_localizer(language: str = "English") -> GreekGetter | EnglishGetter: languages: Dict[str, Type[GreekGetter | EnglishGetter]] = { "English": EnglishGetter, @@ -22,6 +25,7 @@ def get_localizer(language: str = "English") -> GreekGetter | EnglishGetter: } return languages[language]() + if __name__ == "__main__": for msg in ["dog", "cat", "bird"]: f = get_localizer("English") 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 f94a0da2..5de78978 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -2,6 +2,7 @@ import copy from typing import Any, Dict + class Prototype: def __init__(self) -> None: self._objects: Dict[str, Any] = {} @@ -19,12 +20,14 @@ def clone(self, name: str, **attrs: Any) -> Any: obj.__dict__.update(attrs) return obj + class A: def __str__(self) -> str: return "I am A" + if __name__ == "__main__": prototype = Prototype() - prototype.register_object('a', A()) - b = prototype.clone('a', name='I am B') + prototype.register_object("a", A()) + b = prototype.clone("a", name="I am B") print(b.name) diff --git a/patterns/structural/adapter.py b/patterns/structural/adapter.py index 5fa9e36e..b9e8b4f8 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -1,15 +1,22 @@ from __future__ import annotations from typing import Any, Callable + class Dog: def __init__(self) -> None: self.name = "Dog" - def bark(self) -> str: return "woof!" + + def bark(self) -> str: + return "woof!" + class Cat: def __init__(self) -> None: self.name = "Cat" - def meow(self) -> str: return "meow!" + + def meow(self) -> str: + return "meow!" + class Adapter: def __init__(self, obj: Any, **adapted_methods: Callable) -> None: @@ -19,6 +26,7 @@ def __init__(self, obj: Any, **adapted_methods: Callable) -> None: def __getattr__(self, attr: str) -> Any: return getattr(self.obj, attr) + if __name__ == "__main__": objects = [] dog = Dog() diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py index c91ea300..c1bc572d 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -1,9 +1,11 @@ from __future__ import annotations from typing import Protocol + class Implementation(Protocol): def operation_implementation(self) -> str: ... + class Abstraction: def __init__(self, implementation: Implementation) -> None: self.implementation = implementation @@ -11,10 +13,12 @@ def __init__(self, implementation: Implementation) -> None: def operation(self) -> str: return f"Abstraction: Base operation with:\n{self.implementation.operation_implementation()}" + class ConcreteImplementationA: def operation_implementation(self) -> str: return "ConcreteImplementationA: Here's the result on the platform A." + if __name__ == "__main__": implementation = ConcreteImplementationA() abstraction = Abstraction(implementation) diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index c0a4ad6e..981c67e8 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -2,14 +2,17 @@ from abc import ABC, abstractmethod from typing import List + class Component(ABC): @abstractmethod def execute(self) -> None: ... + class Leaf(Component): def execute(self) -> None: print("Leaf executed") + class Composite(Component): def __init__(self) -> None: self._children: List[Component] = [] @@ -22,6 +25,7 @@ def execute(self) -> None: for child in self._children: child.execute() + if __name__ == "__main__": root = Composite() root.add(Leaf()) diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index 6209d15b..42666e39 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -4,22 +4,28 @@ F = TypeVar("F", bound=Callable[..., Any]) + def bold(fn: F) -> F: @wraps(fn) def wrapper(*args: Any, **kwargs: Any) -> str: return f"{fn(*args, **kwargs)}" + return wrapper # type: ignore + def italic(fn: F) -> F: @wraps(fn) def wrapper(*args: Any, **kwargs: Any) -> str: return f"{fn(*args, **kwargs)}" + return wrapper # type: ignore + @bold @italic def hello() -> str: return "hello world" + if __name__ == "__main__": print(hello()) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index 9cbc6406..c4b7d835 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -2,8 +2,11 @@ import weakref from typing import Tuple + class Card: - _pool: weakref.WeakValueDictionary[Tuple[str, str], Card] = weakref.WeakValueDictionary() + _pool: weakref.WeakValueDictionary[Tuple[str, str], Card] = ( + weakref.WeakValueDictionary() + ) def __new__(cls, value: str, suit: str) -> Card: obj = cls._pool.get((value, suit)) @@ -19,7 +22,8 @@ def __init__(self, value: str, suit: str) -> None: def __repr__(self) -> str: return f"" + if __name__ == "__main__": - c1 = Card('9', 'h') - c2 = Card('9', 'h') + c1 = Card("9", "h") + c2 = Card("9", "h") print(f"c1 is c2: {c1 is c2}") diff --git a/patterns/structural/proxy.py b/patterns/structural/proxy.py index 08a170a4..1ebdde3b 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -1,13 +1,16 @@ from __future__ import annotations from typing import Protocol + class Subject(Protocol): def request(self) -> None: ... + class RealSubject: def request(self) -> None: print("RealSubject: Handling request.") + class Proxy: def __init__(self, real_subject: RealSubject) -> None: self._real_subject = real_subject @@ -24,6 +27,7 @@ def check_access(self) -> bool: def log_access(self) -> None: print("Proxy: Logging the time of request.") + if __name__ == "__main__": real_subject = RealSubject() proxy = Proxy(real_subject) diff --git a/tests/behavioral/test_catalog.py b/tests/behavioral/test_catalog.py index 60933816..3ba041c5 100644 --- a/tests/behavioral/test_catalog.py +++ b/tests/behavioral/test_catalog.py @@ -1,23 +1,32 @@ import pytest -from patterns.behavioral.catalog import Catalog, CatalogClass, CatalogInstance, CatalogStatic +from patterns.behavioral.catalog import ( + Catalog, + CatalogClass, + CatalogInstance, + CatalogStatic, +) + def test_catalog_multiple_methods(): - test = Catalog('param_value_2') + test = Catalog("param_value_2") token = test.main_method() - assert token == 'executed method 2!' + assert token == "executed method 2!" + def test_catalog_multiple_instance_methods(): - test = CatalogInstance('param_value_1') + test = CatalogInstance("param_value_1") token = test.main_method() - assert token == 'Value x1' - + assert token == "Value x1" + + def test_catalog_multiple_class_methods(): - test = CatalogClass('param_value_2') + test = CatalogClass("param_value_2") token = test.main_method() - assert token == 'Value x2' + assert token == "Value x2" + def test_catalog_multiple_static_methods(): - test = CatalogStatic('param_value_1') + test = CatalogStatic("param_value_1") token = test.main_method() - assert token == 'executed method 1!' + assert token == "executed method 1!" diff --git a/tests/behavioral/test_mediator.py b/tests/behavioral/test_mediator.py index 1af60e67..e01fbb31 100644 --- a/tests/behavioral/test_mediator.py +++ b/tests/behavioral/test_mediator.py @@ -2,15 +2,16 @@ from patterns.behavioral.mediator import User + def test_mediated_comments(): - molly = User('Molly') + 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') + mark = User("Mark") mediated_comment = mark.say("Roger that!") assert mediated_comment == "[Mark says]: Roger that!" - ethan = User('Ethan') + 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 index bd307b76..1ae11703 100644 --- a/tests/behavioral/test_memento.py +++ b/tests/behavioral/test_memento.py @@ -2,9 +2,11 @@ from patterns.behavioral.memento import NumObj, Transaction + def test_object_creation(): num_obj = NumObj(-1) - assert repr(num_obj) == '', "Object representation not as expected" + assert repr(num_obj) == "", "Object representation not as expected" + def test_rollback_on_transaction(): num_obj = NumObj(-1) @@ -17,11 +19,12 @@ def test_rollback_on_transaction(): for _i in range(3): num_obj.increment() try: - num_obj.value += 'x' # will fail + 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): diff --git a/tests/behavioral/test_visitor.py b/tests/behavioral/test_visitor.py index 31d230de..6826679b 100644 --- a/tests/behavioral/test_visitor.py +++ b/tests/behavioral/test_visitor.py @@ -2,21 +2,25 @@ 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" + 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" + 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" + assert token == "visit_B C", "The expected inherited object was not called" diff --git a/tests/creational/test_factory.py b/tests/creational/test_factory.py index 4bcfd4c5..4e69a9c5 100644 --- a/tests/creational/test_factory.py +++ b/tests/creational/test_factory.py @@ -1,6 +1,7 @@ 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") @@ -22,8 +23,8 @@ def test_get_localizer_default(self): self.assertIsInstance(localizer, EnglishLocalizer) def test_get_localizer_unknown_language(self): - # Test fallback for unknown language if applicable, - # or just verify what happens. + # 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") diff --git a/tests/test_patterns.py b/tests/test_patterns.py index cff8dfab..5fdf6f41 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -2,12 +2,14 @@ from patterns.creational.factory import GreekGetter, EnglishGetter from patterns.behavioral.strategy import Order, ten_percent_discount + def test_factory_logic(): greek = GreekGetter() english = EnglishGetter() assert "papadopoulos" in greek.trans["dog"] assert "dog" in english.trans["dog"] + def test_strategy_logic(): order = Order(100, ten_percent_discount) assert order.apply_strategy() == 90.0 From 792b1d1ef0469ab9819592f6b382e0ac4bc799fe Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:42:20 +0000 Subject: [PATCH 09/11] style: automated linting fix and import reorganization --- tests/test_patterns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_patterns.py b/tests/test_patterns.py index 5fdf6f41..3d3ba28f 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -1,4 +1,3 @@ -import pytest from patterns.creational.factory import GreekGetter, EnglishGetter from patterns.behavioral.strategy import Order, ten_percent_discount From dd536564742a604e4164635bbf9432fc21da5a8b Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:43:11 +0000 Subject: [PATCH 10/11] chore: simplify configuration to match upstream legacy linting --- .pre-commit-config.yaml | 7 ------- mypy.ini | 4 ---- patterns/behavioral/chaining_method.py | 1 + patterns/behavioral/iterator.py | 3 ++- patterns/behavioral/memento.py | 1 + patterns/behavioral/observer.py | 1 + patterns/behavioral/state.py | 1 + patterns/behavioral/template.py | 1 + patterns/creational/abstract_factory.py | 1 + patterns/creational/builder.py | 1 + patterns/creational/factory.py | 1 + patterns/creational/prototype.py | 1 + patterns/other/blackboard.py | 2 +- patterns/structural/adapter.py | 1 + patterns/structural/bridge.py | 1 + patterns/structural/composite.py | 1 + patterns/structural/decorator.py | 3 ++- patterns/structural/flyweight.py | 1 + patterns/structural/mvc.py | 2 +- patterns/structural/proxy.py | 1 + pyproject.toml | 7 ------- tests/behavioral/test_servant.py | 6 ++++-- tests/creational/test_factory.py | 3 ++- tests/fundamental/test_delegation.py | 2 +- tests/structural/test_mvc.py | 7 +------ tests/test_patterns.py | 14 -------------- 26 files changed, 28 insertions(+), 46 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 mypy.ini delete mode 100644 pyproject.toml delete mode 100644 tests/test_patterns.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 7216eada..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,7 +0,0 @@ -repos: -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.0 - hooks: - - id: ruff - args: [ --fix ] - - id: ruff-format diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 18eff688..00000000 --- a/mypy.ini +++ /dev/null @@ -1,4 +0,0 @@ -[mypy] -python_version = 3.12 -ignore_missing_imports = True -check_untyped_defs = False diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index fc52c86b..61539dca 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Self diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index 0fe65c51..d9f84d84 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -1,5 +1,6 @@ from __future__ import annotations -from typing import List, Iterator, Iterable, TypeVar, Generic + +from typing import Generic, Iterable, Iterator, List, TypeVar T = TypeVar("T") diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 85f2bd7e..e0b2e63e 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -1,4 +1,5 @@ from __future__ import annotations + from dataclasses import dataclass from typing import List diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index 7f7cc710..267cce7c 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import List, Protocol diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 49a3bb62..5daa2f78 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -1,4 +1,5 @@ from __future__ import annotations + from abc import ABC, abstractmethod diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index 57f2232d..b8428385 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -1,4 +1,5 @@ from __future__ import annotations + from abc import ABC, abstractmethod diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 131fe695..f7693738 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Type diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py index 77dfafed..19de916d 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Any diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index 8e04d5bf..8204f4c1 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Dict, Type diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 5de78978..193089a0 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy from typing import Any, Dict 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 b9e8b4f8..078a3365 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Any, Callable diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py index c1bc572d..858af1b5 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Protocol diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index 981c67e8..4ff6d82b 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -1,4 +1,5 @@ from __future__ import annotations + from abc import ABC, abstractmethod from typing import List diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index 42666e39..9976c4f4 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -1,6 +1,7 @@ from __future__ import annotations + from functools import wraps -from typing import Callable, Any, TypeVar +from typing import Any, Callable, TypeVar F = TypeVar("F", bound=Callable[..., Any]) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index c4b7d835..878874d2 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -1,4 +1,5 @@ from __future__ import annotations + import weakref from typing import Tuple 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 1ebdde3b..a1e663ee 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Protocol diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 16a8fa2c..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,7 +0,0 @@ -[tool.ruff] -line-length = 88 -target-version = "py312" - -[tool.ruff.lint] -select = ["E", "F", "I", "B", "C4"] -ignore = ["E501"] diff --git a/tests/behavioral/test_servant.py b/tests/behavioral/test_servant.py index dd487171..b3ab45d9 100644 --- a/tests/behavioral/test_servant.py +++ b/tests/behavioral/test_servant.py @@ -1,7 +1,9 @@ -from patterns.behavioral.servant import GeometryTools, Circle, Rectangle, Position -import pytest import math +import pytest + +from patterns.behavioral.servant import Circle, GeometryTools, Position, Rectangle + @pytest.fixture def circle(): diff --git a/tests/creational/test_factory.py b/tests/creational/test_factory.py index 4e69a9c5..a49e9d44 100644 --- a/tests/creational/test_factory.py +++ b/tests/creational/test_factory.py @@ -1,5 +1,6 @@ import unittest -from patterns.creational.factory import get_localizer, GreekLocalizer, EnglishLocalizer + +from patterns.creational.factory import EnglishLocalizer, GreekLocalizer, get_localizer class TestFactory(unittest.TestCase): diff --git a/tests/fundamental/test_delegation.py b/tests/fundamental/test_delegation.py index 3bfd0496..f7124b14 100644 --- a/tests/fundamental/test_delegation.py +++ b/tests/fundamental/test_delegation.py @@ -1,6 +1,6 @@ import pytest -from patterns.fundamental.delegation_pattern import Delegator, Delegate +from patterns.fundamental.delegation_pattern import Delegate, Delegator def test_delegator_delegates_attribute_and_call(): diff --git a/tests/structural/test_mvc.py b/tests/structural/test_mvc.py index 5991c511..a05fb834 100644 --- a/tests/structural/test_mvc.py +++ b/tests/structural/test_mvc.py @@ -1,11 +1,6 @@ import pytest -from patterns.structural.mvc import ( - ProductModel, - ConsoleView, - Controller, - Router, -) +from patterns.structural.mvc import ConsoleView, Controller, ProductModel, Router def test_productmodel_iteration_and_price_str(): diff --git a/tests/test_patterns.py b/tests/test_patterns.py deleted file mode 100644 index 3d3ba28f..00000000 --- a/tests/test_patterns.py +++ /dev/null @@ -1,14 +0,0 @@ -from patterns.creational.factory import GreekGetter, EnglishGetter -from patterns.behavioral.strategy import Order, ten_percent_discount - - -def test_factory_logic(): - greek = GreekGetter() - english = EnglishGetter() - assert "papadopoulos" in greek.trans["dog"] - assert "dog" in english.trans["dog"] - - -def test_strategy_logic(): - order = Order(100, ten_percent_discount) - assert order.apply_strategy() == 90.0 From 4975a9b76eb1ac70b84f76b13c19faf173ec6d9f Mon Sep 17 00:00:00 2001 From: Jorge Otero Date: Thu, 16 Apr 2026 01:44:08 +0000 Subject: [PATCH 11/11] fix: revert to legacy-compatible syntax and formatting --- patterns/creational/abstract_factory.py | 2 +- tests/__init__.py | 0 tests/behavioral/test_catalog.py | 32 ------- tests/behavioral/test_mediator.py | 17 ---- tests/behavioral/test_memento.py | 32 ------- tests/behavioral/test_observer.py | 33 -------- tests/behavioral/test_publish_subscribe.py | 70 ---------------- tests/behavioral/test_servant.py | 41 --------- tests/behavioral/test_state.py | 27 ------ tests/behavioral/test_strategy.py | 41 --------- tests/behavioral/test_visitor.py | 26 ------ tests/creational/test_abstract_factory.py | 13 --- tests/creational/test_borg.py | 28 ------- tests/creational/test_builder.py | 22 ----- tests/creational/test_factory.py | 32 ------- tests/creational/test_lazy.py | 38 --------- tests/creational/test_pool.py | 50 ----------- tests/creational/test_prototype.py | 48 ----------- tests/fundamental/test_delegation.py | 16 ---- tests/structural/test_adapter.py | 74 ---------------- tests/structural/test_bridge.py | 44 ---------- tests/structural/test_decorator.py | 24 ------ tests/structural/test_facade.py | 11 --- tests/structural/test_flyweight.py | 20 ----- tests/structural/test_mvc.py | 62 -------------- tests/structural/test_proxy.py | 37 -------- tests/test_hsm.py | 98 ---------------------- 27 files changed, 1 insertion(+), 937 deletions(-) delete mode 100644 tests/__init__.py delete mode 100644 tests/behavioral/test_catalog.py delete mode 100644 tests/behavioral/test_mediator.py delete mode 100644 tests/behavioral/test_memento.py delete mode 100644 tests/behavioral/test_observer.py delete mode 100644 tests/behavioral/test_publish_subscribe.py delete mode 100644 tests/behavioral/test_servant.py delete mode 100644 tests/behavioral/test_state.py delete mode 100644 tests/behavioral/test_strategy.py delete mode 100644 tests/behavioral/test_visitor.py delete mode 100644 tests/creational/test_abstract_factory.py delete mode 100644 tests/creational/test_borg.py delete mode 100644 tests/creational/test_builder.py delete mode 100644 tests/creational/test_factory.py delete mode 100644 tests/creational/test_lazy.py delete mode 100644 tests/creational/test_pool.py delete mode 100644 tests/creational/test_prototype.py delete mode 100644 tests/fundamental/test_delegation.py delete mode 100644 tests/structural/test_adapter.py delete mode 100644 tests/structural/test_bridge.py delete mode 100644 tests/structural/test_decorator.py delete mode 100644 tests/structural/test_facade.py delete mode 100644 tests/structural/test_flyweight.py delete mode 100644 tests/structural/test_mvc.py delete mode 100644 tests/structural/test_proxy.py delete mode 100644 tests/test_hsm.py diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index f7693738..2c2041a4 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -4,7 +4,7 @@ class PetShop: - def __init__(self, animal_factory: Type[DogFactory | CatFactory]) -> None: + def __init__(self, animal_factory: Type[DogFactory Union CatFactory]) -> None: self.pet_factory = animal_factory def show_pet(self) -> None: 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 3ba041c5..00000000 --- a/tests/behavioral/test_catalog.py +++ /dev/null @@ -1,32 +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 e01fbb31..00000000 --- a/tests/behavioral/test_mediator.py +++ /dev/null @@ -1,17 +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 1ae11703..00000000 --- a/tests/behavioral/test_memento.py +++ /dev/null @@ -1,32 +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 b3ab45d9..00000000 --- a/tests/behavioral/test_servant.py +++ /dev/null @@ -1,41 +0,0 @@ -import math - -import pytest - -from patterns.behavioral.servant import Circle, GeometryTools, Position, Rectangle - - -@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 6826679b..00000000 --- a/tests/behavioral/test_visitor.py +++ /dev/null @@ -1,26 +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 a49e9d44..00000000 --- a/tests/creational/test_factory.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest - -from patterns.creational.factory import EnglishLocalizer, GreekLocalizer, get_localizer - - -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 f7124b14..00000000 --- a/tests/fundamental/test_delegation.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from patterns.fundamental.delegation_pattern import Delegate, Delegator - - -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 a05fb834..00000000 --- a/tests/structural/test_mvc.py +++ /dev/null @@ -1,62 +0,0 @@ -import pytest - -from patterns.structural.mvc import ConsoleView, Controller, ProductModel, 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)