Skip to content

Commit 543a86a

Browse files
committed
Apply hardening to lesson files
1 parent 953c79d commit 543a86a

19 files changed

Lines changed: 128 additions & 106 deletions

‎ultimatepython/advanced/async.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class JobRecord:
3232
started_at: datetime
3333

3434

35-
def _is_valid_record(record):
35+
def _is_valid_record(record: JobRecord) -> bool:
3636
"""Check whether job record is valid or not."""
3737
return record.queued_at < record.started_at
3838

@@ -94,7 +94,7 @@ async def advanced_async_patterns() -> None:
9494
"""Demonstrate advanced asyncio patterns."""
9595

9696
# Task Groups - structured concurrency (Python 3.11+)
97-
async def task_group_example():
97+
async def task_group_example() -> None:
9898
try:
9999
async with asyncio.TaskGroup() as tg:
100100
# Start multiple tasks in a group
@@ -112,7 +112,7 @@ async def task_group_example():
112112
# Semaphores for limiting concurrency
113113
semaphore = asyncio.Semaphore(3) # Allow max 3 concurrent operations
114114

115-
async def limited_concurrency_job(job_id: str):
115+
async def limited_concurrency_job(job_id: str) -> str:
116116
async with semaphore:
117117
# Only 3 jobs can execute this section at once
118118
await asyncio.sleep(_DELAY_SMALL)
@@ -142,7 +142,7 @@ async def limited_concurrency_job(job_id: str):
142142
assert successes_found == 3 # Three jobs succeeded
143143

144144
# Timeouts and cancellation
145-
async def slow_job():
145+
async def slow_job() -> str:
146146
await asyncio.sleep(1.0) # Takes 1 second
147147
return "slow_result"
148148

@@ -160,7 +160,7 @@ async def slow_job():
160160
# Schedule callback on the event loop
161161
callback_result = None
162162

163-
def sync_callback():
163+
def sync_callback() -> None:
164164
nonlocal callback_result
165165
callback_result = "callback_executed"
166166

@@ -170,7 +170,7 @@ def sync_callback():
170170
assert callback_result == "callback_executed"
171171

172172
# Shielding tasks from cancellation
173-
async def important_task():
173+
async def important_task() -> str:
174174
await asyncio.sleep(_DELAY_SMALL)
175175
return "important_result"
176176

‎ultimatepython/advanced/benchmark.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@
1515
_SLEEP_DURATION = 0.001
1616

1717

18-
def finish_slower():
18+
def finish_slower() -> None:
1919
"""Finish slower by sleeping more."""
2020
for _ in range(20):
2121
time.sleep(_SLEEP_DURATION)
2222

2323

24-
def finish_faster():
24+
def finish_faster() -> None:
2525
"""Finish faster by sleeping less."""
2626
for _ in range(10):
2727
time.sleep(_SLEEP_DURATION)
2828

2929

30-
def main():
30+
def main() -> None:
3131
# Create a profile instance
3232
profile = cProfile.Profile()
3333

‎ultimatepython/advanced/data_format.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,17 @@ class Note:
6666
body: str
6767

6868
@classmethod
69-
def from_data(cls, data):
69+
def from_data(cls, data: dict[str, str]) -> "Note":
7070
"""Create note from dictionary data."""
7171
return cls(**data)
7272

7373
@classmethod
74-
def fields(cls):
74+
def fields(cls) -> tuple[str, ...]:
7575
"""Get field names to simplify parsing logic."""
7676
return tuple(field.name for field in fields(cls))
7777

7878

79-
def main():
79+
def main() -> None:
8080
# Let's use `json.load` to parse note data from a JSON file
8181
# https://docs.python.org/3/library/json.html
8282
json_content = json.load(StringIO(_JSON_DATA))
@@ -86,7 +86,7 @@ def main():
8686
# Let's use `ElementTree.parse` to parse note data from a XML file
8787
# https://docs.python.org/3/library/xml.html
8888
tree = ETree.parse(StringIO(_XML_DATA))
89-
xml_notes = [Note.from_data({field: note_el.findtext(field) for field in Note.fields()}) for note_el in tree.getroot()]
89+
xml_notes = [Note.from_data({field: note_el.findtext(field) or "" for field in Note.fields()}) for note_el in tree.getroot()]
9090
assert all(isinstance(note, Note) for note in xml_notes)
9191

9292
# Let's use `csv.DictReader` to parse note data from a CSV file

‎ultimatepython/advanced/meta_class.py‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
from abc import ABC
8+
from typing import Any
89

910

1011
class ModelMeta(type):
@@ -34,8 +35,13 @@ class ModelMeta(type):
3435

3536
# Model table registry
3637
tables: dict[str, "ModelTable"] = {}
38+
model_name: str | None
39+
model_fields: dict[str, "BaseField"]
40+
model_table: "ModelTable" | None
3741

