Skip to content

Commit

Permalink
Add mypy
Browse files Browse the repository at this point in the history
  • Loading branch information
RealOrangeOne committed Mar 18, 2024
1 parent 3bc3845 commit fa6bea6
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 37 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,32 @@ jobs:
- name: Format
run: poetry run ruff format . --check

typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.11"

- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-3.11-${{ hashFiles('poetry.lock') }}

- name: Install poetry
uses: abatilo/actions-poetry@v2
with:
poetry-version: 1.8.2

- name: Install Dependencies
run: poetry install

- name: Lint
run: poetry run mypy .

test:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -67,6 +93,7 @@ jobs:
needs:
- lint
- test
- typecheck
steps:
- uses: actions/checkout@v4

Expand Down
2 changes: 1 addition & 1 deletion calmerge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .config import Config


def get_aiohttp_app(config: Config):
def get_aiohttp_app(config: Config) -> web.Application:
app = web.Application()
app["config"] = config

Expand Down
16 changes: 9 additions & 7 deletions calmerge/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@
from .config import Config


def file_path(path: str):
path = Path(path).resolve()
def file_path(path: str) -> Path:
path_obj = Path(path).resolve()

if not path.is_file():
if not path_obj.is_file():
raise argparse.ArgumentTypeError(f"File not found: {path}")

return path
return path_obj


def serve(args: argparse.Namespace):
def serve(args: argparse.Namespace) -> None:
config = Config.from_file(args.config)
print(f"Found {len(config.calendars)} calendar(s)")
run_app(get_aiohttp_app(config), port=int(os.environ.get("PORT", args.port)))


def validate_config(args: argparse.Namespace):
def validate_config(args: argparse.Namespace) -> None:
try:
Config.from_file(args.config)
except ValidationError as e:
Expand All @@ -33,6 +33,8 @@ def validate_config(args: argparse.Namespace):
else:
print("Config is valid!")

return None


def get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="calmerge")
Expand All @@ -51,7 +53,7 @@ def get_parser() -> argparse.ArgumentParser:
return parser


def main():
def main() -> None:
parser = get_parser()
args = parser.parse_args()

Expand Down
6 changes: 3 additions & 3 deletions calmerge/calendars.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
fetch_cache = Cache(Cache.MEMORY, ttl=3600)


async def fetch_calendar(session: ClientSession, url: str):
async def fetch_calendar(session: ClientSession, url: str) -> icalendar.Calendar:
cache_key = "calendar_" + url
cached_calendar_data = await fetch_cache.get(cache_key)

Expand All @@ -22,7 +22,7 @@ async def fetch_calendar(session: ClientSession, url: str):
return icalendar.Calendar.from_ical(cached_calendar_data)


async def fetch_merged_calendar(calendar_config: CalendarConfig):
async def fetch_merged_calendar(calendar_config: CalendarConfig) -> icalendar.Calendar:
merged_calendar = icalendar.Calendar()

async with ClientSession() as session:
Expand All @@ -36,7 +36,7 @@ async def fetch_merged_calendar(calendar_config: CalendarConfig):
return merged_calendar


def offset_calendar(calendar: icalendar.Calendar, offset_days: int):
def offset_calendar(calendar: icalendar.Calendar, offset_days: int) -> None:
"""
Mutate a calendar and move events by a given offset
"""
Expand Down
6 changes: 3 additions & 3 deletions calmerge/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def expand_vars(cls, v: str) -> str:
def as_basic_auth(self) -> BasicAuth:
return BasicAuth(self.username, self.password)

def validate_header(self, auth_header) -> bool:
def validate_header(self, auth_header: str) -> bool:
try:
parsed_auth_header = BasicAuth.decode(auth_header)
except ValueError:
Expand Down Expand Up @@ -57,11 +57,11 @@ class Config(BaseModel):
calendars: list[CalendarConfig] = Field(alias="calendar", default_factory=list)

@classmethod
def from_file(cls, path: Path):
def from_file(cls, path: Path) -> "Config":
with path.open(mode="rb") as f:
return Config.model_validate(tomllib.load(f))

def get_calendar_by_name(self, name: str):
def get_calendar_by_name(self, name: str) -> CalendarConfig | None:
return next(
(calendar for calendar in self.calendars if calendar.name == name), None
)
2 changes: 1 addition & 1 deletion calmerge/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
def try_parse_int(val: str):
def try_parse_int(val: str) -> int | None:
try:
return int(val)
except (ValueError, TypeError):
Expand Down
6 changes: 3 additions & 3 deletions calmerge/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from .utils import try_parse_int


