Skip to content

Commit

Permalink
Provide type annotations for the public API
Browse files Browse the repository at this point in the history
The package now includes a PEP 561 `py.typed` flag file to let type
checkers know that this package provides type annotations.

The annotations provide full coverage for all public functions and
classes in the aocd package, with the library reaching 100% coverage as
per `pyright --ignoreexternal --verifytypes`.
  • Loading branch information
mjpieters committed Dec 7, 2023
1 parent 34d73e3 commit 184b71e
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 93 deletions.
24 changes: 23 additions & 1 deletion aocd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import typing as t
from functools import partial

from . import _ipykernel
Expand All @@ -11,13 +12,34 @@
from . import post
from . import runner
from . import utils
from . import types
from .exceptions import AocdError
from .get import get_data
from .get import get_day_and_year
from .post import submit as _impartial_submit

__all__ = [
"_ipykernel",
"cli",
"cookies",
"data",
"examples",
"exceptions",
"get",
"models",
"post",
"runner",
"submit",
"utils",
"types",
]

def __getattr__(name):
if t.TYPE_CHECKING: # pragma: no cover
data: str
submit = _impartial_submit


def __getattr__(name: str) -> t.Any:
if name == "data":
day, year = get_day_and_year()
return get_data(day=day, year=year)
Expand Down
2 changes: 1 addition & 1 deletion aocd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .utils import get_plugins


def main():
def main() -> None:
"""Get your puzzle input data, caching it if necessary, and print it on stdout."""
aoc_now = datetime.datetime.now(tz=AOC_TZ)
days = range(1, 26)
Expand Down
6 changes: 3 additions & 3 deletions aocd/cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
from .utils import get_owner


log = logging.getLogger(__name__)
log: logging.Logger = logging.getLogger(__name__)


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


def scrape_session_tokens():
def scrape_session_tokens() -> None:
"""Scrape AoC session tokens from your browser's cookie storage."""
aocd_token_path = AOCD_CONFIG_DIR / "token"
aocd_tokens_path = AOCD_CONFIG_DIR / "tokens.json"
Expand Down
29 changes: 17 additions & 12 deletions aocd/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import re
import sys
import typing as t
from dataclasses import dataclass
from datetime import datetime
from itertools import zip_longest
Expand All @@ -16,7 +17,11 @@
from aocd.utils import get_plugins


log = logging.getLogger(__name__)
log: logging.Logger = logging.getLogger(__name__)

_AnswerElem = t.Literal[
"a_code", "a_li", "a_pre", "a_em", "b_code", "b_li", "b_pre", "b_em"
]


@dataclass
Expand All @@ -34,17 +39,17 @@ class Page:
soup: bs4.BeautifulSoup # The raw_html string parsed into a bs4.BeautifulSoup instance
year: int # AoC puzzle year (2015+) parsed from html title
day: int # AoC puzzle day (1-25) parsed from html title
article_a: bs4.element.Tag # The bs4 tag for the first <article> in the page, i.e. part a
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
article_a: bs4.Tag # The bs4 tag for the first <article> in the page, i.e. part a
article_b: t.Optional[bs4.Tag] # The bs4 tag for the second <article> in the page, i.e. part b. It will be `None` if part b locked
a_raw: str # The first <article> html as a string
b_raw: str # The second <article> html as a string. Will be `None` if part b locked
b_raw: t.Optional[str] # The second <article> html as a string. Will be `None` if part b locked

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

@classmethod
def from_raw(cls, html):
def from_raw(cls, html: str) -> "Page":
soup = _get_soup(html)
title_pat = r"^Day (\d{1,2}) - Advent of Code (\d{4})$"
title_text = soup.title.text
Expand Down Expand Up @@ -77,7 +82,7 @@ def from_raw(cls, html):
)
return page

def __getattr__(self, name):
def __getattr__(self, name: _AnswerElem) -> t.Sequence[str]:
if not name.startswith(("a_", "b_")):
raise AttributeError(name)
part, sep, tag = name.partition("_")
Expand Down Expand Up @@ -118,12 +123,12 @@ class Example(NamedTuple):
"""

input_data: str
answer_a: str = None
answer_b: str = None
extra: str = None
answer_a: t.Optional[str] = None
answer_b: t.Optional[str] = None
extra: t.Optional[str] = None

@property
def answers(self):
def answers(self) -> tuple[t.Optional[str], t.Optional[str]]:
return self.answer_a, self.answer_b


Expand All @@ -144,7 +149,7 @@ def _get_unique_real_inputs(year, day):
return list({}.fromkeys(strs))


def main():
def main() -> None:
"""
Summarize an example parser's results with historical puzzles' prose, and
compare the performance against a reference implementation
Expand Down
18 changes: 12 additions & 6 deletions aocd/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import os
import re
import traceback
from logging import getLogger
import typing as t
from logging import Logger, getLogger

from ._ipykernel import get_ipynb_path
from .exceptions import AocdError
Expand All @@ -14,10 +15,15 @@
from .utils import blocker


log = getLogger(__name__)
log: Logger = getLogger(__name__)


def get_data(session=None, day=None, year=None, block=False):
def get_data(
session: t.Optional[str] = None,
day: t.Optional[int] = None,
year: t.Optional[int] = None,
block: bool = False,
) -> str:
"""
Get data for day (1-25) and year (2015+).
User's session cookie (str) is needed - puzzle inputs differ by user.
Expand Down Expand Up @@ -45,7 +51,7 @@ def get_data(session=None, day=None, year=None, block=False):
return puzzle.input_data


def most_recent_year():
def most_recent_year() -> int:
"""
This year, if it's December.
The most recent year, otherwise.
Expand All @@ -60,7 +66,7 @@ def most_recent_year():
return year


def current_day():
def current_day() -> int:
"""
Most recent day, if it's during the Advent of Code. Happy Holidays!
Day 1 is assumed, otherwise.
Expand All @@ -73,7 +79,7 @@ def current_day():
return day


def get_day_and_year():
def get_day_and_year() -> tuple[int, int]:
"""
Returns tuple (day, year).
Expand Down
Loading

0 comments on commit 184b71e

Please sign in to comment.