diff --git a/.github/workflows/gh-tests-ci.yml b/.github/workflows/gh-tests-ci.yml index 2d46f823..38d0c870 100644 --- a/.github/workflows/gh-tests-ci.yml +++ b/.github/workflows/gh-tests-ci.yml @@ -26,8 +26,13 @@ jobs: - name: Test with pytest run: | pytest --cache-clear --cov=./azure --cov-report=xml --cov-branch tests - - name: Codecov - if: ${{ matrix.python-version }} == 3.9 + - if: matrix.python_version != 3.6 + name: Test with pytest and ujson + run: | + python -m pip install .[ujson] + pytest --cache-clear --cov=./azure --cov-report=xml --cov-branch --cov-append tests + - if: matrix.python_version == 3.9 + name: Codecov uses: codecov/codecov-action@v2 with: file: ./coverage.xml diff --git a/azure/functions/_cosmosdb.py b/azure/functions/_cosmosdb.py index 4a6295c6..684a16df 100644 --- a/azure/functions/_cosmosdb.py +++ b/azure/functions/_cosmosdb.py @@ -2,8 +2,8 @@ # Licensed under the MIT License. import collections -import json +from azure.functions import _json as json from . import _abc diff --git a/azure/functions/_durable_functions.py b/azure/functions/_durable_functions.py index aa533679..a0d9622a 100644 --- a/azure/functions/_durable_functions.py +++ b/azure/functions/_durable_functions.py @@ -12,7 +12,7 @@ def _serialize_custom_object(obj): This function gets called when `json.dumps` cannot serialize an object and returns a serializable dictionary containing enough - metadata to recontrust the original object. + metadata to reconstruct the original object. Parameters ---------- diff --git a/azure/functions/_http.py b/azure/functions/_http.py index b52fbb5f..f42a5216 100644 --- a/azure/functions/_http.py +++ b/azure/functions/_http.py @@ -3,7 +3,7 @@ import collections.abc import io -import json + import types import typing @@ -12,6 +12,7 @@ from ._thirdparty.werkzeug import formparser as _wk_parser from ._thirdparty.werkzeug import http as _wk_http from ._thirdparty.werkzeug.datastructures import Headers +from azure.functions import _json as json class BaseHeaders(collections.abc.Mapping): diff --git a/azure/functions/_json.py b/azure/functions/_json.py new file mode 100644 index 00000000..9a990a49 --- /dev/null +++ b/azure/functions/_json.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from enum import Enum +from typing import AnyStr, Any +import os + +AZUREFUNCTIONS_UJSON_ENV_VAR = 'AZUREFUNCTIONS_UJSON' + +try: + import ujson + if AZUREFUNCTIONS_UJSON_ENV_VAR in os.environ: + HAS_UJSON = bool(os.environ[AZUREFUNCTIONS_UJSON_ENV_VAR]) + else: + HAS_UJSON = True + import json +except ImportError: + import json + HAS_UJSON = False + + +class StringifyEnum(Enum): + """This class output name of enum object when printed as string.""" + + def __str__(self): + return str(self.name) + + def __json__(self): + """For ujson encoding.""" + return f'"{self.name}"' + + +class StringifyEnumJsonEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, StringifyEnum): + return str(o) + + return super().default(o) + + +JSONDecodeError = json.JSONDecodeError + +if HAS_UJSON: + def dumps(v: Any, **kwargs) -> str: + if 'default' in kwargs: + return json.dumps(v, **kwargs) + + if 'cls' in kwargs: + del kwargs['cls'] + + return ujson.dumps(v, **kwargs) + + def loads(s: AnyStr, **kwargs): + if kwargs: + return json.loads(s, **kwargs) + else: # ujson takes no kwargs + return ujson.loads(s) + +else: + dumps = json.dumps # type: ignore + loads = json.loads diff --git a/azure/functions/_queue.py b/azure/functions/_queue.py index c6c7d094..de92772a 100644 --- a/azure/functions/_queue.py +++ b/azure/functions/_queue.py @@ -2,9 +2,10 @@ # Licensed under the MIT License. import datetime -import json + import typing +from azure.functions import _json as json from . import _abc diff --git a/azure/functions/_sql.py b/azure/functions/_sql.py index a673c320..a8beed2c 100644 --- a/azure/functions/_sql.py +++ b/azure/functions/_sql.py @@ -2,8 +2,7 @@ # Licensed under the MIT License. import collections -import json - +from azure.functions import _json as json from . import _abc diff --git a/azure/functions/cosmosdb.py b/azure/functions/cosmosdb.py index 1edc78af..36574caf 100644 --- a/azure/functions/cosmosdb.py +++ b/azure/functions/cosmosdb.py @@ -2,9 +2,10 @@ # Licensed under the MIT License. import collections.abc -import json + import typing +from azure.functions import _json as json from azure.functions import _cosmosdb as cdb from . import meta diff --git a/azure/functions/decorators/utils.py b/azure/functions/decorators/utils.py index 4d7bcbdf..c238f792 100644 --- a/azure/functions/decorators/utils.py +++ b/azure/functions/decorators/utils.py @@ -4,7 +4,6 @@ import re from abc import ABCMeta from enum import Enum -from json import JSONEncoder from typing import TypeVar, Optional, Union, Iterable, Type, Callable T = TypeVar("T", bound=Enum) @@ -12,12 +11,7 @@ re.IGNORECASE) WORD_RE = re.compile(r'^([a-z]+\d*)$', re.IGNORECASE) - -class StringifyEnum(Enum): - """This class output name of enum object when printed as string.""" - - def __str__(self): - return str(self.name) +from azure.functions._json import StringifyEnum, StringifyEnumJsonEncoder # NOQA class BuildDictMeta(type): @@ -166,11 +160,3 @@ def is_word(input_string: str) -> bool: :return: True for one word string, false otherwise. """ return WORD_RE.match(input_string) is not None - - -class StringifyEnumJsonEncoder(JSONEncoder): - def default(self, o): - if isinstance(o, StringifyEnum): - return str(o) - - return super().default(o) diff --git a/azure/functions/durable_functions.py b/azure/functions/durable_functions.py index 16c4fc4c..07b148a2 100644 --- a/azure/functions/durable_functions.py +++ b/azure/functions/durable_functions.py @@ -2,8 +2,8 @@ # Licensed under the MIT License. import typing -import json +from azure.functions import _json as json from azure.functions import _durable_functions from . import meta diff --git a/azure/functions/eventgrid.py b/azure/functions/eventgrid.py index e76f5dde..d3c4d47c 100644 --- a/azure/functions/eventgrid.py +++ b/azure/functions/eventgrid.py @@ -3,9 +3,10 @@ import collections import datetime -import json + from typing import Optional, List, Any, Dict, Union +from azure.functions import _json as json from azure.functions import _eventgrid as azf_eventgrid from . import meta diff --git a/azure/functions/eventhub.py b/azure/functions/eventhub.py index 6aab2be5..946dbcdf 100644 --- a/azure/functions/eventhub.py +++ b/azure/functions/eventhub.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json + from typing import Dict, Any, List, Union, Optional, Mapping +from azure.functions import _json as json from azure.functions import _eventhub from . import meta diff --git a/azure/functions/extension/extension_meta.py b/azure/functions/extension/extension_meta.py index c875722f..1c8df31d 100644 --- a/azure/functions/extension/extension_meta.py +++ b/azure/functions/extension/extension_meta.py @@ -3,7 +3,8 @@ from typing import Optional, Union, Dict, List import abc -import json + +from azure.functions import _json as json from .app_extension_hooks import AppExtensionHooks from .func_extension_hooks import FuncExtensionHooks from .extension_hook_meta import ExtensionHookMeta diff --git a/azure/functions/http.py b/azure/functions/http.py index 61b303ec..05d1604d 100644 --- a/azure/functions/http.py +++ b/azure/functions/http.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json + import logging import sys import typing from http.cookies import SimpleCookie from azure.functions import _abc as azf_abc +from azure.functions import _json as json from azure.functions import _http as azf_http from . import meta from ._thirdparty.werkzeug.datastructures import Headers diff --git a/azure/functions/kafka.py b/azure/functions/kafka.py index 5080aa7f..c9bf3d41 100644 --- a/azure/functions/kafka.py +++ b/azure/functions/kafka.py @@ -2,12 +2,11 @@ # Licensed under the MIT License. import typing -import json - from typing import Any, List from . import meta +from azure.functions import _json as json from ._kafka import AbstractKafkaEvent diff --git a/azure/functions/meta.py b/azure/functions/meta.py index 2ca92563..3abcb5d5 100644 --- a/azure/functions/meta.py +++ b/azure/functions/meta.py @@ -4,7 +4,6 @@ import abc import collections.abc import datetime -import json import re from typing import Dict, Optional, Union, Tuple, Mapping, Any @@ -13,6 +12,7 @@ try_parse_datetime_with_formats, try_parse_timedelta_with_formats ) +from azure.functions import _json as json def is_iterable_type_annotation(annotation: object, pytype: object) -> bool: diff --git a/azure/functions/queue.py b/azure/functions/queue.py index d39c56e5..cb55fe66 100644 --- a/azure/functions/queue.py +++ b/azure/functions/queue.py @@ -3,10 +3,10 @@ import collections.abc import datetime -import json from typing import List, Dict, Any, Union, Optional from azure.functions import _abc as azf_abc +from azure.functions import _json as json from azure.functions import _queue as azf_queue from . import meta diff --git a/azure/functions/servicebus.py b/azure/functions/servicebus.py index 76f31b59..9bee3dfe 100644 --- a/azure/functions/servicebus.py +++ b/azure/functions/servicebus.py @@ -2,9 +2,9 @@ # Licensed under the MIT License. import datetime -import json -from typing import Dict, Any, List, Union, Optional, Mapping, cast +from typing import Dict, Any, List, Union, Optional, Mapping, cast +from azure.functions import _json as json from azure.functions import _servicebus as azf_sbus from . import meta diff --git a/azure/functions/sql.py b/azure/functions/sql.py index 60919c0e..310a7b45 100644 --- a/azure/functions/sql.py +++ b/azure/functions/sql.py @@ -2,9 +2,9 @@ # Licensed under the MIT License. import collections.abc -import json import typing +from azure.functions import _json as json from azure.functions import _sql as sql from . import meta diff --git a/azure/functions/timer.py b/azure/functions/timer.py index 56bdc8eb..6120f46d 100644 --- a/azure/functions/timer.py +++ b/azure/functions/timer.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json import typing from azure.functions import _abc as azf_abc +from azure.functions import _json as json from . import meta diff --git a/setup.py b/setup.py index 89a0ff8c..2bc7d5d4 100644 --- a/setup.py +++ b/setup.py @@ -10,9 +10,12 @@ 'mypy', 'pytest', 'pytest-cov', + 'pytest-benchmark', 'requests==2.*', - 'coverage' - ] + 'coverage', + 'types-ujson' + ], + 'ujson': ['ujson>=5.3.0,<6.0'] } with open("README.md") as readme: @@ -34,6 +37,7 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index 103c7268..f1894c00 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json + import unittest from unittest import mock +from azure.functions import _json as json from azure.functions import WsgiMiddleware, AsgiMiddleware from azure.functions.decorators.constants import HTTP_OUTPUT, HTTP_TRIGGER from azure.functions.decorators.core import DataType, AuthLevel, \ diff --git a/tests/decorators/test_utils.py b/tests/decorators/test_utils.py index 4dd87e06..e8380c81 100644 --- a/tests/decorators/test_utils.py +++ b/tests/decorators/test_utils.py @@ -219,3 +219,18 @@ def test_is_not_supported_trigger_type(self): Trigger.is_supported_trigger_type( GenericTrigger(name='req', type="dummy"), HttpTrigger)) + + +def test_clean_nones_nested_benchmark(benchmark): + data = [ + {"direction": "IN", "type": "b", "name": "t1"}, + {"direction": "IN", "type": "b", "name": "t2"}, + {"direction": "IN", "type": "b", "name": "t3"}, + {"direction": "IN", "type": "b", "name": "t4"}, + {"direction": "IN", "type": "b", "name": "t5"}, + {"direction": "IN", "type": "b", "name": "t6"}, + {"direction": "IN", "type": "b", "name": "t7"}, + {"direction": "IN", "type": "b", "name": "t8"}, + {"direction": "IN", "type": "b", "name": "t9"} + ] + benchmark(BuildDictMeta.clean_nones, data) diff --git a/tests/decorators/testutils.py b/tests/decorators/testutils.py index 6e85f9d1..83e2444e 100644 --- a/tests/decorators/testutils.py +++ b/tests/decorators/testutils.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json +from azure.functions import _json as json from azure.functions.decorators.utils import StringifyEnumJsonEncoder diff --git a/tests/test_blob.py b/tests/test_blob.py index 4a218228..27a25bff 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json import unittest from typing import Any, Dict import azure.functions as func import azure.functions.blob as afb from azure.functions.blob import InputStream +from azure.functions import _json as json from azure.functions.meta import Datum diff --git a/tests/test_durable_functions.py b/tests/test_durable_functions.py index 1739cdcd..9f3a9176 100644 --- a/tests/test_durable_functions.py +++ b/tests/test_durable_functions.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import unittest -import json from azure.functions.durable_functions import ( OrchestrationTriggerConverter, @@ -13,6 +12,7 @@ OrchestrationContext, EntityContext ) +from azure.functions import _json as json from azure.functions.meta import Datum CONTEXT_CLASSES = [OrchestrationContext, EntityContext] diff --git a/tests/test_eventhub.py b/tests/test_eventhub.py index 4b47775a..d30257d1 100644 --- a/tests/test_eventhub.py +++ b/tests/test_eventhub.py @@ -3,12 +3,12 @@ from typing import List, Mapping import unittest -import json from unittest.mock import patch from datetime import datetime import azure.functions as func import azure.functions.eventhub as azf_eh +from azure.functions import _json as json import azure.functions.meta as meta from .testutils import CollectionBytes, CollectionString @@ -113,12 +113,14 @@ def test_eventhub_trigger_multiple_events_json(self): self.assertEqual(result[0].enqueued_time, self.MOCKED_ENQUEUE_TIME) self.assertEqual( - result[0].get_body().decode('utf-8'), '{"device-status": "good1"}' + result[0].get_body().decode('utf-8'), + json.dumps({"device-status": "good1"}) ) self.assertEqual(result[1].enqueued_time, self.MOCKED_ENQUEUE_TIME) self.assertEqual( - result[1].get_body().decode('utf-8'), '{"device-status": "good2"}' + result[1].get_body().decode('utf-8'), + json.dumps({"device-status": "good2"}) ) def test_eventhub_trigger_multiple_events_collection_string(self): @@ -131,12 +133,14 @@ def test_eventhub_trigger_multiple_events_collection_string(self): self.assertEqual(result[0].enqueued_time, self.MOCKED_ENQUEUE_TIME) self.assertEqual( - result[0].get_body().decode('utf-8'), '{"device-status": "good1"}' + result[0].get_body().decode('utf-8'), + json.dumps({"device-status": "good1"}) ) self.assertEqual(result[1].enqueued_time, self.MOCKED_ENQUEUE_TIME) self.assertEqual( - result[1].get_body().decode('utf-8'), '{"device-status": "good2"}' + result[1].get_body().decode('utf-8'), + json.dumps({"device-status": "good2"}) ) def test_eventhub_trigger_multiple_events_collection_bytes(self): @@ -149,12 +153,14 @@ def test_eventhub_trigger_multiple_events_collection_bytes(self): self.assertEqual(result[0].enqueued_time, self.MOCKED_ENQUEUE_TIME) self.assertEqual( - result[0].get_body().decode('utf-8'), '{"device-status": "good1"}' + result[0].get_body().decode('utf-8'), + json.dumps({"device-status": "good1"}) ) self.assertEqual(result[1].enqueued_time, self.MOCKED_ENQUEUE_TIME) self.assertEqual( - result[1].get_body().decode('utf-8'), '{"device-status": "good2"}' + result[1].get_body().decode('utf-8'), + json.dumps({"device-status": "good2"}) ) def test_iothub_metadata_events(self): diff --git a/tests/test_extension.py b/tests/test_extension.py index 2b7452f2..45d20176 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch from logging import Logger +from azure.functions import _json as json from azure.functions.extension import FunctionExtensionException from azure.functions.extension.app_extension_base import AppExtensionBase from azure.functions.extension.func_extension_base import FuncExtensionBase @@ -152,7 +153,8 @@ def test_get_registered_extensions_json_function_ext(self): info_json = self._instance.get_registered_extensions_json() self.assertEqual( info_json, - r'{"FuncExtension": {"HttpTrigger": ["NewFuncExtension"]}}' + json.dumps({"FuncExtension": + {"HttpTrigger": ["NewFuncExtension"]}}) ) def test_get_registered_extension_json_application_ext(self): @@ -164,7 +166,7 @@ def test_get_registered_extension_json_application_ext(self): info_json = self._instance.get_registered_extensions_json() self.assertEqual( info_json, - r'{"AppExtension": ["NewAppExtension"]}' + json.dumps({"AppExtension": ["NewAppExtension"]}) ) def test_get_extension_scope(self): diff --git a/tests/test_http.py b/tests/test_http.py index a64503ef..7679f398 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -166,3 +166,17 @@ def test_http_response_does_not_have_explicit_output(self): self.assertIsNone( getattr(http.HttpResponseConverter, 'has_implicit_output', None) ) + + +def test_http_request_body_json(benchmark): + data: bytes = b'{ "result": "OK", "message": "All good!", "code": 200 }' + request = func.HttpRequest( + method='POST', + url='/foo', + body=data, + headers={ + 'Content-Type': 'application/json; charset=utf-8' + } + ) + + benchmark(request.get_json) diff --git a/tests/test_kafka.py b/tests/test_kafka.py index 7c90e314..52a65a19 100644 --- a/tests/test_kafka.py +++ b/tests/test_kafka.py @@ -3,10 +3,10 @@ from typing import List import unittest -import json from unittest.mock import patch import azure.functions as func +from azure.functions import _json as json import azure.functions.kafka as azf_ka import azure.functions.meta as meta diff --git a/tests/test_queue.py b/tests/test_queue.py index ab83f923..fdf3e528 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json import unittest +from azure.functions import _json as json import azure.functions as func import azure.functions.queue as azf_q diff --git a/tests/test_servicebus.py b/tests/test_servicebus.py index 4f5c2508..aa13cfe1 100644 --- a/tests/test_servicebus.py +++ b/tests/test_servicebus.py @@ -2,11 +2,12 @@ # Licensed under the MIT License. from typing import Dict, List -import json + import unittest from datetime import datetime, timedelta import azure.functions as func +from azure.functions import _json as json import azure.functions.servicebus as azf_sb from azure.functions import meta diff --git a/tests/test_sql.py b/tests/test_sql.py index 3fcae437..3b4522d1 100644 --- a/tests/test_sql.py +++ b/tests/test_sql.py @@ -6,7 +6,7 @@ import azure.functions as func import azure.functions.sql as sql from azure.functions.meta import Datum -import json +from azure.functions import _json as json class TestSql(unittest.TestCase): diff --git a/tests/test_timer.py b/tests/test_timer.py index 4b21022a..6c943377 100644 --- a/tests/test_timer.py +++ b/tests/test_timer.py @@ -1,9 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import json import unittest - +from azure.functions import _json as json import azure.functions.timer as timer from azure.functions.meta import Datum