@typespec/http-client-python 0.29.0 → 0.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -612,6 +612,239 @@ def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typin
612
612
  return _serialize(value, rf._format)
613
613
 
614
614
 
615
+ # ============================================================================
616
+ # Fast-path scalar deserializer functions for rest_field(deserializer=...)
617
+ # These are referenced from rest_field declarations to bypass the generic
618
+ # _deserialize -> _deserialize_with_callable chain.
619
+ # Only simple/primitive types — no models or container types.
620
+ # ============================================================================
621
+
622
+
623
+ def _xml_deser_str(value):
624
+ if isinstance(value, ET.Element):
625
+ return value.text or ""
626
+ return str(value) if value is not None else None
627
+
628
+
629
+ def _xml_deser_int(value):
630
+ if isinstance(value, ET.Element):
631
+ return int(value.text) if value.text else None
632
+ return int(value) if value is not None else None
633
+
634
+
635
+ def _xml_deser_float(value):
636
+ if isinstance(value, ET.Element):
637
+ return float(value.text) if value.text else None
638
+ return float(value) if value is not None else None
639
+
640
+
641
+ def _xml_deser_bool(value):
642
+ if isinstance(value, ET.Element):
643
+ text = value.text
644
+ else:
645
+ text = value
646
+ if text is None:
647
+ return None
648
+ if text in (True, False):
649
+ return text
650
+ return text.lower() == "true"
651
+
652
+
653
+ # pylint: disable=docstring-missing-param
654
+ def _xml_deser_bytes(value):
655
+ """Deserialize bytes from XML (base64)."""
656
+ if isinstance(value, ET.Element):
657
+ text = value.text
658
+ else:
659
+ text = value
660
+ if text is None:
661
+ return None
662
+ return _deserialize_bytes(text)
663
+
664
+
665
+ def _xml_deser_bytes_base64url(value):
666
+ """Deserialize bytes from XML (base64url)."""
667
+ if isinstance(value, ET.Element):
668
+ text = value.text
669
+ else:
670
+ text = value
671
+ if text is None:
672
+ return None
673
+ return _deserialize_bytes_base64(text)
674
+
675
+
676
+ def _xml_deser_datetime(value):
677
+ """Deserialize a datetime from XML (ISO 8601 / rfc3339)."""
678
+ if isinstance(value, ET.Element):
679
+ text = value.text
680
+ else:
681
+ text = value
682
+ if text is None:
683
+ return None
684
+ return _deserialize_datetime(text)
685
+
686
+
687
+ def _xml_deser_datetime_rfc7231(value):
688
+ """Deserialize a datetime from XML (RFC7231 format)."""
689
+ if isinstance(value, ET.Element):
690
+ text = value.text
691
+ else:
692
+ text = value
693
+ if text is None:
694
+ return None
695
+ return _deserialize_datetime_rfc7231(text)
696
+
697
+
698
+ def _xml_deser_datetime_unix_timestamp(value):
699
+ """Deserialize a datetime from XML (Unix timestamp)."""
700
+ if isinstance(value, ET.Element):
701
+ text = value.text
702
+ else:
703
+ text = value
704
+ if text is None:
705
+ return None
706
+ return _deserialize_datetime_unix_timestamp(float(text))
707
+
708
+
709
+ def _xml_deser_date(value):
710
+ """Deserialize a date from XML (ISO 8601)."""
711
+ if isinstance(value, ET.Element):
712
+ text = value.text
713
+ else:
714
+ text = value
715
+ if text is None:
716
+ return None
717
+ return _deserialize_date(text)
718
+
719
+
720
+ def _xml_deser_time(value):
721
+ """Deserialize a time from XML (ISO 8601)."""
722
+ if isinstance(value, ET.Element):
723
+ text = value.text
724
+ else:
725
+ text = value
726
+ if text is None:
727
+ return None
728
+ return _deserialize_time(text)
729
+
730
+
731
+ def _xml_deser_duration(value):
732
+ """Deserialize a timedelta from XML (ISO 8601 duration)."""
733
+ if isinstance(value, ET.Element):
734
+ text = value.text
735
+ else:
736
+ text = value
737
+ if text is None:
738
+ return None
739
+ return _deserialize_duration(text)
740
+
741
+
742
+ def _xml_deser_decimal(value):
743
+ """Deserialize a Decimal from XML."""
744
+ if isinstance(value, ET.Element):
745
+ text = value.text
746
+ else:
747
+ text = value
748
+ if text is None:
749
+ return None
750
+ return _deserialize_decimal(text)
751
+
752
+
753
+ def _xml_deser_enum_or_str(enum_cls, value):
754
+ """Deserialize a Union[EnumType, str] from XML."""
755
+ text = value.text if isinstance(value, ET.Element) else value
756
+ if text is None:
757
+ return None
758
+ try:
759
+ return enum_cls(text)
760
+ except ValueError:
761
+ return text
762
+
763
+
764
+ def _extract_xml_model_type(rf_type):
765
+ """Extract the concrete Model class from a resolved rf._type partial chain.
766
+
767
+ Unwraps ``Optional[Model]`` and ``_deserialize_model(Model, ...)``
768
+ wrappers. Only handles Model and Optional[Model] — other composite
769
+ types (List, Dict, Union, etc.) return None and fall through to the
770
+ generic ``_deserialize`` path at runtime.
771
+ """
772
+ if rf_type is None:
773
+ return None
774
+ if isinstance(rf_type, type) and _is_model(rf_type):
775
+ return rf_type
776
+ if not isinstance(rf_type, functools.partial):
777
+ return None
778
+ func = rf_type.func
779
+ args = rf_type.args
780
+ if func is _deserialize_with_optional and args:
781
+ return _extract_xml_model_type(args[0])
782
+ if func is _deserialize_model and args:
783
+ cls = args[0]
784
+ return cls if isinstance(cls, type) and _is_model(cls) else None
785
+ return None
786
+
787
+
788
+ def _build_xml_field_plan( # pylint: disable=docstring-missing-return, docstring-missing-rtype, unused-variable
789
+ cls, attr_to_rest_field: dict
790
+ ) -> list:
791
+ """Build a precomputed XML field plan for fast _init_from_xml iteration.
792
+
793
+ Called once per model class in __new__. Returns a list of tuples:
794
+ (rest_name, xml_name, kind, deser, rf_type, is_optional, items_name)
795
+
796
+ kind: 0=wrapped, 1=attribute, 2=unwrapped, 3=text
797
+
798
+ For Model and Optional[Model] fields that lack a scalar
799
+ ``_deserializer``, this function precomputes the Model class as the
800
+ deserializer so ``_init_from_xml`` can call ``ModelClass(element)``
801
+ directly instead of going through the expensive
802
+ ``_get_deserialize_callable_from_annotation`` chain at runtime.
803
+ """
804
+ model_meta = getattr(cls, "_xml", {})
805
+ model_ns = model_meta.get("ns") or model_meta.get("namespace")
806
+ plan = []
807
+
808
+ for rf in attr_to_rest_field.values():
809
+ prop_meta = getattr(rf, "_xml", {})
810
+ deser = rf._deserializer
811
+
812
+ xml_name = prop_meta.get("name", rf._rest_name)
813
+ xml_ns = _resolve_xml_ns(prop_meta, model_meta)
814
+ if xml_ns:
815
+ xml_name = "{" + xml_ns + "}" + xml_name
816
+
817
+ is_optional = rf._is_optional
818
+
819
+ # For Model / Optional[Model] fields without a scalar deserializer,
820
+ # precompute the Model class as the deserializer.
821
+ if deser is None and rf._type is not None:
822
+ model_cls = _extract_xml_model_type(rf._type)
823
+ if model_cls is not None:
824
+ deser = model_cls
825
+
826
+ if prop_meta.get("attribute", False):
827
+ plan.append((rf._rest_name, xml_name, 1, deser, rf._type, is_optional, None))
828
+ elif prop_meta.get("unwrapped", False):
829
+ items_name = prop_meta.get("itemsName")
830
+ if items_name:
831
+ items_ns = prop_meta.get("itemsNs")
832
+ if items_ns is not None:
833
+ xml_ns = items_ns
834
+ if xml_ns:
835
+ items_name = "{" + xml_ns + "}" + items_name
836
+ else:
837
+ items_name = xml_name
838
+ plan.append((rf._rest_name, xml_name, 2, deser, rf._type, is_optional, items_name))
839
+ elif prop_meta.get("text", False):
840
+ plan.append((rf._rest_name, xml_name, 3, deser, rf._type, is_optional, None))
841
+ else:
842
+ plan.append((rf._rest_name, xml_name, 0, deser, rf._type, is_optional, None))
843
+
844
+ return plan
845
+
846
+
847
+ # pylint: enable=docstring-missing-param
615
848
  class Model(_MyMutableMapping):
