@autorest/python 6.43.0 → 6.45.0

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.
Files changed (29) hide show
  1. package/generator/build/lib/pygen/codegen/models/__init__.py +2 -0
  2. package/generator/build/lib/pygen/codegen/models/code_model.py +15 -0
  3. package/generator/build/lib/pygen/codegen/models/primitive_types.py +33 -0
  4. package/generator/build/lib/pygen/codegen/models/property.py +2 -0
  5. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +1 -2
  6. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +29 -11
  7. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +5 -0
  8. package/generator/build/lib/pygen/codegen/templates/macros.jinja2 +12 -5
  9. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +92 -1
  10. package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +3 -0
  11. package/generator/build/lib/pygen/codegen/templates/packaging_templates/setup.py.jinja2 +3 -0
  12. package/generator/build/lib/pygen/codegen/templates/utils.py.jinja2 +5 -4
  13. package/generator/build/lib/pygen/preprocess/__init__.py +23 -12
  14. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  15. package/generator/pygen/codegen/models/__init__.py +2 -0
  16. package/generator/pygen/codegen/models/code_model.py +15 -0
  17. package/generator/pygen/codegen/models/primitive_types.py +33 -0
  18. package/generator/pygen/codegen/models/property.py +2 -0
  19. package/generator/pygen/codegen/serializers/builder_serializer.py +1 -2
  20. package/generator/pygen/codegen/serializers/general_serializer.py +29 -11
  21. package/generator/pygen/codegen/serializers/model_serializer.py +5 -0
  22. package/generator/pygen/codegen/templates/macros.jinja2 +12 -5
  23. package/generator/pygen/codegen/templates/model_base.py.jinja2 +92 -1
  24. package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +3 -0
  25. package/generator/pygen/codegen/templates/packaging_templates/setup.py.jinja2 +3 -0
  26. package/generator/pygen/codegen/templates/utils.py.jinja2 +5 -4
  27. package/generator/pygen/preprocess/__init__.py +23 -12
  28. package/package.json +2 -2
  29. package/scripts/__pycache__/venvtools.cpython-310.pyc +0 -0
@@ -31,6 +31,7 @@ from .primitive_types import (
31
31
  SdkCoreType,
32
32
  DecimalType,
33
33
  MultiPartFileType,
34
+ ExternalType,
34
35
  )
35
36
  from .enum_type import EnumType, EnumValue
36
37
  from .base import BaseType
@@ -151,6 +152,7 @@ TYPE_TO_OBJECT = {
151
152
  "credential": StringType,
152
153
  "sdkcore": SdkCoreType,
153
154
  "multipartfile": MultiPartFileType,
155
+ "external": ExternalType,
154
156
  }
155
157
  _LOGGER = logging.getLogger(__name__)
156
158
 
@@ -10,6 +10,7 @@ from .base import BaseType
10
10
  from .enum_type import EnumType
11
11
  from .model_type import ModelType, UsageFlags
12
12
  from .combined_type import CombinedType
13
+ from .primitive_types import ExternalType
13
14
  from .client import Client
14
15
  from .request_builder import RequestBuilder, OverloadedRequestBuilder
15
16
  from .operation_group import OperationGroup
@@ -101,6 +102,7 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
101
102
  self._operations_folder_name: dict[str, str] = {}
102
103
  self._relative_import_path: dict[str, str] = {}
103
104
  self.metadata: dict[str, Any] = yaml_data.get("metadata", {})
105
+ self.has_external_type = any(isinstance(t, ExternalType) for t in self.types_map.values())
104
106
 
105
107
  @staticmethod
106
108
  def get_imported_namespace_for_client(imported_namespace: str, async_mode: bool = False) -> str:
@@ -488,3 +490,16 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
488
490
  @property
489
491
  def has_operation_named_list(self) -> bool:
490
492
  return any(o.name.lower() == "list" for c in self.clients for og in c.operation_groups for o in og.operations)
493
+
494
+ @property
495
+ def has_padded_model_property(self) -> bool:
496
+ for model_type in self.model_types:
497
+ for prop in model_type.properties:
498
+ if prop.original_tsp_name:
499
+ return True
500
+ return False
501
+
502
+ @property
503
+ def external_types(self) -> list[ExternalType]:
504
+ """All of the external types"""
505
+ return [t for t in self.types_map.values() if isinstance(t, ExternalType)]
@@ -615,6 +615,39 @@ class SdkCoreType(PrimitiveType):
615
615
  return self.name
616
616
 
617
617
 
618
+ class ExternalType(PrimitiveType):
619
+ def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None:
620
+ super().__init__(yaml_data=yaml_data, code_model=code_model)
621
+ external_type_info = yaml_data.get("externalTypeInfo", {})
622
+ self.identity = external_type_info.get("identity", "")
623
+ self.submodule = ".".join(self.identity.split(".")[:-1])
624
+ self.min_version = external_type_info.get("minVersion", "")
625
+ self.package_name = external_type_info.get("package", "")
626
+
627
+ def docstring_type(self, **kwargs: Any) -> str:
628
+ return f"~{self.identity}"
629
+
630
+ def type_annotation(self, **kwargs: Any) -> str:
631
+ return self.identity
632
+
633
+ def imports(self, **kwargs: Any) -> FileImport:
634
+ file_import = super().imports(**kwargs)
635
+ file_import.add_import(self.submodule, ImportType.THIRDPARTY, TypingSection.REGULAR)
636
+ return file_import
637
+
638
+ @property
639
+ def instance_check_template(self) -> str:
640
+ return f"isinstance({{}}, {self.identity})"
641
+
642
+ def serialization_type(self, **kwargs: Any) -> str:
643
+ return self.identity
644
+
645
+ @property
646
+ def default_template_representation_declaration(self) -> str:
647
+ value = f"{self.identity}(...)"
648
+ return f'"{value}"' if self.code_model.for_test else value
649
+
650
+
618
651
  class MultiPartFileType(PrimitiveType):
619
652
  def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None:
620
653
  super().__init__(yaml_data=yaml_data, code_model=code_model)
@@ -39,6 +39,8 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
39
39
  self.flattened_names: list[str] = yaml_data.get("flattenedNames", [])
40
40
  self.is_multipart_file_input: bool = yaml_data.get("isMultipartFileInput", False)
41
41
  self.flatten = self.yaml_data.get("flatten", False) and not getattr(self.type, "flattened_property", False)
42
+ self.original_tsp_name: Optional[str] = self.yaml_data.get("originalTspName")
43
+ self.encode: Optional[str] = self.yaml_data.get("encode")
42
44
 
43
45
  def pylint_disable(self) -> str:
44
46
  retval: str = ""
@@ -679,7 +679,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
679
679
  ")",
680
680
  f"_file_fields: list[str] = {file_fields}",
681
681
  f"_data_fields: list[str] = {data_fields}",
682
- "_files, _data = prepare_multipart_form_data(_body, _file_fields, _data_fields)",
682
+ "_files = prepare_multipart_form_data(_body, _file_fields, _data_fields)",
683
683
  ]
684
684
  )
685
685
  return retval
@@ -861,7 +861,6 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
861
861
  retval.append(f" {client_name}=_{client_name},")
862
862
  elif request_builder.has_form_data_body:
863
863
  retval.append(" files=_files,")
864
- retval.append(" data=_data,")
865
864
  elif request_builder.overloads:
866
865
  seen_body_params = set()
867
866
  for overload in request_builder.overloads:
@@ -23,7 +23,7 @@ VERSION_MAP = {
23
23
  "msrest": "0.7.1",
24
24
  "isodate": "0.6.1",
25
25
  "azure-mgmt-core": "1.6.0",
26
- "azure-core": "1.35.0",
26
+ "azure-core": "1.36.0",
27
27
  "typing-extensions": "4.6.0",
28
28
  "corehttp": "1.0.0b6",
29
29
  }
@@ -57,7 +57,16 @@ class GeneralSerializer(BaseSerializer):
57
57
  m = re.search(r"[>=]=?([\d.]+(?:[a-z]+\d+)?)", s)
58
58
  return parse_version(m.group(1)) if m else parse_version("0")
59
59
 
