Skip to content

Commit b7d1ded

Browse files
Ryu-CZTomáš Trval
and
Tomáš Trval
authored
fix: flask import of _endpoint_from_view_func - issue #567 (#572)
* fix: flask import of _endpoint_from_view_func is now resolved new util function import_check_view_func #567 * modify: include new import_check_view_func in utils.__all__ to keep nice static checks * modify: rever changelog * fix: test for name match * modify: changelog - scafold import fix #567 --------- Co-authored-by: Tomáš Trval <[email protected]>
1 parent 3ea4ce1 commit b7d1ded

File tree

4 files changed

+78
-6
lines changed

4 files changed

+78
-6
lines changed

CHANGELOG.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ Releases prior to 0.3.0 were “best effort” filled out, but are missing
2525
some info. If you see your contribution missing info, please open a PR
2626
on the Changelog!
2727

28+
.. _section-1.2.1:
29+
1.2.1
30+
-----
31+
.. _bug_fixes-1.2.1
32+
Bug Fixes
33+
~~~~~~~~~
34+
35+
::
36+
37+
* Fixing flask 3.0+ compatibility of `ModuleNotFoundError: No module named 'flask.scaffold'` Import error. (#567) [Ryu-CZ]
38+
39+
2840
.. _section-1.2.0:
2941
1.2.0
3042
-----

flask_restx/api.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
from flask import url_for, request, current_app
1515
from flask import make_response as original_flask_make_response
1616

17-
try:
18-
from flask.helpers import _endpoint_from_view_func
19-
except ImportError:
20-
from flask.scaffold import _endpoint_from_view_func
2117
from flask.signals import got_request_exception
2218

2319
from jsonschema import RefResolver
@@ -45,10 +41,13 @@
4541
from .postman import PostmanCollectionV1
4642
from .resource import Resource
4743
from .swagger import Swagger
48-
from .utils import default_id, camel_to_dash, unpack
44+
from .utils import default_id, camel_to_dash, unpack, import_check_view_func
4945
from .representations import output_json
5046
from ._http import HTTPStatus
5147

48+
endpoint_from_view_func = import_check_view_func()
49+
50+
5251
RE_RULES = re.compile("(<.*>)")
5352

5453
# List headers that should never be handled by Flask-RESTX
@@ -850,7 +849,7 @@ def _blueprint_setup_add_url_rule_patch(
850849
rule = blueprint_setup.url_prefix + rule
851850
options.setdefault("subdomain", blueprint_setup.subdomain)
852851
if endpoint is None:
853-
endpoint = _endpoint_from_view_func(view_func)
852+
endpoint = endpoint_from_view_func(view_func)
854853
defaults = blueprint_setup.url_defaults
855854
if "defaults" in options:
856855
defaults = dict(defaults, **options.pop("defaults"))

flask_restx/utils.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import re
2+
import warnings
3+
import typing
24

35
from collections import OrderedDict
46
from copy import deepcopy
@@ -17,9 +19,14 @@
1719
"not_none",
1820
"not_none_sorted",
1921
"unpack",
22+
"import_check_view_func",
2023
)
2124

2225

26+
class FlaskCompatibilityWarning(DeprecationWarning):
27+
pass
28+
29+
2330
def merge(first, second):
2431
"""
2532
Recursively merges two dictionaries.
@@ -118,3 +125,43 @@ def unpack(response, default_code=HTTPStatus.OK):
118125
return data, code or default_code, headers
119126
else:
120127
raise ValueError("Too many response values")
128+
129+
130+
def to_view_name(view_func: typing.Callable) -> str:
131+
"""Helper that returns the default endpoint for a given
132+
function. This always is the function name.
133+
134+
Note: copy of simple flask internal helper
135+
"""
136+
assert view_func is not None, "expected view func if endpoint is not provided."
137+
return view_func.__name__
138+
139+
140+
def import_check_view_func():
141+
"""
142+
Resolve import flask _endpoint_from_view_func.
143+
144+
Show warning if function cannot be found and provide copy of last known implementation.
145+
146+
Note: This helper method exists because reoccurring problem with flask function, but
147+
actual method body remaining the same in each flask version.
148+
"""
149+
import importlib.metadata
150+
151+
flask_version = importlib.metadata.version("flask").split(".")
152+
try:
153+
if flask_version[0] == "1":
154+
from flask.helpers import _endpoint_from_view_func
155+
elif flask_version[0] == "2":
156+
from flask.scaffold import _endpoint_from_view_func
157+
elif flask_version[0] == "3":
158+
from flask.sansio.scaffold import _endpoint_from_view_func
159+
else:
160+
warnings.simplefilter("once", FlaskCompatibilityWarning)
161+
_endpoint_from_view_func = None
162+
except ImportError:
163+
warnings.simplefilter("once", FlaskCompatibilityWarning)
164+
_endpoint_from_view_func = None
165+
if _endpoint_from_view_func is None:
166+
_endpoint_from_view_func = to_view_name
167+
return _endpoint_from_view_func

tests/test_utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,17 @@ def test_value_headers_default_code(self):
9898
def test_too_many_values(self):
9999
with pytest.raises(ValueError):
100100
utils.unpack((None, None, None, None))
101+
102+
103+
class ToViewNameTest(object):
104+
def test_none(self):
105+
with pytest.raises(AssertionError):
106+
_ = utils.to_view_name(None)
107+
108+
def test_name(self):
109+
assert utils.to_view_name(self.test_none) == self.test_none.__name__
110+
111+
112+
class ImportCheckViewFuncTest(object):
113+
def test_callable(self):
114+
assert callable(utils.import_check_view_func())

0 commit comments

Comments
 (0)