Skip to content
This repository was archived by the owner on Nov 19, 2023. It is now read-only.

Commit d327cc0

Browse files
committed
chore: Add flake8-type-checking in favor of type_declarations.py
1 parent 3f59603 commit d327cc0

21 files changed

+195
-161
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ repos:
2626
'flake8-use-fstring',
2727
'flake8-simplify',
2828
'flake8-pytest-style',
29+
'flake8-type-checking',
2930
]
3031
- repo: https://github.com/asottile/pyupgrade
3132
rev: v2.29.1

openapi_tester/case_testers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
""" Case testers - this module includes helper functions to test key casing """
2-
from typing import Any, Callable
2+
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING
35

46
from inflection import camelize, dasherize, underscore
57

68
from openapi_tester.exceptions import CaseError
79

10+
if TYPE_CHECKING:
11+
from typing import Any, Callable
12+
813

914
def _create_tester(casing: str, handler: Callable[[Any], str]) -> Callable[[str], None]:
1015
"""factory function for creating testers"""

openapi_tester/loaders.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
""" Loaders Module """
2+
from __future__ import annotations
3+
24
import difflib
35
import json
46
import pathlib
57
from json import dumps, loads
6-
from typing import Callable, Dict, List, Optional, Tuple, cast
7-
from urllib.parse import ParseResult, urlparse
8+
from typing import TYPE_CHECKING, cast
9+
from urllib.parse import urlparse
810

911
import yaml
10-
from django.urls import Resolver404, ResolverMatch, resolve
12+
from django.urls import Resolver404, resolve
1113
from django.utils.functional import cached_property
1214
from openapi_spec_validator import openapi_v2_spec_validator, openapi_v3_spec_validator
13-
14-
# noinspection PyProtectedMember
1515
from prance.util.resolver import RefResolver
16-
17-
# noinspection PyProtectedMember
1816
from rest_framework.schemas.generators import BaseSchemaGenerator, EndpointEnumerator
1917
from rest_framework.settings import api_settings
18+
from rest_framework.views import APIView
19+
20+
if TYPE_CHECKING:
21+
from typing import Callable
22+
from urllib.parse import ParseResult
2023

21-
import openapi_tester.type_declarations as td
24+
from django.urls import ResolverMatch
2225

2326

2427
def handle_recursion_limit(schema: dict) -> Callable:
@@ -47,12 +50,12 @@ class BaseSchemaLoader:
4750
"""
4851

4952
base_path = "/"
50-
field_key_map: Dict[str, str]
51-
schema: Optional[dict] = None
53+
field_key_map: dict[str, str]
54+
schema: dict | None = None
5255

53-
def __init__(self, field_key_map: Optional[Dict[str, str]] = None):
56+
def __init__(self, field_key_map: dict[str, str] | None = None):
5457
super().__init__()
55-
self.schema: Optional[dict] = None
58+
self.schema: dict | None = None
5659
self.field_key_map = field_key_map or {}
5760

5861
def load_schema(self) -> dict:
@@ -82,8 +85,8 @@ def de_reference_schema(self, schema: dict) -> dict:
8285
resolver.resolve_references()
8386
return resolver.specs
8487

85-
def normalize_schema_paths(self, schema: dict) -> Dict[str, dict]:
86-
normalized_paths: Dict[str, dict] = {}
88+
def normalize_schema_paths(self, schema: dict) -> dict[str, dict]:
89+
normalized_paths: dict[str, dict] = {}
8790
for key, value in schema["paths"].items():
8891
try:
8992
parameterized_path, _ = self.resolve_path(endpoint_path=key, method=list(value.keys())[0])
@@ -109,13 +112,13 @@ def set_schema(self, schema: dict) -> None:
109112
self.schema = self.normalize_schema_paths(de_referenced_schema)
110113

111114
@cached_property
112-
def endpoints(self) -> List[str]: # pylint: disable=no-self-use
115+
def endpoints(self) -> list[str]: # pylint: disable=no-self-use
113116
"""
114117
Returns a list of endpoint paths.
115118
"""
116119
return list({endpoint[0] for endpoint in EndpointEnumerator().get_api_endpoints()})
117120

118-
def resolve_path(self, endpoint_path: str, method: str) -> Tuple[str, ResolverMatch]:
121+
def resolve_path(self, endpoint_path: str, method: str) -> tuple[str, ResolverMatch]:
119122
"""
120123
Resolves a Django path.
121124
"""
@@ -147,12 +150,12 @@ def resolve_path(self, endpoint_path: str, method: str) -> Tuple[str, ResolverMa
147150
raise ValueError(message)
148151

149152
@staticmethod
150-
def handle_pk_parameter(resolved_route: ResolverMatch, path: str, method: str) -> Tuple[str, ResolverMatch]:
153+
def handle_pk_parameter(resolved_route: ResolverMatch, path: str, method: str) -> tuple[str, ResolverMatch]:
151154
"""
152155
Handle the DRF conversion of params called {pk} into a named parameter based on Model field
153156
"""
154157
coerced_path = BaseSchemaGenerator().coerce_path(
155-
path=path, method=method, view=cast(td.APIView, resolved_route.func)
158+
path=path, method=method, view=cast(APIView, resolved_route.func)
156159
)
157160
pk_field_name = "".join(
158161
entry.replace("+ ", "") for entry in difflib.Differ().compare(path, coerced_path) if "+ " in entry
@@ -167,7 +170,7 @@ class DrfYasgSchemaLoader(BaseSchemaLoader):
167170
Loads OpenAPI schema generated by drf_yasg.
168171
"""
169172

