Skip to content

Mvc add typing #441

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

Merged
merged 15 commits into from
May 7, 2025
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
83 changes: 48 additions & 35 deletions patterns/structural/mvc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,34 @@
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
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 @@ -41,95 +44,105 @@ def __str__(self):

item_type = "product"

def __iter__(self):
def __iter__(self) -> Any:
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: list) -> 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: list) -> 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
"""
item_type: str = self.model.item_type
try:
item_info = self.model.get(item_name)
item_info: dict = self.model.get(item_name)
except Exception:
item_type = self.model.item_type
self.view.item_not_found(item_type, item_name)
else:
item_type = 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 resolve(self, path):
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)

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 +181,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 +198,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