async def healthcheck(request):
async def healthcheck(request: web.Request) -> web.Response:
return web.Response(text="")


async def calendar(request):
async def calendar(request: web.Request) -> web.Response:
config = request.app["config"]

calendar_config = config.get_calendar_by_name(request.match_info["name"])
Expand All @@ -25,7 +25,7 @@ async def calendar(request):
calendar = await fetch_merged_calendar(calendar_config)

if calendar_config.allow_custom_offset and (
offset_days := try_parse_int(request.query.get("offset_days"))
offset_days := try_parse_int(request.query.get("offset_days", ""))
):
if abs(offset_days) > MAX_OFFSET:
raise web.HTTPBadRequest(
Expand Down
59 changes: 58 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pytest = "^8.1.1"
pytest-aiohttp = "^1.0.5"
pytest-asyncio = "^0.23.5.post1"
pytest-cov = "^4.1.0"
mypy = "^1.9.0"

[build-system]
requires = ["poetry-core"]
Expand All @@ -36,3 +37,17 @@ ignore = ["E501"]

[tool.pytest.ini_options]
asyncio_mode = "auto"

[tool.mypy]
warn_unused_ignores = true
warn_return_any = true
show_error_codes = true
strict_optional = true
implicit_optional = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_decorators = true
check_untyped_defs = true
ignore_missing_imports = true
9 changes: 7 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from asyncio import AbstractEventLoop
from pathlib import Path
from typing import Callable

import pytest
from aiohttp.test_utils import TestClient

from calmerge import get_aiohttp_app
from calmerge.config import Config
Expand All @@ -12,5 +15,7 @@ def config() -> Config:


@pytest.fixture
def client(event_loop, aiohttp_client, config):
return event_loop.run_until_complete(aiohttp_client(get_aiohttp_app(config)))
def client(
event_loop: AbstractEventLoop, aiohttp_client: Callable, config: Config
) -> TestClient:
return event_loop.run_until_complete(aiohttp_client(get_aiohttp_app(config))) # type: ignore
15 changes: 8 additions & 7 deletions tests/test_calendar_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@
import icalendar
import pytest
from aiohttp import BasicAuth
from aiohttp.test_utils import TestClient

from calmerge.config import MAX_OFFSET


async def test_retrieves_calendars(client):
async def test_retrieves_calendars(client: TestClient) -> None:
response = await client.get("/python.ics")
assert response.status == 200

calendar = icalendar.Calendar.from_ical(await response.text())
assert not calendar.is_broken


async def test_404_without_auth(client):
async def test_404_without_auth(client: TestClient) -> None:
response = await client.get("/python-authed.ics")
assert response.status == 404


async def test_requires_auth(client):
async def test_requires_auth(client: TestClient) -> None:
response = await client.get(
"/python-authed.ics", auth=BasicAuth("user", "password")
)
Expand All @@ -30,15 +31,15 @@ async def test_requires_auth(client):
assert not calendar.is_broken


async def test_offset(client):
async def test_offset(client: TestClient) -> None:
response = await client.get("/python-offset.ics")
assert response.status == 200

calendar = icalendar.Calendar.from_ical(await response.text())
assert not calendar.is_broken


async def test_offset_calendar_matches(client):
async def test_offset_calendar_matches(client: TestClient) -> None:
offset_response = await client.get("/python-offset.ics")
offset_calendar = icalendar.Calendar.from_ical(await offset_response.text())

Expand Down Expand Up @@ -75,7 +76,7 @@ async def test_offset_calendar_matches(client):


@pytest.mark.parametrize("offset", [100, -100, MAX_OFFSET, -MAX_OFFSET])
async def test_custom_offset(client, offset):
async def test_custom_offset(client: TestClient, offset: int) -> None:
offset_response = await client.get(
"/python-custom-offset.ics",
params={"offset_days": offset},
Expand Down Expand Up @@ -107,7 +108,7 @@ async def test_custom_offset(client, offset):


@pytest.mark.parametrize("offset", [MAX_OFFSET + 1, -MAX_OFFSET - 1])
async def test_out_of_bounds_custom_offset(client, offset):
async def test_out_of_bounds_custom_offset(client: TestClient, offset: int) -> None:
response = await client.get(
"/python-custom-offset.ics",
params={"offset_days": offset},
Expand Down
Loading

0 comments on commit fa6bea6

Please sign in to comment.