https://github.com/python-restx/flask-restx/pull/572 --- flask_restx/api.py.orig 2023-10-22 12:20:40 UTC +++ flask_restx/api.py @@ -14,10 +14,6 @@ from flask import make_response as original_flask_make from flask import url_for, request, current_app from flask import make_response as original_flask_make_response -try: - from flask.helpers import _endpoint_from_view_func -except ImportError: - from flask.scaffold import _endpoint_from_view_func from flask.signals import got_request_exception from jsonschema import RefResolver @@ -45,10 +41,13 @@ from .swagger import Swagger from .postman import PostmanCollectionV1 from .resource import Resource from .swagger import Swagger -from .utils import default_id, camel_to_dash, unpack +from .utils import default_id, camel_to_dash, unpack, import_check_view_func from .representations import output_json from ._http import HTTPStatus +endpoint_from_view_func = import_check_view_func() + + RE_RULES = re.compile("(<.*>)") # List headers that should never be handled by Flask-RESTX @@ -850,7 +849,7 @@ class Api(object): rule = blueprint_setup.url_prefix + rule options.setdefault("subdomain", blueprint_setup.subdomain) if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) + endpoint = endpoint_from_view_func(view_func) defaults = blueprint_setup.url_defaults if "defaults" in options: defaults = dict(defaults, **options.pop("defaults")) --- flask_restx/utils.py.orig 2023-10-22 12:20:40 UTC +++ flask_restx/utils.py @@ -1,4 +1,6 @@ import re import re +import warnings +import typing from collections import OrderedDict from copy import deepcopy @@ -17,9 +19,14 @@ __all__ = ( "not_none", "not_none_sorted", "unpack", + "import_check_view_func", ) +class FlaskCompatibilityWarning(DeprecationWarning): + pass + + def merge(first, second): """ Recursively merges two dictionaries. @@ -118,3 +125,43 @@ def unpack(response, default_code=HTTPStatus.OK): return data, code or default_code, headers else: raise ValueError("Too many response values") + + +def to_view_name(view_func: typing.Callable) -> str: + """Helper that returns the default endpoint for a given + function. This always is the function name. + + Note: copy of simple flask internal helper + """ + assert view_func is not None, "expected view func if endpoint is not provided." + return view_func.__name__ + + +def import_check_view_func(): + """ + Resolve import flask _endpoint_from_view_func. + + Show warning if function cannot be found and provide copy of last known implementation. + + Note: This helper method exists because reoccurring problem with flask function, but + actual method body remaining the same in each flask version. + """ + import importlib.metadata + + flask_version = importlib.metadata.version("flask").split(".") + try: + if flask_version[0] == "1": + from flask.helpers import _endpoint_from_view_func + elif flask_version[0] == "2": + from flask.scaffold import _endpoint_from_view_func + elif flask_version[0] == "3": + from flask.sansio.scaffold import _endpoint_from_view_func + else: + warnings.simplefilter("once", FlaskCompatibilityWarning) + _endpoint_from_view_func = None + except ImportError: + warnings.simplefilter("once", FlaskCompatibilityWarning) + _endpoint_from_view_func = None + if _endpoint_from_view_func is None: + _endpoint_from_view_func = to_view_name + return _endpoint_from_view_func --- tests/test_utils.py.orig 2023-10-22 12:20:40 UTC +++ tests/test_utils.py @@ -98,3 +98,17 @@ class UnpackTest(object): def test_too_many_values(self): with pytest.raises(ValueError): utils.unpack((None, None, None, None)) + + +class ToViewNameTest(object): + def test_none(self): + with pytest.raises(AssertionError): + _ = utils.to_view_name(None) + + def test_name(self): + assert utils.to_view_name(self.test_none) == self.test_none.__name__ + + +class ImportCheckViewFuncTest(object): + def test_callable(self): + assert callable(utils.import_check_view_func())