616
849
  _is_model = True
617
850
  # label whether current class's _attr_to_rest_field has been calculated
@@ -652,7 +885,9 @@ class Model(_MyMutableMapping):
652
885
  dict_to_pass[rf._rest_name] = _create_value(rf, rf._default)
653
886
  super().__init__(dict_to_pass)
654
887
 
655
- def _init_from_xml(self, element: ET.Element) -> dict[str, typing.Any]:
888
+ def _init_from_xml( # pylint: disable=too-many-branches, too-many-statements
889
+ self, element: ET.Element
890
+ ) -> dict[str, typing.Any]:
656
891
  """Deserialize an XML element into a dict mapping rest field names to values.
657
892
 
658
893
  :param ET.Element element: The XML element to deserialize from.
@@ -660,53 +895,89 @@ class Model(_MyMutableMapping):
660
895
  :rtype: dict
661
896
  """
662
897
  result: dict[str, typing.Any] = {}
663
- model_meta = getattr(self, "_xml", {})
664
898
  existed_attr_keys: list[str] = []
665
899
 
666
- for rf in self._attr_to_rest_field.values():
667
- prop_meta = getattr(rf, "_xml", {})
668
- xml_name = prop_meta.get("name", rf._rest_name)
669
- xml_ns = _resolve_xml_ns(prop_meta, model_meta)
670
- if xml_ns:
671
- xml_name = "{" + xml_ns + "}" + xml_name
672
-
673
- # attribute
674
- if prop_meta.get("attribute", False) and element.get(xml_name) is not None:
675
- existed_attr_keys.append(xml_name)
676
- result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name))
677
- continue
678
-
679
- # unwrapped element is array
680
- if prop_meta.get("unwrapped", False):
681
- # unwrapped array could either use prop items meta/prop meta
682
- _items_name = prop_meta.get("itemsName")
683
- if _items_name:
684
- xml_name = _items_name
685
- _items_ns = prop_meta.get("itemsNs")
686
- if _items_ns is not None:
687
- xml_ns = _items_ns
688
- if xml_ns:
689
- xml_name = "{" + xml_ns + "}" + xml_name
690
- items = element.findall(xml_name) # pyright: ignore
691
- if len(items) > 0:
900
+ field_plan = getattr(self, "_xml_field_plan", None)
901
+ if field_plan:
902
+ for rest_name, xml_name, kind, deser, rf_type, is_optional, items_name in field_plan:
903
+ if kind == 0: # wrapped element (most common)
904
+ item = element.find(xml_name)
905
+ if item is not None:
906
+ existed_attr_keys.append(xml_name)
907
+ if deser:
908
+ result[rest_name] = deser(item)
909
+ else:
910
+ result[rest_name] = _deserialize(rf_type, item)
911
+ elif kind == 1: # attribute
912
+ attr_val = element.get(xml_name)
913
+ if attr_val is not None:
914
+ existed_attr_keys.append(xml_name)
915
+ if deser:
916
+ result[rest_name] = deser(attr_val)
917
+ else:
918
+ result[rest_name] = attr_val
919
+ elif kind == 2: # unwrapped array
920
+ items = element.findall(items_name) # pyright: ignore
921
+ if len(items) > 0:
922
+ existed_attr_keys.append(items_name)
923
+ if deser:
924
+ result[rest_name] = deser(items)
925
+ else:
926
+ result[rest_name] = _deserialize(rf_type, items)
927
+ elif not is_optional:
928
+ existed_attr_keys.append(items_name)
929
+ result[rest_name] = []
930
+ elif kind == 3: # text
931
+ if element.text is not None:
932
+ if deser:
933
+ result[rest_name] = deser(element.text)
934
+ else:
935
+ result[rest_name] = element.text
936
+ else:
937
+ model_meta = getattr(self, "_xml", {})
938
+ for rf in self._attr_to_rest_field.values():
939
+ prop_meta = getattr(rf, "_xml", {})
940
+ xml_name = prop_meta.get("name", rf._rest_name)
941
+ xml_ns = _resolve_xml_ns(prop_meta, model_meta)
942
+ if xml_ns:
943
+ xml_name = "{" + xml_ns + "}" + xml_name
944
+
945
+ # attribute
946
+ if prop_meta.get("attribute", False) and element.get(xml_name) is not None:
692
947
  existed_attr_keys.append(xml_name)
