@typespec/http-client-python 0.22.0 → 0.23.1

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 (31) hide show
  1. package/dist/emitter/types.d.ts.map +1 -1
  2. package/dist/emitter/types.js +1 -0
  3. package/dist/emitter/types.js.map +1 -1
  4. package/dist/emitter/utils.js.map +1 -1
  5. package/emitter/src/types.ts +1 -0
  6. package/emitter/src/utils.ts +3 -3
  7. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  8. package/eng/scripts/ci/regenerate.ts +5 -1
  9. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  10. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  11. package/generator/build/lib/pygen/codegen/models/property.py +1 -0
  12. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +6 -1
  13. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +2 -0
  14. package/generator/build/lib/pygen/codegen/templates/macros.jinja2 +12 -5
  15. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +83 -2
  16. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +14 -3
  17. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  18. package/generator/pygen/codegen/models/property.py +1 -0
  19. package/generator/pygen/codegen/serializers/builder_serializer.py +6 -1
  20. package/generator/pygen/codegen/serializers/model_serializer.py +2 -0
  21. package/generator/pygen/codegen/templates/macros.jinja2 +12 -5
  22. package/generator/pygen/codegen/templates/model_base.py.jinja2 +83 -2
  23. package/generator/pygen/codegen/templates/serialization.py.jinja2 +14 -3
  24. package/generator/test/azure/requirements.txt +1 -0
  25. package/generator/test/generic_mock_api_tests/asynctests/test_encode_array_async.py +43 -0
  26. package/generator/test/generic_mock_api_tests/asynctests/test_specs_documentation_async.py +60 -0
  27. package/generator/test/generic_mock_api_tests/test_encode_array.py +38 -0
  28. package/generator/test/generic_mock_api_tests/test_specs_documentation.py +52 -0
  29. package/generator/test/unbranded/requirements.txt +1 -0
  30. package/generator/test/unittests/test_model_base_serialization.py +521 -5
  31. package/package.json +1 -1
@@ -22,7 +22,7 @@ const argv = parseArgs({
22
22
  });
23
23
 
24
24
  // Add this near the top with other constants
25
- const SKIP_SPECS = ["type/union/discriminated", "documentation"];
25
+ const SKIP_SPECS = ["type/union/discriminated"];
26
26
 
27
27
  // Get the directory of the current file
28
28
  const PLUGIN_DIR = argv.values.pluginDir
@@ -272,6 +272,10 @@ const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<string, st
272
272
  "package-name": "typetest-union",
273
273
  namespace: "typetest.union",
274
274
  },
275
+ documentation: {
276
+ "package-name": "specs-documentation",
277
+ namespace: "specs.documentation",
278
+ },
275
279
  };
276
280
 
277
281
  function toPosix(dir: string): string {
@@ -40,6 +40,7 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
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
42
  self.original_tsp_name: Optional[str] = self.yaml_data.get("originalTspName")
43
+ self.encode: Optional[str] = self.yaml_data.get("encode")
43
44
 
44
45
  def pylint_disable(self) -> str:
45
46
  retval: str = ""
@@ -403,7 +403,12 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
403
403
  builder: RequestBuilderType,
404
404
  ) -> list[str]:
405
405
  def _get_value(param):
406
- declaration = param.get_declaration() if param.constant else None
406
+ if param.constant:
407
+ declaration = param.get_declaration()
408
+ elif param.client_default_value_declaration is not None:
409
+ declaration = param.client_default_value_declaration
410
+ else:
411
+ declaration = None
407
412
  if param.location in [ParameterLocation.HEADER, ParameterLocation.QUERY]:
408
413
  kwarg_dict = "headers" if param.location == ParameterLocation.HEADER else "params"
409
414
  return f"_{kwarg_dict}.pop('{param.wire_name}', {declaration})"
@@ -329,6 +329,8 @@ 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}")
@@ -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 %}
@@ -179,6 +179,19 @@ _VALID_RFC7231 = re.compile(
179
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"
180
180
  )
