Skip to content

Commit

Permalink
feat: replace json with orjson
Browse files Browse the repository at this point in the history
  • Loading branch information
maticardenas committed Aug 11, 2024
1 parent 330e5c8 commit 33874b2
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 50 deletions.
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ repos:
entry: pylint
language: python
types: [ python ]
args: [ "--extension-pkg-allow-list=orjson"]
exclude: tests|test_project|manage.py
additional_dependencies:
- django
Expand All @@ -60,3 +61,4 @@ repos:
- drf-spectacular
- pylint
- faker
- orjson
5 changes: 0 additions & 5 deletions openapi_tester/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def request(self, *args, **kwargs) -> Response: # type: ignore[override]
self.schema_tester.validate_response(response_handler=response_handler)
return response

# pylint: disable=W0622
@serialize_json
def post(
self,
Expand All @@ -63,7 +62,6 @@ def post(
**kwargs,
)

# pylint: disable=W0622
@serialize_json
def put(
self,
Expand All @@ -77,7 +75,6 @@ def put(
**kwargs,
)

# pylint: disable=W0622
@serialize_json
def patch(self, *args, content_type="application/json", **kwargs):
return super().patch(
Expand All @@ -86,7 +83,6 @@ def patch(self, *args, content_type="application/json", **kwargs):
**kwargs,
)

# pylint: disable=W0622
@serialize_json
def delete(
self,
Expand All @@ -100,7 +96,6 @@ def delete(
**kwargs,
)

# pylint: disable=W0622
@serialize_json
def options(
self,
Expand Down
15 changes: 9 additions & 6 deletions openapi_tester/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
from __future__ import annotations

import difflib
import json
import pathlib
import re
from json import dumps, loads
from typing import TYPE_CHECKING, cast
from urllib.parse import urlparse

import orjson
import requests
import yaml
from django.urls import Resolver404, resolve
Expand Down Expand Up @@ -225,7 +224,8 @@ def load_schema(self) -> dict:
Loads generated schema from drf-yasg and returns it as a dict.
"""
odict_schema = self.schema_generator.get_schema(None, True)
return cast("dict", loads(dumps(odict_schema.as_odict())))
str_schema = orjson.dumps(odict_schema.as_odict()).decode("utf-8")
return cast("dict", orjson.loads(str_schema))

def resolve_path(
self, endpoint_path: str, method: str
Expand Down Expand Up @@ -253,7 +253,10 @@ def load_schema(self) -> dict:
"""
Loads generated schema from drf_spectacular and returns it as a dict.
"""
return cast("dict", loads(dumps(self.schema_generator.get_schema(public=True))))
str_schema = orjson.dumps(self.schema_generator.get_schema(public=True)).decode(
"utf-8"
)
return cast("dict", orjson.loads(str_schema))

def resolve_path(
self, endpoint_path: str, method: str
Expand Down Expand Up @@ -290,7 +293,7 @@ def load_schema(self) -> dict[str, Any]:
content = file.read()
return cast(
"dict",
json.loads(content)
orjson.loads(content)
if ".json" in self.path
else yaml.load(content, Loader=yaml.FullLoader),
)
Expand All @@ -316,7 +319,7 @@ def load_schema(self) -> dict[str, Any]:
return cast(
"dict",
(
json.loads(response.content)
orjson.loads(response.content)
if ".json" in self.url
else yaml.load(response.content, Loader=yaml.FullLoader)
),
Expand Down
7 changes: 4 additions & 3 deletions openapi_tester/response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
This module contains the concrete response handlers for both DRF and Django Ninja responses.
"""

import json
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Optional, Union

import orjson

if TYPE_CHECKING:
from django.http.response import HttpResponse
from rest_framework.response import Response
Expand Down Expand Up @@ -95,6 +96,6 @@ def request(self) -> GenericRequest:

def _build_request_data(self, request_data: Any) -> dict:
try:
return json.loads(request_data)
except (json.JSONDecodeError, TypeError, ValueError):
return orjson.loads(request_data)
except (orjson.JSONDecodeError, TypeError, ValueError):
return {}
7 changes: 4 additions & 3 deletions openapi_tester/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

from __future__ import annotations

import json
from copy import deepcopy
from itertools import chain, combinations
from typing import TYPE_CHECKING

import orjson

if TYPE_CHECKING:
from typing import Any, Iterator, Sequence

Expand Down Expand Up @@ -57,7 +58,7 @@ def normalize_schema_section(schema_section: dict[str, Any]) -> dict[str, Any]:


def serialize_schema_section_data(data: dict[str, Any]) -> str:
return json.dumps(data, indent=4, default=str)
return orjson.dumps(data, option=orjson.OPT_INDENT_2, default=str).decode("utf-8")


def lazy_combinations(options_list: Sequence[dict[str, Any]]) -> Iterator[dict]:
Expand All @@ -76,7 +77,7 @@ def wrapper(*args, **kwargs):
content_type = kwargs.get("content_type")
if data and content_type == "application/json":
try:
kwargs["data"] = json.dumps(data)
kwargs["data"] = orjson.dumps(data)
except (TypeError, OverflowError):
kwargs["data"] = data
return func(*args, **kwargs)
Expand Down
6 changes: 4 additions & 2 deletions openapi_tester/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from __future__ import annotations

import base64
import json
import re
from typing import TYPE_CHECKING
from uuid import UUID

import orjson
from django.core.exceptions import ValidationError
from django.core.validators import (
EmailValidator,
Expand Down Expand Up @@ -200,7 +200,9 @@ def validate_unique_items(
unique_items = schema_section.get("uniqueItems")
if unique_items:
comparison_data = (
json.dumps(item, sort_keys=True) if isinstance(item, dict) else item
orjson.dumps(item, option=orjson.OPT_SORT_KEYS).decode("utf-8")
if isinstance(item, dict)
else item
for item in data
)
if len(set(comparison_data)) != len(data):
Expand Down
68 changes: 67 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pyYAML = "*"
drf-spectacular = { version = "*", optional = true }
drf-yasg = { version = "*", optional = true }
django-ninja = {version = "^1.1.0", optional = true}
orjson = "^3.10.7"

[tool.poetry.extras]
drf-yasg = ["drf-yasg"]
Expand Down
4 changes: 2 additions & 2 deletions tests/test_clients.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools
import json
from typing import TYPE_CHECKING

import orjson
import pytest
from django.test.testcases import SimpleTestCase
from rest_framework import status
Expand Down Expand Up @@ -129,7 +129,7 @@ def test_request_on_empty_list(openapi_client):
{
"method": "POST",
"path": "/api/v1/vehicles",
"data": json.dumps({"vehicle_type": "1" * 50}),
"data": orjson.dumps({"vehicle_type": "1" * 50}).decode("utf-8"),
"content_type": "application/json",
},
{
Expand Down
8 changes: 4 additions & 4 deletions tests/test_django_ninja.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from typing import TYPE_CHECKING

import orjson
import pytest

from openapi_tester import SchemaTester
Expand Down Expand Up @@ -40,7 +40,7 @@ def test_create_user(client: OpenAPINinjaClient):
}
response = client.post(
path="/",
data=json.dumps(payload),
data=orjson.dumps(payload).decode("utf-8"),
content_type="application/json",
)
assert response.status_code == 201
Expand All @@ -55,7 +55,7 @@ def test_update_user(client: OpenAPINinjaClient):
}
response = client.put(
path="/1",
data=json.dumps(payload),
data=orjson.dumps(payload).decode("utf-8"),
content_type="application/json",
)
assert response.status_code == 200
Expand All @@ -75,6 +75,6 @@ def test_patch_user_undocumented_path(client: OpenAPINinjaClient):
with pytest.raises(UndocumentedSchemaSectionError):
client.patch(
path="/1",
data=json.dumps(payload),
data=orjson.dumps(payload).decode("utf-8"),
content_type="application/json",
)
19 changes: 10 additions & 9 deletions tests/test_openapi_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ def test_missing_response_key_error():
expected_error_message = (
'The following property was found in the schema definition, but is missing from the response data: "one"'
"\n\nReference:\n\nPOST /endpoint > response > one"
'\n\nResponse body:\n {\n "two": 2\n}'
'\nSchema section:\n {\n "one": {\n "type": "int"\n }\n}'
'\n\nResponse body:\n {\n "two": 2\n}'
'\nSchema section:\n {\n "one": {\n "type": "int"\n }\n}'
"\n\nHint: Remove the key from your OpenAPI docs, or include it in your API response"
)
tester = SchemaTester()
Expand All @@ -27,8 +27,8 @@ def test_missing_schema_key_error():
'The following property was found in the response data, but is missing from the schema definition: "two"'
"\n\nReference:"
"\n\nPOST /endpoint > response > two"
'\n\nResponse body:\n {\n "one": 1,\n "two": 2\n}'
'\n\nSchema section:\n {\n "one": {\n "type": "int"\n }\n}'
'\n\nResponse body:\n {\n "one": 1,\n "two": 2\n}'
'\n\nSchema section:\n {\n "one": {\n "type": "int"\n }\n}'
"\n\nHint: Remove the key from your API response, or include it in your OpenAPI docs"
)
tester = SchemaTester()
Expand All @@ -45,8 +45,8 @@ def test_key_in_write_only_properties_error():
'The following property was found in the response, but is documented as being "writeOnly": "one"'
"\n\nReference:"
"\n\nPOST /endpoint > response > one"
'\n\nResponse body:\n {\n "one": 1\n}'
'\nSchema section:\n {\n "one": {\n "type": "int",\n "writeOnly": true\n }\n}'
'\n\nResponse body:\n {\n "one": 1\n}'
'\nSchema section:\n {\n "one": {\n "type": "int",\n "writeOnly": true\n }\n}'
'\n\nHint: Remove the key from your API response, or remove the "WriteOnly" restriction'
)
tester = SchemaTester()
Expand All @@ -70,9 +70,10 @@ def test_date_serialization():
def test_wrong_date_error():
tester = SchemaTester()
expected_error_message = (
'\n\nExpected: a "date-time" formatted "string" value\n\nReceived: '
'"not-a-date"\n\nReference: \n\nPOST /endpoint > response > updated_at\n\n Response value:\n '
"not-a-date\n Schema description:\n {'type': 'string', 'format': 'date-time'}"
'\n\nExpected: a "date-time" formatted "string" value'
'\n\nReceived: "not-a-date"\n\nReference: '
"\n\nPOST /endpoint > response > updated_at"
"\n\n Response value:\n not-a-date\n Schema description:\n {'type': 'string', 'format': 'date-time'}"
)

with pytest.raises(DocumentationError, match=expected_error_message):
Expand Down
Loading

0 comments on commit 33874b2

Please sign in to comment.