Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide type annotations for the public API #131

Merged
merged 2 commits into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,29 @@ jobs:
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}

typesafety:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What Python version is this going to use by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point; it should really pick a version. It'll use the value from .python-version if that file is present, but if not it'll default to the 'default Python installed on the runner'.

with:
python-version: '3.12'
cache: 'pip'

- run: |
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
# Tell setuptools to *not* create a PEP 660 import hook and to use
# symlinks instead, so that pyright can still find the package. See
# https://microsoft.github.io/pyright/#/import-resolution?id=editable-installs
pip install --editable . --config-settings editable_mode=strict

- run: echo "$PWD/.venv/bin" >> $GITHUB_PATH
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This puts the venv on the PATH for all following steps. It lets pyright find the venv as it looks for the current python executable.


- uses: jakebailey/pyright-action@v1
with:
ignore-external: true
verify-types: "aocd"
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",
"types",
"utils",
]

def __getattr__(name):
if t.TYPE_CHECKING:
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
31 changes: 19 additions & 12 deletions aocd/examples.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

import argparse
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 +19,11 @@
from aocd.utils import get_plugins


log = logging.getLogger(__name__)
log: logging.Logger = logging.getLogger(__name__)
Comment on lines -19 to +22
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log objects in each module should probably be private, so _log. The only reason I annotated them is because they are part of the public API.


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


@dataclass
Expand All @@ -34,17 +41,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: 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
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: str | None # 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 +84,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 +125,12 @@ class Example(NamedTuple):
"""

input_data: str
answer_a: str = None
answer_b: str = None
extra: str = None
answer_a: str | None = None
answer_b: str | None = None
extra: str | None = None

@property
def answers(self):
def answers(self) -> tuple[str | None, str | None]:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the code-path where these can be None? IIRC we raise exception for unavailable answers, and day 25 part b answer is always the empty string.

Copy link
Contributor Author

@mjpieters mjpieters Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the Example named tuple, the default values for answer_a and answer_b is None. Your code creates a missing = Example("") instance, and missing.answers is (None, None).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you were thinking of Puzzle.answers here, which can only return tuple[str, str] and will raise an exception if that's not possible. But anyone can create an Example instance with one or other answer ommitted.

return self.answer_a, self.answer_b


Expand All @@ -144,7 +151,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
20 changes: 14 additions & 6 deletions aocd/get.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

import datetime
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 +17,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: str | None = None,
day: int | None = None,
year: int | None = 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 +53,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 +68,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 +81,7 @@ def current_day():
return day


def get_day_and_year():
def get_day_and_year() -> tuple[int, int | None]:
"""
Returns tuple (day, year).

Expand Down
Loading