diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index c2753f25..5f047d56 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -39,6 +39,7 @@ jobs: - uses: "actions/setup-python@v4" with: python-version: "${{ matrix.python-version }}" + # allow-prereleases: true - uses: actions/cache@v3 id: cache with: diff --git a/.gitignore b/.gitignore index 1ca72cea..953ccec2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ target/ *.iml .DS_Store .coverage +.coverage.* .python-version coverage.* -example.sqlite \ No newline at end of file +example.sqlite diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99fce75b..5e565912 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-added-large-files - id: check-toml @@ -12,6 +12,7 @@ repos: args: - --unsafe - id: end-of-file-fixer + - id: debug-statements - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade rev: v2.37.3 diff --git a/Makefile b/Makefile index 7fd17dff..b727703b 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,18 @@ test: ## Runs the tests coverage: ## Run tests and coverage ESMERALD_SETTINGS_MODULE='tests.settings.TestSettings' pytest --cov=esmerald --cov=tests --cov-report=term-missing:skip-covered --cov-report=html tests +# .PHONY: cov +# cov: ## Run tests and coverage only for specific ones +# ESMERALD_SETTINGS_MODULE='tests.settings.TestSettings' pytest --cov=esmerald --cov=${ONLY} --cov-report=term-missing:skip-covered --cov-report=html ${ONLY} + +.PHONY: cov +cov: ## Run tests and coverage only for specific ones + ESMERALD_SETTINGS_MODULE='tests.settings.TestSettings' coverage run -m pytest tests + ESMERALD_SETTINGS_MODULE='tests.settings.TestSettings' coverage combine + ESMERALD_SETTINGS_MODULE='tests.settings.TestSettings' coverage report --show-missing + ESMERALD_SETTINGS_MODULE='tests.settings.TestSettings' coverage html + + .PHONY: requirements requirements: ## Install requirements for development pip install -e .[dev,test,doc,templates,jwt,encoders,schedulers,ipython,ptpython] diff --git a/esmerald/applications.py b/esmerald/applications.py index 36915772..77824b4f 100644 --- a/esmerald/applications.py +++ b/esmerald/applications.py @@ -1,4 +1,5 @@ from datetime import timezone as dtimezone +from functools import cached_property from typing import ( TYPE_CHECKING, Any, @@ -61,8 +62,8 @@ from esmerald.utils.helpers import is_class_and_subclass if TYPE_CHECKING: - from esmerald.conf import EsmeraldLazySettings - from esmerald.types import SettingsType, TemplateConfig + from esmerald.conf import EsmeraldLazySettings # pragma: no cover + from esmerald.types import SettingsType, TemplateConfig # pragma: no cover AppType = TypeVar("AppType", bound="Esmerald") @@ -525,7 +526,7 @@ def get_settings_value( if local_settings: setting_value = getattr(local_settings, value, None) - if not setting_value: + if setting_value is None: return getattr(global_settings, value, None) return setting_value @@ -662,8 +663,15 @@ def add_websocket_route( ) def add_include(self, include: Include) -> None: - """Adds an include directly to the active application router""" + """ + Adds an include directly to the active application router + and creates the proper signature models. + """ self.router.routes.append(include) + + for route in include.routes: + self.router.create_signature_models(route) + self.activate_openapi() def add_child_esmerald( @@ -946,8 +954,6 @@ def build_pluggable_stack(self) -> Optional["Esmerald"]: "An extension must subclass from esmerald.pluggables.Extension and added to " "a Pluggable object" ) - else: - pluggables[name] = Pluggable(extension) app: "ASGIApp" = self for name, pluggable in pluggables.items(): @@ -971,7 +977,7 @@ def settings(self) -> Type["EsmeraldAPISettings"]: general_settings = self.settings_config if self.settings_config else esmerald_settings return cast("Type[EsmeraldAPISettings]", general_settings) - @property + @cached_property def default_settings(self) -> Union[Type["EsmeraldAPISettings"], Type["EsmeraldLazySettings"]]: """ Returns the default global settings. diff --git a/esmerald/conf/global_settings.py b/esmerald/conf/global_settings.py index a39f8ec8..85946377 100644 --- a/esmerald/conf/global_settings.py +++ b/esmerald/conf/global_settings.py @@ -25,8 +25,8 @@ ) if TYPE_CHECKING: - from esmerald.routing.router import Include - from esmerald.types import TemplateConfig + from esmerald.routing.router import Include # pragma: no cover + from esmerald.types import TemplateConfig # pragma: no cover class EsmeraldAPISettings(BaseSettings): @@ -72,6 +72,7 @@ class EsmeraldAPISettings(BaseSettings): ) swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.1.3/swagger-ui.min.css" swagger_favicon_url: str = "https://esmerald.dev/statics/images/favicon.ico" + with_google_fonts: bool = True # Model configuration model_config = SettingsConfigDict(extra="allow", ignored_types=(cached_property,)) @@ -279,6 +280,7 @@ def openapi_config(self) -> OpenAPIConfig: root_path_in_servers=self.root_path_in_servers, openapi_version=self.openapi_version, openapi_url=self.openapi_url, + with_google_fonts=self.with_google_fonts, ) @property diff --git a/esmerald/config/openapi.py b/esmerald/config/openapi.py index 3d9ca871..7eb97ebe 100644 --- a/esmerald/config/openapi.py +++ b/esmerald/config/openapi.py @@ -39,6 +39,7 @@ class OpenAPIConfig(BaseModel): swagger_js_url: Optional[str] = None swagger_css_url: Optional[str] = None swagger_favicon_url: Optional[str] = None + with_google_fonts: bool = True def openapi(self, app: Any) -> Dict[str, Any]: """Loads the OpenAPI routing schema""" @@ -80,7 +81,7 @@ async def _openapi(request: Request) -> JSONResponse: if self.openapi_url and self.docs_url: @get(path=self.docs_url) - async def swagger_ui_html(request: Request) -> HTMLResponse: + async def swagger_ui_html(request: Request) -> HTMLResponse: # pragma: no cover root_path = request.scope.get("root_path", "").rstrip("/") openapi_url = root_path + self.openapi_url oauth2_redirect_url = self.swagger_ui_oauth2_redirect_url @@ -107,7 +108,7 @@ async def swagger_ui_html(request: Request) -> HTMLResponse: if self.swagger_ui_oauth2_redirect_url: @get(self.swagger_ui_oauth2_redirect_url) - async def swagger_ui_redirect(request: Request) -> HTMLResponse: + async def swagger_ui_redirect(request: Request) -> HTMLResponse: # pragma: no cover return get_swagger_ui_oauth2_redirect_html() app.add_route( @@ -120,7 +121,7 @@ async def swagger_ui_redirect(request: Request) -> HTMLResponse: if self.openapi_url and self.redoc_url: @get(self.redoc_url) - async def redoc_html(request: Request) -> HTMLResponse: + async def redoc_html(request: Request) -> HTMLResponse: # pragma: no cover root_path = request.scope.get("root_path", "").rstrip("/") openapi_url = root_path + self.openapi_url return get_redoc_html( @@ -128,6 +129,7 @@ async def redoc_html(request: Request) -> HTMLResponse: title=self.title + " - ReDoc", redoc_js_url=self.redoc_js_url, redoc_favicon_url=self.redoc_favicon_url, + with_google_fonts=True, ) app.add_route( diff --git a/esmerald/config/static_files.py b/esmerald/config/static_files.py index 49b13418..f7b30ca9 100644 --- a/esmerald/config/static_files.py +++ b/esmerald/config/static_files.py @@ -1,14 +1,12 @@ from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from pydantic import BaseModel, DirectoryPath, constr, field_validator from starlette.staticfiles import StaticFiles +from starlette.types import ASGIApp from esmerald.utils.url import clean_path -if TYPE_CHECKING: - from starlette.types import ASGIApp - class StaticFilesConfig(BaseModel): path: constr(min_length=1) # type: ignore @@ -36,7 +34,7 @@ def _build_kwargs( kwargs.update({"directory": str(self.directory)}) # type: ignore return kwargs # type: ignore - def to_app(self) -> "ASGIApp": + def to_app(self) -> ASGIApp: """ It can be three scenarios """ diff --git a/esmerald/contrib/auth/common/middleware.py b/esmerald/contrib/auth/common/middleware.py index 92ddd12c..6be01910 100644 --- a/esmerald/contrib/auth/common/middleware.py +++ b/esmerald/contrib/auth/common/middleware.py @@ -12,14 +12,14 @@ T = TypeVar("T") -class CommonJWTAuthMiddleware(BaseAuthMiddleware): +class CommonJWTAuthMiddleware(BaseAuthMiddleware): # pragma: no cover """ The simple JWT authentication Middleware. """ def __init__( self, - app: "ASGIApp", + app: ASGIApp, config: "JWTConfig", user_model: T, ): diff --git a/esmerald/core/urls/base.py b/esmerald/core/urls/base.py index 332eeaa5..ae9572ca 100644 --- a/esmerald/core/urls/base.py +++ b/esmerald/core/urls/base.py @@ -6,7 +6,7 @@ from esmerald.exceptions import ImproperlyConfigured -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.routing.gateways import Gateway, WebSocketGateway from esmerald.routing.router import Include @@ -66,14 +66,12 @@ def include( pattern (Optional[str], optional): The name of the list to be read from the module. Defaults to `router_patterns`. """ - if not isinstance(arg, str): raise ImproperlyConfigured("The value should be a string with the format .") router_conf_module = import_module(arg) pattern = pattern or DEFAULT_PATTERN patterns = getattr(router_conf_module, pattern, None) - if not patterns: raise ImproperlyConfigured( f"There is no pattern {pattern} found in {arg}. " diff --git a/esmerald/datastructures/base.py b/esmerald/datastructures/base.py index ac4018e4..aa20b481 100644 --- a/esmerald/datastructures/base.py +++ b/esmerald/datastructures/base.py @@ -31,6 +31,7 @@ from starlette.datastructures import Headers as Headers # noqa: F401 from starlette.datastructures import MutableHeaders as MutableHeaders # noqa from starlette.datastructures import QueryParams as QueryParams # noqa: F401 +from starlette.datastructures import Secret as StarletteSecret # noqa from starlette.datastructures import State as StarletteStateClass # noqa: F401 from starlette.datastructures import UploadFile as StarletteUploadFile # noqa from starlette.datastructures import URLPath as URLPath # noqa: F401 @@ -38,15 +39,15 @@ from typing_extensions import Literal from esmerald.backgound import BackgroundTask, BackgroundTasks # noqa +from esmerald.enums import MediaType R = TypeVar("R", bound=StarletteResponse) -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.enums import MediaType -class UploadFile(StarletteUploadFile): +class UploadFile(StarletteUploadFile): # pragma: no cover """ Adding pydantic specific functionalitty for parsing. """ @@ -80,25 +81,12 @@ def __get_pydantic_core_schema__( return general_plain_validator_function(cls._validate) -class Secret: - def __init__(self, value: str): - self._value = value - - def __repr__(self) -> str: - class_name = self.__class__.__name__ - return f"{class_name}('**********')" - - def __str__(self) -> str: - return self._value - - def __bool__(self) -> bool: - return bool(self._value) - +class Secret(StarletteSecret): # pragma: no cover def __len__(self) -> int: return len(self._value) -class State(StarletteStateClass): +class State(StarletteStateClass): # pragma: no cover state: Dict[str, Any] def __copy__(self) -> "State": diff --git a/esmerald/datastructures/encoders.py b/esmerald/datastructures/encoders.py index f9173a90..8951109c 100644 --- a/esmerald/datastructures/encoders.py +++ b/esmerald/datastructures/encoders.py @@ -1,19 +1,19 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Type, Union from esmerald.datastructures.base import ResponseContainer +from esmerald.enums import MediaType -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.enums import MediaType try: from esmerald.responses.encoders import ORJSONResponse -except ImportError: +except ImportError: # pragma: no cover ORJSONResponse = None # type: ignore try: from esmerald.responses.encoders import UJSONResponse -except ImportError: +except ImportError: # pragma: no cover UJSONResponse = None # type: ignore diff --git a/esmerald/datastructures/file.py b/esmerald/datastructures/file.py index 980b4400..b78286bb 100644 --- a/esmerald/datastructures/file.py +++ b/esmerald/datastructures/file.py @@ -5,10 +5,10 @@ from starlette.responses import FileResponse # noqa from esmerald.datastructures.base import ResponseContainer +from esmerald.enums import MediaType -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.enums import MediaType class File(ResponseContainer[FileResponse]): diff --git a/esmerald/datastructures/json.py b/esmerald/datastructures/json.py index ab5e76ca..bb688546 100644 --- a/esmerald/datastructures/json.py +++ b/esmerald/datastructures/json.py @@ -1,11 +1,11 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Type, Union from esmerald.datastructures.base import ResponseContainer # noqa +from esmerald.enums import MediaType from esmerald.responses import JSONResponse # noqa -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.enums import MediaType class JSON(ResponseContainer[JSONResponse]): diff --git a/esmerald/datastructures/multidict.py b/esmerald/datastructures/multidict.py deleted file mode 100644 index f2ba7a17..00000000 --- a/esmerald/datastructures/multidict.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any - -from starlette.datastructures import ImmutableMultiDict - -from esmerald.datastructures import UploadFile - - -class FormMultiDict(ImmutableMultiDict[str, Any]): - async def close(self) -> None: - for _, value in self.multi_items(): - if isinstance(value, UploadFile): - await value.close() diff --git a/esmerald/datastructures/redirect.py b/esmerald/datastructures/redirect.py index 687f3e57..554444bc 100644 --- a/esmerald/datastructures/redirect.py +++ b/esmerald/datastructures/redirect.py @@ -3,10 +3,10 @@ from starlette.responses import RedirectResponse # noqa from esmerald.datastructures.base import ResponseContainer # noqa +from esmerald.enums import MediaType -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.enums import MediaType class Redirect(ResponseContainer[RedirectResponse]): diff --git a/esmerald/datastructures/stream.py b/esmerald/datastructures/stream.py index 3428a200..eb1c86dd 100644 --- a/esmerald/datastructures/stream.py +++ b/esmerald/datastructures/stream.py @@ -16,10 +16,10 @@ from starlette.responses import StreamingResponse # noqa from esmerald.datastructures.base import ResponseContainer # noqa +from esmerald.enums import MediaType -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.enums import MediaType class Stream(ResponseContainer[StreamingResponse]): diff --git a/esmerald/datastructures/template.py b/esmerald/datastructures/template.py index 8f428489..d128d574 100644 --- a/esmerald/datastructures/template.py +++ b/esmerald/datastructures/template.py @@ -1,12 +1,12 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Type, Union from esmerald.datastructures.base import ResponseContainer # noqa +from esmerald.enums import MediaType from esmerald.exceptions import TemplateNotFound # noqa from esmerald.responses import TemplateResponse # noqa -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.enums import MediaType class Template(ResponseContainer[TemplateResponse]): @@ -47,7 +47,10 @@ def to_response( } try: return TemplateResponse(template_name=self.name, **data) - except TemplateNotFound as e: + except TemplateNotFound as e: # pragma: no cover if self.alternative_template: - return TemplateResponse(template_name=self.alternative_template, **data) + try: + return TemplateResponse(template_name=self.alternative_template, **data) + except TemplateNotFound as ex: # pragma: no cover + raise ex raise e diff --git a/esmerald/exception_handlers.py b/esmerald/exception_handlers.py index 64999626..bce4edf0 100644 --- a/esmerald/exception_handlers.py +++ b/esmerald/exception_handlers.py @@ -14,7 +14,7 @@ async def http_exception_handler( request: Request, exc: Union[HTTPException, StarletteHTTPException] -) -> Union[JSONResponse, Response]: +) -> Union[JSONResponse, Response]: # pragma: no cover """ Default exception handler for StarletteHTTPException and Esmerald HTTPException. """ @@ -40,7 +40,7 @@ async def http_exception_handler( async def validation_error_exception_handler( request: Request, exc: ValidationError -) -> JSONResponse: +) -> JSONResponse: # pragma: no cover extra = getattr(exc, "extra", None) status_code = status.HTTP_400_BAD_REQUEST @@ -57,13 +57,15 @@ async def validation_error_exception_handler( ) -async def http_error_handler(_: Request, exc: ExceptionErrorMap) -> JSONResponse: +async def http_error_handler( + _: Request, exc: ExceptionErrorMap +) -> JSONResponse: # pragma: no cover return JSONResponse({"detail": exc.detail}, status_code=exc.status_code) async def improperly_configured_exception_handler( request: Request, exc: ImproperlyConfigured -) -> StarletteResponse: +) -> StarletteResponse: # pragma: no cover """ When an ImproperlyConfiguredException is raised. """ @@ -90,7 +92,7 @@ async def improperly_configured_exception_handler( async def pydantic_validation_error_handler( request: Request, exc: ValidationError -) -> JSONResponse: +) -> JSONResponse: # pragma: no cover """ This handler is to be used when a pydantic validation error is triggered during the logic of a code block and not the definition of a handler. @@ -101,7 +103,9 @@ async def pydantic_validation_error_handler( return JSONResponse({"detail": loads(exc.json())}, status_code=status_code) -async def value_error_handler(request: Request, exc: ValueError) -> JSONResponse: +async def value_error_handler( + request: Request, exc: ValueError +) -> JSONResponse: # pragma: no cover """ Simple handler that manages all the ValueError exceptions thrown to the user properly formatted. diff --git a/esmerald/exceptions.py b/esmerald/exceptions.py index 54aefece..ce2acdfd 100644 --- a/esmerald/exceptions.py +++ b/esmerald/exceptions.py @@ -15,7 +15,7 @@ def __init__(self, *args: Any, detail: str = ""): self.detail = detail super().__init__(*(str(arg) for arg in args if arg), self.detail) - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover if self.detail: return f"{self.__class__.__name__} - {self.detail}" return self.__class__.__name__ @@ -51,7 +51,7 @@ def __init__( self.args = (f"{self.status_code}: {self.detail}", *args) self.extra = extra - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover return f"<{self.status_code}: {self.__class__.__name__} />" diff --git a/esmerald/injector.py b/esmerald/injector.py index 3030baa5..715c9faa 100644 --- a/esmerald/injector.py +++ b/esmerald/injector.py @@ -5,7 +5,7 @@ from esmerald.typing import Void from esmerald.utils.helpers import is_async_callable -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.typing import AnyCallable diff --git a/esmerald/interceptors/interceptor.py b/esmerald/interceptors/interceptor.py index aa3177fa..0dab1f34 100644 --- a/esmerald/interceptors/interceptor.py +++ b/esmerald/interceptors/interceptor.py @@ -3,7 +3,7 @@ from esmerald.protocols.interceptor import InterceptorProtocol -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from starlette.types import Receive, Scope, Send diff --git a/esmerald/logging.py b/esmerald/logging.py index bf461863..bfeacba4 100644 --- a/esmerald/logging.py +++ b/esmerald/logging.py @@ -2,15 +2,13 @@ from loguru import logger as loguru_logger -logger = logging.getLogger("esmerald") - class InterceptHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: level: str try: level = loguru_logger.level(record.levelname).name - except ValueError: + except ValueError: # pragma: no cover level = str(record.levelno) frame, depth = logging.currentframe(), 2 diff --git a/esmerald/middleware/_exception_handlers.py b/esmerald/middleware/_exception_handlers.py index 46aba9d6..79f55ea2 100644 --- a/esmerald/middleware/_exception_handlers.py +++ b/esmerald/middleware/_exception_handlers.py @@ -20,7 +20,7 @@ def wrap_app_handling_exceptions(app: ASGIApp, conn: typing.Union[Request, WebSo status_handlers: StatusHandlers try: exception_handlers, status_handlers = conn.scope["starlette.exception_handlers"] - except KeyError: + except KeyError: # pragma: no cover exception_handlers, status_handlers = {}, {} async def wrapped_app(scope: Scope, receive: Receive, send: Send) -> None: @@ -47,7 +47,7 @@ async def sender(message: Message) -> None: if handler is None: raise exc - if response_started: + if response_started: # pragma: no cover msg = "Caught handled exception, but response already started." raise RuntimeError(msg) from exc @@ -56,7 +56,9 @@ async def sender(message: Message) -> None: if is_async_callable(handler): response = await handler(conn, exc) else: - response = await run_in_threadpool(handler, conn, exc) + response = await run_in_threadpool( + typing.cast("typing.Callable[..., Response]", handler), conn, exc + ) await response(scope, receive, sender) elif scope["type"] == "websocket": if is_async_callable(handler): diff --git a/esmerald/middleware/asyncexitstack.py b/esmerald/middleware/asyncexitstack.py index 9fc2ae29..6503d816 100644 --- a/esmerald/middleware/asyncexitstack.py +++ b/esmerald/middleware/asyncexitstack.py @@ -20,7 +20,7 @@ def __init__(self, app: "ASGIApp", config: "AsyncExitConfig"): async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: if not AsyncExitStack: - await self.app(scope, receive, send) + await self.app(scope, receive, send) # pragma: no cover exception: Optional[Exception] = None async with AsyncExitStack() as stack: diff --git a/esmerald/middleware/authentication.py b/esmerald/middleware/authentication.py index 4875b34b..970c5df0 100644 --- a/esmerald/middleware/authentication.py +++ b/esmerald/middleware/authentication.py @@ -1,34 +1,32 @@ from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any +from typing import Any from starlette.requests import HTTPConnection +from starlette.types import ASGIApp, Receive, Scope, Send from esmerald.enums import ScopeType from esmerald.parsers import ArbitraryBaseModel from esmerald.protocols.middleware import MiddlewareProtocol -if TYPE_CHECKING: - from starlette.types import ASGIApp, Receive, Scope, Send - class AuthResult(ArbitraryBaseModel): user: Any -class BaseAuthMiddleware(ABC, MiddlewareProtocol): +class BaseAuthMiddleware(ABC, MiddlewareProtocol): # pragma: no cover scopes = {ScopeType.HTTP, ScopeType.WEBSOCKET} - def __init__(self, app: "ASGIApp"): + def __init__(self, app: ASGIApp): super().__init__(app) self.app = app - async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: auth_result = await self.authenticate(HTTPConnection(scope)) scope["user"] = auth_result.user await self.app(scope, receive, send) @abstractmethod - async def authenticate(self, request: HTTPConnection) -> AuthResult: # pragma: no cover + async def authenticate(self, request: HTTPConnection) -> AuthResult: """ The abstract method that needs to be implemented for any authentication middleware. """ diff --git a/esmerald/middleware/csrf.py b/esmerald/middleware/csrf.py index 00b41fcf..205f4ef1 100644 --- a/esmerald/middleware/csrf.py +++ b/esmerald/middleware/csrf.py @@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Optional from starlette.datastructures import MutableHeaders +from starlette.types import ASGIApp, Message, Receive, Scope, Send from esmerald.datastructures import Cookie from esmerald.enums import ScopeType @@ -34,9 +35,7 @@ from esmerald.protocols.middleware import MiddlewareProtocol from esmerald.requests import Request -if TYPE_CHECKING: - from starlette.types import ASGIApp, Message, Receive, Scope, Send - +if TYPE_CHECKING: # pragma: no cover from esmerald.config import CSRFConfig CSRF_SECRET_BYTES = 32 diff --git a/esmerald/middleware/errors.py b/esmerald/middleware/errors.py index c406e758..0715856d 100644 --- a/esmerald/middleware/errors.py +++ b/esmerald/middleware/errors.py @@ -10,7 +10,7 @@ Request = TypeVar("Request", _Request, StarletteRequest) -class ServerErrorMiddleware(StarletteServerErrorMiddleware): +class ServerErrorMiddleware(StarletteServerErrorMiddleware): # pragma: no cover """ Handles returning 500 responses when a server error occurs. diff --git a/esmerald/middleware/exceptions.py b/esmerald/middleware/exceptions.py index b158e33f..509b8391 100644 --- a/esmerald/middleware/exceptions.py +++ b/esmerald/middleware/exceptions.py @@ -42,7 +42,9 @@ def __init__( for key, value in handlers.items(): self.add_exception_handler(key, value) # type: ignore - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + async def __call__( + self, scope: Scope, receive: Receive, send: Send + ) -> None: # pragma: no cover if scope["type"] not in ("http", "websocket"): await self.app(scope, receive, send) return @@ -68,7 +70,7 @@ class ResponseContent(BaseModel): status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR -class EsmeraldAPIExceptionMiddleware: +class EsmeraldAPIExceptionMiddleware: # pragma: no cover def __init__( self, app: "ASGIApp", diff --git a/esmerald/openapi/docs.py b/esmerald/openapi/docs.py index 02cbe3ee..16728fc6 100644 --- a/esmerald/openapi/docs.py +++ b/esmerald/openapi/docs.py @@ -22,7 +22,7 @@ def get_swagger_ui_html( oauth2_redirect_url: Optional[str] = None, init_oauth: Optional[Dict[str, Any]] = None, swagger_ui_parameters: Optional[Dict[str, Any]] = None, -) -> HTMLResponse: +) -> HTMLResponse: # pragma: no cover current_swagger_ui_parameters = swagger_ui_default_parameters.copy() if swagger_ui_parameters: current_swagger_ui_parameters.update(swagger_ui_parameters) diff --git a/esmerald/openapi/openapi.py b/esmerald/openapi/openapi.py index 4d1c22cf..c4711a30 100644 --- a/esmerald/openapi/openapi.py +++ b/esmerald/openapi/openapi.py @@ -77,8 +77,7 @@ def get_fields_from_routes( def get_openapi_operation( *, route: Union[router.HTTPHandler, Any], method: str, operation_ids: Set[str] -) -> Dict[str, Any]: - # operation: Dict[str, Any] = {} +) -> Dict[str, Any]: # pragma: no cover operation = Operation() if route.tags: @@ -115,7 +114,7 @@ def get_openapi_operation_parameters( *, all_route_params: Sequence[FieldInfo], field_mapping: Dict[Tuple[FieldInfo, Literal["validation", "serialization"]], JsonSchemaValue], -) -> List[Dict[str, Any]]: +) -> List[Dict[str, Any]]: # pragma: no cover parameters = [] for param in all_route_params: field_info = cast(Param, param) @@ -148,7 +147,7 @@ def get_openapi_operation_request_body( *, data_field: Optional[FieldInfo], field_mapping: Dict[Tuple[FieldInfo, Literal["validation", "serialization"]], JsonSchemaValue], -) -> Optional[Dict[str, Any]]: +) -> Optional[Dict[str, Any]]: # pragma: no cover if not data_field: return None @@ -174,7 +173,7 @@ def get_openapi_path( route: gateways.Gateway, operation_ids: Set[str], field_mapping: Dict[Tuple[FieldInfo, Literal["validation", "serialization"]], JsonSchemaValue], -) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: +) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: # pragma: no cover path: Dict[str, Any] = {} security_schemes: Dict[str, Any] = {} definitions: Dict[str, Any] = {} @@ -356,7 +355,7 @@ def get_openapi( terms_of_service: Optional[Union[str, AnyUrl]] = None, contact: Optional[Contact] = None, license: Optional[License] = None, -) -> Dict[str, Any]: +) -> Dict[str, Any]: # pragma: no cover """ Builds the whole OpenAPI route structure and object """ diff --git a/esmerald/openapi/responses.py b/esmerald/openapi/responses.py index 5f87f01a..3fc7ea02 100644 --- a/esmerald/openapi/responses.py +++ b/esmerald/openapi/responses.py @@ -9,12 +9,14 @@ from esmerald.openapi._internal import InternalResponse from esmerald.responses import Response as EsmeraldResponse -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.routing.router import HTTPHandler - from esmerald.types import AnyCallable + from esmerald.typing import AnyCallable -def create_internal_response(handler: Union["HTTPHandler", Any]) -> InternalResponse: +def create_internal_response( + handler: Union["HTTPHandler", Any] +) -> InternalResponse: # pragma: no cover signature = Signature.from_callable(cast("AnyCallable", handler.fn)) default_descriptions: Dict[Any, str] = { Stream: "Stream Response", diff --git a/esmerald/openapi/utils.py b/esmerald/openapi/utils.py index 23a04143..7b8770cc 100644 --- a/esmerald/openapi/utils.py +++ b/esmerald/openapi/utils.py @@ -74,7 +74,9 @@ def is_status_code_allowed(status_code: Union[int, str, None]) -> bool: return not (current_status_code < 200 or current_status_code in {204, 304}) -def dict_update(original_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None: +def dict_update( + original_dict: Dict[Any, Any], update_dict: Dict[Any, Any] +) -> None: # pragma: no cover for key, value in update_dict.items(): if ( key in original_dict diff --git a/esmerald/params.py b/esmerald/params.py index fe091eeb..65800890 100644 --- a/esmerald/params.py +++ b/esmerald/params.py @@ -642,7 +642,7 @@ def __init__( @dataclass -class DirectInject: +class DirectInject: # pragma: no cover def __init__( self, dependency: Optional[Callable[..., Any]] = None, diff --git a/esmerald/parsers.py b/esmerald/parsers.py index 0367fa17..273d8880 100644 --- a/esmerald/parsers.py +++ b/esmerald/parsers.py @@ -3,17 +3,17 @@ from typing import TYPE_CHECKING, Any, Dict, List, get_args, get_origin from pydantic import BaseModel, ConfigDict +from pydantic.fields import FieldInfo from starlette.datastructures import UploadFile as StarletteUploadFile from esmerald.datastructures import UploadFile from esmerald.enums import EncodingType -if TYPE_CHECKING: - from pydantic.fields import FieldInfo +if TYPE_CHECKING: # pragma: no cover from starlette.datastructures import FormData -class HashableBaseModel(BaseModel): +class HashableBaseModel(BaseModel): # pragma: no cover """ Pydantic BaseModel by default doesn't handle with hashable types the same way a python object would and therefore there are types that are mutable (list, set) @@ -75,7 +75,9 @@ def flatten(values: List[Any]) -> List[Any]: return flattened -def parse_form_data(media_type: "EncodingType", form_data: "FormData", field: "FieldInfo") -> Any: +def parse_form_data( + media_type: "EncodingType", form_data: "FormData", field: "FieldInfo" +) -> Any: # pragma: no cover """ Converts, parses and transforms a multidict into a dict and tries to load them all into json. diff --git a/esmerald/permissions/base.py b/esmerald/permissions/base.py index dd92eb0f..8fb49be2 100644 --- a/esmerald/permissions/base.py +++ b/esmerald/permissions/base.py @@ -1,5 +1,3 @@ -"""Esmerald permission system""" - from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any @@ -12,7 +10,7 @@ SAFE_METHODS = ("GET", "HEAD", "OPTIONS") -class BaseOperationHolder: +class BaseOperationHolder: # pragma: no cover def __and__(self, other: Any) -> "OperandHolder": return OperandHolder(AND, self, other) diff --git a/esmerald/permissions/utils.py b/esmerald/permissions/utils.py index 9c612625..9b659c8f 100644 --- a/esmerald/permissions/utils.py +++ b/esmerald/permissions/utils.py @@ -1,31 +1,13 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional from esmerald.exceptions import PermissionDenied -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.permissions import BasePermission - from esmerald.permissions.types import Permission from esmerald.requests import Request from esmerald.types import APIGateHandler -def check_permissions( - request: "Request", - apiview: "APIGateHandler", - permissions: List["Permission"], -) -> None: - """ - Check if the request should be permitted. - Raises an appropriate exception if the request is not permitted. - """ - for permission in permissions: - if not permission().has_permission(request, apiview): - permission_denied( - request, - message=getattr(permission, "message", None), - ) - - def continue_or_raise_permission_exception( request: "Request", apiview: "APIGateHandler", diff --git a/esmerald/pluggables/base.py b/esmerald/pluggables/base.py index eb0fbd2c..0d8b901a 100644 --- a/esmerald/pluggables/base.py +++ b/esmerald/pluggables/base.py @@ -3,9 +3,8 @@ from esmerald.protocols.extension import ExtensionProtocol -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald - from esmerald.types import DictAny class Pluggable: @@ -24,14 +23,14 @@ def __iter__(self) -> Iterator: iterator = (self.cls, self.options) return iter(iterator) - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover name = self.__class__.__name__ options = [f"{key}={value!r}" for key, value in self.options.items()] args = ", ".join([self.__class__.__name__] + options) return f"{name}({args})" -class BaseExtension(ABC, ExtensionProtocol): +class BaseExtension(ABC, ExtensionProtocol): # pragma: no cover """ The base for any Esmerald plugglable. """ @@ -41,7 +40,7 @@ def __init__(self, app: Optional["Esmerald"] = None, **kwargs: Any): self.app = app @abstractmethod - def extend(self, **kwargs: "DictAny") -> None: + def extend(self, **kwargs: "Any") -> None: raise NotImplementedError("plug must be implemented by the subclasses.") diff --git a/esmerald/protocols/asyncdao.py b/esmerald/protocols/asyncdao.py index e35d8ac8..6e25e064 100644 --- a/esmerald/protocols/asyncdao.py +++ b/esmerald/protocols/asyncdao.py @@ -1,10 +1,7 @@ -from typing import TYPE_CHECKING, Any, List, TypeVar +from typing import Any, List, TypeVar from typing_extensions import Protocol, runtime_checkable -if TYPE_CHECKING: - from esmerald.types import DictAny - T = TypeVar("T") @@ -24,17 +21,17 @@ class AsyncDAOProtocol(Protocol): # pragma: no cover data access to a database. """ - async def get(self, obj_id: Any, **kwargs: "DictAny") -> Any: + async def get(self, obj_id: Any, **kwargs: Any) -> Any: ... - async def get_all(self, **kwargs: "DictAny") -> List[Any]: + async def get_all(self, **kwargs: Any) -> List[Any]: ... - async def update(self, obj_id: Any, **kwargs: "DictAny") -> Any: + async def update(self, obj_id: Any, **kwargs: Any) -> Any: ... - async def delete(self, obj_id: Any, **kwargs: "DictAny") -> Any: + async def delete(self, obj_id: Any, **kwargs: Any) -> Any: ... - async def create(self, **kwargs: "DictAny") -> Any: + async def create(self, **kwargs: Any) -> Any: ... diff --git a/esmerald/protocols/dao.py b/esmerald/protocols/dao.py index 959e1067..437f09a7 100644 --- a/esmerald/protocols/dao.py +++ b/esmerald/protocols/dao.py @@ -1,10 +1,7 @@ -from typing import TYPE_CHECKING, Any, List +from typing import Any, List from typing_extensions import Protocol, runtime_checkable -if TYPE_CHECKING: - from esmerald.types import DictAny - @runtime_checkable class DaoProtocol(Protocol): # pragma: no cover @@ -24,17 +21,17 @@ def model(self) -> Any: data access to a database. """ - def get(self, obj_id: Any, **kwargs: "DictAny") -> Any: + def get(self, obj_id: Any, **kwargs: Any) -> Any: ... - def get_all(self, **kwargs: "DictAny") -> List[Any]: + def get_all(self, **kwargs: Any) -> List[Any]: ... - def update(self, obj_id: Any, **kwargs: "DictAny") -> Any: + def update(self, obj_id: Any, **kwargs: Any) -> Any: ... - def delete(self, obj_id: Any, **kwargs: "DictAny") -> Any: + def delete(self, obj_id: Any, **kwargs: Any) -> Any: ... - def create(self, **kwargs: "DictAny") -> Any: + def create(self, **kwargs: Any) -> Any: ... diff --git a/esmerald/protocols/extension.py b/esmerald/protocols/extension.py index d53e48b3..4469240a 100644 --- a/esmerald/protocols/extension.py +++ b/esmerald/protocols/extension.py @@ -1,10 +1,8 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import Any, Dict, Optional +from starlette.types import ASGIApp from typing_extensions import Protocol, runtime_checkable -if TYPE_CHECKING: - from esmerald.types import ASGIApp - @runtime_checkable class ExtensionProtocol(Protocol): # pragma: no cover diff --git a/esmerald/protocols/interceptor.py b/esmerald/protocols/interceptor.py index 4f8aa541..545502f2 100644 --- a/esmerald/protocols/interceptor.py +++ b/esmerald/protocols/interceptor.py @@ -1,10 +1,8 @@ -from typing import TYPE_CHECKING, TypeVar +from typing import TypeVar +from starlette.types import Receive, Scope, Send from typing_extensions import Protocol, runtime_checkable -if TYPE_CHECKING: - from starlette.types import Receive, Scope, Send - T = TypeVar("T") diff --git a/esmerald/protocols/middleware.py b/esmerald/protocols/middleware.py index 8684e5a8..551670d5 100644 --- a/esmerald/protocols/middleware.py +++ b/esmerald/protocols/middleware.py @@ -1,10 +1,8 @@ -from typing import TYPE_CHECKING, Any +from typing import Any +from starlette.types import ASGIApp, Receive, Scope, Send from typing_extensions import Protocol, runtime_checkable -if TYPE_CHECKING: - from esmerald.types import ASGIApp, Receive, Scope, Send - @runtime_checkable class MiddlewareProtocol(Protocol): # pragma: no cover diff --git a/esmerald/protocols/utils/protocols.py b/esmerald/protocols/utils/protocols.py deleted file mode 100644 index 140c4a58..00000000 --- a/esmerald/protocols/utils/protocols.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Any, Dict, Protocol, runtime_checkable - - -# According to https://github.com/python/cpython/blob/main/Lib/dataclasses.py#L1213 -# having __dataclass_fields__ is enough to identity a dataclass. -@runtime_checkable -class DataclassProtocol(Protocol): - __dataclass_fields__: Dict[str, Any] diff --git a/esmerald/requests.py b/esmerald/requests.py index da99071d..bb58aff0 100644 --- a/esmerald/requests.py +++ b/esmerald/requests.py @@ -11,7 +11,7 @@ from esmerald.typing import Void -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald from esmerald.conf.global_settings import EsmeraldAPISettings from esmerald.types import HTTPMethod diff --git a/esmerald/responses/base.py b/esmerald/responses/base.py index 10e5f01c..b286fd64 100644 --- a/esmerald/responses/base.py +++ b/esmerald/responses/base.py @@ -1,3 +1,5 @@ +import dataclasses +from dataclasses import is_dataclass from json import dumps from typing import TYPE_CHECKING, Any, Dict, Generic, NoReturn, Optional, TypeVar, Union, cast @@ -14,7 +16,7 @@ from esmerald.enums import MediaType from esmerald.exceptions import ImproperlyConfigured -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.backgound import BackgroundTask, BackgroundTasks from esmerald.types import ResponseCookies @@ -45,6 +47,8 @@ def __init__( def transform(value: Any) -> Dict[str, Any]: if isinstance(value, BaseModel): return value.model_dump() + if is_dataclass(value): + return dataclasses.asdict(value) raise TypeError("unsupported type") # pragma: no cover def render(self, content: Any) -> bytes: @@ -62,5 +66,5 @@ def render(self, content: Any) -> bytes: if self.media_type == MediaType.JSON: return dumps(content, default=self.transform, ensure_ascii=False).encode("utf-8") return super().render(content) - except (AttributeError, ValueError, TypeError) as e: + except (AttributeError, ValueError, TypeError) as e: # pragma: no cover raise ImproperlyConfigured("Unable to serialize response content") from e diff --git a/esmerald/responses/encoders.py b/esmerald/responses/encoders.py index 85021193..9626f066 100644 --- a/esmerald/responses/encoders.py +++ b/esmerald/responses/encoders.py @@ -7,12 +7,12 @@ try: import orjson from orjson import OPT_OMIT_MICROSECONDS, OPT_SERIALIZE_NUMPY -except ImportError: +except ImportError: # pragma: no cover orjson = None try: import ujson -except ImportError: +except ImportError: # pragma: no cover ujson = None diff --git a/esmerald/responses/json.py b/esmerald/responses/json.py index 71137211..98cefc05 100644 --- a/esmerald/responses/json.py +++ b/esmerald/responses/json.py @@ -1,3 +1,5 @@ +import dataclasses +from dataclasses import is_dataclass from typing import Any, Dict from pydantic import BaseModel @@ -11,11 +13,13 @@ class BaseJSONResponse(JSONResponse): """ @staticmethod - def transform(value: Any) -> Dict[str, Any]: + def transform(value: Any) -> Dict[str, Any]: # pragma: no cover """ Makes sure that every value is checked and if it's a pydantic model then parses into a dict(). """ if isinstance(value, BaseModel): return value.model_dump() + if is_dataclass(value): + return dataclasses.asdict(value) raise TypeError("unsupported type") diff --git a/esmerald/responses/template.py b/esmerald/responses/template.py index f3f3b446..a1d075f9 100644 --- a/esmerald/responses/template.py +++ b/esmerald/responses/template.py @@ -7,7 +7,7 @@ from esmerald.enums import MediaType from esmerald.responses.base import Response -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.backgound import BackgroundTask, BackgroundTasks from esmerald.protocols.template import TemplateEngineProtocol from esmerald.types import ResponseCookies @@ -33,7 +33,7 @@ def __init__( media_type = _type break else: - media_type = MediaType.TEXT + media_type = MediaType.TEXT # pragma: no cover self.template = template_engine.get_template(template_name) self.context = context or {} @@ -47,7 +47,9 @@ def __init__( cookies=cookies, ) - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + async def __call__( + self, scope: Scope, receive: Receive, send: Send + ) -> None: # pragma: no cover request = self.context.get("request", {}) extensions = request.get("extensions", {}) if "http.response.template" in extensions: diff --git a/esmerald/routing/_internal.py b/esmerald/routing/_internal.py index 1c16a499..31193c58 100644 --- a/esmerald/routing/_internal.py +++ b/esmerald/routing/_internal.py @@ -45,7 +45,7 @@ def response_models(self) -> Dict[int, Any]: return responses @cached_property - def data_field(self) -> Any: + def data_field(self) -> Any: # pragma: no cover """ The field used for the payload body. diff --git a/esmerald/routing/base.py b/esmerald/routing/base.py index d3ee55b3..75602f0a 100644 --- a/esmerald/routing/base.py +++ b/esmerald/routing/base.py @@ -44,7 +44,7 @@ from esmerald.utils.helpers import is_async_callable, is_class_and_subclass from esmerald.utils.sync import AsyncCallable -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.applications import Esmerald from esmerald.interceptors.interceptor import EsmeraldInterceptor from esmerald.interceptors.types import Interceptor @@ -55,7 +55,6 @@ APIGateHandler, AsyncAnyCallable, Dependencies, - ExceptionHandlerMap, ResponseCookies, ResponseHeaders, ) @@ -86,7 +85,7 @@ class PathParameterSchema(TypedDict): type: Type -class OpenAPIDefinitionMixin: +class OpenAPIDefinitionMixin: # pragma: no cover def parse_path(self, path: str) -> List[Union[str, PathParameterSchema]]: """ Using the Starlette CONVERTORS and the application registered convertors, @@ -224,7 +223,7 @@ async def response_content(data: Response, **kwargs: Dict[str, Any]) -> Starlett **self.allow_header, } for cookie in _cookies: - data.set_cookie(**cookie) + data.set_cookie(**cookie) # pragma: no cover for header, value in _headers.items(): data.headers[header] = value @@ -252,7 +251,7 @@ async def response_content( **self.allow_header, } for cookie in _cookies: - data.set_cookie(**cookie) + data.set_cookie(**cookie) # pragma: no cover for header, value in _headers.items(): data.headers[header] = value @@ -286,7 +285,7 @@ async def response_content(data: Any, **kwargs: Dict[str, Any]) -> StarletteResp ) for cookie in _cookies: - response.set_cookie(**cookie) + response.set_cookie(**cookie) # pragma: no cover return response return response_content @@ -445,19 +444,10 @@ def path_parameters(self) -> Set[str]: return parameters @property - def normalised_path_params(self) -> List[PathParameterSchema]: - """ - Gets the path parameters in a PathParameterSchema format and it is - only used for OpenAPI documentation purposes only. - """ - path_components = self.parse_path(self.path) - parameters = [component for component in path_components if isinstance(component, dict)] - return parameters - - @property - def stringify_parameters(self) -> List[str]: + def stringify_parameters(self) -> List[str]: # pragma: no cover """ Gets the param:type in string like list. + Used for the directive `esmerald show_urls`. """ path_components = self.parse_path(self.path) parameters = [component for component in path_components if isinstance(component, dict)] @@ -548,19 +538,9 @@ def is_unique_dependency(dependencies: "Dependencies", key: str, injector: Injec f"If you wish to override a inject, it must have the same key." ) - def get_exception_handlers(self) -> "ExceptionHandlerMap": - """ - Resolves the exception_handlers by starting from the route handler - and moving up. - """ - resolved_exception_handlers: "ExceptionHandlerMap" = {} - for level in self.parent_levels: - resolved_exception_handlers.update(level.exception_handlers or {}) - return resolved_exception_handlers - def get_cookies( self, local_cookies: "ResponseCookies", other_cookies: "ResponseCookies" - ) -> List[Dict[str, Any]]: + ) -> List[Dict[str, Any]]: # pragma: no cover """ Returns a unique list of cookies. """ @@ -581,7 +561,7 @@ def get_headers(self, headers: "ResponseHeaders") -> Dict[str, Any]: """ return {k: v.value for k, v in headers.items()} - async def get_response_data(self, data: Any) -> Any: + async def get_response_data(self, data: Any) -> Any: # pragma: no cover """ Retrives the response data for sync and async. """ @@ -589,7 +569,7 @@ async def get_response_data(self, data: Any) -> Any: data = await data return data - async def allow_connection(self, connection: "HTTPConnection") -> None: + async def allow_connection(self, connection: "HTTPConnection") -> None: # pragma: no cover """ Validates the connection. @@ -605,7 +585,7 @@ async def allow_connection(self, connection: "HTTPConnection") -> None: continue_or_raise_permission_exception(request, handler, awaitable) -class BaseInterceptorMixin(BaseHandlerMixin): +class BaseInterceptorMixin(BaseHandlerMixin): # pragma: no cover def get_interceptors(self) -> List["AsyncCallable"]: """ Returns all the interceptors in the handler scope from the ownsership layers. diff --git a/esmerald/routing/events.py b/esmerald/routing/events.py index 1faae961..b39a3f4d 100644 --- a/esmerald/routing/events.py +++ b/esmerald/routing/events.py @@ -3,14 +3,14 @@ from starlette._utils import is_async_callable from starlette.types import Lifespan, Receive, Scope, Send -if TYPE_CHECKING: - from esmerald.types import DictAny, LifeSpanHandler +if TYPE_CHECKING: # pragma: no cover + from esmerald.types import LifeSpanHandler _T = TypeVar("_T") -class AyncLifespanContextManager: +class AyncLifespanContextManager: # pragma: no cover """ Manages and handles the on_startup and on_shutdown events in an Esmerald way. @@ -39,26 +39,24 @@ async def __aenter__(self) -> None: """Runs the functions on startup""" for handler in self.on_startup: if is_async_callable(handler): - await handler() # type: ignore[call-arg] + await handler() else: - handler() # type: ignore[call-arg] + handler() - async def __aexit__( - self, scope: Scope, receive: Receive, send: Send, **kwargs: "DictAny" - ) -> None: + async def __aexit__(self, scope: Scope, receive: Receive, send: Send, **kwargs: "Any") -> None: """Runs the functions on shutdown""" for handler in self.on_shutdown: if is_async_callable(handler): - await handler() # type: ignore[call-arg] + await handler() else: - handler() # type: ignore[call-arg] + handler() def handle_lifespan_events( on_startup: Optional[Sequence["LifeSpanHandler"]] = None, on_shutdown: Optional[Sequence["LifeSpanHandler"]] = None, lifespan: Optional[Lifespan[Any]] = None, -) -> Any: +) -> Any: # pragma: no cover """Handles with the lifespan events in the new Starlette format of lifespan. This adds a mask that keeps the old `on_startup` and `on_shutdown` events variable declaration for legacy and comprehension purposes and build the async context manager @@ -75,7 +73,7 @@ def generate_lifespan_events( on_startup: Optional[Sequence["LifeSpanHandler"]] = None, on_shutdown: Optional[Sequence["LifeSpanHandler"]] = None, lifespan: Optional[Lifespan[Any]] = None, -) -> Any: +) -> Any: # pragma: no cover if lifespan: return lifespan return AyncLifespanContextManager(on_startup=on_startup, on_shutdown=on_shutdown) diff --git a/esmerald/routing/gateways.py b/esmerald/routing/gateways.py index a69d8078..b5949aa9 100644 --- a/esmerald/routing/gateways.py +++ b/esmerald/routing/gateways.py @@ -12,7 +12,7 @@ from esmerald.utils.helpers import clean_string, is_class_and_subclass from esmerald.utils.url import clean_path -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.interceptors.types import Interceptor from esmerald.permissions.types import Permission from esmerald.routing.router import HTTPHandler, WebSocketHandler @@ -200,6 +200,6 @@ async def handle(self, scope: "Scope", receive: "Receive", send: "Send") -> None Handles the interception of messages and calls from the API. """ if self.get_interceptors(): - await self.intercept(scope, receive, send) + await self.intercept(scope, receive, send) # pragma: no cover await self.handler.handle(scope, receive, send) diff --git a/esmerald/routing/handlers.py b/esmerald/routing/handlers.py index 6268a465..cd5be7bb 100644 --- a/esmerald/routing/handlers.py +++ b/esmerald/routing/handlers.py @@ -18,7 +18,7 @@ ) from esmerald.utils.constants import AVAILABLE_METHODS -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from openapi_schemas_pydantic.v3_1_0 import SecurityScheme @@ -193,7 +193,7 @@ def __init__( ) -class trace(HTTPHandler): +class trace(HTTPHandler): # pragma: no cover def __init__( self, path: Optional[str] = None, @@ -516,7 +516,7 @@ def __init__( ) methods = [method.upper() for method in methods] - if not status_code: + if not status_code: # pragma: no cover status_code = status.HTTP_200_OK super().__init__( diff --git a/esmerald/routing/router.py b/esmerald/routing/router.py index a3464e75..32feac9b 100644 --- a/esmerald/routing/router.py +++ b/esmerald/routing/router.py @@ -65,7 +65,7 @@ from esmerald.utils.url import clean_path from esmerald.websockets import WebSocket, WebSocketClose -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from openapi_schemas_pydantic.v3_1_0.security_scheme import SecurityScheme from esmerald.applications import Esmerald @@ -102,7 +102,7 @@ def create_signature_models(self, route: "RouteParent") -> None: if isinstance(route, Gateway): if not route.handler.parent: - route.handler.parent = route + route.handler.parent = route # pragma: no cover if not is_class_and_subclass(route.handler, APIView) and not isinstance( route.handler, APIView @@ -134,7 +134,7 @@ def validate_root_route_parent( if not value.handler.parent: value.handler.parent = value else: - if not value.handler.parent: + if not value.handler.parent: # pragma: no cover value(parent=self) # type: ignore handler: APIView = cast("APIView", value.handler) @@ -233,7 +233,9 @@ def __init__( Router, ), ): - raise ImproperlyConfigured(f"The route {route} must be of type Gateway or Include") + raise ImproperlyConfigured( + f"The route {route} must be of type Gateway or Include" + ) # pragma: no cover assert lifespan is None or ( on_startup is None and on_shutdown is None @@ -292,7 +294,7 @@ def add_apiview(self, value: Union["Gateway", "WebSocketGateway"]) -> None: Generates the signature model for it and sorts the routing list. """ routes = [] - if not value.handler.parent: + if not value.handler.parent: # pragma: no cover value.handler(parent=self) # type: ignore route_handlers: List[Union[HTTPHandler, WebSocketHandler]] = value.handler.get_route_handlers() # type: ignore @@ -383,7 +385,9 @@ def add_websocket_route( self.create_signature_models(websocket_gateway) self.routes.append(websocket_gateway) - async def not_found(self, scope: "Scope", receive: "Receive", send: "Send") -> None: + async def not_found( + self, scope: "Scope", receive: "Receive", send: "Send" + ) -> None: # pragma: no cover """Esmerald version of a not found handler when a resource is called and cannot be dealt with properly. @@ -530,7 +534,7 @@ def __init__( self.methods: Set[str] = {HttpMethod[method].value for method in methods} - if isinstance(status_code, IntEnum): + if isinstance(status_code, IntEnum): # pragma: no cover status_code = int(status_code) self.status_code = status_code @@ -600,13 +604,6 @@ def allow_header(self) -> Mapping[str, str]: """ return {"allow": str(self.methods)} - @property - def permission_names(self) -> Sequence[str]: - """ - List of permissions for the route. This is used for OpenAPI Spec purposes only. - """ - return [permission.__name__ for permission in self.permissions] - def get_response_class(self) -> Type["Response"]: """ Returns the closest custom Response class in the parent graph or the @@ -697,14 +694,14 @@ async def index(request: Request) -> Response: "Cannot call check_handler_function without first setting self.fn" ) - if not settings.enable_sync_handlers: + if not settings.enable_sync_handlers: # pragma: no cover fn = cast("AnyCallable", self.fn) if not is_async_callable(fn): raise ImproperlyConfigured( "Functions decorated with 'route, websocket, get, patch, put, post and delete' must be async functions" ) - def validate_annotations(self) -> None: + def validate_annotations(self) -> None: # pragma: no cover """ Validate annotations of the handlers. """ @@ -737,7 +734,7 @@ def validate_annotations(self) -> None: ]: self.media_type = MediaType.TEXT - def validate_reserved_kwargs(self) -> None: + def validate_reserved_kwargs(self) -> None: # pragma: no cover """ Validates if special words are in the signature. """ @@ -822,7 +819,7 @@ def validate_reserved_words(self, signature: "Signature") -> None: f"The '{kwarg}'is not supported with websocket handlers." ) - def validate_websocket_handler_function(self) -> None: + def validate_websocket_handler_function(self) -> None: # pragma: no cover """ Validates the route handler function once it is set by inspecting its return annotations. @@ -971,7 +968,7 @@ def __init__( include_middleware: Sequence["Middleware"] = [] for _middleware in self.middleware: - if isinstance(_middleware, StarletteMiddleware): + if isinstance(_middleware, StarletteMiddleware): # pragma: no cover include_middleware.append(_middleware) # type: ignore else: include_middleware.append( @@ -1073,7 +1070,7 @@ def resolve_route_path_handler( """ routing: List[Union[Gateway, WebSocketGateway, Include]] = [] - for route in routes: + for route in routes: # pragma: no cover if not isinstance(route, (Include, Gateway, WebSocketGateway)): raise ImproperlyConfigured("The route must be of type Gateway or Include") diff --git a/esmerald/routing/views.py b/esmerald/routing/views.py index 34217a01..bf658b2c 100644 --- a/esmerald/routing/views.py +++ b/esmerald/routing/views.py @@ -6,7 +6,7 @@ from esmerald.utils.url import clean_path -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.interceptors.types import Interceptor from esmerald.permissions.types import Permission from esmerald.routing.gateways import Gateway, WebSocketGateway @@ -114,7 +114,7 @@ def get_route_handlers(self) -> List[Union["HTTPHandler", "WebSocketHandler"]]: if self.include_in_schema is not None and not isinstance( route_handler, WebSocketHandler - ): + ): # pragma: no cover route_handler.include_in_schema = self.include_in_schema if self.middleware: @@ -122,7 +122,7 @@ def get_route_handlers(self) -> List[Union["HTTPHandler", "WebSocketHandler"]]: if self.exception_handlers: route_handler.exception_handlers = self.get_exception_handlers(route_handler) - if self.tags or []: + if self.tags or []: # pragma: no cover for tag in reversed(self.tags): route_handler.tags.insert(0, tag) route_handlers.append(route_handler) @@ -147,8 +147,10 @@ def get_exception_handlers( exception_handlers = {**self.exception_handlers, **handler.exception_handlers} return cast("ExceptionHandlerMap", exception_handlers) - async def handle(self, scope: "Scope", receive: "Receive", send: "Send") -> None: + async def handle( + self, scope: "Scope", receive: "Receive", send: "Send" + ) -> None: # pragma: no cover raise NotImplementedError("APIView object does not implement handle()") - def create_signature_model(self, is_websocket: bool = False) -> None: + def create_signature_model(self, is_websocket: bool = False) -> None: # pragma: no cover raise NotImplementedError("APIView object does not implement create_signature_model()") diff --git a/esmerald/security/jwt/token.py b/esmerald/security/jwt/token.py index 8741e76e..a0b2e54c 100644 --- a/esmerald/security/jwt/token.py +++ b/esmerald/security/jwt/token.py @@ -29,10 +29,10 @@ def validate_expiration(cls, date: datetime) -> datetime: date = convert_time(date) if date.timestamp() >= convert_time(datetime.now(timezone.utc)).timestamp(): return date - raise ValueError("The exp must be a date in the future.") + raise ValueError("The exp must be a date in the future.") # pragma: no cover @field_validator("iat") - def validate_iat(cls, date: datetime) -> datetime: + def validate_iat(cls, date: datetime) -> datetime: # pragma: no cover """Ensures that the `Issued At` it's nt bigger than the current time.""" date = convert_time(date) if date.timestamp() <= convert_time(datetime.now(timezone.utc)).timestamp(): @@ -40,13 +40,13 @@ def validate_iat(cls, date: datetime) -> datetime: raise ValueError("iat must be a current or past time") @field_validator("sub") - def validate_sub(cls, subject: Union[str, int]) -> str: + def validate_sub(cls, subject: Union[str, int]) -> str: # pragma: no cover try: return str(subject) except (TypeError, ValueError) as e: raise ValueError(f"{subject} is not a valid string.") from e - def encode(self, key: str, algorithm: str) -> Union[str, Any]: + def encode(self, key: str, algorithm: str) -> Union[str, Any]: # pragma: no cover """ Encodes the token into a proper str formatted and allows passing kwargs. """ @@ -60,7 +60,9 @@ def encode(self, key: str, algorithm: str) -> Union[str, Any]: raise ImproperlyConfigured("Error encoding the token.") from e @staticmethod - def decode(token: str, key: Union[str, Dict[str, str]], algorithms: List[str]) -> "Token": + def decode( + token: str, key: Union[str, Dict[str, str]], algorithms: List[str] + ) -> "Token": # pragma: no cover """ Decodes the given token. """ diff --git a/esmerald/template/jinja.py b/esmerald/template/jinja.py index 6b385938..06660137 100644 --- a/esmerald/template/jinja.py +++ b/esmerald/template/jinja.py @@ -5,14 +5,14 @@ from esmerald.exceptions import MissingDependency, TemplateNotFound from esmerald.protocols.template import TemplateEngineProtocol -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from pydantic import DirectoryPath try: from jinja2 import Environment, FileSystemLoader from jinja2 import Template as JinjaTemplate from jinja2 import TemplateNotFound as JinjaTemplateNotFound -except ImportError as exc: +except ImportError as exc: # pragma: no cover raise MissingDependency("jinja2 is not installed") from exc @@ -52,5 +52,5 @@ def url_for(context: dict, name: str, **path_params: Any) -> Any: def get_template(self, template_name: str) -> JinjaTemplate: try: return self.env.get_template(template_name) - except JinjaTemplateNotFound as e: + except JinjaTemplateNotFound as e: # pragma: no cover raise TemplateNotFound(template_name=template_name) from e diff --git a/esmerald/template/mako.py b/esmerald/template/mako.py index 0cc3ecd5..8ddf3419 100644 --- a/esmerald/template/mako.py +++ b/esmerald/template/mako.py @@ -3,14 +3,14 @@ from esmerald.exceptions import MissingDependency, TemplateNotFound from esmerald.protocols.template import TemplateEngineProtocol -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from pydantic import DirectoryPath try: from mako.exceptions import TemplateLookupException as MakoTemplateNotFound from mako.lookup import TemplateLookup from mako.template import Template as MakoTemplate -except ImportError as exc: +except ImportError as exc: # pragma: no cover raise MissingDependency("mako is not installed") from exc @@ -21,7 +21,7 @@ def __init__(self, directory: Union["DirectoryPath", List["DirectoryPath"]]) -> directories=directory if isinstance(directory, (list, tuple)) else [directory] ) - def get_template(self, template_name: str) -> MakoTemplate: + def get_template(self, template_name: str) -> MakoTemplate: # pragma: no cover try: return self.engine.get_template(template_name) except MakoTemplateNotFound as e: diff --git a/esmerald/testclient.py b/esmerald/testclient.py index 86d7af13..8f4771ae 100644 --- a/esmerald/testclient.py +++ b/esmerald/testclient.py @@ -19,7 +19,7 @@ from esmerald.applications import Esmerald from esmerald.utils.crypto import get_random_secret_key -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from typing_extensions import Literal from esmerald.config import ( diff --git a/esmerald/transformers/datastructures.py b/esmerald/transformers/datastructures.py index d25ee546..49a659e0 100644 --- a/esmerald/transformers/datastructures.py +++ b/esmerald/transformers/datastructures.py @@ -76,7 +76,7 @@ def __init__( ) -> None: super().__init__(**kwargs) if parameter.annotation is Signature.empty: - raise ImproperlyConfigured( + raise ImproperlyConfigured( # pragma: no cover f"The parameter name {param_name} from {fn_name} does not have a type annotation. " "If it should receive any value, use 'Any' as type." ) diff --git a/esmerald/transformers/model.py b/esmerald/transformers/model.py index f58436b2..aa46f756 100644 --- a/esmerald/transformers/model.py +++ b/esmerald/transformers/model.py @@ -19,8 +19,8 @@ from esmerald.utils.pydantic.schema import is_field_optional if TYPE_CHECKING: - from esmerald.types import Dependencies - from esmerald.websockets import WebSocket + from esmerald.types import Dependencies # pragma: no cover + from esmerald.websockets import WebSocket # pragma: no cover MEDIA_TYPES = [EncodingType.MULTI_PART, EncodingType.URL_ENCODED] @@ -73,11 +73,6 @@ def get_query_params(self) -> Set[ParamSetting]: def get_header_params(self) -> Set[ParamSetting]: return self.headers - def is_kwargs( - self, - ) -> Union[Set[ParamSetting], Set[str], Tuple[EncodingType, FieldInfo], Set[Dependency]]: - return self.has_kwargs - @classmethod def dependency_tree(cls, key: str, dependencies: "Dependencies") -> Dependency: inject = dependencies[key] @@ -244,7 +239,7 @@ def update_parameters( headers = merge_sets(headers, dependency_model.headers) if "data" in reserved_kwargs and "data" in dependency_model.reserved_kwargs: - cls.validate_data(form_data, dependency_model) + cls.validate_data(form_data, dependency_model) # pragma: no cover reserved_kwargs.update(dependency_model.reserved_kwargs) return path_params, query_params, cookies, headers, reserved_kwargs @@ -312,7 +307,7 @@ def handle_reserved_kwargs( if "query" in self.reserved_kwargs: reserved_kwargs["query"] = connection_params if "state" in self.reserved_kwargs: - reserved_kwargs["state"] = connection.app.state.copy() + reserved_kwargs["state"] = connection.app.state.copy() # pragma: no cover return {**reserved_kwargs, **path_params, **query_params, **headers, **cookies} @@ -321,7 +316,7 @@ def validate_data( cls, form_data: Optional[Tuple[EncodingType, FieldInfo]], dependency_model: "TransformerModel", - ) -> None: + ) -> None: # pragma: no cover if form_data and dependency_model.form_data: media_type, _ = form_data dependency_media_type, _ = dependency_model.form_data @@ -346,7 +341,7 @@ def validate_kwargs( path_parameters: Set[str], dependencies: "Dependencies", model_fields: Dict[str, FieldInfo], - ) -> None: + ) -> None: # pragma: no cover keys = set(dependencies.keys()) names = set() diff --git a/esmerald/transformers/signature.py b/esmerald/transformers/signature.py index e1982362..6294e3b7 100644 --- a/esmerald/transformers/signature.py +++ b/esmerald/transformers/signature.py @@ -1,5 +1,5 @@ from inspect import Signature as InspectSignature -from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Set, Type +from typing import TYPE_CHECKING, Any, Dict, Generator, Set, Type from pydantic import create_model @@ -12,19 +12,15 @@ from esmerald.utils.dependency import is_dependency_field, should_skip_dependency_validation if TYPE_CHECKING: - from esmerald.typing import AnyCallable + from esmerald.typing import AnyCallable # pragma: no cover object_setattr = object.__setattr__ class SignatureFactory(ArbitraryExtraBaseModel): - def __init__( - self, fn: Optional["AnyCallable"], dependency_names: Set[str], **kwargs: Any - ) -> None: + def __init__(self, fn: "AnyCallable", dependency_names: Set[str], **kwargs: Any) -> None: super().__init__(**kwargs) - if not fn: - raise ImproperlyConfigured("Parameter 'fn' to SignatureFactory cannot be `None`.") self.fn = fn self.signature = InspectSignature.from_callable(self.fn) self.fn_name = fn.__name__ if hasattr(fn, "__name__") else "anonymous" @@ -85,6 +81,6 @@ def create_signature(self) -> Type[EsmeraldSignature]: model.dependency_names = self.dependency_names return model except TypeError as e: - raise ImproperlyConfigured( + raise ImproperlyConfigured( # pragma: no cover f"Error creating signature for '{self.fn_name}': '{e}'." ) from e diff --git a/esmerald/transformers/utils.py b/esmerald/transformers/utils.py index 67bb5c84..48689405 100644 --- a/esmerald/transformers/utils.py +++ b/esmerald/transformers/utils.py @@ -11,7 +11,7 @@ from esmerald.typing import Undefined from esmerald.utils.constants import REQUIRED -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from esmerald.injector import Inject from esmerald.transformers.datastructures import EsmeraldSignature, Parameter from esmerald.types import ConnectionType @@ -136,6 +136,4 @@ def get_field_definition_from_param(param: "Parameter") -> Tuple[Any, Any]: definition = (param.annotation, param.default) elif not param.optional: definition = (param.annotation, ...) - else: - definition = (param.annotation, None) return definition diff --git a/esmerald/types.py b/esmerald/types.py index 7b3dcef7..a9c3c44a 100644 --- a/esmerald/types.py +++ b/esmerald/types.py @@ -118,9 +118,4 @@ DictAny = Dict[str, Any] SettingsType = Type["EsmeraldAPISettings"] -LifeSpanHandler = Union[ - Callable[[], Any], - Callable[[State], Any], - Callable[[], Awaitable[Any]], - Callable[[State], Awaitable[Any]], -] +LifeSpanHandler = Callable[[], Response] diff --git a/esmerald/utils/crypto.py b/esmerald/utils/crypto.py index f43196cd..c255f662 100644 --- a/esmerald/utils/crypto.py +++ b/esmerald/utils/crypto.py @@ -1,21 +1,4 @@ -import hashlib import random -import time - -from esmerald.conf import settings - -try: - _random = random.SystemRandom() - using_sysrandom = True -except NotImplementedError: - import warnings - - warnings.warn( - "A secure pseudo-random number generator is not available " - "on your system. Falling back to Mersenne Twister.", - stacklevel=2, - ) - using_sysrandom = False def get_random_string( @@ -27,20 +10,6 @@ def get_random_string( The default length of 12 with the a-z, A-Z, 0-9 character set returns a 71-bit value. log_2((26+26+10)^12) =~ 71 bits """ - if not using_sysrandom: - # This is ugly, and a hack, but it makes things better than - # the alternative of predictability. This re-seeds the PRNG - # using a value that is hard for an attacker to predict, every - # time a random string is required. This may change the - # properties of the chosen random sequence slightly, but this - # is better than absolute predictability. - _random.seed( - hashlib.sha256( - ("{}{}{}".format(random.getstate(), time.time(), settings.secret_key)).encode( - "utf-8" - ) - ).digest() - ) return "".join(random.choice(allowed_chars) for _ in range(length)) diff --git a/esmerald/utils/functional.py b/esmerald/utils/functional.py index 1c92df09..e357d09d 100644 --- a/esmerald/utils/functional.py +++ b/esmerald/utils/functional.py @@ -15,7 +15,7 @@ def inner(self, *args: Any) -> RT: # type: ignore return inner -class LazyObject: +class LazyObject: # pragma: no cover """ A wrapper for another class that can be used to delay instantiation of the wrapped class. @@ -128,7 +128,7 @@ def __deepcopy__(self, memo: Any) -> Any: __contains__: Any = new_method_proxy(operator.contains) -def unpickle_lazyobject(wrapped: Any) -> Any: +def unpickle_lazyobject(wrapped: Any) -> Any: # pragma: no cover """ Used to unpickle lazy objects. Just return its argument, which will be the wrapped object. @@ -136,7 +136,7 @@ def unpickle_lazyobject(wrapped: Any) -> Any: return wrapped -class SimpleLazyObject(LazyObject): +class SimpleLazyObject(LazyObject): # pragma: no cover """ A lazy object initialized from any function. Designed for compound objects of unknown type. For builtins or objects of diff --git a/esmerald/utils/pydantic/schema.py b/esmerald/utils/pydantic/schema.py index b1158220..787d06d6 100644 --- a/esmerald/utils/pydantic/schema.py +++ b/esmerald/utils/pydantic/schema.py @@ -1,10 +1,9 @@ from decimal import Decimal -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import Any, TypeVar, cast -T = TypeVar("T", int, float, Decimal) +from pydantic.fields import FieldInfo -if TYPE_CHECKING: - from pydantic.fields import FieldInfo +T = TypeVar("T", int, float, Decimal) def is_field_optional(field: "FieldInfo") -> bool: diff --git a/esmerald/utils/sync.py b/esmerald/utils/sync.py index b9a187ff..17b0b2f1 100644 --- a/esmerald/utils/sync.py +++ b/esmerald/utils/sync.py @@ -33,7 +33,7 @@ def execsync(async_function: Callable[..., T], raise_error: bool = True) -> Call """ @functools.wraps(async_function) - def wrapper(*args: Any, **kwargs: Any) -> T: + def wrapper(*args: Any, **kwargs: Any) -> T: # pragma: no cover current_async_module = getattr(threadlocals, "current_async_module", None) partial_func = functools.partial(async_function, *args, **kwargs) if current_async_module is not None and raise_error is True: diff --git a/esmerald/websockets.py b/esmerald/websockets.py index d5b901e8..e932ed12 100644 --- a/esmerald/websockets.py +++ b/esmerald/websockets.py @@ -18,4 +18,4 @@ class WebSocketDisconnect(StarletteWebSocketDisconnect): """Esmerald WebSocketDisconnect""" def __init__(self, code: int = 1000, reason: Optional[str] = None) -> None: - super().__init__(code, reason) + super().__init__(code, reason) # pragma: no cover diff --git a/pyproject.toml b/pyproject.toml index 99062d5b..fdd83d3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,13 +34,14 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + # "Programming Language :: Python :: 3.12", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] dependencies = [ "a2wsgi>=1.7.0,<2", "aiofiles>=0.8.0,<24", - "anyio>=3.6.2,<4.0.0", + "anyio>=3.7.1,<5.0.0", "awesome-slugify>=1.6.5,<2", "click>=8.1.4,<9.0.0", "httpx>=0.24.0,<0.30.0", @@ -55,7 +56,7 @@ dependencies = [ "openapi-schemas-pydantic>=2.0.0", "orjson>=3.8.5,<4.0.0", "rich>=13.3.1,<14.0.0", - "starlette>=0.30.0,<1.0", + "starlette>=0.31.0,<1.0", ] keywords = [ "api", @@ -93,8 +94,8 @@ Source = "https://github.com/dymmond/esmerald" [project.optional-dependencies] test = [ - "pytest>=7.3.1,<8.0.0", - "pytest-cov>=2.12.0,<5.0.0", + "pytest>=7.4.0,<8.0.0", + "pytest-cov>=4.1.0,<5.0.0", "pytest-asyncio>=0.19.0", "mypy>=1.4.1", "flake8>=5.0.4", @@ -142,7 +143,7 @@ doc = [ "pyyaml>=6.0,<7.0.0", ] -templates = ["mako==1.2.4"] +templates = ["mako>=1.2.4,<2.0.0"] jwt = ["passlib==1.7.4", "python-jose>=3.3.0,<4"] encoders = ["orjson>=3.8.5,<4.0.0", "ujson>=5.7.0,<6"] schedulers = ["asyncz>=0.4.0"] @@ -193,6 +194,25 @@ ignore = [ exclude = ["docs_src/*"] +[tool.coverage.run] +parallel = true +context = '${CONTEXT}' +source = ["tests", "esmerald"] +omit = [ + "esmerald/conf/__init__.py", + "esmerald/middleware/errors.py", + "esmerald/permissions/base.py", + "esmerald/core/directives/*", + "esmerald/core/terminal/*", + "esmerald/datastructures/types.py", + "esmerald/types.py", + "esmerald/contrib/*", + "esmerald/openapi/openapi.py", + "tests/databases/saffier/*", + "tests/utils/functional.py", + "tests/cli/*", +] + [[tool.mypy.overrides]] module = "esmerald.tests.*" ignore_missing_imports = true diff --git a/esmerald/protocols/utils/__init__.py b/tests/app_settings/__init__.py similarity index 100% rename from esmerald/protocols/utils/__init__.py rename to tests/app_settings/__init__.py diff --git a/tests/app_settings/test_global_settings.py b/tests/app_settings/test_global_settings.py new file mode 100644 index 00000000..17c605d6 --- /dev/null +++ b/tests/app_settings/test_global_settings.py @@ -0,0 +1,67 @@ +import builtins +import sys +from typing import List + +import pytest +from asyncz.schedulers import AsyncIOScheduler + +from esmerald import CORSConfig, Esmerald, EsmeraldAPISettings +from esmerald.conf.enums import EnvironmentType + +real_import = builtins.__import__ + + +def monkey_import_importerror(name, globals=None, locals=None, fromlist=(), level=0): + if name in ("asyncz", "asyncz.schedulers"): + raise ImportError(f"Mocked import error {name}") + return real_import(name, globals=globals, locals=locals, fromlist=fromlist, level=level) + + +class AppSettings(EsmeraldAPISettings): + environment: str = EnvironmentType.PRODUCTION + + +class SchedulerClassSettings(EsmeraldAPISettings): + enable_scheduler: bool = True + + +class CorsAppSettings(EsmeraldAPISettings): + allow_origins: List[str] = ["*"] + + +def test_reload_false(): + app = Esmerald(routes=[], settings_config=AppSettings) + + assert app.default_settings.reload is True + + # Main settings used + assert app.settings.reload is False + + +def test_routes_empty(): + app = Esmerald(routes=[], settings_config=AppSettings) + + assert app.settings.routes == [] + + +def test_cors_config(): + app = Esmerald(routes=[], settings_config=CorsAppSettings(), allow_origins=["*"]) + + assert app.default_settings.reload is True + + # Main settings used + assert isinstance(app.settings.cors_config, CORSConfig) + + +def test_scheduler_class_raises_error(monkeypatch): + monkeypatch.delitem(sys.modules, "asyncz", raising=False) + monkeypatch.setattr(builtins, "__import__", monkey_import_importerror) + + with pytest.raises(ImportError): + Esmerald(routes=[], settings_config=SchedulerClassSettings) + + +def test_scheduler_class(): + app = Esmerald(routes=[], settings_config=SchedulerClassSettings) + + assert app.settings.scheduler_class == AsyncIOScheduler diff --git a/tests/test_settings.py b/tests/app_settings/test_settings.py similarity index 86% rename from tests/test_settings.py rename to tests/app_settings/test_settings.py index f26cb3e6..12934944 100644 --- a/tests/test_settings.py +++ b/tests/app_settings/test_settings.py @@ -1,5 +1,5 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List +from typing import Any, Dict, List import pytest from pydantic import BaseModel @@ -20,13 +20,11 @@ from esmerald.exceptions import ImproperlyConfigured from esmerald.middleware import RequestSettingsMiddleware from esmerald.testclient import create_client +from esmerald.types import Middleware from esmerald.utils.crypto import get_random_secret_key -if TYPE_CHECKING: - from esmerald.types import Middleware - -def test_default_settings(): +def test_main_settings(): with create_client([]) as client: assert client.app.settings.app_name == settings.app_name assert client.app.settings.environment == "testing" @@ -149,6 +147,38 @@ async def _app_settings(request: Request) -> str: assert client.app.app_name == "new app" assert settings.app_name == "test_client" assert "RequestSettingsMiddleware" == response.json()["middleware"][0] + assert isinstance(client.app.settings_config, EsmeraldAPISettings) + + +def test_inner_settings_config_as_instance(test_client_factory): + """ + Test passing a settings config and being used with teh ESMERALD_SETTINGS_MODULE + """ + + class AppSettings(DisableOpenAPI): + app_name: str = "new app" + allowed_hosts: List[str] = ["*", "*.testserver.com"] + + @property + def middleware(self) -> List["Middleware"]: + return [RequestSettingsMiddleware] + + @get("/app-settings") + async def _app_settings(request: Request) -> str: + return JSONResponse( + {"middleware": [middleware.__name__ for middleware in request.app.settings.middleware]} + ) + + with create_client( + routes=[Gateway(handler=_app_settings)], settings_config=AppSettings() + ) as client: + response = client.get("/app-settings") + + assert client.app.settings.app_name == "new app" + assert client.app.app_name == "new app" + assert settings.app_name == "test_client" + assert "RequestSettingsMiddleware" == response.json()["middleware"][0] + assert isinstance(client.app.settings_config, EsmeraldAPISettings) def test_child_esmerald_independent_settings(test_client_factory): @@ -198,7 +228,7 @@ def cors_config(self) -> CORSConfig: @get("/app-settings") async def _app_settings(request: Request) -> Dict[Any, Any]: - return request.app_settings.model_dump_json() + return request.app_settings.model_dump_json() # pragma: no cover secret = get_random_secret_key() child = ChildEsmerald( @@ -268,7 +298,7 @@ def test_raises_exception_on_wrong_settings(settings_config, test_client_factory """If a settings_config is thrown but not type EsmeraldAPISettings""" with pytest.raises(ImproperlyConfigured): with create_client(routes=[], settings_config=settings_config): - ... + """ """ def test_basic_settings(test_client_factory): @@ -285,3 +315,15 @@ def test_basic_settings(test_client_factory): assert app.include_in_schema is False assert app.enable_openapi is False assert app.redirect_slashes is False + + +def test_default_settings(): + app = Esmerald( + debug=False, + enable_scheduler=False, + include_in_schema=False, + enable_openapi=False, + redirect_slashes=False, + ) + + assert id(app.default_settings) == id(settings) diff --git a/tests/applications/test_applications.py b/tests/applications/test_applications.py index bf4d39c8..dd87c973 100644 --- a/tests/applications/test_applications.py +++ b/tests/applications/test_applications.py @@ -9,7 +9,7 @@ from esmerald import Request from esmerald.applications import Esmerald -from esmerald.exceptions import HTTPException, WebSocketException +from esmerald.exceptions import HTTPException, ImproperlyConfigured, WebSocketException from esmerald.middleware import TrustedHostMiddleware from esmerald.responses import JSONResponse, PlainTextResponse from esmerald.routing.gateways import Gateway, WebSocketGateway @@ -19,7 +19,7 @@ from esmerald.websockets import WebSocket -async def error_500(request, exc): +async def error_500(request, exc): # pragma: no cover return JSONResponse({"detail": "Server Error"}, status_code=500) @@ -59,7 +59,7 @@ def custom_subdomain(request: Request) -> PlainTextResponse: @get() def runtime_error(request: Request) -> None: - raise RuntimeError() + raise RuntimeError() # pragma: no cover @head() @@ -304,6 +304,24 @@ async def homepage(request: Request) -> PlainTextResponse: assert response.text == "Hello, World!" +def test_app_add_route_with_root_path(test_client_factory): + @get() + async def homepage(request: Request) -> PlainTextResponse: + return PlainTextResponse("Hello, World!") + + app = Esmerald( + routes=[ + Gateway("/", handler=homepage), + ], + root_path="/", + ) + + client = test_client_factory(app) + response = client.get("/") + assert response.status_code == 200 + assert response.text == "Hello, World!" + + def test_app_add_websocket_route(test_client_factory): @websocket() async def websocket_endpoint(socket: WebSocket) -> None: @@ -422,3 +440,15 @@ def lifespan(app): assert not cleanup_complete assert startup_complete assert cleanup_complete + + +def test_raise_improperly_configured_on_route_function(test_client_factory): + with pytest.raises(ImproperlyConfigured): + app = Esmerald(routes=[]) + app.route(path="/") + + +def test_raise_improperly_configured_on_websocket_route_function(test_client_factory): + with pytest.raises(ImproperlyConfigured): + app = Esmerald(routes=[]) + app.websocket_route(path="/") diff --git a/tests/applications/test_scheduler.py b/tests/applications/test_scheduler.py index 1f5d4425..2ef9ef0e 100644 --- a/tests/applications/test_scheduler.py +++ b/tests/applications/test_scheduler.py @@ -1,10 +1,36 @@ +import builtins +import sys + +import pytest from asyncz.schedulers import AsyncIOScheduler from esmerald.testclient import create_client +real_import = builtins.__import__ + + +def monkey_import_importerror(name, globals=None, locals=None, fromlist=(), level=0): + if name in ( + "asyncz", + "asyncz.contrib", + "asyncz.contrib.esmerald", + "asyncz.contrib.esmerald.scheduler", + ): + raise ImportError(f"Mocked import error {name}") + return real_import(name, globals=globals, locals=locals, fromlist=fromlist, level=level) + def test_default_scheduler(test_client_factory): with create_client([], enable_scheduler=True, scheduler_class=AsyncIOScheduler) as client: app = client.app assert app.scheduler_class == AsyncIOScheduler + + +def test_raises_import_error_on_missing_module(monkeypatch): + monkeypatch.delitem(sys.modules, "asyncz", raising=False) + monkeypatch.setattr(builtins, "__import__", monkey_import_importerror) + + with pytest.raises(ImportError): + with create_client([], enable_scheduler=True, scheduler_class=AsyncIOScheduler): + """ """ diff --git a/tests/conftest.py b/tests/conftest.py index 31678a8e..b6fc230c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ @pytest.fixture -def no_trio_support(anyio_backend_name): +def no_trio_support(anyio_backend_name): # pragma: no cover if anyio_backend_name == "trio": pytest.skip("Trio not supported (yet!)") diff --git a/tests/datastructures_upload/test_no_upload_file.py b/tests/datastructures_upload/test_no_upload_file.py index 332391a8..59efde5a 100644 --- a/tests/datastructures_upload/test_no_upload_file.py +++ b/tests/datastructures_upload/test_no_upload_file.py @@ -16,8 +16,6 @@ async def create_file(data: Union[UploadFile, None] = File()) -> Dict[str, str]: @post("/upload", status_code=status.HTTP_200_OK) async def upload_file(data: Union[UploadFile, None] = File()) -> Dict[str, str]: - if not data: - return {"details": "No file sent"} return {"size": data.filename} diff --git a/tests/datastructures_upload/test_upload_list_files.py b/tests/datastructures_upload/test_upload_list_files.py index 66bbdbf3..68d18505 100644 --- a/tests/datastructures_upload/test_upload_list_files.py +++ b/tests/datastructures_upload/test_upload_list_files.py @@ -34,11 +34,6 @@ async def upload_list_multiple_file( return {"names": names, "total": total} -@post("/upload-dict-multiple", status_code=status.HTTP_200_OK) -async def upload_dict_multiple_file(data: MultipleFile = File()) -> Dict[str, str]: - return {"names": [], "total": 2} - - app = Esmerald( routes=[ Gateway(handler=upload_file), diff --git a/tests/dependencies/test_dependency_validation.py b/tests/dependencies/test_dependency_validation.py index 2dfbe0aa..43c7799f 100644 --- a/tests/dependencies/test_dependency_validation.py +++ b/tests/dependencies/test_dependency_validation.py @@ -8,12 +8,12 @@ from esmerald.routing.router import Include -def first_method(query_param: int) -> int: +def first_method(query_param: int) -> int: # pragma: no cover assert isinstance(query_param, int) return query_param -def second_method(path_param: str) -> str: +def second_method(path_param: str) -> str: # pragma: no cover assert isinstance(path_param, str) return path_param @@ -26,7 +26,7 @@ def test_dependency_validation() -> None: dependencies=dependencies, ) def test_function(first: int, second: str, third: int) -> None: - pass + """ """ with pytest.raises(ImproperlyConfigured): Esmerald( @@ -45,7 +45,7 @@ def test_dependency_validation_with_include() -> None: dependencies=dependencies, ) def test_function(first: int, second: str, third: int) -> None: - pass + """ """ with pytest.raises(ImproperlyConfigured): Esmerald( @@ -64,7 +64,7 @@ def test_dependency_validation_with_nested_include() -> None: dependencies=dependencies, ) def test_function(first: int, second: str, third: int) -> None: - pass + """ """ with pytest.raises(ImproperlyConfigured): Esmerald( @@ -88,7 +88,7 @@ def test_dependency_validation_with_two_nested_include() -> None: dependencies=dependencies, ) def test_function(first: int, second: str, third: int) -> None: - pass + """ """ with pytest.raises(ImproperlyConfigured): Esmerald( @@ -122,7 +122,7 @@ def test_dependency_validation_with_three_nested_include() -> None: dependencies=dependencies, ) def test_function(first: int, second: str, third: int) -> None: - pass + """ """ with pytest.raises(ImproperlyConfigured): Esmerald( diff --git a/tests/dependencies/test_http_handler_dependency_injection.py b/tests/dependencies/test_http_handler_dependency_injection.py index 6313ba64..e4f7cc3c 100644 --- a/tests/dependencies/test_http_handler_dependency_injection.py +++ b/tests/dependencies/test_http_handler_dependency_injection.py @@ -1,22 +1,20 @@ from asyncio import sleep -from typing import TYPE_CHECKING, Any, Dict +from typing import Any, Dict from starlette.status import HTTP_200_OK, HTTP_400_BAD_REQUEST from esmerald.applications import ChildEsmerald from esmerald.injector import Inject +from esmerald.requests import Request from esmerald.routing.gateways import Gateway from esmerald.routing.handlers import get from esmerald.routing.router import Include from esmerald.routing.views import APIView from esmerald.testclient import create_client -if TYPE_CHECKING: - from esmerald.requests import Request - def router_first_dependency() -> bool: - return True + return True # pragma: no cover async def router_second_dependency() -> bool: @@ -24,7 +22,7 @@ async def router_second_dependency() -> bool: return False -def controller_first_dependency(headers: Dict[str, Any]) -> dict: +def controller_first_dependency(headers: Dict[str, Any]) -> dict: # pragma: no cover assert headers return {} @@ -109,7 +107,7 @@ class SecondController(APIView): @get(path="/") def test_method(self, first: dict) -> None: - pass + """ """ with create_client( routes=[ @@ -127,7 +125,7 @@ class SecondController(APIView): @get(path="/") def test_method(self, first: dict) -> None: - pass + """ """ with create_client( routes=[ @@ -149,7 +147,7 @@ class SecondController(APIView): @get(path="/") def test_method(self, first: dict) -> None: - pass + """ """ with create_client( routes=[ @@ -197,7 +195,7 @@ class SecondController(APIView): @get(path="/") def test_method(self, first: dict) -> None: - pass + """ """ child_esmerald = ChildEsmerald( routes=[ diff --git a/tests/dependencies/test_injection_of_generic_models.py b/tests/dependencies/test_injection_of_generic_models.py index c524cda1..0f1bb7c3 100644 --- a/tests/dependencies/test_injection_of_generic_models.py +++ b/tests/dependencies/test_injection_of_generic_models.py @@ -16,7 +16,7 @@ class Store(BaseModel, Generic[T]): model: Type[T] - def get(self, value_id: str) -> Optional[T]: + def get(self, value_id: str) -> Optional[T]: # pragma: no cover raise NotImplementedError diff --git a/tests/dependencies/test_injects_with_fastapi_examples.py b/tests/dependencies/test_injects_with_fastapi_examples.py index 4e2bb8b3..3a050834 100644 --- a/tests/dependencies/test_injects_with_fastapi_examples.py +++ b/tests/dependencies/test_injects_with_fastapi_examples.py @@ -7,27 +7,27 @@ from esmerald.testclient import EsmeraldTestClient -class CallableDependency: +class CallableDependency: # pragma: no cover def __call__(self, value: str) -> str: return value -class CallableGenDependency: +class CallableGenDependency: # pragma: no cover def __call__(self, value: str) -> Generator[str, None, None]: yield value -class AsyncCallableDependency: +class AsyncCallableDependency: # pragma: no cover async def __call__(self, value: str) -> str: return value -class AsyncCallableGenDependency: +class AsyncCallableGenDependency: # pragma: no cover async def __call__(self, value: str) -> AsyncGenerator[str, None]: yield value -class MethodsDependency: +class MethodsDependency: # pragma: no cover def synchronous(self, value: str) -> str: return value diff --git a/tests/dependencies/test_websocket_handler_dependency_injection.py b/tests/dependencies/test_websocket_handler_dependency_injection.py index 58582ce8..cb4bf5d5 100644 --- a/tests/dependencies/test_websocket_handler_dependency_injection.py +++ b/tests/dependencies/test_websocket_handler_dependency_injection.py @@ -12,7 +12,7 @@ from esmerald.websockets import WebSocket -def router_first_dependency() -> bool: +def router_first_dependency() -> bool: # pragma: no cover return True @@ -21,9 +21,9 @@ async def router_second_dependency() -> bool: return False -def controller_first_dependency(headers: Dict[str, Any]) -> dict: +def controller_first_dependency(headers: Dict[str, Any]) -> dict: # pragma: no cover assert headers - return {} + return {} # pragma: no cover async def controller_second_dependency(socket: WebSocket) -> dict: @@ -110,7 +110,7 @@ async def test_function(socket: WebSocket, first: int, second: bool, third: str) ws.send_json({"data": "123"}) -def test_dependency_isolation() -> None: +def test_dependency_isolation() -> None: # pragma: no cover class SecondController(APIView): path = "/second" diff --git a/tests/exception_handlers/test_custom_exception_handlers.py b/tests/exception_handlers/test_custom_exception_handlers.py index 76a2b48b..fc5c70e8 100644 --- a/tests/exception_handlers/test_custom_exception_handlers.py +++ b/tests/exception_handlers/test_custom_exception_handlers.py @@ -5,7 +5,7 @@ from esmerald.testclient import create_client -class DataIn(BaseModel): +class DataIn(BaseModel): # pragma: no cover """ Model example with DataIn for custom cases and testing purposes. @@ -30,7 +30,7 @@ async def raised() -> JSON: @put("/update") -async def update() -> JSON: +async def update() -> DataIn: # pragma: no cover DataIn(name="Esmerald", email="test@esmerald.dev") diff --git a/tests/exception_handlers/test_exception_handlers.py b/tests/exception_handlers/test_exception_handlers.py index 19361e72..ab3554f6 100644 --- a/tests/exception_handlers/test_exception_handlers.py +++ b/tests/exception_handlers/test_exception_handlers.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Type +from typing import Type import pytest from starlette.status import HTTP_400_BAD_REQUEST @@ -20,9 +20,7 @@ from esmerald.routing.router import Include from esmerald.routing.views import APIView from esmerald.testclient import create_client - -if TYPE_CHECKING: - from esmerald.types import ExceptionHandlerMap +from esmerald.types import ExceptionHandlerMap @pytest.mark.parametrize( diff --git a/tests/handlers/test_to_response.py b/tests/handlers/test_to_response.py index 353d9bf3..12b2d6db 100644 --- a/tests/handlers/test_to_response.py +++ b/tests/handlers/test_to_response.py @@ -2,7 +2,7 @@ from json import loads from pathlib import Path from time import sleep -from typing import TYPE_CHECKING, Any, AsyncIterator, Generator, Iterator +from typing import Any, AsyncGenerator, AsyncIterator, Generator, Iterator import pytest from pydantic import ValidationError @@ -31,25 +31,22 @@ from esmerald.transformers.signature import SignatureFactory from tests.models import Individual, IndividualFactory -if TYPE_CHECKING: - from typing import AsyncGenerator - -def my_generator() -> Generator[str, None, None]: +def my_generator() -> Generator[str, None, None]: # pragma: no cover count = 0 while True: count += 1 yield str(count) -async def my_async_generator() -> "AsyncGenerator[str, None]": +async def my_async_generator() -> "AsyncGenerator[str, None]": # pragma: no cover count = 0 while True: count += 1 yield str(count) -class MySyncIterator: +class MySyncIterator: # pragma: no cover def __init__(self) -> None: self.delay = 0.01 self.i = 0 @@ -68,7 +65,7 @@ def __next__(self) -> str: return str(i) -class MyAsyncIterator: +class MyAsyncIterator: # pragma: no cover def __init__(self) -> None: self.delay = 0.01 self.i = 0 @@ -319,7 +316,7 @@ def test_function() -> Stream: @pytest.mark.asyncio() -async def func_to_response_template_response() -> None: +async def func_to_response_template_response() -> None: # pragma: no cover background_task = BackgroundTask(lambda: "") @get( diff --git a/tests/handlers/test_websocket_validations.py b/tests/handlers/test_websocket_validations.py index 46512fed..862d6b0d 100644 --- a/tests/handlers/test_websocket_validations.py +++ b/tests/handlers/test_websocket_validations.py @@ -11,13 +11,13 @@ def test_websocket_handler_function_validation() -> None: def fn_without_socket_arg(websocket: WebSocket) -> None: - pass + """ """ with pytest.raises(ImproperlyConfigured): websocket(path="/")(fn_without_socket_arg) # type: ignore def fn_with_return_annotation(socket: WebSocket) -> dict: - return {} + return {} # pragma: no cover with pytest.raises(ImproperlyConfigured): websocket(path="/")(fn_with_return_annotation) # type: ignore @@ -31,16 +31,16 @@ def fn_with_return_annotation(socket: WebSocket) -> dict: @websocket(path="/") async def websocket_handler_with_data_kwarg(socket: WebSocket, data: Any) -> None: - ... + """ """ with pytest.raises(ImproperlyConfigured): @websocket(path="/") async def websocket_handler_with_request_kwarg(socket: WebSocket, request: Any) -> None: - ... + """ """ with pytest.raises(ImproperlyConfigured): @websocket(path="/") # type: ignore def sync_websocket_handler(socket: WebSocket, request: Any) -> None: - ... + """ """ diff --git a/tests/interceptors/test_interceptors.py b/tests/interceptors/test_interceptors.py index df695a26..336bdccb 100644 --- a/tests/interceptors/test_interceptors.py +++ b/tests/interceptors/test_interceptors.py @@ -9,6 +9,10 @@ from esmerald.testclient import create_client +class ErrorInterceptor(EsmeraldInterceptor): + """""" + + class TestInterceptor(EsmeraldInterceptor): async def intercept(self, scope: "Scope", receive: "Receive", send: "Send") -> None: request = Request(scope=scope, receive=receive, send=send) @@ -51,7 +55,7 @@ async def create(data: Item, name: str) -> JSONResponse: async def cookie_test( data: Item, name: str, cookie: str = Cookie(value="csrftoken") ) -> JSONResponse: - return JSONResponse({"name": name, "cookie": cookie}) + """ """ @post("/logging/{name}") @@ -59,10 +63,22 @@ async def logging_view(data: Item, name: str) -> JSONResponse: return JSONResponse({"name": name}) +@post("/error") +async def error() -> None: + """""" + + def test_issubclassing_EsmeraldInterceptor(test_client_factory): assert issubclass(TestInterceptor, EsmeraldInterceptor) +def test_interceptor_not_implemented(test_client_factory): + with create_client(routes=[Gateway(handler=error)], interceptors=[ErrorInterceptor]) as client: + response = client.get("/error") + + assert response.status_code == 500 + + def test_interceptor_on_application_instance(test_client_factory): data = {"name": "test", "sku": "12345"} diff --git a/tests/middleware/test_basic.py b/tests/middleware/test_basic.py new file mode 100644 index 00000000..fbc7150d --- /dev/null +++ b/tests/middleware/test_basic.py @@ -0,0 +1,15 @@ +from esmerald import Gateway, get +from esmerald.middleware.basic import BasicHTTPMiddleware +from esmerald.testclient import create_client + + +@get("/home") +async def home() -> str: + return "home" + + +def test_basic_middleware(test_client_factory): + with create_client(routes=[Gateway(handler=home)], middleware=[BasicHTTPMiddleware]) as client: + response = client.get("/home") + + assert response.status_code == 200 diff --git a/tests/middleware/test_csrf.py b/tests/middleware/test_csrf.py index 34fab3ea..71348e1f 100644 --- a/tests/middleware/test_csrf.py +++ b/tests/middleware/test_csrf.py @@ -14,27 +14,27 @@ @get(path="/") def get_handler() -> None: - return None + ... @post(path="/") def post_handler() -> None: - return None + ... @put(path="/") def put_handler() -> None: - return None + """ """ @delete(path="/") def delete_handler() -> None: - return None + """ """ @patch(path="/") def patch_handler() -> None: - return None + """ """ def test_csrf_successful_flow() -> None: diff --git a/tests/middleware/test_exception_handler_middleware.py b/tests/middleware/test_exception_handler_middleware.py index 508902bd..fd3e3309 100644 --- a/tests/middleware/test_exception_handler_middleware.py +++ b/tests/middleware/test_exception_handler_middleware.py @@ -10,7 +10,7 @@ async def dummy_app(scope: Any, receive: Any, send: Any) -> None: - return None + """ """ middleware = EsmeraldAPIExceptionMiddleware(dummy_app, False, {}) diff --git a/tests/middleware/test_middleware_handling.py b/tests/middleware/test_middleware_handling.py index f1b9d52b..c1d8efcd 100644 --- a/tests/middleware/test_middleware_handling.py +++ b/tests/middleware/test_middleware_handling.py @@ -1,10 +1,12 @@ import logging -from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, cast +from typing import Any, Awaitable, Callable, List, Type, cast +from _pytest.logging import LogCaptureFixture from pydantic import BaseModel from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.middleware.cors import CORSMiddleware from starlette.middleware.trustedhost import TrustedHostMiddleware +from starlette.types import ASGIApp, Receive, Scope, Send from esmerald.applications import ChildEsmerald from esmerald.config import CORSConfig @@ -18,13 +20,6 @@ from esmerald.routing.views import APIView from esmerald.testclient import create_client -if TYPE_CHECKING: - from typing import Type - - from _pytest.logging import LogCaptureFixture - - from esmerald.types import ASGIApp, Receive, Scope, Send - logger = logging.getLogger(__name__) @@ -41,13 +36,13 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No await self.app(scope, receive, send) -class BaseMiddlewareRequestLoggingMiddleware(BaseHTTPMiddleware): +class BaseMiddlewareRequestLoggingMiddleware(BaseHTTPMiddleware): # pragma: no cover async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: # type: ignore logging.getLogger(__name__).info("%s - %s", request.method, request.url) return await call_next(request) # type: ignore -class MiddlewareWithArgsAndKwargs(BaseHTTPMiddleware): +class MiddlewareWithArgsAndKwargs(BaseHTTPMiddleware): # pragma: no cover def __init__(self, arg: int = 0, *, app: Any, kwarg: str) -> None: super().__init__(app) self.arg = arg @@ -61,7 +56,7 @@ async def dispatch( # type: ignore @get(path="/") def handler() -> None: - ... + """ """ class JSONRequest(BaseModel): @@ -96,9 +91,6 @@ def test_setting_cors_middleware() -> None: unpacked_middleware.extend(cur.app.user_middleware) _ids.append(id(cur.app.user_middleware)) cur = cast("Any", cur.app) # type: ignore - else: - if id(cur.user_middleware) not in _ids: - unpacked_middleware.extend(cur.user_middleware) assert len(unpacked_middleware) == 2 cors_middleware = cast("Any", unpacked_middleware[1]) @@ -121,9 +113,6 @@ def test_trusted_hosts_middleware() -> None: unpacked_middleware.extend(cur.app.user_middleware) _ids.append(id(cur.app.user_middleware)) cur = cast("Any", cur.app) # type: ignore - else: - if id(cur.user_middleware) not in _ids: - unpacked_middleware.extend(cur.user_middleware) assert len(unpacked_middleware) == 1 trusted_hosts_middleware = cast("Any", unpacked_middleware[0]) @@ -167,7 +156,7 @@ class MyController(APIView): middleware=[create_test_middleware(6), create_test_middleware(7)], ) def my_handler(self) -> None: - return None + """ """ with create_client( routes=[ @@ -212,7 +201,7 @@ class MyController(APIView): middleware=[create_test_middleware(6), create_test_middleware(7)], ) def my_handler(self) -> None: - return None + """ """ with create_client( routes=[ @@ -257,7 +246,7 @@ class MyController(APIView): middleware=[create_test_middleware(6), create_test_middleware(7)], ) def my_handler(self) -> None: - return None + """ """ with create_client( routes=[ @@ -310,7 +299,7 @@ class MyController(APIView): middleware=[create_test_middleware(6), create_test_middleware(7)], ) def my_handler(self) -> None: - return None + """ """ with create_client( routes=[ @@ -412,7 +401,7 @@ class MyController(APIView): middleware=[create_test_middleware(6), create_test_middleware(7)], ) def my_handler(self) -> None: - return None + """ """ child_esmerald = ChildEsmerald(routes=[Gateway(path="/", handler=MyController)]) @@ -464,7 +453,7 @@ class MyController(APIView): middleware=[create_test_middleware(6), create_test_middleware(7)], ) def my_handler(self) -> None: - return None + """ """ child_esmerald = ChildEsmerald(routes=[Gateway(path="/", handler=MyController)]) diff --git a/tests/middleware/test_session_middleware.py b/tests/middleware/test_session_middleware.py index 68399f4b..af87e636 100644 --- a/tests/middleware/test_session_middleware.py +++ b/tests/middleware/test_session_middleware.py @@ -1,7 +1,5 @@ import os import re -import secrets -from typing import Dict import pytest from pydantic import ValidationError @@ -36,10 +34,6 @@ def test_config_validation(secret: bytes, should_raise: bool) -> None: SessionConfig(secret_key=Secret(secret)) -def create_session(size: int = 16) -> Dict[str, str]: - return {"key": secrets.token_hex(size)} - - @get(path="/") def view_session(request: Request) -> JSONResponse: return JSONResponse({"session": request.session}) diff --git a/tests/openapi/test_additional_response_classe.py b/tests/openapi/test_additional_response_classe.py index cb13acb5..8e738d9a 100644 --- a/tests/openapi/test_additional_response_classe.py +++ b/tests/openapi/test_additional_response_classe.py @@ -31,7 +31,7 @@ class Item(BaseModel): responses={500: OpenAPIResponse(model=CustomResponse, description="Error")}, ) def read_people() -> Dict[str, str]: - return {"id": "foo"} + """ """ @get( @@ -39,7 +39,7 @@ def read_people() -> Dict[str, str]: responses={422: OpenAPIResponse(model=Error, description="Error")}, ) async def read_item(id: str) -> None: - ... + """ """ def test_open_api_schema(test_client_factory): diff --git a/tests/openapi/test_bad_response.py b/tests/openapi/test_bad_response.py index 21725f2b..87ddc896 100644 --- a/tests/openapi/test_bad_response.py +++ b/tests/openapi/test_bad_response.py @@ -17,15 +17,15 @@ async def test_invalid_response(test_client_factory): with pytest.raises(OpenAPIException) as raised: @get("/test", responses={"hello": {"description": "Not a valid response"}}) - def read_people() -> Dict[str, str]: - return {"id": "foo"} + def read_people() -> Dict[str, str]: # pragma: no cover + ... - with create_client( + with create_client( # pragma: no cover routes=[ Gateway(handler=read_people), ] - ) as client: - client.get("/openapi.json") + ): + """ """ assert raised.value.detail == "An additional response must be an instance of OpenAPIResponse." @@ -34,14 +34,14 @@ async def test_invalid_response_status(test_client_factory): with pytest.raises(OpenAPIException) as raised: @get("/test", responses={"hello": OpenAPIResponse(model=Item)}) - def read_people() -> Dict[str, str]: - return {"id": "foo"} + def read_people() -> Dict[str, str]: # pragma: no cover + ... - with create_client( + with create_client( # pragma: no cover routes=[ Gateway(handler=read_people), ] - ) as client: - client.get("/openapi.json") + ): + """ """ assert raised.value.detail == "The status is not a valid OpenAPI status response." diff --git a/tests/openapi/test_config.py b/tests/openapi/test_config.py new file mode 100644 index 00000000..2d50285e --- /dev/null +++ b/tests/openapi/test_config.py @@ -0,0 +1,7 @@ +from esmerald import Esmerald + + +def test_swagger_ui_html(): + app = Esmerald(routes=[]) + + assert app.settings.openapi_config.swagger_ui_oauth2_redirect_url == "/docs/oauth2-redirect" diff --git a/tests/openapi/test_default_validation_error.py b/tests/openapi/test_default_validation_error.py index 2a167de4..0b00d8c5 100644 --- a/tests/openapi/test_default_validation_error.py +++ b/tests/openapi/test_default_validation_error.py @@ -4,7 +4,7 @@ @get("/item/{id}") async def read_item(id: str) -> None: - ... + """ """ def test_open_api_schema(test_client_factory): diff --git a/tests/openapi/test_docs.py b/tests/openapi/test_docs.py new file mode 100644 index 00000000..9ebe064b --- /dev/null +++ b/tests/openapi/test_docs.py @@ -0,0 +1,61 @@ +from esmerald import Esmerald, Gateway, get +from esmerald.openapi.docs import ( + get_redoc_html, + get_swagger_ui_html, + get_swagger_ui_oauth2_redirect_html, +) +from esmerald.testclient import EsmeraldTestClient + + +@get("/") +async def home() -> None: + """""" + + +app = Esmerald(routes=[Gateway(handler=home)]) +client = EsmeraldTestClient(app) + + +def test_get_swager_ui(test_client_factory): + response = get_swagger_ui_html( + openapi_url=app.openapi_config.openapi_url, + title=app.openapi_config.title, + swagger_js_url=app.openapi_config.swagger_js_url, + swagger_css_url=app.openapi_config.swagger_css_url, + swagger_favicon_url=app.openapi_config.swagger_favicon_url, + oauth2_redirect_url=app.openapi_config.swagger_ui_oauth2_redirect_url, + init_oauth=app.openapi_config.swagger_ui_init_oauth, + swagger_ui_parameters=app.openapi_config.swagger_ui_parameters, + ) + + assert response.status_code == 200 + assert ( + response.body + == b'\n \n \n \n \n \n Esmerald\n \n \n
\n
\n \n \n \n \n \n ' + ) + + +def test_get_redoc_ui(test_client_factory): + response = get_redoc_html( + openapi_url=app.openapi_config.openapi_url, + title=app.openapi_config.title, + redoc_js_url=app.openapi_config.redoc_js_url, + redoc_favicon_url=app.openapi_config.redoc_favicon_url, + with_google_fonts=app.openapi_config.with_google_fonts, + ) + + assert response.status_code == 200 + assert ( + response.body + == b'\n \n \n \n Esmerald\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' + ) + + +def test_get_swagger_ui_oauth2_redirect_html(test_client_factory): + response = get_swagger_ui_oauth2_redirect_html() + + assert response.status_code == 200 + assert ( + response.body + == b'\n \n \n \n Swagger UI: OAuth2 Redirect\n \n \n \n \n \n ' + ) diff --git a/tests/openapi/test_external_app.py b/tests/openapi/test_external_app.py index b3064b9d..ca946cf8 100644 --- a/tests/openapi/test_external_app.py +++ b/tests/openapi/test_external_app.py @@ -13,7 +13,7 @@ @flask_app.route("/") -def flask_main(): +def flask_main(): # pragma: no cover name = request.args.get("name", "Esmerald") return f"Hello, {escape(name)} from Flask!" @@ -24,7 +24,7 @@ class Item(BaseModel): @get() def read_people() -> Dict[str, str]: - return {"id": "foo"} + """ """ @get( @@ -33,7 +33,7 @@ def read_people() -> Dict[str, str]: responses={200: OpenAPIResponse(model=Item, description="The SKU information of an item")}, ) async def read_item() -> JSON: - return JSON(content={"id": 1}) + """ """ def test_external_app_not_include_in_schema(test_client_factory): diff --git a/tests/openapi/test_include.py b/tests/openapi/test_include.py index 03f7caa8..677e040f 100644 --- a/tests/openapi/test_include.py +++ b/tests/openapi/test_include.py @@ -13,7 +13,7 @@ class Item(BaseModel): @get() def read_people() -> Dict[str, str]: - return {"id": "foo"} + """ """ @get( @@ -22,7 +22,7 @@ def read_people() -> Dict[str, str]: responses={200: OpenAPIResponse(model=Item, description="The SKU information of an item")}, ) async def read_item() -> JSON: - return JSON(content={"id": 1}) + """ """ def test_add_include_to_openapi(test_client_factory): diff --git a/tests/openapi/test_internal_response.py b/tests/openapi/test_internal_response.py new file mode 100644 index 00000000..3f63513f --- /dev/null +++ b/tests/openapi/test_internal_response.py @@ -0,0 +1,7 @@ +from esmerald.openapi._internal import InternalResponse + + +def test_repr_internal_response(): + internal_response = InternalResponse(media_type="application/json", return_annotation=str) + + assert repr(internal_response) == "InternalResponse(annotation=application/json, default=None)" diff --git a/tests/openapi/test_list_openapi_responses.py b/tests/openapi/test_list_openapi_responses.py index f06f4fee..7dfb12e3 100644 --- a/tests/openapi/test_list_openapi_responses.py +++ b/tests/openapi/test_list_openapi_responses.py @@ -34,7 +34,7 @@ class JsonResponse(JSONResponse): responses={500: OpenAPIResponse(model=CustomResponse, description="Error")}, ) def read_people() -> Dict[str, str]: - return {"id": "foo"} + """ """ @get( @@ -45,7 +45,7 @@ def read_people() -> Dict[str, str]: }, ) async def read_item(id: str) -> None: - ... + """ """ def test_open_api_schema(test_client_factory): diff --git a/tests/openapi/test_raise_value_error.py b/tests/openapi/test_raise_value_error.py index d266ed9f..845d2285 100644 --- a/tests/openapi/test_raise_value_error.py +++ b/tests/openapi/test_raise_value_error.py @@ -34,7 +34,7 @@ def test_openapi_response_value_error_for_type(test_client_factory): "/item/{id}", responses={422: OpenAPIResponse(model={"hello", Error}, description="Error")}, ) - async def read_item(id: str) -> None: + async def read_item(id: str) -> None: # pragma: no cover ... @@ -49,7 +49,7 @@ def test_openapi_response_value_for_class(test_client_factory, model): "/item/{id}", responses={422: OpenAPIResponse(model=model, description="Error")}, ) - async def read_item(id: str) -> None: + async def read_item(id: str) -> None: # pragma: no cover ... @@ -64,7 +64,7 @@ def test_openapi_response_value_for_class_as_list(test_client_factory, model): "/item/{id}", responses={422: OpenAPIResponse(model=[model], description="Error")}, ) - async def read_item(id: str) -> None: + async def read_item(id: str) -> None: # pragma: no cover ... @@ -75,7 +75,7 @@ def test_openapi_response_value_for_class_as_list_multiple_models(test_client_fa "/item/{id}", responses={422: OpenAPIResponse(model=[Error, DummyErrorModel], description="Error")}, ) - async def read_item(id: str) -> None: + async def read_item(id: str) -> None: # pragma: no cover ... @@ -88,5 +88,5 @@ def test_openapi_response_value_for_class_as_list_multiple(test_client_factory): 422: OpenAPIResponse(model=[DummyErrorDataclass, DummyError], description="Error") }, ) - async def read_item(id: str) -> None: + async def read_item(id: str) -> None: # pragma: no cover ... diff --git a/tests/openapi/test_response_validation_error.py b/tests/openapi/test_response_validation_error.py index c2d3f725..22839c48 100644 --- a/tests/openapi/test_response_validation_error.py +++ b/tests/openapi/test_response_validation_error.py @@ -28,7 +28,7 @@ class Item(BaseModel): @get() def read_people() -> Dict[str, str]: - return {"id": "foo"} + """ """ @get( @@ -37,7 +37,7 @@ def read_people() -> Dict[str, str]: responses={422: OpenAPIResponse(model=CustomResponse, description="Error")}, ) async def read_item(id: str) -> None: - ... + """ """ def test_open_api_schema(test_client_factory): diff --git a/tests/openapi/test_responses_child_esmerald.py b/tests/openapi/test_responses_child_esmerald.py index d5afd480..d4a5664b 100644 --- a/tests/openapi/test_responses_child_esmerald.py +++ b/tests/openapi/test_responses_child_esmerald.py @@ -13,7 +13,7 @@ class Item(BaseModel): @get() def read_people() -> Dict[str, str]: - return {"id": "foo"} + """ """ @get( @@ -22,7 +22,7 @@ def read_people() -> Dict[str, str]: responses={200: OpenAPIResponse(model=Item, description="The SKU information of an item")}, ) async def read_item() -> JSON: - return JSON(content={"id": 1}) + """ """ def test_add_child_esmerald_to_openapi(test_client_factory): diff --git a/tests/openapi/test_responses_child_esmerald_nested.py b/tests/openapi/test_responses_child_esmerald_nested.py index a0fafb16..9550d2ba 100644 --- a/tests/openapi/test_responses_child_esmerald_nested.py +++ b/tests/openapi/test_responses_child_esmerald_nested.py @@ -13,7 +13,7 @@ class Item(BaseModel): @get() def read_people() -> Dict[str, str]: - return {"id": "foo"} + """ """ @get( @@ -22,7 +22,7 @@ def read_people() -> Dict[str, str]: responses={200: OpenAPIResponse(model=Item, description="The SKU information of an item")}, ) async def read_item() -> JSON: - return JSON(content={"id": 1}) + """ """ def test_child_nested_esmerald_disabled_openapi(): diff --git a/tests/openapi/test_utils.py b/tests/openapi/test_utils.py new file mode 100644 index 00000000..b6443784 --- /dev/null +++ b/tests/openapi/test_utils.py @@ -0,0 +1,47 @@ +import pytest + +from esmerald import status +from esmerald.openapi.utils import is_status_code_allowed + +status_codes_allowed = [ + getattr(status, value) + for value in dir(status) + if value.startswith("HTTP") + and not (getattr(status, value) < 200 or getattr(status, value) in {204, 304}) +] + +status_codes_not_allowed = [ + getattr(status, value) + for value in dir(status) + if value.startswith("HTTP") + and (getattr(status, value) < 200 or getattr(status, value) in {204, 304}) +] + + +def test_is_status_code_allowed(): + assert is_status_code_allowed(None) + + +@pytest.mark.parametrize( + "code", + [ + "default", + "1XX", + "2XX", + "3XX", + "4XX", + "5XX", + ], +) +def test_is_status_code_allowed_str(code): + assert is_status_code_allowed(code) + + +@pytest.mark.parametrize("code", status_codes_allowed) +def test_test_is_status_code_allowed_allowed_code(code): + assert is_status_code_allowed(str(code)) + + +@pytest.mark.parametrize("code", status_codes_not_allowed) +def test_test_is_status_code_not_allowed(code): + assert not is_status_code_allowed(str(code)) diff --git a/tests/pluggables/test_pluggables.py b/tests/pluggables/test_pluggables.py index dd2c90d8..9dffda05 100644 --- a/tests/pluggables/test_pluggables.py +++ b/tests/pluggables/test_pluggables.py @@ -13,7 +13,7 @@ class MyNewPluggable: ... -class PluggableNoPlug(Extension): +class PluggableNoPlug(Extension): # pragma: no cover def __init__(self, app: "Esmerald"): super().__init__(app) self.app = app diff --git a/tests/requests/test_base_http_starlette.py b/tests/requests/test_base_http_starlette.py index dcf36e77..60236d4d 100644 --- a/tests/requests/test_base_http_starlette.py +++ b/tests/requests/test_base_http_starlette.py @@ -3,12 +3,13 @@ https://github.com/encode/starlette/blob/master/tests/test_requests.py. """ -from typing import TYPE_CHECKING, Any, Optional +from typing import Any, Optional import anyio import pytest from starlette.datastructures import Address, State from starlette.status import HTTP_200_OK +from starlette.types import Receive, Send from esmerald.enums import MediaType from esmerald.exceptions import InternalServerError @@ -16,9 +17,6 @@ from esmerald.responses import JSONResponse, PlainTextResponse, Response from esmerald.testclient import EsmeraldTestClient -if TYPE_CHECKING: - from starlette.types import Receive, Send - def test_request_url(test_client_factory) -> None: async def app(scope: Any, receive: "Receive", send: "Send") -> None: @@ -254,7 +252,7 @@ async def app(scope: Any, receive: "Receive", send: "Send") -> None: assert response.json() == {"json": "Receive channel not available"} -async def test_request_disconnect() -> None: +async def test_request_disconnect() -> None: # pragma: no cover """If a client disconnect occurs while reading request body then InternalServerError should be raised.""" diff --git a/tests/requests/test_base_websocket_starlette.py b/tests/requests/test_base_websocket_starlette.py index 63eb5615..52730465 100644 --- a/tests/requests/test_base_websocket_starlette.py +++ b/tests/requests/test_base_websocket_starlette.py @@ -61,7 +61,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: any(module in sys.modules for module in ("brotli", "brotlicffi")), reason='urllib3 includes "br" to the "accept-encoding" headers.', ) -def test_websocket_headers(test_client_factory): +def test_websocket_headers(test_client_factory): # pragma: no cover async def app(scope: Scope, receive: Receive, send: Send) -> None: websocket = WebSocket(scope, receive=receive, send=send) headers = dict(websocket.headers) diff --git a/tests/requests/test_request.py b/tests/requests/test_request.py index 620c3e8c..0f73bfae 100644 --- a/tests/requests/test_request.py +++ b/tests/requests/test_request.py @@ -35,7 +35,7 @@ async def test_request_valid_body_to_json() -> None: def test_request_resolve_url() -> None: @get(path="/proxy") def proxy() -> None: - pass + """ """ @get(path="/test") def root(request: Request) -> dict: diff --git a/tests/requests/test_websocket.py b/tests/requests/test_websocket.py index 83a97666..2c22b9fa 100644 --- a/tests/requests/test_websocket.py +++ b/tests/requests/test_websocket.py @@ -1,16 +1,14 @@ -from typing import TYPE_CHECKING, Any +from typing import Any import pytest from starlette.datastructures import Headers +from typing_extensions import Literal from esmerald.routing.gateways import WebSocketGateway from esmerald.routing.handlers import websocket from esmerald.testclient import create_client from esmerald.websockets import WebSocket -if TYPE_CHECKING: - from typing_extensions import Literal - @pytest.mark.parametrize("mode", ["text", "binary"]) def test_websocket_send_receive_json(mode: "Literal['text', 'binary']") -> None: diff --git a/tests/routing/test_include_errors.py b/tests/routing/test_include_errors.py new file mode 100644 index 00000000..759adf9a --- /dev/null +++ b/tests/routing/test_include_errors.py @@ -0,0 +1,42 @@ +import pytest + +from esmerald import Gateway, ImproperlyConfigured, Include, get + + +@get() +async def home() -> None: + """""" + + +gateway = Gateway(handler=home) + + +route_patterns = [Gateway(handler=home)] + + +def test_raise_error_namespace_and_routes(): + with pytest.raises(ImproperlyConfigured): + Include(namespace="test", routes=[gateway]) + + +@pytest.mark.parametrize("arg", [gateway, 2, get]) +def test_raise_error_namespace(arg): + with pytest.raises(ImproperlyConfigured): + Include(namespace=arg) + + +@pytest.mark.parametrize("arg", [gateway, 2, get]) +def test_raise_error_pattern(arg): + with pytest.raises(ImproperlyConfigured): + Include(pattern=arg) + + +def test_raise_error_pattern_and_routes(): + with pytest.raises(ImproperlyConfigured): + Include(pattern="test", routes=[gateway]) + + +def test_namespace_include_routes(): + include = Include(namespace="tests.routing.test_include_errors") + + assert len(include.routes) == 1 diff --git a/tests/routing/test_routing.py b/tests/routing/test_routing.py index 5afc9ab1..8cbb0c68 100644 --- a/tests/routing/test_routing.py +++ b/tests/routing/test_routing.py @@ -1,5 +1,4 @@ import contextlib -import typing import uuid import pytest @@ -23,7 +22,7 @@ @get(path="/", permissions=[DenyAll]) async def deny_access(request: Request) -> JSONResponse: - return JSONResponse("Hello, world") + """ """ @get(path="/", permissions=[AllowAny]) @@ -65,18 +64,6 @@ async def user_no_match(request: Request) -> StarletteResponse: # pragma: no co return StarletteResponse(content) -@get(path="/", status_code=200) -async def partial_endpoint(arg: typing.Any, request: Request) -> JSONResponse: - return JSONResponse({"arg": arg}) - - -@get(path="/") -async def partial_ws_endpoint(websocket: WebSocket) -> None: - await websocket.accept() - await websocket.send_json({"url": str(websocket.url)}) - await websocket.close() - - @get(path="/", media_type=MediaType.TEXT, status_code=200) async def func_homepage(request: Request) -> StarletteResponse: return StarletteResponse("Hello, world!") @@ -590,7 +577,7 @@ def test_protocol_switch(test_client_factory): @get(path="/") async def get_ok() -> PlainTextResponse: - return PlainTextResponse("OK") + return PlainTextResponse("OK") # pragma: no cover def test_include_urls(test_client_factory): @@ -974,7 +961,7 @@ def test_duplicated_param_names(): def test_exception_on_mounted_apps(test_app_client_factory): class CustomException(Exception): - ... + """ """ @get(path="/") def exc(request: Request) -> CustomException: diff --git a/tests/schedulers/asyncz/test_scheduler.py b/tests/schedulers/asyncz/test_scheduler.py index fd2b0ab0..0d1f8ec6 100644 --- a/tests/schedulers/asyncz/test_scheduler.py +++ b/tests/schedulers/asyncz/test_scheduler.py @@ -1,5 +1,6 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Union +from unittest.mock import MagicMock import pytest from asyncz.contrib.esmerald.decorator import scheduler @@ -11,13 +12,12 @@ from asyncz.triggers import IntervalTrigger from asyncz.triggers.base import BaseTrigger from loguru import logger -from mock import MagicMock from esmerald import Esmerald from esmerald.exceptions import ImproperlyConfigured -class DummyScheduler(BaseScheduler): +class DummyScheduler(BaseScheduler): # pragma: no cover def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.wakeup = MagicMock() @@ -29,7 +29,7 @@ def wakeup(self): ... -class DummyTrigger(BaseTrigger): +class DummyTrigger(BaseTrigger): # pragma: no cover def __init__(self, **args): super().__init__(**args) self.args = args @@ -40,7 +40,7 @@ def get_next_trigger_time( ... -class DummyExecutor(BaseExecutor): +class DummyExecutor(BaseExecutor): # pragma: no cover def __init__(self, **args): super().__init__(**args) self.args = args @@ -52,7 +52,7 @@ def do_send_task(self, task: "TaskType", run_times: List[datetime]) -> Any: return super().do_send_task(task, run_times) -class DummyStore(BaseStore): +class DummyStore(BaseStore): # pragma: no cover def __init__(self, **args): super().__init__(**args) self.args = args @@ -92,14 +92,14 @@ def scheduler_tasks() -> Dict[str, str]: @scheduler(name="task1", trigger=IntervalTrigger(seconds=1), max_intances=3, is_enabled=True) -def task_one(): +def task_one(): # pragma: no cover value = 3 logger.info(value) return 3 @scheduler(name="task2", trigger=IntervalTrigger(seconds=3), max_intances=3, is_enabled=True) -def task_two(): +def task_two(): # pragma: no cover value = 8 logger.info(value) return 8 diff --git a/tests/security/__init__.py b/tests/security/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/security/test_jwt_token.py b/tests/security/test_jwt_token.py new file mode 100644 index 00000000..c1f7deca --- /dev/null +++ b/tests/security/test_jwt_token.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from freezegun import freeze_time + +from esmerald.security.jwt.token import Token + + +@freeze_time("2022-03-03") +def test_token_expiry(): + date = datetime.now() + + token = Token(exp=date) + + assert token.exp == date diff --git a/tests/settings.py b/tests/settings.py index a00548c6..c7cf6c70 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -27,4 +27,4 @@ class TestConfig(TestSettings): @property def scheduler_class(self) -> None: - ... + """""" diff --git a/tests/test_allow_origins.py b/tests/test_allow_origins.py index 2f0eaeb9..c20d60ca 100644 --- a/tests/test_allow_origins.py +++ b/tests/test_allow_origins.py @@ -10,7 +10,7 @@ def test_raise_error_on_allow_origins(test_client_factory): with pytest.raises(ImproperlyConfigured): with create_client([], allow_origins=["*"], cors_config=cors_config): - ... + """ """ def test_raise_error_on_allow_origins_esmerald_object(test_client_factory): diff --git a/tests/test_apiviews.py b/tests/test_apiviews.py index 9c1aa1e3..df4b8332 100644 --- a/tests/test_apiviews.py +++ b/tests/test_apiviews.py @@ -286,7 +286,7 @@ class MyAPIView(APIView): @get(path="/") def get_person(self) -> Individual: - ... + """ """ @websocket(path="/socket") async def ws(self, socket: WebSocket) -> None: @@ -310,7 +310,7 @@ class MyAPIView(APIView): @get(path="/") def get_person(self) -> Individual: - ... + """ """ @websocket(path="/socket") async def ws(self, socket: WebSocket) -> None: @@ -336,7 +336,7 @@ class MyAPIView(APIView): @get(path="/") def get_person(self) -> Individual: - ... + """ """ @websocket(path="/socket") async def ws(self, socket: WebSocket) -> None: @@ -367,7 +367,7 @@ class MyAPIView(APIView): @get(path="/") def get_person(self) -> Individual: - ... + """ """ @websocket(path="/socket") async def ws(self, socket: WebSocket) -> None: diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 1d76365d..80dadf9b 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -74,7 +74,7 @@ async def create_user_with_param( def test_cookies_param_field(test_client_factory): user = {"name": "Esmerald", "email": "test@esmerald.com"} - with create_client(routes=[Gateway(handler=create_user)]) as client: + with create_client(routes=[Gateway(handler=create_user_with_param)]) as client: response = client.post("/create", json=user, cookies={"csrftoken": "my-cookie"}) assert response.status_code == 201 @@ -83,7 +83,7 @@ def test_cookies_param_field(test_client_factory): def test_param_cookie_missing_field(test_client_factory): user = {"name": "Esmerald", "email": "test@esmerald.com"} - with create_client(routes=[Gateway(handler=create_user)]) as client: + with create_client(routes=[Gateway(handler=create_user_with_param)]) as client: response = client.post("/create", json=user, cookies={"csrftoke": "my-cookie"}) assert response.status_code == 400 diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 00000000..a5918265 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass +from typing import Any, Dict + +from pydantic import BaseModel +from pydantic.dataclasses import dataclass as pydantic_dataclass + +from esmerald import Form, Gateway, post +from esmerald.testclient import create_client + + +@pydantic_dataclass +class User: + id: int + name: str + + +@dataclass +class UserOut: + id: int + name: str + + +class UserModel(BaseModel): + id: int + name: str + + +@post("/form") +async def test_form(data: Any = Form()) -> Dict[str, str]: + return {"name": data["name"]} + + +@post("/complex-form-pydantic") +async def test_complex_form_pydantic_dataclass(data: User = Form()) -> User: + return data + + +@post("/complex-form-dataclass") +async def test_complex_form_dataclass(data: UserOut = Form()) -> UserOut: + return data + + +@post("/complex-form-basemodel") +async def test_complex_form_basemodel(data: UserModel = Form()) -> UserModel: + return data + + +def test_send_form(test_client_factory): + data = {"name": "Test"} + + with create_client(routes=[Gateway(handler=test_form)]) as client: + response = client.post("/form", data=data) + + assert response.status_code == 201 + assert response.json() == {"name": "Test"} + + +def test_send_complex_form_pydantic_dataclass(test_client_factory): + data = {"id": 1, "name": "Test"} + with create_client( + routes=[Gateway(handler=test_complex_form_pydantic_dataclass)], + enable_openapi=True, + ) as client: + response = client.post("/complex-form-pydantic", data=data) + assert response.status_code == 201, response.text + assert response.json() == {"id": 1, "name": "Test"} + + +def test_send_complex_form_normal_dataclass(test_client_factory): + data = {"id": 1, "name": "Test"} + with create_client( + routes=[Gateway(handler=test_complex_form_dataclass)], + enable_openapi=True, + ) as client: + response = client.post("/complex-form-dataclass", data=data) + assert response.status_code == 201, response.text + assert response.json() == {"id": 1, "name": "Test"} + + +def test_send_complex_form_base_model(test_client_factory): + data = {"id": 1, "name": "Test"} + with create_client( + routes=[Gateway(handler=test_complex_form_basemodel)], + enable_openapi=True, + ) as client: + response = client.post("/complex-form-basemodel", data=data) + assert response.status_code == 201, response.text + assert response.json() == {"id": 1, "name": "Test"} diff --git a/tests/test_headers.py b/tests/test_headers.py index 72f7f672..0e65f61f 100644 --- a/tests/test_headers.py +++ b/tests/test_headers.py @@ -74,7 +74,7 @@ async def create_user_with_param( def test_headers_param_field(test_client_factory): user = {"name": "Esmerald", "email": "test@esmerald.com"} - with create_client(routes=[Gateway(handler=create_user)]) as client: + with create_client(routes=[Gateway(handler=create_user_with_param)]) as client: response = client.post("/create", json=user, headers={"X-API-TOKEN": "my-token"}) assert response.status_code == 201 @@ -83,7 +83,7 @@ def test_headers_param_field(test_client_factory): def test_param_header_missing_field(test_client_factory): user = {"name": "Esmerald", "email": "test@esmerald.com"} - with create_client(routes=[Gateway(handler=create_user)]) as client: + with create_client(routes=[Gateway(handler=create_user_with_param)]) as client: response = client.post("/create", json=user, headers={"X-API-TOKE": "my-token"}) assert response.status_code == 400 diff --git a/tests/test_injects.py b/tests/test_injects.py index 16cbaff2..5da70046 100644 --- a/tests/test_injects.py +++ b/tests/test_injects.py @@ -130,7 +130,7 @@ def test(value: int = Injects()) -> Dict[str, int]: def test_dependency_not_Injected_and_no_default() -> None: @get() def test(value: int = Injects()) -> Dict[str, int]: - return {"value": value} + """ """ with pytest.raises(ImproperlyConfigured): Esmerald(routes=[Gateway(handler=test)]) @@ -153,7 +153,7 @@ def test(self, value: int = Injects()) -> Dict[str, int]: def test_dependency_skip_validation() -> None: @get("/validated") def validated(value: int = Injects()) -> Dict[str, int]: - return {"value": value} + """ """ @get("/skipped") def skipped(value: int = Injects(skip_validation=True)) -> Dict[str, int]: @@ -168,6 +168,7 @@ def skipped(value: int = Injects(skip_validation=True)) -> Dict[str, int]: ) as client: validated_resp = client.get("/validated") assert validated_resp.status_code == HTTP_500_INTERNAL_SERVER_ERROR + skipped_resp = client.get("/skipped") assert skipped_resp.status_code == HTTP_200_OK assert skipped_resp.json() == {"value": "str"} diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..c32e0b31 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,42 @@ +import logging +import sys +from typing import Any, Optional + +from loguru import logger + +from esmerald import Esmerald, EsmeraldAPISettings, Gateway, get +from esmerald.conf.enums import EnvironmentType +from esmerald.logging import InterceptHandler +from esmerald.testclient import EsmeraldTestClient + + +class DevelopmentAppSettings(EsmeraldAPISettings): + debug: bool = True + app_name: str = "My application in development mode." + title: str = "My linezap" + environment: Optional[str] = EnvironmentType.DEVELOPMENT + + def __init__(self, *args: Any, **kwds: Any) -> Any: + super().__init__(*args, **kwds) + logging_level = logging.DEBUG if self.debug else logging.INFO + loggers = ("esmerald",) + logging.getLogger().handlers = [InterceptHandler()] + for logger_name in loggers: + logging_logger = logging.getLogger(logger_name) + logging_logger.handlers = [InterceptHandler(level=logging_level)] + + logger.configure(handlers=[{"sink": sys.stderr, "level": logging_level}]) + + +def test_logger(): + @get() + def home() -> None: + """""" + + app = Esmerald(routes=[Gateway(handler=home)], settings_config=DevelopmentAppSettings) + + client = EsmeraldTestClient(app) + + response = client.get("/") + + assert response.status_code == 200 diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 9914fd01..695f80ca 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -14,7 +14,7 @@ from esmerald.websockets import WebSocket if TYPE_CHECKING: - from esmerald.types import APIGateHandler + from esmerald.types import APIGateHandler # pragma: no cover class LocalPermission(BasePermission): @@ -90,7 +90,7 @@ async def my_websocket_route_handler(socket: WebSocket) -> None: client = create_client(routes=WebSocketGateway(handler=my_websocket_route_handler)) with pytest.raises(WebSocketDisconnect), client.websocket_connect("/") as ws: - ws.send_json({"data": "123"}) + ws.send_json({"data": "123"}) # pragma: no cover with client.websocket_connect("/", headers={"allow_all": "true"}) as ws: ws.send_json({"data": "123"}) @@ -147,7 +147,7 @@ async def my_asgi_handler() -> None: def test_permissions_with_child_esmerald_three() -> None: @route(methods=["GET"], path="/secret") async def my_asgi_handler() -> None: - ... + """ """ child = ChildEsmerald(routes=[Gateway(handler=my_asgi_handler)], permissions=[AllowAny]) diff --git a/tests/test_repr_params.py b/tests/test_repr_params.py new file mode 100644 index 00000000..5cff961d --- /dev/null +++ b/tests/test_repr_params.py @@ -0,0 +1,16 @@ +import pytest + +from esmerald import Body, Cookie, File, Form, Param, Path, Query + + +def test_reprs_body(): + body = Body(annotation=str, default=None) + + assert repr(body) == "Body(annotation=, default=None)" + + +@pytest.mark.parametrize("obj", [Body, Cookie, Form, Param, Path, Query, File]) +def test_reprs_param(obj): + param = obj(annotation=str, default=None) + + assert repr(param) == f"{obj.__name__}(annotation=, default=None)" diff --git a/tests/test_router.py b/tests/test_router.py index 9cd1aafc..635a49e7 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -1,11 +1,14 @@ +from unittest import mock + +import pytest from starlette import status from esmerald import ChildEsmerald +from esmerald.exceptions import ImproperlyConfigured from esmerald.routing.gateways import Gateway -from esmerald.routing.handlers import get, websocket +from esmerald.routing.handlers import get from esmerald.routing.router import Include, Router from esmerald.testclient import create_client -from esmerald.websockets import WebSocket @get(status_code=status.HTTP_202_ACCEPTED) @@ -18,37 +21,39 @@ def route_two() -> dict: return {"test": 2} -@get(status_code=status.HTTP_200_OK) -def route_three() -> dict: - return {"test": 3} - - -@websocket(path="/") -async def simple_websocket_handler(socket: WebSocket) -> None: - await socket.accept() - data = await socket.receive_json() +def test_add_router(test_client_factory) -> None: + """ + Adds a route to the router. + """ - assert data - await socket.send_json({"data": "esmerald"}) - await socket.close() + with create_client(routes=[Gateway(handler=route_one)]) as client: + response = client.get("/") + assert response.json() == {"test": 1} + assert response.status_code == status.HTTP_202_ACCEPTED + router = Router(path="/aditional", routes=[Gateway("/second", handler=route_two)]) + client.app.add_router(router=router) -@websocket(path="/websocket") -async def simple_websocket_handler_two(socket: WebSocket) -> None: - await socket.accept() - data = await socket.receive_json() + response = client.get("/aditional/second") - assert data - await socket.send_json({"data": "esmerald"}) - await socket.close() + assert response.json() == {"test": 2} + assert response.status_code == status.HTTP_206_PARTIAL_CONTENT -def test_add_router(test_client_factory) -> None: +def test_add_router_events(test_client_factory) -> None: """ - Adds a route to the router. + Adds a route to the router and events """ - with create_client(routes=[Gateway(handler=route_one)]) as client: + def start(): + ... + + def stop(): + ... + + with create_client( + routes=[Gateway(handler=route_one)], on_startup=[start], on_shutdown=[stop] + ) as client: response = client.get("/") assert response.json() == {"test": 1} assert response.status_code == status.HTTP_202_ACCEPTED @@ -61,6 +66,9 @@ def test_add_router(test_client_factory) -> None: assert response.json() == {"test": 2} assert response.status_code == status.HTTP_206_PARTIAL_CONTENT + assert start in client.app.on_startup + assert stop in client.app.on_shutdown + def test_add_router_with_includes(test_client_factory) -> None: """ @@ -208,3 +216,38 @@ def test_add_child_esmerald(test_client_factory): assert response.status_code == 202 assert response.json() == {"test": 1} + + +def test_add_child_esmerald_raise_error(test_client_factory): + child = object() + + with pytest.raises(ImproperlyConfigured): + with create_client(routes=[]) as client: + client.app.add_child_esmerald(path="/child", child=child) + + +def test_add_include(test_client_factory): + include = Include(path="/child", routes=[Gateway(handler=route_one)]) + + with create_client(routes=[]) as client: + client.app.add_include(include) + + response = client.get("/child") + + assert response.status_code == 202 + assert response.json() == {"test": 1} + + +def test_add_include_call_activate_openapi(test_client_factory): + include = Include(path="/child", routes=[Gateway(handler=route_one)]) + + with create_client(routes=[]) as client: + with mock.patch("esmerald.applications.Esmerald.activate_openapi") as mock_call: + client.app.add_include(include) + + response = client.get("/child") + + assert response.status_code == 202 + assert response.json() == {"test": 1} + + mock_call.assert_called_once() diff --git a/tests/test_templates.py b/tests/test_templates.py index a6b7e205..5151b81f 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -61,3 +61,27 @@ async def homepage(request: Request) -> Template: client = EsmeraldTestClient(app) response = client.get("/") assert response.text == "Hello, world" + + +def test_alternative_template(template_dir, test_client_factory): + path = os.path.join(template_dir, "index.html") + with open(path, "w") as file: + file.write("Hello, world") + + @get() + async def homepage(request: Request) -> Template: + return Template( + name="indx.html", context={"request": request}, alternative_template="index.html" + ) + + app = Esmerald( + debug=True, + routes=[Gateway("/", handler=homepage)], + template_config=TemplateConfig( + directory=template_dir, + engine=JinjaTemplateEngine, + ), + ) + client = EsmeraldTestClient(app) + response = client.get("/") + assert response.text == "Hello, world" diff --git a/tests/test_urls_include.py b/tests/test_urls_include.py new file mode 100644 index 00000000..17c599d0 --- /dev/null +++ b/tests/test_urls_include.py @@ -0,0 +1,45 @@ +import pytest + +from esmerald import Gateway, ImproperlyConfigured, get +from esmerald.core.urls.base import include + + +@get("/home") +async def home() -> None: + """""" + + +route_patterns = [Gateway(handler=home)] + + +def test_default_include_router_patterns(): + include_routes = include("tests.test_urls_include") + + assert len(include_routes) == 1 + + +my_urls = [Gateway(handler=home)] + + +def test_pattern_include(): + include_routes = include("tests.test_urls_include", pattern="my_urls") + + assert len(include_routes) == 1 + + +def test_raises_improperly_configured_for_arg(): + with pytest.raises(ImproperlyConfigured): + include(1, pattern="my_urls") + + +def test_raises_improperly_configured_for_patterns(): + with pytest.raises(ImproperlyConfigured): + include("tests.test_urls_include", pattern="my_url_routes") + + +my_url_routes_tuple = Gateway(handler=home) + + +def test_raises_improperly_configured_for_patterns_not_list(): + with pytest.raises(ImproperlyConfigured): + include("tests.test_urls_include", pattern="my_url_routes_tuple") diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..b62fcc8a --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,37 @@ +from datetime import datetime + +import pytz +from freezegun import freeze_time + +from esmerald.parsers import flatten +from esmerald.security.utils import convert_time + + +@freeze_time("2023-01-01 18:05:20") +def test_convert_time(): + now = datetime.now() + + time = convert_time(now) + + assert time is not None + assert isinstance(time, datetime) + assert time.tzinfo is None + + +@freeze_time("2023-01-01 18:05:20") +def test_convert_time_tz_info(): + now = datetime.now(tz=pytz.timezone("Europe/Rome")) + + time = convert_time(now) + + assert time is not None + assert isinstance(time, datetime) + assert str(time.tzinfo) == "Europe/Rome" + + +def test_flatten(): + data = [1, [2], [3, [4, [5]], [[[[[6]]]]]]] + + flatten_data = flatten(data) + + assert flatten_data == [1, 2, 3, 4, 5, 6] diff --git a/tests/transformers/__init__.py b/tests/transformers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/transformers/test_utils.py b/tests/transformers/test_utils.py new file mode 100644 index 00000000..9f489642 --- /dev/null +++ b/tests/transformers/test_utils.py @@ -0,0 +1,56 @@ +import pytest +from pydantic import BaseModel + +from esmerald import File, ImproperlyConfigured, Param, ValidationErrorException, get +from esmerald.enums import ParamType +from esmerald.transformers.model import ParamSetting +from esmerald.transformers.utils import get_request_params, get_signature + + +def test_get_signature_improperly_configured(): + class Test(BaseModel): + """""" + + test = Test() + with pytest.raises(ImproperlyConfigured): + get_signature(test) + + +def test_signature_model_is_none(): + @get() + async def test() -> None: + """""" + + handler = get_signature(test) + + assert handler is None + + +def test_get_request_params_raise_validation_error_exception(): # pragma: no cover + param = Param(default=None) + expected_param = File(default=None) + param_setting = ParamSetting( + default_value=None, + field_alias="param", + field_name="param", + is_required=True, + param_type=ParamType.PATH, + field_info=param, + ) + + expected_param_setting = ParamSetting( + default_value=None, + field_alias="test", + field_name="test", + is_required=True, + param_type=ParamType.PATH, + field_info=expected_param, + ) + + set_params = {param_setting} + expected_params = {expected_param_setting} + + with pytest.raises(ValidationErrorException): + get_request_params( + params=set_params, expected=expected_params, url="http://testserver.com" + ) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/test_module_loading.py b/tests/utils/test_module_loading.py new file mode 100644 index 00000000..5dee286d --- /dev/null +++ b/tests/utils/test_module_loading.py @@ -0,0 +1,26 @@ +import pytest + +from esmerald import EsmeraldAPISettings +from esmerald.utils.module_loading import import_string + + +def test_import_error_module_loading(): + path = "tests" + + with pytest.raises(ImportError): + import_string(path) + + +def test_attribute_error_module_loading(): + path = "tests.settings.TestSetting" + + with pytest.raises(ImportError): + import_string(path) + + +def test_imports_successfully(): + path = "tests.settings.TestSettings" + + settings = import_string(path) + + assert issubclass(settings, EsmeraldAPISettings) diff --git a/tests/utils/test_pydantic_schema.py b/tests/utils/test_pydantic_schema.py new file mode 100644 index 00000000..9569e2a7 --- /dev/null +++ b/tests/utils/test_pydantic_schema.py @@ -0,0 +1,19 @@ +from typing import Any + +import pytest + +from esmerald.params import Param +from esmerald.utils.pydantic.schema import is_any_type, is_field_optional + + +def test_is_any_type(): + field = Param(annotation=Any) + + assert is_any_type(field) + + +@pytest.mark.parametrize("allow_none,return_type", [(True, False), (False, False)]) +def test_is_field_optional(allow_none, return_type): + field = Param(allow_none=allow_none) + + assert is_field_optional(field) == return_type diff --git a/tests/utils/test_sync.py b/tests/utils/test_sync.py new file mode 100644 index 00000000..2daa6a9d --- /dev/null +++ b/tests/utils/test_sync.py @@ -0,0 +1,28 @@ +from esmerald.utils.helpers import is_async_callable +from esmerald.utils.sync import AsyncCallable, execsync + + +async def process() -> None: + """async function""" + + +def another_process() -> None: + """sync function""" + + +def test_async_callable_return_async(): + async_callable = AsyncCallable(process) + + assert is_async_callable(async_callable.fn) + + +def test_async_callable_transform(): + async_callable = AsyncCallable(another_process) + + assert is_async_callable(async_callable.fn) + + +def test_execsync(): + wrapper = execsync(process) + + assert is_async_callable(wrapper) is False diff --git a/tests/utils/test_url.py b/tests/utils/test_url.py new file mode 100644 index 00000000..5eb9c3b1 --- /dev/null +++ b/tests/utils/test_url.py @@ -0,0 +1,29 @@ +from esmerald.utils.url import clean_path, join_paths + + +def test_clean_path(): + path = clean_path("test") + + assert path == "/test" + + +def test_clean_path_underscores(): + path = clean_path("////test") + + assert path == "/test" + + +def test_join_paths(): + paths = ["base", "test"] + + joined_paths = join_paths(paths) + + assert joined_paths == "/base/test" + + +def test_join_paths_complex(): + paths = ["base", "test", "//child", "////under"] + + joined_paths = join_paths(paths) + + assert joined_paths == "/base/test/child/under"