Repository: apiflask/apiflask
Pull request #457 (2023-07-18 15:13:15)

Remove previously deprecated code

Checklist:

- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the `docs` folder and in code docstring.
- [x] Add an entry in `CHANGES.md` summarizing the change and linking to the issue.
- [x] Add `*Version changed*` or `*Version added*` note in any relevant docs and docstring.
- [x] Run `pytest` and `tox`, no tests failed.



Patch:
diff --git a/CHANGES.md b/CHANGES.md
index d82ef75..3b82a5d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -11,6 +11,10 @@ Released: -<br>Codename: Gongqing
   value of `arg_name` ([issue #427][issue_427]).
 - Add `FileSchema` to generate OpenAPI file response ([issue #447][issue_447]).
 - Support using `{}` to represent not only empty body (204) but also empty schema.
+- Remove the previously deprecated code:
+  - The `tag` parameter in `@app.doc`.
+  - The `role` parameter in `@app.auth_required`.
+  - The `redoc_path` parameter in `apiflask.APIFlask` and the `/redoc` path.
 
 [issue_427]: https://github.com/apiflask/apiflask/issues/427
 [issue_442]: https://github.com/apiflask/apiflask/issues/442
diff --git a/docs/usage.md b/docs/usage.md
index 846935b..9259483 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -431,43 +431,6 @@ def delete_pet(pet_id):
     application.
 
 
-## Move to new API decorators
-
-From APIFlask 0.12, the four standalone API decorators (i.e. `@input`, `@output`,
-`@doc`, and `@auth_required`) were moved to `APIFlask` and `APIBlueprint` classes.
-Now access them with your application or blueprint instance:
-
-```python
-from apiflask import APIFlask
-
-app = APIFlask(__name__)
-
-@app.get('/')
-@app.input(Foo)
-@app.output(Bar)
-def hello():
-    return {'message': 'Hello'}
-```
-
-instead of:
-
-```python
-from apiflask import APIFlask
-
-app = APIFlask(__name__)
-
-@app.get('/')
-@input(Foo)
-@output(Bar)
-def hello():
-    return {'message': 'Hello'}
-```
-
-The old standalone decorators were deprecated since 0.12, and will be removed in the
-1.0 version. Notice all the usage in the docs are updated, you may want to
-[upgrade APIFlask](/changelog/) to update the usage.
-
-
 ## Use `@app.input` to validate and deserialize request data
 
 To validate and deserialize a request body or request query parameters, we need to
diff --git a/src/apiflask/app.py b/src/apiflask/app.py
index db62a88..0e8324e 100644
--- a/src/apiflask/app.py
+++ b/src/apiflask/app.py
@@ -48,7 +48,6 @@ from .openapi import get_path_summary
 from .openapi import get_auth_name
 from .openapi import get_argument
 from .openapi import get_security_and_security_schemes
-from .ui_templates import redoc_template
 from .ui_templates import ui_templates
 from .ui_templates import swagger_ui_oauth2_redirect_template
 from .scaffold import APIScaffold
@@ -266,7 +265,6 @@ class APIFlask(APIScaffold, Flask):
         docs_path: t.Optional[str] = '/docs',
         docs_oauth2_redirect_path: t.Optional[str] = '/docs/oauth2-redirect',
         docs_ui: str = 'swagger-ui',
-        redoc_path: t.Optional[str] = '/redoc',
         openapi_blueprint_url_prefix: t.Optional[str] = None,
         json_errors: bool = True,
         enable_openapi: bool = True,
@@ -297,8 +295,6 @@ class APIFlask(APIScaffold, Flask):
             docs_ui: The UI of API documentation, one of `swagger-ui` (default), `redoc`,
                 `elements`, `rapidoc`, and `rapipdf`.
             docs_oauth2_redirect_path: The path to Swagger UI OAuth redirect.
-            redoc_path: Deprecated since 1.1, set `APIFlask(docs_ui='redoc')` to use Redoc.
-                The path to Redoc documentation, defaults to `/redoc`.
             openapi_blueprint_url_prefix: The url prefix of the OpenAPI blueprint. This
                 prefix will append before all the OpenAPI-related paths (`sepc_path`,
                 `docs_path`, etc.), defaults to `None`.
@@ -310,6 +306,10 @@ class APIFlask(APIScaffold, Flask):
 
         Other keyword arguments are directly passed to `flask.Flask`.
 
+        *Version changed: 2.0.0*
+
+        - Remove the deprecated `redoc_path` parameter.
+
         *Version changed: 1.2.0*
 
         - Add `spec_plugins` parameter.
@@ -343,7 +343,6 @@ class APIFlask(APIScaffold, Flask):
         self.spec_path = spec_path
         self.docs_ui = docs_ui
         self.docs_path = docs_path
-        self.redoc_path = redoc_path
         self.docs_oauth2_redirect_path = docs_oauth2_redirect_path
         self.openapi_blueprint_url_prefix = openapi_blueprint_url_prefix
         self.enable_openapi = enable_openapi
@@ -557,24 +556,8 @@ class APIFlask(APIScaffold, Flask):
                     def swagger_ui_oauth_redirect() -> str:
                         return render_template_string(swagger_ui_oauth2_redirect_template)
 
-        if self.redoc_path:
-            @bp.route(self.redoc_path)
-            def redoc():
-                warnings.warn(
-                    'The `/redoc` path and `redoc_path` parameter is deprecated '
-                    'and will be removed in 2.0, Set `APIFlask(docs_ui="redoc")` '
-                    'to use Redoc and then visit "/docs" instead.',
-                    UserWarning,
-                    stacklevel=2,
-                )
-                return render_template_string(
-                    redoc_template,
-                    title=self.title,
-                    version=self.version
-                )
-
         if self.enable_openapi and (
-            self.spec_path or self.docs_path or self.redoc_path
+            self.spec_path or self.docs_path
         ):
             self.register_blueprint(bp)
 
diff --git a/src/apiflask/scaffold.py b/src/apiflask/scaffold.py
index 61128da..a364547 100644
--- a/src/apiflask/scaffold.py
+++ b/src/apiflask/scaffold.py
@@ -1,5 +1,4 @@
 import typing as t
-import warnings
 from collections.abc import Mapping as ABCMapping
 from functools import wraps
 
@@ -144,7 +143,6 @@ class APIScaffold:
     def auth_required(
         self,
         auth: HTTPAuthType,
-        role: t.Optional[str] = None,
         roles: t.Optional[list] = None,
         optional: t.Optional[str] = None
     ) -> t.Callable[[DecoratedType], DecoratedType]:
@@ -171,7 +169,6 @@ class APIScaffold:
             auth: The `auth` object, an instance of
                 [`HTTPBasicAuth`][apiflask.security.HTTPBasicAuth]
                 or [`HTTPTokenAuth`][apiflask.security.HTTPTokenAuth].
-            role: Deprecated since 1.0, use `roles` instead.
             roles: The selected roles to allow to visit this view, accepts a list of role names.
                 See [Flask-HTTPAuth's documentation][_role]{target:_blank} for more details.
                 [_role]: https://flask-httpauth.readthedocs.io/en/latest/#user-roles
@@ -179,6 +176,10 @@ class APIScaffold:
                 information is not included with the request, in which case the attribute
                 `auth.current_user` will be `None`.
 
+        *Version changed: 2.0.0*
+
+        - Remove the deprecated `role` parameter.
+
         *Version changed: 1.0.0*
 
         - The `role` parameter is deprecated.
@@ -191,22 +192,10 @@ class APIScaffold:
 
         - Add parameter `roles`.
         """
-        _roles = None
-        if role is not None:
-            warnings.warn(
-                'The `role` parameter is deprecated and will be removed in 1.1, '
-                'use `roles` and always pass a list instead.',
-                DeprecationWarning,
-                stacklevel=3,
-            )
-            _roles = [role]
-        elif roles is not None:
-            _roles = roles
-
         def decorator(f):
             f = _ensure_sync(f)
-            _annotate(f, auth=auth, roles=_roles or [])
-            return auth.login_required(role=_roles, optional=optional)(f)
+            _annotate(f, auth=auth, roles=roles or [])
+            return auth.login_required(role=roles, optional=optional)(f)
         return decorator
 
     def input(
@@ -525,7 +514,6 @@ class APIScaffold:
         self,
         summary: t.Optional[str] = None,
         description: t.Optional[str] = None,
-        tag: t.Optional[str] = None,
         tags: t.Optional[t.List[str]] = None,
         responses: t.Optional[t.Union[t.List[int], t.Dict[int, str]]] = None,
         deprecated: t.Optional[bool] = None,
@@ -563,7 +551,6 @@ class APIScaffold:
 
             description: The description of this endpoint. If not set, the lines after the empty
                 line of the docstring will be used.
-            tag: Deprecated since 1.0, use `tags` instead.
             tags: A list of tag names of this endpoint, map the tags you passed in the `app.tags`
                 attribute. If `app.tags` is not set, the blueprint name will be used as tag name.
             responses: The other responses for this view function, accepts a dict in a format
@@ -580,6 +567,10 @@ class APIScaffold:
                 pass a security name (equals to `[{'foo': []}]`) or a list of security names (equals
                 to `[{'foo': []}, {'bar': []}]`).
 
+        *Version changed: 2.0.0*
+
+        - Remove the deprecated `tag` parameter.
+
         *Version changed: 1.0*
 
         - Add `security` parameter to support customizing security info.
@@ -608,25 +599,13 @@ class APIScaffold:
 
         *Version added: 0.2.0*
         """
-        _tags = None
-        if tag is not None:
-            warnings.warn(
-                'The `tag` parameter is deprecated and will be removed in 1.1, '
-                'use `tags` and always pass a list instead.',
-                DeprecationWarning,
-                stacklevel=2,
-            )
-            _tags = [tag]
-        elif tags is not None:
-            _tags = tags
-
         def decorator(f):
             f = _ensure_sync(f)
             _annotate(
                 f,
                 summary=summary,
                 description=description,
-                tags=_tags,
+                tags=tags,
                 responses=responses,
                 deprecated=deprecated,
                 hide=hide,


Test patch:
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index 205795f..d1f7a40 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -1,30 +1,4 @@
 from apiflask import APIBlueprint
-from apiflask import HTTPBasicAuth
-
-
-def test_deprecation_warning_for_role(recwarn, app, client):
-    auth = HTTPBasicAuth()
-
-    @app.get('/foo')
-    @app.auth_required(auth, role='admin')
-    def foo():
-        pass
-
-    client.get('/foo')
-    w = recwarn.pop(DeprecationWarning)
-    assert 'The `role` parameter is deprecated and will be removed in 1.1,' in str(w)
-
-
-def test_deprecation_warning_for_tag(recwarn, app, client):
-
-    @app.get('/foo')
-    @app.doc(tag='foo')
-    def foo():
-        pass
-
-    client.get('/foo')
-    w = recwarn.pop(DeprecationWarning)
-    assert 'The `tag` parameter is deprecated and will be removed in 1.1,' in str(w)
 
 
 def test_app_decorators(app):
diff --git a/tests/test_openapi_basic.py b/tests/test_openapi_basic.py
index 4a5f287..f59f6c5 100644
--- a/tests/test_openapi_basic.py
+++ b/tests/test_openapi_basic.py
@@ -90,7 +90,6 @@ def test_spec_bypass_endpoints(app):
     assert '/foo/static' not in spec['paths']
     assert '/docs' not in spec['paths']
     assert '/openapi.json' not in spec['paths']
-    assert '/redoc' not in spec['paths']
     assert '/docs/oauth2-redirect' not in spec['paths']
 
 
diff --git a/tests/test_openapi_blueprint.py b/tests/test_openapi_blueprint.py
index 753d9b3..5ade7e4 100644
--- a/tests/test_openapi_blueprint.py
+++ b/tests/test_openapi_blueprint.py
@@ -7,13 +7,12 @@ def test_openapi_blueprint(app):
     assert 'openapi' in app.blueprints
     rules = list(app.url_map.iter_rules())
     bp_endpoints = [rule.endpoint for rule in rules if rule.endpoint.startswith('openapi')]
-    assert len(bp_endpoints) == 4
+    assert len(bp_endpoints) == 3
     assert 'openapi.spec' in bp_endpoints
     assert 'openapi.docs' in bp_endpoints
     assert 'openapi.swagger_ui_oauth_redirect' in bp_endpoints
-    assert 'openapi.redoc' in bp_endpoints
 
-    app = APIFlask(__name__, spec_path=None, docs_path=None, redoc_path=None)
+    app = APIFlask(__name__, spec_path=None, docs_path=None)
     assert 'openapi' not in app.blueprints
 
 
@@ -25,7 +24,7 @@ def test_spec_path(app):
     assert 'openapi' in app.blueprints
     rules = list(app.url_map.iter_rules())
     bp_endpoints = [rule.endpoint for rule in rules if rule.endpoint.startswith('openapi')]
-    assert len(bp_endpoints) == 3
+    assert len(bp_endpoints) == 2
     assert 'openapi.spec' not in bp_endpoints
 
 
@@ -49,7 +48,7 @@ def test_docs_path(app):
 
     rules = list(app.url_map.iter_rules())
     bp_endpoints = [rule.endpoint for rule in rules if rule.endpoint.startswith('openapi')]
-    assert len(bp_endpoints) == 2
+    assert len(bp_endpoints) == 1
     assert 'openapi.docs' not in bp_endpoints
     assert 'openapi.swagger_ui_oauth_redirect' not in bp_endpoints
 
@@ -75,7 +74,7 @@ def test_docs_oauth2_redirect_path(client):
 
     rules = list(app.url_map.iter_rules())
     bp_endpoints = [rule.endpoint for rule in rules if rule.endpoint.startswith('openapi')]
-    assert len(bp_endpoints) == 3
+    assert len(bp_endpoints) == 2
     assert 'openapi.docs' in bp_endpoints
     assert 'openapi.swagger_ui_oauth_redirect' not in bp_endpoints
     rv = app.test_client().get('/docs')
@@ -83,18 +82,6 @@ def test_docs_oauth2_redirect_path(client):
     assert b'oauth2RedirectUrl' not in rv.data
 
 
-def test_redoc_path(app):
-    assert app.redoc_path
-
-    app = APIFlask(__name__, redoc_path=None)
-    assert app.redoc_path is None
-
-    rules = list(app.url_map.iter_rules())
-    bp_endpoints = [rule.endpoint for rule in rules if rule.endpoint.startswith('openapi')]
-    assert len(bp_endpoints) == 3
-    assert 'openapi.redoc' not in bp_endpoints
-
-
 def test_disable_openapi_with_enable_openapi_arg(app):
     assert app.enable_openapi
 
diff --git a/tests/test_settings_api_docs.py b/tests/test_settings_api_docs.py
index 258d854..f872d31 100644
--- a/tests/test_settings_api_docs.py
+++ b/tests/test_settings_api_docs.py
@@ -12,18 +12,22 @@ def test_docs_favicon(app, client):
 
 
 @pytest.mark.parametrize('config_value', [True, False])
-def test_docs_use_google_font(app, client, config_value):
+def test_docs_use_google_font(client, config_value):
+    app = APIFlask(__name__, docs_ui='redoc')
     app.config['REDOC_USE_GOOGLE_FONT'] = config_value
+    client = app.test_client()
 
-    rv = client.get('/redoc')
+    rv = client.get('/docs')
     assert rv.status_code == 200
     assert bool(b'fonts.googleapis.com' in rv.data) is config_value
 
 
-def test_redoc_standalone_js(app, client):
+def test_redoc_standalone_js(client):
+    app = APIFlask(__name__, docs_ui='redoc')
     app.config['REDOC_STANDALONE_JS'] = 'https://cdn.example.com/redoc.js'
+    client = app.test_client()
 
-    rv = client.get('/redoc')
+    rv = client.get('/docs')
     assert rv.status_code == 200
     assert b'src="https://cdn.example.com/redoc.js"' in rv.data
 
@@ -31,10 +35,12 @@ def test_redoc_standalone_js(app, client):
 @pytest.mark.parametrize('config_value', [
     {}, {'disableSearch': True, 'hideLoading': True}
 ])
-def test_redoc_config(app, client, config_value):
+def test_redoc_config(client, config_value):
+    app = APIFlask(__name__, docs_ui='redoc')
     app.config['REDOC_CONFIG'] = config_value
+    client = app.test_client()
 
-    rv = client.get('/redoc')
+    rv = client.get('/docs')
     assert rv.status_code == 200
     if config_value == {}:
         assert b'{},' in rv.data