181
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
182
195
 
183
196
  def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
184
197
  """Deserialize ISO-8601 formatted string into Datetime object.
@@ -323,6 +336,8 @@ _DESERIALIZE_MAPPING_WITHFORMAT = {
323
336
  def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None):
324
337
  if annotation is int and rf and rf._format == "str":
325
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])
326
341
  if rf and rf._format:
327
342
  return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format)
328
343
  {% if code_model.has_external_type %}
@@ -367,9 +382,39 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
367
382
  return key in self._data
368
383
 
369
384
  def __getitem__(self, key: str) -> typing.Any:
385
+ # If this key has been deserialized (for mutable types), we need to handle serialization
386
+ if hasattr(self, "_attr_to_rest_field"):
387
+ cache_attr = f"_deserialized_{key}"
388
+ if hasattr(self, cache_attr):
389
+ rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key)
390
+ if rf:
391
+ value = self._data.get(key)
392
+ if isinstance(value, (dict, list, set)):
393
+ # For mutable types, serialize and return
394
+ # But also update _data with serialized form and clear flag
395
+ # so mutations via this returned value affect _data
396
+ serialized = _serialize(value, rf._format)
397
+ # If serialized form is same type (no transformation needed),
398
+ # return _data directly so mutations work
399
+ if isinstance(serialized, type(value)) and serialized == value:
400
+ return self._data.get(key)
401
+ # Otherwise return serialized copy and clear flag
402
+ try:
403
+ object.__delattr__(self, cache_attr)
404
+ except AttributeError:
405
+ pass
406
+ # Store serialized form back
407
+ self._data[key] = serialized
408
+ return serialized
370
409
  return self._data.__getitem__(key)
371
410
 
372
411
  def __setitem__(self, key: str, value: typing.Any) -> None:
412
+ # Clear any cached deserialized value when setting through dictionary access
413
+ cache_attr = f"_deserialized_{key}"
414
+ try:
415
+ object.__delattr__(self, cache_attr)
416
+ except AttributeError:
417
+ pass
373
418
  self._data.__setitem__(key, value)
374
419
 
375
420
  def __delitem__(self, key: str) -> None:
@@ -497,6 +542,8 @@ def _is_model(obj: typing.Any) -> bool:
497
542
 
498
543
  def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements
499
544
  if isinstance(o, list):
545
+ if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o):
546
+ return _ARRAY_ENCODE_MAPPING[format].join(o)
500
547
  return [_serialize(x, format) for x in o]
501
548
  if isinstance(o, dict):
502
549
  return {k: _serialize(v, format) for k, v in o.items()}
@@ -809,6 +856,17 @@ def _deserialize_sequence(
809
856
  return obj
810
857
  if isinstance(obj, ET.Element):
811
858
  obj = list(obj)
859
+ try:
860
+ if (
861
+ isinstance(obj, str)
862
+ and isinstance(deserializer, functools.partial)
863
+ and isinstance(deserializer.args[0], functools.partial)
864
+ and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable
865
+ ):
866
+ # encoded string may be deserialized to sequence
867
+ return deserializer(obj)
868
+ except: # pylint: disable=bare-except
869
+ pass
812
870
  return type(obj)(_deserialize(deserializer, entry, module) for entry in obj)
813
871
 
814
872
 
@@ -1065,14 +1123,37 @@ class _RestField:
1065
1123
  def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin
1066
1124
  # by this point, type and rest_name will have a value bc we default
1067
1125
  # them in __new__ of the Model class
1068
- item = obj.get(self._rest_name)
1126
+ # Use _data.get() directly to avoid triggering __getitem__ which clears the cache
1127
+ item = obj._data.get(self._rest_name)
1069
1128
  if item is None:
1070
1129
  return item
1071
1130
  if self._is_model:
1072
1131
  return item
1073
- return _deserialize(self._type, _serialize(item, self._format), rf=self)
1132
+
1133
+ # For mutable types, we want mutations to directly affect _data
1134
+ # Check if we've already deserialized this value
1135
+ cache_attr = f"_deserialized_{self._rest_name}"
1136
+ if hasattr(obj, cache_attr):
1137
+ # Return the value from _data directly (it's been deserialized in place)
1138
+ return obj._data.get(self._rest_name)
1139
+
1140
+ deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1141
+
1142
+ # For mutable types, store the deserialized value back in _data
1143
+ # so mutations directly affect _data
1144
+ if isinstance(deserialized, (dict, list, set)):
1145
+ obj._data[self._rest_name] = deserialized
1146
+ object.__setattr__(obj, cache_attr, True) # Mark as deserialized
1147
+ return deserialized
1148
+
1149
+ return deserialized
1074
1150
 
1075
1151
  def __set__(self, obj: Model, value) -> None:
1152
+ # Clear the cached deserialized object when setting a new value
1153
+ cache_attr = f"_deserialized_{self._rest_name}"
1154
+ if hasattr(obj, cache_attr):
1155
+ object.__delattr__(obj, cache_attr)
1156
+
1076
1157
  if value is None:
1077
1158
  # we want to wipe out entries if users set attr to None
1078
1159
  try:
@@ -817,13 +817,20 @@ class Serializer: # pylint: disable=too-many-public-methods
817
817
  :param str data_type: Type of object in the iterable.
818
818
  :rtype: str, int, float, bool
819
819
  :return: serialized object
820
+ :raises TypeError: raise if data_type is not one of str, int, float, bool.
820
821
  """