170-
def __init__(self, field_key_map: Optional[Dict[str, str]] = None) -> None:
173+
def __init__(self, field_key_map: dict[str, str] | None = None) -> None:
171174
super().__init__(field_key_map=field_key_map)
172175
from drf_yasg.generators import OpenAPISchemaGenerator
173176
from drf_yasg.openapi import Info
@@ -181,7 +184,7 @@ def load_schema(self) -> dict:
181184
odict_schema = self.schema_generator.get_schema(None, True)
182185
return loads(dumps(odict_schema.as_odict()))
183186

184-
def resolve_path(self, endpoint_path: str, method: str) -> Tuple[str, ResolverMatch]:
187+
def resolve_path(self, endpoint_path: str, method: str) -> tuple[str, ResolverMatch]:
185188
de_parameterized_path, resolved_path = super().resolve_path(endpoint_path=endpoint_path, method=method)
186189
path_prefix = self.schema_generator.determine_path_prefix(self.endpoints)
187190
trim_length = len(path_prefix) if path_prefix != "/" else 0
@@ -193,7 +196,7 @@ class DrfSpectacularSchemaLoader(BaseSchemaLoader):
193196
Loads OpenAPI schema generated by drf_spectacular.
194197
"""
195198

196-
def __init__(self, field_key_map: Optional[Dict[str, str]] = None) -> None:
199+
def __init__(self, field_key_map: dict[str, str] | None = None) -> None:
197200
super().__init__(field_key_map=field_key_map)
198201
from drf_spectacular.generators import SchemaGenerator
199202

@@ -205,7 +208,7 @@ def load_schema(self) -> dict:
205208
"""
206209
return loads(dumps(self.schema_generator.get_schema(public=True)))
207210

208-
def resolve_path(self, endpoint_path: str, method: str) -> Tuple[str, ResolverMatch]:
211+
def resolve_path(self, endpoint_path: str, method: str) -> tuple[str, ResolverMatch]:
209212
from drf_spectacular.settings import spectacular_settings
210213

