@@ -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
@@ -146,6 +165,7 @@ class OpenApiModel:
146165 self._spec_property_naming,
147166 self._check_type,
148167 configuration=self._configuration,
168+ request_cache=None, # No cache available in model __setattr__
149169 )
150170 if isinstance(value, list):
151171 for x in value:
@@ -870,7 +890,6 @@ def order_response_types(required_types):
870890 of list or dict with class information inside it.
871891 :rtype: list
872892 """
873-
874893 def index_getter(class_or_instance):
875894 if isinstance(class_or_instance, list):
876895 return COERCION_INDEX_BY_TYPE[list]
@@ -887,31 +906,11 @@ def order_response_types(required_types):
887906 raise ApiValueError("Unsupported type: %s" % class_or_instance)
888907
889908 sorted_types = sorted(required_types, key=index_getter)
890- return sorted_types
891-
909+ return tuple(sorted_types)
892910
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)
914911
912+ def _remove_uncoercible_impl(required_types_classes, current_type_simple, spec_property_naming, must_convert=True):
913+ """Implementation of remove_uncoercible logic."""
915914 results_classes = []
916915 for required_type_class in required_types_classes:
917916 # convert our models to OpenApiModel
@@ -933,7 +932,31 @@ def remove_uncoercible(required_types_classes, current_item, spec_property_namin
933932 results_classes.append(required_type_class)
934933 elif class_pair in UPCONVERSION_TYPE_PAIRS:
935934 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))
937960
938961
939962def get_possible_classes(cls, from_server_context):
@@ -945,7 +968,7 @@ def get_possible_classes(cls, from_server_context):
945968 return possible_classes
946969
947970
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 ):
949972 """Converts the tuple required_types into a tuple and a dict described below.
950973
951974 :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):
965988
966989 :rtype: tuple
967990 """
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."""
9681008 valid_classes = []
9691009 child_req_types_by_current_type = {}
9701010 for required_type in required_types_mixed:
@@ -1164,6 +1204,7 @@ def attempt_convert_item(
11641204 key_type=False,
11651205 must_convert=False,
11661206 check_type=True,
1207+ request_cache=None,
11671208):
11681209 """
11691210 :param input_value: The data to convert.
@@ -1262,7 +1303,7 @@ def is_valid_type(input_class_simple, valid_classes):
12621303
12631304
12641305def 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
12661307):
12671308 """Raises a TypeError is there is a problem, otherwise returns value.
12681309
@@ -1284,27 +1325,46 @@ def validate_and_convert_types(
12841325 :param configuration:: The configuration class to use when converting
12851326 file_type items.
12861327 :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
12871331
12881332 :return: The correctly typed value.
12891333
12901334 :raise: ApiTypeError
12911335 """
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)
12931349 valid_classes, child_req_types_by_current_type = results
12941350
12951351 input_class_simple = get_simple_class(input_value)
12961352 valid_type = is_valid_type(input_class_simple, valid_classes)
12971353 if not valid_type:
12981354 # if input_value is not valid_type try to convert it
1299- return attempt_convert_item(
1355+ result = attempt_convert_item(
13001356 input_value,
13011357 valid_classes,
13021358 path_to_item,
13031359 configuration,
13041360 spec_property_naming,
13051361 must_convert=True,
13061362 check_type=check_type,
1363+ request_cache=request_cache,
13071364 )
1365+ if cache_key and request_cache is not None:
1366+ request_cache[cache_key] = result
1367+ return result
13081368
13091369 # input_value's type is in valid_classes
13101370 if len(valid_classes) > 1 and configuration:
@@ -1313,64 +1373,87 @@ def validate_and_convert_types(
13131373 valid_classes, input_value, spec_property_naming, must_convert=False
13141374 )
13151375 if valid_classes_coercible:
1316- return attempt_convert_item(
1376+ result = attempt_convert_item(
13171377 input_value,
13181378 valid_classes_coercible,
13191379 path_to_item,
13201380 configuration,
13211381 spec_property_naming,
13221382 check_type=check_type,
1383+ request_cache=request_cache,
13231384 )
1385+ if cache_key and request_cache is not None:
1386+ request_cache[cache_key] = result
1387+ return result
13241388
13251389 if child_req_types_by_current_type == {}:
13261390 # all types are of the required types and there are no more inner
13271391 # variables left to look at
1392+ if cache_key and request_cache is not None:
1393+ request_cache[cache_key] = input_value
13281394 return input_value
13291395 inner_required_types = child_req_types_by_current_type.get(type(input_value))
13301396 if inner_required_types is None:
13311397 # 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
13321400 return input_value
13331401 if isinstance(input_value, list):
13341402 if input_value == []:
13351403 # allow an empty list
13361404 return input_value
13371405 result = []
13381406 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)
13411408 try:
13421409 result.append(
13431410 validate_and_convert_types(
13441411 inner_value,
13451412 inner_required_types,
1346- inner_path ,
1413+ path_to_item ,
13471414 spec_property_naming,
13481415 check_type,
13491416 configuration=configuration,
1417+ request_cache=request_cache,
13501418 )
13511419 )
13521420 except TypeError:
13531421 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
13541427 return result
13551428 elif isinstance(input_value, dict):
13561429 if input_value == {}:
13571430 # allow an empty dict
1431+ if cache_key and request_cache is not None:
1432+ request_cache[cache_key] = input_value
13581433 return input_value
13591434 result = {}
13601435 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
13731454 return result
1455+ if cache_key and request_cache is not None:
1456+ request_cache[cache_key] = input_value
13741457 return input_value
13751458
13761459
@@ -1581,6 +1664,7 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
15811664 constant_kwargs.get("_spec_property_naming", False),
15821665 constant_kwargs.get("_check_type", True),
15831666 configuration=constant_kwargs.get("_configuration"),
1667+ request_cache=None, # No cache available in this context
15841668 )
15851669 oneof_instances.append(oneof_instance)
15861670 if len(oneof_instances) != 1:
0 commit comments