From 4e93fb45117aaf800fdf40ac526209e664af62b6 Mon Sep 17 00:00:00 2001 From: Ihor Kalnytskyi Date: Fri, 28 Jun 2024 01:53:59 +0300 Subject: [PATCH] Embrace Ruff linter, use ALL rules Ruff comes with many handy rules out of box. Even though some of them are weird, some of them not applicable to this project, we better maintain the ignore list rather than select list. It looks like most of them are good enough to try. --- docs/conf.py | 2 +- examples/flask/wsgi.py | 2 +- pyproject.toml | 36 ++++++-------------------- src/picobox/_box.py | 33 +++++++++++++----------- src/picobox/_scopes.py | 14 +++++------ src/picobox/_stack.py | 46 ++++++++++++++++++---------------- src/picobox/ext/asgiscopes.py | 17 +++++++------ src/picobox/ext/flaskscopes.py | 6 +++-- src/picobox/ext/wsgiscopes.py | 19 ++++++++------ tests/ext/test_asgiscopes.py | 6 ++--- tests/ext/test_wsgiscopes.py | 6 ++--- tests/test_box.py | 10 ++++---- tests/test_stack.py | 10 ++++---- 13 files changed, 101 insertions(+), 106 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9bc6963..edc1950 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,7 +5,7 @@ # -- Project settings project = "Picobox" author = "Ihor Kalnytskyi" -copyright = "2017, Ihor Kalnytskyi" +project_copyright = "2017, Ihor Kalnytskyi" release = importlib.metadata.version("picobox") version = ".".join(release.split(".")[:2]) diff --git a/examples/flask/wsgi.py b/examples/flask/wsgi.py index 2334234..a58b2d8 100644 --- a/examples/flask/wsgi.py +++ b/examples/flask/wsgi.py @@ -19,4 +19,4 @@ # to test the app since any attempt to override dependencies in tests # will fail due to later attempt to push a new box by request hooks. picobox.push(box) -from example import app # noqa +from example import app diff --git a/pyproject.toml b/pyproject.toml index a6cecb9..1d5aaef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ scripts.run = "python -m pytest --strict-markers {args:-vv}" [tool.hatch.envs.lint] detached = true -dependencies = ["ruff == 0.4.*"] +dependencies = ["ruff == 0.5.*"] scripts.run = ["ruff check {args:.}", "ruff format --check --diff {args:.}"] [tool.hatch.envs.type] @@ -55,39 +55,17 @@ scripts.run = "sphinx-build -W -b html docs docs/_build/" line-length = 100 [tool.ruff.lint] -select = [ - "F", - "E", - "W", - "I", - "D", - "UP", - "S", - "FBT", - "B", - "C4", - "DTZ", - "T10", - "ISC", - "PIE", - "T20", - "PYI", - "PT", - "RET", - "SLF", - "SIM", - "TCH", - "ERA", - "RUF", -] -ignore = ["D107", "D203", "D213", "D401", "S101", "B904", "ISC001", "PT011", "SIM117"] +select = ["ALL"] +ignore = ["ANN", "PLR", "D107", "D203", "D213", "D401", "SIM117", "N801", "PLW2901", "PERF203", "COM812", "ISC001"] [tool.ruff.lint.isort] known-first-party = ["picobox"] [tool.ruff.lint.per-file-ignores] -"examples/*" = ["I", "D", "T20"] -"tests/*" = ["D"] +"docs/*" = ["INP001"] +"examples/*" = ["I", "D", "T20", "INP001"] +"examples/flask/wsgi.py" = ["F401", "E402"] +"tests/*" = ["D", "S101", "ARG001", "BLE001", "INP001"] [tool.mypy] python_version = "3.8" diff --git a/src/picobox/_box.py b/src/picobox/_box.py index aa22c24..31994b3 100644 --- a/src/picobox/_box.py +++ b/src/picobox/_box.py @@ -1,5 +1,7 @@ """Box container.""" +from __future__ import annotations + import functools import inspect import threading @@ -48,8 +50,8 @@ def do(magic): """ def __init__(self) -> None: - self._store: t.Dict[t.Hashable, t.Tuple[_scopes.Scope, t.Callable[[], t.Any]]] = {} - self._scope_instances: t.Dict[t.Type[_scopes.Scope], _scopes.Scope] = {} + self._store: dict[t.Hashable, tuple[_scopes.Scope, t.Callable[[], t.Any]]] = {} + self._scope_instances: dict[type[_scopes.Scope], _scopes.Scope] = {} self._lock = threading.RLock() def put( @@ -57,8 +59,8 @@ def put( key: t.Hashable, value: t.Any = _unset, *, - factory: t.Optional[t.Callable[[], t.Any]] = None, - scope: t.Optional[t.Type[_scopes.Scope]] = None, + factory: t.Callable[[], t.Any] | None = None, + scope: type[_scopes.Scope] | None = None, ) -> None: """Define a dependency (aka service) within the box instance. @@ -80,13 +82,16 @@ def put( :raises ValueError: If both `value` and `factory` are passed. """ if value is _unset and factory is None: - raise TypeError("Box.put() missing 1 required argument: either 'value' or 'factory'") + error_message = "Box.put() missing 1 required argument: either 'value' or 'factory'" + raise TypeError(error_message) if value is not _unset and factory is not None: - raise TypeError("Box.put() takes either 'value' or 'factory', not both") + error_message = "Box.put() takes either 'value' or 'factory', not both" + raise TypeError(error_message) if value is not _unset and scope is not None: - raise TypeError("Box.put() takes 'scope' when 'factory' provided") + error_message = "Box.put() takes 'scope' when 'factory' provided" + raise TypeError(error_message) def _factory() -> t.Any: return value @@ -168,8 +173,8 @@ def pass_( self, key: t.Hashable, *, - as_: t.Optional[str] = None, - ) -> "t.Callable[[t.Callable[P, R[T]]], t.Callable[P, R[T]]]": + as_: str | None = None, + ) -> t.Callable[[t.Callable[P, R[T]]], t.Callable[P, R[T]]]: r"""Pass a dependency to a function if nothing explicitly passed. The decorator implements late binding which means it does not require @@ -185,7 +190,7 @@ def pass_( :raises KeyError: If no dependencies saved under `key` in the box. """ - def decorator(fn: "t.Callable[P, R[T]]") -> "t.Callable[P, R[T]]": + def decorator(fn: t.Callable[P, R[T]]) -> t.Callable[P, R[T]]: # If pass_ decorator is called second time (or more), we can squash # the calls into one and reduce runtime costs of injection. if hasattr(fn, "__dependencies__"): @@ -193,7 +198,7 @@ def decorator(fn: "t.Callable[P, R[T]]") -> "t.Callable[P, R[T]]": return fn @functools.wraps(fn) - def fn_with_dependencies(*args: "P.args", **kwargs: "P.kwargs") -> "R[T]": + def fn_with_dependencies(*args: P.args, **kwargs: P.kwargs) -> R[T]: signature = inspect.signature(fn) arguments = signature.bind_partial(*args, **kwargs) @@ -212,7 +217,7 @@ def fn_with_dependencies(*args: "P.args", **kwargs: "P.kwargs") -> "R[T]": if inspect.iscoroutinefunction(fn): @functools.wraps(fn) - async def wrapper(*args: "P.args", **kwargs: "P.kwargs") -> "T": + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: return await t.cast(t.Awaitable["T"], fn_with_dependencies(*args, **kwargs)) else: wrapper = fn_with_dependencies # type: ignore[assignment] @@ -264,8 +269,8 @@ def put( key: t.Hashable, value: t.Any = _unset, *, - factory: t.Optional[t.Callable[[], t.Any]] = None, - scope: t.Optional[t.Type[_scopes.Scope]] = None, + factory: t.Callable[[], t.Any] | None = None, + scope: type[_scopes.Scope] | None = None, ) -> None: """Same as :meth:`Box.put` but applies to first underlying box.""" return self._boxes[0].put(key, value, factory=factory, scope=scope) diff --git a/src/picobox/_scopes.py b/src/picobox/_scopes.py index 521d929..7c3fdf8 100644 --- a/src/picobox/_scopes.py +++ b/src/picobox/_scopes.py @@ -1,5 +1,7 @@ """Scope interface and builtin implementations.""" +from __future__ import annotations + import abc import contextvars as _contextvars import threading @@ -35,7 +37,7 @@ class singleton(Scope): """Share instances across application.""" def __init__(self) -> None: - self._store: t.Dict[t.Hashable, t.Any] = {} + self._store: dict[t.Hashable, t.Any] = {} def set(self, key: t.Hashable, value: t.Any) -> None: self._store[key] = value @@ -61,7 +63,7 @@ def get(self, key: t.Hashable) -> t.Any: try: rv = self._local.store[key] except AttributeError: - raise KeyError(key) + raise KeyError(key) from None return rv @@ -77,13 +79,11 @@ class contextvars(Scope): .. versionadded:: 2.1 """ - _store_obj: ( - "weakref.WeakKeyDictionary[Scope, t.Dict[t.Hashable, _contextvars.ContextVar[t.Any]]]" - ) + _store_obj: weakref.WeakKeyDictionary[Scope, dict[t.Hashable, _contextvars.ContextVar[t.Any]]] _store_obj = weakref.WeakKeyDictionary() @property - def _store(self) -> t.Dict[t.Hashable, _contextvars.ContextVar[t.Any]]: + def _store(self) -> dict[t.Hashable, _contextvars.ContextVar[t.Any]]: try: scope_store = self._store_obj[self] except KeyError: @@ -98,7 +98,7 @@ def get(self, key: t.Hashable) -> t.Any: try: return self._store[key].get() except LookupError: - raise KeyError(key) + raise KeyError(key) from None class noscope(Scope): diff --git a/src/picobox/_stack.py b/src/picobox/_stack.py index ff61619..ec07c86 100644 --- a/src/picobox/_stack.py +++ b/src/picobox/_stack.py @@ -1,15 +1,18 @@ """Picobox API to work with a box at the top of the stack.""" +from __future__ import annotations + import contextlib import threading import typing as t -from . import _scopes from ._box import Box, ChainBox, _unset if t.TYPE_CHECKING: import typing_extensions + from ._scopes import Scope + P = typing_extensions.ParamSpec("P") T = typing_extensions.TypeVar("T") R = t.Union[T, t.Awaitable[T]] @@ -26,19 +29,20 @@ def _create_push_context_manager( try: yield box finally: - popped_box = pop_callback() - - # Ensure the poped box is the same that was submitted by this exact - # context manager. It may happen if someone messed up with order of - # push() and pop() calls. Normally, push() should be used as a context - # manager to avoid this issue. - assert popped_box is box + if pop_callback() is not box: + error_message = ( + "The .push() context manager has popped the wrong Box instance, " + "meaning it did not pop the one that was pushed. This could " + "occur if the .push() context manager is manipulated manually " + "instead of using the 'with' statement." + ) + raise RuntimeError(error_message) from None class _CurrentBoxProxy(Box): """Delegates operations to the Box instance at the top of the stack.""" - def __init__(self, stack: t.List[Box]) -> None: + def __init__(self, stack: list[Box]) -> None: self._stack = stack def __getattribute__(self, name: str) -> t.Any: @@ -48,7 +52,7 @@ def __getattribute__(self, name: str) -> t.Any: try: return getattr(self._stack[-1], name) except IndexError: - raise RuntimeError(_ERROR_MESSAGE_EMPTY_STACK) + raise RuntimeError(_ERROR_MESSAGE_EMPTY_STACK) from None class Stack: @@ -95,9 +99,9 @@ def do(magic): .. versionadded:: 2.2 """ - def __init__(self, name: t.Optional[str] = None) -> None: + def __init__(self, name: str | None = None) -> None: self._name = name or f"0x{id(t):x}" - self._stack: t.List[Box] = [] + self._stack: list[Box] = [] self._lock = threading.Lock() # A proxy object that proxies all calls to a box instance on the top @@ -157,15 +161,15 @@ def pop(self) -> Box: try: return self._stack.pop() except IndexError: - raise RuntimeError(_ERROR_MESSAGE_EMPTY_STACK) + raise RuntimeError(_ERROR_MESSAGE_EMPTY_STACK) from None def put( self, key: t.Hashable, value: t.Any = _unset, *, - factory: t.Optional[t.Callable[[], t.Any]] = None, - scope: t.Optional[t.Type[_scopes.Scope]] = None, + factory: t.Callable[[], t.Any] | None = None, + scope: type[Scope] | None = None, ) -> None: """The same as :meth:`Box.put` but for a box at the top of the stack.""" return self._current_box.put(key, value, factory=factory, scope=scope) @@ -178,8 +182,8 @@ def pass_( self, key: t.Hashable, *, - as_: t.Optional[str] = None, - ) -> "t.Callable[[t.Callable[P, R[T]]], t.Callable[P, R[T]]]": + as_: str | None = None, + ) -> t.Callable[[t.Callable[P, R[T]]], t.Callable[P, R[T]]]: """The same as :meth:`Box.pass_` but for a box at the top.""" return Box.pass_(self._current_box, key, as_=as_) @@ -207,8 +211,8 @@ def put( key: t.Hashable, value: t.Any = _unset, *, - factory: t.Optional[t.Callable[[], t.Any]] = None, - scope: t.Optional[t.Type[_scopes.Scope]] = None, + factory: t.Callable[[], t.Any] | None = None, + scope: type[Scope] | None = None, ) -> None: """The same as :meth:`Stack.put` but for a shared stack instance.""" return _instance.put(key, value, factory=factory, scope=scope) @@ -222,7 +226,7 @@ def get(key: t.Hashable, default: t.Any = _unset) -> t.Any: def pass_( key: t.Hashable, *, - as_: t.Optional[str] = None, -) -> "t.Callable[[t.Callable[P, R[T]]], t.Callable[P, R[T]]]": + as_: str | None = None, +) -> t.Callable[[t.Callable[P, R[T]]], t.Callable[P, R[T]]]: """The same as :meth:`Stack.pass_` but for a shared stack instance.""" return _instance.pass_(key, as_=as_) diff --git a/src/picobox/ext/asgiscopes.py b/src/picobox/ext/asgiscopes.py index 7409883..74b1211 100644 --- a/src/picobox/ext/asgiscopes.py +++ b/src/picobox/ext/asgiscopes.py @@ -1,5 +1,7 @@ """Scopes for ASGI applications.""" +from __future__ import annotations + import contextvars import typing as t import weakref @@ -16,8 +18,8 @@ ASGIApplication = t.Callable[[ASGIScope, ASGIReceive, ASGISend], t.Awaitable[None]] -_current_app_store: "StoreCtxVar" = contextvars.ContextVar(f"{__name__}.current-app-store") -_current_req_store: "StoreCtxVar" = contextvars.ContextVar(f"{__name__}.current-req-store") +_current_app_store: StoreCtxVar = contextvars.ContextVar(f"{__name__}.current-app-store") +_current_req_store: StoreCtxVar = contextvars.ContextVar(f"{__name__}.current-req-store") class ScopeMiddleware: @@ -35,14 +37,14 @@ class ScopeMiddleware: :param app: The ASGI application to wrap. """ - def __init__(self, app: "ASGIApplication") -> None: + def __init__(self, app: ASGIApplication) -> None: self.app = app # Since we want stored objects to be garbage collected as soon as the # storing scope instance is destroyed, scope instances have to be # weakly referenced. self.store: Store = weakref.WeakKeyDictionary() - async def __call__(self, scope: "ASGIScope", receive: "ASGIReceive", send: "ASGISend") -> None: + async def __call__(self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend) -> None: """Define scopes and invoke the ASGI application.""" # Storing the ASGI application's scope state within a ScopeMiddleware # instance because it's assumed that each ASGI middleware is typically @@ -62,20 +64,21 @@ async def __call__(self, scope: "ASGIScope", receive: "ASGIReceive", send: "ASGI class _asgiscope(picobox.Scope): """A base class for ASGI scopes.""" - _store_cvar: "StoreCtxVar" + _store_cvar: StoreCtxVar @property - def _store(self) -> t.Dict[t.Hashable, t.Any]: + def _store(self) -> dict[t.Hashable, t.Any]: try: store = self._store_cvar.get() except LookupError: - raise RuntimeError( + error_message = ( "Working outside of ASGI context.\n" "\n" "This typically means that you attempted to use picobox with " "ASGI scopes, but 'picobox.ext.asgiscopes.ScopeMiddleware' has " "not been used with your ASGI application." ) + raise RuntimeError(error_message) from None try: scope_store = store[self] diff --git a/src/picobox/ext/flaskscopes.py b/src/picobox/ext/flaskscopes.py index 96c4432..1c093d5 100644 --- a/src/picobox/ext/flaskscopes.py +++ b/src/picobox/ext/flaskscopes.py @@ -1,5 +1,7 @@ """Scopes for Flask framework.""" +from __future__ import annotations + import typing as t import weakref @@ -10,7 +12,7 @@ if t.TYPE_CHECKING: class _flask_store_obj(t.Protocol): - __dependencies__: weakref.WeakKeyDictionary[picobox.Scope, t.Dict[t.Hashable, t.Any]] + __dependencies__: weakref.WeakKeyDictionary[picobox.Scope, dict[t.Hashable, t.Any]] class _flaskscope(picobox.Scope): @@ -20,7 +22,7 @@ def __init__(self, store_obj: object) -> None: self._store_obj = t.cast("_flask_store_obj", store_obj) @property - def _store(self) -> t.Dict[t.Hashable, t.Any]: + def _store(self) -> dict[t.Hashable, t.Any]: try: store = self._store_obj.__dependencies__ except AttributeError: diff --git a/src/picobox/ext/wsgiscopes.py b/src/picobox/ext/wsgiscopes.py index 3289778..ea952f0 100644 --- a/src/picobox/ext/wsgiscopes.py +++ b/src/picobox/ext/wsgiscopes.py @@ -1,5 +1,7 @@ """Scopes for WSGI applications.""" +from __future__ import annotations + import contextvars import typing as t import weakref @@ -13,8 +15,8 @@ StoreCtxVar = contextvars.ContextVar[Store] -_current_app_store: "StoreCtxVar" = contextvars.ContextVar(f"{__name__}.current-app-store") -_current_req_store: "StoreCtxVar" = contextvars.ContextVar(f"{__name__}.current-req-store") +_current_app_store: StoreCtxVar = contextvars.ContextVar(f"{__name__}.current-app-store") +_current_req_store: StoreCtxVar = contextvars.ContextVar(f"{__name__}.current-req-store") class ScopeMiddleware: @@ -32,7 +34,7 @@ class ScopeMiddleware: :param app: The WSGI application to wrap. """ - def __init__(self, app: "WSGIApplication") -> None: + def __init__(self, app: WSGIApplication) -> None: self.app = app # Since we want stored objects to be garbage collected as soon as the # storing scope instance is destroyed, scope instances have to be @@ -41,8 +43,8 @@ def __init__(self, app: "WSGIApplication") -> None: def __call__( self, - environ: "WSGIEnvironment", - start_response: "StartResponse", + environ: WSGIEnvironment, + start_response: StartResponse, ) -> t.Iterable[bytes]: """Define scopes and invoke the WSGI application.""" # Storing the WSGI application's scope state within a ScopeMiddleware @@ -64,20 +66,21 @@ def __call__( class _wsgiscope(picobox.Scope): """A base class for WSGI scopes.""" - _store_cvar: "StoreCtxVar" + _store_cvar: StoreCtxVar @property - def _store(self) -> t.Dict[t.Hashable, t.Any]: + def _store(self) -> dict[t.Hashable, t.Any]: try: store = self._store_cvar.get() except LookupError: - raise RuntimeError( + error_message = ( "Working outside of WSGI context.\n" "\n" "This typically means that you attempted to use picobox with " "WSGI scopes, but 'picobox.ext.wsgiscopes.ScopeMiddleware' has " "not been used with your WSGI application." ) + raise RuntimeError(error_message) from None try: scope_store = store[self] diff --git a/tests/ext/test_asgiscopes.py b/tests/ext/test_asgiscopes.py index 5963739..09bc6af 100644 --- a/tests/ext/test_asgiscopes.py +++ b/tests/ext/test_asgiscopes.py @@ -242,7 +242,7 @@ async def endpoint2(): app_factory( ("/1", endpoint1), ("/2", endpoint2), - ) + ), ) client.run_endpoint("/1") client.run_endpoint("/2") @@ -270,7 +270,7 @@ async def endpoint2(): app_factory( ("/1", endpoint1), ("/2", endpoint2), - ) + ), ) client.run_endpoint("/1") client.run_endpoint("/2") @@ -407,7 +407,7 @@ async def endpoint2(): app_factory( ("/1", endpoint1), ("/2", endpoint2), - ) + ), ) await asyncio.gather( diff --git a/tests/ext/test_wsgiscopes.py b/tests/ext/test_wsgiscopes.py index a19f3b2..f09eb4a 100644 --- a/tests/ext/test_wsgiscopes.py +++ b/tests/ext/test_wsgiscopes.py @@ -207,7 +207,7 @@ def endpoint2(): app_factory( ("/1", endpoint1), ("/2", endpoint2), - ) + ), ) client.run_endpoint("/1") client.run_endpoint("/2") @@ -235,7 +235,7 @@ def endpoint2(): app_factory( ("/1", endpoint1), ("/2", endpoint2), - ) + ), ) client.run_endpoint("/1") client.run_endpoint("/2") @@ -389,7 +389,7 @@ def endpoint2(): app_factory( ("/1", endpoint1), ("/2", endpoint2), - ) + ), ) with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: diff --git a/tests/test_box.py b/tests/test_box.py index 774e636..568fc9c 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -66,7 +66,7 @@ def get(self, key): [ testbox.get("the-key"), testbox.get("the-key"), - ] + ], ) namespace = "two" @@ -74,7 +74,7 @@ def get(self, key): [ testbox.get("the-key"), testbox.get("the-key"), - ] + ], ) assert len(objects) == 4 @@ -471,7 +471,7 @@ def fn(a, b, c): itertools.dropwhile( lambda frame: frame[2] != request.function.__name__, traceback.extract_stack(), - ) + ), ) return backtrace[1:-1] @@ -501,7 +501,7 @@ def fn(a, b, c, d): itertools.dropwhile( lambda frame: frame[2] != request.function.__name__, traceback.extract_stack(), - ) + ), ) return backtrace[1:-1] @@ -523,7 +523,7 @@ async def fn(a, b, c): itertools.dropwhile( lambda frame: frame[2] != request.function.__name__, traceback.extract_stack(), - ) + ), ) return backtrace[1:-1] diff --git a/tests/test_stack.py b/tests/test_stack.py index 092ee62..a9585cb 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -79,7 +79,7 @@ def get(self, key): [ teststack.get("the-key"), teststack.get("the-key"), - ] + ], ) namespace = "two" @@ -87,7 +87,7 @@ def get(self, key): [ teststack.get("the-key"), teststack.get("the-key"), - ] + ], ) assert len(objects) == 4 @@ -588,7 +588,7 @@ def fn(a, b, c): itertools.dropwhile( lambda frame: frame[2] != request.function.__name__, traceback.extract_stack(), - ) + ), ) return backtrace[1:-1] @@ -619,7 +619,7 @@ def fn(a, b, c, d): itertools.dropwhile( lambda frame: frame[2] != request.function.__name__, traceback.extract_stack(), - ) + ), ) return backtrace[1:-1] @@ -642,7 +642,7 @@ async def fn(a, b, c): itertools.dropwhile( lambda frame: frame[2] != request.function.__name__, traceback.extract_stack(), - ) + ), ) return backtrace[1:-1]