Skip to content

Commit 9f4f458

Browse files
authored
Add typechecking, resolves #27 (#37)
* Upgrade python * Initial typecheck setup with fixes * Add linter * Use new venv * ignore ruff cache * Fix typechecking for ai_player * Fix typchecking for game_state * Fix test cases for scuttles * Delete fixed modules from mypy overrides * Fix mypy errors in source files apart from tests * Fix some type errors in tests * Fix remaining typechecking issues * Shorten ai player test * Fix scuttle played by error * Fix game_state test case for scuttling
1 parent c8fc2b7 commit 9f4f458

31 files changed

Lines changed: 2146 additions & 1266 deletions

.github/workflows/python-tests.yml

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# This workflow will install Python dependencies, run tests and lint with a single version of Python
1+
# This workflow will install Python dependencies, run tests, lint, and generate documentation
22
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
33

4-
name: Python application
4+
name: Python Tests and Documentation
55

66
on:
77
push:
@@ -16,26 +16,40 @@ permissions:
1616

1717
jobs:
1818
build:
19-
2019
runs-on: ubuntu-latest
2120

2221
steps:
23-
- uses: actions/checkout@v4
24-
- name: Set up Python 3.8
25-
uses: actions/setup-python@v3
26-
with:
27-
python-version: "3.8"
28-
- name: Install dependencies
29-
run: |
30-
python -m pip install --upgrade pip
31-
pip install flake8 pytest
32-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
33-
- name: Lint with flake8
34-
run: |
35-
# stop the build if there are Python syntax errors or undefined names
36-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
37-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
38-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
39-
- name: Test with pytest
40-
run: |
41-
PYTHONPATH=$(pwd) pytest tests -v --capture=sys
22+
- uses: actions/checkout@v4
23+
24+
- name: Set up Python 3.12
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: "3.12"
28+
29+
- name: Install dependencies
30+
run: |
31+
python -m pip install --upgrade pip
32+
pip install flake8 pytest
33+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
34+
35+
- name: Lint with flake8
36+
run: |
37+
# stop the build if there are Python syntax errors or undefined names
38+
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
39+
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
40+
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
41+
42+
- name: Run tests
43+
run: |
44+
PYTHONPATH=${{ github.workspace }} pytest tests -v --capture=sys
45+
46+
- name: Generate documentation
47+
run: |
48+
PYTHONPATH=${{ github.workspace }} python docs.py
49+
50+
- name: Deploy documentation
51+
if: github.ref == 'refs/heads/main'
52+
uses: peaceiris/actions-gh-pages@v3
53+
with:
54+
github_token: ${{ secrets.GITHUB_TOKEN }}
55+
publish_dir: ./docs

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ share/python-wheels/
2626
*.egg
2727
MANIFEST
2828

29+
# Virtual environments
30+
cuttle-bot/
31+
cuttle-bot-3.12/
32+
venv/
33+
ENV/
34+
2935
# PyInstaller
3036
# Usually these files are written by a python script from a template
3137
# before PyInstaller builds the exe, so as to inject date/other infos into it.
@@ -170,4 +176,8 @@ cython_debug/
170176
test_games/
171177
tmp.txt
172178
game_history/
173-
docs/
179+
docs/
180+
test_outputs/
181+
182+
# linters
183+
.ruff_cache/

Makefile

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
# Get the current working directory
22
CURRENT_DIR := $(shell pwd)
33

4+
# Virtual environment name
5+
VENV_NAME := cuttle-bot-3.12
6+
47
# Add command to run tests
58
# --capture=tee-sys is used to capture the output of the tests and print it to the console
69
test:
7-
PYTHONPATH=$(CURRENT_DIR) pytest tests -v --capture=tee-sys
10+
source $(VENV_NAME)/bin/activate && PYTHONPATH=$(CURRENT_DIR) pytest tests -v --capture=tee-sys
811

912
run:
10-
PYTHONPATH=$(CURRENT_DIR) python main.py
13+
source $(VENV_NAME)/bin/activate && PYTHONPATH=$(CURRENT_DIR) python main.py
1114

1215
# Generate documentation using pdoc
1316
docs:
14-
PYTHONPATH=$(CURRENT_DIR) python docs.py
17+
source $(VENV_NAME)/bin/activate && PYTHONPATH=$(CURRENT_DIR) python docs.py
1518

1619
# Clean generated documentation
1720
clean-docs:
18-
rm -rf docs/
21+
rm -rf docs/
22+
23+
# Setup virtual environment
24+
setup:
25+
python3.12 -m venv $(VENV_NAME)
26+
source $(VENV_NAME)/bin/activate && pip install -r requirements.txt
27+
28+
# Clean virtual environment
29+
clean-venv:
30+
rm -rf $(VENV_NAME)/
31+
32+
# Default target
33+
all: test
34+
35+
# Type checking
36+
typecheck:
37+
@echo "Running mypy type checks..."
38+
source $(VENV_NAME)/bin/activate && mypy .

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
## Create a virtual environment
66

77
```bash
8-
python3 -m venv cuttle-bot
9-
source ./cuttle-bot/bin/activate
8+
python3 -m venv cuttle-bot-3.12
9+
source ./cuttle-bot-3.12/bin/activate
1010
```
1111