693
- result[rf._rest_name] = _deserialize(rf._type, items)
694
- elif not rf._is_optional:
948
+ result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name))
949
+ continue
950
+
951
+ # unwrapped element is array
952
+ if prop_meta.get("unwrapped", False):
953
+ _items_name = prop_meta.get("itemsName")
954
+ if _items_name:
955
+ xml_name = _items_name
956
+ _items_ns = prop_meta.get("itemsNs")
957
+ if _items_ns is not None:
958
+ xml_ns = _items_ns
959
+ if xml_ns:
960
+ xml_name = "{" + xml_ns + "}" + xml_name
961
+ items = element.findall(xml_name) # pyright: ignore
962
+ if len(items) > 0:
963
+ existed_attr_keys.append(xml_name)
964
+ result[rf._rest_name] = _deserialize(rf._type, items)
965
+ elif not rf._is_optional:
966
+ existed_attr_keys.append(xml_name)
967
+ result[rf._rest_name] = []
968
+ continue
969
+
970
+ # text element is primitive type
971
+ if prop_meta.get("text", False):
972
+ if element.text is not None:
973
+ result[rf._rest_name] = _deserialize(rf._type, element.text)
974
+ continue
975
+
976
+ # wrapped element could be normal property or array
977
+ item = element.find(xml_name)
978
+ if item is not None:
695
979
  existed_attr_keys.append(xml_name)