211214
de_parameterized_path, resolved_path = super().resolve_path(endpoint_path=endpoint_path, method=method)
@@ -220,7 +223,7 @@ class StaticSchemaLoader(BaseSchemaLoader):
220223
Loads OpenAPI schema from a static file.
221224
"""
222225

223-
def __init__(self, path: str, field_key_map: Optional[Dict[str, str]] = None):
226+
def __init__(self, path: str, field_key_map: dict[str, str] | None = None):
224227
super().__init__(field_key_map=field_key_map)
225228
self.path = path if not isinstance(path, pathlib.PosixPath) else str(path)
226229

openapi_tester/schema_tester.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
""" Schema Tester """
2+
from __future__ import annotations
3+
24
from itertools import chain
3-
from typing import Any, Callable, Dict, List, Optional, Union, cast
5+
from typing import TYPE_CHECKING, Callable, List, cast
46

57
from django.conf import settings
68
from django.core.exceptions import ImproperlyConfigured
7-
from rest_framework.response import Response
89

9-
from openapi_tester import type_declarations as td
1010
from openapi_tester.constants import (
1111
INIT_ERROR,
1212
UNDOCUMENTED_SCHEMA_SECTION_ERROR,
@@ -37,20 +37,25 @@
3737
validate_unique_items,
3838
)
3939

40+
if TYPE_CHECKING:
41+
from typing import Any
42+
43+
from rest_framework.response import Response
44+
4045

4146
class SchemaTester:
4247
"""Schema Tester: this is the base class of the library."""
4348

44-
loader: Union[StaticSchemaLoader, DrfSpectacularSchemaLoader, DrfYasgSchemaLoader]
45-
validators: List[Callable[[dict, Any], Optional[str]]]
49+
loader: StaticSchemaLoader | DrfSpectacularSchemaLoader | DrfYasgSchemaLoader
50+
validators: list[Callable[[dict, Any], str | None]]
4651

4752
def __init__(
4853
self,
49-
case_tester: Optional[Callable[[str], None]] = None,
50-
ignore_case: Optional[List[str]] = None,
51-
schema_file_path: Optional[str] = None,
52-
validators: Optional[List[Callable[[dict, Any], Optional[str]]]] = None,
53-
field_key_map: Optional[Dict[str, str]] = None,
54+
case_tester: Callable[[str], None] | None = None,
55+
ignore_case: list[str] | None = None,
56+
schema_file_path: str | None = None,
57+
validators: list[Callable[[dict, Any], str | None]] | None = None,
58+
field_key_map: dict[str, str] | None = None,
5459
) -> None:
5560
"""
5661
Iterates through an OpenAPI schema object and API response to check that they match at every level.
@@ -86,7 +91,7 @@ def get_key_value(schema: dict, key: str, error_addon: str = "") -> dict:
8691
) from e
8792

8893
@staticmethod
89-
def get_status_code(schema: dict, status_code: Union[str, int], error_addon: str = "") -> dict:
94+
def get_status_code(schema: dict, status_code: str | int, error_addon: str = "") -> dict:
9095
"""
9196
Returns the status code section of a schema, handles both str and int status codes
9297
"""
@@ -99,14 +104,14 @@ def get_status_code(schema: dict, status_code: Union[str, int], error_addon: str
99104
)
100105

101106
@staticmethod
102-
def get_schema_type(schema: dict) -> Optional[str]:
107+
def get_schema_type(schema: dict) -> str | None:
103108
if "type" in schema:
104109
return schema["type"]
105110
if "properties" in schema or "additionalProperties" in schema:
106111
return "object"
107112
return None
108113

