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

Pass mypy with strict type-checking #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@
extensions += ['sphinx.ext.extlinks']

# local

# 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 @@ -13,3 +13,19 @@ explicit_package_bases = True
disable_error_code =
# Disable due to many false positives
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
Loading