60
- def _keep_pyproject_fields(self, file_content: str) -> dict:
60
+ def _update_version_map(self, version_map: dict[str, str], dep_name: str, dep: str) -> None:
61
+ # For tracked dependencies, check if the version is higher than our default
62
+ default_version = parse_version(version_map[dep_name])
63
+ dep_version = self._extract_min_dependency(dep)
64
+ # If the version is higher than the default, update VERSION_MAP
65
+ # with higher min dependency version
66
+ if dep_version > default_version:
67
+ version_map[dep_name] = str(dep_version)
68
+
69
+ def external_lib_version_map(self, file_content: str, additional_version_map: dict[str, str]) -> dict:
61
70
  # Load the pyproject.toml file if it exists and extract fields to keep.
62
71
  result: dict = {"KEEP_FIELDS": {}}
63
72
  try:
@@ -80,15 +89,11 @@ class GeneralSerializer(BaseSerializer):
80
89
  for dep in loaded_pyproject_toml["project"]["dependencies"]:
81
90
  dep_name = re.split(r"[<>=\[]", dep)[0].strip()
82
91
 
83
- # Check if dependency is one we track in VERSION_MAP
92
+ # Check if dependency is one we track in version map
84
93
  if dep_name in VERSION_MAP:
85
- # For tracked dependencies, check if the version is higher than our default
86
- default_version = parse_version(VERSION_MAP[dep_name])
87
- dep_version = self._extract_min_dependency(dep)
88
- # If the version is higher than the default, update VERSION_MAP
89
- # with higher min dependency version
90
- if dep_version > default_version:
91
- VERSION_MAP[dep_name] = str(dep_version)
94
+ self._update_version_map(VERSION_MAP, dep_name, dep)
95
+ elif dep_name in additional_version_map:
96
+ self._update_version_map(additional_version_map, dep_name, dep)
92
97
  else:
93
98
  # Keep non-default dependencies
94
99
  kept_deps.append(dep)
@@ -107,9 +112,20 @@ class GeneralSerializer(BaseSerializer):
107
112
  def serialize_package_file(self, template_name: str, file_content: str, **kwargs: Any) -> str:
108
113
  template = self.env.get_template(template_name)
109
114
 
115
+ additional_version_map = {}
116
+ if self.code_model.has_external_type:
117
+ for item in self.code_model.external_types:
118
+ if item.package_name:
119
+ if item.min_version:
120
+ additional_version_map[item.package_name] = item.min_version
121
+ else:
122
+ # Use "0" as a placeholder when min_version is not specified for external types.
123
+ # This allows the dependency to be included without a specific version constraint.
124
+ additional_version_map[item.package_name] = "0"
125
+
110
126
  # Add fields to keep from an existing pyproject.toml
111
127
  if template_name == "pyproject.toml.jinja2":
112
- params = self._keep_pyproject_fields(file_content)
128
+ params = self.external_lib_version_map(file_content, additional_version_map)
113
129
  else:
114
130
  params = {}
115
131
 
@@ -126,6 +142,7 @@ class GeneralSerializer(BaseSerializer):
126
142
  dev_status = "4 - Beta"
127
143
  else:
128
144
  dev_status = "5 - Production/Stable"
145
+
129
146
  params |= {
130
147
  "code_model": self.code_model,
131
148
  "dev_status": dev_status,
@@ -136,6 +153,7 @@ class GeneralSerializer(BaseSerializer):
136
153
  "VERSION_MAP": VERSION_MAP,
137
154
  "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION,
138
155
  "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION,
156
+ "ADDITIONAL_DEPENDENCIES": [f"{item[0]}>={item[1]}" for item in additional_version_map.items()],
139
157
  }
140
158
  params |= {"options": self.code_model.options}
141
159
  params |= kwargs
@@ -329,10 +329,15 @@ class DpgModelSerializer(_ModelSerializer):
329
329
  args.append("is_multipart_file_input=True")
330
330
  elif hasattr(prop.type, "encode") and prop.type.encode: # type: ignore
331
331
  args.append(f'format="{prop.type.encode}"') # type: ignore
332
+ elif prop.encode:
333
+ args.append(f'format="{prop.encode}"')
332
334
 
333
335
  if prop.xml_metadata:
334
336
  args.append(f"xml={prop.xml_metadata}")
335
337
 
338
+ if prop.original_tsp_name:
339
+ args.append(f'original_tsp_name="{prop.original_tsp_name}"')
340
+
336
341
  field = "rest_discriminator" if prop.is_discriminator else "rest_field"
337
342
  type_ignore = (
338
343
  " # type: ignore"
@@ -5,21 +5,28 @@
5
5
  {% set enable_custom_handling = "\n* " in doc_string or doc_string.startswith("* ") %}
6
6
  {%- if enable_custom_handling -%}
7
7
  {%- set lines = doc_string.split('\n') -%}
8
+ {%- set base_indent = wrap_string.lstrip('\n') -%}
8
9
  {%- set result_lines = [] -%}
9
10
  {%- for line in lines -%}
10
11
  {%- if line.startswith('* ') -%}
11
12
  {# Handle bullet points with proper continuation alignment #}
12
13
  {%- set bullet_content = line[2:] -%}
13
- {%- set base_indent = wrap_string.lstrip('\n') -%}
14
14
  {%- set bullet_line = base_indent + ' * ' + bullet_content -%}
15
15
  {%- set continuation_spaces = base_indent + ' ' -%}
16
16
  {%- set wrapped = bullet_line | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring='\n' + continuation_spaces) -%}
17
17
  {%- set _ = result_lines.append(wrapped) -%}
18
18
  {%- elif line.strip() -%}
19
- {%- set wrapped = line.strip() | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%}
20
- {%- set _ = result_lines.append(wrapped) -%}
19
+ {%- set line_indent = '' if line.strip().startswith(':') or loop.index == 1 else (base_indent + ' ') -%}
20
+ {%- set wrapped = (line_indent + line) | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%}
21
+ {%- for line in wrapped.split('\n') -%}
22
+ {%- set prefix = "" if loop.index == 1 else " " -%}
23
+ {%- set _ = result_lines.append(prefix + line) -%}
24
+ {%- endfor -%}
21
25
  {%- else -%}
22
- {%- set _ = result_lines.append('') -%}
26
+ {# Do not add continuous blank lines #}
27
+ {%- if (result_lines and result_lines[-1] != '') or not result_lines -%}
28
+ {%- set _ = result_lines.append('') -%}
29
+ {%- endif -%}
23
30
  {%- endif -%}
24
31
  {%- endfor -%}
25
32
  {%- set original_result = result_lines | join('\n') -%}
@@ -37,4 +44,4 @@
37
44
  {% set suffix = suffix_string if list_result | length == loop.index %}
38
45
  {{ prefix }}{{ line }}{{ suffix }}
39
46
  {% endfor %}
40
- {% endmacro %}
47
+ {% endmacro %}
@@ -25,6 +25,9 @@ from {{ code_model.core_library }}.exceptions import DeserializationError
25
25
  from {{ code_model.core_library }}{{ "" if code_model.is_azure_flavor else ".utils" }} import CaseInsensitiveEnumMeta
26
26
  from {{ code_model.core_library }}.{{ "" if code_model.is_azure_flavor else "runtime." }}pipeline import PipelineResponse
27
27
  from {{ code_model.core_library }}.serialization import _Null
28
+ {% if code_model.has_external_type %}
29
+ from {{ code_model.core_library }}.serialization import TypeHandlerRegistry
30
+ {% endif %}
28
31
  from {{ code_model.core_library }}.rest import HttpResponse
29
32
 
30
33
  _LOGGER = logging.getLogger(__name__)
@@ -34,6 +37,10 @@ __all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"]
34
37
  TZ_UTC = timezone.utc
35
38
  _T = typing.TypeVar("_T")
36
39
 
40
+ {% if code_model.has_external_type %}
41
+ TYPE_HANDLER_REGISTRY = TypeHandlerRegistry()
42
+ {% endif %}
43
+
37
44
 
38
45
  def _timedelta_as_isostr(td: timedelta) -> str:
39
46
  """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S'
@@ -158,6 +165,11 @@ class SdkJSONEncoder(JSONEncoder):
158
165
  except AttributeError:
159
166
  # This will be raised when it hits value.total_seconds in the method above
160
167
  pass
168
+ {% if code_model.has_external_type %}
169
+ custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o)
170
+ if custom_serializer:
171
+ return custom_serializer(o)
172
+ {% endif %}
161
173
  return super(SdkJSONEncoder, self).default(o)
162
174
 
163
175
 
@@ -167,6 +179,19 @@ _VALID_RFC7231 = re.compile(
167
179
  r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT"
168
180
  )
169
181
 
182
+ _ARRAY_ENCODE_MAPPING = {
183
+ "pipeDelimited": "|",
184
+ "spaceDelimited": " ",
185
+ "commaDelimited": ",",
186
+ "newlineDelimited": "\n",
187
+ }
188
+
189
+ def _deserialize_array_encoded(delimit: str, attr):
190
+ if isinstance(attr, str):
191
+ if attr == "":
192
+ return []
193
+ return attr.split(delimit)
194
+ return attr
170
195
 
171
196
  def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
172
197
  """Deserialize ISO-8601 formatted string into Datetime object.
@@ -311,9 +336,17 @@ _DESERIALIZE_MAPPING_WITHFORMAT = {
311
336
  def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None):
312
337
  if annotation is int and rf and rf._format == "str":
313
338
  return _deserialize_int_as_str
339
+ if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING:
340
+ return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format])
314
341
  if rf and rf._format:
