Skip to content

Commit

Permalink
Merge branch 'master' into python_312
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-doggart committed Dec 10, 2023
2 parents 87e124a + 7216135 commit 7c685ec
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 20 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.8", "3.12"]
flask: ["<3.0.0", ">=3.0.0"]
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
Expand All @@ -26,6 +28,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "flask${{ matrix.flask }}"
pip install ".[test]"
- name: Test with inv
run: inv cover qa
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ 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 werkzeug 3 deprecated version import. Import is replaced by new style version check with importlib (#573) [Ryu-CZ]
* 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
18 changes: 5 additions & 13 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 All @@ -32,23 +28,19 @@
InternalServerError,
)

from werkzeug import __version__ as werkzeug_version

if werkzeug_version.split(".")[0] >= "2":
from werkzeug.wrappers import Response as BaseResponse
else:
from werkzeug.wrappers import BaseResponse

from . import apidoc
from .mask import ParseError, MaskError
from .namespace import Namespace
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, BaseResponse
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 +842,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
7 changes: 1 addition & 6 deletions flask_restx/resource.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
from flask import request
from flask.views import MethodView
from werkzeug import __version__ as werkzeug_version

if werkzeug_version.split(".")[0] >= "2":
from werkzeug.wrappers import Response as BaseResponse
else:
from werkzeug.wrappers import BaseResponse

from .model import ModelBase

from .utils import unpack
from .utils import unpack, BaseResponse


class Resource(MethodView):
Expand Down
66 changes: 66 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,33 @@
"not_none",
"not_none_sorted",
"unpack",
"BaseResponse",
"import_check_view_func",
)


def import_werkzeug_response():
"""Resolve `werkzeug` `Response` class import because
`BaseResponse` was renamed in version 2.* to `Response`"""
import importlib.metadata

werkzeug_major = int(importlib.metadata.version("werkzeug").split(".")[0])
if werkzeug_major < 2:
from werkzeug.wrappers import BaseResponse

return BaseResponse

from werkzeug.wrappers import Response

return Response


BaseResponse = import_werkzeug_response()

class FlaskCompatibilityWarning(DeprecationWarning):
pass


def merge(first, second):
"""
Recursively merges two dictionaries.
Expand Down Expand Up @@ -118,3 +144,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
19 changes: 19 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def test_recursions_with_empty(self):
assert utils.merge(a, b) == b


class UnpackImportResponse(object):
def test_import_werkzeug_response(self):
assert utils.import_werkzeug_response() != None


class CamelToDashTestCase(object):
def test_no_transform(self):
assert utils.camel_to_dash("test") == "test"
Expand Down Expand Up @@ -98,3 +103,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())
8 changes: 7 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
# and then run "tox" from this directory.

[tox]
envlist = py{38, 39, 310, 311, 312}, pypy3.8, doc
envlist =
py{38, 39, 310, 311}-flask2,
py{311, 312}-flask3
pypy3.8
doc

[testenv]
commands = {posargs:inv test qa}
deps =
flask2: flask<3.0.0
flask3: flask>=3.0.0
-r{toxinidir}/requirements/test.pip
-r{toxinidir}/requirements/develop.pip

Expand Down

0 comments on commit 7c685ec

Please sign in to comment.