Skip to content

Abstract factory fixes #442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions patterns/creational/abstract_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ def main() -> None:


if __name__ == "__main__":
animals = ['dog', 'cat']
random_animal = random.choice(animals)

shop = PetShop(random_animal)
import doctest

Expand Down
71 changes: 38 additions & 33 deletions patterns/other/blackboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,30 @@
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) -> None:
self.blackboard = blackboard

@property
@abstractmethod
def is_eager_to_contribute(self) -> int:
raise NotImplementedError("Must provide implementation in subclass.")

@abstractmethod
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 = []
self.common_state = {
"problems": 0,
"suggestions": 0,
Expand All @@ -30,6 +45,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

Expand All @@ -45,21 +61,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
Expand All @@ -72,6 +78,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)
Expand All @@ -84,6 +94,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
Expand All @@ -107,21 +120,13 @@ def main():

>>> from pprint import pprint
>>> pprint(contributions)
['Student',
'Student',
'Student',
'Student',
'Scientist',
'Student',
'Student',
'Student',
'Scientist',
'Student',
'Scientist',
'Student',
'Student',
'Scientist',
'Professor']
['Student',
'Scientist',
'Student',
'Scientist',
'Student',
'Scientist',
'Professor']
"""


Expand Down
72 changes: 40 additions & 32 deletions patterns/structural/mvc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,30 @@


class Model(ABC):
"""The Model is the data layer of the application."""
@abstractmethod
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


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."""

def __str__(self):
def __str__(self) -> str:
return f"{self:.2f}"

products = {
Expand All @@ -44,92 +46,98 @@ 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:
raise KeyError(str(e) + " not in the model's item list.")


class View(ABC):
"""The View is the presentation layer of the application."""
@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: dict) -> 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: str, item_name: str) -> None:
pass


class ConsoleView(View):
def show_item_list(self, item_type, item_list):
"""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:
print(item)
print("")

@staticmethod
def capitalizer(string):
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):
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():
printout += ", " + self.capitalizer(str(key)) + ": " + str(value)
printout += "\n"
print(printout)

def item_not_found(self, item_type, item_name):
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
"""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):
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: str) -> None:
"""
Show information about a {item_type} item.
: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, controller, model, view):
model = model()
view = view()
self.routes[path] = controller(model, view)
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):
def resolve(self, path: str) -> Controller:
if self.routes.get(path):
controller = self.routes[path]
controller: Controller = self.routes[path]
return controller
else:
return None
raise KeyError(f"No controller registered for path '{path}'")


def main():
Expand Down Expand Up @@ -168,13 +176,13 @@ def main():
if __name__ == "__main__":
router = Router()
router.register("products", Controller, ProductModel, ConsoleView)
controller = router.resolve(argv[1])
controller: 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
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:
Expand All @@ -185,7 +193,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()
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ filterwarnings =
ignore:.*test class 'TestRunner'.*:Warning

[mypy]
python_version = 3.8
python_version = 3.12
ignore_missing_imports = True
9 changes: 3 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py38,py39,py310,cov-report
envlist = py310,py312,cov-report
skip_missing_interpreters = true


Expand Down
Loading