Skip to content

Commit d067569

Browse files
committed
refactor(type-checking): static type checking with mypy of test code (along with prod code)
1 parent d357d6f commit d067569

File tree

8 files changed

+150
-72
lines changed

8 files changed

+150
-72
lines changed

docs/introduction.rst

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,27 @@ Introduction
33

44
In these documentation pages we present the `software_patterns` Python package and the motivation
55
for publishing it. The package currently host only a handful of software patterns, but they come with
6-
a test suite and full CI for local and remote integrations.
6+
a test suite and full CI for local and remote builds.
77

88

99
Why would this project be useful to you?
1010
========================================
1111

12+
To develop your code, while abstracting common software patterns away and focus on important business logic.
13+
------------------------------------------------------------------------------------------------------------
14+
15+
- To promote the DRY prinicipal in your codebase.
16+
- To promote the principal of Single Responsibility in your codebase.
17+
- To promote having Simple Units of Code in your codebase.
18+
- To keep technical debt low in your codebase.
19+
1220
To Learn:
1321
---------
1422

1523
- You may study the source code to find out more about software design patterns.
1624
- You may wanna learn how to make an open source code contribution and find this codebase a good place to start.
1725

1826

19-
To use the python package and its contained software patterns in your project:
20-
------------------------------------------------------------------------------
21-
22-
- To promote good software quality in your python codebase.
23-
- To promote the DRY prinicipal in your client code.
24-
- To promote the principal of Single Responsibility in your client code.
25-
- To promote having Simple Units of Code in your project.
26-
27-
2827
Why use this Python Patterns library?
2928
=====================================
3029

src/software_patterns/notification.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
"""
2626

2727
from abc import ABC, abstractmethod
28-
from typing import Generic, List, TypeVar
28+
from typing import Generic, List, TypeVar, Union
2929

3030
__all__ = ['Subject', 'Observer']
3131

3232

3333
StateType = TypeVar('StateType')
34+
StateVariableType = Union[StateType, None]
3435

3536

3637
class ObserverInterface(ABC):
@@ -125,7 +126,7 @@ class Subject(SubjectInterface, Generic[StateType]):
125126

126127
def __init__(self, *args, **kwargs):
127128
self._observers: List[ObserverInterface] = []
128-
self._state = None
129+
self._state = StateVariableType
129130

130131
def attach(self, observer: ObserverInterface) -> None:
131132
self._observers.append(observer)
@@ -142,7 +143,7 @@ def add(self, *observers):
142143
self._observers.extend(list(observers))
143144

144145
@property
145-
def state(self) -> StateType:
146+
def state(self) -> StateVariableType:
146147
"""Get the state of the Subject.
147148
148149
Returns:

src/software_patterns/singleton.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ class Singleton(type):
1313
1414
>>> class ObjectDict(metaclass=Singleton):
1515
... def __init__(self):
16-
... super().__init__()
1716
... self.objects = {}
1817
1918
>>> reg1 = ObjectDict()

tests/test_notification.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,49 @@
1+
import typing as t
2+
13
import pytest
24

5+
from software_patterns import Observer, Subject
6+
7+
# class SubjectLike(t.Protocol):
8+
# state: t.Any
9+
10+
# class ObserverLike(t.Protocol):
11+
# update: t.Callable[[SubjectLike, t.Any], None]
12+
313

4-
@pytest.fixture
5-
def subject():
6-
from software_patterns import Subject
14+
# @pytest.fixture
15+
# def subject():
16+
# from software_patterns import Subject
717

8-
return Subject
18+
# return Subject
919

1020

11-
@pytest.fixture
12-
def observer():
13-
from software_patterns import Observer
21+
# @pytest.fixture
22+
# def observer() -> t.Type[Observer]:
23+
# """Observer Abstract Class that acts as an Observer interface.
1424

15-
return Observer
25+
# The Client code is expected to implement the 'update' method of the
26+
# interface.
27+
# """
28+
# from software_patterns import Observer
1629

30+
# return Observer
1731

18-
def test_observers_sanity_test1(subject):
19-
subject1 = subject([])
20-
subject2 = subject([])
32+
33+
def test_observers_sanity_test():
34+
subject1: Subject = Subject([])
35+
subject2: Subject = Subject([])
2136
assert hasattr(subject1, '_observers')
2237
assert hasattr(subject2, '_observers')
2338
assert id(subject1._observers) != id(subject2._observers)
2439

2540

26-
def test_observer_as_constructor(observer):
41+
# def test_observer_as_constructor(observer: t.Type[Observer]):
42+
def test_observer_as_constructor():
43+
observer = Observer
44+
2745
with pytest.raises(TypeError) as instantiation_from_interface_error:
28-
_observer_instance = observer()
46+
_observer_instance = observer() # type: ignore[abstract]
2947

3048
import re
3149

@@ -38,26 +56,24 @@ def test_observer_as_constructor(observer):
3856
)
3957

4058

