Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor add_path even more #238

Merged
merged 5 commits into from
Jul 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apispec/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Contains the main `APISpec` class.
"""
from .core import APISpec, Path
from .core import APISpec
from .plugin import BasePlugin

__version__ = '0.39.0'
Expand All @@ -11,6 +11,5 @@

__all__ = [
'APISpec',
'Path',
'BasePlugin',
]
96 changes: 28 additions & 68 deletions apispec/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,22 @@
def clean_operations(operations, openapi_major_version):
"""Ensure that all parameters with "in" equal to "path" are also required
as required by the OpenAPI specification, as well as normalizing any
references to global parameters.
references to global parameters. Also checks for invalid HTTP methods.

See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject.

:param dict operations: Dict mapping status codes to operations
:param int openapi_major_version: The major version of the OpenAPI standard
to use. Supported values are 2 and 3.
"""
invalid = {key for key in
set(iterkeys(operations)) - set(VALID_METHODS)
if not key.startswith('x-')}
if invalid:
raise APISpecError(
'One or more HTTP methods are invalid: {0}'.format(', '.join(invalid)),
)

def get_ref(param, openapi_major_version):
if isinstance(param, dict):
return param
Expand All @@ -60,45 +68,6 @@ def get_ref(param, openapi_major_version):
]


class Path(object):
"""Represents an OpenAPI Path object.

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathsObject

:param str path: The path template, e.g. ``"/pet/{petId}"``
:param str method: The HTTP method.
:param dict operation: The operation object, as a `dict`. See
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operationObject
:param str|OpenAPIVersion openapi_version: The OpenAPI version to use.
Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard.
"""
def __init__(self, path=None, operations=None, openapi_version='2.0'):
self.path = path
operations = operations or OrderedDict()
openapi_version = OpenAPIVersion(openapi_version)
clean_operations(operations, openapi_version.major)
invalid = {key for key in
set(iterkeys(operations)) - set(VALID_METHODS)
if not key.startswith('x-')}
if invalid:
raise APISpecError(
'One or more HTTP methods are invalid: {0}'.format(', '.join(invalid)),
)
self.operations = operations

def to_dict(self):
if not self.path:
raise APISpecError('Path template is not specified')
return {
self.path: self.operations,
}

def update(self, path):
if path.path:
self.path = path.path
self.operations.update(path.operations)


class APISpec(object):
"""Stores metadata that describes a RESTful API using the OpenAPI specification.

Expand Down Expand Up @@ -219,7 +188,7 @@ def add_path(self, path=None, operations=None, **kwargs):

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathsObject

:param str|Path|None path: URL Path component or Path instance
:param str|None path: URL path component
:param dict|None operations: describes the http methods and options for `path`
:param dict kwargs: parameters used by any path helpers see :meth:`register_path_helper`
"""
Expand All @@ -229,53 +198,43 @@ def normalize_path(path):
path = re.sub(pattern, '', path)
return path

if isinstance(path, Path):
path.path = normalize_path(path.path)
if operations:
path.operations.update(operations)
else:
path = Path(
path=normalize_path(path),
operations=operations,
openapi_version=self.openapi_version,
)
path = normalize_path(path)
operations = operations or OrderedDict()

# Execute path helpers
for plugin in self.plugins:
try:
ret = plugin.path_helper(path=path, operations=path.operations, **kwargs)
ret = plugin.path_helper(path=path, operations=operations, **kwargs)
except PluginMethodNotImplementedError:
continue
if isinstance(ret, Path):
ret.path = normalize_path(ret.path)
path.update(ret)
if ret is not None:
path = normalize_path(ret)
# Deprecated interface
for func in self._path_helpers:
try:
ret = func(
self, path=path, operations=path.operations, **kwargs
self, path=path, operations=operations, **kwargs
)
except TypeError:
continue
if isinstance(ret, Path):
ret.path = normalize_path(ret.path)
path.update(ret)
if not path.path:
if ret is not None:
path = normalize_path(ret)
if not path:
raise APISpecError('Path template is not specified')

# Execute operation helpers
for plugin in self.plugins:
try:
plugin.operation_helper(path=path, operations=path.operations, **kwargs)
plugin.operation_helper(path=path, operations=operations, **kwargs)
except PluginMethodNotImplementedError:
continue
# Deprecated interface
for func in self._operation_helpers:
func(self, path=path, operations=path.operations, **kwargs)
func(self, path=path, operations=operations, **kwargs)

# Execute response helpers
# TODO: cache response helpers output for each (method, status_code) couple
for method, operation in iteritems(path.operations):
for method, operation in iteritems(operations):
if method in VALID_METHODS and 'responses' in operation:
for status_code, response in iteritems(operation['responses']):
for plugin in self.plugins:
Expand All @@ -285,17 +244,19 @@ def normalize_path(path):
continue
# Deprecated interface
# Rule is that method + http status exist in both operations and helpers
methods = set(iterkeys(path.operations)) & set(iterkeys(self._response_helpers))
methods = set(iterkeys(operations)) & set(iterkeys(self._response_helpers))
for method in methods:
responses = path.operations[method]['responses']
responses = operations[method]['responses']
statuses = set(iterkeys(responses)) & set(iterkeys(self._response_helpers[method]))
for status_code in statuses:
for func in self._response_helpers[method][status_code]:
responses[status_code].update(
func(self, **kwargs),
)

self._paths.setdefault(path.path, path.operations).update(path.operations)
clean_operations(operations, self.openapi_version.major)

self._paths.setdefault(path, operations).update(operations)

def definition(
self, name, properties=None, enum=None, description=None, extra_fields=None,
Expand Down Expand Up @@ -399,7 +360,7 @@ def register_path_helper(self, func):

- Receive the `APISpec` instance as the first argument.
- Include ``**kwargs`` in signature.
- Return a `apispec.core.Path` object.
- Return a path as `str` or `None` and mutates ``operations`` `dict` kwarg.

The helper may define any named arguments in its signature.
"""
Expand Down Expand Up @@ -462,4 +423,3 @@ def _represent_unicode(_, uni):
yaml.add_representer(unicode, YAMLDumper._represent_unicode, Dumper=YAMLDumper)
yaml.add_representer(OrderedDict, YAMLDumper._represent_dict, Dumper=YAMLDumper)
yaml.add_representer(LazyDict, YAMLDumper._represent_dict, Dumper=YAMLDumper)
yaml.add_representer(Path, YAMLDumper._represent_dict, Dumper=YAMLDumper)
9 changes: 4 additions & 5 deletions apispec/ext/bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def gist_detail(gist_id):

from bottle import default_app

from apispec import Path, BasePlugin, utils
from apispec import BasePlugin, utils
from apispec.exceptions import APISpecError


Expand All @@ -53,13 +53,12 @@ def _route_for_view(app, view):
raise APISpecError('Could not find endpoint for route {0}'.format(view))
return endpoint

def path_helper(self, view, operations, **kwargs):
def path_helper(self, operations, view, **kwargs):
"""Path helper that allows passing a bottle view function."""
operations = utils.load_operations_from_docstring(view.__doc__)
operations.update(utils.load_operations_from_docstring(view.__doc__) or {})
app = kwargs.get('app', _default_app)
route = self._route_for_view(app, view)
bottle_path = self.bottle_path_to_openapi(route.rule)
return Path(path=bottle_path, operations=operations)
return self.bottle_path_to_openapi(route.rule)


# Deprecated interface
Expand Down
16 changes: 6 additions & 10 deletions apispec/ext/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def post(self):
from flask.views import MethodView

from apispec.compat import iteritems
from apispec import Path, BasePlugin, utils
from apispec import BasePlugin, utils
from apispec.exceptions import APISpecError


Expand Down Expand Up @@ -108,24 +108,20 @@ def _rule_for_view(view):
rule = current_app.url_map._rules_by_endpoint[endpoint][0]
return rule

def path_helper(self, view, **kwargs):
def path_helper(self, operations, view, **kwargs):
"""Path helper that allows passing a Flask view function."""
rule = self._rule_for_view(view)
path = self.flaskpath2openapi(rule.rule)
app_root = current_app.config['APPLICATION_ROOT'] or '/'
path = urljoin(app_root.rstrip('/') + '/', path.lstrip('/'))
operations = utils.load_operations_from_docstring(view.__doc__)
path = Path(path=path, operations=operations)
operations.update(utils.load_operations_from_docstring(view.__doc__) or {})
if hasattr(view, 'view_class') and issubclass(view.view_class, MethodView):
operations = {}
for method in view.methods:
if method in rule.methods:
method_name = method.lower()
method = getattr(view.view_class, method_name)
docstring_yaml = utils.load_yaml_from_docstring(method.__doc__)
operations[method_name] = docstring_yaml or dict()
path.operations.update(operations)
return path
path = self.flaskpath2openapi(rule.rule)
app_root = current_app.config['APPLICATION_ROOT'] or '/'
return urljoin(app_root.rstrip('/') + '/', path.lstrip('/'))


