Skip to content

Commit

Permalink
Pass mypy with strict type-checking
Browse files Browse the repository at this point in the history
  • Loading branch information
Avasam committed Aug 29, 2024
1 parent 467154d commit 8304c91
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 37 deletions.
9 changes: 9 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

# Be strict about any broken references
nitpicky = True
nitpick_ignore = []

# Include Python intersphinx mapping to prevent failures
# jaraco/skeleton#51
Expand All @@ -40,3 +41,11 @@

# Preserve authored syntax for defaults
autodoc_preserve_defaults = True


# jaraco/jaraco.xkcd#1
nitpick_ignore += [
('py:class', 'jaraco.text.FoldedCase'),
('py:class', 'StrPath'),
('py:class', 'file_cache.FileCache'),
]
77 changes: 47 additions & 30 deletions jaraco/xkcd.py → jaraco/xkcd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from __future__ import annotations

import contextlib
import datetime
import importlib
import itertools
import os
import pathlib
import random
from collections.abc import Mapping
from typing import TYPE_CHECKING, TypeVar

import cachecontrol
from cachecontrol import heuristics
Expand All @@ -15,11 +19,22 @@
from jaraco.collections import dict_map
from jaraco.functools import except_

if TYPE_CHECKING:
from _typeshed import ConvertibleToInt, StrPath
from typing_extensions import Self

_ConvertibleToIntT = TypeVar("_ConvertibleToIntT", bound=ConvertibleToInt)
else:
_ConvertibleToIntT = TypeVar("_ConvertibleToIntT")

_T = TypeVar("_T")
_VT_co = TypeVar("_VT_co", covariant=True)


def make_cache(path=None):
def make_cache(path: StrPath | None = None) -> file_cache.FileCache:
default = pathlib.Path('~/.cache/xkcd').expanduser()
path = os.environ.get('XKCD_CACHE_DIR', path or default)
return file_cache.FileCache(path)
return file_cache.FileCache(path) # type: ignore[arg-type] # FileCache is using too restrictive pathlib.Path


session = cachecontrol.CacheControl(
Expand All @@ -30,10 +45,11 @@ def make_cache(path=None):


class Comic:
def __init__(self, number):
self._404(number) or self._load(number)
def __init__(self, number: int) -> None:
if not self._404(number):
self._load(number)

def _404(self, number):
def _404(self, number: int) -> Self | None:
"""
The 404 comic is not found.
>>> Comic(404)
Expand All @@ -44,53 +60,51 @@ def _404(self, number):
2008-04-01
"""
if number != 404:
return

vars(self).update(
num=404,
title="Not Found",
img=None,
year=2008,
month=4,
day=1,
)
return None

self.num = 404
self.title = "Not Found"
self.img = None
self.year = 2008
self.month = 4
self.day = 1
return self

def _load(self, number):
def _load(self, number: int) -> None:
resp = session.get(f'{number}/info.0.json')
resp.raise_for_status()
vars(self).update(self._fix_numbers(resp.json()))

@property
def date(self):
def date(self) -> datetime.date:
"""
>>> print(Comic(1).date)
2006-01-01
"""
return datetime.date(self.year, self.month, self.day)

@staticmethod
def _fix_numbers(ob):
def _fix_numbers(ob: Mapping[_T, _VT_co]) -> dict[_T, _VT_co | int]:
"""
Given a dict-like object ob, ensure any integers are integers.
"""
safe_int = except_(TypeError, ValueError, use='args[0]')(int)
return dict_map(safe_int, ob)
return dict_map(safe_int, ob) # type: ignore[no-untyped-call, no-any-return] # jaraco/jaraco.collections#14

@classmethod
def latest(cls):
def latest(cls) -> Self:
headers = {'Cache-Control': 'no-cache'}
resp = session.get('info.0.json', headers=headers)
resp.raise_for_status()
return cls(resp.json()['num'])

@classmethod
def all(cls):
def all(cls) -> map[Self]:
latest = cls.latest()
return map(cls, range(latest.number, 0, -1))

@classmethod
def random(cls):
def random(cls) -> Self:
"""
Return a randomly-selected comic.
Expand All @@ -101,7 +115,7 @@ def random(cls):
return cls(random.randint(1, latest.number))

@classmethod
def search(cls, text):
def search(cls, text: str) -> Self | None:
"""
Find a comic with the matching text
Expand All @@ -121,11 +135,11 @@ def search(cls, text):
return next(matches, None)

@property
def number(self):
def number(self) -> int:
return self.num

@property
def full_text(self):
def full_text(self) -> jaraco.text.FoldedCase:
"""
>>> comic = Comic.random()
>>> str(comic.date) in comic.full_text
Expand All @@ -134,16 +148,19 @@ def full_text(self):
values = itertools.chain(vars(self).values(), [self.date])
return jaraco.text.FoldedCase('|'.join(map(str, values)))

def __repr__(self):
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.number})'

def __str__(self):
def __str__(self) -> str:
return f'xkcd {self.number}:{self.title} ({self.img})'


with contextlib.suppress(ImportError):
core = importlib.import_module('pmxbot.core')
if TYPE_CHECKING:
import pmxbot.core as core
else:
core = importlib.import_module('pmxbot.core')

@core.command() # type: ignore # pragma: no cover
def xkcd(rest):
@core.command() # type: ignore[misc] # pragma: no cover
def xkcd(rest: str | None) -> Comic | None:
return Comic.search(rest) if rest else Comic.random() # pragma: no cover
Empty file added jaraco/xkcd/py.typed
Empty file.
18 changes: 17 additions & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[mypy]
# Is the project well-typed?
strict = False
strict = True

# Early opt-in even when strict = False
warn_unused_ignores = True
Expand All @@ -12,3 +12,19 @@ explicit_package_bases = True

# Disable overload-overlap due to many false-positives
disable_error_code = overload-overlap

# jaraco/jaraco.text#17
[mypy-jaraco.text.*]
ignore_missing_imports = True

# jaraco/tempora#35
[mypy-tempora.*]
ignore_missing_imports = True

# pmxbot/pmxbot#113
[mypy-pmxbot.*]
ignore_missing_imports = True

# requests/toolbelt#279
[mypy-requests_toolbelt.*]
ignore_missing_imports = True
1 change: 1 addition & 0 deletions newsfragments/1.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Complete annotations and add ``py.typed`` marker -- by :user:`Avasam`
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,3 @@ type = [


[tool.setuptools_scm]


[tool.pytest-enabler.mypy]
# Disabled due to jaraco/skeleton#143
6 changes: 4 additions & 2 deletions test_caching.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import pytest
from py.path import local # type: ignore[import-untyped]
from tempora import timing

from jaraco import xkcd


@pytest.fixture
def fresh_cache(tmpdir, monkeypatch):
def fresh_cache(tmpdir: local, monkeypatch: pytest.MonkeyPatch) -> None:
adapter = xkcd.session.get_adapter('http://')
cache = xkcd.make_cache(tmpdir / 'xkcd')
monkeypatch.setattr(adapter, 'cache', cache)
monkeypatch.setattr(adapter.controller, 'cache', cache)


def test_requests_cached(fresh_cache):
@pytest.mark.usefixtures("fresh_cache")
def test_requests_cached() -> None:
"""
A second pass loading Comics should be substantially faster than the
first.
Expand Down

0 comments on commit 8304c91

Please sign in to comment.