315
342
  return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format)
343
+ {% if code_model.has_external_type %}
344
+ if _DESERIALIZE_MAPPING.get(annotation): # pyright: ignore
345
+ return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore
346
+ return TYPE_HANDLER_REGISTRY.get_deserializer(annotation) # pyright: ignore
347
+ {% else %}
316
348
  return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore
349
+ {% endif %}
317
350
 
318
351
 
319
352
  def _get_type_alias_type(module_name: str, alias_name: str):
@@ -479,6 +512,8 @@ def _is_model(obj: typing.Any) -> bool:
479
512
 
480
513
  def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements
481
514
  if isinstance(o, list):
515
+ if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o):
516
+ return _ARRAY_ENCODE_MAPPING[format].join(o)
482
517
  return [_serialize(x, format) for x in o]
483
518
  if isinstance(o, dict):
484
519
  return {k: _serialize(v, format) for k, v in o.items()}
@@ -507,6 +542,14 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m
507
542
  except AttributeError:
508
543
  # This will be raised when it hits value.total_seconds in the method above
509
544
  pass
545
+ {% if code_model.has_external_type %}
546
+
547
+ # Check if there's a custom serializer for the type
548
+ custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o)
549
+ if custom_serializer:
550
+ return custom_serializer(o)
551
+
552
+ {% endif %}
510
553
  return o
511
554
 
512
555
 
@@ -636,6 +679,12 @@ class Model(_MyMutableMapping):
636
679
  if not rf._rest_name_input:
637
680
  rf._rest_name_input = attr
638
681
  cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items())
682
+ {% if code_model.has_padded_model_property %}
683
+ cls._backcompat_attr_to_rest_field: dict[str, _RestField] = {
684
+ Model._get_backcompat_attribute_name(cls._attr_to_rest_field, attr): rf for attr, rf in cls
685
+ ._attr_to_rest_field.items()
686
+ }
687
+ {% endif %}
639
688
  cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
640
689
 
641
690
  return super().__new__(cls)
@@ -645,6 +694,18 @@ class Model(_MyMutableMapping):
645
694
  if hasattr(base, "__mapping__"):
646
695
  base.__mapping__[discriminator or cls.__name__] = cls # type: ignore
647
696
 
697
+ {% if code_model.has_padded_model_property %}
698
+ @classmethod
699
+ def _get_backcompat_attribute_name(cls, attr_to_rest_field: dict[str, "_RestField"], attr_name: str) -> str:
700
+ rest_field_obj = attr_to_rest_field.get(attr_name) # pylint: disable=protected-access
701
+ if rest_field_obj is None:
702
+ return attr_name
703
+ original_tsp_name = getattr(rest_field_obj, "_original_tsp_name", None) # pylint: disable=protected-access
704
+ if original_tsp_name:
705
+ return original_tsp_name
706
+ return attr_name
707
+ {% endif %}
708
+
648
709
  @classmethod
649
710
  def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]:
650
711
  for v in cls.__dict__.values():