41-
def test_scenario(subject, observer):
59+
# def test_scenario(subject: t.Type[Subject], observer: t.Type[Observer]):
60+
def test_scenario():
61+
# Scenario 1
4262
# The client code.
43-
44-
print("------ Scenario 1 ------\n")
45-
46-
class ObserverA(observer):
63+
class ObserverA(Observer):
4764
def update(self, a_subject) -> None:
4865
print("ObserverA: Reacted to the event")
4966

50-
s1 = subject([])
67+
s1: Subject = Subject([])
5168
o1 = ObserverA()
5269
s1.attach(o1)
5370

5471
# business logic
5572
s1.state = 0
5673
s1.notify()
5774

58-
print("------ Scenario 2 ------\n")
59-
# example 2
60-
class Businessubject(subject):
75+
# Scenario 2
76+
class Businessubject(Subject):
6177
def some_business_logic(self) -> None:
6278
"""
6379
Usually, the subscription logic is only a fraction of what a Subject can
@@ -70,7 +86,7 @@ def some_business_logic(self) -> None:
7086
print(f"Subject: My state has just changed to: {self._state}")
7187
self.notify()
7288

73-
class ObserverB(observer):
89+
class ObserverB(Observer):
7490
def update(self, a_subject) -> None:
7591
if a_subject.state == 0 or a_subject.state >= 2:
7692
print("ObserverB: Reacted to the event")

tests/test_proxy.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import pytest
22

33

4-
@pytest.fixture
5-
def proxy_module():
6-
from software_patterns import proxy
7-
8-
return proxy
9-
10-
114
@pytest.fixture
125
def dummy_handle():
136
def handle(self, *args, **kwargs):
@@ -16,11 +9,12 @@ def handle(self, *args, **kwargs):
169
return handle
1710

1811

19-
def test_proxy_behaviour(proxy_module, dummy_handle, capsys):
20-
prm = proxy_module
12+
def test_proxy_behaviour(dummy_handle, capsys):
13+
from typing import List
2114

22-
# replicate client code that wants to use the proxy pattern
15+
from software_patterns import proxy as prm
2316

17+
# replicate client code that wants to use the proxy pattern
2418
# Derive from class RealSubject or use 'python overloading' (use a class
2519
# that has a 'request' method with the same signature as ReadSubject.request)
2620
class ClientSubject(prm.ProxySubject):
@@ -57,7 +51,7 @@ def _dummy_callback(*args, **kwargs):
5751
# use proxy in a scenario
5852

5953
# First test what happens without using proxy
60-
args = [1, 2]
54+
args: List = [1, 2]
6155
kwargs = {'k1': 'v1'}
6256
result = real_subject.request(*args, **kwargs)
6357

@@ -74,6 +68,7 @@ def _dummy_callback(*args, **kwargs):
7468
result = proxy.request(*args, **kwargs)
7569

7670
captured = capsys.readouterr()
71+
7772
assert (
7873
captured.out
7974
== dummy_handle(*list([proxy, 'before'] + args), **kwargs)

tests/test_singleton.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
11
import typing as t
22

3+
import pytest
4+
35
from software_patterns import Singleton
46

57

6-
def test_singleton():
8+
def test_singleton(assert_same_objects):
79
class MySingleton(metaclass=Singleton):
8-
def __init__(self, data: t.Mapping):
10+
def __init__(self, data: t.MutableMapping):
911
self.data = data
1012

1113
instance_1 = MySingleton({'a': 1})
1214
instance_2 = MySingleton({'b': 2})
1315

14-
assert id(instance_1) == id(instance_2)
16+
assert_same_objects(instance_1, instance_2)
17+
1518
assert instance_1.data['a'] == instance_2.data['a'] == 1
1619
assert 'b' not in instance_1.data
1720
assert 'b' not in instance_2.data
1821

1922
instance_1.data['c'] = 0
2023

2124
assert instance_2.data['c'] == 0
25+
26+
27+
@pytest.fixture
28+
def assert_same_objects():
29+
def _assert_same_objects(obj1, obj2):
30+
assert id(obj1) == id(obj2)
31+
attributes_1 = list(dir(obj1))
32+
attributes_2 = list(dir(obj2))
33+
assert attributes_1 == attributes_2
34+
for attr_name in set(attributes_1).difference(
35+
{
36+
'__delattr__',
37+
'__init__',
38+
'__gt__',
39+
'__ne__',
40+
'__dir__',
41+
'__repr__',
42+
'__setattr__',
43+
'__le__',
44+
'__subclasshook__',
45+
'__str__',
46+
'__format__',
47+
'__lt__',
48+
'__eq__',
49+
'__reduce_ex__',
50+
'__getattribute__',
51+
'__reduce__',
52+
'__init_subclass__',
53+
'__hash__',
54+
'__sizeof__',
55+
'__ge__',
56+
}
57+
):
58+
print(attr_name)
59+
assert getattr(obj1, attr_name) == getattr(obj2, attr_name)
60+
assert id(getattr(obj1, attr_name)) == id(getattr(obj2, attr_name))
61+
62+
return _assert_same_objects

0 commit comments

Comments
 (0)