Skip to content

Commit

Permalink
Reimplement Cal Jacobson's coercion work.
Browse files Browse the repository at this point in the history
Co-authored-by: Cal Jacobson <cj81499@users.noreply.github.com>
  • Loading branch information
wimglenn and cj81499 committed Nov 3, 2024
1 parent 7a16c4a commit 8422243
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 24 deletions.
2 changes: 1 addition & 1 deletion aocd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from .post import submit as _impartial_submit

__all__ = [
"_ipykernel",
"AocdError",
"cli",
"cookies",
"data",
Expand Down
3 changes: 2 additions & 1 deletion aocd/_ipykernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def get_ipynb_path():
for serv in serverapp.list_running_servers():
url = url_path_join(serv["url"], "api/sessions")
resp = http.request("GET", url, fields={"token": serv["token"]})
resp.raise_for_status()
if resp.status >= 400:
raise urllib3.exceptions.ResponseError(f"HTTP {resp.status}")
for sess in resp.json():
if kernel_id == sess["kernel"]["id"]:
path = serv["root_dir"]
Expand Down
5 changes: 3 additions & 2 deletions aocd/cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ def get_working_tokens() -> dict[str, str]:
edge = [c for c in edge if c.name == "session"]
log.info("%d candidates from edge", len(edge))

cookies = chrome + firefox + edge
# order preserving de-dupe
tokens = list({}.fromkeys([c.value for c in chrome + firefox + edge]))
removed = len(chrome + firefox + edge) - len(tokens)
tokens = list({}.fromkeys([c.value for c in cookies if c.value is not None]))
removed = len(cookies) - len(tokens)
if removed:
log.info("Removed %d duplicate%s", removed, "s"[: removed - 1])

Expand Down
55 changes: 38 additions & 17 deletions aocd/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextlib
import json
import logging
import os
Expand All @@ -10,6 +11,8 @@
import webbrowser
from datetime import datetime
from datetime import timedelta
from decimal import Decimal
from fractions import Fraction
from functools import cache
from functools import cached_property
from importlib.metadata import entry_points
Expand Down Expand Up @@ -113,10 +116,14 @@ def get_stats(
"""
aoc_now = datetime.now(tz=AOC_TZ)
all_years = range(2015, aoc_now.year + int(aoc_now.month == 12))
if isinstance(years, int) and years in all_years:
if isinstance(years, int):
years = (years,)
if years is None:
years = all_years
invalid_years = sorted([y for y in years if y not in all_years])
if invalid_years:
bad = ', '.join(map(str, invalid_years))
raise ValueError(f"Invalid years: {bad}")
days = {str(i) for i in range(1, 26)}
results = {}
ur_broke = "You haven't collected any stars"
Expand Down Expand Up @@ -248,7 +255,7 @@ def examples(self) -> list[Example]:
html, and they're the same for every user id. This list might be empty (not
every puzzle has usable examples), or it might have several examples, but it
will usually have one element. The list, and the examples themselves, may be
different depending on whether or not part b of the puzzle prose has been
different regardless of whether part b of the puzzle prose has been
unlocked (i.e. part a has already been solved correctly).
"""
return self._get_examples()
Expand Down Expand Up @@ -301,30 +308,44 @@ def _coerce_val(self, val):
# but it's convenient to be able to submit numbers, since many of the answers
# are numeric strings. coerce the values to string safely.
orig_val = val
orig_type = type(val)
coerced = False
floatish = isinstance(val, (float, complex))
if floatish and val.imag == 0.0 and val.real.is_integer():
coerced = True
val = int(val.real)
elif orig_type.__module__ == "numpy" and getattr(val, "ndim", None) == 0:
# deal with numpy scalars
if orig_type.__name__.startswith(("int", "uint", "long", "ulong")):
# A user can't be submitting a numpy type if numpy is not installed, so skip
# handling of those types
with contextlib.suppress(ImportError):
import numpy as np

# "unwrap" arrays that contain a single element
if isinstance(val, np.ndarray) and val.size == 1:
coerced = True
val = val.item()
if isinstance(val, (np.integer, np.floating, np.complexfloating)) and val.imag == 0 and val.real.is_integer():
coerced = True
val = int(orig_val)
elif orig_type.__name__.startswith(("float", "complex")):
if val.imag == 0.0 and float(val.real).is_integer():
coerced = True
val = int(val.real)
val = str(int(val.real))
if isinstance(val, int):
val = str(val)
elif isinstance(val, (float, complex)) and val.imag == 0 and val.real.is_integer():
coerced = True
val = str(int(val.real))
elif isinstance(val, bytes):
coerced = True
val = val.decode()
elif isinstance(val, (Decimal, Fraction)):
# if val can be represented as an integer ratio where the denominator is 1
# val is an integer and val == numerator
numerator, denominator = val.as_integer_ratio()
if denominator == 1:
coerced = True
val = str(numerator)
if not isinstance(val, str):
raise AocdError(f"Failed to coerce {type(orig_val).__name__} value {orig_val!r} for {self.year}/{self.day:02}.")
if coerced:
log.warning(
"coerced %s value %r for %d/%02d",
orig_type.__name__,
"coerced %s value %r for %d/%02d to %r",
type(orig_val).__name__,
orig_val,
self.year,
self.day,
val,
)
return val

Expand Down
2 changes: 1 addition & 1 deletion aocd/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def run_for(
else:
for answer, part in zip((a, b), "ab"):
if day == 25 and part == "b":
# there's no part b on christmas day, skip
# there's no part b on Christmas day, skip
continue
expected = None
try:
Expand Down
47 changes: 46 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import decimal
import fractions
import logging
from datetime import datetime
from datetime import timedelta
from textwrap import dedent

import numpy as np
import pytest
Expand Down Expand Up @@ -445,6 +446,50 @@ def test_type_coercions(v_raw, v_expected, len_logs, caplog):
assert len(caplog.records) == len_logs


@pytest.mark.parametrize(
"v_raw, v_expected, len_logs",
[
("xxx", "xxx", 0), # str -> str
(b"123", "123", 1), # bytes -> str
(123, "123", 0), # int -> str
(123.0, "123", 1), # float -> str
(123.0 + 0.0j, "123", 1), # complex -> str
(np.int32(123), "123", 1), # np.int -> str
(np.uint32(123), "123", 1), # np.uint -> str
(np.double(123.0), "123", 1), # np.double -> str
(np.complex64(123.0 + 0.0j), "123", 1), # np.complex -> str
(np.array([123]), "123", 1), # 1D np.array of int -> str
(np.array([[123.0]]), "123", 1), # 2D np.array of float -> str
(np.array([[[[[[123.0 + 0j]]]]]]), "123", 1), # deep np.array of complex -> str
(fractions.Fraction(123 * 2, 2), "123", 1), # Fraction -> int
(decimal.Decimal("123"), "123", 1), # Decimal -> int
],
)
def test_type_coercions(v_raw, v_expected, len_logs, caplog):
p = Puzzle(2022, 1)
v_actual = p._coerce_val(v_raw)
assert v_actual == v_expected, f"{type(v_raw)} {v_raw})"
assert len(caplog.records) == len_logs


@pytest.mark.parametrize(
"val, error_msg",
[
(123.5, "Failed to coerce float value 123.5 for 2022/01."), # non-integer float
(123.0 + 123.0j, "Failed to coerce complex value (123+123j) for 2022/01."), # complex w/ imag
(np.complex64(123.0 + 0.5j), "Failed to coerce complex64 value np.complex64(123+0.5j) for 2022/01."), # np.complex w/ imag
(np.array([1, 2]), "Failed to coerce ndarray value array([1, 2]) for 2022/01."), # 1D np.array with size != 1
(np.array([[1], [2]]), "Failed to coerce ndarray value array([[1],\n [2]]) for 2022/01."), # 2D np.array with size != 1
(fractions.Fraction(123, 2), "Failed to coerce Fraction value Fraction(123, 2) for 2022/01."), # Fraction
(decimal.Decimal("123.5"), "Failed to coerce Decimal value Decimal('123.5') for 2022/01."), # Decimal
]
)
def test_type_coercions_fail(val, error_msg):
p = Puzzle(2022, 1)
with pytest.raises(AocdError(error_msg)):
p._coerce_val(val)


def test_get_prose_cache(aocd_data_dir):
cached = aocd_data_dir / "other-user-id" / "2022_01_prose.2.html"
cached.parent.mkdir()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,5 +280,5 @@ def test_submit_float_warns(pook, capsys, caplog):
)
submit(1234.0, part="a", day=8, year=2022, session="whatever", reopen=False)
assert post.calls == 1
record = ("aocd.models", logging.WARNING, "coerced float value 1234.0 for 2022/08")
record = ("aocd.models", logging.WARNING, "coerced float value 1234.0 for 2022/08 to '1234'")
assert record in caplog.record_tuples

0 comments on commit 8422243

Please sign in to comment.