@@ -765,6 +826,17 @@ def _deserialize_sequence(
765
826
  return obj
766
827
  if isinstance(obj, ET.Element):
767
828
  obj = list(obj)
829
+ try:
830
+ if (
831
+ isinstance(obj, str)
832
+ and isinstance(deserializer, functools.partial)
833
+ and isinstance(deserializer.args[0], functools.partial)
834
+ and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable
835
+ ):
836
+ # encoded string may be deserialized to sequence
837
+ return deserializer(obj)
838
+ except: # pylint: disable=bare-except
839
+ pass
768
840
  return type(obj)(_deserialize(deserializer, entry, module) for entry in obj)
769
841
 
770
842
 
@@ -971,6 +1043,9 @@ def _failsafe_deserialize_xml(
971
1043
  return None
972
1044
 
973
1045
 
1046
+ {% if code_model.has_padded_model_property %}
1047
+ # pylint: disable=too-many-instance-attributes
1048
+ {% endif %}
974
1049
  class _RestField:
975
1050
  def __init__(
976
1051
  self,
@@ -983,6 +1058,9 @@ class _RestField:
983
1058
  format: typing.Optional[str] = None,
984
1059
  is_multipart_file_input: bool = False,
985
1060
  xml: typing.Optional[dict[str, typing.Any]] = None,
1061
+ {% if code_model.has_padded_model_property %}
1062
+ original_tsp_name: typing.Optional[str] = None,
1063
+ {% endif %}
986
1064
  ):
987
1065
  self._type = type
988
1066
  self._rest_name_input = name
@@ -994,10 +1072,17 @@ class _RestField:
994
1072
  self._format = format
995
1073
  self._is_multipart_file_input = is_multipart_file_input
996
1074
  self._xml = xml if xml is not None else {}
1075
+ {% if code_model.has_padded_model_property %}
1076
+ self._original_tsp_name = original_tsp_name
1077
+ {% endif %}
997
1078
 
998
1079
  @property
999
1080
  def _class_type(self) -> typing.Any:
1000
- return getattr(self._type, "args", [None])[0]
1081
+ result = getattr(self._type, "args", [None])[0]
1082
+ # type may be wrapped by nested functools.partial so we need to check for that
1083
+ if isinstance(result, functools.partial):
1084
+ return getattr(result, "args", [None])[0]
1085
+ return result
1001
1086
 
1002
1087
  @property
1003
1088
  def _rest_name(self) -> str:
@@ -1045,6 +1130,9 @@ def rest_field(
1045
1130
  format: typing.Optional[str] = None,
1046
1131
  is_multipart_file_input: bool = False,
1047
1132
  xml: typing.Optional[dict[str, typing.Any]] = None,
1133
+ {% if code_model.has_padded_model_property %}
1134
+ original_tsp_name: typing.Optional[str] = None,
1135
+ {% endif %}
1048
1136
  ) -> typing.Any:
1049
1137
  return _RestField(
1050
1138
  name=name,
@@ -1054,6 +1142,9 @@ def rest_field(
1054
1142
  format=format,
1055
1143
  is_multipart_file_input=is_multipart_file_input,
1056
1144
  xml=xml,
1145
+ {% if code_model.has_padded_model_property %}
1146
+ original_tsp_name=original_tsp_name,
1147
+ {% endif %}
1057
1148
  )
1058
1149
 
1059
1150
 
@@ -56,6 +56,9 @@ dependencies = [
56
56
  "{{ dep }}",
57
57
  {% endfor %}
58
58
  {% endif %}
59
+ {% for dep in ADDITIONAL_DEPENDENCIES %}
60
+ "{{ dep }}",
61
+ {% endfor %}
59
62
  ]
60
63
  dynamic = [
61
64
  {% if options.get('package-mode') %}"version", {% endif %}"readme"
@@ -108,6 +108,9 @@ setup(
108
108
  "corehttp[requests]>={{ VERSION_MAP["corehttp"] }}",
109
109
  {% endif %}
110
110
  "typing-extensions>={{ VERSION_MAP['typing-extensions'] }}",
111
+ {% for dep in ADDITIONAL_DEPENDENCIES %}
112
+ {{ dep }},
113
+ {% endfor %}
111
114
  ],
112
115
  {% if options["package-mode"] %}
113
116
  python_requires=">={{ MIN_PYTHON_VERSION }}",
@@ -78,9 +78,8 @@ def serialize_multipart_data_entry(data_entry: Any) -> Any:
78
78
 
79
79
  def prepare_multipart_form_data(
80
80
  body: Mapping[str, Any], multipart_fields: list[str], data_fields: list[str]
81
- ) -> tuple[list[FileType], dict[str, Any]]:
81
+ ) -> list[FileType]:
82
82
  files: list[FileType] = []
83
- data: dict[str, Any] = {}
84
83
  for multipart_field in multipart_fields:
85
84
  multipart_entry = body.get(multipart_field)
86
85
  if isinstance(multipart_entry, list):
@@ -88,10 +87,12 @@ def prepare_multipart_form_data(
88
87
  elif multipart_entry:
89
88
  files.append((multipart_field, multipart_entry))
90
89
 
90
+ # if files is empty, sdk core library can't handle multipart/form-data correctly, so
91
+ # we put data fields into files with filename as None to avoid that scenario.
91
92
  for data_field in data_fields:
92
93
  data_entry = body.get(data_field)
93
94
  if data_entry:
94
- data[data_field] = serialize_multipart_data_entry(data_entry)
95
+ files.append((data_field, str(serialize_multipart_data_entry(data_entry))))
95
96
 
96
- return files, data
97
+ return files
97
98
  {% endif %}
@@ -236,7 +236,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
236
236
  body_parameter["type"]["types"].insert(1, any_obj_list_or_dict)
237
237
  code_model["types"].append(body_parameter["type"])
238
238
 
239
- def pad_reserved_words(self, name: str, pad_type: PadType):
239
+ def pad_reserved_words(self, name: str, pad_type: PadType, yaml_type: dict[str, Any]) -> str:
240
240
  # we want to pad hidden variables as well
241
241
  if not name:
242
242
  # we'll pass in empty operation groups sometime etc.
@@ -250,6 +250,10 @@ class PreProcessPlugin(YamlUpdatePlugin):
250
250
  name_prefix = "_" if name[0] == "_" else ""
251
251
  name = name[1:] if name[0] == "_" else name
252
252
  if name.lower() in reserved_words[pad_type]:
253
+ if self.is_tsp and name.lower() in TSP_RESERVED_WORDS.get(pad_type, []):
254
+ # to maintain backcompat for cases where we pad in tsp but not in autorest,
255
+ # if we have a tsp reserved word, we also want to keep track of the original name for backcompat
256
+ yaml_type["originalTspName"] = name_prefix + name
253
257
  return name_prefix + name + pad_type
254
258
  return name_prefix + name
255
259
 
@@ -257,11 +261,13 @@ class PreProcessPlugin(YamlUpdatePlugin):
257
261
  for type in yaml_data:
258
262
  for property in type.get("properties", []):
259
263
  property["description"] = update_description(property.get("description", ""))
260
- property["clientName"] = self.pad_reserved_words(property["clientName"].lower(), PadType.PROPERTY)
264
+ property["clientName"] = self.pad_reserved_words(
265
+ property["clientName"].lower(), PadType.PROPERTY, property
266
+ )
261
267
  add_redefined_builtin_info(property["clientName"], property)
262
268
  if type.get("name"):
263
269
  pad_type = PadType.MODEL if type["type"] == "model" else PadType.ENUM_CLASS
264
- name = self.pad_reserved_words(type["name"], pad_type)
270
+ name = self.pad_reserved_words(type["name"], pad_type, type)
265
271
  type["name"] = name[0].upper() + name[1:]
266
272
  type["description"] = update_description(type.get("description", ""), type["name"])
267
273
  type["snakeCaseName"] = to_snake_case(type["name"])
@@ -269,7 +275,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
269
275
  # we're enums
270
276
  values_to_add = []
271
277
  for value in type["values"]:
272
- padded_name = self.pad_reserved_words(value["name"].lower(), PadType.ENUM_VALUE).upper()
278
+ padded_name = self.pad_reserved_words(value["name"].lower(), PadType.ENUM_VALUE, value).upper()
273
279
  if self.version_tolerant:
274
280
  if padded_name[0] in "0123456789":
275
281
  padded_name = "ENUM_" + padded_name
@@ -364,12 +370,14 @@ class PreProcessPlugin(YamlUpdatePlugin):
364
370
  def update_parameter(self, yaml_data: dict[str, Any]) -> None:
365
371
  yaml_data["description"] = update_description(yaml_data.get("description", ""))
366
372
  if not (yaml_data["location"] == "header" and yaml_data["clientName"] in ("content_type", "accept")):
367
- yaml_data["clientName"] = self.pad_reserved_words(yaml_data["clientName"].lower(), PadType.PARAMETER)
373
+ yaml_data["clientName"] = self.pad_reserved_words(
374
+ yaml_data["clientName"].lower(), PadType.PARAMETER, yaml_data
375
+ )
368
376
  if yaml_data.get("propertyToParameterName"):
369
377
  # need to create a new one with padded keys and values
370
378
  yaml_data["propertyToParameterName"] = {
371
- self.pad_reserved_words(prop, PadType.PROPERTY): self.pad_reserved_words(
372
- param_name, PadType.PARAMETER
379
+ self.pad_reserved_words(prop, PadType.PROPERTY, yaml_data): self.pad_reserved_words(
380
+ param_name, PadType.PARAMETER, yaml_data
373
381
  ).lower()
374
382
  for prop, param_name in yaml_data["propertyToParameterName"].items()
375
383
  }
@@ -390,15 +398,17 @@ class PreProcessPlugin(YamlUpdatePlugin):
390
398
  *,
391
399
  is_overload: bool = False,
392
400
  ) -> None:
393
- yaml_data["groupName"] = self.pad_reserved_words(yaml_data["groupName"], PadType.OPERATION_GROUP)
401
+ yaml_data["groupName"] = self.pad_reserved_words(yaml_data["groupName"], PadType.OPERATION_GROUP, yaml_data)
394
402
  yaml_data["groupName"] = to_snake_case(yaml_data["groupName"])
395
403
  yaml_data["name"] = yaml_data["name"].lower()
396
404
  if yaml_data.get("isLroInitialOperation") is True:
397
405
  yaml_data["name"] = (
398
- "_" + self.pad_reserved_words(extract_original_name(yaml_data["name"]), PadType.METHOD) + "_initial"
406
+ "_"
407
+ + self.pad_reserved_words(extract_original_name(yaml_data["name"]), PadType.METHOD, yaml_data)
408
+ + "_initial"
399
409
  )
400
410
  else:
401
- yaml_data["name"] = self.pad_reserved_words(yaml_data["name"], PadType.METHOD)
411
+ yaml_data["name"] = self.pad_reserved_words(yaml_data["name"], PadType.METHOD, yaml_data)
402
412
  yaml_data["description"] = update_description(yaml_data["description"], yaml_data["name"])
403
413
  yaml_data["summary"] = update_description(yaml_data.get("summary", ""))
404
414
  body_parameter = yaml_data.get("bodyParameter")
@@ -485,7 +495,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
485
495
  item_type = item_type or yaml_data["itemType"]["elementType"]
486
496
  if yaml_data.get("nextOperation"):
487
497
  yaml_data["nextOperation"]["groupName"] = self.pad_reserved_words(
488
- yaml_data["nextOperation"]["groupName"], PadType.OPERATION_GROUP
498
+ yaml_data["nextOperation"]["groupName"], PadType.OPERATION_GROUP, yaml_data["nextOperation"]
489
499
  )
490
500
  yaml_data["nextOperation"]["groupName"] = to_snake_case(yaml_data["nextOperation"]["groupName"])
491
501
  for response in yaml_data["nextOperation"].get("responses", []):
@@ -503,10 +513,11 @@ class PreProcessPlugin(YamlUpdatePlugin):
503
513
  operation_group["identifyName"] = self.pad_reserved_words(
504
514
  operation_group.get("name", operation_group["propertyName"]),
505
515
  PadType.OPERATION_GROUP,
516
+ operation_group,
506
517
  )
507
518
  operation_group["identifyName"] = to_snake_case(operation_group["identifyName"])
508
519
  operation_group["propertyName"] = self.pad_reserved_words(
509
- operation_group["propertyName"], PadType.OPERATION_GROUP
520
+ operation_group["propertyName"], PadType.OPERATION_GROUP, operation_group
510
521
  )
511
522
  operation_group["propertyName"] = to_snake_case(operation_group["propertyName"])
512
523
  operation_group["className"] = update_operation_group_class_name(
@@ -31,6 +31,7 @@ from .primitive_types import (
31
31
  SdkCoreType,
32
32
  DecimalType,
33
33
  MultiPartFileType,
34
+ ExternalType,
34
35
  )
35
36
  from .enum_type import EnumType, EnumValue
36
37
  from .base import BaseType
@@ -151,6 +152,7 @@ TYPE_TO_OBJECT = {
151
152
  "credential": StringType,
152
153
  "sdkcore": SdkCoreType,
153
154
  "multipartfile": MultiPartFileType,
155
+ "external": ExternalType,
154
156
  }
155
157
  _LOGGER = logging.getLogger(__name__)
156
158
 
@@ -10,6 +10,7 @@ from .base import BaseType
10
10
  from .enum_type import EnumType
11
11
  from .model_type import ModelType, UsageFlags
12
12
  from .combined_type import CombinedType
13
+ from .primitive_types import ExternalType
13
14
  from .client import Client
14
15
  from .request_builder import RequestBuilder, OverloadedRequestBuilder
15
16
  from .operation_group import OperationGroup
@@ -101,6 +102,7 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
101
102
  self._operations_folder_name: dict[str, str] = {}
102
103
  self._relative_import_path: dict[str, str] = {}
103
104
  self.metadata: dict[str, Any] = yaml_data.get("metadata", {})
105
+ self.has_external_type = any(isinstance(t, ExternalType) for t in self.types_map.values())
104
106
 
105
107
  @staticmethod
106
108
  def get_imported_namespace_for_client(imported_namespace: str, async_mode: bool = False) -> str:
@@ -488,3 +490,16 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
488
490
  @property
489
491
  def has_operation_named_list(self) -> bool:
490
492
  return any(o.name.lower() == "list" for c in self.clients for og in c.operation_groups for o in og.operations)
493
+
494
+ @property
495
+ def has_padded_model_property(self) -> bool:
496
+ for model_type in self.model_types:
497
+ for prop in model_type.properties:
498
+ if prop.original_tsp_name:
499
+ return True
500
+ return False
501
+
502
+ @property
503
+ def external_types(self) -> list[ExternalType]:
504
+ """All of the external types"""
505
+ return [t for t in self.types_map.values() if isinstance(t, ExternalType)]
@@ -615,6 +615,39 @@ class SdkCoreType(PrimitiveType):
615
615
  return self.name
616
616
 
617
617
 
618
+ class ExternalType(PrimitiveType):
619
+ def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None:
620
+ super().__init__(yaml_data=yaml_data, code_model=code_model)
621
+ external_type_info = yaml_data.get("externalTypeInfo", {})
622
+ self.identity = external_type_info.get("identity", "")
623
+ self.submodule = ".".join(self.identity.split(".")[:-1])
624
+ self.min_version = external_type_info.get("minVersion", "")
625
+ self.package_name = external_type_info.get("package", "")
626
+
627
+ def docstring_type(self, **kwargs: Any) -> str:
628
+ return f"~{self.identity}"
629
+
630
+ def type_annotation(self, **kwargs: Any) -> str:
631
+ return self.identity
632
+
633
+ def imports(self, **kwargs: Any) -> FileImport:
634
+ file_import = super().imports(**kwargs)
635
+ file_import.add_import(self.submodule, ImportType.THIRDPARTY, TypingSection.REGULAR)
636
+ return file_import
637
+
638
+ @property
639
+ def instance_check_template(self) -> str:
640
+ return f"isinstance({{}}, {self.identity})"
641
+
642
+ def serialization_type(self, **kwargs: Any) -> str:
643
+ return self.identity
644
+
645
+ @property
646
+ def default_template_representation_declaration(self) -> str:
647
+ value = f"{self.identity}(...)"
648
+ return f'"{value}"' if self.code_model.for_test else value
649
+
650
+
618
651
  class MultiPartFileType(PrimitiveType):
619
652
  def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None:
620
653
  super().__init__(yaml_data=yaml_data, code_model=code_model)
@@ -39,6 +39,8 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
39
39
  self.flattened_names: list[str] = yaml_data.get("flattenedNames", [])
40
40
  self.is_multipart_file_input: bool = yaml_data.get("isMultipartFileInput", False)
41
41
  self.flatten = self.yaml_data.get("flatten", False) and not getattr(self.type, "flattened_property", False)
42
+ self.original_tsp_name: Optional[str] = self.yaml_data.get("originalTspName")
43
+ self.encode: Optional[str] = self.yaml_data.get("encode")
42
44
 
43
45
  def pylint_disable(self) -> str:
44
46
  retval: str = ""
@@ -679,7 +679,7 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
679
679
  ")",
680
680
  f"_file_fields: list[str] = {file_fields}",
681
681
  f"_data_fields: list[str] = {data_fields}",
682
- "_files, _data = prepare_multipart_form_data(_body, _file_fields, _data_fields)",
682
+ "_files = prepare_multipart_form_data(_body, _file_fields, _data_fields)",
683
683
  ]
684
684
  )
685
685
  return retval
@@ -861,7 +861,6 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
861
861
  retval.append(f" {client_name}=_{client_name},")
862
862
  elif request_builder.has_form_data_body:
863
863
  retval.append(" files=_files,")
864
- retval.append(" data=_data,")
865
864
  elif request_builder.overloads:
866
865
  seen_body_params = set()
867
866
  for overload in request_builder.overloads:
@@ -23,7 +23,7 @@ VERSION_MAP = {
23
23
  "msrest": "0.7.1",
24
24
  "isodate": "0.6.1",
25
25
  "azure-mgmt-core": "1.6.0",
26
- "azure-core": "1.35.0",
26
+ "azure-core": "1.36.0",
27
27
  "typing-extensions": "4.6.0",
28
28
  "corehttp": "1.0.0b6",
29
29
  }
@@ -57,7 +57,16 @@ class GeneralSerializer(BaseSerializer):
57
57
  m = re.search(r"[>=]=?([\d.]+(?:[a-z]+\d+)?)", s)
58
58
  return parse_version(m.group(1)) if m else parse_version("0")
59
59
 
60
- def _keep_pyproject_fields(self, file_content: str) -> dict:
60
+ def _update_version_map(self, version_map: dict[str, str], dep_name: str, dep: str) -> None:
61
+ # For tracked dependencies, check if the version is higher than our default
62
+ default_version = parse_version(version_map[dep_name])
63
+ dep_version = self._extract_min_dependency(dep)
64
+ # If the version is higher than the default, update VERSION_MAP
65
+ # with higher min dependency version
66
+ if dep_version > default_version:
67
+ version_map[dep_name] = str(dep_version)
68
+
69
+ def external_lib_version_map(self, file_content: str, additional_version_map: dict[str, str]) -> dict:
61
70
  # Load the pyproject.toml file if it exists and extract fields to keep.
62
71
  result: dict = {"KEEP_FIELDS": {}}
63
72
  try:
@@ -80,15 +89,11 @@ class GeneralSerializer(BaseSerializer):
80
89
  for dep in loaded_pyproject_toml["project"]["dependencies"]:
81
90
  dep_name = re.split(r"[<>=\[]", dep)[0].strip()
82
91
 
83
- # Check if dependency is one we track in VERSION_MAP
92
+ # Check if dependency is one we track in version map
84
93
  if dep_name in VERSION_MAP:
85
- # For tracked dependencies, check if the version is higher than our default
86
- default_version = parse_version(VERSION_MAP[dep_name])
87
- dep_version = self._extract_min_dependency(dep)
88
- # If the version is higher than the default, update VERSION_MAP
89
- # with higher min dependency version
90
- if dep_version > default_version:
91
- VERSION_MAP[dep_name] = str(dep_version)
94
+ self._update_version_map(VERSION_MAP, dep_name, dep)
95
+ elif dep_name in additional_version_map:
96
+ self._update_version_map(additional_version_map, dep_name, dep)
92
97
  else:
93
98
  # Keep non-default dependencies
94
99
  kept_deps.append(dep)
@@ -107,9 +112,20 @@ class GeneralSerializer(BaseSerializer):
107
112
  def serialize_package_file(self, template_name: str, file_content: str, **kwargs: Any) -> str:
108
113
  template = self.env.get_template(template_name)
109
114
 
115
+ additional_version_map = {}
116
+ if self.code_model.has_external_type:
117
+ for item in self.code_model.external_types:
118
+ if item.package_name:
119
+ if item.min_version:
120
+ additional_version_map[item.package_name] = item.min_version
121
+ else:
122
+ # Use "0" as a placeholder when min_version is not specified for external types.
123
+ # This allows the dependency to be included without a specific version constraint.
124
+ additional_version_map[item.package_name] = "0"
125
+
110
126
  # Add fields to keep from an existing pyproject.toml
111
127
  if template_name == "pyproject.toml.jinja2":
112
- params = self._keep_pyproject_fields(file_content)
128
+ params = self.external_lib_version_map(file_content, additional_version_map)
113
129
  else:
114
130
  params = {}
115
131
 
@@ -126,6 +142,7 @@ class GeneralSerializer(BaseSerializer):
126
142
  dev_status = "4 - Beta"
127
143
  else:
128
144
  dev_status = "5 - Production/Stable"
145
+
129
146
  params |= {
130
147
  "code_model": self.code_model,
131
148
  "dev_status": dev_status,
@@ -136,6 +153,7 @@ class GeneralSerializer(BaseSerializer):
136
153
  "VERSION_MAP": VERSION_MAP,
137
154
  "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION,
138
155
  "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION,
156
+ "ADDITIONAL_DEPENDENCIES": [f"{item[0]}>={item[1]}" for item in additional_version_map.items()],
139
157
  }
140
158
  params |= {"options": self.code_model.options}
141
159
  params |= kwargs
@@ -329,10 +329,15 @@ class DpgModelSerializer(_ModelSerializer):
329
329
  args.append("is_multipart_file_input=True")
330
330
  elif hasattr(prop.type, "encode") and prop.type.encode: # type: ignore
331
331
  args.append(f'format="{prop.type.encode}"') # type: ignore
332
+ elif prop.encode:
333
+ args.append(f'format="{prop.encode}"')
332
334
 
333
335
  if prop.xml_metadata:
334
336
  args.append(f"xml={prop.xml_metadata}")
335
337
 
338
+ if prop.original_tsp_name:
339
+ args.append(f'original_tsp_name="{prop.original_tsp_name}"')
340
+
336
341
  field = "rest_discriminator" if prop.is_discriminator else "rest_field"
337
342
  type_ignore = (
338
343
  " # type: ignore"
@@ -5,21 +5,28 @@
5
5
  {% set enable_custom_handling = "\n* " in doc_string or doc_string.startswith("* ") %}
6
6
  {%- if enable_custom_handling -%}
7
7
  {%- set lines = doc_string.split('\n') -%}
8
+ {%- set base_indent = wrap_string.lstrip('\n') -%}
8
9
  {%- set result_lines = [] -%}
9
10
  {%- for line in lines -%}
10
11
  {%- if line.startswith('* ') -%}
11
12
  {# Handle bullet points with proper continuation alignment #}
12
13
  {%- set bullet_content = line[2:] -%}
13
- {%- set base_indent = wrap_string.lstrip('\n') -%}
14
14
  {%- set bullet_line = base_indent + ' * ' + bullet_content -%}
15
15
  {%- set continuation_spaces = base_indent + ' ' -%}
16
16
  {%- set wrapped = bullet_line | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring='\n' + continuation_spaces) -%}
17
17
  {%- set _ = result_lines.append(wrapped) -%}
18
18
  {%- elif line.strip() -%}
19
- {%- set wrapped = line.strip() | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%}
20
- {%- set _ = result_lines.append(wrapped) -%}
19
+ {%- set line_indent = '' if line.strip().startswith(':') or loop.index == 1 else (base_indent + ' ') -%}
20
+ {%- set wrapped = (line_indent + line) | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%}
21
+ {%- for line in wrapped.split('\n') -%}
22
+ {%- set prefix = "" if loop.index == 1 else " " -%}
23
+ {%- set _ = result_lines.append(prefix + line) -%}
24
+ {%- endfor -%}
21
25
  {%- else -%}
22
- {%- set _ = result_lines.append('') -%}
26
+ {# Do not add continuous blank lines #}
27
+ {%- if (result_lines and result_lines[-1] != '') or not result_lines -%}
28
+ {%- set _ = result_lines.append('') -%}
29
+ {%- endif -%}
23
30
  {%- endif -%}
24
31
  {%- endfor -%}
25
32
  {%- set original_result = result_lines | join('\n') -%}
@@ -37,4 +44,4 @@
37
44
  {% set suffix = suffix_string if list_result | length == loop.index %}
38
45
  {{ prefix }}{{ line }}{{ suffix }}
39
46
  {% endfor %}
40
- {% endmacro %}
47
+ {% endmacro %}
@@ -25,6 +25,9 @@ from {{ code_model.core_library }}.exceptions import DeserializationError
25
25
  from {{ code_model.core_library }}{{ "" if code_model.is_azure_flavor else ".utils" }} import CaseInsensitiveEnumMeta
26
26
  from {{ code_model.core_library }}.{{ "" if code_model.is_azure_flavor else "runtime." }}pipeline import PipelineResponse
27
27
  from {{ code_model.core_library }}.serialization import _Null
28
+ {% if code_model.has_external_type %}
29
+ from {{ code_model.core_library }}.serialization import TypeHandlerRegistry
30
+ {% endif %}
28
31
  from {{ code_model.core_library }}.rest import HttpResponse
29
32
 
30
33
  _LOGGER = logging.getLogger(__name__)
@@ -34,6 +37,10 @@ __all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"]
34
37
  TZ_UTC = timezone.utc
35
38
  _T = typing.TypeVar("_T")
36
39
 
40
+ {% if code_model.has_external_type %}
41
+ TYPE_HANDLER_REGISTRY = TypeHandlerRegistry()
42
+ {% endif %}
43
+
37
44
 
38
45
  def _timedelta_as_isostr(td: timedelta) -> str:
39
46
  """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S'
@@ -158,6 +165,11 @@ class SdkJSONEncoder(JSONEncoder):
158
165
  except AttributeError:
159
166
  # This will be raised when it hits value.total_seconds in the method above
160
167
  pass
168
+ {% if code_model.has_external_type %}
169
+ custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o)
170
+ if custom_serializer:
171
+ return custom_serializer(o)
172
+ {% endif %}
161
173
  return super(SdkJSONEncoder, self).default(o)
162
174
 
163
175
 
@@ -167,6 +179,19 @@ _VALID_RFC7231 = re.compile(
167
179
  r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT"
168
180
  )
169
181
 
182
+ _ARRAY_ENCODE_MAPPING = {
183
+ "pipeDelimited": "|",
184
+ "spaceDelimited": " ",
185
+ "commaDelimited": ",",
186
+ "newlineDelimited": "\n",
187
+ }
188
+
189
+ def _deserialize_array_encoded(delimit: str, attr):
190
+ if isinstance(attr, str):
191
+ if attr == "":
192
+ return []
193
+ return attr.split(delimit)
194
+ return attr
170
195
 
171
196
  def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
172
197
  """Deserialize ISO-8601 formatted string into Datetime object.
@@ -311,9 +336,17 @@ _DESERIALIZE_MAPPING_WITHFORMAT = {
311
336
  def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None):
312
337
  if annotation is int and rf and rf._format == "str":
313
338
  return _deserialize_int_as_str
339
+ if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING:
340
+ return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format])
314
341
  if rf and rf._format:
315
342
  return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format)
343
+ {% if code_model.has_external_type %}
344
+ if _DESERIALIZE_MAPPING.get(annotation): # pyright: ignore
345
+ return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore
346
+ return TYPE_HANDLER_REGISTRY.get_deserializer(annotation) # pyright: ignore
347
+ {% else %}
316
348
  return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore
349
+ {% endif %}
317
350
 
318
351
 
319
352
  def _get_type_alias_type(module_name: str, alias_name: str):
@@ -479,6 +512,8 @@ def _is_model(obj: typing.Any) -> bool:
479
512
 
480
513
  def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements
481
514
  if isinstance(o, list):
515
+ if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o):
516
+ return _ARRAY_ENCODE_MAPPING[format].join(o)
482
517
  return [_serialize(x, format) for x in o]
483
518
  if isinstance(o, dict):
484
519
  return {k: _serialize(v, format) for k, v in o.items()}
@@ -507,6 +542,14 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m
507
542
  except AttributeError:
508
543
  # This will be raised when it hits value.total_seconds in the method above
509
544
  pass
545
+ {% if code_model.has_external_type %}
546
+
547
+ # Check if there's a custom serializer for the type
548
+ custom_serializer = TYPE_HANDLER_REGISTRY.get_serializer(o)
549
+ if custom_serializer:
550
+ return custom_serializer(o)
551
+
552
+ {% endif %}
510
553
  return o
511
554
 
512
555
 
@@ -636,6 +679,12 @@ class Model(_MyMutableMapping):
636
679
  if not rf._rest_name_input:
637
680
  rf._rest_name_input = attr
638
681
  cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items())