696
- result[rf._rest_name] = []
697
- continue
698
-
699
- # text element is primitive type
700
- if prop_meta.get("text", False):
701
- if element.text is not None:
702
- result[rf._rest_name] = _deserialize(rf._type, element.text)
703
- continue
704
-
705
- # wrapped element could be normal property or array, it should only have one element
706
- item = element.find(xml_name)
707
- if item is not None:
708
- existed_attr_keys.append(xml_name)
709
- result[rf._rest_name] = _deserialize(rf._type, item)
980
+ result[rf._rest_name] = _deserialize(rf._type, item)
710
981
 
711
982
  # rest thing is additional properties
712
983
  for e in element:
@@ -745,6 +1016,9 @@ class Model(_MyMutableMapping):
745
1016
  ._attr_to_rest_field.items()
746
1017
  }
747
1018
  {% endif %}
1019
+ # Build XML field plan for fast _init_from_xml (only for XML models)
1020
+ if getattr(cls, "_xml", None):
1021
+ cls._xml_field_plan = _build_xml_field_plan(cls, attr_to_rest_field)
748
1022
  cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
749
1023
 
750
1024
  return super().__new__(cls)
@@ -1132,6 +1406,7 @@ class _RestField:
1132
1406
  format: typing.Optional[str] = None,
1133
1407
  is_multipart_file_input: bool = False,
1134
1408
  xml: typing.Optional[dict[str, typing.Any]] = None,
1409
+ deserializer: typing.Optional[typing.Callable] = None,
1135
1410
  {% if code_model.has_padded_model_property %}
1136
1411
  original_tsp_name: typing.Optional[str] = None,
1137
1412
  {% endif %}
@@ -1147,6 +1422,7 @@ class _RestField:
1147
1422
  self._format = format
1148
1423
  self._is_multipart_file_input = is_multipart_file_input
1149
1424
  self._xml = xml if xml is not None else {}
1425
+ self._deserializer = deserializer
1150
1426
  {% if code_model.has_padded_model_property %}
1151
1427
  self._original_tsp_name = original_tsp_name
1152
1428
  {% endif %}
