Skip to content

Commit

Permalink
Replaced direct config.path references and added tests for each plugi…
Browse files Browse the repository at this point in the history
…n implementation.

Added join_path utility function, with tests.
  • Loading branch information
gdub committed May 5, 2023
1 parent a836562 commit 3771917
Show file tree
Hide file tree
Showing 17 changed files with 208 additions and 54 deletions.
17 changes: 13 additions & 4 deletions spectree/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import posixpath
import warnings
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union
Expand All @@ -7,6 +6,7 @@

from .models import SecurityScheme, Server
from .page import DEFAULT_PAGE_TEMPLATES
from .utils import join_path

# Fall back to a str field if email-validator is not installed.
if TYPE_CHECKING:
Expand Down Expand Up @@ -118,9 +118,18 @@ def convert_to_lower_case(cls, values: Mapping[str, Any]) -> Dict[str, Any]:

@property
def spec_url(self) -> str:
sep = posixpath.sep
parts = (sep, self.path.lstrip(sep), self.filename.lstrip(sep))
return posixpath.join(*parts)
return self.join_doc_path(self.filename)

@property
def doc_root(self) -> str:
return self.join_doc_path("")

def join_doc_path(self, path: str) -> str:
"""
Return the documentation path constructed using the configured
self.path documentation prefix and the given path string.
"""
return f"/{join_path((self.path, path))}"

