Skip to content

Commit eaef1d0

Browse files
committed
lint: add type hints and test for them
Introduce `mypy` and apply strict type hints. Declare typed support via marker file and classifier. Signed-off-by: Mike Fiedler <[email protected]>
1 parent 04d7bd1 commit eaef1d0

File tree

5 files changed

+54
-16
lines changed

5 files changed

+54
-16
lines changed

pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,23 @@ no_lines_before = "THIRDPARTY"
3131
sections = "FUTURE,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"
3232
default_section = "THIRDPARTY"
3333
known_first_party = "pyramid_retry"
34+
35+
[tool.mypy]
36+
check_untyped_defs = true
37+
disallow_any_generics = true
38+
disallow_incomplete_defs = true
39+
disallow_subclassing_any = true
40+
disallow_untyped_calls = true
41+
disallow_untyped_decorators = true
42+
disallow_untyped_defs = true
43+
no_implicit_reexport = true
44+
warn_redundant_casts = true
45+
warn_return_any = true
46+
warn_unused_configs = true
47+
warn_unused_ignores = true
48+
plugins = ["mypy_zope:plugin"]
49+
50+
[[tool.mypy.overrides]]
51+
# https://github.com/Pylons/pyramid/issues/2638
52+
module = "pyramid.*"
53+
ignore_missing_imports = true

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,6 @@ def readfile(name):
6161
'Programming Language :: Python :: 3.11',
6262
'Programming Language :: Python :: Implementation :: CPython',
6363
'Programming Language :: Python :: Implementation :: PyPy',
64+
'Typing :: Typed',
6465
],
6566
)

src/pyramid_retry/__init__.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from __future__ import annotations
12
import inspect
23
from pyramid.config import PHASE1_CONFIG
34
from pyramid.exceptions import ConfigurationError
5+
import typing as t
46
from zope.interface import (
57
Attribute,
68
Interface,
@@ -9,6 +11,13 @@
911
implementer,
1012
)
1113

14+
if t.TYPE_CHECKING: # pragma: no cover
15+
from collections.abc import Callable
16+
from pyramid.config import Configurator
17+
from pyramid.request import Request
18+
from pyramid.response import Response
19+
from pyramid.router import Router
20+
1221

1322
class IRetryableError(Interface):
1423
"""
@@ -55,7 +64,9 @@ class BeforeRetry(object):
5564
5665
"""
5766

58-
def __init__(self, request, exception, response=None):
67+
def __init__(
68+
self, request: Request, exception: Exception, response: Response = None
69+
):
5970
self.request = request
6071
self.environ = request.environ
6172
self.exception = exception
@@ -67,7 +78,10 @@ class RetryableException(Exception):
6778
"""A retryable exception should be raised when an error occurs."""
6879

6980

70-
def RetryableExecutionPolicy(attempts=3, activate_hook=None):
81+
def RetryableExecutionPolicy(
82+
attempts: int = 3,
83+
activate_hook: Callable[..., int | None] | None = None,
84+
) -> Callable[..., Response]:
7185
"""
7286
Create a :term:`execution policy` that catches any
7387
:term:`retryable error` and sends it through the pipeline again up to
@@ -81,7 +95,7 @@ def RetryableExecutionPolicy(attempts=3, activate_hook=None):
8195
"""
8296
assert attempts > 0
8397

84-
def retry_policy(environ, router):
98+
def retry_policy(environ: dict[str, t.Any], router: Router) -> Response:
8599
# make the original request
86100
request_ctx = router.request_context(environ)
87101
request = request_ctx.begin()
@@ -164,7 +178,7 @@ def retry_policy(environ, router):
164178
return retry_policy
165179

166180