@@ -1185,7 +1461,11 @@ class _RestField:
1185
1461
  # Return the value from _data directly (it's been deserialized in place)
1186
1462
  return obj._data.get(self._rest_name)
1187
1463
 
1188
- deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1464
+ # Fast path: use _deserializer directly (avoids _serialize/_deserialize chain)
1465
+ if self._deserializer:
1466
+ deserialized = self._deserializer(item)
1467
+ else:
1468
+ deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1189
1469
 
1190
1470
  # For mutable types, store the deserialized value back in _data
1191
1471
  # so mutations directly affect _data
@@ -1231,6 +1511,7 @@ def rest_field(
1231
1511
  format: typing.Optional[str] = None,
1232
1512
  is_multipart_file_input: bool = False,
1233
1513
  xml: typing.Optional[dict[str, typing.Any]] = None,
1514
+ deserializer: typing.Optional[typing.Callable] = None,
1234
1515
  {% if code_model.has_padded_model_property %}
1235
1516
  original_tsp_name: typing.Optional[str] = None,
1236
1517
  {% endif %}
@@ -1243,6 +1524,7 @@ def rest_field(
1243
1524
  format=format,
1244
1525
  is_multipart_file_input=is_multipart_file_input,
1245
1526
  xml=xml,
1527
+ deserializer=deserializer,
1246
1528
  {% if code_model.has_padded_model_property %}
1247
1529
  original_tsp_name=original_tsp_name,
1248
1530
  {% endif %}
@@ -1481,6 +1763,8 @@ def _deserialize_xml(
1481
1763
  value: str,
1482
1764
  ) -> typing.Any:
1483
1765
  element = ET.fromstring(value) # nosec
1766
+ if _is_model(deserializer):
1767
+ return deserializer._deserialize(element, [])
1484
1768
  return _deserialize(deserializer, element)
1485
1769
 
1486
1770
 
@@ -1401,7 +1401,7 @@ class Deserializer:
1401
1401
  # Otherwise, result are unexpected
1402
1402
  self.additional_properties_detection = True
1403
1403
 
1404
- def __call__(self, target_obj, response_data, content_type=None):
1404
+ def __call__(self, target_obj, response_data, content_type=None): # pylint: disable=too-many-return-statements
1405
1405
  """Call the deserializer to process a REST response.
1406
1406
 
1407
1407
  :param str target_obj: Target data type to deserialize to.
@@ -1411,6 +1411,27 @@ class Deserializer:
1411
1411
  :return: Deserialized object.
1412
1412
  :rtype: object
1413
1413
  """
1414
+ # Fast path for header deserialization: response_data is a plain str or None
1415
+ # and target_obj is a simple scalar type. This avoids the expensive
1416
+ # _unpack_content → _deserialize → _classify_target → deserialize_data chain.
1417
+ if response_data is None:
1418
+ return None
1419
+ if target_obj == "str" and isinstance(response_data, str):
1420
+ return response_data
1421
+ if isinstance(response_data, str):
1422
+ if target_obj == "int":
1423
+ return int(response_data)
1424
+ if target_obj == "bool":
1425
+ if response_data in ("true", "1", "True"):
1426
+ return True
1427
+ if response_data in ("false", "0", "False"):
1428
+ return False
1429
+ return bool(response_data)
1430
+ if target_obj == "rfc-1123":
1431
+ return Deserializer.deserialize_rfc(response_data)
1432
+ if target_obj == "bytearray":
1433
+ return Deserializer.deserialize_bytearray(response_data)
1434
+
1414
1435
  data = self._unpack_content(response_data, content_type)
1415
1436
  return self._deserialize(target_obj, data)
1416
1437
 
@@ -181,7 +181,21 @@ class PagingResponse(Response):
181
181
 
182
182
  def type_annotation(self, **kwargs: Any) -> str:
183
183
  iterable = "AsyncItemPaged" if kwargs["async_mode"] else "ItemPaged"
184
- return f"{iterable}[{self.item_type.type_annotation(**kwargs)}]"
184
+ return f"{iterable}[{self._item_type_annotation(**kwargs)}]"
185
+
186
+ def _item_type_annotation(self, **kwargs: Any) -> str:
187
+ # When the page item is a ListType, render the outer `List`/`list`
188
+ # wrapper here using the operation-file alias decision so a list page
189
+ # item rendered inside an operation file named `list` uses the `List`
190
+ # alias (avoiding the built-in `list` shadowed by `List = list`).
191
+ # Recurse into the element type without is_operation_file so nested
192
+ # generated model types keep their forward-reference quoting
193
+ # (e.g. ItemPaged[List["_models.Product"]]).
194
+ if isinstance(self.item_type, ListType):
195
+ use_list_import = self.code_model.has_operation_named_list
196
+ list_type = "List" if use_list_import else "list"
197
+ return f"{list_type}[{self.item_type.element_type.type_annotation(**kwargs)}]"
198
+ return self.item_type.type_annotation(**kwargs)
185
199
 
186
200
  def docstring_text(self, **kwargs: Any) -> str:
187
201
  base_description = "An iterator like instance of "
@@ -6,13 +6,73 @@
6
6
  from typing import Optional
7
7
  from abc import ABC, abstractmethod
8
8
 
9
- from ..models import ModelType, Property, ConstantType, EnumValue
9
+ from ..models import ModelType, Property, ConstantType, EnumValue, EnumType
10
10
  from ..models.imports import FileImport, TypingSection, MsrestImportType, ImportType
11
+ from ..models.primitive_types import (
12
+ BooleanType,
13
+ IntegerType,
14
+ FloatType,
15
+ DecimalType,
16
+ StringType,
17
+ DatetimeType,
18
+ UnixTimeType,
19
+ TimeType,
20
+ DateType,
21
+ DurationType,
22
+ ByteArraySchema,
23
+ )
11
24
  from .import_serializer import FileImportSerializer
12
25
  from .base_serializer import BaseSerializer
13
26
  from ..models.utils import NamespaceType
14
27
 
15
28
 
29
+ def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements
30
+ """Return the _xml_deser_* function name for a scalar XML property, or None."""
31
+ prop_type = prop.type
32
+ # Unwrap ConstantType to get the underlying value type
33
+ if isinstance(prop_type, ConstantType):
34
+ prop_type = prop_type.value_type
35
+
36
+ if isinstance(prop_type, StringType):
37
+ return "_xml_deser_str"
38
+ if isinstance(prop_type, IntegerType):
39
+ return "_xml_deser_int"
40
+ if isinstance(prop_type, FloatType):
41
+ return "_xml_deser_float"
42
+ if isinstance(prop_type, BooleanType):
43
+ return "_xml_deser_bool"
44
+ if isinstance(prop_type, DecimalType):
45
+ return "_xml_deser_decimal"
46
+ if isinstance(prop_type, DatetimeType):
47
+ if prop_type.encode == "rfc7231":
48
+ return "_xml_deser_datetime_rfc7231"
49
+ return "_xml_deser_datetime"
50
+ if isinstance(prop_type, UnixTimeType):
51
+ return "_xml_deser_datetime_unix_timestamp"
52
+ if isinstance(prop_type, TimeType):
53
+ return "_xml_deser_time"
54
+ if isinstance(prop_type, DateType):
55
+ return "_xml_deser_date"
56
+ if isinstance(prop_type, DurationType):
57
+ return "_xml_deser_duration"
58
+ if isinstance(prop_type, ByteArraySchema):
59
+ encode = getattr(prop_type, "encode", None) or getattr(prop, "encode", None)
60
+ if encode == "base64url":
61
+ return "_xml_deser_bytes_base64url"
62
+ return "_xml_deser_bytes"
63
+ if isinstance(prop_type, EnumType):
64
+ return f"enum:{prop_type.name}"
65
+ return None
66
+
67
+
68
+ def _get_xml_deserializer_enum_type(prop: Property) -> Optional[EnumType]:
69
+ """Return the EnumType for an XML enum property (unwrapping ConstantType), or None."""
70
+ prop_type = prop.type
71
+ if isinstance(prop_type, ConstantType):
72
+ prop_type = prop_type.value_type
73
+ return prop_type if isinstance(prop_type, EnumType) else None
74
+
75
+
16
76
  def _documentation_string(prop: Property, description_keyword: str, docstring_type_keyword: str) -> list[str]:
17
77
  retval: list[str] = []
18
78
  sphinx_prefix = f":{description_keyword} {prop.client_name}:"
@@ -253,6 +313,9 @@ class DpgModelSerializer(_ModelSerializer):
253
313
  TypingSection.REGULAR,
254
314
  alias="_Model",
255
315
  )
316
+ # Collect XML deserializer functions needed by models in this file
317
+ xml_deser_names: set[str] = set()
318
+ xml_deser_enums: dict[str, EnumType] = {}
256
319
  for model in self.models:
257
320
  if model.base == "json":
258
321
  continue
@@ -271,6 +334,15 @@ class DpgModelSerializer(_ModelSerializer):
271
334
  called_by_property=True,
272
335
  )