38-
def __new__(mcs, name, bases, attrs):
42+
def __new__(
43+
mcs, name: str, bases: tuple[type, ...], attrs: dict[str, Any]
44+
) -> "ModelMeta":
3945
"""Factory for modifying the defined class at runtime.
4046
4147
Here are the following steps that we take:
@@ -64,7 +70,8 @@ def __new__(mcs, name, bases, attrs):
6470

6571
# Fill model fields from the parent classes (left-to-right)
6672
for base in bases:
67-
kls.model_fields.update(base.model_fields)
73+
if isinstance(base, ModelMeta):
74+
kls.model_fields.update(base.model_fields)
6875

6976
# Fill model fields from itself
7077
kls.model_fields.update({field_name: field_obj for field_name, field_obj in attrs.items() if isinstance(field_obj, BaseField)})
@@ -83,15 +90,15 @@ def __new__(mcs, name, bases, attrs):
8390
return kls
8491

8592
@property
86-
def is_registered(cls):
93+
def is_registered(cls) -> bool:
8794
"""Check if the model's name is valid and exists in the registry."""
88-
return cls.model_name and cls.model_name in cls.tables
95+
return bool(cls.model_name and cls.model_name in cls.tables)
8996

9097

9198
class ModelTable:
9299
"""Model table."""
93100

94-
def __init__(self, table_name, table_fields):
101+
def __init__(self, table_name: str, table_fields: dict[str, "BaseField"]) -> None:
95102
self.table_name = table_name
96103
self.table_fields = table_fields
97104

@@ -142,7 +149,7 @@ class AddressModel(BaseModel):
142149
zip_code = CharField()
143150

144151

145-
def main():
152+
def main() -> None:
146153
# Real models are given a name at runtime with `ModelMeta`
147154
assert UserModel.model_name == "user_rocks"
148155
assert AddressModel.model_name == "address"

‎ultimatepython/advanced/mixin.py‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class RequestHandler(ABC):
3636
"""
3737

3838
@abstractmethod
39-
def handle(self, request):
39+
def handle(self, request: Request) -> str:
4040
"""Handle incoming request."""
4141
raise NotImplementedError
4242

@@ -53,28 +53,28 @@ class helps if downstream developers typically implement request
5353

5454
template_suffix = ".template"
5555

56-
def handle(self, request):
56+
def handle(self, request: Request) -> str:
5757
template_name = self.get_template_name(request.url)
5858
if not self.is_valid_template(template_name):
5959
return self.handle_invalid_template(request)
6060
return self.render_template(template_name)
6161

6262
@abstractmethod
63-
def get_template_name(self, request_url):
63+
def get_template_name(self, request_url: str) -> str:
6464
"""Get template name."""
6565
raise NotImplementedError
6666

67-
def is_valid_template(self, template_name):
67+
def is_valid_template(self, template_name: str) -> bool:
6868
"""Check if template name is valid."""
6969
return template_name.endswith(self.template_suffix)
7070

7171
@staticmethod
72-
def handle_invalid_template(request):
72+
def handle_invalid_template(request: Request) -> str:
7373
"""Handle request for invalid template."""
7474
return f"<p>Invalid entry for {request.url}</p>"
7575

7676
@abstractmethod
77-
def render_template(self, template_name):
77+
def render_template(self, template_name: str) -> str:
7878
"""Render contents of specified template name."""
7979
raise NotImplementedError
8080

@@ -90,18 +90,18 @@ class AuthHandlerMixin(RequestHandler):
9090
of another mixin in a concrete class MRO.
9191
"""
9292

93-
def handle(self, request):
93+
def handle(self, request: Request) -> str:
9494
if not self.is_valid_user(request.user):
9595
return self.handle_invalid_user(request)
96-
return super().handle(request)
96+
return super().handle(request) # type: ignore[safe-super]
9797

9898
@abstractmethod
99-
def is_valid_user(self, request_user):
99+
def is_valid_user(self, request_user: str) -> bool:
100100
"""Check if user is valid."""
101101
raise NotImplementedError
102102

103103
@staticmethod
104-
def handle_invalid_user(request):
104+
def handle_invalid_user(request: Request) -> str:
105105
"""Handle request for invalid user."""
106106
return f"<p>Access denied for {request.url}</p>"
107107

@@ -114,16 +114,16 @@ class TemplateFolderHandler(TemplateHandlerMixin):
114114
for the `handle` method.
115115
"""
116116

117-
def __init__(self, template_dir):
117+
def __init__(self, template_dir: dict[str, str]) -> None:
118118
self.template_dir = template_dir
119119

120-
def get_template_name(self, request_url):
120+
def get_template_name(self, request_url: str) -> str:
121121
return request_url[1:]
122122

123-
def is_valid_template(self, template_name):
123+
def is_valid_template(self, template_name: str) -> bool:
124124
return super().is_valid_template(template_name) and template_name in self.template_dir
125125

126-
def render_template(self, template_name):
126+
def render_template(self, template_name: str) -> str:
127127
return self.template_dir[template_name]
128128

129129

@@ -135,15 +135,15 @@ class but also gets authentication for free just by implementing
135135
the abstract method of the authentication mixin.
136136
"""
137137

