Skip to content

Commit 40d1ef7

Browse files
authored
Merge pull request #131 from mjpieters/type_annotations
Provide type annotations for the public API
2 parents e37d736 + 589325a commit 40d1ef7

13 files changed

+266
-93
lines changed

.github/workflows/tests.yml

+26
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,29 @@ jobs:
4343
uses: codecov/codecov-action@v3
4444
with:
4545
token: ${{ secrets.CODECOV_TOKEN }}
46+
47+
typesafety:
48+
runs-on: ubuntu-latest
49+
50+
steps:
51+
- uses: actions/checkout@v3
52+
- uses: actions/setup-python@v4
53+
with:
54+
python-version: '3.12'
55+
cache: 'pip'
56+
57+
- run: |
58+
python -m venv .venv
59+
source .venv/bin/activate
60+
pip install --upgrade pip
61+
# Tell setuptools to *not* create a PEP 660 import hook and to use
62+
# symlinks instead, so that pyright can still find the package. See
63+
# https://microsoft.github.io/pyright/#/import-resolution?id=editable-installs
64+
pip install --editable . --config-settings editable_mode=strict
65+
66+
- run: echo "$PWD/.venv/bin" >> $GITHUB_PATH
67+
68+
- uses: jakebailey/pyright-action@v1
69+
with:
70+
ignore-external: true
71+
verify-types: "aocd"

aocd/__init__.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import typing as t
23
from functools import partial
34

45
from . import _ipykernel
@@ -11,13 +12,34 @@
1112
from . import post
1213
from . import runner
1314
from . import utils
15+
from . import types
1416
from .exceptions import AocdError
1517
from .get import get_data
1618
from .get import get_day_and_year
1719
from .post import submit as _impartial_submit
1820

21+
__all__ = [
22+
"_ipykernel",
23+
"cli",
24+
"cookies",
25+
"data",
26+
"examples",
27+
"exceptions",
28+
"get",
29+
"models",
30+
"post",
31+
"runner",
32+
"submit",
33+
"types",
34+
"utils",
35+
]
1936

20-
def __getattr__(name):
37+
if t.TYPE_CHECKING:
38+
data: str
39+
submit = _impartial_submit
40+
41+
42+
def __getattr__(name: str) -> t.Any:
2143
if name == "data":
2244
day, year = get_day_and_year()
2345
return get_data(day=day, year=year)

aocd/cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .utils import get_plugins
1414

1515

16-
def main():
16+
def main() -> None:
1717
"""Get your puzzle input data, caching it if necessary, and print it on stdout."""
1818
aoc_now = datetime.datetime.now(tz=AOC_TZ)
1919
days = range(1, 26)

aocd/cookies.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
from .utils import get_owner
1313

1414

15-
log = logging.getLogger(__name__)
15+
log: logging.Logger = logging.getLogger(__name__)
1616

1717

18-
def get_working_tokens():
18+
def get_working_tokens() -> dict[str, str]:
1919
"""Check browser cookie storage for session tokens from .adventofcode.com domain."""
2020
log.debug("checking for installation of browser-cookie3 package")
2121
try:
@@ -66,7 +66,7 @@ def get_working_tokens():
6666
return result
6767

6868

69-
def scrape_session_tokens():
69+
def scrape_session_tokens() -> None:
7070
"""Scrape AoC session tokens from your browser's cookie storage."""
7171
aocd_token_path = AOCD_CONFIG_DIR / "token"
7272
aocd_tokens_path = AOCD_CONFIG_DIR / "tokens.json"

aocd/examples.py

+19-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from __future__ import annotations
2+
13
import argparse
24
import logging
35
import re
46
import sys
7+
import typing as t
58
from dataclasses import dataclass
69
from datetime import datetime
710
from itertools import zip_longest
@@ -16,7 +19,11 @@
1619
from aocd.utils import get_plugins
1720

1821

19-
log = logging.getLogger(__name__)
22+
log: logging.Logger = logging.getLogger(__name__)
23+
24+
_AnswerElem = t.Literal[
25+
"a_code", "a_li", "a_pre", "a_em", "b_code", "b_li", "b_pre", "b_em"
26+
]
2027

2128

