@@ -4,6 +4,7 @@ from contextlib import suppress
44from datetime import date, datetime
55from uuid import UUID
66import enum
7+ from functools import lru_cache
78import inspect
89import io
910import os
@@ -28,6 +29,24 @@ file_type = io.IOBase
2829empty_dict = MappingProxyType({}) # type: ignore
2930
3031
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+
3150class UnsetType(enum.Enum):
3251 unset = 0
3352
@@ -149,6 +168,7 @@ class OpenApiModel:
149168 self._spec_property_naming,
150169 self._check_type,
151170 configuration=self._configuration,
171+ request_cache=None, # No cache available in model __setattr__
152172 )
153173 if isinstance(value, list):
154174 for x in value:
@@ -873,7 +893,6 @@ def order_response_types(required_types):
873893 of list or dict with class information inside it.
874894 :rtype: list
875895 """
876-
877896 def index_getter(class_or_instance):
878897 if isinstance(class_or_instance, list):
879898 return COERCION_INDEX_BY_TYPE[list]
@@ -890,7 +909,7 @@ def order_response_types(required_types):
890909 raise ApiValueError("Unsupported type: %s" % class_or_instance)
891910
892911 sorted_types = sorted(required_types, key=index_getter)
893- return sorted_types
912+ return tuple( sorted_types)
894913
895914
896915def remove_uncoercible(required_types_classes, current_item, spec_property_naming, must_convert=True):
@@ -914,7 +933,11 @@ def remove_uncoercible(required_types_classes, current_item, spec_property_namin
914933 :rtype: list
915934 """
916935 current_type_simple = get_simple_class(current_item)
936+ return list(_remove_uncoercible_impl(required_types_classes, current_type_simple, spec_property_naming, must_convert))
917937
938+
939+ def _remove_uncoercible_impl(required_types_classes, current_type_simple, spec_property_naming, must_convert=True):
940+ """Implementation of remove_uncoercible logic."""
918941 results_classes = []
919942 for required_type_class in required_types_classes:
920943 # convert our models to OpenApiModel
@@ -936,7 +959,7 @@ def remove_uncoercible(required_types_classes, current_item, spec_property_namin
936959 results_classes.append(required_type_class)
937960 elif class_pair in UPCONVERSION_TYPE_PAIRS:
938961 results_classes.append(required_type_class)
939- return results_classes
962+ return tuple( results_classes)
940963
941964
942965def get_possible_classes(cls, from_server_context):
@@ -948,7 +971,7 @@ def get_possible_classes(cls, from_server_context):
948971 return possible_classes
949972
950973
951- def get_required_type_classes(required_types_mixed, spec_property_naming):
974+ def get_required_type_classes(required_types_mixed, spec_property_naming, request_cache=None ):
952975 """Converts the tuple required_types into a tuple and a dict described below.
953976
954977 :param required_types_mixed: Will contain either classes or instance of
@@ -968,6 +991,23 @@ def get_required_type_classes(required_types_mixed, spec_property_naming):
968991
969992 :rtype: tuple
970993 """
994+ # PERFORMANCE: Cache expensive type class computation within request
995+ if request_cache is not None:
996+ cache_key = ('get_required_type_classes', _make_hashable(required_types_mixed), spec_property_naming)
997+ if cache_key in request_cache:
998+ return request_cache[cache_key]
999+ else:
1000+ cache_key = None
1001+
1002+ result = _get_required_type_classes_impl(required_types_mixed, spec_property_naming)
1003+
1004+ if cache_key and request_cache is not None:
1005+ request_cache[cache_key] = result
1006+ return result
1007+
1008+
1009+ def _get_required_type_classes_impl(required_types_mixed, spec_property_naming):
1010+ """Implementation of get_required_type_classes without caching."""
9711011 valid_classes = []
9721012 child_req_types_by_current_type = {}
9731013 for required_type in required_types_mixed:
@@ -1167,6 +1207,7 @@ def attempt_convert_item(
11671207 key_type=False,
11681208 must_convert=False,
11691209 check_type=True,
1210+ request_cache=None,
11701211):
11711212 """
11721213 :param input_value: The data to convert.
@@ -1265,7 +1306,7 @@ def is_valid_type(input_class_simple, valid_classes):
12651306
12661307
12671308def validate_and_convert_types(
1268- input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None
1309+ input_value, required_types_mixed, path_to_item, spec_property_naming, check_type, configuration=None, request_cache=None
12691310):
12701311 """Raises a TypeError is there is a problem, otherwise returns value.
12711312
@@ -1287,27 +1328,46 @@ def validate_and_convert_types(
12871328 :param configuration:: The configuration class to use when converting
12881329 file_type items.
12891330 :type configuration: Configuration
1331+ :param request_cache: Optional cache dict for storing validation results
1332+ within a single request to avoid redundant validations.
1333+ :type request_cache: dict
12901334
12911335 :return: The correctly typed value.
12921336
12931337 :raise: ApiTypeError
12941338 """
1295- results = get_required_type_classes(required_types_mixed, spec_property_naming)
1339+ # Per-request caching: Cache validation results within a single request
1340+ cache_key = None
1341+ if request_cache is not None:
1342+ try:
1343+ input_hash = _make_hashable(input_value)
1344+ cache_key = (input_hash, _make_hashable(required_types_mixed), tuple(path_to_item), spec_property_naming, check_type)
1345+ if cache_key in request_cache:
1346+ return request_cache[cache_key]
1347+ except (TypeError, AttributeError):
1348+ # If we can't create a cache key, proceed without caching
1349+ cache_key = None
1350+
1351+ results = get_required_type_classes(required_types_mixed, spec_property_naming, request_cache)
12961352 valid_classes, child_req_types_by_current_type = results
12971353
12981354 input_class_simple = get_simple_class(input_value)
12991355 valid_type = is_valid_type(input_class_simple, valid_classes)
13001356 if not valid_type:
13011357 # if input_value is not valid_type try to convert it
1302- return attempt_convert_item(
1358+ result = attempt_convert_item(
13031359 input_value,
13041360 valid_classes,
13051361 path_to_item,
13061362 configuration,
13071363 spec_property_naming,
13081364 must_convert=True,
13091365 check_type=check_type,
1366+ request_cache=request_cache,
13101367 )
1368+ if cache_key and request_cache is not None:
1369+ request_cache[cache_key] = result
1370+ return result
13111371
13121372 # input_value's type is in valid_classes
13131373 if len(valid_classes) > 1 and configuration:
@@ -1316,64 +1376,87 @@ def validate_and_convert_types(
13161376 valid_classes, input_value, spec_property_naming, must_convert=False
13171377 )
13181378 if valid_classes_coercible:
1319- return attempt_convert_item(
1379+ result = attempt_convert_item(
13201380 input_value,
13211381 valid_classes_coercible,
13221382 path_to_item,
13231383 configuration,
13241384 spec_property_naming,
13251385 check_type=check_type,
1386+ request_cache=request_cache,
13261387 )
1388+ if cache_key and request_cache is not None:
1389+ request_cache[cache_key] = result
1390+ return result
13271391
13281392 if child_req_types_by_current_type == {}:
13291393 # all types are of the required types and there are no more inner
13301394 # variables left to look at
1395+ if cache_key and request_cache is not None:
1396+ request_cache[cache_key] = input_value
13311397 return input_value
13321398 inner_required_types = child_req_types_by_current_type.get(type(input_value))
13331399 if inner_required_types is None:
13341400 # for this type, there are not more inner variables left to look at
1401+ if cache_key and request_cache is not None:
1402+ request_cache[cache_key] = input_value
13351403 return input_value
13361404 if isinstance(input_value, list):
13371405 if input_value == []:
13381406 # allow an empty list
13391407 return input_value
13401408 result = []
13411409 for index, inner_value in enumerate(input_value):
1342- inner_path = list(path_to_item)
1343- inner_path.append(index)
1410+ path_to_item.append(index)
13441411 try:
13451412 result.append(
13461413 validate_and_convert_types(
13471414 inner_value,
13481415 inner_required_types,
1349- inner_path ,
1416+ path_to_item ,
13501417 spec_property_naming,
13511418 check_type,
13521419 configuration=configuration,
1420+ request_cache=request_cache,
13531421 )
13541422 )
13551423 except TypeError:
13561424 result.append(UnparsedObject(**inner_value))
1425+ finally:
1426+ # Restore path state
1427+ path_to_item.pop()
1428+ if cache_key and request_cache is not None:
1429+ request_cache[cache_key] = result
13571430 return result
13581431 elif isinstance(input_value, dict):
13591432 if input_value == {}:
13601433 # allow an empty dict
1434+ if cache_key and request_cache is not None:
1435+ request_cache[cache_key] = input_value
13611436 return input_value
13621437 result = {}
13631438 for inner_key, inner_val in input_value.items():
1364- inner_path = list(path_to_item)
1365- inner_path.append(inner_key)
1366- if get_simple_class(inner_key) != str:
1367- raise get_type_error(inner_key, inner_path, valid_classes, key_type=True)
1368- result[inner_key] = validate_and_convert_types(
1369- inner_val,
1370- inner_required_types,
1371- inner_path,
1372- spec_property_naming,
1373- check_type,
1374- configuration=configuration,
1375- )
1439+ path_to_item.append(inner_key)
1440+ try:
1441+ if get_simple_class(inner_key) != str:
1442+ raise get_type_error(inner_key, path_to_item, valid_classes, key_type=True)
1443+ result[inner_key] = validate_and_convert_types(
1444+ inner_val,
1445+ inner_required_types,
1446+ path_to_item,
1447+ spec_property_naming,
1448+ check_type,
1449+ configuration=configuration,
1450+ request_cache=request_cache,
1451+ )
1452+ finally:
1453+ # Restore path state
1454+ path_to_item.pop()
1455+ if cache_key and request_cache is not None:
1456+ request_cache[cache_key] = result
13761457 return result
1458+ if cache_key and request_cache is not None:
1459+ request_cache[cache_key] = input_value
13771460 return input_value
13781461
13791462
@@ -1611,6 +1694,7 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
16111694 constant_kwargs.get("_spec_property_naming", False),
16121695 constant_kwargs.get("_check_type", True),
16131696 configuration=constant_kwargs.get("_configuration"),
1697+ request_cache=None, # No cache available in this context
16141698 )
16151699 oneof_instances.append(oneof_instance)
16161700 if len(oneof_instances) != 1:
0 commit comments