109-
def get_response_schema_section(self, response: td.Response) -> Dict[str, Any]:
114+
def get_response_schema_section(self, response: Response) -> dict[str, Any]:
110115
"""
111116
Fetches the response section of a schema, wrt. the route, method, status code, and schema version.
112117
@@ -175,7 +180,7 @@ def handle_one_of(self, schema_section: dict, data: Any, reference: str, **kwarg
175180
raise DocumentationError(f"{VALIDATE_ONE_OF_ERROR.format(matches=matches)}\n\nReference: {reference}.oneOf")
176181

177182
def handle_any_of(self, schema_section: dict, data: Any, reference: str, **kwargs: Any):
178-
any_of: List[Dict[str, Any]] = schema_section.get("anyOf", [])
183+
any_of: list[dict[str, Any]] = schema_section.get("anyOf", [])
179184
for schema in chain(any_of, lazy_combinations(any_of)):
180185
try:
181186
self.test_schema_section(schema_section=schema, data=data, reference=f"{reference}.anyOf", **kwargs)
@@ -205,8 +210,8 @@ def test_is_nullable(schema_item: dict) -> bool:
205210
def test_key_casing(
206211
self,
207212
key: str,
208-
case_tester: Optional[Callable[[str], None]] = None,
209-
ignore_case: Optional[List[str]] = None,
213+
case_tester: Callable[[str], None] | None = None,
214+
ignore_case: list[str] | None = None,
210215
) -> None:
211216
tester = case_tester or getattr(self, "case_tester", None)
212217
ignore_case = [*self.ignore_case, *(ignore_case or [])]
@@ -218,7 +223,7 @@ def test_schema_section(
218223
schema_section: dict,
219224
data: Any,
220225
reference: str = "init",
221-
validators: Optional[List[Callable[[dict, dict], Optional[str]]]] = None,
226+
validators: list[Callable[[dict, dict], str | None]] | None = None,
222227
**kwargs: Any,
223228
) -> None:
224229
"""
@@ -280,8 +285,8 @@ def test_openapi_object(
280285
schema_section: dict,
281286
data: dict,
282287
reference: str,
283-
case_tester: Optional[Callable[[str], None]] = None,
284-
ignore_case: Optional[List[str]] = None,
288+
case_tester: Callable[[str], None] | None = None,
289+
ignore_case: list[str] | None = None,
285290
) -> None:
286291
"""
287292
1. Validate that casing is correct for both response and schema
@@ -294,7 +299,7 @@ def test_openapi_object(
294299
write_only_properties = [key for key in properties.keys() if properties[key].get("writeOnly")]
295300
required_keys = [key for key in schema_section.get("required", []) if key not in write_only_properties]
296301
response_keys = data.keys()
297-
additional_properties: Optional[Union[bool, dict]] = schema_section.get("additionalProperties")
302+
additional_properties: bool | dict | None = schema_section.get("additionalProperties")
298303
additional_properties_allowed = additional_properties is not None
299304
if additional_properties_allowed and not isinstance(additional_properties, (bool, dict)):
300305
raise OpenAPISchemaError("Invalid additionalProperties type")
@@ -350,9 +355,9 @@ def test_openapi_array(self, schema_section: dict, data: dict, reference: str, *
350355
def validate_response(
351356
self,
352357
response: Response,
353-
case_tester: Optional[Callable[[str], None]] = None,
354-
ignore_case: Optional[List[str]] = None,
355-
validators: Optional[List[Callable[[dict, Any], Optional[str]]]] = None,
358+
case_tester: Callable[[str], None] | None = None,
359+
ignore_case: list[str] | None = None,
360+
validators: list[Callable[[dict, Any], str | None]] | None = None,
356361
):
357362
"""
358363
Verifies that an OpenAPI schema definition matches an API response.

openapi_tester/type_declarations.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

openapi_tester/utils.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
""" Utils Module - this file contains utility functions used in multiple places """
2+
from __future__ import annotations
3+
24
from copy import deepcopy
35
from itertools import chain, combinations
4-
from typing import Any, Dict, Iterator, Sequence
6+
from typing import TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from typing import Any, Iterator, Sequence
510

611

7-
def merge_objects(dictionaries: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
12+
def merge_objects(dictionaries: Sequence[dict[str, Any]]) -> dict[str, Any]:
813
"""helper function to deep merge objects"""
9-
output: Dict[str, Any] = {}
14+
output: dict[str, Any] = {}
1015
for dictionary in dictionaries:
1116
for key, value in dictionary.items():
1217
if key not in output:
@@ -24,7 +29,7 @@ def merge_objects(dictionaries: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
2429

2530
def normalize_schema_section(schema_section: dict) -> dict:
2631
"""helper method to remove allOf and handle edge uses of oneOf"""
27-
output: Dict[str, Any] = deepcopy(schema_section)
32+
output: dict[str, Any] = deepcopy(schema_section)
2833
if output.get("allOf"):
2934
all_of = output.pop("allOf")
3035
output = {**output, **merge_objects(all_of)}
@@ -40,7 +45,7 @@ def normalize_schema_section(schema_section: dict) -> dict:
4045
return output
4146

4247

43-
def lazy_combinations(options_list: Sequence[Dict[str, Any]]) -> Iterator[dict]:
48+
def lazy_combinations(options_list: Sequence[dict[str, Any]]) -> Iterator[dict]:
4449
"""helper to lazy evaluate possible permutations of possible combinations"""
4550
for i in range(2, len(options_list) + 1):
4651
for combination in combinations(options_list, i):

0 commit comments

Comments
 (0)