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

Rework components interface #381

Merged
merged 10 commits into from
Feb 5, 2019
33 changes: 29 additions & 4 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,44 @@ Changelog
Features:

- Expanded support for OpenAPI Specification version 3 (:issue:`165`).
- Add ``apispec.core.Components.security_scheme`` for adding Security
- Add `apispec.core.Components.security_scheme` for adding Security
Scheme Objects (:issue:`245`).
- [apispec.ext.marshmallow]: Add support for outputting field patterns
from ``Regexp`` validators (:pr:`364`).
Thanks :user:`DStape` for the PR.
- A `DuplicateComponentNameError` is raised when registering two
components with the same name (:issue:`340`).

Other changes:

- *Backwards-incompatible*: Components properties are now passed as dictionaries rather than keyword arguments (:pr:`381`).

.. code-block:: python

# <1.0.0
spec.components.schema("Pet", properties={"name": {"type": "string"}})
spec.components.parameter("PetId", "path", format="int64", type="integer")
spec.component.response("NotFound", description="Pet not found")

# >=1.0.0
spec.components.schema("Pet", {"properties": {"name": {"type": "string"}}})
spec.components.parameter("PetId", "path", {"format": "int64", "type": "integer"})
spec.component.response("NotFound", {"description": "Pet not found"})

Deprecations/Removals:

- *Backwards-incompatible*: The ``ref`` argument passed to fields is no
longer used (:issue:`354`). References for nested ``Schema`` are
stored automatically.
- *Backwards-incompatible*: The ``extra_fields`` argument of
`apispec.core.Components.schema` is removed. All properties may be
passed in the ``component`` argument.

.. code-block:: python

# <1.0.0
spec.components.schema("Pet", schema=PetSchema, extra_fields={"discriminator": "name"})

# >=1.0.0
spec.components.schema("Pet", component={"discriminator": "name"}, schema=PetSchema)

1.0.0rc1 (2018-01-29)
+++++++++++++++++++++
Expand All @@ -32,7 +57,7 @@ Features:
- Ability to opt out of the above behavior by passing a ``schema_name_resolver``
function that returns ``None`` to ``api.ext.MarshmallowPlugin`` on initialization.
- References now respect Schema initialization modifiers such as exclude.
- *Backwards-incompatible*: A `DuplicateComponentNameError` is raised
- *Backwards-incompatible*: A `apispec.exceptions.DuplicateComponentNameError` is raised
when registering two components with the same name (:issue:`340`).

1.0.0b6 (2018-12-16)
Expand Down
76 changes: 35 additions & 41 deletions apispec/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,11 @@ def to_dict(self):
security_key: self._security_schemes,
}

def schema(
self,
name,
properties=None,
enum=None,
description=None,
extra_fields=None,
**kwargs
):
"""Add a new definition to the spec.
def schema(self, name, component=None, **kwargs):
"""Add a new schema to the spec.

:param str name: identifier by which schema may be referenced.
:param dict component: schema definition.

.. note::

