diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5f9e42d..8c9d64d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: version: + - "3.11" - "3.12" steps: - uses: actions/checkout@v2 diff --git a/docs/index.md b/docs/index.md index 9ed4f71..652d165 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ Selva is a tool for creating ASGI applications that are easy to build and maintain. It is built on top of [asgikit](https://pypi.org/project/asgikit/) and comes with -a dependency injection system built upon Python type annotations. +a dependency injection system built upon Python type annotations. It is compatible with python 3.11+. ## Quickstart diff --git a/examples/middleware/application/middleware.py b/examples/middleware/application/middleware.py index 5c09637..8f19600 100644 --- a/examples/middleware/application/middleware.py +++ b/examples/middleware/application/middleware.py @@ -1,6 +1,7 @@ import base64 from datetime import datetime from http import HTTPStatus + from asgikit.requests import Request from asgikit.responses import respond_status from loguru import logger @@ -34,9 +35,7 @@ async def __call__(self, chain, request: Request): request_line = f"{request.method} {request.path} HTTP/{request.http_version}" status = request.response.status - logger.info( - '{} "{}" {} {}', client, request_line, status.value, status.phrase - ) + logger.info('{} "{}" {} {}', client, request_line, status.value, status.phrase) class AuthMiddleware(Middleware): diff --git a/examples/settings/application.py b/examples/settings/application.py index 1aea226..51a6d6b 100644 --- a/examples/settings/application.py +++ b/examples/settings/application.py @@ -14,4 +14,4 @@ class Controller: @get async def index(self, request: Request): - await respond_text(request.response, self.settings.application.message) + await respond_text(request.response, self.settings.message) diff --git a/examples/settings/configuration/settings.yaml b/examples/settings/configuration/settings.yaml index 7f71292..c4908c7 100644 --- a/examples/settings/configuration/settings.yaml +++ b/examples/settings/configuration/settings.yaml @@ -1,7 +1 @@ -message: Hello, World! -application: - message: ${MESSAGE:Hello, World!} -logging: - root: INFO - level: - application: WARNING \ No newline at end of file +message: ${MESSAGE:Hello, World!} \ No newline at end of file diff --git a/examples/settings/configuration/settings_dev.yaml b/examples/settings/configuration/settings_dev.yaml index fe293c1..271dd36 100644 --- a/examples/settings/configuration/settings_dev.yaml +++ b/examples/settings/configuration/settings_dev.yaml @@ -1,2 +1 @@ -application: - message: ${MESSAGE:Hello, dev World!} +message: ${MESSAGE:Hello, dev World!} diff --git a/pyproject.toml b/pyproject.toml index 634306d..91d5728 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", @@ -27,7 +27,7 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.12" +python = "^3.11" asgikit = "^0.5" pydantic = "^2.4" loguru = "^0.7" diff --git a/src/selva/_util/import_item.py b/src/selva/_util/import_item.py index 69d7987..ba02cb1 100644 --- a/src/selva/_util/import_item.py +++ b/src/selva/_util/import_item.py @@ -1,6 +1,5 @@ from importlib import import_module - __all__ = ("import_item",) diff --git a/src/selva/_util/maybe_async.py b/src/selva/_util/maybe_async.py index 3a8465f..acd1865 100644 --- a/src/selva/_util/maybe_async.py +++ b/src/selva/_util/maybe_async.py @@ -2,10 +2,12 @@ import functools import inspect from collections.abc import Awaitable, Callable -from typing import Any +from typing import Any, ParamSpec +P = ParamSpec("P") -async def maybe_async[**P]( + +async def maybe_async( target: Awaitable | Callable[P, Any], *args: P.args, **kwargs: P.kwargs ) -> Any: if inspect.isawaitable(target): diff --git a/src/selva/configuration/settings.py b/src/selva/configuration/settings.py index 1c52954..e3b24e7 100644 --- a/src/selva/configuration/settings.py +++ b/src/selva/configuration/settings.py @@ -8,7 +8,10 @@ from loguru import logger from selva.configuration.defaults import default_settings -from selva.configuration.environment import parse_settings_from_env, replace_variables_recursive +from selva.configuration.environment import ( + parse_settings_from_env, + replace_variables_recursive, +) __all__ = ("Settings", "SettingsError", "get_settings") diff --git a/src/selva/di/container.py b/src/selva/di/container.py index aed0516..56ac1d7 100644 --- a/src/selva/di/container.py +++ b/src/selva/di/container.py @@ -2,7 +2,7 @@ import inspect from collections.abc import AsyncGenerator, Awaitable, Generator, Iterable from types import FunctionType, ModuleType -from typing import Any, Type +from typing import Any, Type, TypeVar from loguru import logger @@ -19,6 +19,8 @@ from selva.di.service.parse import get_dependencies, parse_service_spec from selva.di.service.registry import ServiceRegistry +T = TypeVar("T") + class Container: def __init__(self): @@ -28,11 +30,7 @@ def __init__(self): self.interceptors: list[Type[Interceptor]] = [] def register( - self, - service: InjectableType, - *, - provides: type = None, - name: str = None, + self, service: InjectableType, *, provides: type = None, name: str = None ): self._register_service_spec(service, provides, name) @@ -116,8 +114,8 @@ def iter_all_services( for name, definition in record.providers.items(): yield interface, definition.service, name - async def get[T](self, service_type: T, *, name: str = None, optional=False,) -> T: - dependency = ServiceDependency(service_type, name=name, optional=optional) + async def get(self, service: T, *, name: str = None, optional=False) -> T: + dependency = ServiceDependency(service, name=name, optional=optional) return await self._get(dependency) async def create(self, service: type) -> Any: diff --git a/src/selva/di/decorator.py b/src/selva/di/decorator.py index be729ab..b58e987 100644 --- a/src/selva/di/decorator.py +++ b/src/selva/di/decorator.py @@ -1,5 +1,6 @@ import inspect from collections.abc import Callable +from typing import TypeVar from selva.di.inject import Inject from selva.di.service.model import InjectableType, ServiceInfo @@ -8,12 +9,14 @@ DI_SERVICE_ATTRIBUTE = "__selva_di_service__" +T = TypeVar("T") + def _is_inject(value) -> bool: return isinstance(value, Inject) or value is Inject -def service[T]( +def service( injectable: T = None, /, *, provides: type = None, name: str = None ) -> T | Callable[[T], T]: """Declare a class or function as a service diff --git a/src/selva/logging/setup.py b/src/selva/logging/setup.py index 5604384..c494d86 100644 --- a/src/selva/logging/setup.py +++ b/src/selva/logging/setup.py @@ -18,17 +18,13 @@ def setup_logger(settings: Settings): log_config = settings.get("logging", {}) root_level = logger.level(log_config.get("root", "WARNING")) log_level = { - name: logger.level(value) - for name, value in log_config.get("level", {}).items() + name: logger.level(value) for name, value in log_config.get("level", {}).items() } filter_func = filter_func_factory(root_level, log_level) handler = {"sink": sys.stderr, "filter": filter_func} - logger.configure( - handlers=[handler], - activation=activation - ) + logger.configure(handlers=[handler], activation=activation) setup_loguru_std_logging_interceptor() diff --git a/src/selva/web/application.py b/src/selva/web/application.py index 62b8e53..f0b31c3 100644 --- a/src/selva/web/application.py +++ b/src/selva/web/application.py @@ -14,8 +14,8 @@ from loguru import logger from selva._util.base_types import get_base_types -from selva._util.maybe_async import maybe_async from selva._util.import_item import import_item +from selva._util.maybe_async import maybe_async from selva.configuration.settings import Settings, get_settings from selva.di.container import Container from selva.di.decorator import DI_SERVICE_ATTRIBUTE diff --git a/src/selva/web/converter/from_request.py b/src/selva/web/converter/from_request.py index fd2a9f4..84f591a 100644 --- a/src/selva/web/converter/from_request.py +++ b/src/selva/web/converter/from_request.py @@ -1,13 +1,15 @@ from collections.abc import Awaitable -from typing import Any, Protocol, Type, runtime_checkable +from typing import Any, Protocol, Type, TypeVar, runtime_checkable from asgikit.requests import Request __all__ = ("FromRequest",) +T = TypeVar("T") + @runtime_checkable -class FromRequest[T](Protocol[T]): +class FromRequest(Protocol[T]): """Base class for services that extract values from the request""" def from_request( diff --git a/src/selva/web/converter/param_converter.py b/src/selva/web/converter/param_converter.py index 8193df9..ae0b39e 100644 --- a/src/selva/web/converter/param_converter.py +++ b/src/selva/web/converter/param_converter.py @@ -1,11 +1,13 @@ from collections.abc import Awaitable -from typing import Protocol, runtime_checkable +from typing import Protocol, TypeVar, runtime_checkable __all__ = ("ParamConverter",) +T = TypeVar("T") + @runtime_checkable -class ParamConverter[T](Protocol[T]): +class ParamConverter(Protocol[T]): """Convert values from and to request parameters Request parameters come from path, querystring or headers. diff --git a/src/selva/web/converter/param_extractor.py b/src/selva/web/converter/param_extractor.py index ef49d93..bc68ffd 100644 --- a/src/selva/web/converter/param_extractor.py +++ b/src/selva/web/converter/param_extractor.py @@ -1,9 +1,11 @@ -from typing import Protocol, Type, runtime_checkable +from typing import Protocol, Type, TypeVar, runtime_checkable from asgikit.requests import Request +T = TypeVar("T") + @runtime_checkable -class ParamExtractor[T](Protocol[T]): +class ParamExtractor(Protocol[T]): def extract(self, request: Request, parameter_name: str, metadata: T | Type[T]): raise NotImplementedError() diff --git a/src/selva/web/exception_handler.py b/src/selva/web/exception_handler.py index 65f9631..783c1ae 100644 --- a/src/selva/web/exception_handler.py +++ b/src/selva/web/exception_handler.py @@ -1,17 +1,19 @@ -from typing import Protocol, Type, runtime_checkable +from typing import Protocol, Type, TypeVar, runtime_checkable from asgikit.requests import Request from selva.di.decorator import service +TExc = TypeVar("TExc", bound=BaseException) + @runtime_checkable -class ExceptionHandler[TExc: BaseException](Protocol[TExc]): +class ExceptionHandler(Protocol[TExc]): async def handle_exception(self, request: Request, exc: TExc): raise NotImplementedError() -def exception_handler(exc: Type[Exception]): +def exception_handler(exc: Type[BaseException]): def inner(cls): assert issubclass(cls, ExceptionHandler) return service(cls, provides=ExceptionHandler[exc]) diff --git a/tests/configuration/test_environment.py b/tests/configuration/test_environment.py index f187391..968f903 100644 --- a/tests/configuration/test_environment.py +++ b/tests/configuration/test_environment.py @@ -2,8 +2,8 @@ from selva.configuration.environment import ( parse_settings_from_env, - replace_variables_with_env, replace_variables_recursive, + replace_variables_with_env, ) @@ -79,14 +79,14 @@ def test_replace_variables_recursive(): "dict": { "var": "3", "subdict": {"var": "4"}, - } + }, } def test_replace_variables_recursive_with_invalid_value_should_fail(): - settings = { - "prop": 1 - } + settings = {"prop": 1} - with pytest.raises(TypeError, match="settings should contain only str, list or dict"): - replace_variables_recursive(settings, {}) \ No newline at end of file + with pytest.raises( + TypeError, match="settings should contain only str, list or dict" + ): + replace_variables_recursive(settings, {}) diff --git a/tests/configuration/test_settings.py b/tests/configuration/test_settings.py index 9620f73..08cf119 100644 --- a/tests/configuration/test_settings.py +++ b/tests/configuration/test_settings.py @@ -289,7 +289,7 @@ def test_invalid_yaml_should_fail(monkeypatch): "replace nested value", "replace dict with value", "replace value with dict", - ] + ], ) def test_merge_recursive(settings, extra, expected): merge_recursive(settings, extra) diff --git a/tests/di/services/scan_package/generic.py b/tests/di/services/scan_package/generic.py index 9ddc74c..d83bc80 100644 --- a/tests/di/services/scan_package/generic.py +++ b/tests/di/services/scan_package/generic.py @@ -1,7 +1,11 @@ +from typing import Generic, TypeVar + from selva.di import service +T = TypeVar("T") + -class Interface[T]: +class Interface(Generic[T]): pass diff --git a/tests/di/test_generics.py b/tests/di/test_generics.py index c0052c2..0b83c68 100644 --- a/tests/di/test_generics.py +++ b/tests/di/test_generics.py @@ -1,4 +1,4 @@ -from typing import Annotated, TypeVar +from typing import Annotated, Generic, TypeVar import pytest @@ -8,8 +8,10 @@ from .fixtures import ioc +T = TypeVar("T") -class GenericService[T]: + +class GenericService(Generic[T]): pass