Skip to content

Commit

Permalink
refactor: removes deprecated OpenAPIController (#3360)
Browse files Browse the repository at this point in the history
* refactor: removes deprecated OpenAPIController

This PR removes all deprecated elements of OpenAPIConfig and the OpenAPIController, removes any obsolete tests and refactors tests that were parametrized to test both OpenAPIController and the router-based approach.

* docs: What's new entry

* Update docs/usage/openapi/ui_plugins.rst

Co-authored-by: Jacob Coffee <[email protected]>

* Update litestar/openapi/config.py

* fix: remove whitespace

* fix: import table formatting

(i hope)

---------

Co-authored-by: Jacob Coffee <[email protected]>
  • Loading branch information
2 people authored and provinzkraut committed Apr 13, 2024
1 parent 29b4196 commit 5bf9352
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 1,281 deletions.
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@
(PY_CLASS, "litestar.template.base.TemplateType_co"),
(PY_CLASS, "litestar.template.base.ContextType_co"),
(PY_CLASS, "litestar.template.base.R"),
(PY_ATTR, "litestar.openapi.controller.OpenAPIController.swagger_ui_init_oauth"),
# intentionally undocumented
(PY_CLASS, "BacklogStrategy"),
(PY_CLASS, "ExceptionT"),
Expand Down Expand Up @@ -191,6 +190,8 @@
("py:exc", "HTTPExceptions"),
(PY_CLASS, "litestar.template.Template"),
(PY_CLASS, "litestar.middleware.compression.gzip_facade.GzipCompression"),
(PY_CLASS, "litestar.openapi.OpenAPIController"),
(PY_CLASS, "openapi.controller.OpenAPIController"),
]

