From eb738d9570115af559b12cc8ffdd43cb656ccedb Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Sun, 7 Aug 2022 14:57:05 +0200 Subject: [PATCH 1/8] Switched from _request_ctx_stack.top to flask.g --- flask_jwt_extended/utils.py | 10 +++++----- flask_jwt_extended/view_decorators.py | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/flask_jwt_extended/utils.py b/flask_jwt_extended/utils.py index 4f1004a2..45a2f3aa 100644 --- a/flask_jwt_extended/utils.py +++ b/flask_jwt_extended/utils.py @@ -3,7 +3,7 @@ from typing import Optional import jwt -from flask import _request_ctx_stack +from flask import g from flask import Response from werkzeug.local import LocalProxy @@ -23,7 +23,7 @@ def get_jwt() -> dict: :return: The payload (claims) of the JWT in the current request """ - decoded_jwt = getattr(_request_ctx_stack.top, "jwt", None) + decoded_jwt = getattr(g, "_jwt_extended_jwt", None) if decoded_jwt is None: raise RuntimeError( "You must call `@jwt_required()` or `verify_jwt_in_request()` " @@ -41,7 +41,7 @@ def get_jwt_header() -> dict: :return: The headers of the JWT in the current request """ - decoded_header = getattr(_request_ctx_stack.top, "jwt_header", None) + decoded_header = getattr(g, "_jwt_extended_jwt_header", None) if decoded_header is None: raise RuntimeError( "You must call `@jwt_required()` or `verify_jwt_in_request()` " @@ -73,7 +73,7 @@ def get_jwt_request_location() -> Optional[str]: The location of the JWT in the current request; e.g., "cookies", "query-string", "headers", or "json" """ - return getattr(_request_ctx_stack.top, "jwt_location", None) + return getattr(g, "_jwt_extended_jwt_location", None) def get_current_user() -> Any: @@ -91,7 +91,7 @@ def get_current_user() -> Any: The current user object for the JWT in the current request """ get_jwt() # Raise an error if not in a decorated context - jwt_user_dict = getattr(_request_ctx_stack.top, "jwt_user", None) + jwt_user_dict = getattr(g, "_jwt_extended_jwt_user", None) if jwt_user_dict is None: raise RuntimeError( "You must provide a `@jwt.user_lookup_loader` callback to use " diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index 2c2bf826..da23f6d8 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -8,8 +8,8 @@ from typing import Tuple from typing import Union -from flask import _request_ctx_stack from flask import current_app +from flask import g from flask import request from werkzeug.exceptions import BadRequest @@ -86,8 +86,9 @@ def verify_jwt_in_request( return None # Should be impossible to hit, this makes mypy checks happy - if not _request_ctx_stack.top: # pragma: no cover - raise RuntimeError("No _request_ctx_stack.top present, aborting") + # TODO: Convert this check to the new `flask.g` object + # if not _request_ctx_stack.top: # pragma: no cover + # raise RuntimeError("No _request_ctx_stack.top present, aborting") try: jwt_data, jwt_header, jwt_location = _decode_jwt_from_request( @@ -97,18 +98,18 @@ def verify_jwt_in_request( except NoAuthorizationError: if not optional: raise - _request_ctx_stack.top.jwt = {} - _request_ctx_stack.top.jwt_header = {} - _request_ctx_stack.top.jwt_user = {"loaded_user": None} - _request_ctx_stack.top.jwt_location = None + g._jwt_extended_jwt = {} + g._jwt_extended_jwt_header = {} + g._jwt_extended_jwt_user = {"loaded_user": None} + g._jwt_extended_jwt_location = None return None # Save these at the very end so that they are only saved in the requet # context if the token is valid and all callbacks succeed - _request_ctx_stack.top.jwt_user = _load_user(jwt_header, jwt_data) - _request_ctx_stack.top.jwt_header = jwt_header - _request_ctx_stack.top.jwt = jwt_data - _request_ctx_stack.top.jwt_location = jwt_location + g._jwt_extended_jwt_user = _load_user(jwt_header, jwt_data) + g._jwt_extended_jwt_header = jwt_header + g._jwt_extended_jwt = jwt_data + g._jwt_extended_jwt_location = jwt_location return jwt_header, jwt_data From 4f18acae751806ac7ceb306a06517f382ce1b6df Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Sun, 7 Aug 2022 15:56:37 +0200 Subject: [PATCH 2/8] Use the old JSONEncoder from flask as a default Created a copy of the deprecated JSONEncoder used by flask and use this as a default for PyJWT. This might break for some installations which further extended the flask provided JSONEncoder. To get this working again the extension must be adjusted such that a custom Encoder can be configured. --- flask_jwt_extended/config.py | 31 +++++++++++++++++++++++++++++-- tests/test_config.py | 10 ---------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index 05322d8d..f0339b15 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -1,6 +1,8 @@ +import json from datetime import datetime from datetime import timedelta from datetime import timezone +from typing import Any from typing import Iterable from typing import List from typing import Optional @@ -9,12 +11,37 @@ from typing import Union from flask import current_app -from flask.json import JSONEncoder +from flask.json.provider import _default from jwt.algorithms import requires_cryptography from flask_jwt_extended.typing import ExpiresDelta +class JSONEncoder(json.JSONEncoder): + """The default JSON encoder. Handles extra types compared to the + built-in :class:`json.JSONEncoder`. + + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`decimal.Decimal` is serialized to a string. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. + + """ + + def default(self, o: Any) -> Any: + """Convert ``o`` to a JSON serializable type. See + :meth:`json.JSONEncoder.default`. Python does not support + overriding how basic types like ``str`` or ``list`` are + serialized, they are handled before this method. + """ + return _default(o) + + class _Config(object): """ Helper object for accessing and verifying options in this extension. This @@ -284,7 +311,7 @@ def error_msg_key(self) -> str: @property def json_encoder(self) -> Type[JSONEncoder]: - return current_app.json_encoder + return JSONEncoder @property def decode_audience(self) -> Union[str, Iterable[str]]: diff --git a/tests/test_config.py b/tests/test_config.py index 343d1e66..a5515ab8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,7 +3,6 @@ import pytest from dateutil.relativedelta import relativedelta from flask import Flask -from flask.json import JSONEncoder from flask_jwt_extended import JWTManager from flask_jwt_extended.config import config @@ -65,8 +64,6 @@ def test_default_configs(app): assert config.identity_claim_key == "sub" - assert config.json_encoder is app.json_encoder - assert config.error_msg_key == "msg" @@ -112,11 +109,6 @@ def test_override_configs(app, delta_func): app.config["JWT_ERROR_MESSAGE_KEY"] = "message" - class CustomJSONEncoder(JSONEncoder): - pass - - app.json_encoder = CustomJSONEncoder - with app.test_request_context(): assert config.token_location == ["cookies", "query_string", "json"] assert config.jwt_in_query_string is True @@ -162,8 +154,6 @@ class CustomJSONEncoder(JSONEncoder): assert config.identity_claim_key == "foo" - assert config.json_encoder is CustomJSONEncoder - assert config.error_msg_key == "message" From a21a336fe6464846ec87851eef511674688a5446 Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Fri, 12 Aug 2022 18:10:54 +0200 Subject: [PATCH 3/8] Moved JSONEncoder to separate file --- flask_jwt_extended/config.py | 29 +----------------------- flask_jwt_extended/json_encoder.py | 36 ++++++++++++++++++++++++++++++ flask_jwt_extended/tokens.py | 2 +- 3 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 flask_jwt_extended/json_encoder.py diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index f0339b15..37649432 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -1,8 +1,6 @@ -import json from datetime import datetime from datetime import timedelta from datetime import timezone -from typing import Any from typing import Iterable from typing import List from typing import Optional @@ -11,37 +9,12 @@ from typing import Union from flask import current_app -from flask.json.provider import _default from jwt.algorithms import requires_cryptography +from flask_jwt_extended.json_encoder import JSONEncoder from flask_jwt_extended.typing import ExpiresDelta -class JSONEncoder(json.JSONEncoder): - """The default JSON encoder. Handles extra types compared to the - built-in :class:`json.JSONEncoder`. - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`decimal.Decimal` is serialized to a string. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - - """ - - def default(self, o: Any) -> Any: - """Convert ``o`` to a JSON serializable type. See - :meth:`json.JSONEncoder.default`. Python does not support - overriding how basic types like ``str`` or ``list`` are - serialized, they are handled before this method. - """ - return _default(o) - - class _Config(object): """ Helper object for accessing and verifying options in this extension. This diff --git a/flask_jwt_extended/json_encoder.py b/flask_jwt_extended/json_encoder.py new file mode 100644 index 00000000..e8960662 --- /dev/null +++ b/flask_jwt_extended/json_encoder.py @@ -0,0 +1,36 @@ +import json +from typing import Any + + +try: + # Flask 2.2 deprecated the flask.json.JSONEncoder (see below), lets recreate + # a class with the same semantics as the old JSONEncoder. + + from flask.json.provider import DefaultJSONProvider + + class JSONEncoder(json.JSONEncoder): + """The default JSON encoder. Handles extra types compared to the + built-in :class:`json.JSONEncoder`. + + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`decimal.Decimal` is serialized to a string. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. + + """ + + def default(self, o: Any) -> Any: + """Convert ``o`` to a JSON serializable type. See + :meth:`json.JSONEncoder.default`. Python does not support + overriding how basic types like ``str`` or ``list`` are + serialized, they are handled before this method. + """ + return DefaultJSONProvider.default(o) + +except ModuleNotFoundError: # pragma: no cover + from flask.json import JSONEncoder # type: ignore # noqa: F401 diff --git a/flask_jwt_extended/tokens.py b/flask_jwt_extended/tokens.py index 260d665b..6897b93f 100644 --- a/flask_jwt_extended/tokens.py +++ b/flask_jwt_extended/tokens.py @@ -10,10 +10,10 @@ from typing import Union import jwt -from flask.json import JSONEncoder from flask_jwt_extended.exceptions import CSRFError from flask_jwt_extended.exceptions import JWTDecodeError +from flask_jwt_extended.json_encoder import JSONEncoder from flask_jwt_extended.typing import ExpiresDelta From fcea0592d9ed4b815d4088bb7df4bd8502da0c87 Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Fri, 12 Aug 2022 18:18:32 +0200 Subject: [PATCH 4/8] Switched from getattribute(g, ...) to g.get(...) --- flask_jwt_extended/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flask_jwt_extended/utils.py b/flask_jwt_extended/utils.py index 45a2f3aa..da01f5b2 100644 --- a/flask_jwt_extended/utils.py +++ b/flask_jwt_extended/utils.py @@ -23,7 +23,7 @@ def get_jwt() -> dict: :return: The payload (claims) of the JWT in the current request """ - decoded_jwt = getattr(g, "_jwt_extended_jwt", None) + decoded_jwt = g.get("_jwt_extended_jwt", None) if decoded_jwt is None: raise RuntimeError( "You must call `@jwt_required()` or `verify_jwt_in_request()` " @@ -41,7 +41,7 @@ def get_jwt_header() -> dict: :return: The headers of the JWT in the current request """ - decoded_header = getattr(g, "_jwt_extended_jwt_header", None) + decoded_header = g.get("_jwt_extended_jwt_header", None) if decoded_header is None: raise RuntimeError( "You must call `@jwt_required()` or `verify_jwt_in_request()` " @@ -73,7 +73,7 @@ def get_jwt_request_location() -> Optional[str]: The location of the JWT in the current request; e.g., "cookies", "query-string", "headers", or "json" """ - return getattr(g, "_jwt_extended_jwt_location", None) + return g.get("_jwt_extended_jwt_location", None) def get_current_user() -> Any: @@ -91,7 +91,7 @@ def get_current_user() -> Any: The current user object for the JWT in the current request """ get_jwt() # Raise an error if not in a decorated context - jwt_user_dict = getattr(g, "_jwt_extended_jwt_user", None) + jwt_user_dict = g.get("_jwt_extended_jwt_user", None) if jwt_user_dict is None: raise RuntimeError( "You must provide a `@jwt.user_lookup_loader` callback to use " From 3f8c4b643bcc04123c2789ae1a958d32c778569d Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Fri, 12 Aug 2022 22:40:09 +0200 Subject: [PATCH 5/8] Respect the json_provider_class Respect the json_provider_class when available and stay compatible with flask < 2.2. Extended tox configuration to also test with flask < 2.2. --- flask_jwt_extended/config.py | 5 ++-- flask_jwt_extended/internal_utils.py | 41 ++++++++++++++++++++++++++++ flask_jwt_extended/json_encoder.py | 36 ------------------------ flask_jwt_extended/tokens.py | 2 +- tox.ini | 2 +- 5 files changed, 46 insertions(+), 40 deletions(-) delete mode 100644 flask_jwt_extended/json_encoder.py diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index 37649432..9089b91f 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -1,6 +1,7 @@ from datetime import datetime from datetime import timedelta from datetime import timezone +from json import JSONEncoder from typing import Iterable from typing import List from typing import Optional @@ -11,7 +12,7 @@ from flask import current_app from jwt.algorithms import requires_cryptography -from flask_jwt_extended.json_encoder import JSONEncoder +from flask_jwt_extended.internal_utils import get_json_encoder from flask_jwt_extended.typing import ExpiresDelta @@ -284,7 +285,7 @@ def error_msg_key(self) -> str: @property def json_encoder(self) -> Type[JSONEncoder]: - return JSONEncoder + return get_json_encoder(current_app) @property def decode_audience(self) -> Union[str, Iterable[str]]: diff --git a/flask_jwt_extended/internal_utils.py b/flask_jwt_extended/internal_utils.py index 3220ff1c..e74e1db7 100644 --- a/flask_jwt_extended/internal_utils.py +++ b/flask_jwt_extended/internal_utils.py @@ -1,12 +1,25 @@ +import json from typing import Any +from typing import Type from typing import TYPE_CHECKING from flask import current_app +from flask import Flask from flask_jwt_extended.exceptions import RevokedTokenError from flask_jwt_extended.exceptions import UserClaimsVerificationError from flask_jwt_extended.exceptions import WrongTokenError +try: + from flask.json.provider import DefaultJSONProvider + + HAS_JSON_PROVIDER = True +except ModuleNotFoundError: # pragma: no cover + # The flask.json.provider module was added in Flask 2.2. + # Further details are handled in get_json_encoder. + HAS_JSON_PROVIDER = False + + if TYPE_CHECKING: # pragma: no cover from flask_jwt_extended import JWTManager @@ -51,3 +64,31 @@ def custom_verification_for_token(jwt_header: dict, jwt_data: dict) -> None: if not jwt_manager._token_verification_callback(jwt_header, jwt_data): error_msg = "User claims verification failed" raise UserClaimsVerificationError(error_msg, jwt_header, jwt_data) + + +def get_json_encoder(app: Flask) -> Type[json.JSONEncoder]: + """Get the JSON Encoder for the provided flask app + + Starting with flask version 2.2 the flask application provides a + interface to register a custom JSON Encoder/Decoder under the json_provider_class. + As this interface is not compatible with the standard JSONEncoder, the `default` + method of the class is wrapped. + + Lookup Order: + - app.json_encoder - For Flask < 2.2 + - app.json_provider_class.default + - flask.json.provider.DefaultJSONProvider.default + + """ + if not HAS_JSON_PROVIDER: # pragma: no cover + return app.json_encoder + + # If the registered JSON provider does not implement a default classmethod + # use the method defined by the DefaultJSONProvider + default = getattr(app.json_provider_class, "default", DefaultJSONProvider.default) + + class JSONEncoder(json.JSONEncoder): + def default(self, o: Any) -> Any: + return default(o) # pragma: no cover + + return JSONEncoder diff --git a/flask_jwt_extended/json_encoder.py b/flask_jwt_extended/json_encoder.py deleted file mode 100644 index e8960662..00000000 --- a/flask_jwt_extended/json_encoder.py +++ /dev/null @@ -1,36 +0,0 @@ -import json -from typing import Any - - -try: - # Flask 2.2 deprecated the flask.json.JSONEncoder (see below), lets recreate - # a class with the same semantics as the old JSONEncoder. - - from flask.json.provider import DefaultJSONProvider - - class JSONEncoder(json.JSONEncoder): - """The default JSON encoder. Handles extra types compared to the - built-in :class:`json.JSONEncoder`. - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`decimal.Decimal` is serialized to a string. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - - """ - - def default(self, o: Any) -> Any: - """Convert ``o`` to a JSON serializable type. See - :meth:`json.JSONEncoder.default`. Python does not support - overriding how basic types like ``str`` or ``list`` are - serialized, they are handled before this method. - """ - return DefaultJSONProvider.default(o) - -except ModuleNotFoundError: # pragma: no cover - from flask.json import JSONEncoder # type: ignore # noqa: F401 diff --git a/flask_jwt_extended/tokens.py b/flask_jwt_extended/tokens.py index 6897b93f..9a600a8e 100644 --- a/flask_jwt_extended/tokens.py +++ b/flask_jwt_extended/tokens.py @@ -3,6 +3,7 @@ from datetime import timedelta from datetime import timezone from hmac import compare_digest +from json import JSONEncoder from typing import Any from typing import Iterable from typing import List @@ -13,7 +14,6 @@ from flask_jwt_extended.exceptions import CSRFError from flask_jwt_extended.exceptions import JWTDecodeError -from flask_jwt_extended.json_encoder import JSONEncoder from flask_jwt_extended.typing import ExpiresDelta diff --git a/tox.ini b/tox.ini index 32c97fc8..bdf70cd0 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py37,py38,py39,py310,pypy3.9,mypy,coverage,style,docs +envlist = py{37,38,39,310}-{flask21,flask22},pypy3.9,mypy,coverage,style,docs [testenv] commands = From 48db40b7d341ce8548699e44b6848c778c51d200 Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Fri, 12 Aug 2022 22:53:48 +0200 Subject: [PATCH 6/8] fixed tox ini: Test against Flask < 2.2 and latest flask --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index bdf70cd0..50c26c36 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py{37,38,39,310}-{flask21,flask22},pypy3.9,mypy,coverage,style,docs +envlist = py{37,38,39,310}-{flask21,flask},pypy3.9,mypy,coverage,style,docs [testenv] commands = @@ -13,6 +13,8 @@ deps = pytest cryptography python-dateutil + flask21: Flask>=2.1,<2.2 + flask: Flask>=2.2 [testenv:mypy] commands = From 6076e2ce5d82eea4e18fb420302e6f767346345a Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Fri, 12 Aug 2022 22:54:55 +0200 Subject: [PATCH 7/8] Removed TODO including code in question The code in question was a runtime check which is no longer necesary. --- flask_jwt_extended/view_decorators.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/flask_jwt_extended/view_decorators.py b/flask_jwt_extended/view_decorators.py index da23f6d8..c87b01a9 100644 --- a/flask_jwt_extended/view_decorators.py +++ b/flask_jwt_extended/view_decorators.py @@ -85,11 +85,6 @@ def verify_jwt_in_request( if request.method in config.exempt_methods: return None - # Should be impossible to hit, this makes mypy checks happy - # TODO: Convert this check to the new `flask.g` object - # if not _request_ctx_stack.top: # pragma: no cover - # raise RuntimeError("No _request_ctx_stack.top present, aborting") - try: jwt_data, jwt_header, jwt_location = _decode_jwt_from_request( locations, fresh, refresh=refresh, verify_type=verify_type From ed86799d7b5bbd8b6c9115ba2558d6b28825faf6 Mon Sep 17 00:00:00 2001 From: "J. Rast" Date: Fri, 12 Aug 2022 23:31:44 +0200 Subject: [PATCH 8/8] Added tests and a more sane way for the custom JSONEncoder - Added tests to check the config.json_encoder - Don't create the JSONEncoder class on the fly, instead get the JSON provider class at runtime. --- flask_jwt_extended/internal_utils.py | 20 ++++++++++++-------- tests/test_config.py | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/flask_jwt_extended/internal_utils.py b/flask_jwt_extended/internal_utils.py index e74e1db7..d3b7fb05 100644 --- a/flask_jwt_extended/internal_utils.py +++ b/flask_jwt_extended/internal_utils.py @@ -66,6 +66,18 @@ def custom_verification_for_token(jwt_header: dict, jwt_data: dict) -> None: raise UserClaimsVerificationError(error_msg, jwt_header, jwt_data) +class JSONEncoder(json.JSONEncoder): + """A JSON encoder which uses the app.json_provider_class for the default""" + + def default(self, o: Any) -> Any: + # If the registered JSON provider does not implement a default classmethod + # use the method defined by the DefaultJSONProvider + default = getattr( + current_app.json_provider_class, "default", DefaultJSONProvider.default + ) + return default(o) + + def get_json_encoder(app: Flask) -> Type[json.JSONEncoder]: """Get the JSON Encoder for the provided flask app @@ -83,12 +95,4 @@ def get_json_encoder(app: Flask) -> Type[json.JSONEncoder]: if not HAS_JSON_PROVIDER: # pragma: no cover return app.json_encoder - # If the registered JSON provider does not implement a default classmethod - # use the method defined by the DefaultJSONProvider - default = getattr(app.json_provider_class, "default", DefaultJSONProvider.default) - - class JSONEncoder(json.JSONEncoder): - def default(self, o: Any) -> Any: - return default(o) # pragma: no cover - return JSONEncoder diff --git a/tests/test_config.py b/tests/test_config.py index a5515ab8..514d619e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,11 +1,18 @@ +import json +from datetime import date from datetime import timedelta import pytest from dateutil.relativedelta import relativedelta +from flask import __version__ as flask_version from flask import Flask from flask_jwt_extended import JWTManager from flask_jwt_extended.config import config +from flask_jwt_extended.internal_utils import JSONEncoder + + +flask_version_tuple = tuple(map(int, flask_version.split("."))) @pytest.fixture(scope="function") @@ -157,6 +164,26 @@ def test_override_configs(app, delta_func): assert config.error_msg_key == "message" +@pytest.mark.skipif( + flask_version_tuple >= (2, 2, 0), reason="Only applies to Flask <= 2.2.0" +) +def test_config_json_encoder_flask21(app): + with app.test_request_context(): + assert config.json_encoder == app.json_encoder + dump = json.dumps({"d": date(2022, 8, 12)}, cls=config.json_encoder) + assert dump == '{"d": "Fri, 12 Aug 2022 00:00:00 GMT"}' + + +@pytest.mark.skipif( + flask_version_tuple < (2, 2, 0), reason="Only applies to Flask > 2.2.0" +) +def test_config_json_encoder_flask(app): + with app.test_request_context(): + assert config.json_encoder == JSONEncoder + dump = json.dumps({"d": date(2022, 8, 12)}, cls=config.json_encoder) + assert dump == '{"d": "Fri, 12 Aug 2022 00:00:00 GMT"}' + + def test_tokens_never_expire(app): app.config["JWT_ACCESS_TOKEN_EXPIRES"] = False app.config["JWT_REFRESH_TOKEN_EXPIRES"] = False