Skip to content

Werkzeug 3.0.1 #32

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
29 changes: 8 additions & 21 deletions firetail/apps/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@
This module defines a FlaskApp, a Firetail application to wrap a Flask application.
"""

import datetime
import logging
import pathlib
from decimal import Decimal
from types import FunctionType # NOQA

import flask
import werkzeug.exceptions
from flask import json, signals
from flask import signals

from ..apis.flask_api import FlaskApi
from ..exceptions import ProblemException
from ..jsonifier import wrap_default
from ..problem import problem
from .abstract import AbstractApp

Expand All @@ -33,7 +32,7 @@ def __init__(self, import_name, server="flask", extra_files=None, **kwargs):

def create_app(self):
app = flask.Flask(self.import_name, **self.server_args)
app.json_encoder = FlaskJSONEncoder
app.json = FlaskJSONProvider(app)
app.url_map.converters["float"] = NumberConverter
app.url_map.converters["int"] = IntegerConverter
return app
Expand Down Expand Up @@ -148,24 +147,12 @@ def run(self, port=None, server=None, debug=None, host=None, extra_files=None, *
raise Exception(f"Server {self.server} not recognized")


class FlaskJSONEncoder(json.JSONEncoder):
class FlaskJSONProvider(flask.json.provider.DefaultJSONProvider):
"""Custom JSONProvider which adds firetail defaults on top of Flask's"""

@wrap_default
def default(self, o):
if isinstance(o, datetime.datetime):
if o.tzinfo:
# eg: '2015-09-25T23:14:42.588601+00:00'
return o.isoformat("T")
else:
# No timezone present - assume UTC.
# eg: '2015-09-25T23:14:42.588601Z'
return o.isoformat("T") + "Z"

if isinstance(o, datetime.date):
return o.isoformat()

if isinstance(o, Decimal):
return float(o)

return json.JSONEncoder.default(self, o)
return super().default(o)


class NumberConverter(werkzeug.routing.BaseConverter):
Expand Down
35 changes: 28 additions & 7 deletions firetail/jsonifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@
"""

import datetime
import functools
import json
import typing as t
import uuid
from decimal import Decimal


class JSONEncoder(json.JSONEncoder):
"""The default Firetail JSON encoder. Handles extra types compared to the
built-in :class:`json.JSONEncoder`. # noqa RST304
def wrap_default(default_fn: t.Callable) -> t.Callable:
"""The Firetail defaults for JSON encoding. Handles extra types compared to the
built-in :class:`json.JSONEncoder`.

- :class:`datetime.datetime` and :class:`datetime.date` are # noqa RST304
- :class:`datetime.datetime` and :class:`datetime.date` are
serialized to :rfc:`822` strings. This is the same as the HTTP
date format.
- :class:`uuid.UUID` is serialized to a string. # noqa RST304
- :class:`decimal.Decimal` is serialized to a float.
- :class:`uuid.UUID` is serialized to a string.
"""

def default(self, o):
@functools.wraps(default_fn)
def wrapped_default(self, o):
if isinstance(o, datetime.datetime):
if o.tzinfo:
# eg: '2015-09-25T23:14:42.588601+00:00'
Expand All @@ -30,10 +35,25 @@ def default(self, o):
if isinstance(o, datetime.date):
return o.isoformat()

if isinstance(o, Decimal):
return float(o)

if isinstance(o, uuid.UUID):
return str(o)

return json.JSONEncoder.default(self, o)
return default_fn(o)

return wrapped_default


class JSONEncoder(json.JSONEncoder):
"""The default Firetail JSON encoder. Handles extra types compared to the
built-in :class:`json.JSONEncoder`.
"""

@wrap_default
def default(self, o):
return super().default(o)


class Jsonifier:
Expand All @@ -48,6 +68,7 @@ def __init__(self, json_=json, **kwargs):
"""
self.json = json_
self.dumps_args = kwargs
self.dumps_args.setdefault("cls", JSONEncoder)

def dumps(self, data, **kwargs):
"""Central point where JSON serialization happens inside
Expand Down
5 changes: 0 additions & 5 deletions firetail/middleware/swagger_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from starlette.types import ASGIApp, Receive, Scope, Send

from firetail.apis import AbstractSwaggerUIAPI
from firetail.jsonifier import JSONEncoder, Jsonifier
from firetail.utils import yamldumper