167-
def mark_error_retryable(error):
181+
def mark_error_retryable(error: t.Any) -> None:
168182
"""
169183
Mark an exception instance or type as retryable. If this exception
170184
is caught by ``pyramid_retry`` then it may retry the request.
@@ -173,14 +187,14 @@ def mark_error_retryable(error):
173187
if isinstance(error, Exception):
174188
alsoProvides(error, IRetryableError)
175189
elif inspect.isclass(error) and issubclass(error, Exception):
176-
classImplements(error, IRetryableError)
190+
classImplements(error, IRetryableError) # type: ignore[misc] # TODO: https://github.com/Shoobx/mypy-zope/issues/50
177191
else:
178192
raise ValueError(
179193
'only exception objects or types may be marked retryable'
180194
)
181195

182196

183-
def is_error_retryable(request, exc):
197+
def is_error_retryable(request: Request, exc: Exception | None) -> bool:
184198
"""
185199
Return ``True`` if the exception is recognized as :term:`retryable error`.
186200
@@ -197,7 +211,7 @@ def is_error_retryable(request, exc):
197211
)
198212

199213

200-
def is_last_attempt(request):
214+
def is_last_attempt(request: Request) -> bool:
201215
"""
202216
Return ``True`` if the request is on its last attempt, meaning that
203217
``pyramid_retry`` will not be issuing any new attempts, regardless of
@@ -213,7 +227,7 @@ def is_last_attempt(request):
213227
if attempt is None or attempts is None:
214228
return True
215229

216-
return attempt + 1 == attempts
230+
return attempt + 1 == attempts # type: ignore[no-any-return] # TODO: what's the best way to determine these?
217231

218232

219233
class RetryableErrorPredicate(object):
@@ -226,20 +240,20 @@ class RetryableErrorPredicate(object):
226240
227241
"""
228242

229-
def __init__(self, val, config):
243+
def __init__(self, val: t.Any, config: Configurator):
230244
if not isinstance(val, bool):
231245
raise ConfigurationError(
232246
'The "retryable_error" view predicate value must be '
233247
'True or False.',
234248
)
235249
self.val = val
236250

237-
def text(self):
251+
def text(self) -> str:
238252
return 'retryable_error = %s' % (self.val,)
239253

240254
phash = text
241255

242-
def __call__(self, context, request):
256+
def __call__(self, context: t.Any, request: Request) -> bool:
243257
exc = getattr(request, 'exception', None)
244258
is_retryable = is_error_retryable(request, exc)
245259
return (self.val and is_retryable) or (
@@ -257,25 +271,25 @@ class LastAttemptPredicate(object):
257271
258272
"""
259273

260-
def __init__(self, val, config):
274+
def __init__(self, val: t.Any, config: Configurator):
261275
if not isinstance(val, bool):
262276
raise ConfigurationError(
263277
'The "last_retry_attempt" view predicate value must be '
264278
'True or False.',
265279
)
266280
self.val = val
267281

268-
def text(self):
282+
def text(self) -> str:
269283
return 'last_retry_attempt = %s' % (self.val,)
270284

271285
phash = text
272286

273-
def __call__(self, context, request):
287+
def __call__(self, contex: t.Any, request: Request) -> bool:
274288
is_last = is_last_attempt(request)
275289
return (self.val and is_last) or (not self.val and not is_last)
276290

277291

278-
def includeme(config):
292+
def includeme(config: Configurator) -> None:
279293
"""
280294
Activate the ``pyramid_retry`` execution policy in your application.
281295
@@ -294,7 +308,7 @@ def includeme(config):
294308
config.add_view_predicate('last_retry_attempt', LastAttemptPredicate)
295309
config.add_view_predicate('retryable_error', RetryableErrorPredicate)
296310

297-
def register():
311+
def register() -> None:
298312
attempts = int(settings.get('retry.attempts') or 3)
299313
settings['retry.attempts'] = attempts
300314

src/pyramid_retry/py.typed

Whitespace-only changes.

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ commands =
3939
isort --check-only --df src/pyramid_retry tests setup.py
4040
black --check --diff src/pyramid_retry tests setup.py
4141
flake8 src/pyramid_retry tests setup.py
42+
mypy src/pyramid_retry
4243
check-manifest
4344
# build sdist/wheel
4445
python -m build .
@@ -50,6 +51,8 @@ deps =
5051
flake8
5152
flake8-bugbear
5253
isort
54+
mypy
55+
mypy-zope
5356
readme_renderer
5457
twine
5558

0 commit comments

Comments
 (0)