821
822
  custom_serializer = cls._get_custom_serializers(data_type, **kwargs)
822
823
  if custom_serializer:
823
824
  return custom_serializer(data)
824
825
  if data_type == "str":
825
826
  return cls.serialize_unicode(data)
826
- return eval(data_type)(data) # nosec # pylint: disable=eval-used
827
+ if data_type == "int":
828
+ return int(data)
829
+ if data_type == "float":
830
+ return float(data)
831
+ if data_type == "bool":
832
+ return bool(data)
833
+ raise TypeError("Unknown basic data type: {}".format(data_type))
827
834
 
828
835
  @classmethod
829
836
  def serialize_unicode(cls, data):
@@ -1753,7 +1760,7 @@ class Deserializer:
1753
1760
  :param str data_type: deserialization data type.
1754
1761
  :return: Deserialized basic type.
1755
1762
  :rtype: str, int, float or bool
1756
- :raises TypeError: if string format is not valid.
1763
+ :raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool.
1757
1764
  """
1758
1765
  # If we're here, data is supposed to be a basic type.
1759
1766
  # If it's still an XML node, take the text
@@ -1779,7 +1786,11 @@ class Deserializer:
1779
1786
 
1780
1787
  if data_type == "str":
1781
1788
  return self.deserialize_unicode(attr)
1782
- return eval(data_type)(attr) # nosec # pylint: disable=eval-used
1789
+ if data_type == "int":
1790
+ return int(attr)
1791
+ if data_type == "float":
1792
+ return float(attr)
1793
+ raise TypeError("Unknown basic data type: {}".format(data_type))
1783
1794
 
1784
1795
  @staticmethod
1785
1796
  def deserialize_unicode(data):
@@ -40,6 +40,7 @@ class Property(BaseModel): # pylint: disable=too-many-instance-attributes
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
42
  self.original_tsp_name: Optional[str] = self.yaml_data.get("originalTspName")
43
+ self.encode: Optional[str] = self.yaml_data.get("encode")
43
44
 
44
45
  def pylint_disable(self) -> str:
45
46
  retval: str = ""
@@ -403,7 +403,12 @@ class RequestBuilderSerializer(_BuilderBaseSerializer[RequestBuilderType]):
403
403
  builder: RequestBuilderType,
404
404
  ) -> list[str]:
405
405
  def _get_value(param):
406
- declaration = param.get_declaration() if param.constant else None
406
+ if param.constant:
407
+ declaration = param.get_declaration()
408
+ elif param.client_default_value_declaration is not None:
409
+ declaration = param.client_default_value_declaration
410
+ else:
411
+ declaration = None
407
412
  if param.location in [ParameterLocation.HEADER, ParameterLocation.QUERY]:
408
413
  kwarg_dict = "headers" if param.location == ParameterLocation.HEADER else "params"
409
414
  return f"_{kwarg_dict}.pop('{param.wire_name}', {declaration})"
@@ -329,6 +329,8 @@ 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}")
@@ -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 %}
@@ -179,6 +179,19 @@ _VALID_RFC7231 = re.compile(
179
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"
180
180
  )
181
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
182
195
 
183
196
  def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
184
197
  """Deserialize ISO-8601 formatted string into Datetime object.
