From cb5b5fb94e3ff6a89d9f95a56471aa1cfbefb680 Mon Sep 17 00:00:00 2001 From: rednafi Date: Sat, 27 Jun 2020 05:53:56 +0600 Subject: [PATCH 001/127] ABC based interface in mvc pattern --- patterns/structural/mvc.py | 54 ++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index ff22ea59..75d3c9e0 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -3,19 +3,24 @@ Separates data in GUIs from the ways it is presented, and accepted. """ +from abc import ABC, abstractmethod -class Model: + +class Model(ABC): + @abstractmethod def __iter__(self): - raise NotImplementedError + pass + @abstractmethod def get(self, item): """Returns an object with a .items() call method that iterates over key,value pairs of its information.""" - raise NotImplementedError + pass @property + @abstractmethod def item_type(self): - raise NotImplementedError + pass class ProductModel(Model): @@ -27,12 +32,12 @@ def __str__(self): return "{:.2f}".format(self) products = { - 'milk': {'price': Price(1.50), 'quantity': 10}, - 'eggs': {'price': Price(0.20), 'quantity': 100}, - 'cheese': {'price': Price(2.00), 'quantity': 10}, + "milk": {"price": Price(1.50), "quantity": 10}, + "eggs": {"price": Price(0.20), "quantity": 100}, + "cheese": {"price": Price(2.00), "quantity": 10}, } - item_type = 'product' + item_type = "product" def __iter__(self): for item in self.products: @@ -45,36 +50,39 @@ def get(self, product): raise KeyError(str(e) + " not in the model's item list.") -class View: +class View(ABC): + @abstractmethod def show_item_list(self, item_type, item_list): - raise NotImplementedError + pass + @abstractmethod def show_item_information(self, item_type, item_name, item_info): """Will look for item information by iterating over key,value pairs yielded by item_info.items()""" - raise NotImplementedError + pass + @abstractmethod def item_not_found(self, item_type, item_name): - raise NotImplementedError + pass class ConsoleView(View): def show_item_list(self, item_type, item_list): - print(item_type.upper() + ' LIST:') + print(item_type.upper() + " LIST:") for item in item_list: print(item) - print('') + print("") @staticmethod def capitalizer(string): return string[0].upper() + string[1:].lower() def show_item_information(self, item_type, item_name, item_info): - print(item_type.upper() + ' INFORMATION:') - printout = 'Name: %s' % item_name + print(item_type.upper() + " INFORMATION:") + printout = "Name: %s" % item_name for key, value in item_info.items(): - printout += ', ' + self.capitalizer(str(key)) + ': ' + str(value) - printout += '\n' + printout += ", " + self.capitalizer(str(key)) + ": " + str(value) + printout += "\n" print(printout) def item_not_found(self, item_type, item_name): @@ -102,16 +110,16 @@ def show_item_information(self, item_name): self.view.show_item_information(item_type, item_name, item_info) -if __name__ == '__main__': +if __name__ == "__main__": model = ProductModel() view = ConsoleView() controller = Controller(model, view) controller.show_items() - controller.show_item_information('cheese') - controller.show_item_information('eggs') - controller.show_item_information('milk') - controller.show_item_information('arepas') + controller.show_item_information("cheese") + controller.show_item_information("eggs") + controller.show_item_information("milk") + controller.show_item_information("arepas") ### OUTPUT ### From 44ce0a51513952a51c44a10493bc059342ee9192 Mon Sep 17 00:00:00 2001 From: rednafi Date: Sun, 28 Jun 2020 01:01:24 +0600 Subject: [PATCH 002/127] Type checked adapter pattern --- patterns/structural/adapter.py | 38 ++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/patterns/structural/adapter.py b/patterns/structural/adapter.py index 99314a2a..433369ee 100644 --- a/patterns/structural/adapter.py +++ b/patterns/structural/adapter.py @@ -28,58 +28,63 @@ Allows the interface of an existing class to be used as another interface. """ +from typing import Callable, TypeVar + +T = TypeVar("T") + class Dog: - def __init__(self): + def __init__(self) -> None: self.name = "Dog" - def bark(self): + def bark(self) -> str: return "woof!" class Cat: - def __init__(self): + def __init__(self) -> None: self.name = "Cat" - def meow(self): + def meow(self) -> str: return "meow!" class Human: - def __init__(self): + def __init__(self) -> None: self.name = "Human" - def speak(self): + def speak(self) -> str: return "'hello'" class Car: - def __init__(self): + def __init__(self) -> None: self.name = "Car" - def make_noise(self, octane_level): - return "vroom{0}".format("!" * octane_level) + def make_noise(self, octane_level: int) -> str: + return f"vroom{'!' * octane_level}" class Adapter: - """ - Adapts an object by replacing methods. - Usage: + """Adapts an object by replacing methods. + + Usage + ------ dog = Dog() dog = Adapter(dog, make_noise=dog.bark) """ - def __init__(self, obj, **adapted_methods): - """We set the adapted methods in the object's dict""" + def __init__(self, obj: T, **adapted_methods: Callable): + """We set the adapted methods in the object's dict.""" self.obj = obj self.__dict__.update(adapted_methods) def __getattr__(self, attr): - """All non-adapted calls are passed to the object""" + """All non-adapted calls are passed to the object.""" return getattr(self.obj, attr) def original_dict(self): - """Print original object dict""" + """Print original object dict.""" return self.obj.__dict__ @@ -116,4 +121,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) From 19ef91674ec4452261de3b4a10d032369f9a70c3 Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Fri, 3 Jul 2020 01:01:16 +0800 Subject: [PATCH 003/127] substitute tests with doctest in composite.py --- patterns/structural/composite.py | 45 ++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index 2f2e6da9..e3a6b296 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -55,29 +55,34 @@ def render(self): print("Ellipse: {}".format(self.name)) -if __name__ == '__main__': - ellipse1 = Ellipse("1") - ellipse2 = Ellipse("2") - ellipse3 = Ellipse("3") - ellipse4 = Ellipse("4") +def main(): + """ + >>> ellipse1 = Ellipse("1") + >>> ellipse2 = Ellipse("2") + >>> ellipse3 = Ellipse("3") + >>> ellipse4 = Ellipse("4") - graphic1 = CompositeGraphic() - graphic2 = CompositeGraphic() + >>> graphic1 = CompositeGraphic() + >>> graphic2 = CompositeGraphic() - graphic1.add(ellipse1) - graphic1.add(ellipse2) - graphic1.add(ellipse3) - graphic2.add(ellipse4) + >>> graphic1.add(ellipse1) + >>> graphic1.add(ellipse2) + >>> graphic1.add(ellipse3) + >>> graphic2.add(ellipse4) - graphic = CompositeGraphic() + >>> graphic = CompositeGraphic() - graphic.add(graphic1) - graphic.add(graphic2) + >>> graphic.add(graphic1) + >>> graphic.add(graphic2) - graphic.render() + >>> graphic.render() + Ellipse: 1 + Ellipse: 2 + Ellipse: 3 + Ellipse: 4 + """ -### OUTPUT ### -# Ellipse: 1 -# Ellipse: 2 -# Ellipse: 3 -# Ellipse: 4 + +if __name__ == "__main__": + import doctest + doctest.testmod() From 8ff7ae422b341890f66581f2674034fd379a2ab4 Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Fri, 3 Jul 2020 15:09:10 +0800 Subject: [PATCH 004/127] Add doctest for pool --- patterns/creational/pool.py | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py index a58ff8d8..69d22932 100644 --- a/patterns/creational/pool.py +++ b/patterns/creational/pool.py @@ -51,32 +51,34 @@ def __del__(self): def main(): - import queue + """ + >>> import queue - def test_object(queue): - pool = ObjectPool(queue, True) - print('Inside func: {}'.format(pool.item)) + >>> def test_object(queue): + ... pool = ObjectPool(queue, True) + ... print('Inside func: {}'.format(pool.item)) - sample_queue = queue.Queue() + >>> sample_queue = queue.Queue() - sample_queue.put('yam') - with ObjectPool(sample_queue) as obj: - print('Inside with: {}'.format(obj)) - print('Outside with: {}'.format(sample_queue.get())) + >>> sample_queue.put('yam') + >>> with ObjectPool(sample_queue) as obj: + ... print('Inside with: {}'.format(obj)) + Inside with: yam - sample_queue.put('sam') - test_object(sample_queue) - print('Outside func: {}'.format(sample_queue.get())) + >>> print('Outside with: {}'.format(sample_queue.get())) + Outside with: yam + + >>> sample_queue.put('sam') + >>> test_object(sample_queue) + Inside func: sam + + >>> print('Outside func: {}'.format(sample_queue.get())) + Outside func: sam if not sample_queue.empty(): print(sample_queue.get()) - + """ if __name__ == '__main__': - main() - -### OUTPUT ### -# Inside with: yam -# Outside with: yam -# Inside func: sam -# Outside func: sam + import doctest + doctest.testmod() From 76b6c24ad8d7d5aaf0963694f022344cb0f922dc Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Fri, 3 Jul 2020 15:18:37 +0800 Subject: [PATCH 005/127] Add doctest for 3-tier --- patterns/structural/3-tier.py | 58 +++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index 5497730c..b6832cc5 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -61,32 +61,36 @@ def get_product_information(self, product: str) -> None: def main(): - ui = Ui() - ui.get_product_list() - ui.get_product_information("cheese") - ui.get_product_information("eggs") - ui.get_product_information("milk") - ui.get_product_information("arepas") - + """ + >>> ui = Ui() + >>> ui.get_product_list() + PRODUCT LIST: + (Fetching from Data Store) + milk + eggs + cheese + + + >>> ui.get_product_information("cheese") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Cheese, Price: 2.00, Quantity: 10 + + >>> ui.get_product_information("eggs") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Eggs, Price: 0.20, Quantity: 100 + + >>> ui.get_product_information("milk") + (Fetching from Data Store) + PRODUCT INFORMATION: + Name: Milk, Price: 1.50, Quantity: 10 + + >>> ui.get_product_information("arepas") + (Fetching from Data Store) + That product 'arepas' does not exist in the records + """ if __name__ == "__main__": - main() - -### OUTPUT ### -# PRODUCT LIST: -# (Fetching from Data Store) -# cheese -# eggs -# milk -# -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# (Fetching from Data Store) -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# (Fetching from Data Store) -# That product "arepas" does not exist in the records + import doctest + doctest.testmod() From 01fc93221e2c82d018e00e87ff12d65161b80d07 Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Fri, 3 Jul 2020 15:22:18 +0800 Subject: [PATCH 006/127] Add doctest for decorator --- patterns/structural/decorator.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index b94c0527..e64b8f8f 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -55,12 +55,18 @@ def render(self): return "{}".format(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! + """ + if __name__ == '__main__': - simple_hello = TextTag("hello, world!") - special_hello = ItalicWrapper(BoldWrapper(simple_hello)) - print("before:", simple_hello.render()) - print("after:", special_hello.render()) - -### OUTPUT ### -# before: hello, world! -# after: hello, world! + import doctest + doctest.testmod() From d730dd0873ff3b3491adab9b3dbd39eac5a18b16 Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Fri, 3 Jul 2020 15:28:20 +0800 Subject: [PATCH 007/127] Add doctest for front_controller --- patterns/structural/front_controller.py | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 346392e4..4aa30a78 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -58,17 +58,23 @@ def __init__(self, request): self.type = self.tablet_type -if __name__ == '__main__': - front_controller = RequestController() - front_controller.dispatch_request(Request('mobile')) - front_controller.dispatch_request(Request('tablet')) - - front_controller.dispatch_request(Request('desktop')) - front_controller.dispatch_request('mobile') - - -### OUTPUT ### -# Displaying mobile index page -# Displaying tablet index page -# cant dispatch the request -# request must be a Request object +def main(): + """ + >>> front_controller = RequestController() + + >>> front_controller.dispatch_request(Request('mobile')) + Displaying mobile index page + + >>> front_controller.dispatch_request(Request('tablet')) + Displaying tablet index page + + >>> front_controller.dispatch_request(Request('desktop')) + cant dispatch the request + + >>> front_controller.dispatch_request('mobile') + request must be a Request object + """ + +if __name__ == "__main__": + import doctest + doctest.testmod() From a77c79c17eaffc0dcfe1ad3a98fa34e24275c25a Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Fri, 3 Jul 2020 15:35:46 +0800 Subject: [PATCH 008/127] Add doctest for mvc --- patterns/structural/mvc.py | 63 +++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 75d3c9e0..a0e2c7aa 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -110,31 +110,38 @@ def show_item_information(self, item_name): self.view.show_item_information(item_type, item_name, item_info) -if __name__ == "__main__": - - model = ProductModel() - view = ConsoleView() - controller = Controller(model, view) - controller.show_items() - controller.show_item_information("cheese") - controller.show_item_information("eggs") - controller.show_item_information("milk") - controller.show_item_information("arepas") - - -### OUTPUT ### -# PRODUCT LIST: -# cheese -# eggs -# milk -# -# PRODUCT INFORMATION: -# Name: Cheese, Price: 2.00, Quantity: 10 -# -# PRODUCT INFORMATION: -# Name: Eggs, Price: 0.20, Quantity: 100 -# -# PRODUCT INFORMATION: -# Name: Milk, Price: 1.50, Quantity: 10 -# -# That product "arepas" does not exist in the records +def main(): + """ + >>> model = ProductModel() + >>> view = ConsoleView() + >>> controller = Controller(model, view) + + >>> controller.show_items() + PRODUCT LIST: + milk + eggs + cheese + + + >>> controller.show_item_information("cheese") + PRODUCT INFORMATION: + Name: cheese, Price: 2.00, Quantity: 10 + + + >>> controller.show_item_information("eggs") + PRODUCT INFORMATION: + Name: eggs, Price: 0.20, Quantity: 100 + + + >>> controller.show_item_information("milk") + PRODUCT INFORMATION: + Name: milk, Price: 1.50, Quantity: 10 + + + >>> controller.show_item_information("arepas") + That product "arepas" does not exist in the records + """ + +if __name__ == '__main__': + import doctest + doctest.testmod() From 985822a4c368657bb4f9e3afa6d269af33c4bb35 Mon Sep 17 00:00:00 2001 From: rednafi Date: Sat, 4 Jul 2020 11:29:09 +0600 Subject: [PATCH 009/127] Added type hints to composite pattern --- patterns/structural/composite.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index 2f2e6da9..99fa9ad1 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -26,36 +26,40 @@ Describes a group of objects that is treated as a single instance. """ +from abc import ABC, abstractmethod +from typing import List -class Graphic: - def render(self): - raise NotImplementedError("You should implement this.") + +class Graphic(ABC): + @abstractmethod + def render(self) -> None: + raise NotImplementedError("You should implement this!") class CompositeGraphic(Graphic): - def __init__(self): - self.graphics = [] + def __init__(self) -> None: + self.graphics: List[Graphic] = [] - def render(self): + def render(self) -> None: for graphic in self.graphics: graphic.render() - def add(self, graphic): + def add(self, graphic: Graphic) -> None: self.graphics.append(graphic) - def remove(self, graphic): + def remove(self, graphic: Graphic): self.graphics.remove(graphic) class Ellipse(Graphic): - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def render(self): - print("Ellipse: {}".format(self.name)) + def render(self) -> None: + print(f"Ellipse: {self.name}") -if __name__ == '__main__': +if __name__ == "__main__": ellipse1 = Ellipse("1") ellipse2 = Ellipse("2") ellipse3 = Ellipse("3") From c1ce8b89a33aef852038acdf47e175a02bc69d98 Mon Sep 17 00:00:00 2001 From: Redowan Delowar Date: Sat, 4 Jul 2020 13:10:13 +0600 Subject: [PATCH 010/127] Fixed missing hints --- patterns/structural/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index 9684501e..f6b6ccd4 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -47,7 +47,7 @@ def render(self) -> None: def add(self, graphic: Graphic) -> None: self.graphics.append(graphic) - def remove(self, graphic: Graphic): + def remove(self, graphic: Graphic) -> None: self.graphics.remove(graphic) From 6a29d64bb540280637addfb4805b06bfd5d3b986 Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Sat, 4 Jul 2020 16:44:03 +0800 Subject: [PATCH 011/127] fixed blank lines and whitespace warnings raised by tox --- patterns/creational/pool.py | 1 + patterns/structural/3-tier.py | 1 + patterns/structural/decorator.py | 3 ++- patterns/structural/front_controller.py | 7 ++++--- patterns/structural/mvc.py | 1 + 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py index 69d22932..7caf9afa 100644 --- a/patterns/creational/pool.py +++ b/patterns/creational/pool.py @@ -79,6 +79,7 @@ def main(): print(sample_queue.get()) """ + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index b6832cc5..a0b2236b 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -91,6 +91,7 @@ def main(): That product 'arepas' does not exist in the records """ + if __name__ == "__main__": import doctest doctest.testmod() diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index e64b8f8f..c7eed36a 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -59,7 +59,7 @@ def main(): """ >>> simple_hello = TextTag("hello, world!") >>> special_hello = ItalicWrapper(BoldWrapper(simple_hello)) - + >>> print("before:", simple_hello.render()) before: hello, world! @@ -67,6 +67,7 @@ def main(): after: hello, world! """ + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 4aa30a78..25d88dea 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -61,20 +61,21 @@ def __init__(self, request): def main(): """ >>> front_controller = RequestController() - + >>> front_controller.dispatch_request(Request('mobile')) Displaying mobile index page >>> front_controller.dispatch_request(Request('tablet')) Displaying tablet index page - + >>> front_controller.dispatch_request(Request('desktop')) cant dispatch the request - + >>> front_controller.dispatch_request('mobile') request must be a Request object """ + if __name__ == "__main__": import doctest doctest.testmod() diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index a0e2c7aa..890483f7 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -142,6 +142,7 @@ def main(): That product "arepas" does not exist in the records """ + if __name__ == '__main__': import doctest doctest.testmod() From beac52d135994fb7974b80a518d9b76a519da7be Mon Sep 17 00:00:00 2001 From: Modasser Billah Date: Sat, 4 Jul 2020 20:59:12 +0600 Subject: [PATCH 012/127] Update borg pattern to reflect discussion in issue#3 --- .gitignore | 2 ++ patterns/creational/borg.py | 47 ++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 4285f0cf..a7379521 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__ *.egg-info/ .tox/ venv +.vscode/ +.python-version \ No newline at end of file diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index 83b42142..4d27e68e 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -25,7 +25,9 @@ https://github.com/onetwopunch/pythonDbTemplate/blob/master/database.py *References: -https://fkromer.github.io/python-pattern-references/design/#singleton +- https://fkromer.github.io/python-pattern-references/design/#singleton +- https://learning.oreilly.com/library/view/python-cookbook/0596001673/ch05s23.html +- http://www.aleax.it/5ep.html *TL;DR Provides singleton-like behavior sharing state between instances. @@ -33,24 +35,31 @@ class Borg: - __shared_state = {} + _shared_state = {} def __init__(self): - self.__dict__ = self.__shared_state - self.state = 'Init' - - def __str__(self): - return self.state + self.__dict__ = self._shared_state class YourBorg(Borg): - pass + def __init__(self, state=None): + super().__init__() + if state: + self.state = state + else: + # initiate the first instance with default state + if not hasattr(self, 'state'): + self.state = 'Init' + + def __str__(self): + return self.state + def main(): """ - >>> rm1 = Borg() - >>> rm2 = Borg() + >>> rm1 = YourBorg() + >>> rm2 = YourBorg() >>> rm1.state = 'Idle' >>> rm2.state = 'Running' @@ -73,15 +82,25 @@ def main(): >>> rm1 is rm2 False - # Shared state is also modified from a subclass instance `rm3` + # New instances also get the same shared state >>> rm3 = YourBorg() >>> print('rm1: {0}'.format(rm1)) - rm1: Init + rm1: Zombie >>> print('rm2: {0}'.format(rm2)) - rm2: Init + rm2: Zombie + >>> print('rm3: {0}'.format(rm3)) + rm3: Zombie + + # A new instance can explicitly change the state during creation + >>> rm4 = YourBorg('Running') + + >>> print('rm4: {0}'.format(rm4)) + rm4: Running + + # Existing instances reflect that change as well >>> print('rm3: {0}'.format(rm3)) - rm3: Init + rm3: Running """ From 1176d609e8d04542bc886f75378a4fd380dba5b4 Mon Sep 17 00:00:00 2001 From: rednafi Date: Sun, 5 Jul 2020 22:05:03 +0600 Subject: [PATCH 013/127] Added typehint to catalog pattern --- patterns/behavioral/catalog.py | 76 ++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index 0570f7d3..27243dad 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -6,34 +6,40 @@ __author__ = "Ibrahim Diop " +from typing import Callable, Dict + + class Catalog: """catalog of multiple static methods that are executed depending on an init parameter """ - def __init__(self, param): + def __init__(self, param: str) -> None: # dictionary that will be used to determine which static method is # to be executed but that will be also used to store possible param # value - self._static_method_choices = {'param_value_1': self._static_method_1, 'param_value_2': self._static_method_2} + self._static_method_choices: Dict[Callable] = { + "param_value_1": self._static_method_1, + "param_value_2": self._static_method_2, + } # simple test to validate param value if param in self._static_method_choices.keys(): self.param = param else: - raise ValueError("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") @staticmethod - def _static_method_1(): + def _static_method_1() -> None: print("executed method 1!") @staticmethod - def _static_method_2(): + def _static_method_2() -> None: print("executed method 2!") - def main_method(self): + def main_method(self) -> None: """will execute either _static_method_1 or _static_method_2 depending on self.param value @@ -49,24 +55,27 @@ class CatalogInstance: parameter """ - def __init__(self, param): - self.x1 = 'x1' - self.x2 = 'x2' + def __init__(self, param: str) -> None: + self.x1 = "x1" + self.x2 = "x2" # simple test to validate param value if param in self._instance_method_choices: self.param = param else: - raise ValueError("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") - def _instance_method_1(self): - print("Value {}".format(self.x1)) + def _instance_method_1(self) -> None: + print(f"Value {self.x1}") - def _instance_method_2(self): - print("Value {}".format(self.x2)) + def _instance_method_2(self) -> None: + print(f"Value {self.x2}") - _instance_method_choices = {'param_value_1': _instance_method_1, 'param_value_2': _instance_method_2} + _instance_method_choices = { + "param_value_1": _instance_method_1, + "param_value_2": _instance_method_2, + } - def main_method(self): + def main_method(self) -> None: """will execute either _instance_method_1 or _instance_method_2 depending on self.param value @@ -81,25 +90,28 @@ class CatalogClass: parameter """ - x1 = 'x1' - x2 = 'x2' + x1 = "x1" + x2 = "x2" - def __init__(self, param): + def __init__(self, param: str) -> None: # simple test to validate param value if param in self._class_method_choices: self.param = param else: - raise ValueError("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") @classmethod - def _class_method_1(cls): - print("Value {}".format(cls.x1)) + def _class_method_1(cls) -> None: + print(f"Value {cls.x1}") @classmethod - def _class_method_2(cls): - print("Value {}".format(cls.x2)) + def _class_method_2(cls)->None: + print(f"Value {cls.x2}") - _class_method_choices = {'param_value_1': _class_method_1, 'param_value_2': _class_method_2} + _class_method_choices: Dict[Callable] = { + "param_value_1": _class_method_1, + "param_value_2": _class_method_2, + } def main_method(self): """will execute either _class_method_1 or _class_method_2 @@ -116,22 +128,25 @@ class CatalogStatic: parameter """ - def __init__(self, param): + def __init__(self, param: str) -> None: # simple test to validate param value if param in self._static_method_choices: self.param = param else: - raise ValueError("Invalid Value for Param: {0}".format(param)) + raise ValueError(f"Invalid Value for Param: {param}") @staticmethod - def _static_method_1(): + def _static_method_1() -> None: print("executed method 1!") @staticmethod - def _static_method_2(): + def _static_method_2() -> None: print("executed method 2!") - _static_method_choices = {'param_value_1': _static_method_1, 'param_value_2': _static_method_2} + _static_method_choices: Dict[Callable] = { + "param_value_1": _static_method_1, + "param_value_2": _static_method_2, + } def main_method(self): """will execute either _static_method_1 or _static_method_2 @@ -163,4 +178,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() From 4ba0c3fe28bb445cdfc2744b9f3b0c963ea85996 Mon Sep 17 00:00:00 2001 From: rednafi Date: Sun, 5 Jul 2020 23:53:11 +0600 Subject: [PATCH 014/127] Added typehint to catalog pattern --- patterns/behavioral/catalog.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index 27243dad..d1b87a42 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -3,10 +3,10 @@ init. Note the use of a single dictionary instead of multiple conditions """ -__author__ = "Ibrahim Diop " - +from __future__ import annotations +from typing import Callable, Dict, Any -from typing import Callable, Dict +__author__ = "Ibrahim Diop " class Catalog: @@ -20,7 +20,7 @@ def __init__(self, param: str) -> None: # dictionary that will be used to determine which static method is # to be executed but that will be also used to store possible param # value - self._static_method_choices: Dict[Callable] = { + self._static_method_choices = { "param_value_1": self._static_method_1, "param_value_2": self._static_method_2, } @@ -105,10 +105,10 @@ def _class_method_1(cls) -> None: print(f"Value {cls.x1}") @classmethod - def _class_method_2(cls)->None: + def _class_method_2(cls) -> None: print(f"Value {cls.x2}") - _class_method_choices: Dict[Callable] = { + _class_method_choices = { "param_value_1": _class_method_1, "param_value_2": _class_method_2, } @@ -143,16 +143,17 @@ def _static_method_1() -> None: def _static_method_2() -> None: print("executed method 2!") - _static_method_choices: Dict[Callable] = { + _static_method_choices = { "param_value_1": _static_method_1, "param_value_2": _static_method_2, } - def main_method(self): + def main_method(self) -> None: """will execute either _static_method_1 or _static_method_2 depending on self.param value """ + self._static_method_choices[self.param].__get__(None, self.__class__)() From af85c4b95f0c83a649d6bd8f49925b922f3f632a Mon Sep 17 00:00:00 2001 From: rednafi Date: Sun, 5 Jul 2020 23:56:31 +0600 Subject: [PATCH 015/127] Added typehint to catalog pattern --- patterns/behavioral/catalog.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index d1b87a42..f979ac32 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -3,9 +3,6 @@ init. Note the use of a single dictionary instead of multiple conditions """ -from __future__ import annotations -from typing import Callable, Dict, Any - __author__ = "Ibrahim Diop " From 71e0d558b641bfeb7a7d70b03655bead1bb38fa7 Mon Sep 17 00:00:00 2001 From: Alan Tan Date: Mon, 6 Jul 2020 21:55:56 +0800 Subject: [PATCH 016/127] Add doctest for abstract_factory.py --- patterns/creational/abstract_factory.py | 49 +++++++++++++------------ 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index a9ed4bf5..e158aa2d 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -75,29 +75,32 @@ def random_animal(): # Show pets with various factories -if __name__ == "__main__": - +def main(): + """ # A Shop that sells only cats - cat_shop = PetShop(Cat) - cat_shop.show_pet() - print("") + >>> cat_shop = PetShop(Cat) + >>> cat_shop.show_pet() + We have a lovely Cat + It says meow # A shop that sells random animals - shop = PetShop(random_animal) - for i in range(3): - shop.show_pet() - print("=" * 20) - -### OUTPUT ### -# We have a lovely Cat -# It says meow -# -# We have a lovely Dog -# It says woof -# ==================== -# We have a lovely Cat -# It says meow -# ==================== -# We have a lovely Cat -# It says meow -# ==================== + >>> shop = PetShop(random_animal) + >>> for i in range(3): + ... shop.show_pet() + ... print("=" * 20) + We have a lovely Cat + It says meow + ==================== + We have a lovely Dog + It says woof + ==================== + We have a lovely Dog + It says woof + ==================== + """ + + +if __name__ == "__main__": + random.seed(1234) # for deterministic doctest outputs + import doctest + doctest.testmod() From 055edfa8438586f7ec89a763781a0899910cf4a9 Mon Sep 17 00:00:00 2001 From: rednafi Date: Wed, 15 Jul 2020 22:17:14 +0600 Subject: [PATCH 017/127] Type checked chain of responsibility pattern --- .../behavioral/chain_of_responsibility.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index ecc00f77..0004902c 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -18,15 +18,18 @@ Allow a request to pass down a chain of receivers until it is handled. """ -import abc +from abc import ABC, abstractmethod +from typing import Callable, Optional, Tuple, TypeVar -class Handler(metaclass=abc.ABCMeta): +T = TypeVar("T") - def __init__(self, successor=None): + +class Handler(ABC): + def __init__(self, successor: Optional[T] = None): self.successor = successor - def handle(self, request): + def handle(self, request: int) -> None: """ Handle request and stop. If can't - call next handler in chain. @@ -38,8 +41,8 @@ def handle(self, request): if not res and self.successor: self.successor.handle(request) - @abc.abstractmethod - def check_range(self, request): + @abstractmethod + def check_range(self, request: int) -> Optional[bool]: """Compare passed value to predefined interval""" @@ -49,9 +52,9 @@ class ConcreteHandler0(Handler): """ @staticmethod - def check_range(request): + def check_range(request: int) -> Optional[bool]: if 0 <= request < 10: - print("request {} handled in handler 0".format(request)) + print(f"request {request} handled in handler 0") return True @@ -60,7 +63,7 @@ class ConcreteHandler1(Handler): start, end = 10, 20 - def check_range(self, request): + def check_range(self, request: int) -> Optional[bool]: if self.start <= request < self.end: print("request {} handled in handler 1".format(request)) return True @@ -69,21 +72,21 @@ def check_range(self, request): class ConcreteHandler2(Handler): """... With helper methods.""" - def check_range(self, request): + def check_range(self, request: int) -> Optional[bool]: start, end = self.get_interval_from_db() if start <= request < end: print("request {} handled in handler 2".format(request)) return True @staticmethod - def get_interval_from_db(): + def get_interval_from_db() -> Tuple[int, int]: return (20, 30) class FallbackHandler(Handler): @staticmethod - def check_range(request): - print("end of chain, no handler for {}".format(request)) + def check_range(request: int) -> Optional[bool]: + print(f"end of chain, no handler for {request}") return False From 68e62a98c1fd4d115e8b40a386dd609b38e20738 Mon Sep 17 00:00:00 2001 From: rednafi Date: Wed, 15 Jul 2020 22:19:44 +0600 Subject: [PATCH 018/127] Type checked chain of responsibility pattern --- patterns/behavioral/chain_of_responsibility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index 0004902c..e03059b3 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -65,7 +65,7 @@ class ConcreteHandler1(Handler): def check_range(self, request: int) -> Optional[bool]: if self.start <= request < self.end: - print("request {} handled in handler 1".format(request)) + print(f"request {request} handled in handler 1") return True @@ -75,7 +75,7 @@ class ConcreteHandler2(Handler): def check_range(self, request: int) -> Optional[bool]: start, end = self.get_interval_from_db() if start <= request < end: - print("request {} handled in handler 2".format(request)) + print(f"request {request} handled in handler 2") return True @staticmethod From 2e021bfe5c4fd1305112134d86e0be626933d0e1 Mon Sep 17 00:00:00 2001 From: rednafi Date: Mon, 20 Jul 2020 00:49:32 +0600 Subject: [PATCH 019/127] Added type hint to command pattern --- patterns/behavioral/command.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py index c989d2a9..b21d7f73 100644 --- a/patterns/behavioral/command.py +++ b/patterns/behavioral/command.py @@ -20,23 +20,25 @@ https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects """ +from typing import Union + class HideFileCommand: """ A command to hide a file given its name """ - def __init__(self): + def __init__(self) -> None: # an array of files hidden, to undo them as needed self._hidden_files = [] - def execute(self, filename): - print(f'hiding {filename}') + def execute(self, filename: str) -> None: + print(f"hiding {filename}") self._hidden_files.append(filename) - def undo(self): + def undo(self) -> None: filename = self._hidden_files.pop() - print(f'un-hiding {filename}') + print(f"un-hiding {filename}") class DeleteFileCommand: @@ -44,17 +46,17 @@ class DeleteFileCommand: A command to delete a file given its name """ - def __init__(self): + def __init__(self) -> None: # an array of deleted files, to undo them as needed self._deleted_files = [] - def execute(self, filename): - print(f'deleting {filename}') + def execute(self, filename: str) -> None: + print(f"deleting {filename}") self._deleted_files.append(filename) - def undo(self): + def undo(self) -> None: filename = self._deleted_files.pop() - print(f'restoring {filename}') + print(f"restoring {filename}") class MenuItem: @@ -62,13 +64,13 @@ class MenuItem: The invoker class. Here it is items in a menu. """ - def __init__(self, command): + def __init__(self, command: Union[HideFileCommand, DeleteFileCommand]) -> None: self._command = command - def on_do_press(self, filename): + def on_do_press(self, filename: str) -> None: self._command.execute(filename) - def on_undo_press(self): + def on_undo_press(self) -> None: self._command.undo() @@ -101,4 +103,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() From 51e83c508ac3bcf21d04d8dc823160e5085b2d6d Mon Sep 17 00:00:00 2001 From: rednafi Date: Mon, 20 Jul 2020 00:53:37 +0600 Subject: [PATCH 020/127] Added dependency free makefile for mformatting --- makefile | 88 +++++++++++++++++++ .../behavioral/chain_of_responsibility.py | 2 +- patterns/behavioral/chaining_method.py | 9 +- patterns/behavioral/iterator.py | 1 + patterns/behavioral/iterator_alt.py | 12 +-- patterns/behavioral/mediator.py | 3 +- patterns/behavioral/memento.py | 8 +- patterns/behavioral/observer.py | 9 +- patterns/behavioral/publish_subscribe.py | 1 + patterns/behavioral/registry.py | 1 + patterns/behavioral/specification.py | 15 +++- patterns/behavioral/state.py | 3 +- patterns/behavioral/strategy.py | 1 + patterns/behavioral/template.py | 1 + patterns/behavioral/visitor.py | 7 +- patterns/creational/abstract_factory.py | 3 +- patterns/creational/borg.py | 8 +- patterns/creational/builder.py | 17 ++-- patterns/creational/lazy_evaluation.py | 3 +- patterns/creational/pool.py | 3 +- patterns/creational/prototype.py | 5 +- patterns/fundamental/delegation_pattern.py | 1 + patterns/other/blackboard.py | 46 +++++----- patterns/other/graph_search.py | 1 + patterns/other/hsm/hsm.py | 50 +++++------ patterns/structural/3-tier.py | 1 + patterns/structural/bridge.py | 7 +- patterns/structural/composite.py | 1 + patterns/structural/decorator.py | 3 +- patterns/structural/facade.py | 5 ++ patterns/structural/flyweight.py | 1 + .../structural/flyweight_with_metaclass.py | 16 ++-- patterns/structural/front_controller.py | 13 +-- patterns/structural/mvc.py | 3 +- setup.py | 2 +- tests/behavioral/test_observer.py | 10 ++- tests/behavioral/test_publish_subscribe.py | 51 ++++++----- tests/behavioral/test_state.py | 13 +-- tests/creational/test_abstract_factory.py | 4 +- tests/creational/test_borg.py | 11 +-- tests/creational/test_builder.py | 15 ++-- tests/creational/test_lazy.py | 20 +++-- tests/creational/test_pool.py | 26 +++--- tests/creational/test_prototype.py | 25 +++--- tests/structural/test_adapter.py | 3 +- tests/structural/test_bridge.py | 10 ++- tests/structural/test_decorator.py | 18 ++-- tests/structural/test_proxy.py | 28 +++--- tests/test_hsm.py | 48 +++++----- 49 files changed, 403 insertions(+), 229 deletions(-) create mode 100644 makefile diff --git a/makefile b/makefile new file mode 100644 index 00000000..25826c8b --- /dev/null +++ b/makefile @@ -0,0 +1,88 @@ +# REDNAFI +# This only works with embedded venv not virtualenv +# Install venv: python3.8 -m venv venv +# Activate venv: source venv/bin/activate + +# Usage (line =black line length, path = action path, ignore= exclude folders) +# ------ +# make pylinter [make pylinter line=88 path=.] +# make pyupgrade + +path := . +line := 88 +ignore := *env + +all: + @echo + +.PHONY: checkvenv +checkvenv: +# raises error if environment is not active +ifeq ("$(VIRTUAL_ENV)","") + @echo "Venv is not activated!" + @echo "Activate venv first." + @echo + exit 1 +endif + +.PHONY: pyupgrade +pyupgrade: checkvenv +# checks if pip-tools is installed +ifeq ("$(wildcard venv/bin/pip-compile)","") + @echo "Installing Pip-tools..." + @pip install pip-tools +endif + +ifeq ("$(wildcard venv/bin/pip-sync)","") + @echo "Installing Pip-tools..." + @pip install pip-tools +endif + +# pip-tools + @pip-compile --upgrade requirements-dev.txt + @pip-compile --upgrade requirements.txt + @pip-sync requirements-dev.txt requirements.txt + + +.PHONY: pylinter +pylinter: checkvenv +# checks if black is installed +ifeq ("$(wildcard venv/bin/black)","") + @echo "Installing Black..." + @pip install black +endif + +# checks if isort is installed +ifeq ("$(wildcard venv/bin/isort)","") + @echo "Installing Isort..." + @pip install isort +endif + +# checks if flake8 is installed +ifeq ("$(wildcard venv/bin/flake8)","") + @echo -e "Installing flake8..." + @pip install flake8 + @echo +endif + +# black + @echo "Applying Black" + @echo "----------------\n" + @black --line-length $(line) --exclude $(ignore) $(path) + @echo + +# isort + @echo "Applying Isort" + @echo "----------------\n" + @isort --atomic --profile black $(path) + @echo + +# flake8 + @echo "Applying Flake8" + @echo "----------------\n" + @flake8 --max-line-length "$(line)" \ + --max-complexity "18" \ + --select "B,C,E,F,W,T4,B9" \ + --ignore "E203,E266,E501,W503,F403,F401,E402" \ + --exclude ".git,__pycache__,old, build, \ + dist, venv" $(path) diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index e03059b3..d9bb80b5 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -21,7 +21,6 @@ from abc import ABC, abstractmethod from typing import Callable, Optional, Tuple, TypeVar - T = TypeVar("T") @@ -115,4 +114,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 13c8032c..1fd261a4 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -4,7 +4,7 @@ def __init__(self, name, action): self.action = action def do_action(self): - print(self.name, self.action.name, end=' ') + print(self.name, self.action.name, end=" ") return self.action @@ -13,11 +13,11 @@ def __init__(self, name): self.name = name def amount(self, val): - print(val, end=' ') + print(val, end=" ") return self def stop(self): - print('then stop') + print("then stop") def main(): @@ -29,6 +29,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index c329e64d..7af9ea02 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -40,4 +40,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py index afc23a03..2e3a8ba3 100644 --- a/patterns/behavioral/iterator_alt.py +++ b/patterns/behavioral/iterator_alt.py @@ -8,12 +8,13 @@ class NumberWords: """Counts by word numbers, up to a maximum of five""" + _WORD_MAP = ( - 'one', - 'two', - 'three', - 'four', - 'five', + "one", + "two", + "three", + "four", + "five", ) def __init__(self, start, stop): @@ -33,6 +34,7 @@ def __next__(self): # this makes the class an Iterator # Test the iterator + def main(): """ # Counting to two... diff --git a/patterns/behavioral/mediator.py b/patterns/behavioral/mediator.py index 0410d2c3..f7265f39 100644 --- a/patterns/behavioral/mediator.py +++ b/patterns/behavioral/mediator.py @@ -45,6 +45,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 7e4c62fe..ef551438 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -5,8 +5,7 @@ Provides the ability to restore an object to its previous state. """ -from copy import copy -from copy import deepcopy +from copy import copy, deepcopy def memento(obj, deep=False): @@ -67,14 +66,14 @@ def __init__(self, value): self.value = value def __repr__(self): - return '<%s: %r>' % (self.__class__.__name__, self.value) + return "<%s: %r>" % (self.__class__.__name__, self.value) def increment(self): self.value += 1 @Transactional def do_stuff(self): - self.value = '1111' # <- invalid value + self.value = "1111" # <- invalid value self.increment() # <- will fail and rollback @@ -134,4 +133,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index a3077558..49075fac 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -31,7 +31,7 @@ def notify(self, modifier=None): class Data(Subject): - def __init__(self, name=''): + def __init__(self, name=""): Subject.__init__(self) self.name = name self._data = 0 @@ -48,12 +48,14 @@ def data(self, value): class HexViewer: def update(self, subject): - print('HexViewer: Subject {} has data 0x{:x}'.format(subject.name, subject.data)) + print( + "HexViewer: Subject {} has data 0x{:x}".format(subject.name, subject.data) + ) class DecimalViewer: def update(self, subject): - print('DecimalViewer: Subject %s has data %d' % (subject.name, subject.data)) + print("DecimalViewer: Subject %s has data %d" % (subject.name, subject.data)) def main(): @@ -97,4 +99,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index abd8fac1..d9e2a7c4 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -89,4 +89,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py index 5ed18f46..a9fca443 100644 --- a/patterns/behavioral/registry.py +++ b/patterns/behavioral/registry.py @@ -42,4 +42,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/behavioral/specification.py b/patterns/behavioral/specification.py index f125859a..07db267e 100644 --- a/patterns/behavioral/specification.py +++ b/patterns/behavioral/specification.py @@ -47,7 +47,10 @@ def __init__(self, one, other): self._other = other def is_satisfied_by(self, candidate): - return bool(self._one.is_satisfied_by(candidate) and self._other.is_satisfied_by(candidate)) + return bool( + self._one.is_satisfied_by(candidate) + and self._other.is_satisfied_by(candidate) + ) class OrSpecification(CompositeSpecification): @@ -59,7 +62,10 @@ def __init__(self, one, other): self._other = other def is_satisfied_by(self, candidate): - return bool(self._one.is_satisfied_by(candidate) or self._other.is_satisfied_by(candidate)) + return bool( + self._one.is_satisfied_by(candidate) + or self._other.is_satisfied_by(candidate) + ) class NotSpecification(CompositeSpecification): @@ -84,7 +90,7 @@ def is_satisfied_by(self, candidate): class SuperUserSpecification(CompositeSpecification): def is_satisfied_by(self, candidate): - return getattr(candidate, 'super_user', False) + return getattr(candidate, "super_user", False) def main(): @@ -105,6 +111,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 971da428..3c606ca8 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -83,6 +83,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index e6f0aab3..92d11f25 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -48,4 +48,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index 98c0cf2d..6596fee5 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -69,4 +69,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/behavioral/visitor.py b/patterns/behavioral/visitor.py index a9bbd7e8..00d95248 100644 --- a/patterns/behavioral/visitor.py +++ b/patterns/behavioral/visitor.py @@ -36,7 +36,7 @@ class Visitor: def visit(self, node, *args, **kwargs): meth = None for cls in node.__class__.__mro__: - meth_name = 'visit_' + cls.__name__ + meth_name = "visit_" + cls.__name__ meth = getattr(self, meth_name, None) if meth: break @@ -46,10 +46,10 @@ def visit(self, node, *args, **kwargs): return meth(node, *args, **kwargs) def generic_visit(self, node, *args, **kwargs): - print('generic_visit ' + node.__class__.__name__) + print("generic_visit " + node.__class__.__name__) def visit_B(self, node, *args, **kwargs): - print('visit_B ' + node.__class__.__name__) + print("visit_B " + node.__class__.__name__) def main(): @@ -70,4 +70,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index e158aa2d..63648636 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -101,6 +101,7 @@ def main(): if __name__ == "__main__": - random.seed(1234) # for deterministic doctest outputs + random.seed(1234) # for deterministic doctest outputs import doctest + doctest.testmod() diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index 4d27e68e..e3f04b66 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -42,19 +42,18 @@ def __init__(self): class YourBorg(Borg): - def __init__(self, state=None): super().__init__() if state: self.state = state else: # initiate the first instance with default state - if not hasattr(self, 'state'): - self.state = 'Init' + if not hasattr(self, "state"): + self.state = "Init" def __str__(self): return self.state - + def main(): """ @@ -106,4 +105,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py index 73f7d2fc..b1f463ee 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -45,24 +45,24 @@ def build_size(self): raise NotImplementedError def __repr__(self): - return 'Floor: {0.floor} | Size: {0.size}'.format(self) + return "Floor: {0.floor} | Size: {0.size}".format(self) # Concrete Buildings class House(Building): def build_floor(self): - self.floor = 'One' + self.floor = "One" def build_size(self): - self.size = 'Big' + self.size = "Big" class Flat(Building): def build_floor(self): - self.floor = 'More than One' + self.floor = "More than One" def build_size(self): - self.size = 'Small' + self.size = "Small" # In some very complex cases, it might be desirable to pull out the building @@ -73,15 +73,15 @@ def build_size(self): class ComplexBuilding: def __repr__(self): - return 'Floor: {0.floor} | Size: {0.size}'.format(self) + return "Floor: {0.floor} | Size: {0.size}".format(self) class ComplexHouse(ComplexBuilding): def build_floor(self): - self.floor = 'One' + self.floor = "One" def build_size(self): - self.size = 'Big and fancy' + self.size = "Big and fancy" def construct_building(cls): @@ -110,4 +110,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py index cc752e2e..b3e73743 100644 --- a/patterns/creational/lazy_evaluation.py +++ b/patterns/creational/lazy_evaluation.py @@ -36,7 +36,7 @@ def __get__(self, obj, type_): def lazy_property2(fn): - attr = '_lazy__' + fn.__name__ + attr = "_lazy__" + fn.__name__ @property def _lazy_property(self): @@ -101,4 +101,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/creational/pool.py b/patterns/creational/pool.py index 7caf9afa..1d70ea69 100644 --- a/patterns/creational/pool.py +++ b/patterns/creational/pool.py @@ -80,6 +80,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 55af0dec..18fd602c 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -24,7 +24,7 @@ class Prototype: - value = 'default' + value = "default" def clone(self, **attrs): """Clone a prototype and update inner attributes dictionary""" @@ -68,6 +68,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py index bc6a8366..2d2f8534 100644 --- a/patterns/fundamental/delegation_pattern.py +++ b/patterns/fundamental/delegation_pattern.py @@ -7,6 +7,7 @@ """ from __future__ import annotations + from typing import Any, Callable, Union diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 551411c5..999da064 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -17,10 +17,10 @@ class Blackboard: def __init__(self): self.experts = [] self.common_state = { - 'problems': 0, - 'suggestions': 0, - 'contributions': [], - 'progress': 0, # percentage, if 100 -> task is finished + "problems": 0, + "suggestions": 0, + "contributions": [], + "progress": 0, # percentage, if 100 -> task is finished } def add_expert(self, expert): @@ -32,26 +32,25 @@ def __init__(self, blackboard): self.blackboard = blackboard def run_loop(self): - while self.blackboard.common_state['progress'] < 100: + while self.blackboard.common_state["progress"] < 100: for expert in self.blackboard.experts: if expert.is_eager_to_contribute: expert.contribute() - return self.blackboard.common_state['contributions'] + return self.blackboard.common_state["contributions"] class AbstractExpert(metaclass=abc.ABCMeta): - def __init__(self, blackboard): self.blackboard = blackboard @property @abc.abstractmethod def is_eager_to_contribute(self): - raise NotImplementedError('Must provide implementation in subclass.') + raise NotImplementedError("Must provide implementation in subclass.") @abc.abstractmethod def contribute(self): - raise NotImplementedError('Must provide implementation in subclass.') + raise NotImplementedError("Must provide implementation in subclass.") class Student(AbstractExpert): @@ -60,10 +59,10 @@ def is_eager_to_contribute(self): return True def contribute(self): - self.blackboard.common_state['problems'] += random.randint(1, 10) - self.blackboard.common_state['suggestions'] += random.randint(1, 10) - self.blackboard.common_state['contributions'] += [self.__class__.__name__] - self.blackboard.common_state['progress'] += random.randint(1, 2) + self.blackboard.common_state["problems"] += random.randint(1, 10) + self.blackboard.common_state["suggestions"] += random.randint(1, 10) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(1, 2) class Scientist(AbstractExpert): @@ -72,22 +71,22 @@ def is_eager_to_contribute(self): return random.randint(0, 1) def contribute(self): - self.blackboard.common_state['problems'] += random.randint(10, 20) - self.blackboard.common_state['suggestions'] += random.randint(10, 20) - self.blackboard.common_state['contributions'] += [self.__class__.__name__] - self.blackboard.common_state['progress'] += random.randint(10, 30) + self.blackboard.common_state["problems"] += random.randint(10, 20) + self.blackboard.common_state["suggestions"] += random.randint(10, 20) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(10, 30) class Professor(AbstractExpert): @property def is_eager_to_contribute(self): - return True if self.blackboard.common_state['problems'] > 100 else False + return True if self.blackboard.common_state["problems"] > 100 else False def contribute(self): - self.blackboard.common_state['problems'] += random.randint(1, 2) - self.blackboard.common_state['suggestions'] += random.randint(10, 20) - self.blackboard.common_state['contributions'] += [self.__class__.__name__] - self.blackboard.common_state['progress'] += random.randint(10, 100) + self.blackboard.common_state["problems"] += random.randint(1, 2) + self.blackboard.common_state["suggestions"] += random.randint(10, 20) + self.blackboard.common_state["contributions"] += [self.__class__.__name__] + self.blackboard.common_state["progress"] += random.randint(10, 100) def main(): @@ -120,7 +119,8 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": random.seed(1234) # for deterministic doctest outputs import doctest + doctest.testmod() diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index 968e4342..7e0885c7 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -65,4 +65,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/other/hsm/hsm.py b/patterns/other/hsm/hsm.py index 26bb2da8..44498014 100644 --- a/patterns/other/hsm/hsm.py +++ b/patterns/other/hsm/hsm.py @@ -29,17 +29,17 @@ def __init__(self): self._failed_state = Failed(self) # Unit.OutOfService.Failed() self._current_state = self._standby_state self.states = { - 'active': self._active_state, - 'standby': self._standby_state, - 'suspect': self._suspect_state, - 'failed': self._failed_state, + "active": self._active_state, + "standby": self._standby_state, + "suspect": self._suspect_state, + "failed": self._failed_state, } self.message_types = { - 'fault trigger': self._current_state.on_fault_trigger, - 'switchover': self._current_state.on_switchover, - 'diagnostics passed': self._current_state.on_diagnostics_passed, - 'diagnostics failed': self._current_state.on_diagnostics_failed, - 'operator inservice': self._current_state.on_operator_inservice, + "fault trigger": self._current_state.on_fault_trigger, + "switchover": self._current_state.on_switchover, + "diagnostics passed": self._current_state.on_diagnostics_passed, + "diagnostics failed": self._current_state.on_diagnostics_failed, + "operator inservice": self._current_state.on_operator_inservice, } def _next_state(self, state): @@ -49,34 +49,34 @@ def _next_state(self, state): raise UnsupportedState def _send_diagnostics_request(self): - return 'send diagnostic request' + return "send diagnostic request" def _raise_alarm(self): - return 'raise alarm' + return "raise alarm" def _clear_alarm(self): - return 'clear alarm' + return "clear alarm" def _perform_switchover(self): - return 'perform switchover' + return "perform switchover" def _send_switchover_response(self): - return 'send switchover response' + return "send switchover response" def _send_operator_inservice_response(self): - return 'send operator inservice response' + return "send operator inservice response" def _send_diagnostics_failure_report(self): - return 'send diagnostics failure report' + return "send diagnostics failure report" def _send_diagnostics_pass_report(self): - return 'send diagnostics pass report' + return "send diagnostics pass report" def _abort_diagnostics(self): - return 'abort diagnostics' + return "abort diagnostics" def _check_mate_status(self): - return 'check mate status' + return "check mate status" def on_message(self, message_type): # message ignored if message_type in self.message_types.keys(): @@ -110,7 +110,7 @@ def __init__(self, HierachicalStateMachine): self._hsm = HierachicalStateMachine def on_fault_trigger(self): - self._hsm._next_state('suspect') + self._hsm._next_state("suspect") self._hsm._send_diagnostics_request() self._hsm._raise_alarm() @@ -130,7 +130,7 @@ def on_fault_trigger(self): def on_switchover(self): self._hsm.on_switchover() # message ignored - self._hsm.next_state('standby') + self._hsm.next_state("standby") class Standby(Inservice): @@ -139,7 +139,7 @@ def __init__(self, HierachicalStateMachine): def on_switchover(self): super().on_switchover() # message ignored - self._hsm._next_state('active') + self._hsm._next_state("active") class OutOfService(Unit): @@ -149,7 +149,7 @@ def __init__(self, HierachicalStateMachine): def on_operator_inservice(self): self._hsm.on_switchover() # message ignored self._hsm.send_operator_inservice_response() - self._hsm.next_state('suspect') + self._hsm.next_state("suspect") class Suspect(OutOfService): @@ -158,12 +158,12 @@ def __init__(self, HierachicalStateMachine): def on_diagnostics_failed(self): super().send_diagnostics_failure_report() - super().next_state('failed') + super().next_state("failed") def on_diagnostics_passed(self): super().send_diagnostics_pass_report() super().clear_alarm() # loss of redundancy alarm - super().next_state('standby') + super().next_state("standby") def on_operator_inservice(self): super().abort_diagnostics() diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index a0b2236b..0fd4cbda 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -94,4 +94,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py index 64b4b422..e2f6fa10 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -10,13 +10,13 @@ # ConcreteImplementor 1/2 class DrawingAPI1: def draw_circle(self, x, y, radius): - print('API1.circle at {}:{} radius {}'.format(x, y, radius)) + print("API1.circle at {}:{} radius {}".format(x, y, radius)) # ConcreteImplementor 2/2 class DrawingAPI2: def draw_circle(self, x, y, radius): - print('API2.circle at {}:{} radius {}'.format(x, y, radius)) + print("API2.circle at {}:{} radius {}".format(x, y, radius)) # Refined Abstraction @@ -48,6 +48,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/composite.py b/patterns/structural/composite.py index f6b6ccd4..a4bedc1d 100644 --- a/patterns/structural/composite.py +++ b/patterns/structural/composite.py @@ -89,4 +89,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index c7eed36a..08504bcf 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -68,6 +68,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/facade.py b/patterns/structural/facade.py index 6c04c472..290afe6d 100644 --- a/patterns/structural/facade.py +++ b/patterns/structural/facade.py @@ -34,6 +34,7 @@ class CPU: """ Simple CPU representation. """ + def freeze(self): print("Freezing processor.") @@ -48,6 +49,7 @@ class Memory: """ Simple memory representation. """ + def load(self, position, data): print("Loading from {0} data: '{1}'.".format(position, data)) @@ -56,6 +58,7 @@ class SolidStateDrive: """ Simple solid state drive representation. """ + def read(self, lba, size): return "Some data from sector {0} with size {1}".format(lba, size) @@ -64,6 +67,7 @@ class ComputerFacade: """ Represents a facade for various computer parts. """ + def __init__(self): self.cpu = CPU() self.memory = Memory() @@ -89,4 +93,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod(optionflags=doctest.ELLIPSIS) diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index b5911370..b9589319 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -81,4 +81,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/flyweight_with_metaclass.py b/patterns/structural/flyweight_with_metaclass.py index bba21360..ced8d915 100644 --- a/patterns/structural/flyweight_with_metaclass.py +++ b/patterns/structural/flyweight_with_metaclass.py @@ -12,7 +12,7 @@ def __new__(mcs, name, parents, dct): static methods, etc :return: new class """ - dct['pool'] = weakref.WeakValueDictionary() + dct["pool"] = weakref.WeakValueDictionary() return super().__new__(mcs, name, parents, dct) @staticmethod @@ -23,12 +23,12 @@ def _serialize_params(cls, *args, **kwargs): """ args_list = list(map(str, args)) args_list.extend([str(kwargs), cls.__name__]) - key = ''.join(args_list) + key = "".join(args_list) return key def __call__(cls, *args, **kwargs): key = FlyweightMeta._serialize_params(cls, *args, **kwargs) - pool = getattr(cls, 'pool', {}) + pool = getattr(cls, "pool", {}) instance = pool.get(key) if instance is None: @@ -43,11 +43,11 @@ def __init__(self, *args, **kwargs): pass -if __name__ == '__main__': - instances_pool = getattr(Card2, 'pool') - cm1 = Card2('10', 'h', a=1) - cm2 = Card2('10', 'h', a=1) - cm3 = Card2('10', 'h', a=2) +if __name__ == "__main__": + instances_pool = getattr(Card2, "pool") + cm1 = Card2("10", "h", a=1) + cm2 = Card2("10", "h", a=1) + cm3 = Card2("10", "h", a=2) assert (cm1 == cm2) and (cm1 != cm3) assert (cm1 is cm2) and (cm1 is not cm3) diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 25d88dea..02d6aecb 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -8,12 +8,12 @@ class MobileView: def show_index_page(self): - print('Displaying mobile index page') + print("Displaying mobile index page") class TabletView: def show_index_page(self): - print('Displaying tablet index page') + print("Displaying tablet index page") class Dispatcher: @@ -27,7 +27,7 @@ def dispatch(self, request): elif request.type == Request.tablet_type: self.tablet_view.show_index_page() else: - print('cant dispatch the request') + print("cant dispatch the request") class RequestController: @@ -40,14 +40,14 @@ def dispatch_request(self, request): if isinstance(request, Request): self.dispatcher.dispatch(request) else: - print('request must be a Request object') + print("request must be a Request object") class Request: """ request """ - mobile_type = 'mobile' - tablet_type = 'tablet' + mobile_type = "mobile" + tablet_type = "tablet" def __init__(self, request): self.type = None @@ -78,4 +78,5 @@ def main(): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 890483f7..c57c9041 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -143,6 +143,7 @@ def main(): """ -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/setup.py b/setup.py index 80930a8b..b4218c1c 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( name="patterns", diff --git a/tests/behavioral/test_observer.py b/tests/behavioral/test_observer.py index e24efe44..821f97a6 100644 --- a/tests/behavioral/test_observer.py +++ b/tests/behavioral/test_observer.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest @@ -7,7 +7,8 @@ @pytest.fixture def observable(): - return Data('some data') + return Data("some data") + def test_attach_detach(observable): decimal_viewer = DecimalViewer() @@ -19,11 +20,14 @@ def test_attach_detach(observable): 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: + 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 index 84015080..86488343 100644 --- a/tests/behavioral/test_publish_subscribe.py +++ b/tests/behavioral/test_publish_subscribe.py @@ -1,5 +1,6 @@ import unittest -from unittest.mock import patch, call +from unittest.mock import call, patch + from patterns.behavioral.publish_subscribe import Provider, Publisher, Subscriber @@ -9,17 +10,17 @@ class TestProvider(unittest.TestCase): """ def test_subscriber_shall_be_attachable_to_subscriptions(cls): - subscription = 'sub msg' + subscription = "sub msg" pro = Provider() cls.assertEqual(len(pro.subscribers), 0) - sub = Subscriber('sub name', pro) + 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' + subscription = "sub msg" pro = Provider() - sub = Subscriber('sub name', pro) + sub = Subscriber("sub name", pro) sub.subscribe(subscription) cls.assertEqual(len(pro.subscribers[subscription]), 1) sub.unsubscribe(subscription) @@ -27,35 +28,41 @@ def test_subscriber_shall_be_detachable_from_subscriptions(cls): def test_publisher_shall_append_subscription_message_to_queue(cls): """ msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg) """ - expected_msg = 'expected msg' + expected_msg = "expected msg" pro = Provider() pub = Publisher(pro) - Subscriber('sub name', 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): + 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: + 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: + 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')] + 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')] + 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_state.py b/tests/behavioral/test_state.py index adaae509..77473f51 100644 --- a/tests/behavioral/test_state.py +++ b/tests/behavioral/test_state.py @@ -7,18 +7,21 @@ def radio(): return Radio() + def test_initial_state(radio): - assert radio.state.name == 'AM' + assert radio.state.name == "AM" + def test_initial_am_station(radio): initial_pos = radio.state.pos - assert radio.state.stations[initial_pos] == '1250' + assert radio.state.stations[initial_pos] == "1250" + def test_toggle_amfm(radio): - assert radio.state.name == 'AM' + assert radio.state.name == "AM" radio.toggle_amfm() - assert radio.state.name == 'FM' + assert radio.state.name == "FM" radio.toggle_amfm() - assert radio.state.name == 'AM' + assert radio.state.name == "AM" diff --git a/tests/creational/test_abstract_factory.py b/tests/creational/test_abstract_factory.py index ad7a7fcf..ad818f59 100644 --- a/tests/creational/test_abstract_factory.py +++ b/tests/creational/test_abstract_factory.py @@ -1,12 +1,12 @@ import unittest from unittest.mock import patch -from patterns.creational.abstract_factory import PetShop, Dog +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: + with patch.object(Dog, "speak") as mock_Dog_speak: dog_pet_shop.show_pet() self.assertEqual(mock_Dog_speak.call_count, 1) diff --git a/tests/creational/test_borg.py b/tests/creational/test_borg.py index 82d9efaf..019bc5aa 100644 --- a/tests/creational/test_borg.py +++ b/tests/creational/test_borg.py @@ -1,4 +1,5 @@ import unittest + from patterns.creational.borg import Borg, YourBorg @@ -10,13 +11,13 @@ def setUp(self): def test_initial_borg_state_shall_be_init(self): b = Borg() - self.assertEqual(b.state, 'Init') + 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') + 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 index 7f0d8e72..923bc4a5 100644 --- a/tests/creational/test_builder.py +++ b/tests/creational/test_builder.py @@ -1,21 +1,22 @@ import unittest -from patterns.creational.builder import construct_building, House, Flat, ComplexHouse + +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') + 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') + 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') + self.assertEqual(house.size, "Big and fancy") + self.assertEqual(house.floor, "One") diff --git a/tests/creational/test_lazy.py b/tests/creational/test_lazy.py index 8da429ec..8d92be88 100644 --- a/tests/creational/test_lazy.py +++ b/tests/creational/test_lazy.py @@ -1,28 +1,38 @@ from __future__ import print_function + import unittest + from patterns.creational.lazy_evaluation import Person class TestDynamicExpanding(unittest.TestCase): def setUp(self): - self.John = Person('John', 'Coder') + self.John = Person("John", "Coder") def test_innate_properties(self): - self.assertDictEqual({'name': 'John', 'occupation': 'Coder', 'call_count2': 0}, self.John.__dict__) + 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__) + self.assertNotIn("relatives", self.John.__dict__) def test_extended_properties(self): print(u"John's relatives: {0}".format(self.John.relatives)) self.assertDictEqual( - {'name': 'John', 'occupation': 'Coder', 'relatives': 'Many relatives.', 'call_count2': 0}, + { + "name": "John", + "occupation": "Coder", + "relatives": "Many relatives.", + "call_count2": 0, + }, self.John.__dict__, ) def test_relatives_after_access(self): print(u"John's relatives: {0}".format(self.John.relatives)) - self.assertIn('relatives', self.John.__dict__) + self.assertIn("relatives", self.John.__dict__) def test_parents(self): for _ in range(2): diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py index b63f58c7..18f844e3 100644 --- a/tests/creational/test_pool.py +++ b/tests/creational/test_pool.py @@ -1,5 +1,5 @@ -import unittest import queue +import unittest from patterns.creational.pool import ObjectPool @@ -7,24 +7,24 @@ class TestPool(unittest.TestCase): def setUp(self): self.sample_queue = queue.Queue() - self.sample_queue.put('first') - self.sample_queue.put('second') + 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.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.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.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.get() == "first") self.assertTrue(self.sample_queue.empty()) @@ -36,12 +36,12 @@ class TestNaitivePool(unittest.TestCase): def test_pool_behavior_with_single_object_inside(self): sample_queue = queue.Queue() - sample_queue.put('yam') + sample_queue.put("yam") with ObjectPool(sample_queue) as obj: # print('Inside with: {}'.format(obj)) - self.assertEqual(obj, 'yam') + self.assertEqual(obj, "yam") self.assertFalse(sample_queue.empty()) - self.assertTrue(sample_queue.get() == 'yam') + self.assertTrue(sample_queue.get() == "yam") self.assertTrue(sample_queue.empty()) # sample_queue.put('sam') diff --git a/tests/creational/test_prototype.py b/tests/creational/test_prototype.py index 74eb12cf..758ac872 100644 --- a/tests/creational/test_prototype.py +++ b/tests/creational/test_prototype.py @@ -1,4 +1,5 @@ import unittest + from patterns.creational.prototype import Prototype, PrototypeDispatcher @@ -13,13 +14,13 @@ def test_cloning_propperty_innate_values(self): def test_extended_property_values_cloning(self): sample_object_1 = self.prototype.clone() - sample_object_1.some_value = 'test string' + 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') + sample_object_2 = self.prototype.clone(value="re-assigned") self.assertNotEqual(sample_object_1.value, sample_object_2.value) @@ -28,20 +29,20 @@ 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) + 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') + 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) + self.assertEqual(self.dispatcher.get_objects()["A"].ext_value, "E") + self.assertTrue(self.dispatcher.get_objects()["B"].diff) diff --git a/tests/structural/test_adapter.py b/tests/structural/test_adapter.py index 76f80425..01323075 100644 --- a/tests/structural/test_adapter.py +++ b/tests/structural/test_adapter.py @@ -1,5 +1,6 @@ import unittest -from patterns.structural.adapter import Dog, Cat, Human, Car, Adapter + +from patterns.structural.adapter import Adapter, Car, Cat, Dog, Human class ClassTest(unittest.TestCase): diff --git a/tests/structural/test_bridge.py b/tests/structural/test_bridge.py index 0bb704f3..7fa8a278 100644 --- a/tests/structural/test_bridge.py +++ b/tests/structural/test_bridge.py @@ -1,15 +1,15 @@ import unittest from unittest.mock import patch -from patterns.structural.bridge import DrawingAPI1, DrawingAPI2, CircleShape +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' + 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() @@ -33,7 +33,9 @@ def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): 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: + 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) diff --git a/tests/structural/test_decorator.py b/tests/structural/test_decorator.py index df8e9b21..8a4154a9 100644 --- a/tests/structural/test_decorator.py +++ b/tests/structural/test_decorator.py @@ -1,16 +1,24 @@ import unittest -from patterns.structural.decorator import TextTag, BoldWrapper, ItalicWrapper + +from patterns.structural.decorator import BoldWrapper, ItalicWrapper, TextTag class TestTextWrapping(unittest.TestCase): def setUp(self): - self.raw_string = TextTag('raw but not cruel') + self.raw_string = TextTag("raw but not cruel") def test_italic(self): - self.assertEqual(ItalicWrapper(self.raw_string).render(), 'raw but not cruel') + 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') + 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') + self.assertEqual( + BoldWrapper(ItalicWrapper(self.raw_string)).render(), + "raw but not cruel", + ) diff --git a/tests/structural/test_proxy.py b/tests/structural/test_proxy.py index e0dcaac0..ff4dcfb1 100644 --- a/tests/structural/test_proxy.py +++ b/tests/structural/test_proxy.py @@ -1,9 +1,9 @@ import sys -from time import time import unittest from io import StringIO +from time import time -from patterns.structural.proxy import Proxy, NoTalkProxy +from patterns.structural.proxy import NoTalkProxy, Proxy class ProxyTest(unittest.TestCase): @@ -24,27 +24,27 @@ def tearDown(cls): sys.stdout = cls.saved_stdout def test_sales_manager_shall_talk_through_proxy_with_delay(cls): - cls.p.busy = 'No' + cls.p.busy = "No" start_time = time() cls.p.talk() end_time = time() execution_time = end_time - start_time print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -Sales Manager ready to talk\n' + expected_print_output = "Proxy checking for Sales Manager availability\n\ +Sales Manager ready to talk\n" cls.assertEqual(print_output, expected_print_output) expected_execution_time = 1 cls.assertEqual(int(execution_time * 10), expected_execution_time) def test_sales_manager_shall_respond_through_proxy_with_delay(cls): - cls.p.busy = 'Yes' + cls.p.busy = "Yes" start_time = time() cls.p.talk() end_time = time() execution_time = end_time - start_time print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -Sales Manager is busy\n' + expected_print_output = "Proxy checking for Sales Manager availability\n\ +Sales Manager is busy\n" cls.assertEqual(print_output, expected_print_output) expected_execution_time = 1 cls.assertEqual(int(execution_time * 10), expected_execution_time) @@ -68,27 +68,27 @@ def tearDown(cls): sys.stdout = cls.saved_stdout def test_sales_manager_shall_not_talk_through_proxy_with_delay(cls): - cls.ntp.busy = 'No' + cls.ntp.busy = "No" start_time = time() cls.ntp.talk() end_time = time() execution_time = end_time - start_time print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -This Sales Manager will not talk to you whether he/she is busy or not\n' + expected_print_output = "Proxy checking for Sales Manager availability\n\ +This Sales Manager will not talk to you whether he/she is busy or not\n" cls.assertEqual(print_output, expected_print_output) expected_execution_time = 1 cls.assertEqual(int(execution_time * 10), expected_execution_time) def test_sales_manager_shall_not_respond_through_proxy_with_delay(cls): - cls.ntp.busy = 'Yes' + cls.ntp.busy = "Yes" start_time = time() cls.ntp.talk() end_time = time() execution_time = end_time - start_time print_output = cls.output.getvalue() - expected_print_output = 'Proxy checking for Sales Manager availability\n\ -This Sales Manager will not talk to you whether he/she is busy or not\n' + expected_print_output = "Proxy checking for Sales Manager availability\n\ +This Sales Manager will not talk to you whether he/she is busy or not\n" cls.assertEqual(print_output, expected_print_output) expected_execution_time = 1 cls.assertEqual(int(execution_time * 10), expected_execution_time) diff --git a/tests/test_hsm.py b/tests/test_hsm.py index 7b759e79..b6ff3065 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -2,13 +2,13 @@ from unittest.mock import patch from patterns.other.hsm.hsm import ( + Active, HierachicalStateMachine, + Standby, + Suspect, UnsupportedMessageType, UnsupportedState, UnsupportedTransition, - Active, - Standby, - Suspect, ) @@ -22,15 +22,15 @@ def test_initial_state_shall_be_standby(cls): def test_unsupported_state_shall_raise_exception(cls): with cls.assertRaises(UnsupportedState): - cls.hsm._next_state('missing') + cls.hsm._next_state("missing") def test_unsupported_message_type_shall_raise_exception(cls): with cls.assertRaises(UnsupportedMessageType): - cls.hsm.on_message('trigger') + 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.hsm._next_state("active") cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) cls.hsm._current_state = Standby(cls.hsm) # initial state @@ -38,7 +38,7 @@ 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' + expected_return_value = "perform switchover" cls.assertEqual(return_value, expected_return_value) @@ -54,38 +54,46 @@ 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.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' + 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' + cls.hsm, "_send_switchover_response" ) as mock_send_switchover_response, patch.object( - cls.hsm, '_next_state' + cls.hsm, "_next_state" ) as mock_next_state: - cls.hsm.on_message('switchover') + 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.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): + 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.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): + 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.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): + 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.hsm.on_message("operator inservice") cls.assertEqual(isinstance(cls.hsm._current_state, Standby), True) From f49d4e98c19b27e235e52dd79efe1285525502bd Mon Sep 17 00:00:00 2001 From: rednafi Date: Tue, 21 Jul 2020 10:33:43 +0600 Subject: [PATCH 021/127] Added type hints to mediator pattern --- makefile => Makefile | 0 patterns/behavioral/mediator.py | 12 +++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) rename makefile => Makefile (100%) diff --git a/makefile b/Makefile similarity index 100% rename from makefile rename to Makefile diff --git a/patterns/behavioral/mediator.py b/patterns/behavioral/mediator.py index f7265f39..e4b3c34a 100644 --- a/patterns/behavioral/mediator.py +++ b/patterns/behavioral/mediator.py @@ -8,25 +8,27 @@ Encapsulates how a set of objects interact. """ +from __future__ import annotations + class ChatRoom: """Mediator class""" - def display_message(self, user, message): - print("[{} says]: {}".format(user, message)) + def display_message(self, user: User, message: str) -> None: + print(f"[{user} says]: {message}") class User: """A class whose instances want to interact with each other""" - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name self.chat_room = ChatRoom() - def say(self, message): + def say(self, message: str) -> None: self.chat_room.display_message(self, message) - def __str__(self): + def __str__(self) -> str: return self.name From b4a65f8dfb557c9b3cfaff8075d16c625bde808e Mon Sep 17 00:00:00 2001 From: rednafi Date: Thu, 23 Jul 2020 14:44:00 +0600 Subject: [PATCH 022/127] Added type hints to observer pattern --- patterns/behavioral/observer.py | 37 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index 49075fac..ea32e3ca 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -8,54 +8,61 @@ Django Signals: https://docs.djangoproject.com/en/2.1/topics/signals/ Flask Signals: http://flask.pocoo.org/docs/1.0/signals/ """ +from __future__ import annotations + +from typing import List, Optional, Protocol + + +# define a generic observer type +class Observer(Protocol): + def update(self, subject: Subject) -> None: + pass class Subject: - def __init__(self): - self._observers = [] + def __init__(self) -> None: + self._observers: List[Observer] = [] - def attach(self, observer): + def attach(self, observer: Observer) -> None: if observer not in self._observers: self._observers.append(observer) - def detach(self, observer): + def detach(self, observer: Observer) -> None: try: self._observers.remove(observer) except ValueError: pass - def notify(self, modifier=None): + def notify(self, modifier: Optional[Observer] = None) -> None: for observer in self._observers: if modifier != observer: observer.update(self) class Data(Subject): - def __init__(self, name=""): - Subject.__init__(self) + def __init__(self, name: str = "") -> None: + super().__init__() self.name = name self._data = 0 @property - def data(self): + def data(self) -> int: return self._data @data.setter - def data(self, value): + def data(self, value: int) -> None: self._data = value self.notify() class HexViewer: - def update(self, subject): - print( - "HexViewer: Subject {} has data 0x{:x}".format(subject.name, subject.data) - ) + def update(self, subject: Data) -> None: + print(f"HexViewer: Subject {subject.name} has data 0x{subject.data:x}") class DecimalViewer: - def update(self, subject): - print("DecimalViewer: Subject %s has data %d" % (subject.name, subject.data)) + def update(self, subject: Data) -> None: + print(f"DecimalViewer: Subject {subject.name} has data {subject.data}") def main(): From 27115a87cac1e79581635aacda345a824260afe6 Mon Sep 17 00:00:00 2001 From: rednafi Date: Thu, 23 Jul 2020 14:47:45 +0600 Subject: [PATCH 023/127] Updated signal links --- patterns/behavioral/observer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index ea32e3ca..9c930389 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -5,8 +5,8 @@ Maintains a list of dependents and notifies them of any state changes. *Examples in Python ecosystem: -Django Signals: https://docs.djangoproject.com/en/2.1/topics/signals/ -Flask Signals: http://flask.pocoo.org/docs/1.0/signals/ +Django Signals: https://docs.djangoproject.com/en/3.1/topics/signals/ +Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ """ from __future__ import annotations From daa2219f00c993c8697ceb20a704b9d5c4a50808 Mon Sep 17 00:00:00 2001 From: rednafi Date: Thu, 23 Jul 2020 17:48:05 +0600 Subject: [PATCH 024/127] Handle unnamed exception with contextlib.suppress --- patterns/behavioral/observer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index 9c930389..b2d503b7 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -8,8 +8,10 @@ Django Signals: https://docs.djangoproject.com/en/3.1/topics/signals/ Flask Signals: https://flask.palletsprojects.com/en/1.1.x/signals/ """ + from __future__ import annotations +from contextlib import suppress from typing import List, Optional, Protocol @@ -28,10 +30,8 @@ def attach(self, observer: Observer) -> None: self._observers.append(observer) def detach(self, observer: Observer) -> None: - try: + with suppress(ValueError): self._observers.remove(observer) - except ValueError: - pass def notify(self, modifier: Optional[Observer] = None) -> None: for observer in self._observers: From 2ed51f85bc0f95a03ab64236ecda220b72576cb9 Mon Sep 17 00:00:00 2001 From: Roman Imankulov Date: Wed, 12 Aug 2020 07:06:55 +0100 Subject: [PATCH 025/127] Rewrite tests for the Proxy class Remove outdated tests and replace them with ones that check the new behavior. --- tests/structural/test_proxy.py | 87 ++++++---------------------------- 1 file changed, 15 insertions(+), 72 deletions(-) diff --git a/tests/structural/test_proxy.py b/tests/structural/test_proxy.py index ff4dcfb1..ec660986 100644 --- a/tests/structural/test_proxy.py +++ b/tests/structural/test_proxy.py @@ -1,16 +1,15 @@ import sys import unittest from io import StringIO -from time import time -from patterns.structural.proxy import NoTalkProxy, Proxy +from patterns.structural.proxy import Proxy, client class ProxyTest(unittest.TestCase): @classmethod def setUpClass(cls): """ Class scope setup. """ - cls.p = Proxy() + cls.proxy = Proxy() def setUp(cls): """ Function/test case scope setup. """ @@ -23,72 +22,16 @@ def tearDown(cls): cls.output.close() sys.stdout = cls.saved_stdout - def test_sales_manager_shall_talk_through_proxy_with_delay(cls): - cls.p.busy = "No" - start_time = time() - cls.p.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = "Proxy checking for Sales Manager availability\n\ -Sales Manager ready to talk\n" - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) - - def test_sales_manager_shall_respond_through_proxy_with_delay(cls): - cls.p.busy = "Yes" - start_time = time() - cls.p.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = "Proxy checking for Sales Manager availability\n\ -Sales Manager is busy\n" - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) - - -class NoTalkProxyTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - """ Class scope setup. """ - cls.ntp = NoTalkProxy() - - 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_sales_manager_shall_not_talk_through_proxy_with_delay(cls): - cls.ntp.busy = "No" - start_time = time() - cls.ntp.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = "Proxy checking for Sales Manager availability\n\ -This Sales Manager will not talk to you whether he/she is busy or not\n" - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) - - def test_sales_manager_shall_not_respond_through_proxy_with_delay(cls): - cls.ntp.busy = "Yes" - start_time = time() - cls.ntp.talk() - end_time = time() - execution_time = end_time - start_time - print_output = cls.output.getvalue() - expected_print_output = "Proxy checking for Sales Manager availability\n\ -This Sales Manager will not talk to you whether he/she is busy or not\n" - cls.assertEqual(print_output, expected_print_output) - expected_execution_time = 1 - cls.assertEqual(int(execution_time * 10), expected_execution_time) + 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" + ) From 3ac21883eb6999bbff801a82d52c1badec0a43aa Mon Sep 17 00:00:00 2001 From: Roman Imankulov Date: Wed, 12 Aug 2020 07:15:35 +0100 Subject: [PATCH 026/127] Fix borg tests Add a tearDown method that sets the shared state to "Init" --- tests/creational/test_borg.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/creational/test_borg.py b/tests/creational/test_borg.py index 019bc5aa..182611c3 100644 --- a/tests/creational/test_borg.py +++ b/tests/creational/test_borg.py @@ -7,8 +7,13 @@ 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") From 2f0ebe02f7894ff369be99b2ae17119f5156e90a Mon Sep 17 00:00:00 2001 From: Roman Imankulov Date: Wed, 12 Aug 2020 07:27:11 +0100 Subject: [PATCH 027/127] Ignore Flake8 rule W503 As defined in https://www.flake8rules.com/rules/W503.html, this rule goes against the PEP8 recommendation and conflicts with W504. In the code, the rule results in a false-positive in patterns/behavioral/specification.py. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 536a52d7..159357a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [flake8] max-line-length = 120 -ignore = E266 E731 +ignore = E266 E731 W503 exclude = .venv* [tool:pytest] From 242dd659d09c8a38c93f29aefbac1d93628528cc Mon Sep 17 00:00:00 2001 From: Roman Imankulov Date: Wed, 12 Aug 2020 07:30:24 +0100 Subject: [PATCH 028/127] Remove unused imports Make the flake8's test "F401 imported but unused" happy. --- patterns/behavioral/chain_of_responsibility.py | 2 +- patterns/structural/3-tier.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index d9bb80b5..d80b1633 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -19,7 +19,7 @@ """ from abc import ABC, abstractmethod -from typing import Callable, Optional, Tuple, TypeVar +from typing import Optional, Tuple, TypeVar T = TypeVar("T") diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index 0fd4cbda..64835f99 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -3,7 +3,7 @@ Separates presentation, application processing, and data management functions. """ -from typing import Dict, KeysView, Optional, Type, TypeVar, Union +from typing import Dict, KeysView, Optional, Union class Data: From b1da0670b2787517701eaa0cfe7204d5ec56527d Mon Sep 17 00:00:00 2001 From: Roman Imankulov Date: Wed, 12 Aug 2020 07:31:45 +0100 Subject: [PATCH 029/127] Remove unnecessary f-string modifier Make the flake8's "F541 f-string is missing placeholders" happy --- patterns/structural/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/structural/proxy.py b/patterns/structural/proxy.py index 51edb4a7..3ef74ec0 100644 --- a/patterns/structural/proxy.py +++ b/patterns/structural/proxy.py @@ -56,7 +56,7 @@ def do_the_job(self, user: str) -> None: if user == "admin": self._real_subject.do_the_job(user) else: - print(f"[log] I can do the job just for `admins`.") + print("[log] I can do the job just for `admins`.") def client(job_doer: Union[RealSubject, Proxy], user: str) -> None: From 5ed7713fc8e2ff5bcc1fa59f776fa49c79749b75 Mon Sep 17 00:00:00 2001 From: Roman Imankulov Date: Wed, 12 Aug 2020 18:21:39 +0100 Subject: [PATCH 030/127] Update tox.ini to use py37,py38 instead of ci37,ci38 As defined in the tox documentation (https://tox.readthedocs.io/en/latest/config.html#tox-environments), environment with names "pyNM" have special meaning and implicitly define the Python interpreter. Environment names ci37 and ci38 don't have any special meaning and run the default version of the interpreter. This results in that that both environments use the same interpreter locally. The PR changes the names of environments to make sure that tox creates a virtual environment with the correct interpreter version. --- .travis.yml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index ba68fde3..0e8de47f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,9 @@ sudo: false matrix: include: - python: "3.7" - env: TOXENV=ci37 + env: TOXENV=py37 - python: "3.8" - env: TOXENV=ci38 + env: TOXENV=py38 cache: - pip diff --git a/tox.ini b/tox.ini index d86eeec9..cfbc689a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = ci37,ci38,cov-report +envlist = py37,py38,cov-report [testenv] From 62e6fd509deba8382ea508138406eab130b24cb9 Mon Sep 17 00:00:00 2001 From: LCVcode Date: Sun, 16 Aug 2020 00:37:54 -0400 Subject: [PATCH 031/127] Implemented breadth first search for shortest path in a graph --- patterns/other/graph_search.py | 42 ++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index 7e0885c7..04a128d4 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -1,12 +1,15 @@ class GraphSearch: """Graph search emulation in python, from source - http://www.python.org/doc/essays/graphs/""" + http://www.python.org/doc/essays/graphs/ + + dfs stands for Depth First Search + bfs stands for Breadth First Search""" def __init__(self, graph): self.graph = graph - def find_path(self, start, end, path=None): + def find_path_dfs(self, start, end, path=None): path = path or [] path.append(start) @@ -18,7 +21,7 @@ def find_path(self, start, end, path=None): if newpath: return newpath - def find_all_path(self, start, end, path=None): + def find_all_paths_dfs(self, start, end, path=None): path = path or [] path.append(start) if start == end: @@ -30,7 +33,7 @@ def find_all_path(self, start, end, path=None): paths.extend(newpaths) return paths - def find_shortest_path(self, start, end, path=None): + def find_shortest_path_dfs(self, start, end, path=None): path = path or [] path.append(start) @@ -45,6 +48,29 @@ def find_shortest_path(self, start, end, path=None): shortest = newpath return shortest + def find_shortest_path_bfs(self, start, end): + queue = [start] + dist_to = {start: 0} + edge_to = {} + + while len(queue): + value = queue.pop(0) + for node in self.graph[value]: + if node not in dist_to.keys(): + edge_to[node] = value + dist_to[node] = dist_to[value] + 1 + queue.append(node) + if end in edge_to.values(): + break + + path = [] + node = end + while dist_to[node] != 0: + path.insert(0, node) + node = edge_to[node] + path.insert(0, start) + return path + def main(): """ @@ -54,12 +80,14 @@ def main(): # initialization of new graph search object >>> graph1 = GraphSearch(graph) - >>> print(graph1.find_path('A', 'D')) + >>> print(graph1.find_path_dfs('A', 'D')) ['A', 'B', 'C', 'D'] - >>> print(graph1.find_all_path('A', 'D')) + >>> print(graph1.find_all_paths_dfs('A', 'D')) [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] - >>> print(graph1.find_shortest_path('A', 'D')) + >>> print(graph1.find_shortest_path_dfs('A', 'D')) ['A', 'B', 'D'] + >>> print(graph1.find_shortest_path_bfs('A', 'D')) + ['A', 'B', 'C'] """ From 0829e8ce3c6c6f844d33228df015022196e1667b Mon Sep 17 00:00:00 2001 From: Santosh Bandichode Date: Wed, 23 Sep 2020 05:22:47 -0700 Subject: [PATCH 032/127] Used correct function names in recursive calls and fixed the doctest --- patterns/other/graph_search.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index 04a128d4..94dc4ebd 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -17,7 +17,7 @@ def find_path_dfs(self, start, end, path=None): return path for node in self.graph.get(start, []): if node not in path: - newpath = self.find_path(node, end, path[:]) + newpath = self.find_path_dfs(node, end, path[:]) if newpath: return newpath @@ -29,7 +29,7 @@ def find_all_paths_dfs(self, start, end, path=None): paths = [] for node in self.graph.get(start, []): if node not in path: - newpaths = self.find_all_path(node, end, path[:]) + newpaths = self.find_all_paths_dfs(node, end, path[:]) paths.extend(newpaths) return paths @@ -42,7 +42,7 @@ def find_shortest_path_dfs(self, start, end, path=None): shortest = None for node in self.graph.get(start, []): if node not in path: - newpath = self.find_shortest_path(node, end, path[:]) + newpath = self.find_shortest_path_dfs(node, end, path[:]) if newpath: if not shortest or len(newpath) < len(shortest): shortest = newpath @@ -87,7 +87,7 @@ def main(): >>> print(graph1.find_shortest_path_dfs('A', 'D')) ['A', 'B', 'D'] >>> print(graph1.find_shortest_path_bfs('A', 'D')) - ['A', 'B', 'C'] + ['A', 'B', 'D'] """ From 27e9de226e804be83cebdb2abc86894c4d6bf5af Mon Sep 17 00:00:00 2001 From: Santosh Bandichode Date: Sat, 26 Sep 2020 01:28:03 -0700 Subject: [PATCH 033/127] Added base case and return the path as soon as end node is visited --- patterns/other/graph_search.py | 72 ++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index 94dc4ebd..9b04925f 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -53,6 +53,9 @@ def find_shortest_path_bfs(self, start, end): dist_to = {start: 0} edge_to = {} + if start == end: + return queue + while len(queue): value = queue.pop(0) for node in self.graph[value]: @@ -60,34 +63,71 @@ def find_shortest_path_bfs(self, start, end): edge_to[node] = value dist_to[node] = dist_to[value] + 1 queue.append(node) - if end in edge_to.values(): - break - - path = [] - node = end - while dist_to[node] != 0: - path.insert(0, node) - node = edge_to[node] - path.insert(0, start) - return path + if end in edge_to.keys(): + path = [] + node = end + while dist_to[node] != 0: + path.insert(0, node) + node = edge_to[node] + path.insert(0, start) + return path def main(): """ # example of graph usage - >>> graph = {'A': ['B', 'C'], 'B': ['C', 'D'], 'C': ['D'], 'D': ['C'], 'E': ['F'], 'F': ['C']} + >>> graph = { + ... 'A': ['B', 'C'], + ... 'B': ['C', 'D'], + ... 'C': ['D', 'G'], + ... 'D': ['C'], + ... 'E': ['F'], + ... 'F': ['C'], + ... 'G': ['E'], + ... 'H': ['C'] + ... } # initialization of new graph search object - >>> graph1 = GraphSearch(graph) + >>> graph_search = GraphSearch(graph) - >>> print(graph1.find_path_dfs('A', 'D')) + >>> print(graph_search.find_path_dfs('A', 'D')) ['A', 'B', 'C', 'D'] - >>> print(graph1.find_all_paths_dfs('A', 'D')) + + # start the search somewhere in the middle + >>> print(graph_search.find_path_dfs('G', 'F')) + ['G', 'E', 'F'] + + # unreachable node + >>> print(graph_search.find_path_dfs('C', 'H')) + None + + # non existing node + >>> print(graph_search.find_path_dfs('C', 'X')) + None + + >>> print(graph_search.find_all_paths_dfs('A', 'D')) [['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']] - >>> print(graph1.find_shortest_path_dfs('A', 'D')) + >>> print(graph_search.find_shortest_path_dfs('A', 'D')) ['A', 'B', 'D'] - >>> print(graph1.find_shortest_path_bfs('A', 'D')) + >>> print(graph_search.find_shortest_path_dfs('A', 'F')) + ['A', 'C', 'G', 'E', 'F'] + + >>> print(graph_search.find_shortest_path_bfs('A', 'D')) ['A', 'B', 'D'] + >>> print(graph_search.find_shortest_path_bfs('A', 'F')) + ['A', 'C', 'G', 'E', 'F'] + + # start the search somewhere in the middle + >>> print(graph_search.find_shortest_path_bfs('G', 'F')) + ['G', 'E', 'F'] + + # unreachable node + >>> print(graph_search.find_shortest_path_bfs('A', 'H')) + None + + # non existing node + >>> print(graph_search.find_shortest_path_bfs('A', 'X')) + None """ From 335fd5b38ea517a15f9767f20c4fa774ce3b41ab Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:33:45 +0100 Subject: [PATCH 034/127] Fix typo discovered by codespell ___codespell --quiet-level=2___ ``` ./python-patterns/patterns/behavioral/publish_subscribe.py:68: subscirbed ==> subscribed ./python-patterns/patterns/structural/front_controller.py:30: cant ==> can't ./python-patterns/patterns/structural/front_controller.py:72: cant ==> can't ``` --- patterns/behavioral/publish_subscribe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index d9e2a7c4..698cf8d1 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -65,7 +65,7 @@ def main(): >>> vani.subscribe("movie") >>> vani.unsubscribe("movie") - # Note that no one subscirbed to `ads` + # Note that no one subscribed to `ads` # and that vani changed their mind >>> fftv.publish("cartoon") From d8c0fd620968637ccff299e1f212c858a8a141fd Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:37:47 +0100 Subject: [PATCH 035/127] =?UTF-8?q?cant=20=E2=80=94>=20Cannot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://www.grammarly.com/blog/cannot-or-can-not/ --- patterns/structural/front_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 02d6aecb..9377fefe 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -27,7 +27,7 @@ def dispatch(self, request): elif request.type == Request.tablet_type: self.tablet_view.show_index_page() else: - print("cant dispatch the request") + print("Cannot dispatch the request") class RequestController: @@ -69,7 +69,7 @@ def main(): Displaying tablet index page >>> front_controller.dispatch_request(Request('desktop')) - cant dispatch the request + Cannot dispatch the request >>> front_controller.dispatch_request('mobile') request must be a Request object From f5d0a43fd2d1e36177e6f9ae5193783a931babc2 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:40:07 +0100 Subject: [PATCH 036/127] Travis CI: Test on Python 3.9 --- .travis.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0e8de47f..f7719a4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,19 @@ -dist: xenial +os: linux +dist: focal language: python -sudo: false - -matrix: +jobs: include: - python: "3.7" env: TOXENV=py37 - - python: "3.8" - env: TOXENV=py38 + - python: "3.9" + env: TOXENV=py39 cache: - pip install: - - pip install tox - - pip install codecov + - pip install codecov tox script: - tox From aa794c8e38f6e01457a15850ca61b7f3de94a7a7 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:41:12 +0100 Subject: [PATCH 037/127] tox.ini: py39 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cfbc689a..96239dc8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,cov-report +envlist = py37,py39,cov-report [testenv] From 62c72ca30c2855c42df2ea3d76b72feb08efb559 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:46:57 +0100 Subject: [PATCH 038/127] GitHub Action to lint Python code --- .github/workflows/lint_python.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/lint_python.yml diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml new file mode 100644 index 00000000..5639a1ce --- /dev/null +++ b/.github/workflows/lint_python.yml @@ -0,0 +1,19 @@ +name: lint_python +on: [pull_request, push] +jobs: + lint_python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install bandit black codespell flake8 isort pytest pyupgrade tox + - run: bandit -r . || true + - run: black --check . || true + - run: codespell --quiet-level=2 || true # --ignore-words-list="" --skip="" + - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + - run: isort --profile black . || true + - run: tox + - run: pip install -r requirements.txt || true + - run: pytest . || true + - run: pytest --doctest-modules . || true + - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true From b936fcbac5066addea61a3cbf13056cbaeeadeb4 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:49:39 +0100 Subject: [PATCH 039/127] tox.ini: py38,py39 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cfbc689a..b664ae2a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,cov-report +envlist = py38,py39,cov-report [testenv] From f343b5a1918000dd55e309889bbf510d68dd8be0 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:56:19 +0100 Subject: [PATCH 040/127] skip_missing_interpreters --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b664ae2a..4b9efb9c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = py38,py39,cov-report +skip_missing_interpreters = true [testenv] @@ -8,7 +9,7 @@ setenv = deps = -r requirements-dev.txt commands = - flake8 patterns/ + flake8 . ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` pytest --randomly-seed=1234 --doctest-modules patterns/ pytest -s -vv --cov={envsitepackagesdir}/patterns --log-level=INFO tests/ From 5ef9a81d71820f4c4ea22d0d4de6b94ad32dc888 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 03:59:06 +0100 Subject: [PATCH 041/127] Update lint_python.yml --- .github/workflows/lint_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 5639a1ce..7a4251f0 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -10,7 +10,7 @@ jobs: - run: bandit -r . || true - run: black --check . || true - run: codespell --quiet-level=2 || true # --ignore-words-list="" --skip="" - - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + - run: flake8 . --count --show-source --statistics - run: isort --profile black . || true - run: tox - run: pip install -r requirements.txt || true From ab62f57d45ce1b32fcf21a9ffdfd17bd87ff5ba2 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 31 Dec 2020 04:00:15 +0100 Subject: [PATCH 042/127] flake8 . --exclude=./.* --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4b9efb9c..c969e891 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ setenv = deps = -r requirements-dev.txt commands = - flake8 . + flake8 . --exclude=./.* ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` pytest --randomly-seed=1234 --doctest-modules patterns/ pytest -s -vv --cov={envsitepackagesdir}/patterns --log-level=INFO tests/ From 54c21bdc9fcec0d6a0412227735bb26ab4958679 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 2 Jan 2021 08:11:52 +0100 Subject: [PATCH 043/127] Make codespell and isort mandatory tests Make codespell and isort mandatory tests. Drop bandit whose test results are not enlightening. --- .github/workflows/lint_python.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 7a4251f0..56868af1 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -6,14 +6,14 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - run: pip install bandit black codespell flake8 isort pytest pyupgrade tox - - run: bandit -r . || true + - run: pip install --upgrade pip + - run: pip install black codespell flake8 isort pytest pyupgrade tox - run: black --check . || true - - run: codespell --quiet-level=2 || true # --ignore-words-list="" --skip="" + - run: codespell --quiet-level=2 # --ignore-words-list="" --skip="" - run: flake8 . --count --show-source --statistics - - run: isort --profile black . || true + - run: isort --profile black . - run: tox - - run: pip install -r requirements.txt || true + - run: pip install -e . - run: pytest . || true - run: pytest --doctest-modules . || true - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true From 3f94a9e08580ce27e2d9813adcab0baedf1133e9 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 2 Jan 2021 08:49:55 +0100 Subject: [PATCH 044/127] Make pytest . a mandatory test --- .github/workflows/lint_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 56868af1..2eb31725 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -14,6 +14,6 @@ jobs: - run: isort --profile black . - run: tox - run: pip install -e . - - run: pytest . || true + - run: pytest . - run: pytest --doctest-modules . || true - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true From a133a6a32a57554bd7fab5a7ae06d303cd929e5e Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 2 Jan 2021 12:43:33 +0100 Subject: [PATCH 045/127] pyupgrade --py36-plus **/*.py --- .github/workflows/lint_python.yml | 7 ++++--- patterns/behavioral/iterator.py | 3 +-- patterns/behavioral/memento.py | 2 +- patterns/behavioral/publish_subscribe.py | 2 +- patterns/behavioral/template.py | 6 +++--- patterns/creational/abstract_factory.py | 4 ++-- patterns/dependency_injection.py | 2 +- patterns/structural/bridge.py | 4 ++-- patterns/structural/decorator.py | 4 ++-- patterns/structural/facade.py | 4 ++-- patterns/structural/flyweight.py | 2 +- patterns/structural/mvc.py | 7 +++---- tests/creational/test_lazy.py | 6 ++---- tests/creational/test_pool.py | 4 ++-- tests/test_hsm.py | 8 ++++---- 15 files changed, 31 insertions(+), 34 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 2eb31725..7775b439 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -7,13 +7,14 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: pip install --upgrade pip - - run: pip install black codespell flake8 isort pytest pyupgrade tox - - run: black --check . || true + - run: pip install black codespell flake8 isort mypy pytest pyupgrade tox + - run: black --check . - run: codespell --quiet-level=2 # --ignore-words-list="" --skip="" - run: flake8 . --count --show-source --statistics - run: isort --profile black . - run: tox - run: pip install -e . + - run: mypy --ignore-missing-imports . - run: pytest . - run: pytest --doctest-modules . || true - - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true + - run: shopt -s globstar && pyupgrade --py36-plus **/*.py diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index 7af9ea02..3c6ec985 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -10,8 +10,7 @@ def count_to(count): """Counts by word numbers, up to a maximum of five""" numbers = ["one", "two", "three", "four", "five"] - for number in numbers[:count]: - yield number + yield from numbers[:count] # Test the generator diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index ef551438..7ac7aa28 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -66,7 +66,7 @@ def __init__(self, value): self.value = value def __repr__(self): - return "<%s: %r>" % (self.__class__.__name__, self.value) + return f"<{self.__class__.__name__}: {self.value!r}>" def increment(self): self.value += 1 diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index 698cf8d1..760d8e7b 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -46,7 +46,7 @@ def unsubscribe(self, msg): self.provider.unsubscribe(msg, self) def run(self, msg): - print("{} got {}".format(self.name, msg)) + print(f"{self.name} got {msg}") def main(): diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index 6596fee5..d2d83174 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -24,7 +24,7 @@ def get_csv(): def convert_to_text(data): print("[CONVERT]") - return "{} as text".format(data) + return f"{data} as text" def saver(): @@ -33,7 +33,7 @@ def saver(): def template_function(getter, converter=False, to_save=False): data = getter() - print("Got `{}`".format(data)) + print(f"Got `{data}`") if len(data) <= 3 and converter: data = converter(data) @@ -43,7 +43,7 @@ def template_function(getter, converter=False, to_save=False): if to_save: saver() - print("`{}` was processed".format(data)) + print(f"`{data}` was processed") def main(): diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 63648636..9e2e73d4 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -46,8 +46,8 @@ def show_pet(self): """Creates and shows a pet using the abstract factory""" pet = self.pet_factory() - print("We have a lovely {}".format(pet)) - print("It says {}".format(pet.speak())) + print(f"We have a lovely {pet}") + print(f"It says {pet.speak()}") class Dog: diff --git a/patterns/dependency_injection.py b/patterns/dependency_injection.py index e5ddfbac..2979f763 100644 --- a/patterns/dependency_injection.py +++ b/patterns/dependency_injection.py @@ -74,7 +74,7 @@ def production_code_time_provider() -> str: datetime for this example). """ current_time = datetime.datetime.now() - current_time_formatted = "{}:{}".format(current_time.hour, current_time.minute) + current_time_formatted = f"{current_time.hour}:{current_time.minute}" return current_time_formatted diff --git a/patterns/structural/bridge.py b/patterns/structural/bridge.py index e2f6fa10..feddb675 100644 --- a/patterns/structural/bridge.py +++ b/patterns/structural/bridge.py @@ -10,13 +10,13 @@ # ConcreteImplementor 1/2 class DrawingAPI1: def draw_circle(self, x, y, radius): - print("API1.circle at {}:{} radius {}".format(x, y, radius)) + print(f"API1.circle at {x}:{y} radius {radius}") # ConcreteImplementor 2/2 class DrawingAPI2: def draw_circle(self, x, y, radius): - print("API2.circle at {}:{} radius {}".format(x, y, radius)) + print(f"API2.circle at {x}:{y} radius {radius}") # Refined Abstraction diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index 08504bcf..01c91b00 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -42,7 +42,7 @@ def __init__(self, wrapped): self._wrapped = wrapped def render(self): - return "{}".format(self._wrapped.render()) + return f"{self._wrapped.render()}" class ItalicWrapper(TextTag): @@ -52,7 +52,7 @@ def __init__(self, wrapped): self._wrapped = wrapped def render(self): - return "{}".format(self._wrapped.render()) + return f"{self._wrapped.render()}" def main(): diff --git a/patterns/structural/facade.py b/patterns/structural/facade.py index 290afe6d..6561c6dc 100644 --- a/patterns/structural/facade.py +++ b/patterns/structural/facade.py @@ -51,7 +51,7 @@ class Memory: """ def load(self, position, data): - print("Loading from {0} data: '{1}'.".format(position, data)) + print(f"Loading from {position} data: '{data}'.") class SolidStateDrive: @@ -60,7 +60,7 @@ class SolidStateDrive: """ def read(self, lba, size): - return "Some data from sector {0} with size {1}".format(lba, size) + return f"Some data from sector {lba} with size {size}" class ComputerFacade: diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index b9589319..29015705 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -53,7 +53,7 @@ def __new__(cls, value, suit): # self.value, self.suit = value, suit def __repr__(self): - return "".format(self.value, self.suit) + return f"" def main(): diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index c57c9041..df6611ce 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -29,7 +29,7 @@ class Price(float): __str__ functionality.""" def __str__(self): - return "{:.2f}".format(self) + return f"{self:.2f}" products = { "milk": {"price": Price(1.50), "quantity": 10}, @@ -40,8 +40,7 @@ def __str__(self): item_type = "product" def __iter__(self): - for item in self.products: - yield item + yield from self.products def get(self, product): try: @@ -86,7 +85,7 @@ def show_item_information(self, item_type, item_name, item_info): print(printout) def item_not_found(self, item_type, item_name): - print('That {} "{}" does not exist in the records'.format(item_type, item_name)) + print(f'That {item_type} "{item_name}" does not exist in the records') class Controller: diff --git a/tests/creational/test_lazy.py b/tests/creational/test_lazy.py index 8d92be88..1b815b60 100644 --- a/tests/creational/test_lazy.py +++ b/tests/creational/test_lazy.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import unittest from patterns.creational.lazy_evaluation import Person @@ -19,7 +17,7 @@ def test_relatives_not_in_properties(self): self.assertNotIn("relatives", self.John.__dict__) def test_extended_properties(self): - print(u"John's relatives: {0}".format(self.John.relatives)) + print(f"John's relatives: {self.John.relatives}") self.assertDictEqual( { "name": "John", @@ -31,7 +29,7 @@ def test_extended_properties(self): ) def test_relatives_after_access(self): - print(u"John's relatives: {0}".format(self.John.relatives)) + print(f"John's relatives: {self.John.relatives}") self.assertIn("relatives", self.John.__dict__) def test_parents(self): diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py index 18f844e3..38476eb7 100644 --- a/tests/creational/test_pool.py +++ b/tests/creational/test_pool.py @@ -31,8 +31,8 @@ def test_frozen_pool(self): class TestNaitivePool(unittest.TestCase): """def test_object(queue): - queue_object = QueueObject(queue, True) - print('Inside func: {}'.format(queue_object.object))""" + 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() diff --git a/tests/test_hsm.py b/tests/test_hsm.py index b6ff3065..f42323a9 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -35,16 +35,16 @@ def test_calling_next_state_shall_change_current_state(cls): 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... """ + """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... """ + """Exemplary 2nd level state test class (here: Standby state). Add missing + state test classes...""" @classmethod def setUpClass(cls): From c6d82e158254f7a113a995ce2706bf49eaf330ff Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 2 Jan 2021 12:46:57 +0100 Subject: [PATCH 046/127] pyupgrade --py36-plus **/*.py --- .github/workflows/lint_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 7775b439..63796567 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -14,7 +14,7 @@ jobs: - run: isort --profile black . - run: tox - run: pip install -e . - - run: mypy --ignore-missing-imports . + - run: mypy --ignore-missing-imports . || true - run: pytest . - run: pytest --doctest-modules . || true - run: shopt -s globstar && pyupgrade --py36-plus **/*.py From 20f06fdb8352c3a11c2908c416d789d21450a1f7 Mon Sep 17 00:00:00 2001 From: Antony Oduor Date: Tue, 19 Jan 2021 21:24:43 -0700 Subject: [PATCH 047/127] Fix Typo --- patterns/creational/lazy_evaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py index b3e73743..ea087e59 100644 --- a/patterns/creational/lazy_evaluation.py +++ b/patterns/creational/lazy_evaluation.py @@ -10,7 +10,7 @@ https://github.com/django/django/blob/ffd18732f3ee9e6f0374aff9ccf350d85187fac2/django/utils/functional.py#L19 pip https://github.com/pypa/pip/blob/cb75cca785629e15efb46c35903827b3eae13481/pip/utils/__init__.py#L821 -pyramimd +pyramid https://github.com/Pylons/pyramid/blob/7909e9503cdfc6f6e84d2c7ace1d3c03ca1d8b73/pyramid/decorator.py#L4 werkzeug https://github.com/pallets/werkzeug/blob/5a2bf35441006d832ab1ed5a31963cbc366c99ac/werkzeug/utils.py#L35 From f72b1ba1b466c5dd1f772e69cd90e18ff6f834be Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Sun, 24 Jan 2021 16:17:17 +0900 Subject: [PATCH 048/127] Update abstract_factory more elaborate --- patterns/creational/abstract_factory.py | 82 +++++++++++++++---------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 9e2e73d4..3c221a36 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -31,71 +31,85 @@ """ import random +from typing import Type -class PetShop: +class Pet: + def __init__(self, name: str) -> None: + self.name = name - """A pet shop""" + def speak(self) -> None: + raise NotImplementedError - def __init__(self, animal_factory=None): - """pet_factory is our abstract factory. We can set it at will.""" + def __str__(self) -> str: + raise NotImplementedError - self.pet_factory = animal_factory - def show_pet(self): - """Creates and shows a pet using the abstract factory""" +class Dog(Pet): + def speak(self) -> None: + print("woof") + + def __str__(self) -> str: + return f"Dog<{self.name}>" + - pet = self.pet_factory() - print(f"We have a lovely {pet}") - print(f"It says {pet.speak()}") +class Cat(Pet): + def speak(self) -> None: + print("meow") + def __str__(self) -> str: + return f"Cat<{self.name}>" -class Dog: - def speak(self): - return "woof" - def __str__(self): - return "Dog" +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.""" -class Cat: - def speak(self): - return "meow" + self.pet_factory = animal_factory + + def buy_pet(self, name: str) -> Pet: + """Creates and shows a pet using the abstract factory""" - def __str__(self): - return "Cat" + pet = self.pet_factory(name) + print(f"Here is your lovely {pet}") + return pet # Additional factories: # Create a random animal -def random_animal(): +def random_animal(name: str) -> Pet: """Let's be dynamic!""" - return random.choice([Dog, Cat])() + return random.choice([Dog, Cat])(name) # Show pets with various factories -def main(): +def main() -> None: """ # A Shop that sells only cats >>> cat_shop = PetShop(Cat) - >>> cat_shop.show_pet() - We have a lovely Cat - It says meow + >>> pet = cat_shop.buy_pet("Lucy") + Here is your lovely Cat + >>> pet.speak() + meow # A shop that sells random animals >>> shop = PetShop(random_animal) - >>> for i in range(3): - ... shop.show_pet() + >>> for name in ["Max", "Jack", "Buddy"]: + ... pet = shop.buy_pet(name) + ... pet.speak() ... print("=" * 20) - We have a lovely Cat - It says meow + Here is your lovely Cat + meow ==================== - We have a lovely Dog - It says woof + Here is your lovely Dog + woof ==================== - We have a lovely Dog - It says woof + Here is your lovely Dog + woof ==================== """ From 74eb6343f846ed3a220a2c078dd4901cd0be0957 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Sun, 24 Jan 2021 16:31:06 +0900 Subject: [PATCH 049/127] Update test of abstract factory --- tests/creational/test_abstract_factory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/creational/test_abstract_factory.py b/tests/creational/test_abstract_factory.py index ad818f59..1676e59d 100644 --- a/tests/creational/test_abstract_factory.py +++ b/tests/creational/test_abstract_factory.py @@ -8,5 +8,6 @@ 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: - dog_pet_shop.show_pet() + pet = dog_pet_shop.buy_pet("") + pet.speak() self.assertEqual(mock_Dog_speak.call_count, 1) From a12ad8e7dc00572ba09bcacddb4862cc8cbdba4d Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Sun, 24 Jan 2021 19:14:20 +0900 Subject: [PATCH 050/127] Update prototype --- patterns/creational/prototype.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 18fd602c..20b367ee 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -16,20 +16,20 @@ 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 typing import Any, Dict -class Prototype: - value = "default" +class Prototype: + def __init__(self, value: str = "default", **attrs: Any) -> None: + self.value = value + self.__dict__.update(attrs) - def clone(self, **attrs): + def clone(self, **attrs: Any) -> None: """Clone a prototype and update inner attributes dictionary""" - # Python in Practice, Mark Summerfield - obj = self.__class__() + # copy.deepcopy can be used instead of next line. + obj = self.__class__(**self.__dict__) obj.__dict__.update(attrs) return obj @@ -38,33 +38,36 @@ class PrototypeDispatcher: def __init__(self): self._objects = {} - def get_objects(self): + def get_objects(self) -> Dict[str, Prototype]: """Get all objects""" return self._objects - def register_object(self, name, obj): + def register_object(self, name: str, obj: Prototype) -> None: """Register an object""" self._objects[name] = obj - def unregister_object(self, name): + def unregister_object(self, name: str) -> None: """Unregister an object""" del self._objects[name] -def main(): +def main() -> None: """ >>> dispatcher = PrototypeDispatcher() >>> prototype = Prototype() >>> d = prototype.clone() >>> a = prototype.clone(value='a-value', category='a') - >>> b = prototype.clone(value='b-value', is_checked=True) + >>> 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 """ From b548b455b2801039bfa679b978df1497521bf53a Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Sun, 24 Jan 2021 19:49:58 +0900 Subject: [PATCH 051/127] house keeping update --- .gitignore | 3 ++- .travis.yml | 4 ++-- Makefile | 7 +++---- patterns/behavioral/iterator.py | 8 ++++++-- requirements-dev.txt | 8 +++++--- setup.cfg | 2 +- setup.py | 2 ++ tox.ini | 4 ++-- 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index a7379521..8b2c28d8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ __pycache__ .tox/ venv .vscode/ -.python-version \ No newline at end of file +.python-version +.coverage diff --git a/.travis.yml b/.travis.yml index f7719a4c..ab6ba6bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ language: python jobs: include: - - python: "3.7" - env: TOXENV=py37 + - python: "3.8" + env: TOXENV=py38 - python: "3.9" env: TOXENV=py39 diff --git a/Makefile b/Makefile index 25826c8b..92ba244a 100644 --- a/Makefile +++ b/Makefile @@ -39,9 +39,8 @@ ifeq ("$(wildcard venv/bin/pip-sync)","") endif # pip-tools - @pip-compile --upgrade requirements-dev.txt - @pip-compile --upgrade requirements.txt - @pip-sync requirements-dev.txt requirements.txt + # @pip-compile --upgrade requirements-dev.txt + @pip-sync requirements-dev.txt .PHONY: pylinter @@ -85,4 +84,4 @@ endif --select "B,C,E,F,W,T4,B9" \ --ignore "E203,E266,E501,W503,F403,F401,E402" \ --exclude ".git,__pycache__,old, build, \ - dist, venv" $(path) + dist, venv, .tox" $(path) diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index 3c6ec985..40162461 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -14,8 +14,12 @@ def count_to(count): # Test the generator -count_to_two = lambda: count_to(2) -count_to_five = lambda: count_to(5) +def count_to_two() -> None: + return count_to(2) + + +def count_to_five() -> None: + return count_to(5) def main(): diff --git a/requirements-dev.txt b/requirements-dev.txt index 451dad45..0de4748b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,8 @@ -e . -pytest~=4.3.0 -pytest-cov~=2.6.0 -flake8~=3.7.0 +pytest~=6.2.0 +pytest-cov~=2.11.0 pytest-randomly~=3.1.0 +black>=20.8b1 +isort~=5.7.0 +flake8~=3.8.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 159357a4..8f2de0ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [flake8] max-line-length = 120 ignore = E266 E731 W503 -exclude = .venv* +exclude = venv* [tool:pytest] filterwarnings = diff --git a/setup.py b/setup.py index b4218c1c..b4d2cdf1 100644 --- a/setup.py +++ b/setup.py @@ -10,5 +10,7 @@ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], ) diff --git a/tox.ini b/tox.ini index ee8b9579..168e2c9d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,cov-report +envlist = py38,py39,cov-report skip_missing_interpreters = true @@ -9,7 +9,7 @@ setenv = deps = -r requirements-dev.txt commands = - flake8 . --exclude=./.* + flake8 . --exclude="./.*, venv" ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` pytest --randomly-seed=1234 --doctest-modules patterns/ pytest -s -vv --cov={envsitepackagesdir}/patterns --log-level=INFO tests/ From 9a8be5cd4c4cdfc77e943f4e39a0582c733bf677 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Sun, 24 Jan 2021 20:38:37 +0900 Subject: [PATCH 052/127] Update prototype.py --- patterns/creational/prototype.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 20b367ee..ebb0e9e1 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -16,6 +16,9 @@ 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 typing import Any, Dict From 666ef63f5753f532b490fe062e4b906d371f114d Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Sun, 24 Jan 2021 20:38:58 +0900 Subject: [PATCH 053/127] Update prototype.py --- patterns/creational/prototype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index ebb0e9e1..6dbb66ff 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -17,7 +17,7 @@ Below provides an example of such Dispatcher, which contains three copies of the prototype: 'default', 'objecta' and 'objectb'. -*TL;DR +*TL;DR Creates new object instances by cloning prototype. """ From 7c3f71fd7aca4dfef3382efc1e189cc7f6fe5f16 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Sun, 24 Jan 2021 20:40:03 +0900 Subject: [PATCH 054/127] Update prototype.py --- patterns/creational/prototype.py | 1 + 1 file changed, 1 insertion(+) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 6dbb66ff..597f409d 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -31,6 +31,7 @@ def __init__(self, value: str = "default", **attrs: Any) -> None: def clone(self, **attrs: Any) -> None: """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) From 7f71d370879b6c0602c5ac3c91ed9ceaa241dea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB?= Date: Thu, 27 May 2021 17:44:08 +0300 Subject: [PATCH 055/127] Added type hints and pytest tests --- patterns/behavioral/strategy.py | 67 +++++++++++++++++++++++-------- tests/behavioral/test_strategy.py | 52 ++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 tests/behavioral/test_strategy.py diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 92d11f25..638a40f6 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -7,42 +7,77 @@ Enables selecting an algorithm at runtime. """ +from __future__ import annotations + +from typing import Callable, Type + + +class DiscountStrategyValidator: # Descriptor class for check perform + def validate(self, obj: Order, value: Callable) -> bool: + try: + if obj.price - value(obj) < 0: + raise ValueError(f"Discount cannot be applied due to negative price resulting. {value.__name__}") + return True + except ValueError as ex: + print(str(ex)) + return False + + def __set_name__(self, owner, name: str) -> None: + self.private_name = '_' + name + + def __set__(self, obj: Order, value: Callable = None) -> None: + if value and self.validate(obj, value): + setattr(obj, self.private_name, value) + else: + setattr(obj, self.private_name, None) + + def __get__(self, obj: object, objtype: Type = None): + return getattr(obj, self.private_name) + class Order: - def __init__(self, price, discount_strategy=None): - self.price = price + discount_strategy = DiscountStrategyValidator() + + def __init__(self, price: float, discount_strategy: Callable = None) -> None: + self.price: float = price self.discount_strategy = discount_strategy - def price_after_discount(self): + def apply_discount(self) -> float: if self.discount_strategy: discount = self.discount_strategy(self) else: discount = 0 + return self.price - discount - def __repr__(self): - fmt = "" - return fmt.format(self.price, self.price_after_discount()) + def __repr__(self) -> str: + return f"" -def ten_percent_discount(order): +def ten_percent_discount(order: Order) -> float: return order.price * 0.10 -def on_sale_discount(order): +def on_sale_discount(order: Order) -> float: return order.price * 0.25 + 20 def main(): """ - >>> Order(100) - - - >>> Order(100, discount_strategy=ten_percent_discount) - - - >>> Order(1000, discount_strategy=on_sale_discount) - + >>> order = Order(100, discount_strategy=ten_percent_discount) + >>> print(order) + + >>> print(order.apply_discount()) + 90.0 + >>> order = Order(100, discount_strategy=on_sale_discount) + >>> print(order) + + >>> print(order.apply_discount()) + 55.0 + >>> order = Order(10, discount_strategy=on_sale_discount) + Discount cannot be applied due to negative price resulting. on_sale_discount + >>> print(order) + """ diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py new file mode 100644 index 00000000..6a3b2504 --- /dev/null +++ b/tests/behavioral/test_strategy.py @@ -0,0 +1,52 @@ +import pytest + +from patterns.behavioral.strategy import Order, ten_percent_discount, on_sale_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 From b8849c1dbbf0585d331a41ba7b80212a2930c79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB?= Date: Fri, 28 May 2021 09:08:07 +0300 Subject: [PATCH 056/127] resolve review --- patterns/behavioral/strategy.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 638a40f6..93432275 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -7,23 +7,26 @@ Enables selecting an algorithm at runtime. """ + from __future__ import annotations from typing import Callable, Type class DiscountStrategyValidator: # Descriptor class for check perform - def validate(self, obj: Order, value: Callable) -> bool: + @staticmethod + def validate(obj: Order, value: Callable) -> bool: try: if obj.price - value(obj) < 0: raise ValueError(f"Discount cannot be applied due to negative price resulting. {value.__name__}") - return True except ValueError as ex: print(str(ex)) return False + else: + return True def __set_name__(self, owner, name: str) -> None: - self.private_name = '_' + name + self.private_name = f"_{name}" def __set__(self, obj: Order, value: Callable = None) -> None: if value and self.validate(obj, value): From c0677d7302d4de398b8738db19285b4bc345b2d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=B8=D0=BB?= Date: Mon, 31 May 2021 09:06:18 +0300 Subject: [PATCH 057/127] Formatted with black --- patterns/behavioral/strategy.py | 4 +++- patterns/structural/3-tier.py | 6 +++--- patterns/structural/front_controller.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 93432275..595df255 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -18,7 +18,9 @@ class DiscountStrategyValidator: # Descriptor class for check perform def validate(obj: Order, value: Callable) -> bool: try: if obj.price - value(obj) < 0: - raise ValueError(f"Discount cannot be applied due to negative price resulting. {value.__name__}") + raise ValueError( + f"Discount cannot be applied due to negative price resulting. {value.__name__}" + ) except ValueError as ex: print(str(ex)) return False diff --git a/patterns/structural/3-tier.py b/patterns/structural/3-tier.py index 64835f99..ecc04243 100644 --- a/patterns/structural/3-tier.py +++ b/patterns/structural/3-tier.py @@ -7,7 +7,7 @@ class Data: - """ Data Store Class """ + """Data Store Class""" products = { "milk": {"price": 1.50, "quantity": 10}, @@ -22,7 +22,7 @@ def __get__(self, obj, klas): class BusinessLogic: - """ Business logic holding data store instances """ + """Business logic holding data store instances""" data = Data() @@ -36,7 +36,7 @@ def product_information( class Ui: - """ UI interaction class """ + """UI interaction class""" def __init__(self) -> None: self.business_logic = BusinessLogic() diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 9377fefe..d93f74d6 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -31,7 +31,7 @@ def dispatch(self, request): class RequestController: - """ front controller """ + """front controller""" def __init__(self): self.dispatcher = Dispatcher() @@ -44,7 +44,7 @@ def dispatch_request(self, request): class Request: - """ request """ + """request""" mobile_type = "mobile" tablet_type = "tablet" From b78348e98906ecb550a4aa4f47a5c4d1dfaa24b9 Mon Sep 17 00:00:00 2001 From: Paras Date: Tue, 6 Jul 2021 12:12:34 +0530 Subject: [PATCH 058/127] Closes #377: No return value expeced :bug: --- patterns/creational/prototype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 597f409d..111d19ef 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -29,7 +29,7 @@ def __init__(self, value: str = "default", **attrs: Any) -> None: self.value = value self.__dict__.update(attrs) - def clone(self, **attrs: Any) -> None: + 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. From 2364304bd46f32ce9145e290a6510cdd062c9a87 Mon Sep 17 00:00:00 2001 From: Paras Date: Wed, 7 Jul 2021 10:27:48 +0530 Subject: [PATCH 059/127] :snake: Reformatted with black Closes #375 --- tests/behavioral/test_publish_subscribe.py | 2 +- tests/behavioral/test_strategy.py | 17 +++-------------- tests/structural/test_proxy.py | 6 +++--- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/tests/behavioral/test_publish_subscribe.py b/tests/behavioral/test_publish_subscribe.py index 86488343..c153da5b 100644 --- a/tests/behavioral/test_publish_subscribe.py +++ b/tests/behavioral/test_publish_subscribe.py @@ -27,7 +27,7 @@ def test_subscriber_shall_be_detachable_from_subscriptions(cls): 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) """ + """msg_queue ~ Provider.notify(msg) ~ Publisher.publish(msg)""" expected_msg = "expected msg" pro = Provider() pub = Publisher(pro) diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py index 6a3b2504..86018a44 100644 --- a/tests/behavioral/test_strategy.py +++ b/tests/behavioral/test_strategy.py @@ -9,22 +9,14 @@ def order(): @pytest.mark.parametrize( - "func, discount", - [ - (ten_percent_discount, 10.0), - (on_sale_discount, 45.0) - ] + "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) - ] + "func, price", [(ten_percent_discount, 100), (on_sale_discount, 100)] ) def test_order_discount_strategy_validate_success(func, price): order = Order(price, func) @@ -41,10 +33,7 @@ def test_order_discount_strategy_validate_error(): @pytest.mark.parametrize( "func, price, discount", - [ - (ten_percent_discount, 100, 90.0), - (on_sale_discount, 100, 55.0) - ] + [(ten_percent_discount, 100, 90.0), (on_sale_discount, 100, 55.0)], ) def test_discount_apply_success(func, price, discount): order = Order(price, func) diff --git a/tests/structural/test_proxy.py b/tests/structural/test_proxy.py index ec660986..3409bf0b 100644 --- a/tests/structural/test_proxy.py +++ b/tests/structural/test_proxy.py @@ -8,17 +8,17 @@ class ProxyTest(unittest.TestCase): @classmethod def setUpClass(cls): - """ Class scope setup. """ + """Class scope setup.""" cls.proxy = Proxy() def setUp(cls): - """ Function/test case scope setup. """ + """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. """ + """Function/test case scope teardown.""" cls.output.close() sys.stdout = cls.saved_stdout From ade29d6e54b42de37d03d44daf5709782b8f99d9 Mon Sep 17 00:00:00 2001 From: Yeonggwang Date: Mon, 6 Dec 2021 10:30:26 +0900 Subject: [PATCH 060/127] Added type hints to chaining method pattern --- patterns/behavioral/chaining_method.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 1fd261a4..195bfa58 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -1,22 +1,25 @@ +from __future__ import annotations + + class Person: - def __init__(self, name, action): + def __init__(self, name: str, action: Action) -> None: self.name = name self.action = action - def do_action(self): + def do_action(self) -> Action: print(self.name, self.action.name, end=" ") return self.action class Action: - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - def amount(self, val): + def amount(self, val: str) -> Action: print(val, end=" ") return self - def stop(self): + def stop(self) -> None: print("then stop") From d752949b0efacaff1f300096f0dc4f71ae70a428 Mon Sep 17 00:00:00 2001 From: Yeonggwang Date: Mon, 6 Dec 2021 10:35:04 +0900 Subject: [PATCH 061/127] Added type hints to iterator pattern --- patterns/behavioral/iterator_alt.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py index 2e3a8ba3..d6fb0df9 100644 --- a/patterns/behavioral/iterator_alt.py +++ b/patterns/behavioral/iterator_alt.py @@ -4,6 +4,7 @@ *TL;DR Traverses a container and accesses the container's elements. """ +from __future__ import annotations class NumberWords: @@ -17,14 +18,14 @@ class NumberWords: "five", ) - def __init__(self, start, stop): + def __init__(self, start: int, stop: int) -> None: self.start = start self.stop = stop - def __iter__(self): # this makes the class an Iterable + def __iter__(self) -> NumberWords: # this makes the class an Iterable return self - def __next__(self): # this makes the class an Iterator + def __next__(self) -> str: # this makes the class an Iterator if self.start > self.stop or self.start > len(self._WORD_MAP): raise StopIteration current = self.start From 0ef9e80c2b4dfa427b163cd2900ca95c689c8583 Mon Sep 17 00:00:00 2001 From: Yeonggwang Date: Mon, 6 Dec 2021 11:45:19 +0900 Subject: [PATCH 062/127] Change string literals to class name. [PEP-0563] --- patterns/creational/prototype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 111d19ef..906ed7ab 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -20,6 +20,7 @@ *TL;DR Creates new object instances by cloning prototype. """ +from __future__ import annotations from typing import Any, Dict @@ -29,7 +30,7 @@ def __init__(self, value: str = "default", **attrs: Any) -> None: self.value = value self.__dict__.update(attrs) - def clone(self, **attrs: Any) -> "Prototype": + 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. From d3d00e5809b9abd59bddcd898d28e4e0127c050b Mon Sep 17 00:00:00 2001 From: Yeonggwang Date: Wed, 8 Dec 2021 05:42:00 +0900 Subject: [PATCH 063/127] Solve linter error --- patterns/behavioral/observer.py | 6 +++--- patterns/behavioral/state.py | 2 +- patterns/behavioral/strategy.py | 4 ++-- patterns/creational/prototype.py | 4 ++-- patterns/fundamental/delegation_pattern.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/patterns/behavioral/observer.py b/patterns/behavioral/observer.py index b2d503b7..03d970ad 100644 --- a/patterns/behavioral/observer.py +++ b/patterns/behavioral/observer.py @@ -12,7 +12,7 @@ from __future__ import annotations from contextlib import suppress -from typing import List, Optional, Protocol +from typing import Protocol # define a generic observer type @@ -23,7 +23,7 @@ def update(self, subject: Subject) -> None: class Subject: def __init__(self) -> None: - self._observers: List[Observer] = [] + self._observers: list[Observer] = [] def attach(self, observer: Observer) -> None: if observer not in self._observers: @@ -33,7 +33,7 @@ def detach(self, observer: Observer) -> None: with suppress(ValueError): self._observers.remove(observer) - def notify(self, modifier: Optional[Observer] = None) -> None: + def notify(self, modifier: Observer | None = None) -> None: for observer in self._observers: if modifier != observer: observer.update(self) diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 3c606ca8..423b749e 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -18,7 +18,7 @@ def scan(self): self.pos += 1 if self.pos == len(self.stations): self.pos = 0 - print("Scanning... Station is {} {}".format(self.stations[self.pos], self.name)) + print(f"Scanning... Station is {self.stations[self.pos]} {self.name}") class AmState(State): diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 595df255..88862fa4 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -10,7 +10,7 @@ from __future__ import annotations -from typing import Callable, Type +from typing import Callable class DiscountStrategyValidator: # Descriptor class for check perform @@ -36,7 +36,7 @@ def __set__(self, obj: Order, value: Callable = None) -> None: else: setattr(obj, self.private_name, None) - def __get__(self, obj: object, objtype: Type = None): + def __get__(self, obj: object, objtype: type = None): return getattr(obj, self.private_name) diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 906ed7ab..4151ffbf 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -22,7 +22,7 @@ """ from __future__ import annotations -from typing import Any, Dict +from typing import Any class Prototype: @@ -43,7 +43,7 @@ class PrototypeDispatcher: def __init__(self): self._objects = {} - def get_objects(self) -> Dict[str, Prototype]: + def get_objects(self) -> dict[str, Prototype]: """Get all objects""" return self._objects diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py index 2d2f8534..bdcefc9d 100644 --- a/patterns/fundamental/delegation_pattern.py +++ b/patterns/fundamental/delegation_pattern.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import Any, Callable, Union +from typing import Any, Callable class Delegator: @@ -31,7 +31,7 @@ class Delegator: def __init__(self, delegate: Delegate): self.delegate = delegate - def __getattr__(self, name: str) -> Union[Any, Callable]: + def __getattr__(self, name: str) -> Any | Callable: attr = getattr(self.delegate, name) if not callable(attr): From 2940bc2c6948cf765feb623080b1605f51843e37 Mon Sep 17 00:00:00 2001 From: Eugene Morozov Date: Mon, 13 Dec 2021 08:29:43 +0300 Subject: [PATCH 064/127] Replaces dead link with web archive copy. --- patterns/creational/factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index e70e0f15..905e12ef 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -12,8 +12,8 @@ *Where can the pattern be used practically? The Factory Method can be seen in the popular web framework Django: -http://django.wikispaces.asu.edu/*NEW*+Django+Design+Patterns For -example, in a contact form of a web page, the subject and the message +http://web.archive.org/web/http://django.wikispaces.asu.edu/*NEW*+Django+Design+Patterns +For example, in a contact form of a web page, the subject and the message fields are created using the same form factory (CharField()), even though they have different implementations according to their purposes. From 0bc59cc8716c4995d4512dfa084848564a13a0ea Mon Sep 17 00:00:00 2001 From: flencydoc Date: Thu, 23 Dec 2021 03:54:16 +0000 Subject: [PATCH 065/127] Added docstrings to several undocumented functions. --- patterns/behavioral/memento.py | 5 ++ patterns/creational/lazy_evaluation.py | 6 ++ patterns/dependency_injection.py | 18 ++++++ patterns/other/blackboard.py | 84 +++++++++++++++++++++++++ patterns/other/graph_search.py | 12 ++++ patterns/structural/front_controller.py | 9 +++ patterns/structural/mvc.py | 12 ++++ 7 files changed, 146 insertions(+) diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 7ac7aa28..7e799d7b 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -50,6 +50,11 @@ def __init__(self, method): self.method = method def __get__(self, obj, T): + """ + A decorator that makes a function transactional. + + :param method: The function to be decorated. + """ def transaction(*args, **kwargs): state = memento(obj) try: diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py index ea087e59..70229d2b 100644 --- a/patterns/creational/lazy_evaluation.py +++ b/patterns/creational/lazy_evaluation.py @@ -36,6 +36,12 @@ def __get__(self, obj, type_): def lazy_property2(fn): + """ + A lazy property decorator. + + The function decorated is called the first time to retrieve the result and then that calculated result is used the next + time you access the value. + """ attr = "_lazy__" + fn.__name__ @property diff --git a/patterns/dependency_injection.py b/patterns/dependency_injection.py index 2979f763..3840a97d 100644 --- a/patterns/dependency_injection.py +++ b/patterns/dependency_injection.py @@ -17,6 +17,15 @@ def __init__(self): self.time_provider = datetime.datetime.now def get_current_time_as_html_fragment(self): + """ + Returns the current time as a string in 12-hour clock format with seconds and am/pm. + """ + """ + Returns the current time as a string in 12-hour clock format with seconds and am/pm. + """ + """ + Returns the current time as a string in 12-hour clock format with seconds and am/pm. + """ current_time = self.time_provider() current_time_as_html_fragment = "{}".format(current_time) return current_time_as_html_fragment @@ -61,6 +70,15 @@ def set_time_provider(self, time_provider: Callable): self.time_provider = time_provider def get_current_time_as_html_fragment(self): + """ + Returns the current time as a string in 12-hour clock format with seconds and am/pm. + """ + """ + Returns the current time as a string in 12-hour clock format with seconds and am/pm. + """ + """ + Returns the current time as a string in 12-hour clock format with seconds and am/pm. + """ current_time = self.time_provider() current_time_as_html_fragment = '{}'.format( current_time diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 999da064..814f2634 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -32,6 +32,10 @@ def __init__(self, blackboard): self.blackboard = blackboard def run_loop(self): + """ + This function is a loop that runs until the progress reaches 100. + It checks if an expert is eager to contribute and then calls its contribute method. + """ while self.blackboard.common_state["progress"] < 100: for expert in self.blackboard.experts: if expert.is_eager_to_contribute: @@ -50,6 +54,26 @@ def is_eager_to_contribute(self): @abc.abstractmethod def contribute(self): + """ + This function is responsible for contributing to the common state of the project. + It adds a random number between 1 and 2 to problems, between 10 and + 20 suggestions, + and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. + """ + """ + :param self: + :returns None: + """ + """ + This function is responsible for contributing to the project. + It adds a random number of problems and suggestions, as well as adding its name to the + list of contributions. + """ + """ + This function adds a random number of problems and suggestions to the common state, + and also adds its name to the list of contributions. It also + increments progress by a random number between 10 and 100. + """ raise NotImplementedError("Must provide implementation in subclass.") @@ -59,6 +83,26 @@ def is_eager_to_contribute(self): return True def contribute(self): + """ + This function is responsible for contributing to the common state of the project. + It adds a random number between 1 and 2 to problems, between 10 and + 20 suggestions, + and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. + """ + """ + :param self: + :returns None: + """ + """ + This function is responsible for contributing to the project. + It adds a random number of problems and suggestions, as well as adding its name to the + list of contributions. + """ + """ + This function adds a random number of problems and suggestions to the common state, + and also adds its name to the list of contributions. It also + increments progress by a random number between 10 and 100. + """ self.blackboard.common_state["problems"] += random.randint(1, 10) self.blackboard.common_state["suggestions"] += random.randint(1, 10) self.blackboard.common_state["contributions"] += [self.__class__.__name__] @@ -71,6 +115,26 @@ def is_eager_to_contribute(self): return random.randint(0, 1) def contribute(self): + """ + This function is responsible for contributing to the common state of the project. + It adds a random number between 1 and 2 to problems, between 10 and + 20 suggestions, + and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. + """ + """ + :param self: + :returns None: + """ + """ + This function is responsible for contributing to the project. + It adds a random number of problems and suggestions, as well as adding its name to the + list of contributions. + """ + """ + This function adds a random number of problems and suggestions to the common state, + and also adds its name to the list of contributions. It also + increments progress by a random number between 10 and 100. + """ self.blackboard.common_state["problems"] += random.randint(10, 20) self.blackboard.common_state["suggestions"] += random.randint(10, 20) self.blackboard.common_state["contributions"] += [self.__class__.__name__] @@ -83,6 +147,26 @@ def is_eager_to_contribute(self): return True if self.blackboard.common_state["problems"] > 100 else False def contribute(self): + """ + This function is responsible for contributing to the common state of the project. + It adds a random number between 1 and 2 to problems, between 10 and + 20 suggestions, + and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. + """ + """ + :param self: + :returns None: + """ + """ + This function is responsible for contributing to the project. + It adds a random number of problems and suggestions, as well as adding its name to the + list of contributions. + """ + """ + This function adds a random number of problems and suggestions to the common state, + and also adds its name to the list of contributions. It also + increments progress by a random number between 10 and 100. + """ self.blackboard.common_state["problems"] += random.randint(1, 2) self.blackboard.common_state["suggestions"] += random.randint(10, 20) self.blackboard.common_state["contributions"] += [self.__class__.__name__] diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index 9b04925f..6a79d7cc 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -49,6 +49,18 @@ def find_shortest_path_dfs(self, start, end, path=None): return shortest def find_shortest_path_bfs(self, start, end): + """ + Finds the shortest path between two nodes in a graph using breadth-first search. + + :param start: The node to start from. + :type start: str or int + :param end: The node to find the shortest path to. + :type end: str or int + + :returns queue_path_to_end, dist_to[end]: A list of nodes + representing the shortest path from `start` to `end`, and a dictionary mapping each node in the graph (except for `start`) with its distance from it + (in terms of hops). If no such path exists, returns an empty list and an empty dictionary instead. + """ queue = [start] dist_to = {start: 0} edge_to = {} diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index d93f74d6..4852208d 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -22,6 +22,12 @@ def __init__(self): self.tablet_view = TabletView() def dispatch(self, request): + """ + This function is used to dispatch the request based on the type of device. + If it is a mobile, then mobile view will be called and if it is a tablet, + then tablet view will be called. + Otherwise, an error message will be printed saying that cannot dispatch the request. + """ if request.type == Request.mobile_type: self.mobile_view.show_index_page() elif request.type == Request.tablet_type: @@ -37,6 +43,9 @@ def __init__(self): self.dispatcher = Dispatcher() def dispatch_request(self, request): + """ + This function takes a request object and sends it to the dispatcher. + """ if isinstance(request, Request): self.dispatcher.dispatch(request) else: diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index df6611ce..d82d3e35 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -99,6 +99,18 @@ def show_items(self): self.view.show_item_list(item_type, items) def show_item_information(self, item_name): + """ + Show information about a {item_type} item. + :param str item_name: the name of the {item_type} item to show information about + """ + """ + Show information about a {item_type} item. + :param str item_name: the name of the {item_type} item to show information about + """ + """ + Prints the information of a given item. + :param str item_name: name of the item to be found + """ try: item_info = self.model.get(item_name) except Exception: From a5282337817348c24b111b6e60b30a1a4ec25929 Mon Sep 17 00:00:00 2001 From: Fluency <93673101+fluencydoc@users.noreply.github.com> Date: Thu, 23 Dec 2021 00:01:18 -0400 Subject: [PATCH 066/127] Fixed corrupted and unnecessary example docstrings --- patterns/dependency_injection.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/patterns/dependency_injection.py b/patterns/dependency_injection.py index 3840a97d..2979f763 100644 --- a/patterns/dependency_injection.py +++ b/patterns/dependency_injection.py @@ -17,15 +17,6 @@ def __init__(self): self.time_provider = datetime.datetime.now def get_current_time_as_html_fragment(self): - """ - Returns the current time as a string in 12-hour clock format with seconds and am/pm. - """ - """ - Returns the current time as a string in 12-hour clock format with seconds and am/pm. - """ - """ - Returns the current time as a string in 12-hour clock format with seconds and am/pm. - """ current_time = self.time_provider() current_time_as_html_fragment = "{}".format(current_time) return current_time_as_html_fragment @@ -70,15 +61,6 @@ def set_time_provider(self, time_provider: Callable): self.time_provider = time_provider def get_current_time_as_html_fragment(self): - """ - Returns the current time as a string in 12-hour clock format with seconds and am/pm. - """ - """ - Returns the current time as a string in 12-hour clock format with seconds and am/pm. - """ - """ - Returns the current time as a string in 12-hour clock format with seconds and am/pm. - """ current_time = self.time_provider() current_time_as_html_fragment = '{}'.format( current_time From caf059dbe82cc2cc173a6c159ac368e29d6986cf Mon Sep 17 00:00:00 2001 From: Fluency <93673101+fluencydoc@users.noreply.github.com> Date: Thu, 23 Dec 2021 00:07:44 -0400 Subject: [PATCH 067/127] Fixed corrupted docstrings --- patterns/other/blackboard.py | 80 ------------------------------------ 1 file changed, 80 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 814f2634..49f8775f 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -54,26 +54,6 @@ def is_eager_to_contribute(self): @abc.abstractmethod def contribute(self): - """ - This function is responsible for contributing to the common state of the project. - It adds a random number between 1 and 2 to problems, between 10 and - 20 suggestions, - and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. - """ - """ - :param self: - :returns None: - """ - """ - This function is responsible for contributing to the project. - It adds a random number of problems and suggestions, as well as adding its name to the - list of contributions. - """ - """ - This function adds a random number of problems and suggestions to the common state, - and also adds its name to the list of contributions. It also - increments progress by a random number between 10 and 100. - """ raise NotImplementedError("Must provide implementation in subclass.") @@ -83,26 +63,6 @@ def is_eager_to_contribute(self): return True def contribute(self): - """ - This function is responsible for contributing to the common state of the project. - It adds a random number between 1 and 2 to problems, between 10 and - 20 suggestions, - and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. - """ - """ - :param self: - :returns None: - """ - """ - This function is responsible for contributing to the project. - It adds a random number of problems and suggestions, as well as adding its name to the - list of contributions. - """ - """ - This function adds a random number of problems and suggestions to the common state, - and also adds its name to the list of contributions. It also - increments progress by a random number between 10 and 100. - """ self.blackboard.common_state["problems"] += random.randint(1, 10) self.blackboard.common_state["suggestions"] += random.randint(1, 10) self.blackboard.common_state["contributions"] += [self.__class__.__name__] @@ -115,26 +75,6 @@ def is_eager_to_contribute(self): return random.randint(0, 1) def contribute(self): - """ - This function is responsible for contributing to the common state of the project. - It adds a random number between 1 and 2 to problems, between 10 and - 20 suggestions, - and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. - """ - """ - :param self: - :returns None: - """ - """ - This function is responsible for contributing to the project. - It adds a random number of problems and suggestions, as well as adding its name to the - list of contributions. - """ - """ - This function adds a random number of problems and suggestions to the common state, - and also adds its name to the list of contributions. It also - increments progress by a random number between 10 and 100. - """ self.blackboard.common_state["problems"] += random.randint(10, 20) self.blackboard.common_state["suggestions"] += random.randint(10, 20) self.blackboard.common_state["contributions"] += [self.__class__.__name__] @@ -147,26 +87,6 @@ def is_eager_to_contribute(self): return True if self.blackboard.common_state["problems"] > 100 else False def contribute(self): - """ - This function is responsible for contributing to the common state of the project. - It adds a random number between 1 and 2 to problems, between 10 and - 20 suggestions, - and it adds this class name to contributions. It also increments progress by a random number between 10 and 100. - """ - """ - :param self: - :returns None: - """ - """ - This function is responsible for contributing to the project. - It adds a random number of problems and suggestions, as well as adding its name to the - list of contributions. - """ - """ - This function adds a random number of problems and suggestions to the common state, - and also adds its name to the list of contributions. It also - increments progress by a random number between 10 and 100. - """ self.blackboard.common_state["problems"] += random.randint(1, 2) self.blackboard.common_state["suggestions"] += random.randint(10, 20) self.blackboard.common_state["contributions"] += [self.__class__.__name__] From 402a1d30adede83f9cffac534aba13a21121cba7 Mon Sep 17 00:00:00 2001 From: Fluency <93673101+fluencydoc@users.noreply.github.com> Date: Thu, 23 Dec 2021 00:08:53 -0400 Subject: [PATCH 068/127] Fixed more docstring corruption --- patterns/structural/mvc.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index d82d3e35..3f7dc315 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -103,14 +103,6 @@ def show_item_information(self, item_name): Show information about a {item_type} item. :param str item_name: the name of the {item_type} item to show information about """ - """ - Show information about a {item_type} item. - :param str item_name: the name of the {item_type} item to show information about - """ - """ - Prints the information of a given item. - :param str item_name: name of the item to be found - """ try: item_info = self.model.get(item_name) except Exception: From 4e870fd19c1df22be59f7d34cec9bc2ebe1792a3 Mon Sep 17 00:00:00 2001 From: flencydoc Date: Thu, 23 Dec 2021 16:32:59 +0000 Subject: [PATCH 069/127] Fixed linter errors --- patterns/behavioral/memento.py | 1 + 1 file changed, 1 insertion(+) diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 7e799d7b..3ec7e6be 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -55,6 +55,7 @@ def __get__(self, obj, T): :param method: The function to be decorated. """ + def transaction(*args, **kwargs): state = memento(obj) try: From 75d3ec059e5e41a210d79e71c686d968242e15ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ty=C3=A1s=20J=C3=A1nos?= Date: Tue, 8 Feb 2022 00:23:11 +0100 Subject: [PATCH 070/127] Fixed linter errors --- patterns/creational/lazy_evaluation.py | 4 ++-- patterns/other/graph_search.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/patterns/creational/lazy_evaluation.py b/patterns/creational/lazy_evaluation.py index 70229d2b..b56daf0c 100644 --- a/patterns/creational/lazy_evaluation.py +++ b/patterns/creational/lazy_evaluation.py @@ -39,8 +39,8 @@ def lazy_property2(fn): """ A lazy property decorator. - The function decorated is called the first time to retrieve the result and then that calculated result is used the next - time you access the value. + The function decorated is called the first time to retrieve the result and + then that calculated result is used the next time you access the value. """ attr = "_lazy__" + fn.__name__ diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index 6a79d7cc..ad224db3 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -58,8 +58,10 @@ def find_shortest_path_bfs(self, start, end): :type end: str or int :returns queue_path_to_end, dist_to[end]: A list of nodes - representing the shortest path from `start` to `end`, and a dictionary mapping each node in the graph (except for `start`) with its distance from it - (in terms of hops). If no such path exists, returns an empty list and an empty dictionary instead. + representing the shortest path from `start` to `end`, and a dictionary + mapping each node in the graph (except for `start`) with its distance from it + (in terms of hops). If no such path exists, returns an empty list and an empty + dictionary instead. """ queue = [start] dist_to = {start: 0} From fdb8e782400afe636f14424c5cd5ca2175f52f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ty=C3=A1s=20J=C3=A1nos?= Date: Tue, 8 Feb 2022 01:00:39 +0100 Subject: [PATCH 071/127] Use URL shortener to fix archive link --- patterns/creational/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index 905e12ef..4854f98f 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -12,7 +12,7 @@ *Where can the pattern be used practically? The Factory Method can be seen in the popular web framework Django: -http://web.archive.org/web/http://django.wikispaces.asu.edu/*NEW*+Django+Design+Patterns +https://shorturl.at/ctMPZ For example, in a contact form of a web page, the subject and the message fields are created using the same form factory (CharField()), even though they have different implementations according to their From 193ee4c70a815abdd94468255c3a6e4127878890 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Thu, 17 Feb 2022 17:00:07 +0900 Subject: [PATCH 072/127] feat(typing): fix all problems by mypy re #373 --- patterns/behavioral/catalog.py | 9 ++++++--- patterns/behavioral/chain_of_responsibility.py | 9 +++++---- patterns/behavioral/command.py | 6 +++--- patterns/behavioral/memento.py | 3 ++- patterns/behavioral/registry.py | 5 ++++- patterns/behavioral/specification.py | 18 +++++------------- patterns/creational/borg.py | 3 ++- patterns/structural/flyweight.py | 2 +- setup.cfg | 4 ++++ 9 files changed, 32 insertions(+), 27 deletions(-) diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index f979ac32..7c91aa7d 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -77,7 +77,8 @@ def main_method(self) -> None: depending on self.param value """ - self._instance_method_choices[self.param].__get__(self)() + self._instance_method_choices[self.param].__get__(self)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 class CatalogClass: @@ -115,7 +116,8 @@ def main_method(self): depending on self.param value """ - self._class_method_choices[self.param].__get__(None, self.__class__)() + self._class_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 class CatalogStatic: @@ -151,7 +153,8 @@ def main_method(self) -> None: depending on self.param value """ - self._static_method_choices[self.param].__get__(None, self.__class__)() + self._static_method_choices[self.param].__get__(None, self.__class__)() # type: ignore + # type ignore reason: https://github.com/python/mypy/issues/10206 def main(): diff --git a/patterns/behavioral/chain_of_responsibility.py b/patterns/behavioral/chain_of_responsibility.py index d80b1633..9d46c4a8 100644 --- a/patterns/behavioral/chain_of_responsibility.py +++ b/patterns/behavioral/chain_of_responsibility.py @@ -19,13 +19,11 @@ """ from abc import ABC, abstractmethod -from typing import Optional, Tuple, TypeVar - -T = TypeVar("T") +from typing import Optional, Tuple class Handler(ABC): - def __init__(self, successor: Optional[T] = None): + def __init__(self, successor: Optional["Handler"] = None): self.successor = successor def handle(self, request: int) -> None: @@ -55,6 +53,7 @@ def check_range(request: int) -> Optional[bool]: if 0 <= request < 10: print(f"request {request} handled in handler 0") return True + return None class ConcreteHandler1(Handler): @@ -66,6 +65,7 @@ def check_range(self, request: int) -> Optional[bool]: if self.start <= request < self.end: print(f"request {request} handled in handler 1") return True + return None class ConcreteHandler2(Handler): @@ -76,6 +76,7 @@ def check_range(self, request: int) -> Optional[bool]: if start <= request < end: print(f"request {request} handled in handler 2") return True + return None @staticmethod def get_interval_from_db() -> Tuple[int, int]: diff --git a/patterns/behavioral/command.py b/patterns/behavioral/command.py index b21d7f73..a88ea8be 100644 --- a/patterns/behavioral/command.py +++ b/patterns/behavioral/command.py @@ -20,7 +20,7 @@ https://docs.djangoproject.com/en/2.1/ref/request-response/#httprequest-objects """ -from typing import Union +from typing import List, Union class HideFileCommand: @@ -30,7 +30,7 @@ class HideFileCommand: def __init__(self) -> None: # an array of files hidden, to undo them as needed - self._hidden_files = [] + self._hidden_files: List[str] = [] def execute(self, filename: str) -> None: print(f"hiding {filename}") @@ -48,7 +48,7 @@ class DeleteFileCommand: def __init__(self) -> None: # an array of deleted files, to undo them as needed - self._deleted_files = [] + self._deleted_files: List[str] = [] def execute(self, filename: str) -> None: print(f"deleting {filename}") diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index 3ec7e6be..e1d42fc2 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -5,6 +5,7 @@ Provides the ability to restore an object to its previous state. """ +from typing import Callable, List from copy import copy, deepcopy @@ -25,7 +26,7 @@ class Transaction: """ deep = False - states = [] + states: List[Callable[[], None]] = [] def __init__(self, deep, *targets): self.deep = deep diff --git a/patterns/behavioral/registry.py b/patterns/behavioral/registry.py index a9fca443..d44a992e 100644 --- a/patterns/behavioral/registry.py +++ b/patterns/behavioral/registry.py @@ -1,6 +1,9 @@ +from typing import Dict + + class RegistryHolder(type): - REGISTRY = {} + REGISTRY: Dict[str, "RegistryHolder"] = {} def __new__(cls, name, bases, attrs): new_cls = type.__new__(cls, name, bases, attrs) diff --git a/patterns/behavioral/specification.py b/patterns/behavioral/specification.py index 07db267e..303ee513 100644 --- a/patterns/behavioral/specification.py +++ b/patterns/behavioral/specification.py @@ -39,12 +39,9 @@ def not_specification(self): class AndSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() - def __init__(self, one, other): - self._one = one - self._other = other + self._one: Specification = one + self._other: Specification = other def is_satisfied_by(self, candidate): return bool( @@ -54,12 +51,9 @@ def is_satisfied_by(self, candidate): class OrSpecification(CompositeSpecification): - _one = Specification() - _other = Specification() - def __init__(self, one, other): - self._one = one - self._other = other + self._one: Specification = one + self._other: Specification = other def is_satisfied_by(self, candidate): return bool( @@ -69,10 +63,8 @@ def is_satisfied_by(self, candidate): class NotSpecification(CompositeSpecification): - _wrapped = Specification() - def __init__(self, wrapped): - self._wrapped = wrapped + self._wrapped: Specification = wrapped def is_satisfied_by(self, candidate): return bool(not self._wrapped.is_satisfied_by(candidate)) diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index e3f04b66..3ddc8c1d 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -32,10 +32,11 @@ *TL;DR Provides singleton-like behavior sharing state between instances. """ +from typing import Dict class Borg: - _shared_state = {} + _shared_state: Dict[str, str] = {} def __init__(self): self.__dict__ = self._shared_state diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight.py index 29015705..fad17a8b 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight.py @@ -34,7 +34,7 @@ class Card: # Could be a simple dict. # With WeakValueDictionary garbage collection can reclaim the object # when there are no other references to it. - _pool = weakref.WeakValueDictionary() + _pool: weakref.WeakValueDictionary = weakref.WeakValueDictionary() def __new__(cls, value, suit): # If the object exists in the pool - just return it diff --git a/setup.cfg b/setup.cfg index 8f2de0ff..eb556c0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,7 @@ exclude = venv* filterwarnings = ; ignore TestRunner class from facade example ignore:.*test class 'TestRunner'.*:Warning + +[mypy] +python_version = 3.8 +ignore_missing_imports = True From 0a15bbe459d198d37bba648d8752ad75f1ed0c5d Mon Sep 17 00:00:00 2001 From: Sakis Kasampalis Date: Tue, 31 May 2022 19:30:32 +0200 Subject: [PATCH 073/127] Replaced spammy link with a direct one --- patterns/creational/factory.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index 4854f98f..8dba488d 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -12,11 +12,8 @@ *Where can the pattern be used practically? The Factory Method can be seen in the popular web framework Django: -https://shorturl.at/ctMPZ -For example, in a contact form of a web page, the subject and the message -fields are created using the same form factory (CharField()), even -though they have different implementations according to their -purposes. +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/ From 5c5b9717288b934baf3d7563d89c7d371fa1dd78 Mon Sep 17 00:00:00 2001 From: Sakis Kasampalis Date: Thu, 2 Jun 2022 23:15:13 +0200 Subject: [PATCH 074/127] Close #258 All files have doctests, so remove script and output section --- README.md | 6 ------ append_output.sh | 19 ------------------- 2 files changed, 25 deletions(-) delete mode 100755 append_output.sh diff --git a/README.md b/README.md index 49ad4d4a..3067cc88 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,6 @@ Contributing ------------ When an implementation is added or modified, please review the following guidelines: -##### Output -All files with example patterns have `### OUTPUT ###` section at the bottom -(migration to OUTPUT = """...""" is in progress). - -Run `append_output.sh` (e.g. `./append_output.sh borg.py`) to generate/update it. - ##### Docstrings Add module level description in form of a docstring with links to corresponding references or other useful information. diff --git a/append_output.sh b/append_output.sh deleted file mode 100755 index 3bb9202c..00000000 --- a/append_output.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# This script (given path to a python script as an argument) -# appends python outputs to given file. - -set -e - -output_marker='OUTPUT = """' - -# get everything (excluding part between `output_marker` and the end of the file) -# into `src` var -src=$(sed -n -e "/$output_marker/,\$!p" "$1") -output=$(python3 "$1") - -echo "$src" > $1 -echo -e "\n" >> $1 -echo "$output_marker" >> $1 -echo "$output" >> $1 -echo '""" # noqa' >> $1 From 93b5937d5b4fbc0248eb5bcf7b7a5f221a294a86 Mon Sep 17 00:00:00 2001 From: Sakis Kasampalis Date: Thu, 2 Jun 2022 23:20:04 +0200 Subject: [PATCH 075/127] Cleanup README Remove remaining OUTPUT mentioning part --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 3067cc88..74b689da 100644 --- a/README.md +++ b/README.md @@ -98,17 +98,12 @@ Add "Examples in Python ecosystem" section if you know some. It shows how patter [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. -In some cases class-level docstring with doctest would also help (see [adapter.py](patterns/structural/adapter.py)) -but readable OUTPUT section is much better. - - ##### 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 `tox` or `tox -e ci37` before submitting a patch to be sure your changes will pass CI. From 778e3c5a663e0a2ab663b577350595ebe58f2f11 Mon Sep 17 00:00:00 2001 From: Abhinav Mathur Date: Sun, 26 Jun 2022 10:45:20 +0530 Subject: [PATCH 076/127] Update .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8b2c28d8..aaf2ded3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,10 @@ __pycache__ .idea *.egg-info/ .tox/ -venv +env/ +venv/ +.env +.venv .vscode/ .python-version .coverage From 6f6cfbe39c221503c302afca15b54a6770e2634d Mon Sep 17 00:00:00 2001 From: Abhinav Mathur Date: Sun, 26 Jun 2022 11:01:21 +0530 Subject: [PATCH 077/127] fix: created PetShop instance --- patterns/creational/abstract_factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 3c221a36..d092a6b4 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -116,6 +116,7 @@ def main() -> None: if __name__ == "__main__": random.seed(1234) # for deterministic doctest outputs + shop = PetShop(random_animal) import doctest doctest.testmod() From aa357eeef11c807bd6881f708eafafe2f9829be6 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sat, 2 Jul 2022 19:50:02 +1000 Subject: [PATCH 078/127] docs: fix simple typo, assigining -> assigning There is a small typo in patterns/creational/borg.py. Should read `assigning` rather than `assigining`. --- patterns/creational/borg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index 3ddc8c1d..ab364f61 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -13,7 +13,7 @@ its own dictionary, but the Borg pattern modifies this so that all instances have the same dictionary. In this example, the __shared_state attribute will be the dictionary -shared between all instances, and this is ensured by assigining +shared between all instances, and this is ensured by assigning __shared_state to the __dict__ variable when initializing a new instance (i.e., in the __init__ method). Other attributes are usually added to the instance's attribute dictionary, but, since the attribute From 0f5d2cae4e5bc69a14cc6345235fb76980e3fc64 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 19 Jul 2022 01:10:20 +0200 Subject: [PATCH 079/127] Upgrade GitHub Actions --- .github/workflows/lint_python.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 63796567..2fd12494 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -4,8 +4,10 @@ jobs: lint_python: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x - run: pip install --upgrade pip - run: pip install black codespell flake8 isort mypy pytest pyupgrade tox - run: black --check . @@ -17,4 +19,4 @@ jobs: - run: mypy --ignore-missing-imports . || true - run: pytest . - run: pytest --doctest-modules . || true - - run: shopt -s globstar && pyupgrade --py36-plus **/*.py + - run: shopt -s globstar && pyupgrade --py37-plus **/*.py From 129cb3cba7502fd76514ba98a2a2b70517cc5e9a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 19 Jul 2022 01:13:46 +0200 Subject: [PATCH 080/127] tox.ini: Add py310 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 168e2c9d..1eca32ab 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,py39,cov-report +envlist = py38,py39,py310,cov-report skip_missing_interpreters = true From 3a02da23c60eedc5a21b84d3c577443a0233ba23 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 19 Jul 2022 01:20:19 +0200 Subject: [PATCH 081/127] Add Python 3.10 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index b4d2cdf1..ec2528f4 100644 --- a/setup.py +++ b/setup.py @@ -12,5 +12,6 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], ) From 7c140ebb96b641d5ee9f9360398a1ef4dccd21b5 Mon Sep 17 00:00:00 2001 From: Abe Date: Tue, 19 Jul 2022 14:54:52 -0400 Subject: [PATCH 082/127] add type hints --- patterns/behavioral/iterator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/behavioral/iterator.py b/patterns/behavioral/iterator.py index 40162461..3ed4043b 100644 --- a/patterns/behavioral/iterator.py +++ b/patterns/behavioral/iterator.py @@ -7,7 +7,7 @@ """ -def count_to(count): +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] From ca8706a0142ca8a0f51df783716869bb224aebd9 Mon Sep 17 00:00:00 2001 From: Abe Date: Tue, 19 Jul 2022 15:02:45 -0400 Subject: [PATCH 083/127] add type hints --- patterns/behavioral/template.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/patterns/behavioral/template.py b/patterns/behavioral/template.py index d2d83174..76fc136b 100644 --- a/patterns/behavioral/template.py +++ b/patterns/behavioral/template.py @@ -10,28 +10,28 @@ """ -def get_text(): +def get_text() -> str: return "plain-text" -def get_pdf(): +def get_pdf() -> str: return "pdf" -def get_csv(): +def get_csv() -> str: return "csv" -def convert_to_text(data): +def convert_to_text(data: str) -> str: print("[CONVERT]") return f"{data} as text" -def saver(): +def saver() -> None: print("[SAVE]") -def template_function(getter, converter=False, to_save=False): +def template_function(getter, converter=False, to_save=False) -> None: data = getter() print(f"Got `{data}`") From 9d4170dc65f1df5926be2b3cef9f4251044d3511 Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Tue, 26 Jul 2022 20:23:25 -0700 Subject: [PATCH 084/127] issue 373: add more type hints --- patterns/behavioral/publish_subscribe.py | 25 +++++++++++--------- patterns/behavioral/state.py | 20 ++++++++-------- patterns/creational/borg.py | 6 ++--- patterns/creational/builder.py | 20 ++++++++-------- patterns/fundamental/delegation_pattern.py | 4 ++-- patterns/other/blackboard.py | 27 ++++++++++++---------- patterns/structural/decorator.py | 12 +++++----- patterns/structural/facade.py | 10 ++++---- patterns/structural/front_controller.py | 16 ++++++++----- 9 files changed, 75 insertions(+), 65 deletions(-) diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index 760d8e7b..40aefd2e 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -5,21 +5,24 @@ """ +from __future__ import annotations + + class Provider: - def __init__(self): + def __init__(self) -> None: self.msg_queue = [] self.subscribers = {} - def notify(self, msg): + def notify(self, msg: str) -> None: self.msg_queue.append(msg) - def subscribe(self, msg, subscriber): + def subscribe(self, msg: str, subscriber: Subscriber) -> None: self.subscribers.setdefault(msg, []).append(subscriber) - def unsubscribe(self, msg, subscriber): + def unsubscribe(self, msg: str, subscriber: Subscriber) -> None: self.subscribers[msg].remove(subscriber) - def update(self): + def update(self) -> None: for msg in self.msg_queue: for sub in self.subscribers.get(msg, []): sub.run(msg) @@ -27,25 +30,25 @@ def update(self): class Publisher: - def __init__(self, msg_center): + def __init__(self, msg_center: Provider) -> None: self.provider = msg_center - def publish(self, msg): + def publish(self, msg: str) -> None: self.provider.notify(msg) class Subscriber: - def __init__(self, name, msg_center): + def __init__(self, name: str, msg_center: Provider) -> None: self.name = name self.provider = msg_center - def subscribe(self, msg): + def subscribe(self, msg: str) -> None: self.provider.subscribe(msg, self) - def unsubscribe(self, msg): + def unsubscribe(self, msg: str) -> None: self.provider.unsubscribe(msg, self) - def run(self, msg): + def run(self, msg: str) -> None: print(f"{self.name} got {msg}") diff --git a/patterns/behavioral/state.py b/patterns/behavioral/state.py index 423b749e..db4d9468 100644 --- a/patterns/behavioral/state.py +++ b/patterns/behavioral/state.py @@ -8,12 +8,13 @@ Implements state transitions by invoking methods from the pattern's superclass. """ +from __future__ import annotations -class State: +class State: """Base state. This is to share functionality""" - def scan(self): + def scan(self) -> None: """Scan the dial to the next station""" self.pos += 1 if self.pos == len(self.stations): @@ -22,43 +23,42 @@ def scan(self): class AmState(State): - def __init__(self, radio): + def __init__(self, radio: Radio) -> None: self.radio = radio self.stations = ["1250", "1380", "1510"] self.pos = 0 self.name = "AM" - def toggle_amfm(self): + def toggle_amfm(self) -> None: print("Switching to FM") self.radio.state = self.radio.fmstate class FmState(State): - def __init__(self, radio): + 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): + def toggle_amfm(self) -> None: print("Switching to AM") self.radio.state = self.radio.amstate class Radio: - """A radio. It has a scan button, and an AM/FM toggle switch.""" - def __init__(self): + 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 - def toggle_amfm(self): + def toggle_amfm(self) -> None: self.state.toggle_amfm() - def scan(self): + def scan(self) -> None: self.state.scan() diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index ab364f61..de36a23f 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -38,12 +38,12 @@ class Borg: _shared_state: Dict[str, str] = {} - def __init__(self): + def __init__(self) -> None: self.__dict__ = self._shared_state class YourBorg(Borg): - def __init__(self, state=None): + def __init__(self, state: str = None) -> None: super().__init__() if state: self.state = state @@ -52,7 +52,7 @@ def __init__(self, state=None): if not hasattr(self, "state"): self.state = "Init" - def __str__(self): + def __str__(self) -> str: return self.state diff --git a/patterns/creational/builder.py b/patterns/creational/builder.py index b1f463ee..22383923 100644 --- a/patterns/creational/builder.py +++ b/patterns/creational/builder.py @@ -34,7 +34,7 @@ class for a building, where the initializer (__init__ method) specifies the # Abstract Building class Building: - def __init__(self): + def __init__(self) -> None: self.build_floor() self.build_size() @@ -44,24 +44,24 @@ def build_floor(self): def build_size(self): raise NotImplementedError - def __repr__(self): + def __repr__(self) -> str: return "Floor: {0.floor} | Size: {0.size}".format(self) # Concrete Buildings class House(Building): - def build_floor(self): + def build_floor(self) -> None: self.floor = "One" - def build_size(self): + def build_size(self) -> None: self.size = "Big" class Flat(Building): - def build_floor(self): + def build_floor(self) -> None: self.floor = "More than One" - def build_size(self): + def build_size(self) -> None: self.size = "Small" @@ -72,19 +72,19 @@ def build_size(self): class ComplexBuilding: - def __repr__(self): + def __repr__(self) -> str: return "Floor: {0.floor} | Size: {0.size}".format(self) class ComplexHouse(ComplexBuilding): - def build_floor(self): + def build_floor(self) -> None: self.floor = "One" - def build_size(self): + def build_size(self) -> None: self.size = "Big and fancy" -def construct_building(cls): +def construct_building(cls) -> Building: building = cls() building.build_floor() building.build_size() diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py index bdcefc9d..34e1071f 100644 --- a/patterns/fundamental/delegation_pattern.py +++ b/patterns/fundamental/delegation_pattern.py @@ -28,7 +28,7 @@ class Delegator: AttributeError: 'Delegate' object has no attribute 'do_anything' """ - def __init__(self, delegate: Delegate): + def __init__(self, delegate: Delegate) -> None: self.delegate = delegate def __getattr__(self, name: str) -> Any | Callable: @@ -44,7 +44,7 @@ def wrapper(*args, **kwargs): class Delegate: - def __init__(self): + def __init__(self) -> None: self.p1 = 123 def do_something(self, something: str) -> str: diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 49f8775f..7966db34 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -8,14 +8,17 @@ https://en.wikipedia.org/wiki/Blackboard_system """ +from __future__ import annotations import abc import random +from typing import List + class Blackboard: - def __init__(self): - self.experts = [] + def __init__(self) -> None: + self.experts: List = [] self.common_state = { "problems": 0, "suggestions": 0, @@ -23,15 +26,15 @@ def __init__(self): "progress": 0, # percentage, if 100 -> task is finished } - def add_expert(self, expert): + def add_expert(self, expert: AbstractExpert) -> None: self.experts.append(expert) class Controller: - def __init__(self, blackboard): + def __init__(self, blackboard: Blackboard) -> None: self.blackboard = blackboard - def run_loop(self): + def run_loop(self) -> List[str]: """ This function is a loop that runs until the progress reaches 100. It checks if an expert is eager to contribute and then calls its contribute method. @@ -44,7 +47,7 @@ def run_loop(self): class AbstractExpert(metaclass=abc.ABCMeta): - def __init__(self, blackboard): + def __init__(self, blackboard: Blackboard) -> None: self.blackboard = blackboard @property @@ -59,10 +62,10 @@ def contribute(self): class Student(AbstractExpert): @property - def is_eager_to_contribute(self): + def is_eager_to_contribute(self) -> bool: return True - def contribute(self): + def contribute(self) -> None: self.blackboard.common_state["problems"] += random.randint(1, 10) self.blackboard.common_state["suggestions"] += random.randint(1, 10) self.blackboard.common_state["contributions"] += [self.__class__.__name__] @@ -71,10 +74,10 @@ def contribute(self): class Scientist(AbstractExpert): @property - def is_eager_to_contribute(self): + def is_eager_to_contribute(self) -> int: return random.randint(0, 1) - def contribute(self): + def contribute(self) -> None: self.blackboard.common_state["problems"] += random.randint(10, 20) self.blackboard.common_state["suggestions"] += random.randint(10, 20) self.blackboard.common_state["contributions"] += [self.__class__.__name__] @@ -83,10 +86,10 @@ def contribute(self): class Professor(AbstractExpert): @property - def is_eager_to_contribute(self): + def is_eager_to_contribute(self) -> bool: return True if self.blackboard.common_state["problems"] > 100 else False - def contribute(self): + def contribute(self) -> None: self.blackboard.common_state["problems"] += random.randint(1, 2) self.blackboard.common_state["suggestions"] += random.randint(10, 20) self.blackboard.common_state["contributions"] += [self.__class__.__name__] diff --git a/patterns/structural/decorator.py b/patterns/structural/decorator.py index 01c91b00..a32e2b06 100644 --- a/patterns/structural/decorator.py +++ b/patterns/structural/decorator.py @@ -28,30 +28,30 @@ class TextTag: """Represents a base text tag""" - def __init__(self, text): + def __init__(self, text: str) -> None: self._text = text - def render(self): + def render(self) -> str: return self._text class BoldWrapper(TextTag): """Wraps a tag in """ - def __init__(self, wrapped): + def __init__(self, wrapped: TextTag) -> None: self._wrapped = wrapped - def render(self): + def render(self) -> str: return f"{self._wrapped.render()}" class ItalicWrapper(TextTag): """Wraps a tag in """ - def __init__(self, wrapped): + def __init__(self, wrapped: TextTag) -> None: self._wrapped = wrapped - def render(self): + def render(self) -> str: return f"{self._wrapped.render()}" diff --git a/patterns/structural/facade.py b/patterns/structural/facade.py index 6561c6dc..f7b00be3 100644 --- a/patterns/structural/facade.py +++ b/patterns/structural/facade.py @@ -35,13 +35,13 @@ class CPU: Simple CPU representation. """ - def freeze(self): + def freeze(self) -> None: print("Freezing processor.") - def jump(self, position): + def jump(self, position: str) -> None: print("Jumping to:", position) - def execute(self): + def execute(self) -> None: print("Executing.") @@ -50,7 +50,7 @@ class Memory: Simple memory representation. """ - def load(self, position, data): + def load(self, position: str, data: str) -> None: print(f"Loading from {position} data: '{data}'.") @@ -59,7 +59,7 @@ class SolidStateDrive: Simple solid state drive representation. """ - def read(self, lba, size): + def read(self, lba: str, size: str) -> str: return f"Some data from sector {lba} with size {size}" diff --git a/patterns/structural/front_controller.py b/patterns/structural/front_controller.py index 4852208d..92f58b21 100644 --- a/patterns/structural/front_controller.py +++ b/patterns/structural/front_controller.py @@ -5,23 +5,27 @@ Provides a centralized entry point that controls and manages request handling. """ +from __future__ import annotations + +from typing import Any + class MobileView: - def show_index_page(self): + def show_index_page(self) -> None: print("Displaying mobile index page") class TabletView: - def show_index_page(self): + def show_index_page(self) -> None: print("Displaying tablet index page") class Dispatcher: - def __init__(self): + def __init__(self) -> None: self.mobile_view = MobileView() self.tablet_view = TabletView() - def dispatch(self, request): + def dispatch(self, request: Request) -> None: """ This function is used to dispatch the request based on the type of device. If it is a mobile, then mobile view will be called and if it is a tablet, @@ -39,10 +43,10 @@ def dispatch(self, request): class RequestController: """front controller""" - def __init__(self): + def __init__(self) -> None: self.dispatcher = Dispatcher() - def dispatch_request(self, request): + def dispatch_request(self, request: Any) -> None: """ This function takes a request object and sends it to the dispatcher. """ From f89c748957a7e3521c7bf5ab050b49cc50f2e29a Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Sun, 7 Aug 2022 14:16:46 -0700 Subject: [PATCH 085/127] fix blackboard linter issue --- patterns/other/blackboard.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 7966db34..b6b56d81 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -13,12 +13,10 @@ import abc import random -from typing import List - class Blackboard: def __init__(self) -> None: - self.experts: List = [] + self.experts: [] self.common_state = { "problems": 0, "suggestions": 0, @@ -34,7 +32,7 @@ class Controller: def __init__(self, blackboard: Blackboard) -> None: self.blackboard = blackboard - def run_loop(self) -> List[str]: + def run_loop(self): """ This function is a loop that runs until the progress reaches 100. It checks if an expert is eager to contribute and then calls its contribute method. From 36f4fac10b2aa2428a544ac226a105a338c64420 Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Sun, 7 Aug 2022 14:34:48 -0700 Subject: [PATCH 086/127] fix experts --- patterns/other/blackboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index b6b56d81..ef48f501 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -16,7 +16,7 @@ class Blackboard: def __init__(self) -> None: - self.experts: [] + self.experts = [] self.common_state = { "problems": 0, "suggestions": 0, From ad6fd4b8e3aeb3fd0fbeccff09e870c3e899fd36 Mon Sep 17 00:00:00 2001 From: Mateus Furquim Date: Fri, 20 Jan 2023 13:24:38 -0300 Subject: [PATCH 087/127] Add Protocol to factory pattern --- patterns/creational/factory.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index 8dba488d..a1beffa8 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -21,6 +21,14 @@ *TL;DR Creates objects without having to specify the exact class. """ +from typing import Dict +from typing import Protocol +from typing import Type + + +class Localizer(Protocol): + def localize(self, msg: str) -> str: + pass class GreekLocalizer: @@ -41,10 +49,10 @@ def localize(self, msg: str) -> str: return msg -def get_localizer(language: str = "English") -> object: +def get_localizer(language: str = "English") -> Localizer: """Factory""" - localizers = { + localizers: Dict[str, Type[Localizer]] = { "English": EnglishLocalizer, "Greek": GreekLocalizer, } From 4e62fd0fd633f58c926730e98e9b7fd9a600a5d8 Mon Sep 17 00:00:00 2001 From: asaffifee <123254078+asaffifee@users.noreply.github.com> Date: Sat, 21 Jan 2023 22:40:16 +0200 Subject: [PATCH 088/127] Update chaining_method.py It makes more sense to pass the `move` object to the `do_action()` method instead of having to initialize a `Person` instance with an `Action` object. --- patterns/behavioral/chaining_method.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/patterns/behavioral/chaining_method.py b/patterns/behavioral/chaining_method.py index 195bfa58..26f11018 100644 --- a/patterns/behavioral/chaining_method.py +++ b/patterns/behavioral/chaining_method.py @@ -2,13 +2,12 @@ class Person: - def __init__(self, name: str, action: Action) -> None: + def __init__(self, name: str) -> None: self.name = name - self.action = action - def do_action(self) -> Action: - print(self.name, self.action.name, end=" ") - return self.action + def do_action(self, action: Action) -> Action: + print(self.name, action.name, end=" ") + return action class Action: @@ -26,8 +25,8 @@ def stop(self) -> None: def main(): """ >>> move = Action('move') - >>> person = Person('Jack', move) - >>> person.do_action().amount('5m').stop() + >>> person = Person('Jack') + >>> person.do_action(move).amount('5m').stop() Jack move 5m then stop """ From 76a5d217e078dc8ba423396fc73592eae253349d Mon Sep 17 00:00:00 2001 From: "yang.lei@cerence.com" Date: Wed, 24 May 2023 08:51:00 +0800 Subject: [PATCH 089/127] simplify patterns/behavioral/memento, changing Transactional from descriptor class to decorator method --- patterns/behavioral/memento.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index e1d42fc2..d4d13325 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -41,32 +41,20 @@ def rollback(self): a_state() -class Transactional: +def Transactional(method): """Adds transactional semantics to methods. Methods decorated with + @Transactional will roll back to entry-state upon exceptions. - @Transactional will rollback to entry-state upon exceptions. + :param method: The function to be decorated. """ - - def __init__(self, method): - self.method = method - - def __get__(self, obj, T): - """ - 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 - + def transaction(obj, *args, **kwargs): + state = memento(obj) + try: + return method(obj, *args, **kwargs) + except Exception as e: + state() + raise e + return transaction class NumObj: def __init__(self, value): From 53ba9bcb9fd8db2d2a8be7960e5aa880a3447c46 Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Wed, 8 May 2024 07:43:19 +0000 Subject: [PATCH 090/127] add build and dist folders to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index aaf2ded3..d272a2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ venv/ .vscode/ .python-version .coverage +build/ +dist/ \ No newline at end of file From 11113ea3cd9654350e34f50b6866ac6a5fd666d3 Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Wed, 8 May 2024 07:44:53 +0000 Subject: [PATCH 091/127] linter updates - changes to comply with black --- patterns/behavioral/catalog.py | 3 --- patterns/behavioral/iterator_alt.py | 1 + patterns/behavioral/memento.py | 2 +- patterns/behavioral/publish_subscribe.py | 1 - patterns/behavioral/strategy.py | 1 - patterns/creational/abstract_factory.py | 2 +- patterns/creational/borg.py | 1 + patterns/creational/factory.py | 6 ++---- patterns/creational/prototype.py | 1 + patterns/other/blackboard.py | 1 + patterns/other/graph_search.py | 1 - tests/behavioral/test_strategy.py | 2 +- tests/creational/test_pool.py | 1 - 13 files changed, 9 insertions(+), 14 deletions(-) diff --git a/patterns/behavioral/catalog.py b/patterns/behavioral/catalog.py index 7c91aa7d..ba85f500 100644 --- a/patterns/behavioral/catalog.py +++ b/patterns/behavioral/catalog.py @@ -46,7 +46,6 @@ def main_method(self) -> None: # Alternative implementation for different levels of methods class CatalogInstance: - """catalog of multiple methods that are executed depending on an init parameter @@ -82,7 +81,6 @@ def main_method(self) -> None: class CatalogClass: - """catalog of multiple class methods that are executed depending on an init parameter @@ -121,7 +119,6 @@ def main_method(self): class CatalogStatic: - """catalog of multiple static methods that are executed depending on an init parameter diff --git a/patterns/behavioral/iterator_alt.py b/patterns/behavioral/iterator_alt.py index d6fb0df9..a2a71d82 100644 --- a/patterns/behavioral/iterator_alt.py +++ b/patterns/behavioral/iterator_alt.py @@ -4,6 +4,7 @@ *TL;DR Traverses a container and accesses the container's elements. """ + from __future__ import annotations diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index e1d42fc2..545975d3 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -5,8 +5,8 @@ Provides the ability to restore an object to its previous state. """ -from typing import Callable, List from copy import copy, deepcopy +from typing import Callable, List def memento(obj, deep=False): diff --git a/patterns/behavioral/publish_subscribe.py b/patterns/behavioral/publish_subscribe.py index 40aefd2e..7e76955c 100644 --- a/patterns/behavioral/publish_subscribe.py +++ b/patterns/behavioral/publish_subscribe.py @@ -4,7 +4,6 @@ Author: https://github.com/HanWenfang """ - from __future__ import annotations diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 88862fa4..1d2f22eb 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -7,7 +7,6 @@ Enables selecting an algorithm at runtime. """ - from __future__ import annotations from typing import Callable diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index d092a6b4..55ca524b 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -62,7 +62,6 @@ def __str__(self) -> str: class PetShop: - """A pet shop""" def __init__(self, animal_factory: Type[Pet]) -> None: @@ -80,6 +79,7 @@ def buy_pet(self, name: str) -> Pet: # Additional factories: + # Create a random animal def random_animal(name: str) -> Pet: """Let's be dynamic!""" diff --git a/patterns/creational/borg.py b/patterns/creational/borg.py index de36a23f..edd0589d 100644 --- a/patterns/creational/borg.py +++ b/patterns/creational/borg.py @@ -32,6 +32,7 @@ *TL;DR Provides singleton-like behavior sharing state between instances. """ + from typing import Dict diff --git a/patterns/creational/factory.py b/patterns/creational/factory.py index a1beffa8..3ef2d2a8 100644 --- a/patterns/creational/factory.py +++ b/patterns/creational/factory.py @@ -21,9 +21,8 @@ *TL;DR Creates objects without having to specify the exact class. """ -from typing import Dict -from typing import Protocol -from typing import Type + +from typing import Dict, Protocol, Type class Localizer(Protocol): @@ -50,7 +49,6 @@ def localize(self, msg: str) -> str: def get_localizer(language: str = "English") -> Localizer: - """Factory""" localizers: Dict[str, Type[Localizer]] = { "English": EnglishLocalizer, diff --git a/patterns/creational/prototype.py b/patterns/creational/prototype.py index 4151ffbf..4c2dd7ed 100644 --- a/patterns/creational/prototype.py +++ b/patterns/creational/prototype.py @@ -20,6 +20,7 @@ *TL;DR Creates new object instances by cloning prototype. """ + from __future__ import annotations from typing import Any diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index ef48f501..cd2eb7ab 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -8,6 +8,7 @@ https://en.wikipedia.org/wiki/Blackboard_system """ + from __future__ import annotations import abc diff --git a/patterns/other/graph_search.py b/patterns/other/graph_search.py index ad224db3..262a6f08 100644 --- a/patterns/other/graph_search.py +++ b/patterns/other/graph_search.py @@ -1,5 +1,4 @@ class GraphSearch: - """Graph search emulation in python, from source http://www.python.org/doc/essays/graphs/ diff --git a/tests/behavioral/test_strategy.py b/tests/behavioral/test_strategy.py index 86018a44..53976f38 100644 --- a/tests/behavioral/test_strategy.py +++ b/tests/behavioral/test_strategy.py @@ -1,6 +1,6 @@ import pytest -from patterns.behavioral.strategy import Order, ten_percent_discount, on_sale_discount +from patterns.behavioral.strategy import Order, on_sale_discount, ten_percent_discount @pytest.fixture diff --git a/tests/creational/test_pool.py b/tests/creational/test_pool.py index 38476eb7..cd501db3 100644 --- a/tests/creational/test_pool.py +++ b/tests/creational/test_pool.py @@ -29,7 +29,6 @@ def test_frozen_pool(self): class TestNaitivePool(unittest.TestCase): - """def test_object(queue): queue_object = QueueObject(queue, True) print('Inside func: {}'.format(queue_object.object))""" From af6fc3e6bce49eeb6b3994d8653c0abf5ac16d41 Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Sun, 12 May 2024 12:43:49 +0000 Subject: [PATCH 092/127] Additional linting error in patterns/behavioral/strategy.py added spaces, changed to multiline string --- patterns/behavioral/strategy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 1d2f22eb..4bfde487 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -55,7 +55,9 @@ def apply_discount(self) -> float: return self.price - discount def __repr__(self) -> str: - return f"" + return f""" + + """ def ten_percent_discount(order: Order) -> float: From e0b0061ad2a97061ecaf1c4731961773f30a88f6 Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Sun, 12 May 2024 12:54:25 +0000 Subject: [PATCH 093/127] extract complexity from docstring line --- patterns/behavioral/strategy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 4bfde487..0a2e0724 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -55,9 +55,8 @@ def apply_discount(self) -> float: return self.price - discount def __repr__(self) -> str: - return f""" - - """ + strategy = getattr(self.discount_strategy, '__name__', None) + return f"" def ten_percent_discount(order: Order) -> float: From 87a1777578e40f46d560f7312eafb96d5385befb Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Sun, 12 May 2024 13:07:56 +0000 Subject: [PATCH 094/127] linter reformats quote marks in strategy pattern --- patterns/behavioral/strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/behavioral/strategy.py b/patterns/behavioral/strategy.py index 0a2e0724..000ff2ad 100644 --- a/patterns/behavioral/strategy.py +++ b/patterns/behavioral/strategy.py @@ -55,7 +55,7 @@ def apply_discount(self) -> float: return self.price - discount def __repr__(self) -> str: - strategy = getattr(self.discount_strategy, '__name__', None) + strategy = getattr(self.discount_strategy, "__name__", None) return f"" From ffb45ca52dea6b80a33c00942f0808b539db2c99 Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Sun, 12 May 2024 13:43:48 +0000 Subject: [PATCH 095/127] put lint into a single shell command --- .github/workflows/lint_python.yml | 15 +++------------ lint.sh | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 lint.sh diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 2fd12494..4b654cff 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -8,15 +8,6 @@ jobs: - uses: actions/setup-python@v4 with: python-version: 3.x - - run: pip install --upgrade pip - - run: pip install black codespell flake8 isort mypy pytest pyupgrade tox - - run: black --check . - - run: codespell --quiet-level=2 # --ignore-words-list="" --skip="" - - run: flake8 . --count --show-source --statistics - - run: isort --profile black . - - run: tox - - run: pip install -e . - - run: mypy --ignore-missing-imports . || true - - run: pytest . - - run: pytest --doctest-modules . || true - - run: shopt -s globstar && pyupgrade --py37-plus **/*.py + - shell: bash + name: Lint and test + run: ./lint.sh diff --git a/lint.sh b/lint.sh new file mode 100644 index 00000000..8428cc64 --- /dev/null +++ b/lint.sh @@ -0,0 +1,14 @@ +#! /bin/sh + +pip install --upgrade pip +pip install black codespell flake8 isort mypy pytest pyupgrade tox +black --check . +codespell --quiet-level=2 # --ignore-words-list="" --skip="" +flake8 . --count --show-source --statistics +isort --profile black . +tox +pip install -e . +mypy --ignore-missing-imports . || true +pytest . +pytest --doctest-modules . || true +shopt -s globstar && pyupgrade --py37-plus **/*.py \ No newline at end of file From b6ceef47034e3a33f577c914d7f7f4025642728d Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Thu, 16 May 2024 11:03:25 +0000 Subject: [PATCH 096/127] Removed Random Petshop tests from abstract_factory.py as per #418 --- lint.sh | 2 +- patterns/creational/abstract_factory.py | 25 ------------------------- 2 files changed, 1 insertion(+), 26 deletions(-) mode change 100644 => 100755 lint.sh diff --git a/lint.sh b/lint.sh old mode 100644 new mode 100755 index 8428cc64..a3ce9719 --- a/lint.sh +++ b/lint.sh @@ -1,4 +1,4 @@ -#! /bin/sh +#! /bin/bash pip install --upgrade pip pip install black codespell flake8 isort mypy pytest pyupgrade tox diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 55ca524b..464b0388 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -77,14 +77,6 @@ def buy_pet(self, name: str) -> Pet: return pet -# Additional factories: - - -# Create a random animal -def random_animal(name: str) -> Pet: - """Let's be dynamic!""" - return random.choice([Dog, Cat])(name) - # Show pets with various factories def main() -> None: @@ -95,27 +87,10 @@ def main() -> None: Here is your lovely Cat >>> pet.speak() meow - - # A shop that sells random animals - >>> shop = PetShop(random_animal) - >>> for name in ["Max", "Jack", "Buddy"]: - ... pet = shop.buy_pet(name) - ... pet.speak() - ... print("=" * 20) - Here is your lovely Cat - meow - ==================== - Here is your lovely Dog - woof - ==================== - Here is your lovely Dog - woof - ==================== """ if __name__ == "__main__": - random.seed(1234) # for deterministic doctest outputs shop = PetShop(random_animal) import doctest From e05b35f22d26a49cd625f54ea6af284c8c055390 Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Thu, 16 May 2024 11:06:22 +0000 Subject: [PATCH 097/127] linted abstract_factory.py --- patterns/creational/abstract_factory.py | 1 - 1 file changed, 1 deletion(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 464b0388..0ec49bbf 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -77,7 +77,6 @@ def buy_pet(self, name: str) -> Pet: return pet - # Show pets with various factories def main() -> None: """ From 9581e5a67ed1ba6d0d735fa3274269a99847932d Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Thu, 16 May 2024 11:14:49 +0000 Subject: [PATCH 098/127] updated readme to describe purpose of lint.sh file --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 74b689da..d49f7fb7 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,12 @@ To see Python 2 compatible versions of some patterns please check-out the [legac When everything else is done - update corresponding part of README. ##### Travis CI -Please run `tox` or `tox -e ci37` before submitting a patch to be sure your changes will pass CI. +Please run the following before submitting a patch +- `black .` This lints your code. + +Then either: +- `tox` or `tox -e ci37` This runs unit tests. see tox.ini for further details. +- If you have a bash compatible shell use `./lint.sh` This script will lint and test your code. This script mirrors the CI pipeline actions. You can also run `flake8` or `pytest` commands manually. Examples can be found in `tox.ini`. From 1a94d1f8b5c44f39dd278f1bfad6529033534686 Mon Sep 17 00:00:00 2001 From: Mahdi Azarboon <21277296+azarboon@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:29:57 +0800 Subject: [PATCH 099/127] Update README.md Before diving into any of the patterns, readers should be reminded of two fundamental laws in software architecture: 1.Everything is a trade-ff 2."Why is more important than the how" So, readers face the nuances and reality of these patterns from the beginning. These two laws are coined by two thought leaders in software architecture: Mark Richards and Neal Ford. They have explained these two laws in various conference talks and books. For example, Here you can read about these two laws here: https://www.infoq.com/podcasts/software-architecture-hard-parts/ Also, here is a book for reference: https://a.co/d/fKOodW9 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d49f7fb7..05222bc9 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ python-patterns A collection of design patterns and idioms in Python. +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. + Current Patterns ---------------- From cffe6cd9c80a0b3e44c00ce4374e1e20d5d0bca2 Mon Sep 17 00:00:00 2001 From: Sakis Kasampalis Date: Thu, 5 Sep 2024 22:44:52 +0200 Subject: [PATCH 100/127] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 05222bc9..6becf693 100644 --- a/README.md +++ b/README.md @@ -119,3 +119,8 @@ You can also run `flake8` or `pytest` commands manually. Examples can be found i ## 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 which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to python-patterns on CodeTriage](https://www.codetriage.com/faif/python-patterns). + +## AI codebase assistance ## + +The folks at Mutable.ai have built an AI assistant that is codebase-aware. Give it a try +[![Mutable.ai Auto Wiki](https://img.shields.io/badge/Auto_Wiki-Mutable.ai-blue)](https://wiki.mutable.ai/faif/python-patterns) From fa56fdecf908f0f94a819adcf19abbd07d2066c5 Mon Sep 17 00:00:00 2001 From: Sakis Kasampalis Date: Thu, 5 Sep 2024 22:45:47 +0200 Subject: [PATCH 101/127] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6becf693..6e873aa0 100644 --- a/README.md +++ b/README.md @@ -122,5 +122,4 @@ You can triage issues and pull requests which may include reproducing bug report ## AI codebase assistance ## -The folks at Mutable.ai have built an AI assistant that is codebase-aware. Give it a try -[![Mutable.ai Auto Wiki](https://img.shields.io/badge/Auto_Wiki-Mutable.ai-blue)](https://wiki.mutable.ai/faif/python-patterns) +The folks at Mutable.ai have built an AI assistant that is codebase-aware. Give it a try [![Mutable.ai Auto Wiki](https://img.shields.io/badge/Auto_Wiki-Mutable.ai-blue)](https://wiki.mutable.ai/faif/python-patterns) From a40009447810050ee60b6800c41a262da6e18946 Mon Sep 17 00:00:00 2001 From: cdorsman Date: Wed, 23 Apr 2025 02:10:21 +0200 Subject: [PATCH 102/127] Added routing --- patterns/structural/mvc.py | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 3f7dc315..b81e10be 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -4,6 +4,8 @@ """ from abc import ABC, abstractmethod +from inspect import signature +from sys import argv class Model(ABC): @@ -113,6 +115,23 @@ def show_item_information(self, item_name): self.view.show_item_information(item_type, item_name, item_info) +class Router: + def __init__(self): + self.routes = {} + + def register(self, path, controller, model, view): + model = model() + view = view() + self.routes[path] = controller(model, view) + + def resolve(self, path): + if self.routes.get(path): + controller = self.routes[path] + return controller + else: + return None + + def main(): """ >>> model = ProductModel() @@ -147,6 +166,26 @@ def main(): if __name__ == "__main__": - import doctest + router = Router() + router.register("products", Controller, ProductModel, ConsoleView) + controller = router.resolve(argv[1]) + + command = str(argv[2]) if len(argv) > 2 else None + args = ' '.join(map(str, argv[3:])) if len(argv) > 3 else None + + if hasattr(controller, command): + command = getattr(controller, command) + sig = signature(command) + + if len(sig.parameters) > 0: + if args: + command(args) + else: + print("Command requires arguments.") + else: + command() + else: + print(f"Command {command} not found in the controller.") + import doctest doctest.testmod() From 8f0a91c3d021599d78159b1a59f452c15119bdaf Mon Sep 17 00:00:00 2001 From: cdorsman Date: Wed, 23 Apr 2025 16:28:33 +0200 Subject: [PATCH 103/127] Cleaned up lint.sh --- .codespellignore | 14 ++++++++++++++ lint.sh | 20 +++++++++++--------- tox.ini | 4 ++-- 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .codespellignore diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 00000000..d272a2e1 --- /dev/null +++ b/.codespellignore @@ -0,0 +1,14 @@ +__pycache__ +*.pyc +.idea +*.egg-info/ +.tox/ +env/ +venv/ +.env +.venv +.vscode/ +.python-version +.coverage +build/ +dist/ \ No newline at end of file diff --git a/lint.sh b/lint.sh index a3ce9719..5c418249 100755 --- a/lint.sh +++ b/lint.sh @@ -2,13 +2,15 @@ pip install --upgrade pip pip install black codespell flake8 isort mypy pytest pyupgrade tox -black --check . -codespell --quiet-level=2 # --ignore-words-list="" --skip="" -flake8 . --count --show-source --statistics -isort --profile black . -tox pip install -e . -mypy --ignore-missing-imports . || true -pytest . -pytest --doctest-modules . || true -shopt -s globstar && pyupgrade --py37-plus **/*.py \ No newline at end of file + +source_dir="./patterns" + +codespell --quiet-level=2 ./patterns # --ignore-words-list="" --skip="" +flake8 "${source_dir}" --count --show-source --statistics +isort --profile black "${source_dir}" +tox +mypy --ignore-missing-imports "${source_dir}" || true +pytest "${source_dir}" +pytest --doctest-modules "${source_dir}" || true +shopt -s globstar && pyupgrade --py37-plus ${source_dir}/*.py diff --git a/tox.ini b/tox.ini index 1eca32ab..3ce6e132 100644 --- a/tox.ini +++ b/tox.ini @@ -9,10 +9,10 @@ setenv = deps = -r requirements-dev.txt commands = - flake8 . --exclude="./.*, venv" + flake8 --exclude="venv/,.tox/" patterns/ ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` pytest --randomly-seed=1234 --doctest-modules patterns/ - pytest -s -vv --cov={envsitepackagesdir}/patterns --log-level=INFO tests/ + pytest -s -vv --cov=patterns/ --log-level=INFO tests/ [testenv:cov-report] From 901685851ebc79344237e89c14aeb857e6229ae8 Mon Sep 17 00:00:00 2001 From: Sakis Kasampalis Date: Fri, 25 Apr 2025 16:21:08 +0200 Subject: [PATCH 104/127] Update README.md Remove mutable.ai references --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 6e873aa0..05222bc9 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,3 @@ You can also run `flake8` or `pytest` commands manually. Examples can be found i ## 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 which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to python-patterns on CodeTriage](https://www.codetriage.com/faif/python-patterns). - -## AI codebase assistance ## - -The folks at Mutable.ai have built an AI assistant that is codebase-aware. Give it a try [![Mutable.ai Auto Wiki](https://img.shields.io/badge/Auto_Wiki-Mutable.ai-blue)](https://wiki.mutable.ai/faif/python-patterns) From ce06e8b0492ea7bc002c8f59658fbabd236e6fc3 Mon Sep 17 00:00:00 2001 From: danny crasto Date: Sat, 26 Apr 2025 09:24:30 +0400 Subject: [PATCH 105/127] Highlight the need for a wrapper in delegate - It's required to capture the args/kwargs when called at run time. - Fix doctest output to include hints --- patterns/fundamental/delegation_pattern.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/patterns/fundamental/delegation_pattern.py b/patterns/fundamental/delegation_pattern.py index 34e1071f..f7a7c2f5 100644 --- a/patterns/fundamental/delegation_pattern.py +++ b/patterns/fundamental/delegation_pattern.py @@ -19,13 +19,15 @@ class Delegator: >>> delegator.p2 Traceback (most recent call last): ... - AttributeError: 'Delegate' object has no attribute 'p2' + AttributeError: 'Delegate' object has no attribute 'p2'. Did you mean: 'p1'? >>> delegator.do_something("nothing") 'Doing nothing' + >>> delegator.do_something("something", kw=", faif!") + 'Doing something, faif!' >>> delegator.do_anything() Traceback (most recent call last): ... - AttributeError: 'Delegate' object has no attribute 'do_anything' + AttributeError: 'Delegate' object has no attribute 'do_anything'. Did you mean: 'do_something'? """ def __init__(self, delegate: Delegate) -> None: @@ -47,8 +49,8 @@ class Delegate: def __init__(self) -> None: self.p1 = 123 - def do_something(self, something: str) -> str: - return f"Doing {something}" + def do_something(self, something: str, kw=None) -> str: + return f"Doing {something}{kw or ''}" if __name__ == "__main__": From ab82cbe3cf11741cbae3204e37693227dc32c9d4 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 14:14:59 +0200 Subject: [PATCH 106/127] Removed old Python versions --- .travis.yml | 8 +++----- lint.sh | 2 +- setup.cfg | 2 +- setup.py | 9 +++------ tox.ini | 2 +- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index ab6ba6bf..dfeece70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,11 @@ os: linux -dist: focal +dist: noble language: python jobs: include: - - python: "3.8" - env: TOXENV=py38 - - python: "3.9" - env: TOXENV=py39 + - python: "3.12" + env: TOXENV=py312 cache: - pip diff --git a/lint.sh b/lint.sh index 5c418249..a7eebda1 100755 --- a/lint.sh +++ b/lint.sh @@ -13,4 +13,4 @@ tox mypy --ignore-missing-imports "${source_dir}" || true pytest "${source_dir}" pytest --doctest-modules "${source_dir}" || true -shopt -s globstar && pyupgrade --py37-plus ${source_dir}/*.py +shopt -s globstar && pyupgrade --py312-plus ${source_dir}/*.py diff --git a/setup.cfg b/setup.cfg index eb556c0a..e109555b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,5 +9,5 @@ filterwarnings = ignore:.*test class 'TestRunner'.*:Warning [mypy] -python_version = 3.8 +python_version = 3.12 ignore_missing_imports = True diff --git a/setup.py b/setup.py index ec2528f4..72bc2b46 100644 --- a/setup.py +++ b/setup.py @@ -5,13 +5,10 @@ packages=find_packages(), description="A collection of design patterns and idioms in Python.", classifiers=[ - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], ) diff --git a/tox.ini b/tox.ini index 3ce6e132..7c23885f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,py39,py310,cov-report +envlist = py310,py312,cov-report skip_missing_interpreters = true From 3b585656af4c297bfc99cb716084b9e8c4c56b76 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 14:36:42 +0200 Subject: [PATCH 107/127] Added typing --- patterns/structural/mvc.py | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index b81e10be..5fe454f4 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -14,14 +14,14 @@ def __iter__(self): pass @abstractmethod - def get(self, item): + def get(self, item: str) -> dict: """Returns an object with a .items() call method that iterates over key,value pairs of its information.""" pass @property @abstractmethod - def item_type(self): + def item_type(self) -> str: pass @@ -30,7 +30,7 @@ class Price(float): """A polymorphic way to pass a float with a particular __str__ functionality.""" - def __str__(self): + def __str__(self) -> str: return f"{self:.2f}" products = { @@ -44,7 +44,7 @@ def __str__(self): def __iter__(self): yield from self.products - def get(self, product): + def get(self, product: str) -> dict: try: return self.products[product] except KeyError as e: @@ -53,32 +53,32 @@ def get(self, product): class View(ABC): @abstractmethod - def show_item_list(self, item_type, item_list): + def show_item_list(self, item_type: str, item_list: dict) -> None: pass @abstractmethod - def show_item_information(self, item_type, item_name, item_info): + def show_item_information(self, item_type: str, item_name: str, item_info: str) -> None: """Will look for item information by iterating over key,value pairs yielded by item_info.items()""" pass @abstractmethod - def item_not_found(self, item_type, item_name): + def item_not_found(self, item_type, item_name) -> None: pass class ConsoleView(View): - def show_item_list(self, item_type, item_list): + def show_item_list(self, item_type, item_list) -> None: print(item_type.upper() + " LIST:") for item in item_list: print(item) print("") @staticmethod - def capitalizer(string): + def capitalizer(string) -> str: return string[0].upper() + string[1:].lower() - def show_item_information(self, item_type, item_name, item_info): + def show_item_information(self, item_type, item_name, item_info) -> None: print(item_type.upper() + " INFORMATION:") printout = "Name: %s" % item_name for key, value in item_info.items(): @@ -86,7 +86,7 @@ def show_item_information(self, item_type, item_name, item_info): printout += "\n" print(printout) - def item_not_found(self, item_type, item_name): + def item_not_found(self, item_type, item_name) -> None: print(f'That {item_type} "{item_name}" does not exist in the records') @@ -95,12 +95,12 @@ def __init__(self, model, view): self.model = model self.view = view - def show_items(self): + def show_items(self) -> None: items = list(self.model) item_type = self.model.item_type self.view.show_item_list(item_type, items) - def show_item_information(self, item_name): + def show_item_information(self, item_name) -> None: """ Show information about a {item_type} item. :param str item_name: the name of the {item_type} item to show information about @@ -117,16 +117,16 @@ def show_item_information(self, item_name): class Router: def __init__(self): - self.routes = {} + self.routes: dict = {} - def register(self, path, controller, model, view): - model = model() - view = view() + def register(self, path: str, controller: object, model: object, view: object) -> None: + model: object = model() + view: object = view() self.routes[path] = controller(model, view) - def resolve(self, path): + def resolve(self, path) -> Controller: if self.routes.get(path): - controller = self.routes[path] + controller: object = self.routes[path] return controller else: return None @@ -166,12 +166,12 @@ def main(): if __name__ == "__main__": - router = Router() + router: object = Router() router.register("products", Controller, ProductModel, ConsoleView) - controller = router.resolve(argv[1]) + controller: object = router.resolve(argv[1]) - command = str(argv[2]) if len(argv) > 2 else None - args = ' '.join(map(str, argv[3:])) if len(argv) > 3 else None + command: str = str(argv[2]) if len(argv) > 2 else None + args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else None if hasattr(controller, command): command = getattr(controller, command) From 24f8dcdd13fe08eb751e387f8b5a4e43e6d0391a Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 14:41:30 +0200 Subject: [PATCH 108/127] Fixed bug --- patterns/structural/mvc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 5fe454f4..b01f9fc2 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -170,8 +170,8 @@ def main(): router.register("products", Controller, ProductModel, ConsoleView) controller: object = router.resolve(argv[1]) - command: str = str(argv[2]) if len(argv) > 2 else None - args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else None + command: str = str(argv[2]) if len(argv) > 2 else "" + args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else "" if hasattr(controller, command): command = getattr(controller, command) From 93b4e16bf681e49ec625ea9aea66982653032a82 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 14:54:11 +0200 Subject: [PATCH 109/127] Removed bugs and added more types --- patterns/structural/mvc.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index b01f9fc2..a406fb06 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -63,12 +63,12 @@ def show_item_information(self, item_type: str, item_name: str, item_info: str) pass @abstractmethod - def item_not_found(self, item_type, item_name) -> None: + def item_not_found(self, item_type: str, item_name: str) -> None: pass class ConsoleView(View): - def show_item_list(self, item_type, item_list) -> None: + def show_item_list(self, item_type: str, item_list: dict) -> None: print(item_type.upper() + " LIST:") for item in item_list: print(item) @@ -86,21 +86,21 @@ def show_item_information(self, item_type, item_name, item_info) -> None: printout += "\n" print(printout) - def item_not_found(self, item_type, item_name) -> None: + def item_not_found(self, item_type: str, item_name: str) -> None: print(f'That {item_type} "{item_name}" does not exist in the records') class Controller: - def __init__(self, model, view): - self.model = model - self.view = view + def __init__(self, model_class, view_class) -> None: + self.model = model_class + self.view = view_class def show_items(self) -> None: items = list(self.model) item_type = self.model.item_type self.view.show_item_list(item_type, items) - def show_item_information(self, item_name) -> None: + def show_item_information(self, item_name: str) -> None: """ Show information about a {item_type} item. :param str item_name: the name of the {item_type} item to show information about @@ -119,15 +119,15 @@ class Router: def __init__(self): self.routes: dict = {} - def register(self, path: str, controller: object, model: object, view: object) -> None: - model: object = model() - view: object = view() - self.routes[path] = controller(model, view) + def register(self, path: str, controller_class: object, model_class: object, view_class: object) -> None: + model_instance: object = model_class() + view_instance: object = view_class() + self.routes[path] = controller_class(model_instance, view_instance) - def resolve(self, path) -> Controller: + def resolve(self, path: str) -> Controller: if self.routes.get(path): - controller: object = self.routes[path] - return controller + controller_class: object = self.routes[path] + return controller_class else: return None @@ -170,11 +170,11 @@ def main(): router.register("products", Controller, ProductModel, ConsoleView) controller: object = router.resolve(argv[1]) - command: str = str(argv[2]) if len(argv) > 2 else "" + action: str = str(argv[2]) if len(argv) > 2 else "" args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else "" - if hasattr(controller, command): - command = getattr(controller, command) + if hasattr(controller, action): + command = getattr(controller, action) sig = signature(command) if len(sig.parameters) > 0: @@ -185,7 +185,7 @@ def main(): else: command() else: - print(f"Command {command} not found in the controller.") + print(f"Command {action} not found in the controller.") import doctest doctest.testmod() From ccc17b499784aa20a0b663a044175e318030c950 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 14:59:48 +0200 Subject: [PATCH 110/127] Fixed bug on check if controller is defined --- patterns/structural/mvc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index a406fb06..96396f9a 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -120,16 +120,16 @@ def __init__(self): self.routes: dict = {} def register(self, path: str, controller_class: object, model_class: object, view_class: object) -> None: - model_instance: object = model_class() - view_instance: object = view_class() + model_instance = model_class() + view_instance = view_class() self.routes[path] = controller_class(model_instance, view_instance) def resolve(self, path: str) -> Controller: if self.routes.get(path): - controller_class: object = self.routes[path] - return controller_class + controller: Controller = self.routes[path] + return controller else: - return None + raise KeyError(f"No controller registered for path '{path}'") def main(): From 65fcf56ddaf2282d5340511e8f1ccdb41b055130 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 15:03:01 +0200 Subject: [PATCH 111/127] removed object definition from routes --- patterns/structural/mvc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 96396f9a..af6305ef 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -117,9 +117,9 @@ def show_item_information(self, item_name: str) -> None: class Router: def __init__(self): - self.routes: dict = {} + self.routes = {} - def register(self, path: str, controller_class: object, model_class: object, view_class: object) -> None: + def register(self, path: str, controller_class, model_class, view_class) -> None: model_instance = model_class() view_instance = view_class() self.routes[path] = controller_class(model_instance, view_instance) From 6af5a8273bf82bda202a58c5a6d90c6a741ab1e4 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 15:05:39 +0200 Subject: [PATCH 112/127] I fixed a bug --- patterns/structural/mvc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index af6305ef..64581d48 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -166,9 +166,9 @@ def main(): if __name__ == "__main__": - router: object = Router() + router = Router() router.register("products", Controller, ProductModel, ConsoleView) - controller: object = router.resolve(argv[1]) + controller: Controller = router.resolve(argv[1]) action: str = str(argv[2]) if len(argv) > 2 else "" args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else "" From f6bc58d09b49b6a7e2d2807b6d9b443a107afd25 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 23:40:17 +0200 Subject: [PATCH 113/127] =?UTF-8?q?=C3=84dded=20comments=20and=20lost=20ty?= =?UTF-8?q?pes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patterns/structural/mvc.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 64581d48..e06d16c4 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -9,6 +9,7 @@ class Model(ABC): + """The Model is the data layer of the application.""" @abstractmethod def __iter__(self): pass @@ -26,6 +27,7 @@ def item_type(self) -> str: class ProductModel(Model): + """The Model is the data layer of the application.""" class Price(float): """A polymorphic way to pass a float with a particular __str__ functionality.""" @@ -52,12 +54,13 @@ def get(self, product: str) -> dict: class View(ABC): + """The View is the presentation layer of the application.""" @abstractmethod def show_item_list(self, item_type: str, item_list: dict) -> None: pass @abstractmethod - def show_item_information(self, item_type: str, item_name: str, item_info: str) -> None: + def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: """Will look for item information by iterating over key,value pairs yielded by item_info.items()""" pass @@ -68,6 +71,7 @@ def item_not_found(self, item_type: str, item_name: str) -> None: class ConsoleView(View): + """The View is the presentation layer of the application.""" def show_item_list(self, item_type: str, item_list: dict) -> None: print(item_type.upper() + " LIST:") for item in item_list: @@ -75,10 +79,12 @@ def show_item_list(self, item_type: str, item_list: dict) -> None: print("") @staticmethod - def capitalizer(string) -> str: + def capitalizer(string: str) -> str: + """Capitalizes the first letter of a string and lowercases the rest.""" return string[0].upper() + string[1:].lower() - def show_item_information(self, item_type, item_name, item_info) -> None: + def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: + """Will look for item information by iterating over key,value pairs""" print(item_type.upper() + " INFORMATION:") printout = "Name: %s" % item_name for key, value in item_info.items(): @@ -91,9 +97,10 @@ def item_not_found(self, item_type: str, item_name: str) -> None: class Controller: - def __init__(self, model_class, view_class) -> None: - self.model = model_class - self.view = view_class + """The Controller is the intermediary between the Model and the View.""" + def __init__(self, model_class: Model, view_class: View) -> None: + self.model: Model = model_class + self.view: View = view_class def show_items(self) -> None: items = list(self.model) @@ -106,22 +113,23 @@ def show_item_information(self, item_name: str) -> None: :param str item_name: the name of the {item_type} item to show information about """ try: - item_info = self.model.get(item_name) + item_info: str = self.model.get(item_name) except Exception: - item_type = self.model.item_type + item_type: str = self.model.item_type self.view.item_not_found(item_type, item_name) else: - item_type = self.model.item_type + item_type: str = self.model.item_type self.view.show_item_information(item_type, item_name, item_info) class Router: + """The Router is the entry point of the application.""" def __init__(self): self.routes = {} - def register(self, path: str, controller_class, model_class, view_class) -> None: - model_instance = model_class() - view_instance = view_class() + def register(self, path: str, controller_class: Controller, model_class: Model, view_class: View) -> None: + model_instance: Model = model_class() + view_instance: View = view_class() self.routes[path] = controller_class(model_instance, view_instance) def resolve(self, path: str) -> Controller: From 58bd201b48802dc0f69eafb083366d2fbf1824d6 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sat, 3 May 2025 23:52:57 +0200 Subject: [PATCH 114/127] Defined "random_animal" with random animal from list. --- patterns/creational/abstract_factory.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 0ec49bbf..51658f4e 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -90,6 +90,9 @@ def main() -> None: if __name__ == "__main__": + animals = ['dog', 'cat'] + random_animal = random.choice(animals) + shop = PetShop(random_animal) import doctest From a50bb549fedf403d0b18f6ba38e9cf5a7b62f903 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 02:08:23 +0200 Subject: [PATCH 115/127] - Moved AbstractExpert - Changed __init__ in AbstractExpert to abstract method - Added comments --- patterns/other/blackboard.py | 46 +++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index cd2eb7ab..6eb7f82e 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -9,13 +9,27 @@ https://en.wikipedia.org/wiki/Blackboard_system """ -from __future__ import annotations - -import abc +from abc import ABC, abstractmethod import random +class AbstractExpert(ABC): + """Abstract class for experts in the blackboard system.""" + @abstractmethod + def __init__(self, blackboard: object) -> None: + self.blackboard = blackboard + + @property + @abstractmethod + def is_eager_to_contribute(self): + raise NotImplementedError("Must provide implementation in subclass.") + + @abstractmethod + def contribute(self): + raise NotImplementedError("Must provide implementation in subclass.") + class Blackboard: + """The blackboard system that holds the common state.""" def __init__(self) -> None: self.experts = [] self.common_state = { @@ -30,6 +44,7 @@ def add_expert(self, expert: AbstractExpert) -> None: class Controller: + """The controller that manages the blackboard system.""" def __init__(self, blackboard: Blackboard) -> None: self.blackboard = blackboard @@ -45,21 +60,11 @@ def run_loop(self): return self.blackboard.common_state["contributions"] -class AbstractExpert(metaclass=abc.ABCMeta): - def __init__(self, blackboard: Blackboard) -> None: - self.blackboard = blackboard - - @property - @abc.abstractmethod - def is_eager_to_contribute(self): - raise NotImplementedError("Must provide implementation in subclass.") - - @abc.abstractmethod - def contribute(self): - raise NotImplementedError("Must provide implementation in subclass.") - - class Student(AbstractExpert): + """Concrete class for a student expert.""" + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + @property def is_eager_to_contribute(self) -> bool: return True @@ -72,6 +77,10 @@ def contribute(self) -> None: class Scientist(AbstractExpert): + """Concrete class for a scientist expert.""" + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + @property def is_eager_to_contribute(self) -> int: return random.randint(0, 1) @@ -84,6 +93,9 @@ def contribute(self) -> None: class Professor(AbstractExpert): + def __init__(self, blackboard) -> None: + super().__init__(blackboard) + @property def is_eager_to_contribute(self) -> bool: return True if self.blackboard.common_state["problems"] > 100 else False From 9ad720667af7dd83e636da7eb48573d0d806154f Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 02:11:26 +0200 Subject: [PATCH 116/127] Removed object type from init --- patterns/other/blackboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 6eb7f82e..3eea314a 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -15,7 +15,7 @@ class AbstractExpert(ABC): """Abstract class for experts in the blackboard system.""" @abstractmethod - def __init__(self, blackboard: object) -> None: + def __init__(self, blackboard) -> None: self.blackboard = blackboard @property From e8343854455ca2a00268571a31de7ea60931f029 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 02:21:47 +0200 Subject: [PATCH 117/127] Retry --- patterns/other/blackboard.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 3eea314a..970259d0 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -12,6 +12,7 @@ from abc import ABC, abstractmethod import random + class AbstractExpert(ABC): """Abstract class for experts in the blackboard system.""" @abstractmethod @@ -20,18 +21,18 @@ def __init__(self, blackboard) -> None: @property @abstractmethod - def is_eager_to_contribute(self): + def is_eager_to_contribute(self) -> bool: raise NotImplementedError("Must provide implementation in subclass.") @abstractmethod - def contribute(self): + def contribute(self) -> None: raise NotImplementedError("Must provide implementation in subclass.") class Blackboard: """The blackboard system that holds the common state.""" def __init__(self) -> None: - self.experts = [] + self.experts: list = [AbstractExpert] self.common_state = { "problems": 0, "suggestions": 0, @@ -138,7 +139,7 @@ def main(): if __name__ == "__main__": - random.seed(1234) # for deterministic doctest outputs + #random.seed(1234) # for deterministic doctest outputs import doctest doctest.testmod() From 8c0b293b4590f25bb864bcbfca16a696e8a8194a Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 02:34:47 +0200 Subject: [PATCH 118/127] Retry2 --- patterns/other/blackboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 970259d0..e02246b9 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -21,7 +21,7 @@ def __init__(self, blackboard) -> None: @property @abstractmethod - def is_eager_to_contribute(self) -> bool: + def is_eager_to_contribute(self) -> int: raise NotImplementedError("Must provide implementation in subclass.") @abstractmethod @@ -32,7 +32,7 @@ def contribute(self) -> None: class Blackboard: """The blackboard system that holds the common state.""" def __init__(self) -> None: - self.experts: list = [AbstractExpert] + self.experts: list = [] self.common_state = { "problems": 0, "suggestions": 0, From 092fdd3837c408ad8fd32213e52cfe993ec0b44f Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 02:40:37 +0200 Subject: [PATCH 119/127] fix doctest --- patterns/other/blackboard.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index e02246b9..df4b7697 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -121,13 +121,9 @@ def main(): >>> from pprint import pprint >>> pprint(contributions) ['Student', - 'Student', - 'Student', - 'Student', 'Scientist', 'Student', 'Student', - 'Student', 'Scientist', 'Student', 'Scientist', From 049d5559b32f21a1667485bb0115133b8a17f562 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 02:44:18 +0200 Subject: [PATCH 120/127] Retry3 --- patterns/other/blackboard.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index df4b7697..a981dcc0 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -123,19 +123,12 @@ def main(): ['Student', 'Scientist', 'Student', - 'Student', - 'Scientist', - 'Student', - 'Scientist', - 'Student', - 'Student', - 'Scientist', - 'Professor'] + 'Student',] """ if __name__ == "__main__": - #random.seed(1234) # for deterministic doctest outputs + random.seed(1234) # for deterministic doctest outputs import doctest doctest.testmod() From 79a41c7e9710336c495bd015810689170ccbdcc4 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 02:47:38 +0200 Subject: [PATCH 121/127] Retry4 --- patterns/other/blackboard.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index a981dcc0..58fbdb98 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -120,10 +120,13 @@ def main(): >>> from pprint import pprint >>> pprint(contributions) - ['Student', - 'Scientist', - 'Student', - 'Student',] + ['Student', + 'Scientist', + 'Student', + 'Scientist', + 'Student', + 'Scientist', + 'Professor'] """ From 871fd8a1bea6a9f8aa79b962972dd893a44f922d Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 03:49:14 +0200 Subject: [PATCH 122/127] Added type to random_animal --- patterns/creational/abstract_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 51658f4e..ec356eac 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -91,7 +91,7 @@ def main() -> None: if __name__ == "__main__": animals = ['dog', 'cat'] - random_animal = random.choice(animals) + random_animal: Pet = random.choice(animals) shop = PetShop(random_animal) import doctest From 7f4e2664b085763a35920f35873aba1f403db7f8 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 03:51:15 +0200 Subject: [PATCH 123/127] Pet to type[Pet] --- patterns/creational/abstract_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index ec356eac..92ce04b2 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -91,7 +91,7 @@ def main() -> None: if __name__ == "__main__": animals = ['dog', 'cat'] - random_animal: Pet = random.choice(animals) + random_animal: type[Pet] = random.choice(animals) shop = PetShop(random_animal) import doctest From 84b4b7b5e6d0b15469896bf7769e7abe90739056 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 03:56:48 +0200 Subject: [PATCH 124/127] woof --- patterns/creational/abstract_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 92ce04b2..780b682e 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -90,7 +90,7 @@ def main() -> None: if __name__ == "__main__": - animals = ['dog', 'cat'] + animals = [Dog, Cat] random_animal: type[Pet] = random.choice(animals) shop = PetShop(random_animal) From 7db462e8e72696bb11567207c4fb4f416129f2a3 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Sun, 4 May 2025 04:01:59 +0200 Subject: [PATCH 125/127] t to T --- patterns/creational/abstract_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/creational/abstract_factory.py b/patterns/creational/abstract_factory.py index 780b682e..15e5d67f 100644 --- a/patterns/creational/abstract_factory.py +++ b/patterns/creational/abstract_factory.py @@ -91,7 +91,7 @@ def main() -> None: if __name__ == "__main__": animals = [Dog, Cat] - random_animal: type[Pet] = random.choice(animals) + random_animal: Type[Pet] = random.choice(animals) shop = PetShop(random_animal) import doctest From ecc5e1709389633095a0be3c84085d6d8c4e3109 Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Wed, 7 May 2025 17:48:16 +0200 Subject: [PATCH 126/127] Remove old py versions (#440) * Removed old Python versions * Removed 3.10 from tox and upgraded requirements-dev.txt becasue of higher versions in lint.sh * 3.13 changed to 3.12 * Adjusted lint_python workflow Upgraded flake8 to 7.1 * Added continue-on-error: true. So that if the workflow stop comes in error, it will continue. * Added workflow to check per PR * Moved workflow * Changed name workflow * Changed job name * Added approval for non-Python files and removed continue-on-error * Optimzed lint_pr.yml * Added fix for PyTest * Let pytest only test on changed python design patterns * Optimized Tox * Allow tox execute it's checks * Tox optimization 2 * Optimized check * Ignore setup.py from linting unless it is changes * Fixed bug * Testing a idea * Revert idea * added __init__.py to tests/ for tox * Let tox only test on Python files that are in the PR. * Adjusted .coveragerc * added usedevelop = true to tox.ini * Change cov from patterns to main * Rewrote check. * retry fixing coverage * Change cov to main * Added coverage run to execute pytest * changed cov to patterns * created pyproject.toml and moved old config to backup folder * Testing * Changed opts to doctest * Fix for error Unknown config option: randomly_seed * Trying fix for No data was collected. (no-data-collected) * Changed source from patterns to ./ * Changed source from patterns to ./ --- .coveragerc | 6 - .github/workflows/lint_pr.yml | 286 +++++++++++++++++++++++++++ .github/workflows/lint_python.yml | 33 +++- config_backup/.coveragerc | 25 +++ setup.cfg => config_backup/setup.cfg | 0 tox.ini => config_backup/tox.ini | 8 +- pyproject.toml | 98 +++++++++ requirements-dev.txt | 17 +- tests/__init__.py | 0 9 files changed, 452 insertions(+), 21 deletions(-) delete mode 100644 .coveragerc create mode 100644 .github/workflows/lint_pr.yml create mode 100644 config_backup/.coveragerc rename setup.cfg => config_backup/setup.cfg (100%) rename tox.ini => config_backup/tox.ini (85%) create mode 100644 pyproject.toml create mode 100644 tests/__init__.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3778bf3d..00000000 --- a/.coveragerc +++ /dev/null @@ -1,6 +0,0 @@ -[report] -exclude_lines = - pragma: no cover - # Don't complain if tests don't hit defensive assertion code: - # See: https://stackoverflow.com/a/9212387/3407256 - raise NotImplementedError diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml new file mode 100644 index 00000000..f18e5c2e --- /dev/null +++ b/.github/workflows/lint_pr.yml @@ -0,0 +1,286 @@ +name: lint_pull_request +on: [pull_request, push] +jobs: + check_changes: + runs-on: ubuntu-24.04 + outputs: + has_python_changes: ${{ steps.changed-files.outputs.has_python_changes }} + files: ${{ steps.changed-files.outputs.files }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # To get all history for git diff commands + + - name: Get changed Python files + id: changed-files + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + # For PRs, compare against base branch + CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep '\.py$' | grep -v "^setup\.py$" || echo "") + # Check if setup.py specifically changed + SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep "^setup\.py$" || echo "") + if [ ! -z "$SETUP_PY_CHANGED" ]; then + CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" + fi + else + # For pushes, use the before/after SHAs + CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep '\.py$' | grep -v "^setup\.py$" || echo "") + # Check if setup.py specifically changed + SETUP_PY_CHANGED=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.before }} ${{ github.event.after }} | grep "^setup\.py$" || echo "") + if [ ! -z "$SETUP_PY_CHANGED" ]; then + CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" + fi + fi + + # Check if any Python files were changed and set the output accordingly + if [ -z "$CHANGED_FILES" ]; then + echo "No Python files changed" + echo "has_python_changes=false" >> $GITHUB_OUTPUT + echo "files=" >> $GITHUB_OUTPUT + else + echo "Changed Python files: $CHANGED_FILES" + echo "has_python_changes=true" >> $GITHUB_OUTPUT + echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT + fi + + - name: PR information + if: ${{ github.event_name == 'pull_request' }} + run: | + if [[ "${{ steps.changed-files.outputs.has_python_changes }}" == "true" ]]; then + echo "This PR contains Python changes that will be linted." + else + echo "This PR contains no Python changes, but still requires manual approval." + fi + + lint: + needs: check_changes + if: ${{ needs.check_changes.outputs.has_python_changes == 'true' }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + tool: [flake8, format, mypy, pytest, pyupgrade, tox] + steps: + # Additional check to ensure we have Python files before proceeding + - name: Verify Python changes + run: | + if [[ "${{ needs.check_changes.outputs.has_python_changes }}" != "true" ]]; then + echo "No Python files were changed. Skipping linting." + exit 0 + fi + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + # Flake8 linting + - name: Lint with flake8 + if: ${{ matrix.tool == 'flake8' }} + id: flake8 + run: | + echo "Linting files: ${{ needs.check_changes.outputs.files }}" + flake8 ${{ needs.check_changes.outputs.files }} --count --show-source --statistics + + # Format checking with isort and black + - name: Format check + if: ${{ matrix.tool == 'format' }} + id: format + run: | + echo "Checking format with isort for: ${{ needs.check_changes.outputs.files }}" + isort --profile black --check ${{ needs.check_changes.outputs.files }} + echo "Checking format with black for: ${{ needs.check_changes.outputs.files }}" + black --check ${{ needs.check_changes.outputs.files }} + + # Type checking with mypy + - name: Type check with mypy + if: ${{ matrix.tool == 'mypy' }} + id: mypy + run: | + echo "Type checking: ${{ needs.check_changes.outputs.files }}" + mypy --ignore-missing-imports ${{ needs.check_changes.outputs.files }} + + # Run tests with pytest + - name: Run tests with pytest + if: ${{ matrix.tool == 'pytest' }} + id: pytest + run: | + echo "Running pytest discovery..." + python -m pytest --collect-only -v + + # First run any test files that correspond to changed files + echo "Running tests for changed files..." + changed_files="${{ needs.check_changes.outputs.files }}" + + # Extract module paths from changed files + modules=() + for file in $changed_files; do + # Convert file path to module path (remove .py and replace / with .) + if [[ $file == patterns/* ]]; then + module_path=${file%.py} + module_path=${module_path//\//.} + modules+=("$module_path") + fi + done + + # Run tests for each module + for module in "${modules[@]}"; do + echo "Testing module: $module" + python -m pytest -xvs tests/ -k "$module" || true + done + + # Then run doctests on the changed files + echo "Running doctests for changed files..." + for file in $changed_files; do + if [[ $file == *.py ]]; then + echo "Running doctest for $file" + python -m pytest --doctest-modules -v $file || true + fi + done + + # Check Python version compatibility + - name: Check Python version compatibility + if: ${{ matrix.tool == 'pyupgrade' }} + id: pyupgrade + run: pyupgrade --py312-plus ${{ needs.check_changes.outputs.files }} + + # Run tox + - name: Run tox + if: ${{ matrix.tool == 'tox' }} + id: tox + run: | + echo "Running tox integration for changed files..." + changed_files="${{ needs.check_changes.outputs.files }}" + + # Create a temporary tox configuration that extends the original one + echo "[tox]" > tox_pr.ini + echo "envlist = py312" >> tox_pr.ini + echo "skip_missing_interpreters = true" >> tox_pr.ini + + echo "[testenv]" >> tox_pr.ini + echo "setenv =" >> tox_pr.ini + echo " COVERAGE_FILE = .coverage.{envname}" >> tox_pr.ini + echo "deps =" >> tox_pr.ini + echo " -r requirements-dev.txt" >> tox_pr.ini + echo "allowlist_externals =" >> tox_pr.ini + echo " pytest" >> tox_pr.ini + echo " coverage" >> tox_pr.ini + echo " python" >> tox_pr.ini + echo "commands =" >> tox_pr.ini + + # Check if we have any implementation files that changed + pattern_files=0 + test_files=0 + + for file in $changed_files; do + if [[ $file == patterns/* ]]; then + pattern_files=1 + elif [[ $file == tests/* ]]; then + test_files=1 + fi + done + + # Only run targeted tests, no baseline + echo " # Run specific tests for changed files" >> tox_pr.ini + + has_tests=false + + # Add coverage-focused test commands + for file in $changed_files; do + if [[ $file == *.py ]]; then + # Run coverage tests for implementation files + if [[ $file == patterns/* ]]; then + module_name=$(basename $file .py) + + # Get the pattern type (behavioral, structural, etc.) + if [[ $file == patterns/behavioral/* ]]; then + pattern_dir="behavioral" + elif [[ $file == patterns/creational/* ]]; then + pattern_dir="creational" + elif [[ $file == patterns/structural/* ]]; then + pattern_dir="structural" + elif [[ $file == patterns/fundamental/* ]]; then + pattern_dir="fundamental" + elif [[ $file == patterns/other/* ]]; then + pattern_dir="other" + else + pattern_dir="" + fi + + echo " # Testing $file" >> tox_pr.ini + + # Check if specific test exists + if [ -n "$pattern_dir" ]; then + test_path="tests/${pattern_dir}/test_${module_name}.py" + echo " if [ -f \"${test_path}\" ]; then echo \"Test file ${test_path} exists: true\" && coverage run -m pytest -xvs --cov=patterns --cov-append ${test_path}; else echo \"Test file ${test_path} exists: false\"; fi" >> tox_pr.ini + + # Also try to find any test that might include this module + echo " coverage run -m pytest -xvs --cov=patterns --cov-append tests/${pattern_dir}/ -k \"${module_name}\" --no-header" >> tox_pr.ini + fi + + # Run doctests for the file + echo " coverage run -m pytest --doctest-modules -v --cov=patterns --cov-append $file" >> tox_pr.ini + + has_tests=true + fi + + # Run test files directly if modified + if [[ $file == tests/* ]]; then + echo " coverage run -m pytest -xvs --cov=patterns --cov-append $file" >> tox_pr.ini + has_tests=true + fi + fi + done + + # If we didn't find any specific tests to run, mention it + if [ "$has_tests" = false ]; then + echo " python -c \"print('No specific tests found for changed files. Consider adding tests.')\"" >> tox_pr.ini + # Add a minimal test to avoid failure, but ensure it generates coverage data + echo " coverage run -m pytest -xvs --cov=patterns --cov-append -k \"not integration\" --no-header" >> tox_pr.ini + fi + + # Add coverage report command + echo " coverage combine" >> tox_pr.ini + echo " coverage report -m" >> tox_pr.ini + + # Run tox with the custom configuration + echo "Running tox with custom PR configuration..." + echo "======================== TOX CONFIG ========================" + cat tox_pr.ini + echo "===========================================================" + tox -c tox_pr.ini + + summary: + needs: [check_changes, lint] + # Run summary in all cases, regardless of whether lint job ran + if: ${{ always() }} + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + + - name: Summarize results + run: | + echo "## Pull Request Lint Results" >> $GITHUB_STEP_SUMMARY + if [[ "${{ needs.check_changes.outputs.has_python_changes }}" == "true" ]]; then + echo "Linting has completed for all Python files changed in this PR." >> $GITHUB_STEP_SUMMARY + echo "See individual job logs for detailed results." >> $GITHUB_STEP_SUMMARY + else + echo "No Python files were changed in this PR. Linting was skipped." >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ **Note:** This PR still requires manual approval regardless of linting results." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 4b654cff..19d6c078 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -2,12 +2,35 @@ name: lint_python on: [pull_request, push] jobs: lint_python: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.x - - shell: bash - name: Lint and test - run: ./lint.sh + python-version: 3.12 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Lint with flake8 + run: flake8 ./patterns --count --show-source --statistics + continue-on-error: true + - name: Format check with isort and black + run: | + isort --profile black --check ./patterns + black --check ./patterns + continue-on-error: true + - name: Type check with mypy + run: mypy --ignore-missing-imports ./patterns || true + continue-on-error: true + - name: Run tests with pytest + run: | + pytest ./patterns + pytest --doctest-modules ./patterns || true + continue-on-error: true + - name: Check Python version compatibility + run: shopt -s globstar && pyupgrade --py312-plus ./patterns/**/*.py + continue-on-error: true + - name: Run tox + run: tox + continue-on-error: true diff --git a/config_backup/.coveragerc b/config_backup/.coveragerc new file mode 100644 index 00000000..98306ea9 --- /dev/null +++ b/config_backup/.coveragerc @@ -0,0 +1,25 @@ +[run] +branch = True + +[report] +; Regexes for lines to exclude from consideration +exclude_also = + ; Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + ; Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + ; Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + ; Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + +ignore_errors = True + +[html] +directory = coverage_html_report \ No newline at end of file diff --git a/setup.cfg b/config_backup/setup.cfg similarity index 100% rename from setup.cfg rename to config_backup/setup.cfg diff --git a/tox.ini b/config_backup/tox.ini similarity index 85% rename from tox.ini rename to config_backup/tox.ini index 7c23885f..36e2577e 100644 --- a/tox.ini +++ b/config_backup/tox.ini @@ -1,13 +1,17 @@ [tox] -envlist = py310,py312,cov-report +envlist = py312,cov-report skip_missing_interpreters = true - +usedevelop = true [testenv] setenv = COVERAGE_FILE = .coverage.{envname} deps = -r requirements-dev.txt +allowlist_externals = + pytest + flake8 + mypy commands = flake8 --exclude="venv/,.tox/" patterns/ ; `randomly-seed` option from `pytest-randomly` helps with deterministic outputs for examples like `other/blackboard.py` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..57f6fbe7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "patterns" +description = "A collection of design patterns and idioms in Python." +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.optional-dependencies] +dev = ["pytest", "pytest-cov", "pytest-randomly", "flake8", "mypy", "coverage"] + +[tool.setuptools] +packages = ["patterns"] + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::Warning:.*test class 'TestRunner'.*" +] +# Adding settings from tox.ini for pytest +testpaths = ["tests"] +#testpaths = ["tests", "patterns"] +python_files = ["test_*.py", "*_test.py"] +# Enable doctest discovery in patterns directory +addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing" +doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"] +log_level = "INFO" + +[tool.coverage.run] +branch = true +source = ["./"] +#source = ["patterns"] +# Ensure coverage data is collected properly +relative_files = true +parallel = true +dynamic_context = "test_function" +data_file = ".coverage" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + "def __repr__", + "if self\\.debug", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod" +] +ignore_errors = true + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.mypy] +python_version = "3.12" +ignore_missing_imports = true + +[tool.flake8] +max-line-length = 120 +ignore = ["E266", "E731", "W503"] +exclude = ["venv*"] + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py312,cov-report +skip_missing_interpreters = true +usedevelop = true + +#[testenv] +#setenv = +# COVERAGE_FILE = .coverage.{envname} +#deps = +# -r requirements-dev.txt +#commands = +# flake8 --exclude="venv/,.tox/" patterns/ +# coverage run -m pytest --randomly-seed=1234 --doctest-modules patterns/ +# coverage run -m pytest -s -vv --cov=patterns/ --log-level=INFO tests/ + +#[testenv:cov-report] +#setenv = +# COVERAGE_FILE = .coverage +#deps = coverage +#commands = +# coverage combine +# coverage report +#""" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 0de4748b..4aaa81f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,9 @@ --e . - -pytest~=6.2.0 -pytest-cov~=2.11.0 -pytest-randomly~=3.1.0 -black>=20.8b1 -isort~=5.7.0 -flake8~=3.8.0 \ No newline at end of file +mypy +pyupgrade +pytest>=6.2.0 +pytest-cov>=2.11.0 +pytest-randomly>=3.1.0 +black>=25.1.0 +isort>=5.7.0 +flake8>=7.1.0 +tox>=4.25.0 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b From 879ac0107f7f0005767d0e67c1555f54515c10ae Mon Sep 17 00:00:00 2001 From: Chris Dorsman <39407105+cdorsman@users.noreply.github.com> Date: Wed, 7 May 2025 17:49:35 +0200 Subject: [PATCH 127/127] Mvc add typing (#441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Removed old Python versions * Added typing * Fixed bug * Removed bugs and added more types * Fixed bug on check if controller is defined * removed object definition from routes * I fixed a bug * Ädded comments and lost types * Fixed types for Router * Fixed lines * yeah sure * List dammit! * . * oops * . --- patterns/structural/mvc.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index e06d16c4..24b0017a 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -6,12 +6,13 @@ from abc import ABC, abstractmethod from inspect import signature from sys import argv +from typing import Any class Model(ABC): """The Model is the data layer of the application.""" @abstractmethod - def __iter__(self): + def __iter__(self) -> Any: pass @abstractmethod @@ -43,7 +44,7 @@ def __str__(self) -> str: item_type = "product" - def __iter__(self): + def __iter__(self) -> Any: yield from self.products def get(self, product: str) -> dict: @@ -56,7 +57,7 @@ def get(self, product: str) -> dict: class View(ABC): """The View is the presentation layer of the application.""" @abstractmethod - def show_item_list(self, item_type: str, item_list: dict) -> None: + def show_item_list(self, item_type: str, item_list: list) -> None: pass @abstractmethod @@ -72,7 +73,7 @@ def item_not_found(self, item_type: str, item_name: str) -> None: class ConsoleView(View): """The View is the presentation layer of the application.""" - def show_item_list(self, item_type: str, item_list: dict) -> None: + def show_item_list(self, item_type: str, item_list: list) -> None: print(item_type.upper() + " LIST:") for item in item_list: print(item) @@ -112,13 +113,12 @@ def show_item_information(self, item_name: str) -> None: Show information about a {item_type} item. :param str item_name: the name of the {item_type} item to show information about """ + item_type: str = self.model.item_type try: - item_info: str = self.model.get(item_name) + item_info: dict = self.model.get(item_name) except Exception: - item_type: str = self.model.item_type self.view.item_not_found(item_type, item_name) else: - item_type: str = self.model.item_type self.view.show_item_information(item_type, item_name, item_info) @@ -127,7 +127,12 @@ class Router: def __init__(self): self.routes = {} - def register(self, path: str, controller_class: Controller, model_class: Model, view_class: View) -> None: + def register( + self, + path: str, + controller_class: type[Controller], + model_class: type[Model], + view_class: type[View]) -> None: model_instance: Model = model_class() view_instance: View = view_class() self.routes[path] = controller_class(model_instance, view_instance)