@@ -4,6 +4,7 @@ from contextlib import suppress
4
4
from datetime import date, datetime
5
5
from uuid import UUID
6
6
import enum
7
+ from functools import lru_cache
7
8
import inspect
8
9
import io
9
10
import os
@@ -28,6 +29,24 @@ file_type = io.IOBase
28
29
empty_dict = MappingProxyType({}) # type: ignore
29
30
30
31
32
+ def _make_hashable(obj):
33
+ """Convert potentially unhashable objects to hashable representations for caching."""
34
+ if isinstance(obj, (list, tuple)):
35
+ return tuple(_make_hashable(item) for item in obj)
36
+ elif isinstance(obj, dict):
37
+ return tuple(sorted((_make_hashable(k), _make_hashable(v)) for k, v in obj.items()))
38
+ elif isinstance(obj, set):
39
+ return tuple(sorted(_make_hashable(item) for item in obj))
40
+ elif hasattr(obj, '__name__'): # Classes and functions
41
+ return obj.__name__
42
+ else:
43
+ try:
44
+ hash(obj)
45
+ return obj
46
+ except TypeError:
47
+ return str(obj)
48
+
49
+
31
50
class UnsetType(enum.Enum):
32
51
unset = 0
33
52
@@ -146,6 +165,7 @@ class OpenApiModel:
146
165
self._spec_property_naming,
147
166
self._check_type,
148
167
configuration=self._configuration,
168
+ request_cache=None, # No cache available in model __setattr__
149
169
)
150
170
if isinstance(value, list):
151
171
for x in value:
@@ -870,7 +890,6 @@ def order_response_types(required_types):
870
890
of list or dict with class information inside it.
871
891
:rtype: list
872
892
"""
873
-
874
893
def index_getter(class_or_instance):
875
894
if isinstance(class_or_instance, list):
876
895
return COERCION_INDEX_BY_TYPE[list]
@@ -887,31 +906,11 @@ def order_response_types(required_types):
887
906
raise ApiValueError("Unsupported type: %s" % class_or_instance)
888
907
889
908
sorted_types = sorted(required_types, key=index_getter)
890
- return sorted_types
891
-
909
+ return tuple(sorted_types)
892
910
893
- def remove_uncoercible(required_types_classes, current_item, spec_property_naming, must_convert=True):
894
- """Only keeps the type conversions that are possible.
895
-
896
- :param required_types_classes: Classes that are required, these should be
897
- ordered by COERCION_INDEX_BY_TYPE.
898
- :type required_types_classes: tuple
899
- :param spec_property_naming: True if the variable names in the input data
900
- are serialized names as specified in the OpenAPI document. False if the
901
- variables names in the input data are python variable names in PEP-8 snake
902
- case.
903
- :type spec_property_naming: bool
904
- :param current_item: The current item (input data) to be converted.
905
-
906
- :param must_convert: If True the item to convert is of the wrong type and
907
- we want a big list of coercibles if False, we want a limited list of coercibles.
908
- :type must_convert: bool
909
-
910
- :return: The remaining coercible required types, classes only.
911
- :rtype: list
912
- """
913
- current_type_simple = get_simple_class(current_item)
914
911
912
+ def _remove_uncoercible_impl(required_types_classes, current_type_simple, spec_property_naming, must_convert=True):
913
+ """Implementation of remove_uncoercible logic."""
915
914
results_classes = []
916
915
for required_type_class in required_types_classes:
917
916
# convert our models to OpenApiModel
@@ -933,7 +932,31 @@ def remove_uncoercible(required_types_classes, current_item, spec_property_namin
933
932
results_classes.append(required_type_class)
934
933
elif class_pair in UPCONVERSION_TYPE_PAIRS:
935
934
results_classes.append(required_type_class)
936
- return results_classes
935
+ return tuple(results_classes)
936
+
937
+
938
+ def remove_uncoercible(required_types_classes, current_item, spec_property_naming, must_convert=True):
939
+ """Only keeps the type conversions that are possible.
940
+
941
+ :param required_types_classes: Classes that are required, these should be
942
+ ordered by COERCION_INDEX_BY_TYPE.
943
+ :type required_types_classes: tuple
944
+ :param spec_property_naming: True if the variable names in the input data
945
+ are serialized names as specified in the OpenAPI document. False if the
946
+ variables names in the input data are python variable names in PEP-8 snake
947
+ case.
948
+ :type spec_property_naming: bool
949
+ :param current_item: The current item (input data) to be converted.
950
+
951
+ :param must_convert: If True the item to convert is of the wrong type and
952
+ we want a big list of coercibles if False, we want a limited list of coercibles.
953
+ :type must_convert: bool
954
+
955
+ :return: The remaining coercible required types, classes only.
956
+ :rtype: list
957
+ """
958
+ current_type_simple = get_simple_class(current_item)
959
+ return list(_remove_uncoercible_impl(required_types_classes, current_type_simple, spec_property_naming, must_convert))
937
960
938
961
939
962
def get_possible_classes(cls, from_server_context):
@@ -945,7 +968,7 @@ def get_possible_classes(cls, from_server_context):
945
968
return possible_classes
946
969
947
970
948
- def get_required_type_classes(required_types_mixed, spec_property_naming):
971
+ def get_required_type_classes(required_types_mixed, spec_property_naming, request_cache=None ):
949
972
"""Converts the tuple required_types into a tuple and a dict described below.
950
973
951
974
:param required_types_mixed: Will contain either classes or instance of
@@ -965,6 +988,23 @@ def get_required_type_classes(required_types_mixed, spec_property_naming):
965
988
966
989
:rtype: tuple
967
990
"""
991
+ # PERFORMANCE: Cache expensive type class computation within request
992
+ if request_cache is not None:
993
+ cache_key = ('get_required_type_classes', _make_hashable(required_types_mixed), spec_property_naming)
994
+ if cache_key in request_cache:
995
+ return request_cache[cache_key]
996
+ else:
997
+ cache_key = None
998
+
999
+ result = _get_required_type_classes_impl(required_types_mixed, spec_property_naming)
1000
+
1001
+ if cache_key and request_cache is not None:
1002
+ request_cache[cache_key] = result
1003
+ return result
1004
+
1005
+
1006
+ def _get_required_type_classes_impl(required_types_mixed, spec_property_naming):
1007
+ """Implementation of get_required_type_classes without caching."""
968
1008
valid_classes = []
969
1009
child_req_types_by_current_type = {}
970
1010
for required_type in required_types_mixed:
@@ -1164,6 +1204,7 @@ def attempt_convert_item(
1164
1204
key_type=False,
1165
1205
must_convert=False,
1166
1206
check_type=True,
1207
+ request_cache=None,
1167
1208
):
1168
1209
"""
1169
1210
:param input_value: The data to convert.
@@ -1262,7 +1303,7 @@ def is_valid_type(input_class_simple, valid_classes):
1262
1303
1263
1304
1264
1305
def validate_and_convert_types(
1265
- input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None
1306
+ input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None, request_cache=None
1266
1307
):
1267
1308
"""Raises a TypeError is there is a problem, otherwise returns value.
1268
1309
@@ -1284,27 +1325,46 @@ def validate_and_convert_types(
1284
1325
:param configuration:: The configuration class to use when converting
1285
1326
file_type items.
1286
1327
:type configuration: Configuration
1328
+ :param request_cache: Optional cache dict for storing validation results
1329
+ within a single request to avoid redundant validations.
1330
+ :type request_cache: dict
1287
1331
1288
1332
:return: The correctly typed value.
1289
1333
1290
1334
:raise: ApiTypeError
1291
1335
"""
1292
- results = get_required_type_classes(required_types_mixed, spec_property_naming)
1336
+ # Per-request caching: Cache validation results within a single request
1337
+ cache_key = None
1338
+ if request_cache is not None:
1339
+ try:
1340
+ input_hash = _make_hashable(input_value)
1341
+ cache_key = (input_hash, _make_hashable(required_types_mixed), tuple(path_to_item), spec_property_naming, check_type)
1342
+ if cache_key in request_cache:
1343
+ return request_cache[cache_key]
1344
+ except (TypeError, AttributeError):
1345
+ # If we can't create a cache key, proceed without caching
1346
+ cache_key = None
1347
+
1348
+ results = get_required_type_classes(required_types_mixed, spec_property_naming, request_cache)
1293
1349
valid_classes, child_req_types_by_current_type = results
1294
1350
1295
1351
input_class_simple = get_simple_class(input_value)
1296
1352
valid_type = is_valid_type(input_class_simple, valid_classes)
1297
1353
if not valid_type:
1298
1354
# if input_value is not valid_type try to convert it
1299
- return attempt_convert_item(
1355
+ result = attempt_convert_item(
1300
1356
input_value,
1301
1357
valid_classes,
1302
1358
path_to_item,
1303
1359
configuration,
1304
1360
spec_property_naming,
1305
1361
must_convert=True,
1306
1362
check_type=check_type,
1363
+ request_cache=request_cache,
1307
1364
)
1365
+ if cache_key and request_cache is not None:
1366
+ request_cache[cache_key] = result
1367
+ return result
1308
1368
1309
1369
# input_value's type is in valid_classes
1310
1370
if len(valid_classes) > 1 and configuration:
@@ -1313,64 +1373,87 @@ def validate_and_convert_types(
1313
1373
valid_classes, input_value, spec_property_naming, must_convert=False
1314
1374
)
1315
1375
if valid_classes_coercible:
1316
- return attempt_convert_item(
1376
+ result = attempt_convert_item(
1317
1377
input_value,
1318
1378
valid_classes_coercible,
1319
1379
path_to_item,
1320
1380
configuration,
1321
1381
spec_property_naming,
1322
1382
check_type=check_type,
1383
+ request_cache=request_cache,
1323
1384
)
1385
+ if cache_key and request_cache is not None:
1386
+ request_cache[cache_key] = result
1387
+ return result
1324
1388
1325
1389
if child_req_types_by_current_type == {}:
1326
1390
# all types are of the required types and there are no more inner
1327
1391
# variables left to look at
1392
+ if cache_key and request_cache is not None:
1393
+ request_cache[cache_key] = input_value
1328
1394
return input_value
1329
1395
inner_required_types = child_req_types_by_current_type.get(type(input_value))
1330
1396
if inner_required_types is None:
1331
1397
# for this type, there are not more inner variables left to look at
1398
+ if cache_key and request_cache is not None:
1399
+ request_cache[cache_key] = input_value
1332
1400
return input_value
1333
1401
if isinstance(input_value, list):
1334
1402
if input_value == []:
1335
1403
# allow an empty list
1336
1404
return input_value
1337
1405
result = []
1338
1406
for index, inner_value in enumerate(input_value):
1339
- inner_path = list(path_to_item)
1340
- inner_path.append(index)
1407
+ path_to_item.append(index)
1341
1408
try:
1342
1409
result.append(
1343
1410
validate_and_convert_types(
1344
1411
inner_value,
1345
1412
inner_required_types,
1346
- inner_path ,
1413
+ path_to_item ,
1347
1414
spec_property_naming,
1348
1415
check_type,
1349
1416
configuration=configuration,
1417
+ request_cache=request_cache,
1350
1418
)
1351
1419
)
1352
1420
except TypeError:
1353
1421
result.append(UnparsedObject(**inner_value))
1422
+ finally:
1423
+ # Restore path state
1424
+ path_to_item.pop()
1425
+ if cache_key and request_cache is not None:
1426
+ request_cache[cache_key] = result
1354
1427
return result
1355
1428
elif isinstance(input_value, dict):
1356
1429
if input_value == {}:
1357
1430
# allow an empty dict
1431
+ if cache_key and request_cache is not None:
1432
+ request_cache[cache_key] = input_value
1358
1433
return input_value
1359
1434
result = {}
1360
1435
for inner_key, inner_val in input_value.items():
1361
- inner_path = list(path_to_item)
1362
- inner_path.append(inner_key)
1363
- if get_simple_class(inner_key) != str:
1364
- raise get_type_error(inner_key, inner_path, valid_classes, key_type=True)
1365
- result[inner_key] = validate_and_convert_types(
1366
- inner_val,
1367
- inner_required_types,
1368
- inner_path,
1369
- spec_property_naming,
1370
- check_type,
1371
- configuration=configuration,
1372
- )
1436
+ path_to_item.append(inner_key)
1437
+ try:
1438
+ if get_simple_class(inner_key) != str:
1439
+ raise get_type_error(inner_key, path_to_item, valid_classes, key_type=True)
1440
+ result[inner_key] = validate_and_convert_types(
1441
+ inner_val,
1442
+ inner_required_types,
1443
+ path_to_item,
1444
+ spec_property_naming,
1445
+ check_type,
1446
+ configuration=configuration,
1447
+ request_cache=request_cache,
1448
+ )
1449
+ finally:
1450
+ # Restore path state
1451
+ path_to_item.pop()
1452
+ if cache_key and request_cache is not None:
1453
+ request_cache[cache_key] = result
1373
1454
return result
1455
+ if cache_key and request_cache is not None:
1456
+ request_cache[cache_key] = input_value
1374
1457
return input_value
1375
1458
1376
1459
@@ -1581,6 +1664,7 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
1581
1664
constant_kwargs.get("_spec_property_naming", False),
1582
1665
constant_kwargs.get("_check_type", True),
1583
1666
configuration=constant_kwargs.get("_configuration"),
1667
+ request_cache=None, # No cache available in this context
1584
1668
)
1585
1669
oneof_instances.append(oneof_instance)
1586
1670
if len(oneof_instances) != 1:
0 commit comments