Skip to content

Commit eb30a4b

Browse files
authored
Add type annotations and mypy type-checking (#2)
* annotate src code * add ruff rules for annotations etc, fixed related source * add and configure mypy * enable mypy in workflow pipeline * add pytest to pre-commit pipeline
1 parent 45308bd commit eb30a4b

File tree

9 files changed

+1410
-513
lines changed

9 files changed

+1410
-513
lines changed

.github/workflows/pipeline.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ jobs:
8080
if: ${{ inputs.run-lint }}
8181
run: uv run ruff check --output-format=github .
8282

83-
# - name: Run typechecker
84-
# if: ${{ inputs.run-typecheck }}
85-
# run: uv run mypy .
83+
- name: Run typechecker
84+
if: ${{ inputs.run-typecheck }}
85+
run: uv run mypy
8686

8787
- name: Run tests
8888
if: ${{ inputs.run-test }}

.pre-commit-config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,19 @@ repos:
2525
types: [python]
2626
args: ["run", "ruff", "check", "--fix"]
2727
always_run: true
28+
29+
- id: mypy
30+
name: MyPy Type-Checker
31+
entry: uv
32+
language: system
33+
types: [python]
34+
args: ["run", "mypy"]
35+
always_run: true
36+
37+
- id: pytest
38+
name: PyTest
39+
entry: uv
40+
language: system
41+
types: [python]
42+
args: ["run", "pytest"]
43+
always_run: true

pyproject.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,33 @@ pythonpath = [
7373
testpaths = [
7474
"tests",
7575
]
76+
77+
[tool.ruff]
78+
line-length = 80
79+
80+
[tool.ruff.lint]
81+
select = [
82+
"E", # pycodestyle errors
83+
"W", # pycodestyle warnings
84+
"F", # pyflakes
85+
"I", # isort
86+
"B", # flake8-bugbear
87+
"C4", # flake8-comprehensions
88+
"ANN", # annotations :)
89+
"D", # code documentation :)
90+
]
91+
ignore = ["F403", "F405"]
92+
93+
[tool.ruff.lint.pydocstyle]
94+
convention = "google"
95+
96+
[tool.mypy]
97+
python_version = "3.10"
98+
warn_return_any = true
99+
warn_unused_configs = true
100+
files = ["src", "tests"]
101+
exclude = [
102+
'(^|.*/)__pycache__/', # matches __pycache__ at any depth
103+
'(^|.*/).ruff_cache/',
104+
'(^|.*/).mypy_cache/',
105+
]

src/chordnet/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from .node import Node
1+
"""init.py: defines importable classes."""
22
from .address import Address
33
from .net import _Net
4+
from .node import Node
45

56
__all__=['Node', 'Address', '_Net']

src/chordnet/address.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# address.py
1+
"""address.py: class that represents a ring address."""
22

33
import hashlib
44

5+
56
class Address:
6-
"""
7-
Represents a network address with a unique key in a distributed system.
7+
"""Represents a network address with a unique key in a distributed system.
88
99
This class encapsulates the network location (IP and port) and a unique
1010
identifier (key) used for routing and comparison in Chord.
@@ -17,24 +17,28 @@ class Address:
1717
Provides methods for equality comparison and string representation.
1818
"""
1919
__slots__= ('key', 'ip', 'port')
20-
_M = 16
21-
_SPACE = 2 ** _M
20+
_M: int = 16
21+
_SPACE: int = 2 ** _M
2222

2323

24-
def __init__(self, ip, port):
24+
def __init__(self, ip: str, port: int) -> None:
25+
"""Creates a new Address object.
26+
27+
Args:
28+
ip: ip address of the node.
29+
port: port of the node.
30+
"""
2531
self.key = self._hash(f"{ip}:{port}")
2632
self.ip = ip
2733
self.port = port
2834

2935

3036

31-
32-
def _hash(self, key):
33-
"""
34-
Generates a consistent hash for identifiers.
37+
def _hash(self, key: str) -> int:
38+
"""Generates a consistent hash for identifiers.
3539
3640
Args:
37-
key (str): Input string to hash.
41+
key: Input string to hash.
3842
3943
Returns:
4044
int: Hashed identifier within the hash space.
@@ -43,14 +47,27 @@ def _hash(self, key):
4347

4448

4549

46-
def __eq__(self, other):
50+
def __eq__(self, other: object) -> bool:
51+
"""Checks for equality.
52+
53+
Args:
54+
other: other object to check.
55+
56+
Returns:
57+
true if the other address is identical, false otherwise.
58+
"""
4759
if not isinstance(other, Address):
48-
return False
60+
return NotImplemented
4961
return (self.ip == other.ip and
5062
self.port == other.port and
5163
self.key == other.key)
5264

5365

5466

55-
def __repr__(self):
67+
def __repr__(self) -> str:
68+
"""String representation of this address.
69+
70+
Returns:
71+
String format for this address.
72+
"""
5673
return f"{self.key}:{self.ip}:{self.port}"

src/chordnet/net.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1+
"""net.py: Class for handling networking operations for nodes."""
12
import socket
23
import sys
34
import threading
5+
from typing import Callable, Tuple
6+
7+
from .address import Address
8+
9+
callback = Callable[[str, list[str]], str| Address | None]
410

511
class _Net:
612

7-
def __init__(self, ip, port, request_handler):
13+
_ip: str
14+
_port: int
15+
_request_handler: callback
16+
_running: bool
17+
_network_thread: threading.Thread | None
18+
server_socket: socket.socket | None
19+
network_thread: threading.Thread | None
20+
21+
def __init__(self, ip: str, port: int, request_handler: callback) -> None:
822
self._ip = ip
923
self._port = port
1024
self._request_handler = request_handler
@@ -13,9 +27,8 @@ def __init__(self, ip, port, request_handler):
1327
self.server_socket = None
1428
self.network_thread = None
1529

16-
def start(self):
17-
"""
18-
Starts the Chord node's network listener.
30+
def start(self) -> None:
31+
"""Starts the Chord node's network listener.
1932
2033
Begins accepting incoming network connections in a separate thread.
2134
"""
@@ -33,9 +46,8 @@ def start(self):
3346

3447

3548

36-
def stop(self):
37-
"""
38-
Gracefully stops the Chord node's network listener.
49+
def stop(self) -> None:
50+
"""Gracefully stops the Chord node's network listener.
3951
4052
Closes the server socket and waits for the network thread to terminate.
4153
"""
@@ -47,9 +59,10 @@ def stop(self):
4759

4860

4961

50-
def send_request(self, dest_node, method, *args):
51-
"""
52-
Sends a network request to a specific node.
62+
def send_request(
63+
self, dest_node: Address, method: str, *args: object
64+
) -> str | None:
65+
"""Sends a network request to a specific node.
5366
5467
Args:
5568
dest_node (Address): The network address to send the request to
@@ -78,7 +91,7 @@ def send_request(self, dest_node, method, *args):
7891
sock.send(request.encode())
7992

8093
# Receive the response
81-
response = sock.recv(1024).decode()
94+
response: str = sock.recv(1024).decode()
8295

8396
return response
8497

@@ -95,15 +108,19 @@ def send_request(self, dest_node, method, *args):
95108

96109

97110

98-
def _listen_for_connections(self):
99-
"""
100-
Continuously listens for incoming network connections.
111+
def _listen_for_connections(self) -> None:
112+
"""Continuously listens for incoming network connections.
101113
102-
Accepts client connections and spawns a thread to handle each connection.
114+
Accepts client connections and spawns a thread to handle
115+
each connection.
103116
"""
104117
while self._running:
105118
try:
106-
client_socket, address = self.server_socket.accept()
119+
client_socket: socket.socket | None = None
120+
address: Tuple[str, int] | None = None
121+
122+
if self.server_socket:
123+
client_socket, address = self.server_socket.accept()
107124
# Handle each connection in a separate thread
108125
threading.Thread(
109126
target=self._handle_connection,
@@ -117,16 +134,15 @@ def _listen_for_connections(self):
117134

118135

119136

120-
def _handle_connection(self, client_socket):
121-
"""
122-
Processes an individual network connection.
137+
def _handle_connection(self, client_socket: socket.socket) -> None:
138+
"""Processes an individual network connection.
123139
124140
Args:
125141
client_socket (socket): The socket connection to handle.
126142
"""
127143
try:
128144
# Receive request
129-
request = client_socket.recv(1024).decode()
145+
request: str = client_socket.recv(1024).decode()
130146

131147
# Parse request
132148
method, *args = request.split(':')

0 commit comments

Comments
 (0)