Expand All @@ -138,82 +133,81 @@ def schema(
raise DuplicateComponentNameError(
'Another schema with name "{}" is already registered.'.format(name)
)
ret = {}
component = component or {}
ret = component.copy()
# Execute all helpers from plugins
for plugin in self._plugins:
try:
ret.update(plugin.schema_helper(name, definition=ret, **kwargs) or {})
ret.update(plugin.schema_helper(name, component, **kwargs) or {})
except PluginMethodNotImplementedError:
continue
if properties:
ret["properties"] = properties
if enum:
ret["enum"] = enum
if description:
ret["description"] = description
if extra_fields:
Copy link
Member

@sloria sloria Feb 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, nice. This cleans things up quite a bit.

special_topics.rst will need to be updated--we can probably just get rid of the "Adding Additional Fields To Schema Objects" section altogether.

Update: ended up modifying the section to show how to add additional properties to auto-generated properties.

ret.update(extra_fields)
self._schemas[name] = ret
return self

def parameter(self, param_id, location, **kwargs):
def parameter(self, component_id, location, component=None, **kwargs):
""" Add a parameter which can be referenced.

:param str param_id: identifier by which parameter may be referenced.
:param str location: location of the parameter.
:param dict kwargs: parameter fields.
:param dict component: parameter fields.
:param dict kwargs: plugin-specific arguments
"""
if param_id in self._parameters:
if component_id in self._parameters:
raise DuplicateComponentNameError(
'Another parameter with name "{}" is already registered.'.format(
param_id
component_id
)
)
ret = kwargs.copy()
ret.setdefault("name", param_id)
component = component or {}
ret = component.copy()
ret.setdefault("name", component_id)
ret["in"] = location
# Execute all helpers from plugins
for plugin in self._plugins:
try:
ret.update(plugin.parameter_helper(**kwargs) or {})
ret.update(plugin.parameter_helper(component, **kwargs) or {})
except PluginMethodNotImplementedError:
continue
self._parameters[param_id] = ret
self._parameters[component_id] = ret
return self

def response(self, ref_id, **kwargs):
def response(self, component_id, component=None, **kwargs):
"""Add a response which can be referenced.

:param str ref_id: ref_id to use as reference
:param dict kwargs: response fields
:param str component_id: ref_id to use as reference
:param dict component: response fields
:param dict kwargs: plugin-specific arguments
"""
if ref_id in self._responses:
if component_id in self._responses:
raise DuplicateComponentNameError(
'Another response with name "{}" is already registered.'.format(ref_id)
'Another response with name "{}" is already registered.'.format(
component_id
)
)
ret = kwargs.copy()
component = component or {}
ret = component.copy()
# Execute all helpers from plugins
for plugin in self._plugins:
try:
ret.update(plugin.response_helper(**kwargs) or {})
ret.update(plugin.response_helper(component, **kwargs) or {})
except PluginMethodNotImplementedError:
continue
self._responses[ref_id] = ret
self._responses[component_id] = ret
return self

def security_scheme(self, sec_id, **kwargs):
def security_scheme(self, component_id, component):
"""Add a security scheme which can be referenced.

:param str sec_id: sec_id to use as reference
:param str component_id: component_id to use as reference
:param dict kwargs: security scheme fields
"""
if sec_id in self._security_schemes:
if component_id in self._security_schemes:
raise DuplicateComponentNameError(
'Another security scheme with name "{}" is already registered.'.format(
sec_id
component_id
)
)
self._security_schemes[sec_id] = kwargs
self._security_schemes[component_id] = component
return self


Expand Down
20 changes: 11 additions & 9 deletions apispec/ext/marshmallow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class MyCustomFieldThatsKindaLikeAnInteger(Integer):
"""
return self.openapi.map_to_openapi_type(*args)

def schema_helper(self, name, schema=None, **kwargs):
def schema_helper(self, name, _, schema=None, **kwargs):
"""Definition helper that allows using a marshmallow
:class:`Schema <marshmallow.Schema>` to provide OpenAPI
metadata.
Expand All @@ -159,24 +159,26 @@ def schema_helper(self, name, schema=None, **kwargs):

return json_schema

def parameter_helper(self, **kwargs):
def parameter_helper(self, parameter, **kwargs):
"""Parameter component helper that allows using a marshmallow
:class:`Schema <marshmallow.Schema>` in parameter definition.

:param type|Schema schema: A marshmallow Schema class or instance.
:param dict parameter: parameter fields. May contain a marshmallow
Schema class or instance.
"""
# In OpenAPIv3, this only works when using the complex form using "content"
self.resolve_schema(kwargs)
return kwargs
self.resolve_schema(parameter)
return parameter

def response_helper(self, **kwargs):
def response_helper(self, response, **kwargs):
"""Response component helper that allows using a marshmallow
:class:`Schema <marshmallow.Schema>` in response definition.

:param type|Schema schema: A marshmallow Schema class or instance.
:param dict parameter: response fields. May contain a marshmallow
Schema class or instance.
"""
self.resolve_schema(kwargs)
return kwargs
self.resolve_schema(response)
return response

def operation_helper(self, operations, **kwargs):
for operation in operations.values():
Expand Down
4 changes: 2 additions & 2 deletions apispec/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ def schema_helper(self, name, definition, **kwargs):
"""May return definition as a dict."""
raise PluginMethodNotImplementedError

def parameter_helper(self, **kwargs):
def parameter_helper(self, parameter, **kwargs):
"""May return parameter component description as a dict."""
raise PluginMethodNotImplementedError

def response_helper(self, **kwargs):
def response_helper(self, response, **kwargs):
"""May return response component description as a dict."""
raise PluginMethodNotImplementedError

Expand Down
8 changes: 5 additions & 3 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ Add schemas to your spec using `spec.components.schema <apispec.core.Components.

spec.components.schema(
"Gist",
properties={
"id": {"type": "integer", "format": "int64"},
"name": {"type": "string"},
{
"properties": {
"id": {"type": "integer", "format": "int64"},
"name": {"type": "string"},
}
},
)

Expand Down
13 changes: 6 additions & 7 deletions docs/special_topics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Solutions to specific problems are documented here.
Adding Additional Fields To Schema Objects
------------------------------------------

To add additional fields (e.g. ``"discriminator"``) to Schema objects generated from `spec.components.schema <apispec.core.Components.schema>` , pass the ``extra_fields`` argument.
To add additional fields (e.g. ``"discriminator"``) to Schema objects generated from `spec.components.schema <apispec.core.Components.schema>` , pass them
to the ``component`` parameter. If your'e using ``MarshmallowPlugin``, the ``component`` properties will get merged with the autogenerated properties.

.. code-block:: python

Expand All @@ -16,13 +17,11 @@ To add additional fields (e.g. ``"discriminator"``) to Schema objects generated
"name": {"type": "string", "example": "doggie"},
}

spec.components.schema(
"Pet", properties=properties, extra_fields={"discriminator": "petType"}
)
spec.components.schema("Pet", component={"discriminator": "petType"}, schema=PetSchema)


.. note::
Be careful about the input that you pass to ``extra_fields``. ``apispec`` will not guarantee that the passed fields are valid against the OpenAPI spec.
Be careful about the input that you pass to ``component``. ``apispec`` will not guarantee that the passed fields are valid against the OpenAPI spec.

Rendering to YAML or JSON
-------------------------
Expand Down Expand Up @@ -129,8 +128,8 @@ to document `Security Scheme Objects <https://github.com/OAI/OpenAPI-Specificati
api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
jwt_scheme = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}