138-
def __init__(self, admin_users, template_dir):
138+
def __init__(self, admin_users: set[str], template_dir: dict[str, str]) -> None:
139139
super().__init__(template_dir)
140140
self.admin_users = admin_users
141141

142-
def is_valid_user(self, request_user):
142+
def is_valid_user(self, request_user: str) -> bool:
143143
return request_user in self.admin_users
144144

145145

146-
def main():
146+
def main() -> None:
147147
# Handle requests with simple template handler
148148
simple_dir = {"welcome.template": "<p>Hello world</p>", "about.template": "<p>About me</p>"}
149149
simple_handler = TemplateFolderHandler(simple_dir)

‎ultimatepython/advanced/mocking.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,28 @@ class AppServer:
2727
is lightweight, so it's okay to mock this.
2828
"""
2929

30-
def __init__(self, host, port, proto):
30+
def __init__(self, host: str, port: int, proto: str) -> None:
3131
self._host = host
3232
self._port = port
3333
self._proto = proto
3434
self._pid = -1
3535

3636
@property
37-
def endpoint(self):
37+
def endpoint(self) -> str:
3838
"""Get application server endpoint URL."""
3939
return f"{self._proto}://{self._host}:{self._port}"
4040

4141
@property
42-
def pid(self):
42+
def pid(self) -> int:
4343
"""Get application server process ID."""
4444
return self._pid
4545

4646
@property
47-
def started(self):
47+
def started(self) -> bool:
4848
"""Check if application server is started."""
4949
return self.pid > 0
5050

51-
def start(self):
51+
def start(self) -> str:
5252
"""Start application server."""
5353
if self.started:
5454
return _START_FAILURE
@@ -61,17 +61,17 @@ class FakeServer(AppServer):
6161
"""Subclass parent and fake some routines."""
6262

6363
@property
64-
def endpoint(self):
64+
def endpoint(self) -> str:
6565
"""Mock output of endpoint URL."""
6666
return _FAKE_BASE_URL
6767

6868
@property
69-
def pid(self):
69+
def pid(self) -> int:
7070
"""Mock output of process ID."""
7171
return _FAKE_PID
7272

7373

74-
def main():
74+
def main() -> None:
7575
# This is the original class instance and it works as expected
7676
app_server = AppServer("localhost", 8000, _PROTOCOL_HTTP)
7777
assert app_server.endpoint == f"{_PROTOCOL_HTTP}://localhost:8000"

‎ultimatepython/advanced/mro.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
class BasePlayer:
1010
"""Base player."""
1111

12-
def ping(self):
12+
def ping(self) -> str:
1313
return "ping"
1414

15-
def pong(self):
15+
def pong(self) -> str:
1616
return "pong"
1717

1818

1919
class PongPlayer(BasePlayer):
2020
"""Pong player."""
2121

22-
def pong(self):
22+
def pong(self) -> str:
2323
return "PONG"
2424

2525

@@ -43,11 +43,11 @@ class ConfusedPlayer(PongPlayer, NeutralPlayer):
4343
https://www.python.org/download/releases/2.3/mro/
4444
"""
4545

46-
def ping(self):
46+
def ping(self) -> str:
4747
"""Override `ping` method."""
4848
return "pINg"
4949

50-
def ping_pong(self):
50+
def ping_pong(self) -> list[str]:
5151
"""Run `ping` and `pong` in different ways."""
5252
return [self.ping(), super().ping(), self.pong(), super().pong()]
5353

@@ -63,11 +63,11 @@ class IndecisivePlayer(NeutralPlayer, PongPlayer):
6363
as this bypasses the default method resolution process.
6464
"""
6565

66-
def pong(self):
66+
def pong(self) -> str:
6767
"""Override `pong` method."""
6868
return "pONg"
6969

70-
def ping_pong(self):
70+
def ping_pong(self) -> list[str]:
7171
"""Run `ping` and `pong` in different ways."""
7272
return [
7373
self.ping(),
@@ -77,7 +77,7 @@ def ping_pong(self):
7777
]
7878

7979

80-
def main():
80+
def main() -> None:
8181
# `ConfusedPlayer` methods are resolved from child to parent like this
8282
assert ConfusedPlayer.mro() == [ConfusedPlayer, PongPlayer, NeutralPlayer, BasePlayer, object]
8383

0 commit comments

Comments
 (0)