Skip to content

Commit

Permalink
Add jsonifier as argument to app / api (#1656)
Browse files Browse the repository at this point in the history
This PR adds a `jsonifier` argument to the app and api to align it with
other customization options. We also no longer pass it via the operation
object, which brings us closer to operations as data class only.
  • Loading branch information
RobbeSneyders committed Mar 2, 2023
1 parent 1cb5f83 commit 4f6ddfb
Show file tree
Hide file tree
Showing 16 changed files with 72 additions and 134 deletions.
7 changes: 7 additions & 0 deletions connexion/apps/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from starlette.testclient import TestClient
from starlette.types import Receive, Scope, Send

from connexion.jsonifier import Jsonifier
from connexion.middleware import ConnexionMiddleware, SpecMiddleware
from connexion.resolver import Resolver
from connexion.uri_parsing import AbstractURIParser
Expand All @@ -35,6 +36,7 @@ def __init__(
middlewares: t.Optional[list] = None,
arguments: t.Optional[dict] = None,
auth_all_paths: t.Optional[bool] = None,
jsonifier: t.Optional[Jsonifier] = None,
pythonic_params: t.Optional[bool] = None,
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
Expand All @@ -56,6 +58,7 @@ def __init__(
:param arguments: Arguments to substitute the specification using Jinja.
:param auth_all_paths: whether to authenticate not paths not defined in the specification.
Defaults to False.
:param jsonifier: Custom jsonifier to overwrite json encoding for json responses.
:param pythonic_params: When True, CamelCase parameters are converted to snake_case and an
underscore is appended to any shadowed built-ins. Defaults to False.
:param resolver: Callable that maps operationId to a function or instance of
Expand All @@ -80,6 +83,7 @@ def __init__(
middlewares=middlewares,
arguments=arguments,
auth_all_paths=auth_all_paths,
jsonifier=jsonifier,
swagger_ui_options=swagger_ui_options,
pythonic_params=pythonic_params,
resolver=resolver,
Expand All @@ -97,6 +101,7 @@ def add_api(
base_path: t.Optional[str] = None,
arguments: t.Optional[dict] = None,
auth_all_paths: t.Optional[bool] = None,
jsonifier: t.Optional[Jsonifier] = None,
pythonic_params: t.Optional[bool] = None,
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
Expand All @@ -118,6 +123,7 @@ def add_api(
:param arguments: Arguments to substitute the specification using Jinja.
:param auth_all_paths: whether to authenticate not paths not defined in the specification.
Defaults to False.
:param jsonifier: Custom jsonifier to overwrite json encoding for json responses.
:param pythonic_params: When True, CamelCase parameters are converted to snake_case and an
underscore is appended to any shadowed built-ins. Defaults to False.
:param resolver: Callable that maps operationId to a function or instance of
Expand Down Expand Up @@ -145,6 +151,7 @@ def add_api(
base_path=base_path,
arguments=arguments,
auth_all_paths=auth_all_paths,
jsonifier=jsonifier,
pythonic_params=pythonic_params,
resolver=resolver,
resolver_error=resolver_error,
Expand Down
39 changes: 26 additions & 13 deletions connexion/apps/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@

from connexion.apps.abstract import AbstractApp
from connexion.decorators import StarletteDecorator
from connexion.middleware.abstract import (
AbstractRoutingAPI,
RoutedAPI,
RoutedMiddleware,
)
from connexion.jsonifier import Jsonifier
from connexion.middleware.abstract import RoutedAPI, RoutedMiddleware
from connexion.operations import AbstractOperation
from connexion.resolver import Resolver
from connexion.uri_parsing import AbstractURIParser
Expand All @@ -30,26 +27,30 @@ def __init__(
operation: AbstractOperation,
fn: t.Callable,
uri_parser: AbstractURIParser,
api: AbstractRoutingAPI,
jsonifier: Jsonifier,
operation_id: str,
pythonic_params: bool,
) -> None:
self._operation = operation
self._fn = fn
self.uri_parser = uri_parser
self.api = api
self.jsonifier = jsonifier
self.operation_id = operation_id
self.pythonic_params = pythonic_params

@classmethod
def from_operation(
cls, operation: AbstractOperation, pythonic_params: bool
cls,
operation: AbstractOperation,
*,
pythonic_params: bool,
jsonifier: Jsonifier,
) -> "AsyncOperation":
return cls(
operation,
fn=operation.function,
uri_parser=operation.uri_parser_class,
api=operation.api,
jsonifier=jsonifier,
operation_id=operation.operation_id,
pythonic_params=pythonic_params,
)
Expand All @@ -58,7 +59,7 @@ def from_operation(
def fn(self) -> t.Callable:
decorator = StarletteDecorator(
pythonic_params=self.pythonic_params,
jsonifier=self.api.jsonifier,
jsonifier=self.jsonifier,
)
return decorator(self._fn)

Expand All @@ -70,14 +71,23 @@ async def __call__(


class AsyncApi(RoutedAPI[AsyncOperation]):
def __init__(self, *args, pythonic_params: bool, **kwargs) -> None:
def __init__(
self,
*args,
pythonic_params: bool,
jsonifier: t.Optional[Jsonifier] = None,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.pythonic_params = pythonic_params
self.jsonifier = jsonifier or Jsonifier()
self.router = Router()
self.add_paths()

def make_operation(self, operation: AbstractOperation) -> AsyncOperation:
return AsyncOperation.from_operation(operation, self.pythonic_params)
return AsyncOperation.from_operation(
operation, pythonic_params=self.pythonic_params, jsonifier=self.jsonifier
)


class AsyncMiddlewareApp(RoutedMiddleware[AsyncApi]):
Expand Down Expand Up @@ -118,6 +128,7 @@ def __init__(
middlewares: t.Optional[list] = None,
arguments: t.Optional[dict] = None,
auth_all_paths: t.Optional[bool] = None,
jsonifier: t.Optional[Jsonifier] = None,
pythonic_params: t.Optional[bool] = None,
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
Expand All @@ -139,6 +150,7 @@ def __init__(
:param arguments: Arguments to substitute the specification using Jinja.
:param auth_all_paths: whether to authenticate not paths not defined in the specification.
Defaults to False.
:param jsonifier: Custom jsonifier to overwrite json encoding for json responses.
:param pythonic_params: When True, CamelCase parameters are converted to snake_case and an
underscore is appended to any shadowed built-ins. Defaults to False.
:param resolver: Callable that maps operationId to a function or instance of
Expand All @@ -164,11 +176,12 @@ def __init__(
middlewares=middlewares,
arguments=arguments,
auth_all_paths=auth_all_paths,
swagger_ui_options=swagger_ui_options,
jsonifier=jsonifier,
pythonic_params=pythonic_params,
resolver=resolver,
resolver_error=resolver_error,
strict_validation=strict_validation,
swagger_ui_options=swagger_ui_options,
uri_parser_class=uri_parser_class,
validate_responses=validate_responses,
validator_map=validator_map,
Expand Down
20 changes: 15 additions & 5 deletions connexion/apps/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ def __init__(

@classmethod
def from_operation(
cls, operation: AbstractOperation, pythonic_params: bool
cls,
operation: AbstractOperation,
*,
pythonic_params: bool,
jsonifier: Jsonifier,
) -> "FlaskOperation":
return cls(
fn=operation.function,
jsonifier=operation.api.jsonifier,
jsonifier=jsonifier,
operation_id=operation.operation_id,
pythonic_params=pythonic_params,
)
Expand All @@ -61,8 +65,11 @@ def __call__(self, *args, **kwargs) -> FlaskResponse:


class FlaskApi(AbstractRoutingAPI):

jsonifier = Jsonifier(flask.json, indent=2)
def __init__(
self, *args, jsonifier: t.Optional[Jsonifier] = None, **kwargs
) -> None:
self.jsonifier = jsonifier or Jsonifier(flask.json, indent=2)
super().__init__(*args, **kwargs)

def _set_base_path(self, base_path: t.Optional[str] = None) -> None:
super()._set_base_path(base_path)
Expand All @@ -81,7 +88,7 @@ def _add_resolver_error_handler(self, method: str, path: str, err: ResolverError

def make_operation(self, operation):
return FlaskOperation.from_operation(
operation, pythonic_params=self.pythonic_params
operation, pythonic_params=self.pythonic_params, jsonifier=self.jsonifier
)

@staticmethod
Expand Down Expand Up @@ -173,6 +180,7 @@ def __init__(
middlewares: t.Optional[list] = None,
arguments: t.Optional[dict] = None,
auth_all_paths: t.Optional[bool] = None,
jsonifier: t.Optional[Jsonifier] = None,
pythonic_params: t.Optional[bool] = None,
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
Expand All @@ -195,6 +203,7 @@ def __init__(
:param arguments: Arguments to substitute the specification using Jinja.
:param auth_all_paths: whether to authenticate not paths not defined in the specification.
Defaults to False.
:param jsonifier: Custom jsonifier to overwrite json encoding for json responses.
:param swagger_ui_options: A :class:`options.ConnexionOptions` instance with configuration
options for the swagger ui.
:param pythonic_params: When True, CamelCase parameters are converted to snake_case and an
Expand All @@ -220,6 +229,7 @@ def __init__(
middlewares=middlewares,
arguments=arguments,
auth_all_paths=auth_all_paths,
jsonifier=jsonifier,
pythonic_params=pythonic_params,
resolver=resolver,
resolver_error=resolver_error,
Expand Down
20 changes: 6 additions & 14 deletions connexion/middleware/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from connexion.exceptions import MissingMiddleware, ResolverError
from connexion.http_facts import METHODS
from connexion.jsonifier import Jsonifier
from connexion.operations import AbstractOperation
from connexion.resolver import Resolver
from connexion.spec import Specification
Expand Down Expand Up @@ -38,8 +37,6 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send):
class AbstractSpecAPI:
"""Base API class with only minimal behavior related to the specification."""

jsonifier = Jsonifier()

def __init__(
self,
specification: t.Union[pathlib.Path, str, dict],
Expand Down Expand Up @@ -126,10 +123,9 @@ def add_operation(self, path: str, method: str) -> None:
spec_operation_cls = self.specification.operation_cls
spec_operation = spec_operation_cls.from_spec(
self.specification,
self,
path,
method,
self.resolver,
path=path,
method=method,
resolver=self.resolver,
uri_parser_class=self.uri_parser_class,
)
operation = self.make_operation(spec_operation)
Expand Down Expand Up @@ -175,9 +171,6 @@ def _handle_add_operation_error(
logger.error(error_msg)
raise exc from None

def json_loads(self, data):
return self.jsonifier.loads(data)


class RoutedAPI(AbstractSpecAPI, t.Generic[OP]):
def __init__(
Expand Down Expand Up @@ -207,10 +200,9 @@ def add_operation(self, path: str, method: str) -> None:
operation_spec_cls = self.specification.operation_cls
operation = operation_spec_cls.from_spec(
self.specification,
self,
path,
method,
self.resolver,
path=path,
method=method,
resolver=self.resolver,
uri_parser_class=self.uri_parser_class,
)
routed_operation = self.make_operation(operation)
Expand Down
8 changes: 8 additions & 0 deletions connexion/middleware/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from connexion import utils
from connexion.handlers import ResolverErrorHandler
from connexion.jsonifier import Jsonifier
from connexion.middleware.abstract import SpecMiddleware
from connexion.middleware.context import ContextMiddleware
from connexion.middleware.exceptions import ExceptionMiddleware
Expand Down Expand Up @@ -39,6 +40,7 @@ class _Options:

arguments: t.Optional[dict] = None
auth_all_paths: t.Optional[bool] = False
jsonifier: t.Optional[Jsonifier] = None
pythonic_params: t.Optional[bool] = False
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None
resolver_error: t.Optional[int] = None
Expand Down Expand Up @@ -101,6 +103,7 @@ def __init__(
middlewares: t.Optional[list] = None,
arguments: t.Optional[dict] = None,
auth_all_paths: t.Optional[bool] = None,
jsonifier: t.Optional[Jsonifier] = None,
pythonic_params: t.Optional[bool] = None,
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
Expand All @@ -122,6 +125,7 @@ def __init__(
:param arguments: Arguments to substitute the specification using Jinja.
:param auth_all_paths: whether to authenticate not paths not defined in the specification.
Defaults to False.
:param jsonifier: Custom jsonifier to overwrite json encoding for json responses.
:param pythonic_params: When True, CamelCase parameters are converted to snake_case and an
underscore is appended to any shadowed built-ins. Defaults to False.
:param resolver: Callable that maps operationId to a function or instance of
Expand Down Expand Up @@ -151,6 +155,7 @@ def __init__(
self.options = _Options(
arguments=arguments,
auth_all_paths=auth_all_paths,
jsonifier=jsonifier,
pythonic_params=pythonic_params,
resolver=resolver,
resolver_error=resolver_error,
Expand Down Expand Up @@ -198,6 +203,7 @@ def add_api(
base_path: t.Optional[str] = None,
arguments: t.Optional[dict] = None,
auth_all_paths: t.Optional[bool] = None,
jsonifier: t.Optional[Jsonifier] = None,
pythonic_params: t.Optional[bool] = None,
resolver: t.Optional[t.Union[Resolver, t.Callable]] = None,
resolver_error: t.Optional[int] = None,
Expand All @@ -219,6 +225,7 @@ def add_api(
:param arguments: Arguments to substitute the specification using Jinja.
:param auth_all_paths: whether to authenticate not paths not defined in the specification.
Defaults to False.
:param jsonifier: Custom jsonifier to overwrite json encoding for json responses.
:param pythonic_params: When True, CamelCase parameters are converted to snake_case and an
underscore is appended to any shadowed built-ins. Defaults to False.
:param resolver: Callable that maps operationId to a function or instance of
Expand Down Expand Up @@ -254,6 +261,7 @@ def add_api(
options = self.options.replace(
arguments=arguments,
auth_all_paths=auth_all_paths,
jsonifier=jsonifier,
swagger_ui_options=swagger_ui_options,
pythonic_params=pythonic_params,
resolver=resolver,
Expand Down
9 changes: 7 additions & 2 deletions connexion/middleware/swagger_ui.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import pathlib
import re
Expand All @@ -12,6 +13,7 @@
from starlette.templating import Jinja2Templates
from starlette.types import ASGIApp, Receive, Scope, Send

from connexion.jsonifier import Jsonifier
from connexion.middleware import SpecMiddleware
from connexion.middleware.abstract import AbstractSpecAPI
from connexion.options import SwaggerUIOptions
Expand Down Expand Up @@ -99,8 +101,11 @@ def add_openapi_yaml(self):
)

async def _get_openapi_json(self, request):
# Yaml parses datetime objects when loading the spec, so we need our custom jsonifier to dump it
jsonifier = Jsonifier()

return StarletteResponse(
content=self.jsonifier.dumps(self._spec_for_prefix(request)),
content=jsonifier.dumps(self._spec_for_prefix(request)),
status_code=200,
media_type="application/json",
)
Expand Down Expand Up @@ -170,7 +175,7 @@ async def _get_swagger_ui_config(self, request):
return StarletteResponse(
status_code=200,
media_type="application/json",
content=self.jsonifier.dumps(self.options.openapi_console_ui_config),
content=json.dumps(self.options.openapi_console_ui_config),
)


Expand Down

0 comments on commit 4f6ddfb

Please sign in to comment.