From a592146d9a6a20f4e0eed82eaab2c8edf3680e36 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 26 Aug 2024 03:39:20 -0400 Subject: [PATCH] Pass mypy with strict type-checking --- docs/conf.py | 7 +++ jaraco/{xkcd.py => xkcd/__init__.py} | 77 +++++++++++++++++----------- jaraco/xkcd/py.typed | 0 mypy.ini | 18 ++++++- newsfragments/1.feature.rst | 1 + pyproject.toml | 4 -- test_caching.py | 6 ++- 7 files changed, 76 insertions(+), 37 deletions(-) rename jaraco/{xkcd.py => xkcd/__init__.py} (63%) create mode 100644 jaraco/xkcd/py.typed create mode 100644 newsfragments/1.feature.rst diff --git a/docs/conf.py b/docs/conf.py index 240329c..cf80dcc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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'), +] diff --git a/jaraco/xkcd.py b/jaraco/xkcd/__init__.py similarity index 63% rename from jaraco/xkcd.py rename to jaraco/xkcd/__init__.py index 4edf9e3..6f183bb 100644 --- a/jaraco/xkcd.py +++ b/jaraco/xkcd/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import datetime import importlib @@ -5,6 +7,8 @@ import os import pathlib import random +from collections.abc import Mapping +from typing import TYPE_CHECKING, TypeVar import cachecontrol from cachecontrol import heuristics @@ -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( @@ -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) @@ -44,25 +60,23 @@ 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 @@ -70,27 +84,27 @@ def date(self): 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. @@ -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 @@ -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 @@ -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 diff --git a/jaraco/xkcd/py.typed b/jaraco/xkcd/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/mypy.ini b/mypy.ini index efcb8cb..4dc9cb8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -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 @@ -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 diff --git a/newsfragments/1.feature.rst b/newsfragments/1.feature.rst new file mode 100644 index 0000000..88e9b7c --- /dev/null +++ b/newsfragments/1.feature.rst @@ -0,0 +1 @@ +Complete annotations and add ``py.typed`` marker -- by :user:`Avasam` diff --git a/pyproject.toml b/pyproject.toml index bd97927..05c5bd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,3 @@ type = [ [tool.setuptools_scm] - - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 diff --git a/test_caching.py b/test_caching.py index f256faf..3297f0b 100644 --- a/test_caching.py +++ b/test_caching.py @@ -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.