nitpick_ignore_regex = [
Expand Down Expand Up @@ -229,7 +230,6 @@
# No idea what autodoc is doing here. Possibly unfixable on our end
"litestar.template.base.TemplateEngineProtocol.get_template": {"litestar.template.base.T_co"},
"litestar.template": {"litestar.template.base.T_co"},
"litestar.openapi.OpenAPIController.security": {"SecurityRequirement"},
"litestar.response.file.async_file_iterator": {"FileSystemAdapter"},
"advanced_alchemy._listeners.touch_updated_timestamp": {"Session"},
re.compile("litestar.response.redirect.*"): {"RedirectStatusType"},
Expand Down
46 changes: 45 additions & 1 deletion docs/release-notes/whats-new-3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Imports
+====================================================+========================================================================+
| **SECTION** |
+----------------------------------------------------+------------------------------------------------------------------------+
+ Put your shit here from v2 | Put your shit here from v3 |
| Put your shit here from v2 | Put your shit here from v3 |
+----------------------------------------------------+------------------------------------------------------------------------+


Expand Down Expand Up @@ -61,3 +61,47 @@ must explicitly set it:
@get("/")
def my_handler(param: int | None = None) -> ...:
...
OpenAPI Controller Replaced by Plugins
--------------------------------------

In version 3.0, the OpenAPI controller pattern, deprecated in v2.8, has been removed in
favor of a more flexible plugin system.

Elimination of ``OpenAPIController`` Subclassing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Previously, users configured elements such as the root path and styling by subclassing OpenAPIController and setting it
on the ``OpenAPIConfig.openapi_controller`` attribute. As of version 3.0, this pattern has been removed. Instead, users
are required to transition to using UI plugins for configuration.

Migration Steps:

1. Remove any implementations subclassing ``OpenAPIController``.
2. Use the :attr:`OpenAPIConfig.render_plugins` attribute to configure the OpenAPI UI made available to your users.
If no plugin is supplied, we automatically add the :class:`ScalarRenderPlugin` for the default configuration.
3. Use the :attr:`OpenAPIConfig.openapi_router` attribute for additional configuration.

See the :doc:`/usage/openapi/ui_plugins` documentation for more information on how to configure OpenAPI plugins.

Changes to Endpoint Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The ``OpenAPIConfig.enabled_endpoints`` attribute is no longer available in version 3.0.0. This attribute previously
enabled a set of endpoints that would serve different OpenAPI UIs. In the new version, only the ``openapi.json``
endpoint is enabled by default, alongside the ``Scalar`` UI plugin as the default.

To adapt to this change, you should explicitly configure any additional endpoints you need by properly setting up the
necessary plugins within the :attr:`OpenAPIConfig.render_plugins` parameter.

Modification to ``root_schema_site`` Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The ``root_schema_site`` attribute, which enabled serving a particular UI at the OpenAPI root path, has been removed in
version 3.0. The new approach automatically assigns the first :class:`OpenAPIRenderPlugin` listed in the
:attr:`OpenAPIConfig.render_plugins` list to serve at the ``/schema`` endpoint, unless a plugin has been defined with
the root path (``/``), in which case that plugin will be used.

For those previously using the ``root_schema_site`` attribute, the migration involves ensuring that the UI intended to
be served at the ``/schema`` endpoint is the first plugin listed in the :attr:`OpenAPIConfig.render_plugins`.
57 changes: 3 additions & 54 deletions docs/usage/openapi/ui_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,57 +139,6 @@ In the following example, we configure the OpenAPI root path to be ``/docs``:
This will result in any of the OpenAPI endpoints being served at ``/docs`` instead of ``/schema``, e.g.,
``/docs/openapi.json``.

Backward Compatibility
----------------------

OpenAPI UI plugins are a new feature introduced in ``v2.8.0``.

Providing a subclass of OpenAPIController
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: v2.8.0

The previous method of configuring elements such as the root path and styling was to subclass
:class:`OpenAPIController`, and set it on the :attr:`OpenAPIConfig.openapi_controller` attribute. This approach is now
deprecated and slated for removal in ``v3.0.0``, but if you are using it, there should be no change in behavior.

To maintain backward compatibility with the previous approach, if neither the :attr:`OpenAPIConfig.openapi_controller`
or :attr:`OpenAPIConfig.render_plugins` attributes are set, we will automatically add the plugins to respect the also
deprecated :attr:`OpenAPIConfig.enabled_endpoints` attribute. By default, this will result in the following endpoints
being enabled:

- ``/schema/openapi.json``
- ``/schema/redoc``
- ``/schema/rapidoc``
- ``/schema/elements``
- ``/schema/swagger``
- ``/schema/openapi.yml``
- ``/schema/openapi.yaml``

In ``v3.0.0``, the :attr:`OpenAPIConfig.enabled_endpoints` attribute will be removed, and only a single UI plugin will be
enabled by default, in addition to the ``openapi.json`` endpoint which will always be enabled. ``Scalar`` will also
become the default UI plugin in ``v3.0.0``.

To adopt the future behavior, explicitly set the :attr:`OpenAPIConfig.render_plugins` field to an instance of
:class:`ScalarRenderPlugin`:

.. literalinclude:: /examples/openapi/plugins/scalar_simple.py
:language: python
:lines: 13-21

Backward compatibility with ``root_schema_site``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Litestar has always supported a ``root_schema_site`` attribute on the :class:`OpenAPIConfig` class. This attribute
allows you to elect to serve a UI at the OpenAPI root path, e.g., by default ``redoc`` would be served at both
``/schema`` and ``/schema/redoc``.

In ``v3.0.0``, the ``root_schema_site`` attribute will be removed, and the first :class:`OpenAPIRenderPlugin` in the
:attr:`OpenAPIConfig.render_plugins` list will be assigned to the ``/schema`` endpoint.

As of ``v2.8.0``, if you explicitly use the new :attr:`OpenAPIConfig.render_plugins` attribute, you will be
automatically opted in to the new behavior, and the ``root_schema_site`` attribute will be ignored.

Building your own OpenAPI UI Plugin
-----------------------------------

Expand Down Expand Up @@ -277,9 +226,9 @@ This can be used for a variety of purposes, including adding additional routes t
OAuth2 in Swagger UI
--------------------

When using Swagger, OAuth2 settings can be configured via
:attr:`swagger_ui_init_oauth <litestar.openapi.controller.OpenAPIController.swagger_ui_init_oauth>`, which can be set to
a dictionary containing the parameters described in the Swagger UI documentation for OAuth2
When using Swagger, OAuth2 settings can be configured via the :paramref:`~.openapi.plugins.SwaggerRenderPlugin.init_oauth` param of
:meth:`SwaggerRenderPlugin <litestar.openapi.plugins.SwaggerRenderPlugin.__init__>`, which can be set to a dictionary
containing the parameters described in the Swagger UI documentation for OAuth2
`here <https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/>`_.

With that, you can preset your clientId or enable PKCE support.
Expand Down
5 changes: 1 addition & 4 deletions litestar/_openapi/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,7 @@ def _handler(request: Request) -> bytes:
def on_app_init(self, app_config: AppConfig) -> AppConfig:
if app_config.openapi_config:
self._openapi_config = app_config.openapi_config
if (controller := app_config.openapi_config.openapi_controller) is not None:
app_config.route_handlers.append(controller)
else:
app_config.route_handlers.append(self.create_openapi_router())
app_config.route_handlers.append(self.create_openapi_router())
return app_config

@property
Expand Down
12 changes: 2 additions & 10 deletions litestar/cli/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,7 @@ def show_app_info(app: Litestar) -> None: # pragma: no cover

openapi_enabled = _format_is_enabled(app.openapi_config)
if app.openapi_config:
path = (
app.openapi_config.openapi_controller.path
if app.openapi_config.openapi_controller
else app.openapi_config.path or "/schema"
)
path = app.openapi_config.get_path()
openapi_enabled += f" path=[yellow]{path}"
table.add_row("OpenAPI", openapi_enabled)

Expand Down Expand Up @@ -534,11 +530,7 @@ def remove_routes_with_patterns(
def remove_default_schema_routes(
routes: list[HTTPRoute | ASGIRoute | WebSocketRoute], openapi_config: OpenAPIConfig
) -> list[HTTPRoute | ASGIRoute | WebSocketRoute]:
schema_path = (
(openapi_config.path or "/schema")
if openapi_config.openapi_controller is None
else openapi_config.openapi_controller.path
)
schema_path = openapi_config.path if openapi_config.openapi_router is None else openapi_config.openapi_router.path
return remove_routes_with_patterns(routes, (schema_path,))


Expand Down
3 changes: 1 addition & 2 deletions litestar/openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .config import OpenAPIConfig
from .controller import OpenAPIController
from .datastructures import ResponseSpec

__all__ = ("OpenAPIController", "OpenAPIConfig", "ResponseSpec")
__all__ = ("OpenAPIConfig", "ResponseSpec")
148 changes: 21 additions & 127 deletions litestar/openapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@

from copy import deepcopy
from dataclasses import dataclass, field, fields
from typing import TYPE_CHECKING, Final, Literal, Sequence
from typing import TYPE_CHECKING, Sequence

from litestar._openapi.utils import default_operation_id_creator
from litestar.openapi.plugins import (
JsonRenderPlugin,
RapidocRenderPlugin,
RedocRenderPlugin,
StoplightRenderPlugin,
SwaggerRenderPlugin,
YamlRenderPlugin,
)
from litestar.openapi.plugins import ScalarRenderPlugin
from litestar.openapi.spec import (
Components,
Contact,
Expand All @@ -26,30 +19,15 @@
Server,
Tag,
)
from litestar.utils.deprecation import warn_deprecation
from litestar.utils.path import normalize_path

if TYPE_CHECKING:
from litestar.openapi.controller import OpenAPIController
from litestar.openapi.plugins import OpenAPIRenderPlugin
from litestar.router import Router
from litestar.types.callable_types import OperationIDCreator

__all__ = ("OpenAPIConfig",)

_enabled_plugin_map = {
"elements": StoplightRenderPlugin,
"openapi.json": JsonRenderPlugin,
"openapi.yaml": YamlRenderPlugin,
"openapi.yml": YamlRenderPlugin,
"rapidoc": RapidocRenderPlugin,
"redoc": RedocRenderPlugin,
"swagger": SwaggerRenderPlugin,
"oauth2-redirect.html": None,
}

_DEFAULT_SCHEMA_SITE: Final = "redoc"


@dataclass
class OpenAPIConfig:
Expand Down Expand Up @@ -110,130 +88,46 @@ class OpenAPIConfig:
"""
operation_id_creator: OperationIDCreator = default_operation_id_creator
"""A callable that generates unique operation ids"""
path: str | None = field(default=None)
path: str = "/schema"
"""Base path for the OpenAPI documentation endpoints.
If no path is provided the default is ``/schema``.
Ignored if :attr:`openapi_router` is provided.
"""
render_plugins: Sequence[OpenAPIRenderPlugin] = field(default=())
"""Plugins for rendering OpenAPI documentation UIs."""
render_plugins: Sequence[OpenAPIRenderPlugin] = field(default=(ScalarRenderPlugin(),))
"""Plugins for rendering OpenAPI documentation UIs.
.. versionchanged:: 3.0.0
Default behavior changed to serve only :class:`ScalarRenderPlugin`.
"""
openapi_router: Router | None = None
"""An optional router for serving OpenAPI documentation and schema files.
If provided, ``path`` is ignored.
This parameter is also ignored if the deprecated :attr:`openapi_router <.openapi.OpenAPIConfig.openapi_controller>`
kwarg is provided.
:attr:`openapi_router` is not required, but it can be passed to customize the configuration of the router used to
serve the documentation endpoints. For example, you can add middleware or guards to the router.
Handlers to serve the OpenAPI schema and documentation sites are added to this router according to
:attr:`render_plugins`, so routes shouldn't be added that conflict with these.
"""
openapi_controller: type[OpenAPIController] | None = None
"""Controller for generating OpenAPI routes.
Must be subclass of :class:`OpenAPIController <litestar.openapi.controller.OpenAPIController>`.
.. deprecated:: v2.8.0
"""
root_schema_site: Literal["redoc", "swagger", "elements", "rapidoc"] | None = None
"""The static schema generator to use for the "root" path of ``/schema/``.
.. deprecated:: v2.8.0
"""
enabled_endpoints: set[str] | None = None
"""A set of the enabled documentation sites and schema download endpoints.
.. deprecated:: v2.8.0
"""

def __post_init__(self) -> None:
self._issue_deprecations()

self.root_schema_site = self.root_schema_site or _DEFAULT_SCHEMA_SITE

self.enabled_endpoints = (
set(_enabled_plugin_map.keys()) if self.enabled_endpoints is None else self.enabled_endpoints
)

if self.path:
self.path = normalize_path(self.path)

if self.path and self.openapi_controller is not None:
self.openapi_controller = type("OpenAPIController", (self.openapi_controller,), {"path": self.path})
self.path = normalize_path(self.path)

self.default_plugin: OpenAPIRenderPlugin | None = None
if self.openapi_controller is None:
if not self.render_plugins:
self._plugin_backward_compatibility()
else:
# user is implicitly opted into the future plugin-based OpenAPI implementation
# behavior by explicitly providing a list of render plugins
for plugin in self.render_plugins:
if plugin.has_path("/"):
self.default_plugin = plugin
break
else:
self.default_plugin = self.render_plugins[0]

def _issue_deprecations(self) -> None:
"""Handle deprecated config options."""
deprecated_in = "v2.8.0"
removed_in = "v3.0.0"
if self.openapi_controller is not None:
warn_deprecation(
deprecated_in,
"openapi_controller",
"attribute",
removal_in=removed_in,
alternative="render_plugins",
)

if self.root_schema_site is not None:
warn_deprecation(
deprecated_in,
"root_schema_site",
"attribute",
removal_in=removed_in,
alternative="render_plugins",
info="Any 'render_plugin' with path '/' or first 'render_plugin' in list will be served at the OpenAPI root.",
)

if self.enabled_endpoints is not None:
warn_deprecation(
deprecated_in,
"enabled_endpoints",
"attribute",
removal_in=removed_in,
alternative="render_plugins",
info="Configure a 'render_plugin' to enable an endpoint.",
)

def _plugin_backward_compatibility(self) -> None:
"""Backward compatibility for the plugin-based OpenAPI implementation.
This preserves backward compatibility with the Controller-based OpenAPI implementation.
We add a plugin for each enabled endpoint and set the default plugin to the plugin
that has a path ending in the value of ``root_schema_site``.
"""

def is_default_plugin(plugin_: OpenAPIRenderPlugin) -> bool:
"""Return True if the plugin is the default plugin."""
root_schema_site = self.root_schema_site or _DEFAULT_SCHEMA_SITE
return any(path.endswith(root_schema_site) for path in plugin_.paths)

self.render_plugins = rps = []
for key in self.enabled_endpoints or ():
if plugin_type := _enabled_plugin_map[key]:
plugin = plugin_type()
rps.append(plugin)
if is_default_plugin(plugin):
self.default_plugin = plugin
for plugin in self.render_plugins:
if plugin.has_path("/"):
self.default_plugin = plugin
break
else:
if self.render_plugins:
self.default_plugin = self.render_plugins[0]

def get_path(self) -> str:
return self.openapi_router.path if self.openapi_router else self.path

def to_openapi_schema(self) -> OpenAPI:
"""Return an ``OpenAPI`` instance from the values stored in ``self``.
Expand Down
Loading

0 comments on commit 5bf9352

Please sign in to comment.