Skip to content

Commit 07a5f33

Browse files
authored
Merge pull request #443 from cdorsman/abstract-factory-fixes
Abstract factory fixes
2 parents 75c27bf + 7db462e commit 07a5f33

File tree

8 files changed

+90
-79
lines changed

8 files changed

+90
-79
lines changed

.travis.yml

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
os: linux
2-
dist: focal
2+
dist: noble
33
language: python
44

55
jobs:
66
include:
7-
- python: "3.8"
8-
env: TOXENV=py38
9-
- python: "3.9"
10-
env: TOXENV=py39
7+
- python: "3.12"
8+
env: TOXENV=py312
119

1210
cache:
1311
- pip

lint.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ tox
1313
mypy --ignore-missing-imports "${source_dir}" || true
1414
pytest "${source_dir}"
1515
pytest --doctest-modules "${source_dir}" || true
16-
shopt -s globstar && pyupgrade --py37-plus ${source_dir}/*.py
16+
shopt -s globstar && pyupgrade --py312-plus ${source_dir}/*.py

patterns/creational/abstract_factory.py

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def main() -> None:
9090

9191

9292
if __name__ == "__main__":
93+
animals = [Dog, Cat]
94+
random_animal: Type[Pet] = random.choice(animals)
95+
9396
shop = PetShop(random_animal)
9497
import doctest
9598

patterns/other/blackboard.py

+38-33
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,30 @@
99
https://en.wikipedia.org/wiki/Blackboard_system
1010
"""
1111

12-
from __future__ import annotations
13-
14-
import abc
12+
from abc import ABC, abstractmethod
1513
import random
1614

1715

16+
class AbstractExpert(ABC):
17+
"""Abstract class for experts in the blackboard system."""
18+
@abstractmethod
19+
def __init__(self, blackboard) -> None:
20+
self.blackboard = blackboard
21+
22+
@property
23+
@abstractmethod
24+
def is_eager_to_contribute(self) -> int:
25+
raise NotImplementedError("Must provide implementation in subclass.")
26+
27+
@abstractmethod
28+
def contribute(self) -> None:
29+
raise NotImplementedError("Must provide implementation in subclass.")
30+
31+
1832
class Blackboard:
33+
"""The blackboard system that holds the common state."""
1934
def __init__(self) -> None:
20-
self.experts = []
35+
self.experts: list = []
2136
self.common_state = {
2237
"problems": 0,
2338
"suggestions": 0,
@@ -30,6 +45,7 @@ def add_expert(self, expert: AbstractExpert) -> None:
3045

3146

3247
class Controller:
48+
"""The controller that manages the blackboard system."""
3349
def __init__(self, blackboard: Blackboard) -> None:
3450
self.blackboard = blackboard
3551

@@ -45,21 +61,11 @@ def run_loop(self):
4561
return self.blackboard.common_state["contributions"]
4662

4763

48-
class AbstractExpert(metaclass=abc.ABCMeta):
49-
def __init__(self, blackboard: Blackboard) -> None:
50-
self.blackboard = blackboard
51-
52-
@property
53-
@abc.abstractmethod
54-
def is_eager_to_contribute(self):
55-
raise NotImplementedError("Must provide implementation in subclass.")
56-
57-
@abc.abstractmethod
58-
def contribute(self):
59-
raise NotImplementedError("Must provide implementation in subclass.")
60-
61-
6264
class Student(AbstractExpert):
65+
"""Concrete class for a student expert."""
66+
def __init__(self, blackboard) -> None:
67+
super().__init__(blackboard)
68+
6369
@property
6470
def is_eager_to_contribute(self) -> bool:
6571
return True
@@ -72,6 +78,10 @@ def contribute(self) -> None:
7278

7379

7480
class Scientist(AbstractExpert):
81+
"""Concrete class for a scientist expert."""
82+
def __init__(self, blackboard) -> None:
83+
super().__init__(blackboard)
84+
7585
@property
7686
def is_eager_to_contribute(self) -> int:
7787
return random.randint(0, 1)
@@ -84,6 +94,9 @@ def contribute(self) -> None:
8494

8595

8696
class Professor(AbstractExpert):
97+
def __init__(self, blackboard) -> None:
98+
super().__init__(blackboard)
99+
87100
@property
88101
def is_eager_to_contribute(self) -> bool:
89102
return True if self.blackboard.common_state["problems"] > 100 else False
@@ -107,21 +120,13 @@ def main():
107120
108121
>>> from pprint import pprint
109122
>>> pprint(contributions)
110-
['Student',
111-
'Student',
112-
'Student',
113-
'Student',
114-
'Scientist',
115-
'Student',
116-
'Student',
117-
'Student',
118-
'Scientist',
119-
'Student',
120-
'Scientist',
121-
'Student',
122-
'Student',
123-
'Scientist',
124-
'Professor']
123+
['Student',
124+
'Scientist',
125+
'Student',
126+
'Scientist',
127+
'Student',
128+
'Scientist',
129+
'Professor']
125130
"""
126131

127132

patterns/structural/mvc.py

+40-32
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,30 @@
99

1010

1111
class Model(ABC):
12+
"""The Model is the data layer of the application."""
1213
@abstractmethod
1314
def __iter__(self):
1415
pass
1516

1617
@abstractmethod
17-
def get(self, item):
18+
def get(self, item: str) -> dict:
1819
"""Returns an object with a .items() call method
1920
that iterates over key,value pairs of its information."""
2021
pass
2122

2223
@property
2324
@abstractmethod
24-
def item_type(self):
25+
def item_type(self) -> str:
2526
pass
2627

2728

2829
class ProductModel(Model):
30+
"""The Model is the data layer of the application."""
2931
class Price(float):
3032
"""A polymorphic way to pass a float with a particular
3133
__str__ functionality."""
3234

33-
def __str__(self):
35+
def __str__(self) -> str:
3436
return f"{self:.2f}"
3537

3638
products = {
@@ -44,92 +46,98 @@ def __str__(self):
4446
def __iter__(self):
4547
yield from self.products
4648

47-
def get(self, product):
49+
def get(self, product: str) -> dict:
4850
try:
4951
return self.products[product]
5052
except KeyError as e:
5153
raise KeyError(str(e) + " not in the model's item list.")
5254

5355

5456
class View(ABC):
57+
"""The View is the presentation layer of the application."""
5558
@abstractmethod
56-
def show_item_list(self, item_type, item_list):
59+
def show_item_list(self, item_type: str, item_list: dict) -> None:
5760
pass
5861

5962
@abstractmethod
60-
def show_item_information(self, item_type, item_name, item_info):
63+
def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None:
6164
"""Will look for item information by iterating over key,value pairs
6265
yielded by item_info.items()"""
6366
pass
6467

6568
@abstractmethod
66-
def item_not_found(self, item_type, item_name):
69+
def item_not_found(self, item_type: str, item_name: str) -> None:
6770
pass
6871

6972

7073
class ConsoleView(View):
71-
def show_item_list(self, item_type, item_list):
74+
"""The View is the presentation layer of the application."""
75+
def show_item_list(self, item_type: str, item_list: dict) -> None:
7276
print(item_type.upper() + " LIST:")
7377
for item in item_list:
7478
print(item)
7579
print("")
7680

7781
@staticmethod
78-
def capitalizer(string):
82+
def capitalizer(string: str) -> str:
83+
"""Capitalizes the first letter of a string and lowercases the rest."""
7984
return string[0].upper() + string[1:].lower()
8085

81-
def show_item_information(self, item_type, item_name, item_info):
86+
def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None:
87+
"""Will look for item information by iterating over key,value pairs"""
8288
print(item_type.upper() + " INFORMATION:")
8389
printout = "Name: %s" % item_name
8490
for key, value in item_info.items():
8591
printout += ", " + self.capitalizer(str(key)) + ": " + str(value)
8692
printout += "\n"
8793
print(printout)
8894

89-
def item_not_found(self, item_type, item_name):
95+
def item_not_found(self, item_type: str, item_name: str) -> None:
9096
print(f'That {item_type} "{item_name}" does not exist in the records')
9197

9298

9399
class Controller:
94-
def __init__(self, model, view):
95-
self.model = model
96-
self.view = view
100+
"""The Controller is the intermediary between the Model and the View."""
101+
def __init__(self, model_class: Model, view_class: View) -> None:
102+
self.model: Model = model_class
103+
self.view: View = view_class
97104

98-
def show_items(self):
105+
def show_items(self) -> None:
99106
items = list(self.model)
100107
item_type = self.model.item_type
101108
self.view.show_item_list(item_type, items)
102109

103-
def show_item_information(self, item_name):
110+
def show_item_information(self, item_name: str) -> None:
104111
"""
105112
Show information about a {item_type} item.
106113
:param str item_name: the name of the {item_type} item to show information about
107114
"""
108115
try:
109-
item_info = self.model.get(item_name)
116+
item_info: str = self.model.get(item_name)
110117
except Exception:
111-
item_type = self.model.item_type
118+
item_type: str = self.model.item_type
112119
self.view.item_not_found(item_type, item_name)
113120
else:
114-
item_type = self.model.item_type
121+
item_type: str = self.model.item_type
115122
self.view.show_item_information(item_type, item_name, item_info)
116123

117124

118125
class Router:
126+
"""The Router is the entry point of the application."""
119127
def __init__(self):
120128
self.routes = {}
121129

122-
def register(self, path, controller, model, view):
123-
model = model()
124-
view = view()
125-
self.routes[path] = controller(model, view)
130+
def register(self, path: str, controller_class: Controller, model_class: Model, view_class: View) -> None:
131+
model_instance: Model = model_class()
132+
view_instance: View = view_class()
133+
self.routes[path] = controller_class(model_instance, view_instance)
126134

127-
def resolve(self, path):
135+
def resolve(self, path: str) -> Controller:
128136
if self.routes.get(path):
129-
controller = self.routes[path]
137+
controller: Controller = self.routes[path]
130138
return controller
131139
else:
132-
return None
140+
raise KeyError(f"No controller registered for path '{path}'")
133141

134142

135143
def main():
@@ -168,13 +176,13 @@ def main():
168176
if __name__ == "__main__":
169177
router = Router()
170178
router.register("products", Controller, ProductModel, ConsoleView)
171-
controller = router.resolve(argv[1])
179+
controller: Controller = router.resolve(argv[1])
172180

173-
command = str(argv[2]) if len(argv) > 2 else None
174-
args = ' '.join(map(str, argv[3:])) if len(argv) > 3 else None
181+
action: str = str(argv[2]) if len(argv) > 2 else ""
182+
args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else ""
175183

176-
if hasattr(controller, command):
177-
command = getattr(controller, command)
184+
if hasattr(controller, action):
185+
command = getattr(controller, action)
178186
sig = signature(command)
179187

180188
if len(sig.parameters) > 0:
@@ -185,7 +193,7 @@ def main():
185193
else:
186194
command()
187195
else:
188-
print(f"Command {command} not found in the controller.")
196+
print(f"Command {action} not found in the controller.")
189197

190198
import doctest
191199
doctest.testmod()

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ filterwarnings =
99
ignore:.*test class 'TestRunner'.*:Warning
1010

1111
[mypy]
12-
python_version = 3.8
12+
python_version = 3.12
1313
ignore_missing_imports = True

setup.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@
55
packages=find_packages(),
66
description="A collection of design patterns and idioms in Python.",
77
classifiers=[
8-
"Programming Language :: Python :: 2",
9-
"Programming Language :: Python :: 2.7",
10-
"Programming Language :: Python :: 3",
11-
"Programming Language :: Python :: 3.6",
12-
"Programming Language :: Python :: 3.7",
13-
"Programming Language :: Python :: 3.8",
148
"Programming Language :: Python :: 3.9",
159
"Programming Language :: Python :: 3.10",
10+
"Programming Language :: Python :: 3.11",
11+
"Programming Language :: Python :: 3.12",
12+
"Programming Language :: Python :: 3.13",
1613
],
1714
)

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py38,py39,py310,cov-report
2+
envlist = py310,py312,cov-report
33
skip_missing_interpreters = true
44

55

0 commit comments

Comments
 (0)