Skip to content

Commit 9d6ba68

Browse files
authored
Add mypy type checker to GitHub Actions (#1056)
* Fix yaml formatting * Bump actions versions to latest * Bump pipenv version to latest * Add mypy lint action * Fix mypy lint errors * Suppress warnings about unchecked function bodies for now * Add deprecation comment * Log OperationalError exception traces
1 parent fac8a5b commit 9d6ba68

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+619
-276
lines changed

.github/workflows/doc.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ jobs:
99
deploy:
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v3
12+
- uses: actions/checkout@v4
1313

1414
- name: Setup Python
15-
uses: actions/setup-python@v4
15+
uses: actions/setup-python@v5
1616
with:
1717
python-version: '3.10'
1818

.github/workflows/lint.yml

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ jobs:
1313
isort:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v3
16+
- uses: actions/checkout@v4
1717

1818
- name: Setup Python
19-
uses: actions/setup-python@v4
19+
uses: actions/setup-python@v5
2020
with:
2121
python-version: '3.10'
2222

@@ -28,10 +28,10 @@ jobs:
2828
flake8:
2929
runs-on: ubuntu-latest
3030
steps:
31-
- uses: actions/checkout@v3
31+
- uses: actions/checkout@v4
3232

3333
- name: Setup Python
34-
uses: actions/setup-python@v4
34+
uses: actions/setup-python@v5
3535
with:
3636
python-version: '3.10'
3737

@@ -40,13 +40,40 @@ jobs:
4040
flake8_version: 6.0.0
4141
plugins: flake8-quotes~=3.3
4242

43+
mypy:
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- name: Create requirements file
49+
run: |
50+
pip install pipenv==2024.4.1
51+
pipenv requirements --dev > requirements.txt
52+
53+
# Mypy-check runs inside its own docker image
54+
- uses: jpetrucciani/mypy-check@master
55+
with:
56+
python_version: '3.10'
57+
requirements: |
58+
types-PyYAML
59+
types-PyMySQL
60+
types-cachetools
61+
requirements_file: requirements.txt
62+
mypy_flags: |
63+
--disable-error-code=annotation-unchecked
64+
--follow-untyped-imports
65+
--strict-equality
66+
--warn-redundant-casts
67+
--warn-unused-ignores
68+
path: server/ main.py
69+
4370
pipenv-verify:
4471
runs-on: ubuntu-latest
4572
steps:
46-
- uses: actions/checkout@v3
73+
- uses: actions/checkout@v4
4774

4875
- name: Setup Python
49-
uses: actions/setup-python@v4
76+
uses: actions/setup-python@v5
5077
with:
5178
python-version: '3.10'
5279

.github/workflows/test.yml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@ jobs:
5151
RABBITMQ_DEFAULT_VHOST: /
5252

5353
steps:
54-
- uses: actions/checkout@v3
54+
- uses: actions/checkout@v4
5555

5656
- name: Cache hypothesis examples
57-
uses: actions/cache@v3
57+
uses: actions/cache@v4
5858
with:
5959
path: .hypothesis
6060
key: .hypothesis
6161

6262
- name: Setup Python
63-
uses: actions/setup-python@v4
63+
uses: actions/setup-python@v5
6464
with:
6565
python-version: '3.10'
6666

@@ -81,7 +81,7 @@ jobs:
8181

8282
- name: Install dependencies with pipenv
8383
run: |
84-
pip install pipenv==2023.4.20
84+
pip install pipenv==2024.4.1
8585
pipenv sync --dev
8686
pipenv run pip install pytest-github-actions-annotate-failures
8787
@@ -105,14 +105,14 @@ jobs:
105105
run: PYTHONWARNINGS='error::UserWarning' pipenv run pdoc3 server >/dev/null
106106

107107
docker-build:
108-
runs-on: ubuntu-latest
109-
steps:
110-
- uses: actions/checkout@v3
108+
runs-on: ubuntu-latest
109+
steps:
110+
- uses: actions/checkout@v4
111111

112-
- name: Build docker image
113-
run: docker build --build-arg GITHUB_REF -t test_image .
112+
- name: Build docker image
113+
run: docker build --build-arg GITHUB_REF -t test_image .
114114

115-
- name: Test image
116-
run: |
117-
docker run --rm -d -p 8001:8001 test_image
118-
nc -z localhost 8001
115+
- name: Test image
116+
run: |
117+
docker run --rm -d -p 8001:8001 test_image
118+
nc -z localhost 8001

main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def drain_handler(sig: int, frame):
216216
stop_time = time.perf_counter()
217217
logger.info(
218218
"Total server uptime: %s",
219-
humanize.precisedelta(stop_time - startup_time)
219+
humanize.precisedelta(int(stop_time - startup_time))
220220
)
221221

222222
if shutdown_time is not None:

server/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
import asyncio
113113
import logging
114114
import time
115-
from typing import Optional
115+
from typing import Optional, cast
116116

117117
import server.metrics as metrics
118118

@@ -176,7 +176,7 @@ def __init__(
176176
self,
177177
name: str,
178178
database: FAFDatabase,
179-
loop: asyncio.BaseEventLoop,
179+
loop: asyncio.AbstractEventLoop,
180180
# For testing
181181
_override_services: Optional[dict[str, Service]] = None
182182
):
@@ -320,8 +320,8 @@ async def drain(self):
320320
"""
321321
Wait for all games to end.
322322
"""
323-
game_service: GameService = self.services["game_service"]
324-
broadcast_service: BroadcastService = self.services["broadcast_service"]
323+
game_service = cast(GameService, self.services["game_service"])
324+
broadcast_service = cast(BroadcastService, self.services["broadcast_service"])
325325
try:
326326
await asyncio.wait_for(
327327
game_service.drain_games(),

server/asyncio_extensions.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
Optional,
1717
Protocol,
1818
TypeVar,
19-
cast,
2019
overload
2120
)
2221

@@ -27,7 +26,7 @@
2726
T = TypeVar("T")
2827

2928

30-
class AsyncLock(Protocol, AsyncContextManager["AsyncLock"]):
29+
class AsyncLock(Protocol, AsyncContextManager[None]):
3130
def locked(self) -> bool: ...
3231
async def acquire(self) -> bool: ...
3332
def release(self) -> None: ...
@@ -153,7 +152,7 @@ async def wrapped(*args, **kwargs):
153152
nonlocal lock
154153

155154
if lock is None:
156-
lock = lock or cast(AsyncLock, asyncio.Lock())
155+
lock = lock or asyncio.Lock()
157156

158157
async with lock:
159158
return await function(*args, **kwargs)

server/broadcast_service.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import asyncio
2+
import logging
3+
from typing import TYPE_CHECKING, ClassVar, Optional
24

35
import humanize
46
from aio_pika import DeliveryMode
@@ -12,13 +14,18 @@
1214
from .player_service import PlayerService
1315
from .timing import LazyIntervalTimer
1416

17+
if TYPE_CHECKING:
18+
from server import ServerInstance
19+
1520

1621
@with_logger
1722
class BroadcastService(Service):
1823
"""
1924
Broadcast updates about changed entities.
2025
"""
2126

27+
_logger: ClassVar[logging.Logger]
28+
2229
def __init__(
2330
self,
2431
server: "ServerInstance",
@@ -30,7 +37,7 @@ def __init__(
3037
self.message_queue_service = message_queue_service
3138
self.game_service = game_service
3239
self.player_service = player_service
33-
self._report_dirties_event = None
40+
self._report_dirties_event: Optional[asyncio.Event] = None
3441

3542
async def initialize(self):
3643
# Using a lazy interval timer so that the intervals can be changed

server/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import logging
77
import os
88
import statistics
9-
from typing import Callable
9+
from typing import Callable, ClassVar, Iterable
1010

1111
import trueskill
1212
import yaml
@@ -27,7 +27,7 @@
2727
# see: http://forums.faforever.com/viewtopic.php?f=45&t=11698#p119599
2828
# Optimum values for ladder here, using them for global as well.
2929
trueskill.setup(mu=1500, sigma=500, beta=240, tau=10, draw_probability=0.10)
30-
MAP_POOL_RATING_SELECTION_FUNCTIONS = {
30+
MAP_POOL_RATING_SELECTION_FUNCTIONS: dict[str, Callable[[Iterable[float]], float]] = {
3131
"mean": statistics.mean,
3232
"min": min,
3333
"max": max,
@@ -36,6 +36,8 @@
3636

3737
@with_logger
3838
class ConfigurationStore:
39+
_logger: ClassVar[logging.Logger]
40+
3941
def __init__(self):
4042
"""
4143
Change default values here.

server/configuration_service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"""
44

55
import asyncio
6+
import logging
7+
from typing import ClassVar, Optional
68

79
from .config import config
810
from .core import Service
@@ -11,9 +13,11 @@
1113

1214
@with_logger
1315
class ConfigurationService(Service):
16+
_logger: ClassVar[logging.Logger]
17+
1418
def __init__(self) -> None:
1519
self._store = config
16-
self._task = None
20+
self._task: Optional[asyncio.Task] = None
1721

1822
async def initialize(self) -> None:
1923
self._task = asyncio.create_task(self._worker_loop())

server/control.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,34 @@
22
Tiny http server for introspecting state
33
"""
44

5+
import logging
56
import socket
7+
from typing import TYPE_CHECKING, ClassVar, Optional, cast
68

79
from aiohttp import web
810

911
from .config import config
1012
from .decorators import with_logger
13+
from .game_service import GameService
14+
from .player_service import PlayerService
15+
16+
if TYPE_CHECKING:
17+
from server import ServerInstance
1118

1219

1320
@with_logger
1421
class ControlServer:
22+
_logger: ClassVar[logging.Logger]
23+
1524
def __init__(
1625
self,
1726
lobby_server: "ServerInstance",
1827
):
1928
self.lobby_server = lobby_server
20-
self.game_service = lobby_server.services["game_service"]
21-
self.player_service = lobby_server.services["player_service"]
22-
self.host = None
23-
self.port = None
29+
self.game_service = cast(GameService, lobby_server.services["game_service"])
30+
self.player_service = cast(PlayerService, lobby_server.services["player_service"])
31+
self.host: Optional[str] = None
32+
self.port: Optional[int] = None
2433

2534
self.app = web.Application()
2635
self.runner = web.AppRunner(self.app)
@@ -55,13 +64,13 @@ async def shutdown(self) -> None:
5564
self.host = None
5665
self.port = None
5766

58-
async def games(self, request) -> web.Response:
67+
async def games(self, request: web.Request) -> web.Response:
5968
return web.json_response([
6069
game.to_dict()
6170
for game in self.game_service.all_games
6271
])
6372

64-
async def players(self, request) -> web.Response:
73+
async def players(self, request: web.Request) -> web.Response:
6574
return web.json_response([
6675
player.to_dict()
6776
for player in self.player_service.all_players

server/core/dependency_injector.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import inspect
22
from collections import ChainMap, defaultdict
3+
from typing import Any
34

45
DependencyGraph = dict[str, list[str]]
56

67

7-
class DependencyInjector(object):
8+
class DependencyInjector():
89
"""
910
Does dependency injection.
1011
@@ -48,10 +49,10 @@ def __init__(self, hello, world):
4849

4950
def __init__(self) -> None:
5051
# Objects which are available to the constructors of injected objects
51-
self.injectables: dict[str, object] = {}
52+
self.injectables: dict[str, Any] = {}
5253

5354
def add_injectables(
54-
self, injectables: dict[str, object] = {}, **kwargs: object
55+
self, injectables: dict[str, Any] = {}, **kwargs: Any
5556
) -> None:
5657
"""
5758
Register additional objects that can be requested by injected classes.
@@ -61,7 +62,7 @@ def add_injectables(
6162

6263
def build_classes(
6364
self, classes: dict[str, type] = {}, **kwargs: type
64-
) -> dict[str, object]:
65+
) -> dict[str, Any]:
6566
"""
6667
Resolve dependencies by name and instantiate each class.
6768
"""
@@ -89,10 +90,11 @@ def _make_dependency_graph(self, classes: dict[str, type]) -> DependencyGraph:
8990
graph[name] = []
9091

9192
for obj_name, klass in classes.items():
92-
signature = inspect.signature(klass.__init__)
93-
# Strip off the `self` parameter
94-
params = list(signature.parameters.values())[1:]
95-
graph[obj_name] = [param.name for param in params]
93+
signature = inspect.signature(klass)
94+
graph[obj_name] = [
95+
param.name
96+
for param in signature.parameters.values()
97+
]
9698

9799
return graph
98100

@@ -101,7 +103,7 @@ def _build_classes_from_dependencies(
101103
dep: DependencyGraph,
102104
classes: dict[str, type],
103105
param_map: dict[str, list[str]]
104-
) -> dict[str, object]:
106+
) -> dict[str, Any]:
105107
"""
106108
Tries to build all classes in the dependency graph. Raises RuntimeError
107109
if some dependencies are not available or there was a cyclic dependency.

0 commit comments

Comments
 (0)