def swagger_oauth2_config(self) -> Dict[str, str]:
"""
Expand Down
4 changes: 2 additions & 2 deletions spectree/plugins/falcon_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ def register_route(self, app: Any):
)
for ui in self.config.page_templates:
self.app.add_route(
f"/{self.config.path}/{ui}",
self.config.join_doc_path(ui),
self.DOC_PAGE_ROUTE_CLASS(
self.config.page_templates[ui],
spec_url=self.config.spec_url,
spec_path=self.config.path,
spec_path=self.config.doc_root,
**self.config.swagger_oauth2_config(),
),
)
Expand Down
11 changes: 5 additions & 6 deletions spectree/plugins/flask_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class FlaskPlugin(BasePlugin):
def find_routes(self):
for rule in current_app.url_map.iter_rules():
if any(
str(rule).startswith(path)
for path in (f"/{self.config.path}", "/static")
str(rule).startswith(path) for path in (self.config.doc_root, "/static")
):
continue
if rule.endpoint.startswith("openapi"):
Expand Down Expand Up @@ -252,13 +251,13 @@ def gen_doc_page(ui):

return self.config.page_templates[ui].format(
spec_url=spec_url,
spec_path=self.config.path,
spec_path=self.config.doc_root,
**self.config.swagger_oauth2_config(),
)

for ui in self.config.page_templates:
app.add_url_rule(
rule=f"/{self.config.path}/{ui}/",
rule=f"{self.config.join_doc_path(ui)}/",
endpoint=f"openapi_{self.config.path}_{ui.replace('.', '_')}",
view_func=lambda ui=ui: gen_doc_page(ui),
)
Expand All @@ -267,11 +266,11 @@ def gen_doc_page(ui):
else:
for ui in self.config.page_templates:
app.add_url_rule(
rule=f"/{self.config.path}/{ui}/",
rule=f"{self.config.join_doc_path(ui)}/",
endpoint=f"openapi_{self.config.path}_{ui}",
view_func=lambda ui=ui: self.config.page_templates[ui].format(
spec_url=self.config.spec_url,
spec_path=self.config.path,
spec_path=self.config.doc_root,
**self.config.swagger_oauth2_config(),
),
)
8 changes: 4 additions & 4 deletions spectree/plugins/quart_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,13 @@ def gen_doc_page(ui):

return self.config.page_templates[ui].format(
spec_url=spec_url,
spec_path=self.config.path,
spec_path=self.config.doc_root,
**self.config.swagger_oauth2_config(),
)

for ui in self.config.page_templates:
app.add_url_rule(
f"/{self.config.path}/{ui}/",
f"{self.config.join_doc_path(ui)}/",
endpoint=f"openapi_{self.config.path}_{ui.replace('.', '_')}",
view_func=lambda ui=ui: gen_doc_page(ui),
)
Expand All @@ -279,11 +279,11 @@ def gen_doc_page(ui):
else:
for ui in self.config.page_templates:
app.add_url_rule(
f"/{self.config.path}/{ui}/",
f"{self.config.join_doc_path(ui)}/",
endpoint=f"openapi_{self.config.path}_{ui}",
view_func=lambda ui=ui: self.config.page_templates[ui].format(
spec_url=self.config.spec_url,
spec_path=self.config.path,
spec_path=self.config.doc_root,
**self.config.swagger_oauth2_config(),
),
)
4 changes: 2 additions & 2 deletions spectree/plugins/starlette_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ def register_route(self, app):

for ui in self.config.page_templates:
self.app.add_route(
f"/{self.config.path}/{ui}",
self.config.join_doc_path(ui),
lambda request, ui=ui: HTMLResponse(
self.config.page_templates[ui].format(
spec_url=self.config.spec_url,
spec_path=self.config.path,
spec_path=self.config.doc_root,
**self.config.swagger_oauth2_config(),
)
),
Expand Down
14 changes: 14 additions & 0 deletions spectree/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
Mapping,
Expand Down Expand Up @@ -349,3 +350,16 @@ def parse_resp(func: Any, naming_strategy: NamingStrategy = get_model_key):
responses = func.resp.generate_spec(naming_strategy)

return responses


def join_path(paths: Iterable[str]) -> str:
"""
Join path parts together.
Normalizes any leading or trailing slashes on the given path parts and avoids
behavior of urllib.parse.urljoin and posixpath.join where a path part with
leading slash overrides any path parts before it due to being treated as an
absolute path.
"""
normalized_paths = (x.strip("/") for x in paths if x)
return "/".join(x for x in normalized_paths if x)
7 changes: 6 additions & 1 deletion tests/flask_imports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from .dry_plugin_flask import (
import pytest

# Enable pytest assertion rewriting for dry module. Must come before module is imported.
pytest.register_assert_rewrite("tests.flask_imports.dry_plugin_flask")

from .dry_plugin_flask import ( # noqa: E402
test_flask_doc,
test_flask_list_json_request,
test_flask_no_response,
Expand Down
20 changes: 14 additions & 6 deletions tests/flask_imports/dry_plugin_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,36 @@ def test_flask_validation_error_response_status_code(


@pytest.mark.parametrize(
"test_client_and_api, expected_doc_pages",
"test_client_and_api, doc_prefix, expected_doc_pages",
[
pytest.param({}, ["redoc", "swagger"], id="default-page-templates"),
pytest.param({}, "/apidoc", ["redoc", "swagger"], id="default-page-templates"),
pytest.param(
{"api_kwargs": {"path": ""}},
"",
["redoc", "swagger"],
id="default-page-templates-in-root-path",
),
pytest.param(
{"api_kwargs": {"page_templates": {"custom_page": "{spec_url}"}}},
"/apidoc",
["custom_page"],
id="custom-page-templates",
),
],
indirect=["test_client_and_api"],
)
def test_flask_doc(test_client_and_api, expected_doc_pages):
def test_flask_doc(test_client_and_api, doc_prefix, expected_doc_pages):
client, api = test_client_and_api

resp = client.get("/apidoc/openapi.json")
resp = client.get(f"{doc_prefix}/openapi.json")
assert resp.status_code == 200
assert resp.json == api.spec

for doc_page in expected_doc_pages:
resp = client.get(f"/apidoc/{doc_page}/")
resp = client.get(f"{doc_prefix}/{doc_page}/")
assert resp.status_code == 200

resp = client.get(f"/apidoc/{doc_page}")
resp = client.get(f"{doc_prefix}/{doc_page}")
assert resp.status_code == 308


Expand Down
6 changes: 5 additions & 1 deletion tests/quart_imports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from .dry_plugin_quart import (
import pytest

pytest.register_assert_rewrite("tests.quart_imports.dry_plugin_quart")

from .dry_plugin_quart import ( # noqa: E402
test_quart_doc,
test_quart_no_response,
test_quart_return_model,
Expand Down
20 changes: 14 additions & 6 deletions tests/quart_imports/dry_plugin_quart.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,36 @@ def test_quart_validation_error_response_status_code(


@pytest.mark.parametrize(
"test_client_and_api, expected_doc_pages",
"test_client_and_api, doc_prefix, expected_doc_pages",
[
pytest.param({}, ["redoc", "swagger"], id="default-page-templates"),
pytest.param({}, "/apidoc", ["redoc", "swagger"], id="default-page-templates"),
pytest.param(
{"api_kwargs": {"path": ""}},
"",
["redoc", "swagger"],
id="default-page-templates-in-root-path",
),
pytest.param(
{"api_kwargs": {"page_templates": {"custom_page": "{spec_url}"}}},
"/apidoc",
["custom_page"],
id="custom-page-templates",
),
],
indirect=["test_client_and_api"],
)
def test_quart_doc(test_client_and_api, expected_doc_pages):
def test_quart_doc(test_client_and_api, doc_prefix, expected_doc_pages):
client, api = test_client_and_api

resp = asyncio.run(client.get("/apidoc/openapi.json"))
resp = asyncio.run(client.get(f"{doc_prefix}/openapi.json"))
assert resp.status_code == 200
assert asyncio.run(resp.json) == api.spec

for doc_page in expected_doc_pages:
resp = asyncio.run(client.get(f"/apidoc/{doc_page}/"))
resp = asyncio.run(client.get(f"{doc_prefix}/{doc_page}/"))
assert resp.status_code == 200

resp = asyncio.run(client.get(f"/apidoc/{doc_page}"))
resp = asyncio.run(client.get(f"{doc_prefix}/{doc_page}"))
assert resp.status_code == 308


Expand Down
30 changes: 30 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ def test_config_spec_url_when_given_path_and_filename(path, filename):
assert config.spec_url == "/prefix/openapi.json"


@pytest.mark.parametrize(
"config_path, test_path, expected_path",
[
pytest.param("prefix", "", "/prefix", id="root"),
pytest.param(
"prefix", "swagger", "/prefix/swagger", id="test-path-no-trailing-slash"
),
pytest.param(
"prefix", "swagger/", "/prefix/swagger", id="test-path-trailing-slash"
),
],
)
def test_config_join_doc_path(config_path, test_path, expected_path):
config = Configuration(path=config_path)
assert config.join_doc_path(test_path) == expected_path


@pytest.mark.parametrize(
"config_path, expected_doc_root_path",
[
pytest.param("", "/", id="root"),
pytest.param("prefix", "/prefix", id="path-no-trailing-slash"),
pytest.param("prefix/", "/prefix", id="path-trailing-slash"),
],
)
def test_config_doc_root(config_path, expected_doc_root_path):
config = Configuration(path=config_path)
assert config.doc_root == expected_doc_root_path


@pytest.mark.parametrize(("secure_item"), SECURITY_SCHEMAS)
def test_update_security_scheme(secure_item: Type[SecurityScheme]):
# update and validate each schema type
Expand Down
18 changes: 13 additions & 5 deletions tests/test_plugin_falcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,25 +400,33 @@ def test_validation_error_response_status_code(


@pytest.mark.parametrize(
"test_client_and_api, expected_doc_pages",
"test_client_and_api, doc_prefix, expected_doc_pages",
[
pytest.param({}, ["redoc", "swagger"], id="default-page-templates"),
pytest.param({}, "/apidoc", ["redoc", "swagger"], id="default-page-templates"),
pytest.param(
{"api_kwargs": {"path": ""}},
"",
["redoc", "swagger"],
id="default-page-templates-in-root-path",
),
pytest.param(
{"api_kwargs": {"page_templates": {"custom_page": "{spec_url}"}}},
"/apidoc",
["custom_page"],
id="custom-page-templates",
),
],
indirect=["test_client_and_api"],
)
def test_falcon_doc(test_client_and_api, expected_doc_pages):
def test_falcon_doc(test_client_and_api, doc_prefix, expected_doc_pages):
client, api = test_client_and_api

resp = client.simulate_get("/apidoc/openapi.json")
resp = client.simulate_get(f"{doc_prefix}/openapi.json")
assert resp.status_code == 200
assert resp.json == api.spec

for doc_page in expected_doc_pages:
resp = client.simulate_get(f"/apidoc/{doc_page}")
resp = client.simulate_get(f"{doc_prefix}/{doc_page}")
assert resp.status_code == 200


Expand Down
18 changes: 13 additions & 5 deletions tests/test_plugin_falcon_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,25 +310,33 @@ def test_validation_error_response_status_code(


@pytest.mark.parametrize(
"test_client_and_api, expected_doc_pages",
"test_client_and_api, doc_prefix, expected_doc_pages",
[
pytest.param({}, ["redoc", "swagger"], id="default-page-templates"),
pytest.param({}, "/apidoc", ["redoc", "swagger"], id="default-page-templates"),
pytest.param(
{"api_kwargs": {"path": ""}},
"",
["redoc", "swagger"],
id="default-page-templates-in-root-path",
),
pytest.param(
{"api_kwargs": {"page_templates": {"custom_page": "{spec_url}"}}},
"/apidoc",
["custom_page"],
id="custom-page-templates",
),
],
indirect=["test_client_and_api"],
)
def test_falcon_doc(test_client_and_api, expected_doc_pages):
def test_falcon_doc(test_client_and_api, doc_prefix, expected_doc_pages):
client, api = test_client_and_api

resp = client.simulate_get("/apidoc/openapi.json")
resp = client.simulate_get(f"{doc_prefix}/openapi.json")
assert resp.status_code == 200
assert resp.json == api.spec

for doc_page in expected_doc_pages:
resp = client.simulate_get(f"/apidoc/{doc_page}")
resp = client.simulate_get(f"{doc_prefix}/{doc_page}")
assert resp.status_code == 200


Expand Down
Loading

0 comments on commit 3771917

Please sign in to comment.