# Deprecated interface
Expand Down
14 changes: 4 additions & 10 deletions apispec/ext/marshmallow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class UserSchema(Schema):

import marshmallow

from apispec import Path, BasePlugin, utils
from apispec import BasePlugin, utils
from .common import resolve_schema_cls, resolve_schema_instance
from .openapi import OpenAPIConverter

Expand Down Expand Up @@ -167,7 +167,7 @@ def definition_helper(self, name, schema, **kwargs):

return json_schema

def path_helper(self, view=None, **kwargs):
def path_helper(self, operations, view=None, **kwargs):
"""Path helper that allows passing a Schema as a response. Responses can be
defined in a view's docstring.
::
Expand Down Expand Up @@ -228,14 +228,8 @@ def get(self):
# 'items': {'$ref': '#/definitions/User'}}}}}}}

"""
operations = (
kwargs.get('operations') or
(view and utils.load_operations_from_docstring(view.__doc__))
)
if not operations:
return None
operations = operations.copy()
return Path(operations=operations)
if view:
operations.update(utils.load_operations_from_docstring(view.__doc__) or {})

def operation_helper(self, operations, **kwargs):
for operation in operations.values():
Expand Down
16 changes: 6 additions & 10 deletions apispec/ext/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get(self):
import sys
from tornado.web import URLSpec

from apispec import Path, BasePlugin, utils
from apispec import BasePlugin, utils
from apispec.exceptions import APISpecError


Expand Down Expand Up @@ -90,23 +90,19 @@ def _extensions_from_handler(handler_class):
extensions = utils.load_yaml_from_docstring(handler_class.__doc__) or {}
return extensions

def path_helper(self, urlspec, operations, **kwargs):
def path_helper(self, operations, urlspec, **kwargs):
"""Path helper that allows passing a Tornado URLSpec or tuple."""
if not isinstance(urlspec, URLSpec):
urlspec = URLSpec(*urlspec)
if not operations:
operations = {}
for operation in self._operations_from_methods(urlspec.handler_class):
operations.update(operation)
for operation in self._operations_from_methods(urlspec.handler_class):
operations.update(operation)
if not operations:
raise APISpecError(
'Could not find endpoint for urlspec {0}'.format(urlspec),
)
params_method = getattr(urlspec.handler_class, list(operations.keys())[0])
path = self.tornadopath2openapi(urlspec, params_method)
extensions = self._extensions_from_handler(urlspec.handler_class)
operations.update(extensions)
return Path(path=path, operations=operations)
operations.update(self._extensions_from_handler(urlspec.handler_class))
return self.tornadopath2openapi(urlspec, params_method)


# Deprecated interface
Expand Down
20 changes: 18 additions & 2 deletions apispec/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,27 @@ def definition_helper(self, name, definition, **kwargs):
raise PluginMethodNotImplementedError

def path_helper(self, path=None, operations=None, **kwargs):
"""Should return a Path instance. Any other return value type is ignored"""
"""May return a path as string and mutate operations dict.

:param str path: Path to the resource
:param dict operations: A `dict` mapping HTTP methods to operation object. See
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operationObject

Return value should be a string or None. If a string is returned, it
is set as the path.

The last path helper returning a string sets the path value. Therefore,
the order of plugin registration matters. However, generally, registering
several plugins that return a path does not make sense.
"""
raise PluginMethodNotImplementedError

def operation_helper(self, path=None, operations=None, **kwargs):
"""Should mutate operations. Return value ignored."""
"""Should mutate operations.

:param str path: Path to the resource
:param dict operations: A `dict` mapping HTTP methods to operation object. See
"""
raise PluginMethodNotImplementedError

def response_helper(self, method, status_code, **kwargs):
Expand Down
Loading