Skip to content
Merged
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
10 changes: 5 additions & 5 deletions .github/workflows/lint_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
has_python_changes: ${{ steps.changed-files.outputs.has_python_changes }}
files: ${{ steps.changed-files.outputs.files }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
with:
fetch-depth: 0 # To get all history for git diff commands

Expand Down Expand Up @@ -71,15 +71,15 @@ jobs:
exit 0
fi

- uses: actions/checkout@v3
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: actions/setup-python@v4
- uses: actions/setup-python@v6
with:
python-version: 3.12

- uses: actions/cache@v3
- uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }}
Expand Down Expand Up @@ -273,7 +273,7 @@ jobs:
if: ${{ always() }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6

- name: Summarize results
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ jobs:
lint_python:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.12
- name: Install dependencies
Expand Down
4 changes: 4 additions & 0 deletions patterns/behavioral/chain_of_responsibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
As a variation some receivers may be capable of sending requests out
in several directions, forming a `tree of responsibility`.

*Examples in Python ecosystem:
Django Middleware: https://docs.djangoproject.com/en/stable/topics/http/middleware/
The middleware components act as a chain where each processes the request/response.

*TL;DR
Allow a request to pass down a chain of receivers until it is handled.
"""
Expand Down
2 changes: 1 addition & 1 deletion patterns/behavioral/memento.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

from copy import copy, deepcopy
from typing import Callable, List
from typing import Any, Callable, List, Type


def memento(obj: Any, deep: bool = False) -> Callable:
Expand Down
1 change: 0 additions & 1 deletion patterns/structural/mvc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

from abc import ABC, abstractmethod
from ProductModel import Price
from typing import Dict, List, Union, Any
from inspect import signature
from sys import argv
Expand Down
3 changes: 3 additions & 0 deletions pytest_local.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
addopts = -q
testpaths = tests
30 changes: 30 additions & 0 deletions tests/creational/test_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import unittest
from patterns.creational.factory import get_localizer, GreekLocalizer, EnglishLocalizer

class TestFactory(unittest.TestCase):
def test_get_localizer_greek(self):
localizer = get_localizer("Greek")
self.assertIsInstance(localizer, GreekLocalizer)
self.assertEqual(localizer.localize("dog"), "σκύλος")
self.assertEqual(localizer.localize("cat"), "γάτα")
# Test unknown word returns the word itself
self.assertEqual(localizer.localize("monkey"), "monkey")

def test_get_localizer_english(self):
localizer = get_localizer("English")
self.assertIsInstance(localizer, EnglishLocalizer)
self.assertEqual(localizer.localize("dog"), "dog")
self.assertEqual(localizer.localize("cat"), "cat")

def test_get_localizer_default(self):
# Test default argument
localizer = get_localizer()
self.assertIsInstance(localizer, EnglishLocalizer)

def test_get_localizer_unknown_language(self):
# Test fallback for unknown language if applicable,
# or just verify what happens.
# Based on implementation: localizers.get(language, EnglishLocalizer)()
# It defaults to EnglishLocalizer for unknown keys.
localizer = get_localizer("Spanish")
self.assertIsInstance(localizer, EnglishLocalizer)
16 changes: 16 additions & 0 deletions tests/fundamental/test_delegation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pytest

from patterns.fundamental.delegation_pattern import Delegator, Delegate


def test_delegator_delegates_attribute_and_call():
d = Delegator(Delegate())
assert d.p1 == 123
assert d.do_something("something") == "Doing something"
assert d.do_something("something", kw=", hi") == "Doing something, hi"


def test_delegator_missing_attribute_raises():
d = Delegator(Delegate())
with pytest.raises(AttributeError):
_ = d.p2
11 changes: 11 additions & 0 deletions tests/structural/test_facade.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from patterns.structural.facade import ComputerFacade


def test_computer_facade_start(capsys):
cf = ComputerFacade()
cf.start()
out = capsys.readouterr().out
assert "Freezing processor." in out
assert "Loading from 0x00 data:" in out
assert "Jumping to: 0x00" in out
assert "Executing." in out
20 changes: 20 additions & 0 deletions tests/structural/test_flyweight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from patterns.structural.flyweight import Card


def test_card_flyweight_identity_and_repr():
c1 = Card("9", "h")
c2 = Card("9", "h")
assert c1 is c2
assert repr(c1) == "<Card: 9h>"


def test_card_attribute_persistence_and_pool_clear():
Card._pool.clear()
c1 = Card("A", "s")
c1.temp = "t"
c2 = Card("A", "s")
assert hasattr(c2, "temp")

Card._pool.clear()
c3 = Card("A", "s")
assert not hasattr(c3, "temp")
67 changes: 67 additions & 0 deletions tests/structural/test_mvc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest

from patterns.structural.mvc import (
ProductModel,
ConsoleView,
Controller,
Router,
)


def test_productmodel_iteration_and_price_str():
pm = ProductModel()
items = list(pm)
assert set(items) == {"milk", "eggs", "cheese"}

info = pm.get("cheese")
assert info["quantity"] == 10
assert str(info["price"]) == "2.00"


def test_productmodel_get_raises_keyerror():
pm = ProductModel()
with pytest.raises(KeyError) as exc:
pm.get("unknown_item")
assert "not in the model's item list." in str(exc.value)


def test_consoleview_capitalizer_and_list_and_info(capsys):
view = ConsoleView()
# capitalizer
assert view.capitalizer("heLLo") == "Hello"

# show item list
view.show_item_list("product", ["x", "y"])
out = capsys.readouterr().out
assert "PRODUCT LIST:" in out
assert "x" in out and "y" in out

# show item information formatting
pm = ProductModel()
controller = Controller(pm, view)
controller.show_item_information("milk")
out = capsys.readouterr().out
assert "PRODUCT INFORMATION:" in out
assert "Name: milk" in out
assert "Price: 1.50" in out
assert "Quantity: 10" in out


def test_show_item_information_missing_calls_item_not_found(capsys):
view = ConsoleView()
pm = ProductModel()
controller = Controller(pm, view)

controller.show_item_information("arepas")
out = capsys.readouterr().out
assert 'That product "arepas" does not exist in the records' in out


def test_router_register_resolve_and_unknown():
router = Router()
router.register("products", Controller, ProductModel, ConsoleView)
controller = router.resolve("products")
assert isinstance(controller, Controller)

with pytest.raises(KeyError):
router.resolve("no-such-path")
Loading