2229
@dataclass
@@ -34,17 +41,17 @@ class Page:
3441
soup: bs4.BeautifulSoup # The raw_html string parsed into a bs4.BeautifulSoup instance
3542
year: int # AoC puzzle year (2015+) parsed from html title
3643
day: int # AoC puzzle day (1-25) parsed from html title
37-
article_a: bs4.element.Tag # The bs4 tag for the first <article> in the page, i.e. part a
38-
article_b: bs4.element.Tag # The bs4 tag for the second <article> in the page, i.e. part b. It will be `None` if part b locked
44+
article_a: bs4.Tag # The bs4 tag for the first <article> in the page, i.e. part a
45+
article_b: bs4.Tag | None # The bs4 tag for the second <article> in the page, i.e. part b. It will be `None` if part b locked
3946
a_raw: str # The first <article> html as a string
40-
b_raw: str # The second <article> html as a string. Will be `None` if part b locked
47+
b_raw: str | None # The second <article> html as a string. Will be `None` if part b locked
4148

42-
def __repr__(self):
49+
def __repr__(self) -> str:
4350
part_a_only = "*" if self.article_b is None else ""
4451
return f"<Page({self.year}, {self.day}){part_a_only} at {hex(id(self))}>"
4552

4653
@classmethod
47-
def from_raw(cls, html):
54+
def from_raw(cls, html: str) -> Page:
4855
soup = _get_soup(html)
4956
title_pat = r"^Day (\d{1,2}) - Advent of Code (\d{4})$"
5057
title_text = soup.title.text
@@ -77,7 +84,7 @@ def from_raw(cls, html):
7784
)
7885
return page
7986

80-
def __getattr__(self, name):
87+
def __getattr__(self, name: _AnswerElem) -> t.Sequence[str]:
8188
if not name.startswith(("a_", "b_")):
8289
raise AttributeError(name)
8390
part, sep, tag = name.partition("_")
@@ -118,12 +125,12 @@ class Example(NamedTuple):
118125
"""
119126

120127
input_data: str
121-
answer_a: str = None
122-
answer_b: str = None
123-
extra: str = None
128+
answer_a: str | None = None
129+
answer_b: str | None = None
130+
extra: str | None = None
124131

125132
@property
126-
def answers(self):
133+
def answers(self) -> tuple[str | None, str | None]:
127134
return self.answer_a, self.answer_b
128135

129136

@@ -144,7 +151,7 @@ def _get_unique_real_inputs(year, day):
144151
return list({}.fromkeys(strs))
145152

146153

147-
def main():
154+
def main() -> None:
148155
"""
149156
Summarize an example parser's results with historical puzzles' prose, and
150157
compare the performance against a reference implementation

aocd/get.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from __future__ import annotations
2+
13
import datetime
24
import os
35
import re
46
import traceback
5-
from logging import getLogger
7+
import typing as t
8+
from logging import Logger, getLogger
69

710
from ._ipykernel import get_ipynb_path
811
from .exceptions import AocdError
@@ -14,10 +17,15 @@
1417
from .utils import blocker
1518

1619

17-
log = getLogger(__name__)
20+
log: Logger = getLogger(__name__)
1821

1922

20-
def get_data(session=None, day=None, year=None, block=False):
23+
def get_data(
24+
session: str | None = None,
25+
day: int | None = None,
26+
year: int | None = None,
27+
block: bool = False,
28+
) -> str:
2129
"""
2230
Get data for day (1-25) and year (2015+).
2331
User's session cookie (str) is needed - puzzle inputs differ by user.
@@ -45,7 +53,7 @@ def get_data(session=None, day=None, year=None, block=False):
4553
return puzzle.input_data
4654

4755

48-
def most_recent_year():
56+
def most_recent_year() -> int:
4957
"""
5058
This year, if it's December.
5159
The most recent year, otherwise.
@@ -60,7 +68,7 @@ def most_recent_year():
6068
return year
6169

6270

63-
def current_day():
71+
def current_day() -> int:
6472
"""
6573
Most recent day, if it's during the Advent of Code. Happy Holidays!
6674
Day 1 is assumed, otherwise.
@@ -73,7 +81,7 @@ def current_day():
7381
return day
7482

7583

76-
def get_day_and_year():
84+
def get_day_and_year() -> tuple[int, int | None]:
7785
"""
7886
Returns tuple (day, year).
7987

0 commit comments

Comments
 (0)