1212

@@ -33,7 +33,7 @@ The test output can be quite verbose, so it's recommended to redirect the output
3333
`tmp.txt` is added to `.gitignore` to avoid polluting the repo with test output.
3434

3535
```bash
36-
source ./cuttle-bot/bin/activate && make test > tmp.txt 2>&1
36+
source ./cuttle-bot-3.12/bin/activate && make test > tmp.txt 2>&1
3737
```
3838

3939
Or you can simply run `make test` to run the tests and see the output in the terminal.

docs.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44
This script generates HTML documentation using pdoc.
55
"""
66

7-
import os
8-
import sys
97
import pdoc
108
from pathlib import Path
119

12-
def generate_docs():
10+
11+
def generate_docs() -> None:
1312
"""
1413
Generate documentation for the project.
1514
"""
1615
# Define the output directory
1716
output_dir = Path("docs")
1817
output_dir.mkdir(exist_ok=True)
19-
18+
2019
# Define the modules to document
2120
modules = [
2221
"game",
@@ -30,13 +29,13 @@ def generate_docs():
3029
"game.utils",
3130
"main",
3231
]
33-
32+
3433
# Generate documentation
3534
pdoc.render.configure(
3635
docformat="google", # Use Google-style docstrings
37-
show_source=True, # Show source code
36+
show_source=True, # Show source code
3837
)
39-
38+
4039
# Generate documentation for each module
4140
for module in modules:
4241
try:
@@ -47,9 +46,10 @@ def generate_docs():
4746
print(f"Generated documentation for {module}")
4847
except Exception as e:
4948
print(f"Error generating documentation for {module}: {e}")
50-
49+
5150
print(f"\nDocumentation generated in {output_dir.absolute()}")
5251
print("You can view the documentation by opening docs/index.html in your browser")
5352

53+
5454
if __name__ == "__main__":
55-
generate_docs()
55+
generate_docs()

game/action.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
from __future__ import annotations
1111

12-
from game.card import Card
1312
from enum import Enum
13+
from typing import Optional
14+
15+
from game.card import Card
1416

1517

1618
class ActionSource(Enum):
@@ -50,28 +52,28 @@ class Action:
5052
"""
5153

5254
action_type: ActionType
53-
card: Card
54-
target: Card
55+
card: Optional[Card]
56+
target: Optional[Card]
5557
played_by: int
5658
requires_additional_input: bool
5759
source: ActionSource
5860

5961
def __init__(
6062
self,
6163
action_type: ActionType,
62-
card: Card,
63-
target: Card,
6464
played_by: int,
65+
card: Optional[Card] = None,
66+
target: Optional[Card] = None,
6567
requires_additional_input: bool = False,
6668
source: ActionSource = ActionSource.HAND,
6769
):
6870
"""Initialize a new Action instance.
6971
7072
Args:
7173
action_type (ActionType): The type of action being performed.
72-
card (Card): The card being played or used.
73-
target (Card): The target card (if any) for the action.
7474
played_by (int): Index of the player performing the action (0 or 1).
75+
card (Optional[Card], optional): The card being played or used. Defaults to None.
76+
target (Optional[Card], optional): The target card (if any) for the action. Defaults to None.
7577
requires_additional_input (bool, optional): Whether more input is needed.
7678
Defaults to False.
7779
source (ActionSource, optional): Where the card comes from.
@@ -107,15 +109,27 @@ def __repr__(self) -> str:
107109
elif self.action_type == ActionType.ONE_OFF:
108110
return f"Play {self.card} as one-off"
109111
elif self.action_type == ActionType.SCUTTLE:
110-
return f"Scuttle {self.target} on P{self.target.played_by}'s field with {self.card}"
112+
target_str = str(self.target) if self.target else "None"
113+
card_str = str(self.card) if self.card else "None"
114+
target_player = self.target.played_by if self.target else '?'
115+
return f"Scuttle {target_str} on P{target_player}'s field with {card_str}"
111116
elif self.action_type == ActionType.DRAW:
112117
return "Draw a card from deck"
113118
elif self.action_type == ActionType.COUNTER:
114-
return f"Counter {self.target} with {self.card}"
119+
target_str = str(self.target) if self.target else "None"
120+
card_str = str(self.card) if self.card else "None"
121+
return f"Counter {target_str} with {card_str}"
115122
elif self.action_type == ActionType.JACK:
116-
return f"Play {self.card} as jack on {self.target}"
123+
target_str = str(self.target) if self.target else "None"
124+
card_str = str(self.card) if self.card else "None"
125+
return f"Play {card_str} as jack on {target_str}"
117126
elif self.action_type == ActionType.RESOLVE:
118-
return f"Resolve one-off {self.target}"
127+
target_str = str(self.target) if self.target else "None"
128+
return f"Resolve one-off {target_str}"
129+
else:
130+
# Handle any unexpected action types
131+
card_str = str(self.card) if self.card else "None"
132+
return f"Unknown Action: {self.action_type.value} with card {card_str}"
119133

120134
def __str__(self) -> str:
121135
"""Get a string representation of the action.

0 commit comments

Comments
 (0)