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

fix: flask import of _endpoint_from_view_func - issue #567 #572

Merged
merged 7 commits into from
Dec 10, 2023
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
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ Releases prior to 0.3.0 were “best effort” filled out, but are missing
some info. If you see your contribution missing info, please open a PR
on the Changelog!

.. _section-1.2.1:
1.2.1
-----
.. _bug_fixes-1.2.1
Bug Fixes
~~~~~~~~~

::

* Fixing flask 3.0+ compatibility of `ModuleNotFoundError: No module named 'flask.scaffold'` Import error. (#567) [Ryu-CZ]


.. _section-1.2.0:
1.2.0
-----
Expand Down
11 changes: 5 additions & 6 deletions flask_restx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
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
Expand Down Expand Up @@ -45,10 +41,13 @@
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
Expand Down Expand Up @@ -850,7 +849,7 @@ def _blueprint_setup_add_url_rule_patch(
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"))
Expand Down
47 changes: 47 additions & 0 deletions flask_restx/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import re
import warnings
import typing

from collections import OrderedDict
from copy import deepcopy
Expand All @@ -17,9 +19,14 @@
"not_none",
"not_none_sorted",
"unpack",
"import_check_view_func",
)


class FlaskCompatibilityWarning(DeprecationWarning):
pass


def merge(first, second):
"""
Recursively merges two dictionaries.
Expand Down Expand Up @@ -118,3 +125,43 @@
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

Check warning on line 154 in flask_restx/utils.py

View check run for this annotation

Codecov / codecov/patch

flask_restx/utils.py#L154

Added line #L154 was not covered by tests
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

Check warning on line 158 in flask_restx/utils.py

View check run for this annotation

Codecov / codecov/patch

flask_restx/utils.py#L157-L158

Added lines #L157 - L158 were not covered by tests
else:
warnings.simplefilter("once", FlaskCompatibilityWarning)
_endpoint_from_view_func = None
except ImportError:
warnings.simplefilter("once", FlaskCompatibilityWarning)
_endpoint_from_view_func = None

Check warning on line 164 in flask_restx/utils.py

View check run for this annotation

Codecov / codecov/patch

flask_restx/utils.py#L160-L164

Added lines #L160 - L164 were not covered by tests
if _endpoint_from_view_func is None:
_endpoint_from_view_func = to_view_name

Check warning on line 166 in flask_restx/utils.py

View check run for this annotation

Codecov / codecov/patch

flask_restx/utils.py#L166

Added line #L166 was not covered by tests
return _endpoint_from_view_func
14 changes: 14 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,17 @@ def test_value_headers_default_code(self):
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())
Loading