@@ -323,6 +336,8 @@ _DESERIALIZE_MAPPING_WITHFORMAT = {
323
336
  def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None):
324
337
  if annotation is int and rf and rf._format == "str":
325
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])
326
341
  if rf and rf._format:
327
342
  return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format)
328
343
  {% if code_model.has_external_type %}
@@ -367,9 +382,39 @@ class _MyMutableMapping(MutableMapping[str, typing.Any]):
367
382
  return key in self._data
368
383
 
369
384
  def __getitem__(self, key: str) -> typing.Any:
385
+ # If this key has been deserialized (for mutable types), we need to handle serialization
386
+ if hasattr(self, "_attr_to_rest_field"):
387
+ cache_attr = f"_deserialized_{key}"
388
+ if hasattr(self, cache_attr):
389
+ rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key)
390
+ if rf:
391
+ value = self._data.get(key)
392
+ if isinstance(value, (dict, list, set)):
393
+ # For mutable types, serialize and return
394
+ # But also update _data with serialized form and clear flag
395
+ # so mutations via this returned value affect _data
396
+ serialized = _serialize(value, rf._format)
397
+ # If serialized form is same type (no transformation needed),
398
+ # return _data directly so mutations work
399
+ if isinstance(serialized, type(value)) and serialized == value:
400
+ return self._data.get(key)
401
+ # Otherwise return serialized copy and clear flag
402
+ try:
403
+ object.__delattr__(self, cache_attr)
404
+ except AttributeError:
405
+ pass
406
+ # Store serialized form back
407
+ self._data[key] = serialized
408
+ return serialized
370
409
  return self._data.__getitem__(key)
371
410
 
372
411
  def __setitem__(self, key: str, value: typing.Any) -> None:
412
+ # Clear any cached deserialized value when setting through dictionary access
413
+ cache_attr = f"_deserialized_{key}"
414
+ try:
415
+ object.__delattr__(self, cache_attr)
416
+ except AttributeError:
417
+ pass
373
418
  self._data.__setitem__(key, value)
374
419
 
375
420
  def __delitem__(self, key: str) -> None:
@@ -497,6 +542,8 @@ def _is_model(obj: typing.Any) -> bool:
497
542
 
498
543
  def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements
499
544
  if isinstance(o, list):
545
+ if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o):
546
+ return _ARRAY_ENCODE_MAPPING[format].join(o)
500
547
  return [_serialize(x, format) for x in o]
501
548
  if isinstance(o, dict):
502
549
  return {k: _serialize(v, format) for k, v in o.items()}