273
336
  )
337
+ # Track XML deserializer functions needed
338
+ if prop.xml_metadata:
339
+ deser_name = _get_xml_deserializer_name(prop)
340
+ if deser_name:
341
+ xml_deser_names.add(deser_name)
342
+ if deser_name and deser_name.startswith("enum:"):
343
+ enum_type = _get_xml_deserializer_enum_type(prop)
344
+ if enum_type is not None:
345
+ xml_deser_enums[enum_type.name] = enum_type
274
346
  for parent in model.parents:
275
347
  if parent.client_namespace != model.client_namespace:
276
348
  file_import.add_submodule_import(
@@ -285,6 +357,38 @@ class DpgModelSerializer(_ModelSerializer):
285
357
  file_import.add_submodule_import("typing", "overload", ImportType.STDLIB)
286
358
  file_import.add_submodule_import("typing", "Mapping", ImportType.STDLIB)
287
359
  file_import.add_submodule_import("typing", "Any", ImportType.STDLIB)
360
+ # Add imports for XML deserializer functions
361
+ has_enum_deser = False
362
+ for deser_name in sorted(xml_deser_names):
363
+ if deser_name.startswith("enum:"):
364
+ has_enum_deser = True
365
+ continue
366
+ file_import.add_submodule_import(
367
+ self.code_model.get_relative_import_path(self.serialize_namespace, module_name="_utils.model_base"),
368
+ deser_name,
369
+ ImportType.LOCAL,
370
+ )
371
+ if has_enum_deser:
372
+ file_import.add_import("functools", ImportType.STDLIB)
373
+ file_import.add_submodule_import(
374
+ self.code_model.get_relative_import_path(self.serialize_namespace, module_name="_utils.model_base"),
375
+ "_xml_deser_enum_or_str",
376
+ ImportType.LOCAL,
377
+ )
378
+ # Ensure each referenced enum class is imported at runtime (not just
379
+ # under TYPE_CHECKING), so functools.partial can bind the class at
380
+ # class-body evaluation time.
381
+ for enum_name, enum_type in xml_deser_enums.items():
382
+ file_import.add_submodule_import(
383
+ self.code_model.get_relative_import_path(
384
+ self.serialize_namespace,
385
+ self.code_model.get_imported_namespace_for_model(enum_type.client_namespace),
386
+ module_name=self.code_model.enums_filename,
387
+ ),
388
+ enum_name,
389
+ ImportType.LOCAL,
390
+ TypingSection.REGULAR,
391
+ )
288
392
  # if there is a property named `list` we have to make sure there's no conflict with the built-in `list`
289
393
  if self.code_model.has_property_named_list:
290
394
  file_import.define_mypy_type("List", "list")
@@ -337,6 +441,14 @@ class DpgModelSerializer(_ModelSerializer):
337
441
 
338
442
  if prop.xml_metadata:
339
443
  args.append(f"xml={prop.xml_metadata}")
444
+ # Add fast-path deserializer for scalar XML fields
445
+ deser_name = _get_xml_deserializer_name(prop)
446
+ if deser_name:
447
+ if deser_name.startswith("enum:"):
448
+ enum_name = deser_name[5:]
449
+ args.append(f"deserializer=functools.partial(_xml_deser_enum_or_str, {enum_name})")
450
+ else:
451
+ args.append(f"deserializer={deser_name}")
340
452
 
341
453
  if prop.original_tsp_name:
342
454
  args.append(f'original_tsp_name="{prop.original_tsp_name}"')