from .base import AppMiddleware
Expand Down Expand Up @@ -198,7 +197,3 @@ async def _get_swagger_ui_config(self, request):
media_type="application/json",
content=self.jsonifier.dumps(self.options.openapi_console_ui_config),
)

@classmethod
def _set_jsonifier(cls):
cls.jsonifier = Jsonifier(cls=JSONEncoder)
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
a2wsgi==1.4
clickclick==1.2
flask[async]==2.2.5
flask[async]==2.3.3
inflection==0.3.1
jsonschema==4.0.1
PyJWT==2.4.0
PyYAML==6.0.1
requests==2.31.0
starlette==0.27.0
werkzeug==2.2.3
werkzeug==3.0.1
aiohttp_jinja2
aiohttp
aiohttp_remotes
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ def read_version(package):
"PyJWT>=2.4.0",
"requests>=2.31,<3",
"inflection>=0.3.1,<0.6",
"werkzeug>=2.2.2,<3",
"werkzeug>=3.0.1",
"starlette>=0.27,<1",
]

swagger_ui_require = "swagger-ui-bundle>=0.0.2,<0.1"

flask_require = [
"flask[async]>=2.2.5,<3.0",
"flask[async]>=2.3.0,<3.0",
"a2wsgi>=1.4,<2",
]

Expand Down
2 changes: 1 addition & 1 deletion tests/api/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ def test_get_unicode_request(simple_app):

def test_cookie_param(simple_app):
app_client = simple_app.app.test_client()
app_client.set_cookie("localhost", "test_cookie", "hello")
app_client.set_cookie("test_cookie", "hello")
response = app_client.get("/v1.0/test-cookie-param")
assert response.status_code == 200
assert response.json == {"cookie_value": "hello"}
7 changes: 4 additions & 3 deletions tests/api/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from struct import unpack

import yaml
from firetail.apps.flask_app import FlaskJSONEncoder
from werkzeug.test import Client, EnvironBuilder

from firetail.apps.flask_app import FlaskJSONProvider


def test_app(simple_app):
assert simple_app.port == 5001
Expand Down Expand Up @@ -242,11 +243,11 @@ def test_nested_additional_properties(simple_openapi_app):


def test_custom_encoder(simple_app):
class CustomEncoder(FlaskJSONEncoder):
class CustomEncoder(FlaskJSONProvider):
def default(self, o):
if o.__class__.__name__ == "DummyClass":
return "cool result"
return FlaskJSONEncoder.default(self, o)
return FlaskJSONProvider.default(self, o)

flask_app = simple_app.app
flask_app.json_encoder = CustomEncoder
Expand Down
15 changes: 8 additions & 7 deletions tests/test_flask_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@

import pytest
from conftest import build_app_from_fixture
from firetail.apps.flask_app import FlaskJSONEncoder

from firetail.apps.flask_app import FlaskJSONProvider

SPECS = ["swagger.yaml", "openapi.yaml"]


def test_json_encoder():
s = json.dumps({1: 2}, cls=FlaskJSONEncoder)
s = json.dumps({1: 2}, cls=FlaskJSONProvider)
assert '{"1": 2}' == s

s = json.dumps(datetime.date.today(), cls=FlaskJSONEncoder)
s = json.dumps(datetime.date.today(), cls=FlaskJSONProvider)
assert len(s) == 12

s = json.dumps(datetime.datetime.utcnow(), cls=FlaskJSONEncoder)
s = json.dumps(datetime.datetime.utcnow(), cls=FlaskJSONProvider)
assert s.endswith('Z"')

s = json.dumps(Decimal(1.01), cls=FlaskJSONEncoder)
s = json.dumps(Decimal(1.01), cls=FlaskJSONProvider)
assert s == "1.01"

s = json.dumps(math.expm1(1e-10), cls=FlaskJSONEncoder)
s = json.dumps(math.expm1(1e-10), cls=FlaskJSONProvider)
assert s == "1.00000000005e-10"


Expand All @@ -35,7 +36,7 @@ def utcoffset(self, dt):
def dst(self, dt):
return datetime.timedelta(0)

s = json.dumps(datetime.datetime.now(DummyTimezone()), cls=FlaskJSONEncoder)
s = json.dumps(datetime.datetime.now(DummyTimezone()), cls=FlaskJSONProvider)
assert s.endswith('+00:00"')


Expand Down