From 29e69b189958dd6dcc4e8827ccebc311f315e888 Mon Sep 17 00:00:00 2001 From: Keming Date: Sat, 15 Oct 2022 11:03:00 +0800 Subject: [PATCH 1/3] feat: import plugin using import_module Signed-off-by: Keming --- Makefile | 10 +++++++++- spectree/plugins/__init__.py | 19 +++++++++++-------- spectree/plugins/falcon_plugin.py | 17 +++++++---------- spectree/plugins/flask_plugin.py | 12 ++---------- spectree/plugins/starlette_plugin.py | 11 ++++------- spectree/spec.py | 9 +++++++-- tests/import_module/__init__.py | 0 tests/import_module/test_falcon_plugin.py | 5 +++++ tests/import_module/test_flask_plugin.py | 4 ++++ tests/import_module/test_starlette_plugin.py | 4 ++++ tests/test_spec.py | 2 +- 11 files changed, 54 insertions(+), 39 deletions(-) create mode 100644 tests/import_module/__init__.py create mode 100644 tests/import_module/test_falcon_plugin.py create mode 100644 tests/import_module/test_flask_plugin.py create mode 100644 tests/import_module/test_starlette_plugin.py diff --git a/Makefile b/Makefile index 50d60ca8..8e8bb775 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,17 @@ SOURCE_FILES=spectree tests examples setup.py install: pip install -e .[email,flask,falcon,starlette,dev] -test: +import_test: + for module in flask falcon starlette; do \ + pip install -U $$module; \ + bash -c "python tests/import_module/test_$${module}_plugin.py"; \ + pip uninstall $$module -y; \ + done + +test: import_test pip install -U -e .[email,flask,falcon,starlette] pytest tests -vv -rs + doc: cd docs && make html diff --git a/spectree/plugins/__init__.py b/spectree/plugins/__init__.py index bebc018b..6796218d 100644 --- a/spectree/plugins/__init__.py +++ b/spectree/plugins/__init__.py @@ -1,12 +1,15 @@ +from collections import namedtuple + from .base import BasePlugin -from .falcon_plugin import FalconAsgiPlugin, FalconPlugin -from .flask_plugin import FlaskPlugin -from .starlette_plugin import StarlettePlugin + +__all__ = ["BasePlugin"] + +Plugin = namedtuple("Plugin", ("name", "package", "class_name")) PLUGINS = { - "base": BasePlugin, - "flask": FlaskPlugin, - "falcon": FalconPlugin, - "falcon-asgi": FalconAsgiPlugin, - "starlette": StarlettePlugin, + "base": Plugin(".base", __name__, "BasePlugin"), + "flask": Plugin(".flask_plugin", __name__, "FlaskPlugin"), + "falcon": Plugin(".falcon_plugin", __name__, "FalconPlugin"), + "falcon-asgi": Plugin(".falcon_plugin", __name__, "FalconAsgiPlugin"), + "starlette": Plugin(".starlette_plugin", __name__, "StarlettePlugin"), } diff --git a/spectree/plugins/falcon_plugin.py b/spectree/plugins/falcon_plugin.py index 46179a5c..7cd8cb65 100644 --- a/spectree/plugins/falcon_plugin.py +++ b/spectree/plugins/falcon_plugin.py @@ -3,6 +3,8 @@ from functools import partial from typing import Any, Callable, Dict, List, Mapping, Optional, get_type_hints +from falcon import HTTP_400, HTTP_415, HTTPError +from falcon.routing.compiled import _FIELD_PATTERN as FALCON_FIELD_PATTERN from pydantic import ValidationError from .._types import ModelType @@ -51,12 +53,7 @@ class FalconPlugin(BasePlugin): def __init__(self, spectree): super().__init__(spectree) - from falcon import HTTP_400, HTTP_415, HTTPError - from falcon.routing.compiled import _FIELD_PATTERN - - self.FALCON_HTTP_ERROR = HTTPError self.FALCON_MEDIA_ERROR_CODE = (HTTP_400, HTTP_415) - self.FIELD_PATTERN = _FIELD_PATTERN # NOTE from `falcon.routing.compiled.CompiledRouterNode` self.ESCAPE = r"[\.\(\)\[\]\?\$\*\+\^\|]" self.ESCAPE_TO = r"\\\g<0>" @@ -109,13 +106,13 @@ def parse_func(self, route: Any) -> Dict[str, Any]: def parse_path(self, route, path_parameter_descriptions): subs, parameters = [], [] for segment in route.uri_template.strip("/").split("/"): - matches = self.FIELD_PATTERN.finditer(segment) + matches = FALCON_FIELD_PATTERN.finditer(segment) if not matches: subs.append(segment) continue escaped = re.sub(self.ESCAPE, self.ESCAPE_TO, segment) - subs.append(self.FIELD_PATTERN.sub(self.EXTRACT, escaped)) + subs.append(FALCON_FIELD_PATTERN.sub(self.EXTRACT, escaped)) for field in matches: variable, converter, argstr = [ @@ -180,7 +177,7 @@ def request_validation(self, req, query, json, form, headers, cookies): if json: try: media = req.media - except self.FALCON_HTTP_ERROR as err: + except HTTPError as err: if err.status not in self.FALCON_MEDIA_ERROR_CODE: raise media = None @@ -268,7 +265,7 @@ async def request_validation(self, req, query, json, form, headers, cookies): if json: try: media = await req.get_media() - except self.FALCON_HTTP_ERROR as err: + except HTTPError as err: if err.status not in self.FALCON_MEDIA_ERROR_CODE: raise media = None @@ -276,7 +273,7 @@ async def request_validation(self, req, query, json, form, headers, cookies): if form: try: form_data = await req.get_media() - except self.FALCON_HTTP_ERROR as err: + except HTTPError as err: if err.status not in self.FALCON_MEDIA_ERROR_CODE: raise req.context.form = None diff --git a/spectree/plugins/flask_plugin.py b/spectree/plugins/flask_plugin.py index 6f624b72..8ec40d45 100644 --- a/spectree/plugins/flask_plugin.py +++ b/spectree/plugins/flask_plugin.py @@ -1,6 +1,8 @@ from typing import Any, Callable, Mapping, Optional, Tuple, get_type_hints +from flask import Blueprint, abort, current_app, jsonify, make_response, request from pydantic import BaseModel, ValidationError +from werkzeug.routing import parse_converter_args from .._types import ModelType from ..response import Response @@ -12,8 +14,6 @@ class FlaskPlugin(BasePlugin): blueprint_state = None def find_routes(self): - from flask import current_app - for rule in current_app.url_map.iter_rules(): if any( str(rule).startswith(path) @@ -39,8 +39,6 @@ def bypass(self, func, method): return method in ["HEAD", "OPTIONS"] def parse_func(self, route: Any): - from flask import current_app - if self.blueprint_state: func = self.blueprint_state.app.view_functions[route.endpoint] else: @@ -62,8 +60,6 @@ def parse_path( route: Optional[Mapping[str, str]], path_parameter_descriptions: Optional[Mapping[str, str]], ) -> Tuple[str, list]: - from werkzeug.routing import parse_converter_args - subs = [] parameters = [] @@ -183,8 +179,6 @@ def validate( *args: Any, **kwargs: Any, ): - from flask import abort, jsonify, make_response, request - response, req_validation_error, resp_validation_error = None, None, None try: self.request_validation(request, query, json, form, headers, cookies) @@ -240,8 +234,6 @@ def validate( return response def register_route(self, app): - from flask import Blueprint, jsonify - app.add_url_rule( rule=self.config.spec_url, endpoint=f"openapi_{self.config.path}", diff --git a/spectree/plugins/starlette_plugin.py b/spectree/plugins/starlette_plugin.py index 199b4275..f43fe76e 100644 --- a/spectree/plugins/starlette_plugin.py +++ b/spectree/plugins/starlette_plugin.py @@ -5,6 +5,10 @@ from typing import Any, Callable, Optional, get_type_hints from pydantic import ValidationError +from starlette.convertors import CONVERTOR_TYPES +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse +from starlette.routing import compile_path from .._types import ModelType from ..response import Response @@ -15,8 +19,6 @@ def PydanticResponse(content): - from starlette.responses import JSONResponse - class _PydanticResponse(JSONResponse): def render(self, content) -> bytes: self._model_class = content.__class__ @@ -30,13 +32,11 @@ class StarlettePlugin(BasePlugin): def __init__(self, spectree): super().__init__(spectree) - from starlette.convertors import CONVERTOR_TYPES self.conv2type = {conv: typ for typ, conv in CONVERTOR_TYPES.items()} def register_route(self, app): self.app = app - from starlette.responses import HTMLResponse, JSONResponse self.app.add_route( self.config.spec_url, @@ -86,8 +86,6 @@ async def validate( *args: Any, **kwargs: Any, ): - from starlette.requests import Request - from starlette.responses import JSONResponse if isinstance(args[0], Request): instance, request = None, args[0] @@ -193,7 +191,6 @@ def parse_func(self, route): yield method, route.func def parse_path(self, route, path_parameter_descriptions): - from starlette.routing import compile_path _, path, variables = compile_path(route.path) parameters = [] diff --git a/spectree/spec.py b/spectree/spec.py index b2cae016..5533bf6c 100644 --- a/spectree/spec.py +++ b/spectree/spec.py @@ -1,6 +1,7 @@ from collections import defaultdict from copy import deepcopy from functools import wraps +from importlib import import_module from typing import ( Any, Callable, @@ -67,8 +68,12 @@ def __init__( self.validation_error_status = validation_error_status self.config: Configuration = Configuration.parse_obj(kwargs) self.backend_name = backend_name - self.backend = backend(self) if backend else PLUGINS[backend_name](self) - # init + if backend: + self.backend = backend(self) + else: + plugin = PLUGINS[backend_name] + module = import_module(plugin.name, plugin.package) + self.backend = getattr(module, plugin.class_name)(self) self.models: Dict[str, Any] = {} if app: self.register(app) diff --git a/tests/import_module/__init__.py b/tests/import_module/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/import_module/test_falcon_plugin.py b/tests/import_module/test_falcon_plugin.py new file mode 100644 index 00000000..82837308 --- /dev/null +++ b/tests/import_module/test_falcon_plugin.py @@ -0,0 +1,5 @@ +from spectree import SpecTree + +SpecTree("falcon") +SpecTree("falcon-asgi") +print("=> passed falcon plugin import test") diff --git a/tests/import_module/test_flask_plugin.py b/tests/import_module/test_flask_plugin.py new file mode 100644 index 00000000..6afebb60 --- /dev/null +++ b/tests/import_module/test_flask_plugin.py @@ -0,0 +1,4 @@ +from spectree import SpecTree + +SpecTree("flask") +print("=> passed flask plugin import test") diff --git a/tests/import_module/test_starlette_plugin.py b/tests/import_module/test_starlette_plugin.py new file mode 100644 index 00000000..5ae74c1f --- /dev/null +++ b/tests/import_module/test_starlette_plugin.py @@ -0,0 +1,4 @@ +from spectree import SpecTree + +SpecTree("starlette") +print("=> passed starlette plugin import test") diff --git a/tests/test_spec.py b/tests/test_spec.py index 504591ae..2ace6b15 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -7,7 +7,7 @@ from spectree import Response from spectree.config import Configuration from spectree.models import Server, ValidationError -from spectree.plugins import FlaskPlugin +from spectree.plugins.flask_plugin import FlaskPlugin from spectree.spec import SpecTree from .common import get_paths From eda282e1cba5cd14b46198627fa731f73dc32940 Mon Sep 17 00:00:00 2001 From: Keming Date: Sat, 15 Oct 2022 11:09:14 +0800 Subject: [PATCH 2/3] exit if err Signed-off-by: Keming --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8e8bb775..f80a5e79 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ install: import_test: for module in flask falcon starlette; do \ pip install -U $$module; \ - bash -c "python tests/import_module/test_$${module}_plugin.py"; \ + bash -c "python tests/import_module/test_$${module}_plugin.py" || exit 1; \ pip uninstall $$module -y; \ done From 9c7c8cf84fcde7a7c2e4b9a47b1b816d01b161e1 Mon Sep 17 00:00:00 2001 From: Keming Date: Sat, 15 Oct 2022 11:12:11 +0800 Subject: [PATCH 3/3] fix test Signed-off-by: Keming --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index f80a5e79..d6d9a17f 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ install: pip install -e .[email,flask,falcon,starlette,dev] import_test: + pip install -e .[email] for module in flask falcon starlette; do \ pip install -U $$module; \ bash -c "python tests/import_module/test_$${module}_plugin.py" || exit 1; \