@@ -809,6 +856,17 @@ def _deserialize_sequence(
809
856
  return obj
810
857
  if isinstance(obj, ET.Element):
811
858
  obj = list(obj)
859
+ try:
860
+ if (
861
+ isinstance(obj, str)
862
+ and isinstance(deserializer, functools.partial)
863
+ and isinstance(deserializer.args[0], functools.partial)
864
+ and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable
865
+ ):
866
+ # encoded string may be deserialized to sequence
867
+ return deserializer(obj)
868
+ except: # pylint: disable=bare-except
869
+ pass
812
870
  return type(obj)(_deserialize(deserializer, entry, module) for entry in obj)
813
871
 
814
872
 
@@ -1065,14 +1123,37 @@ class _RestField:
1065
1123
  def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin
1066
1124
  # by this point, type and rest_name will have a value bc we default
1067
1125
  # them in __new__ of the Model class
1068
- item = obj.get(self._rest_name)
1126
+ # Use _data.get() directly to avoid triggering __getitem__ which clears the cache
1127
+ item = obj._data.get(self._rest_name)
1069
1128
  if item is None:
1070
1129
  return item
1071
1130
  if self._is_model:
1072
1131
  return item
1073
- return _deserialize(self._type, _serialize(item, self._format), rf=self)
1132
+
1133
+ # For mutable types, we want mutations to directly affect _data
1134
+ # Check if we've already deserialized this value
1135
+ cache_attr = f"_deserialized_{self._rest_name}"
1136
+ if hasattr(obj, cache_attr):
1137
+ # Return the value from _data directly (it's been deserialized in place)
1138
+ return obj._data.get(self._rest_name)
1139
+
1140
+ deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1141
+
1142
+ # For mutable types, store the deserialized value back in _data
1143
+ # so mutations directly affect _data
1144
+ if isinstance(deserialized, (dict, list, set)):
1145
+ obj._data[self._rest_name] = deserialized
1146
+ object.__setattr__(obj, cache_attr, True) # Mark as deserialized
1147
+ return deserialized
1148
+
1149
+ return deserialized
1074
1150
 
1075
1151
  def __set__(self, obj: Model, value) -> None:
1152
+ # Clear the cached deserialized object when setting a new value
1153
+ cache_attr = f"_deserialized_{self._rest_name}"
1154
+ if hasattr(obj, cache_attr):
1155
+ object.__delattr__(obj, cache_attr)
1156
+
1076
1157
  if value is None:
1077
1158
  # we want to wipe out entries if users set attr to None
1078
1159
  try:
@@ -817,13 +817,20 @@ class Serializer: # pylint: disable=too-many-public-methods
817
817
  :param str data_type: Type of object in the iterable.
818
818
  :rtype: str, int, float, bool
819
819
  :return: serialized object
820
+ :raises TypeError: raise if data_type is not one of str, int, float, bool.
820
821
  """
821
822
  custom_serializer = cls._get_custom_serializers(data_type, **kwargs)
822
823
  if custom_serializer:
823
824
  return custom_serializer(data)
824
825
  if data_type == "str":
825
826
  return cls.serialize_unicode(data)
826
- return eval(data_type)(data) # nosec # pylint: disable=eval-used
827
+ if data_type == "int":
828
+ return int(data)
829
+ if data_type == "float":
830
+ return float(data)
831
+ if data_type == "bool":
832
+ return bool(data)
833
+ raise TypeError("Unknown basic data type: {}".format(data_type))
827
834
 
828
835
  @classmethod
829
836
  def serialize_unicode(cls, data):
@@ -1753,7 +1760,7 @@ class Deserializer:
1753
1760
  :param str data_type: deserialization data type.
1754
1761
  :return: Deserialized basic type.
1755
1762
  :rtype: str, int, float or bool
1756
- :raises TypeError: if string format is not valid.
1763
+ :raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool.
1757
1764
  """
1758
1765
  # If we're here, data is supposed to be a basic type.
1759
1766
  # If it's still an XML node, take the text
@@ -1779,7 +1786,11 @@ class Deserializer:
1779
1786
 
1780
1787
  if data_type == "str":
1781
1788
  return self.deserialize_unicode(attr)
1782
- return eval(data_type)(attr) # nosec # pylint: disable=eval-used
1789
+ if data_type == "int":
1790
+ return int(attr)
1791
+ if data_type == "float":
1792
+ return float(attr)
1793
+ raise TypeError("Unknown basic data type: {}".format(data_type))
1783
1794
 
1784
1795
  @staticmethod
1785
1796
  def deserialize_unicode(data):
@@ -52,6 +52,7 @@ azure-mgmt-core==1.6.0
52
52
  -e ./generated/authentication-oauth2
53
53
  -e ./generated/authentication-union
54
54
  -e ./generated/setuppy-authentication-union
55
+ -e ./generated/specs-documentation
55
56
  -e ./generated/encode-duration
56
57
  -e ./generated/encode-numeric
57
58
  -e ./generated/encode-array
@@ -0,0 +1,43 @@
1
+ # -------------------------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # --------------------------------------------------------------------------
6
+
7
+ import pytest
8
+ from encode.array.aio import ArrayClient
9
+ from encode.array import models
10
+
11
+
12
+ @pytest.fixture
13
+ async def client():
14
+ async with ArrayClient() as client:
15
+ yield client
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_comma_delimited(client: ArrayClient):
20
+ body = models.CommaDelimitedArrayProperty(value=["blue", "red", "green"])
21
+ result = await client.property.comma_delimited(body)
22
+ assert result.value == ["blue", "red", "green"]
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_space_delimited(client: ArrayClient):
27
+ body = models.SpaceDelimitedArrayProperty(value=["blue", "red", "green"])
28
+ result = await client.property.space_delimited(body)
29
+ assert result.value == ["blue", "red", "green"]
30
+
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_pipe_delimited(client: ArrayClient):
34
+ body = models.PipeDelimitedArrayProperty(value=["blue", "red", "green"])
35
+ result = await client.property.pipe_delimited(body)
36
+ assert result.value == ["blue", "red", "green"]
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_newline_delimited(client: ArrayClient):
41
+ body = models.NewlineDelimitedArrayProperty(value=["blue", "red", "green"])
42
+ result = await client.property.newline_delimited(body)
43
+ assert result.value == ["blue", "red", "green"]
@@ -0,0 +1,60 @@
1
+ # -------------------------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # --------------------------------------------------------------------------
6
+
7
+ import pytest
8
+ from specs.documentation.aio import DocumentationClient
9
+ from specs.documentation import models
10
+
11
+
12
+ @pytest.fixture
13
+ async def client():
14
+ async with DocumentationClient(endpoint="http://localhost:3000") as client:
15
+ yield client
16
+
17
+
18
+ class TestLists:
19
+ @pytest.mark.asyncio
20
+ async def test_bullet_points_op(self, client: DocumentationClient):
21
+ # GET /documentation/lists/bullet-points/op
22
+ # Expected: 204 No Content
23
+ await client.lists.bullet_points_op()
24
+
25
+ @pytest.mark.skip(reason="https://github.com/microsoft/typespec/issues/9173")
26
+ @pytest.mark.asyncio
27
+ async def test_bullet_points_model(self, client: DocumentationClient):
28
+ # POST /documentation/lists/bullet-points/model
29
+ # Expected request body: {"prop": "Simple"}
30
+ # Expected: 200 OK
31
+ await client.lists.bullet_points_model(input=models.BulletPointsModel(prop="Simple"))
32
+
33
+ # Also test with JSON
34
+ await client.lists.bullet_points_model(body={"input": {"prop": "Simple"}})
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_numbered(self, client: DocumentationClient):
38
+ # GET /documentation/lists/numbered
39
+ # Expected: 204 No Content
40
+ await client.lists.numbered()
41
+
42
+
43
+ class TestTextFormatting:
44
+ @pytest.mark.asyncio
45
+ async def test_bold_text(self, client: DocumentationClient):
46
+ # GET /documentation/text-formatting/bold
47
+ # Expected: 204 No Content
48
+ await client.text_formatting.bold_text()
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_italic_text(self, client: DocumentationClient):
52
+ # GET /documentation/text-formatting/italic
53
+ # Expected: 204 No Content
54
+ await client.text_formatting.italic_text()
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_combined_formatting(self, client: DocumentationClient):
58
+ # GET /documentation/text-formatting/combined
59
+ # Expected: 204 No Content
60
+ await client.text_formatting.combined_formatting()