spec.components.security_scheme("api_key", **api_key_scheme)
spec.components.security_scheme("jwt", **jwt_scheme)
spec.components.security_scheme("api_key", api_key_scheme)
spec.components.security_scheme("jwt", jwt_scheme)

pprint(spec.to_dict()["components"]["securitySchemes"], indent=2)
# { 'api_key': {'in': 'header', 'name': 'X-API-Key', 'type': 'apiKey'},
Expand Down
28 changes: 12 additions & 16 deletions docs/using_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,12 @@ If your API uses `method-based dispatching <http://flask.pocoo.org/docs/0.12/vie
def get(self):
"""Gist view
---
description: get a gist
description: Get a gist
responses:
200:
content:
application/json:
schema: GistSchema
200:
content:
application/json:
schema: GistSchema
"""
pass

Expand Down Expand Up @@ -207,29 +207,25 @@ By default, Marshmallow `Nested` fields are represented by a `JSON Reference obj
If the schema has been added to the spec via `spec.components.schema <apispec.core.Components.schema>`,
the user-supplied name will be used in the reference. Otherwise apispec will
add the nested schema to the spec using an automatically resolved name for the
nested schema. The default `schema_name_resolver <apispec.ext.marshmallow.resolver>`
nested schema. The default `resolver <apispec.ext.marshmallow.resolver>`
function will resolve a name based on the schema's class `__name__`, dropping a
trailing "Schema" so that `class PetSchema(Schema)` resolves to "Pet".

To change the behavior of the name resolution simply pass an alternative
To change the behavior of the name resolution simply pass a
function accepting a `Schema` class and returning a string to the plugin's
constructor. If the `schema_name_resolver` function returns a value that
evaluates to `False` in a boolean context the nested schema will not be added to
the spec and instead defined in-line.

Note: Circular-referencing schemas cannot be defined in-line due to infinite
recursion so a `schema_name_resolver` function must return a string name when
working with circular-referencing schemas.
.. note::
Circular-referencing schemas cannot be defined in-line due to infinite
recursion so a `schema_name_resolver` function must return a string name when
working with circular-referencing schemas.

Schema Modifiers
****************

`Schema` instances can be initialized with modifier parameters to exclude
fields or ignore the absence of required fields. apispec will respect
schema modifiers in the generated schema definition. If a particular schema is
initialized in an application with modifiers, it may be added to the spec with
each set of modifiers and apispec will treat each unique set of modifiers --
including no modifiers - as a unique schema definition.
apispec will respect schema modifiers such as ``exclude`` and ``partial`` in the generated schema definition. If a schema is initialized with modifiers, apispec will treat each combination of modifiers as a unique schema definition.

Custom Fields
***************
Expand Down
11 changes: 4 additions & 7 deletions docs/writing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,9 @@ A plugin with a path helper function may look something like this:
The ``init_spec`` Method
------------------------

`BasePlugin` has an `init_spec` method that `APISpec` calls on each plugin at initialization with the spec object itself as parameter. It is no-op by default, but a plugin may override it to access and store useful information from the spec object.

A typical use case is conditional code depending on the OpenAPI version, which is stored as ``openapi_version`` attribute of the spec object. See source code for `apispec.ext.marshmallow.MarshmallowPlugin </_modules/apispec/ext/marshmallow.html>`_ for an example.

Note that the OpenAPI version is stored in the spec object as an `apispec.utils.OpenAPIVersion`. An ``OpenAPIVersion`` instance provides the version as string as well as shortcuts to version digits.
`BasePlugin` has an `init_spec` method that `APISpec` calls on each plugin at initialization with the spec object itself as parameter. It is no-op by default, but a plugin may override it to access and store useful information on the spec object.

A typical use case is conditional code depending on the OpenAPI version, which is stored as ``openapi_version`` on the `spec` object. See source code for `apispec.ext.marshmallow.MarshmallowPlugin </_modules/apispec/ext/marshmallow.html>`_ for an example.

Example: Docstring-parsing Plugin
---------------------------------
Expand Down Expand Up @@ -98,7 +95,7 @@ To use the plugin:
200:
content:
application/json:
schema: '#/definitions/Gist'
schema: '#/definitions/Gist'
"""
pass

Expand All @@ -111,7 +108,7 @@ To use the plugin:
Next Steps
----------

To learn more about how to write plugins
To learn more about how to write plugins:

* Consult the :doc:`Core API docs <api_core>` for `BasePlugin <apispec.BasePlugin>`
* View the source for an existing apispec plugin, e.g. `FlaskPlugin <https://github.com/marshmallow-code/apispec-webframeworks/blob/master/apispec_webframeworks/flask.py>`_.
Expand Down
Loading