Skip to content

Commit

Permalink
Expose additional context (#1620)
Browse files Browse the repository at this point in the history
This PR contains 2 main changes:
- Expose additional context. We now expose the scope, operation,
connexion context, and receive channel as context aware globals. This
makes them available to the decorators independent of the framework in
between. The user will also be able to use these.
I also implemented a `TestContext` class which can be used to populate
the context during testing. It's minimal, but can be extended towards
the future.
- Rename the decorators to be framework specific. This is part of a
bigger change for which I'll submit a follow up PR. I was working on
this first when it became clear that the context would need to be
extended, which is why this is already included.
  • Loading branch information
RobbeSneyders committed Jan 13, 2023
1 parent f064fd0 commit 2bf18f6
Show file tree
Hide file tree
Showing 19 changed files with 302 additions and 206 deletions.
10 changes: 2 additions & 8 deletions connexion/apis/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
from flask import Response as FlaskResponse

from connexion.apis.abstract import AbstractAPI
from connexion.decorators import SyncDecorator
from connexion.decorators import FlaskDecorator
from connexion.frameworks import flask as flask_utils
from connexion.frameworks.flask import Flask as FlaskFramework
from connexion.jsonifier import Jsonifier
from connexion.operations import AbstractOperation
from connexion.uri_parsing import AbstractURIParser
Expand Down Expand Up @@ -91,12 +90,7 @@ def from_operation(

@property
def fn(self) -> t.Callable:
decorator = SyncDecorator(
self._operation,
uri_parser_cls=self.uri_parser_class,
framework=FlaskFramework,
parameter=True,
response=True,
decorator = FlaskDecorator(
pythonic_params=self.pythonic_params,
jsonifier=self.api.jsonifier,
)
Expand Down
10 changes: 2 additions & 8 deletions connexion/apps/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@

from connexion.apis.abstract import AbstractAPI
from connexion.apps.abstract import AbstractApp
from connexion.decorators import AsyncDecorator
from connexion.decorators import StarletteDecorator
from connexion.exceptions import MissingMiddleware, ProblemException
from connexion.frameworks.starlette import Starlette as StarletteFramework
from connexion.middleware.main import ConnexionMiddleware
from connexion.middleware.routing import ROUTING_CONTEXT
from connexion.operations import AbstractOperation
Expand Down Expand Up @@ -192,12 +191,7 @@ def from_operation(

@property
def fn(self) -> t.Callable:
decorator = AsyncDecorator(
self._operation,
uri_parser_cls=self._operation.uri_parser_class,
framework=StarletteFramework,
parameter=True,
response=True,
decorator = StarletteDecorator(
pythonic_params=self.pythonic_params,
jsonifier=self.api.jsonifier,
)
Expand Down
26 changes: 19 additions & 7 deletions connexion/context.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
from contextvars import ContextVar

from starlette.types import Scope
from starlette.types import Receive, Scope
from werkzeug.local import LocalProxy

from connexion.operations import AbstractOperation

UNBOUND_MESSAGE = (
"Working outside of operation context. Make sure your app is wrapped in a "
"ContextMiddleware and you're processing a request while accessing the context."
)

_scope: ContextVar[Scope] = ContextVar("SCOPE")

_context: ContextVar[dict] = ContextVar("CONTEXT")
context = LocalProxy(_context, unbound_message=UNBOUND_MESSAGE)

def __getattr__(name):
if name == "scope":
return _scope.get()
if name == "context":
return _scope.get().get("extensions", {}).get("connexion_context", {})
_operation: ContextVar[AbstractOperation] = ContextVar("OPERATION")
operation = LocalProxy(_operation, unbound_message=UNBOUND_MESSAGE)

_receive: ContextVar[Receive] = ContextVar("RECEIVE")
receive = LocalProxy(_receive, unbound_message=UNBOUND_MESSAGE)

_scope: ContextVar[Scope] = ContextVar("SCOPE")
scope = LocalProxy(_scope, unbound_message=UNBOUND_MESSAGE)
2 changes: 1 addition & 1 deletion connexion/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
This module defines decorators which Connexion uses to wrap user provided view functions.
"""
from .main import AsyncDecorator, SyncDecorator # noqa
from .main import FlaskDecorator, StarletteDecorator # noqa
113 changes: 62 additions & 51 deletions connexion/decorators/main.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,46 @@
import abc
import asyncio
import functools
import json
import typing as t

from asgiref.sync import async_to_sync
from starlette.concurrency import run_in_threadpool

from connexion.context import operation, receive, scope
from connexion.decorators.parameter import (
AsyncParameterDecorator,
BaseParameterDecorator,
SyncParameterDecorator,
inspect_function_arguments,
)
from connexion.decorators.response import (
AsyncResponseDecorator,
BaseResponseDecorator,
SyncResponseDecorator,
)
from connexion.frameworks.abstract import Framework
from connexion.operations import AbstractOperation
from connexion.frameworks.flask import Flask as FlaskFramework
from connexion.frameworks.starlette import Starlette as StarletteFramework
from connexion.uri_parsing import AbstractURIParser


class BaseDecorator:
"""Base class for connexion decorators."""

framework: t.Type[Framework]

def __init__(
self,
operation_spec: AbstractOperation,
*,
uri_parser_cls: t.Type[AbstractURIParser],
framework: t.Type[Framework],
parameter: bool,
response: bool,
pythonic_params: bool = False,
jsonifier,
uri_parser_class: AbstractURIParser = None,
jsonifier=json,
) -> None:
self.operation_spec = operation_spec
self.uri_parser = uri_parser_cls(
operation_spec.parameters, operation_spec.body_definition()
)
self.framework = framework
self.produces = self.operation_spec.produces
self.parameter = parameter
self.response = response
self.pythonic_params = pythonic_params
self.uri_parser_class = uri_parser_class
self.jsonifier = jsonifier

if self.parameter:
self.arguments, self.has_kwargs = inspect_function_arguments(
operation_spec.function
)
self.arguments, self.has_kwargs = None, None

@property
@abc.abstractmethod
Expand All @@ -68,27 +58,25 @@ def _sync_async_decorator(self) -> t.Callable[[t.Callable], t.Callable]:
"""Decorator to translate between sync and async functions."""
raise NotImplementedError

@property
def uri_parser(self):
uri_parser_class = self.uri_parser_class or operation.uri_parser_class
return uri_parser_class(operation.parameters, operation.body_definition())

def decorate(self, function: t.Callable) -> t.Callable:
"""Decorate a function with decorators based on the operation."""
function = self._sync_async_decorator(function)

if self.parameter:
parameter_decorator = self._parameter_decorator_cls(
self.operation_spec,
get_body_fn=self.framework.get_body,
arguments=self.arguments,
has_kwargs=self.has_kwargs,
pythonic_params=self.pythonic_params,
)
function = parameter_decorator(function)
parameter_decorator = self._parameter_decorator_cls(
pythonic_params=self.pythonic_params,
)
function = parameter_decorator(function)

if self.response:
response_decorator = self._response_decorator_cls(
self.operation_spec,
framework=self.framework,
jsonifier=self.jsonifier,
)
function = response_decorator(function)
response_decorator = self._response_decorator_cls(
framework=self.framework,
jsonifier=self.jsonifier,
)
function = response_decorator(function)

return function

Expand All @@ -97,7 +85,13 @@ def __call__(self, function: t.Callable) -> t.Callable:
raise NotImplementedError


class SyncDecorator(BaseDecorator):
class FlaskDecorator(BaseDecorator):
"""Decorator for usage with Flask. The parameter decorator works with a Flask request,
and provides Flask datastructures to the view function. The response decorator returns
a Flask response"""

framework = FlaskFramework

@property
def _parameter_decorator_cls(self) -> t.Type[SyncParameterDecorator]:
return SyncParameterDecorator
Expand All @@ -123,25 +117,33 @@ def wrapper(*args, **kwargs) -> t.Callable:
def __call__(self, function: t.Callable) -> t.Callable:
@functools.wraps(function)
def wrapper(*args, **kwargs):
# TODO: move into parameter decorator?
connexion_request = self.framework.get_request(
*args, uri_parser=self.uri_parser, **kwargs
)

request = self.framework.get_request(uri_parser=self.uri_parser)
decorated_function = self.decorate(function)
return decorated_function(connexion_request)
return decorated_function(request)

return wrapper


class AsyncDecorator(BaseDecorator):
class ASGIDecorator(BaseDecorator):
"""Decorator for usage with ASGI apps. The parameter decorator works with a Starlette request,
and provides Starlette datastructures to the view function. This works for any ASGI app, since
we get the request via the connexion context provided by ASGI middleware.
This decorator does not parse responses, but passes them directly to the ASGI App."""

framework = StarletteFramework

@property
def _parameter_decorator_cls(self) -> t.Type[AsyncParameterDecorator]:
return AsyncParameterDecorator

@property
def _response_decorator_cls(self) -> t.Type[AsyncResponseDecorator]:
return AsyncResponseDecorator
def _response_decorator_cls(self) -> t.Type[BaseResponseDecorator]:
class NoResponseDecorator(BaseResponseDecorator):
def __call__(self, function: t.Callable) -> t.Callable:
return lambda request: function(request)

return NoResponseDecorator

@property
def _sync_async_decorator(self) -> t.Callable[[t.Callable], t.Callable]:
Expand All @@ -160,15 +162,24 @@ async def wrapper(*args, **kwargs):
def __call__(self, function: t.Callable) -> t.Callable:
@functools.wraps(function)
async def wrapper(*args, **kwargs):
# TODO: move into parameter decorator?
connexion_request = self.framework.get_request(
*args, uri_parser=self.uri_parser, **kwargs
request = self.framework.get_request(
uri_parser=self.uri_parser, scope=scope, receive=receive
)

decorated_function = self.decorate(function)
response = decorated_function(connexion_request)
response = decorated_function(request)
while asyncio.iscoroutine(response):
response = await response
return response

return wrapper


class StarletteDecorator(ASGIDecorator):
"""Decorator for usage with Connexion or Starlette apps. The parameter decorator works with a
Starlette request, and provides Starlette datastructures to the view function.
The response decorator returns Starlette responses."""

@property
def _response_decorator_cls(self) -> t.Type[AsyncResponseDecorator]:
return AsyncResponseDecorator

0 comments on commit 2bf18f6

Please sign in to comment.