682
+ {% if code_model.has_padded_model_property %}
683
+ cls._backcompat_attr_to_rest_field: dict[str, _RestField] = {
684
+ Model._get_backcompat_attribute_name(cls._attr_to_rest_field, attr): rf for attr, rf in cls
685
+ ._attr_to_rest_field.items()
686
+ }
687
+ {% endif %}
639
688
  cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
640
689
 
641
690
  return super().__new__(cls)
@@ -645,6 +694,18 @@ class Model(_MyMutableMapping):
645
694
  if hasattr(base, "__mapping__"):
646
695
  base.__mapping__[discriminator or cls.__name__] = cls # type: ignore
647
696
 
697
+ {% if code_model.has_padded_model_property %}
698
+ @classmethod
699
+ def _get_backcompat_attribute_name(cls, attr_to_rest_field: dict[str, "_RestField"], attr_name: str) -> str:
700
+ rest_field_obj = attr_to_rest_field.get(attr_name) # pylint: disable=protected-access
701
+ if rest_field_obj is None:
702
+ return attr_name
703
+ original_tsp_name = getattr(rest_field_obj, "_original_tsp_name", None) # pylint: disable=protected-access
704
+ if original_tsp_name:
705
+ return original_tsp_name
706
+ return attr_name
707
+ {% endif %}
708
+
648
709
  @classmethod
649
710
  def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]:
650
711
  for v in cls.__dict__.values():
@@ -765,6 +826,17 @@ def _deserialize_sequence(
765
826
  return obj
766
827
  if isinstance(obj, ET.Element):
767
828
  obj = list(obj)
829
+ try:
830
+ if (
831
+ isinstance(obj, str)
832
+ and isinstance(deserializer, functools.partial)
833
+ and isinstance(deserializer.args[0], functools.partial)
834
+ and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable
835
+ ):
836
+ # encoded string may be deserialized to sequence
837
+ return deserializer(obj)
838
+ except: # pylint: disable=bare-except
839
+ pass
768
840
  return type(obj)(_deserialize(deserializer, entry, module) for entry in obj)
769
841
 
770
842
 
@@ -971,6 +1043,9 @@ def _failsafe_deserialize_xml(
971
1043
  return None
972
1044
 
973
1045
 
1046
+ {% if code_model.has_padded_model_property %}
1047
+ # pylint: disable=too-many-instance-attributes
1048
+ {% endif %}
974
1049
  class _RestField:
975
1050
  def __init__(
976
1051
  self,
@@ -983,6 +1058,9 @@ class _RestField:
983
1058
  format: typing.Optional[str] = None,
984
1059
  is_multipart_file_input: bool = False,
985
1060
  xml: typing.Optional[dict[str, typing.Any]] = None,
1061
+ {% if code_model.has_padded_model_property %}
1062
+ original_tsp_name: typing.Optional[str] = None,
1063
+ {% endif %}
986
1064
  ):
987
1065
  self._type = type
988
1066
  self._rest_name_input = name
@@ -994,10 +1072,17 @@ class _RestField:
994
1072
  self._format = format
995
1073
  self._is_multipart_file_input = is_multipart_file_input
996
1074
  self._xml = xml if xml is not None else {}
1075
+ {% if code_model.has_padded_model_property %}
1076
+ self._original_tsp_name = original_tsp_name
1077
+ {% endif %}
997
1078
 
998
1079
  @property
999
1080
  def _class_type(self) -> typing.Any:
1000
- return getattr(self._type, "args", [None])[0]
1081
+ result = getattr(self._type, "args", [None])[0]
1082
+ # type may be wrapped by nested functools.partial so we need to check for that
1083
+ if isinstance(result, functools.partial):
1084
+ return getattr(result, "args", [None])[0]
1085
+ return result
1001
1086
 
1002
1087
  @property
1003
1088
  def _rest_name(self) -> str:
@@ -1045,6 +1130,9 @@ def rest_field(
1045
1130
  format: typing.Optional[str] = None,
1046
1131
  is_multipart_file_input: bool = False,
1047
1132
  xml: typing.Optional[dict[str, typing.Any]] = None,
1133
+ {% if code_model.has_padded_model_property %}
1134
+ original_tsp_name: typing.Optional[str] = None,
1135
+ {% endif %}
1048
1136
  ) -> typing.Any:
1049
1137
  return _RestField(
1050
1138
  name=name,
@@ -1054,6 +1142,9 @@ def rest_field(
1054
1142
  format=format,
1055
1143
  is_multipart_file_input=is_multipart_file_input,
1056
1144
  xml=xml,
1145
+ {% if code_model.has_padded_model_property %}
1146
+ original_tsp_name=original_tsp_name,
1147
+ {% endif %}
1057
1148
  )
1058
1149
 
1059
1150
 
@@ -56,6 +56,9 @@ dependencies = [
56
56
  "{{ dep }}",
57
57
  {% endfor %}
58
58
  {% endif %}
59
+ {% for dep in ADDITIONAL_DEPENDENCIES %}
60
+ "{{ dep }}",
61
+ {% endfor %}
59
62
  ]
60
63
  dynamic = [
61
64
  {% if options.get('package-mode') %}"version", {% endif %}"readme"
@@ -108,6 +108,9 @@ setup(
108
108
  "corehttp[requests]>={{ VERSION_MAP["corehttp"] }}",
109
109
  {% endif %}
110
110
  "typing-extensions>={{ VERSION_MAP['typing-extensions'] }}",
111
+ {% for dep in ADDITIONAL_DEPENDENCIES %}
112
+ {{ dep }},
113
+ {% endfor %}
111
114
  ],
112
115
  {% if options["package-mode"] %}
113
116
  python_requires=">={{ MIN_PYTHON_VERSION }}",
@@ -78,9 +78,8 @@ def serialize_multipart_data_entry(data_entry: Any) -> Any:
78
78
 
79
79
  def prepare_multipart_form_data(
80
80
  body: Mapping[str, Any], multipart_fields: list[str], data_fields: list[str]
81
- ) -> tuple[list[FileType], dict[str, Any]]:
81
+ ) -> list[FileType]:
82
82
  files: list[FileType] = []
83
- data: dict[str, Any] = {}
84
83
  for multipart_field in multipart_fields:
85
84
  multipart_entry = body.get(multipart_field)
86
85
  if isinstance(multipart_entry, list):
@@ -88,10 +87,12 @@ def prepare_multipart_form_data(
88
87
  elif multipart_entry:
89
88
  files.append((multipart_field, multipart_entry))
90
89
 
90
+ # if files is empty, sdk core library can't handle multipart/form-data correctly, so
91
+ # we put data fields into files with filename as None to avoid that scenario.
91
92
  for data_field in data_fields:
92
93
  data_entry = body.get(data_field)
93
94
  if data_entry:
94
- data[data_field] = serialize_multipart_data_entry(data_entry)
95
+ files.append((data_field, str(serialize_multipart_data_entry(data_entry))))
95
96
 
96
- return files, data
97
+ return files
97
98
  {% endif %}
@@ -236,7 +236,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
236
236
  body_parameter["type"]["types"].insert(1, any_obj_list_or_dict)
237
237
  code_model["types"].append(body_parameter["type"])
238
238
 
239
- def pad_reserved_words(self, name: str, pad_type: PadType):
239
+ def pad_reserved_words(self, name: str, pad_type: PadType, yaml_type: dict[str, Any]) -> str:
240
240
  # we want to pad hidden variables as well
241
241
  if not name:
242
242
  # we'll pass in empty operation groups sometime etc.
@@ -250,6 +250,10 @@ class PreProcessPlugin(YamlUpdatePlugin):
250
250
  name_prefix = "_" if name[0] == "_" else ""
251
251
  name = name[1:] if name[0] == "_" else name
252
252
  if name.lower() in reserved_words[pad_type]:
253
+ if self.is_tsp and name.lower() in TSP_RESERVED_WORDS.get(pad_type, []):
254
+ # to maintain backcompat for cases where we pad in tsp but not in autorest,
255
+ # if we have a tsp reserved word, we also want to keep track of the original name for backcompat
256
+ yaml_type["originalTspName"] = name_prefix + name
253
257
  return name_prefix + name + pad_type
254
258
  return name_prefix + name
255
259
 
@@ -257,11 +261,13 @@ class PreProcessPlugin(YamlUpdatePlugin):
257
261
  for type in yaml_data:
258
262
  for property in type.get("properties", []):
259
263
  property["description"] = update_description(property.get("description", ""))
260
- property["clientName"] = self.pad_reserved_words(property["clientName"].lower(), PadType.PROPERTY)
264
+ property["clientName"] = self.pad_reserved_words(
265
+ property["clientName"].lower(), PadType.PROPERTY, property
266
+ )
261
267
  add_redefined_builtin_info(property["clientName"], property)
262
268
  if type.get("name"):
263
269
  pad_type = PadType.MODEL if type["type"] == "model" else PadType.ENUM_CLASS
264
- name = self.pad_reserved_words(type["name"], pad_type)
270
+ name = self.pad_reserved_words(type["name"], pad_type, type)
265
271
  type["name"] = name[0].upper() + name[1:]
266
272
  type["description"] = update_description(type.get("description", ""), type["name"])
267
273
  type["snakeCaseName"] = to_snake_case(type["name"])
@@ -269,7 +275,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
269
275
  # we're enums
270
276
  values_to_add = []
271
277
  for value in type["values"]:
272
- padded_name = self.pad_reserved_words(value["name"].lower(), PadType.ENUM_VALUE).upper()
278
+ padded_name = self.pad_reserved_words(value["name"].lower(), PadType.ENUM_VALUE, value).upper()
273
279
  if self.version_tolerant:
274
280
  if padded_name[0] in "0123456789":
275
281
  padded_name = "ENUM_" + padded_name
@@ -364,12 +370,14 @@ class PreProcessPlugin(YamlUpdatePlugin):
364
370
  def update_parameter(self, yaml_data: dict[str, Any]) -> None:
365
371
  yaml_data["description"] = update_description(yaml_data.get("description", ""))
366
372
  if not (yaml_data["location"] == "header" and yaml_data["clientName"] in ("content_type", "accept")):
367
- yaml_data["clientName"] = self.pad_reserved_words(yaml_data["clientName"].lower(), PadType.PARAMETER)
373
+ yaml_data["clientName"] = self.pad_reserved_words(
374
+ yaml_data["clientName"].lower(), PadType.PARAMETER, yaml_data
375
+ )
368
376
  if yaml_data.get("propertyToParameterName"):
369
377
  # need to create a new one with padded keys and values
370
378
  yaml_data["propertyToParameterName"] = {
371
- self.pad_reserved_words(prop, PadType.PROPERTY): self.pad_reserved_words(
372
- param_name, PadType.PARAMETER
379
+ self.pad_reserved_words(prop, PadType.PROPERTY, yaml_data): self.pad_reserved_words(
380
+ param_name, PadType.PARAMETER, yaml_data
373
381
  ).lower()
374
382
  for prop, param_name in yaml_data["propertyToParameterName"].items()
375
383
  }
@@ -390,15 +398,17 @@ class PreProcessPlugin(YamlUpdatePlugin):
390
398
  *,
391
399
  is_overload: bool = False,
392
400
  ) -> None:
393
- yaml_data["groupName"] = self.pad_reserved_words(yaml_data["groupName"], PadType.OPERATION_GROUP)
401
+ yaml_data["groupName"] = self.pad_reserved_words(yaml_data["groupName"], PadType.OPERATION_GROUP, yaml_data)
394
402
  yaml_data["groupName"] = to_snake_case(yaml_data["groupName"])
395
403
  yaml_data["name"] = yaml_data["name"].lower()
396
404
  if yaml_data.get("isLroInitialOperation") is True:
397
405
  yaml_data["name"] = (
398
- "_" + self.pad_reserved_words(extract_original_name(yaml_data["name"]), PadType.METHOD) + "_initial"
406
+ "_"
407
+ + self.pad_reserved_words(extract_original_name(yaml_data["name"]), PadType.METHOD, yaml_data)
408
+ + "_initial"
399
409
  )
400
410
  else:
401
- yaml_data["name"] = self.pad_reserved_words(yaml_data["name"], PadType.METHOD)
411
+ yaml_data["name"] = self.pad_reserved_words(yaml_data["name"], PadType.METHOD, yaml_data)
402
412
  yaml_data["description"] = update_description(yaml_data["description"], yaml_data["name"])
403
413
  yaml_data["summary"] = update_description(yaml_data.get("summary", ""))
404
414
  body_parameter = yaml_data.get("bodyParameter")
@@ -485,7 +495,7 @@ class PreProcessPlugin(YamlUpdatePlugin):
485
495
  item_type = item_type or yaml_data["itemType"]["elementType"]
486
496
  if yaml_data.get("nextOperation"):
487
497
  yaml_data["nextOperation"]["groupName"] = self.pad_reserved_words(
488
- yaml_data["nextOperation"]["groupName"], PadType.OPERATION_GROUP
498
+ yaml_data["nextOperation"]["groupName"], PadType.OPERATION_GROUP, yaml_data["nextOperation"]
489
499
  )
490
500
  yaml_data["nextOperation"]["groupName"] = to_snake_case(yaml_data["nextOperation"]["groupName"])
491
501
  for response in yaml_data["nextOperation"].get("responses", []):
@@ -503,10 +513,11 @@ class PreProcessPlugin(YamlUpdatePlugin):
503
513
  operation_group["identifyName"] = self.pad_reserved_words(
504
514
  operation_group.get("name", operation_group["propertyName"]),
505
515
  PadType.OPERATION_GROUP,
516
+ operation_group,
506
517
  )
507
518
  operation_group["identifyName"] = to_snake_case(operation_group["identifyName"])
508
519
  operation_group["propertyName"] = self.pad_reserved_words(
509
- operation_group["propertyName"], PadType.OPERATION_GROUP
520
+ operation_group["propertyName"], PadType.OPERATION_GROUP, operation_group
510
521
  )
511
522
  operation_group["propertyName"] = to_snake_case(operation_group["propertyName"])
512
523
  operation_group["className"] = update_operation_group_class_name(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autorest/python",
3
- "version": "6.43.0",
3
+ "version": "6.45.0",
4
4
  "description": "The Python extension for generators in AutoRest.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "homepage": "https://github.com/Azure/autorest.python/blob/main/README.md",
21
21
  "dependencies": {
22
- "@typespec/http-client-python": "~0.21.0",
22
+ "@typespec/http-client-python": "~0.23.0",
23
23
  "@autorest/system-requirements": "~1.0.2",
24
24
  "fs-extra": "~11.2.0",
25
25
  "tsx": "~4.19.1"