@typespec/http-client-python 0.34.2 → 0.35.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 (37) hide show
  1. package/dist/emitter/code-model.d.ts.map +1 -1
  2. package/dist/emitter/code-model.js.map +1 -1
  3. package/dist/emitter/types.d.ts.map +1 -1
  4. package/dist/emitter/types.js +5 -1
  5. package/dist/emitter/types.js.map +1 -1
  6. package/emitter/src/code-model.ts +1 -2
  7. package/emitter/src/types.ts +6 -2
  8. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  9. package/eng/scripts/ci/config/pylintrc +5 -1
  10. package/eng/scripts/ci/dev_requirements.txt +3 -3
  11. package/eng/scripts/ci/regenerate-common.ts +3 -0
  12. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  13. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  14. package/generator/build/lib/pygen/codegen/models/client.py +15 -0
  15. package/generator/build/lib/pygen/codegen/models/operation.py +10 -2
  16. package/generator/build/lib/pygen/codegen/models/operation_group.py +2 -0
  17. package/generator/build/lib/pygen/codegen/models/primitive_types.py +5 -0
  18. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +50 -5
  19. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +23 -3
  20. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +41 -11
  21. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +17 -1
  22. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  23. package/generator/build/lib/pygen/preprocess/__init__.py +19 -0
  24. package/generator/build/lib/pygen/utils.py +1 -1
  25. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  26. package/generator/pygen/codegen/models/client.py +15 -0
  27. package/generator/pygen/codegen/models/operation.py +10 -2
  28. package/generator/pygen/codegen/models/operation_group.py +2 -0
  29. package/generator/pygen/codegen/models/primitive_types.py +5 -0
  30. package/generator/pygen/codegen/serializers/builder_serializer.py +50 -5
  31. package/generator/pygen/codegen/serializers/model_serializer.py +23 -3
  32. package/generator/pygen/codegen/serializers/types_serializer.py +41 -11
  33. package/generator/pygen/codegen/templates/model_base.py.jinja2 +17 -1
  34. package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  35. package/generator/pygen/preprocess/__init__.py +19 -0
  36. package/generator/pygen/utils.py +1 -1
  37. package/package.json +3 -2
@@ -23,7 +23,7 @@ from ..models.primitive_types import (
23
23
  )
24
24
  from .import_serializer import FileImportSerializer
25
25
  from .base_serializer import BaseSerializer
26
- from ..models.utils import NamespaceType
26
+ from ..models.utils import NamespaceType, add_to_pylint_disable
27
27
 
28
28
 
29
29
  def _get_xml_deserializer_name(prop: Property) -> Optional[str]: # pylint: disable=too-many-return-statements
@@ -199,6 +199,16 @@ class _ModelSerializer(BaseSerializer, ABC):
199
199
  def pylint_disable(self, model: ModelType) -> str:
200
200
  return " # pylint: disable=" + ", ".join(self.pylint_disable_items(model))
201
201
 
202
+ def class_pylint_disable(self, model: ModelType) -> str:
203
+ """Class-level pylint disables for the model declaration line."""
204
+ retval = model.pylint_disable()
205
+ # When the model's only constructor is ``def __init__(self, *args, **kwargs)`` (i.e. no
206
+ # typed overload is generated), the guideline checker reports the ``*args`` vararg as an
207
+ # undocumented param. There is no meaningful param to document, so silence the check.
208
+ if not self.need_init(model) and self.initialize_properties(model):
209
+ retval = add_to_pylint_disable(retval, "docstring-missing-param")
210
+ return retval
211
+
202
212
  def global_pylint_disables(self) -> str:
203
213
  return ""
204
214
 
@@ -244,7 +254,7 @@ class MsrestModelSerializer(_ModelSerializer):
244
254
  )
245
255
  if model.parents:
246
256
  basename = ", ".join([m.name for m in model.parents])
247
- return f"class {model.name}({basename}):{model.pylint_disable()}"
257
+ return f"class {model.name}({basename}):{self.class_pylint_disable(model)}"
248
258
 
249
259
  @staticmethod
250
260
  def get_properties_to_initialize(model: ModelType) -> list[Property]:
@@ -407,7 +417,17 @@ class DpgModelSerializer(_ModelSerializer):
407
417
  basename = ", ".join([m.name for m in model.parents])
408
418
  if model.discriminator_value:
409
419
  basename += f", discriminator='{model.discriminator_value}'"
410
- return f"class {model.name}({basename}):{model.pylint_disable()}"
420
+ return f"class {model.name}({basename}):{self.class_pylint_disable(model)}"
421
+
422
+ def class_pylint_disable(self, model: ModelType) -> str:
423
+ retval = super().class_pylint_disable(model)
424
+ # DPG models render an empty ``@overload def __init__(self, *, ...)`` body, so the
425
+ # guideline checker validates the keyword-only arguments against the *class* docstring.
426
+ # We only document those arguments as instance variables (``:ivar:``) to avoid a redundant
427
+ # ``:keyword:`` entry per property, so silence the keyword/keyword-only correlation check.
428
+ if self._init_line_parameters(model):
429
+ retval = add_to_pylint_disable(retval, "docstring-keyword-should-match-keyword-only")
430
+ return retval
411
431
 
412
432
  @staticmethod
413
433
  def get_properties_to_declare(model: ModelType) -> list[Property]:
@@ -40,13 +40,38 @@ _BUILTIN_TYPE_NAMES = frozenset(
40
40
  )
41
41
 
42
42
 
43
+ # Matches a single- or double-quoted string literal (escape-aware). Used to split an
44
+ # annotation so that bare-identifier rewriting/detection skips text inside string literals
45
+ # such as ``Literal["type"]`` values or quoted forward references like ``"SomeModel"``.
46
+ _STRING_LITERAL_RE = re.compile(r"""('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")""")
47
+
48
+
49
+ def _annotation_references_builtin(annotation: str, name: str) -> bool:
50
+ """Whether ``name`` appears as a bare (non-dotted) identifier outside any string literal."""
51
+ pattern = re.compile(rf"(?<!\.)\b{name}\b")
52
+ # re.split with a single capturing group yields code/string/code/string/... segments.
53
+ for index, segment in enumerate(_STRING_LITERAL_RE.split(annotation)):
54
+ if index % 2 == 0 and pattern.search(segment):
55
+ return True
56
+ return False
57
+
58
+
43
59
  def _qualify_shadowed_builtins(annotation: str, shadowed: frozenset[str]) -> str:
44
- """Replace bare builtin type references with builtins.X when shadowed by a field name."""
60
+ """Replace bare builtin type references with builtins.X when shadowed by a field name.
61
+
62
+ Only bare identifiers in real type-expression positions are rewritten; identifiers
63
+ inside string literals (e.g. ``Literal["type"]`` values, quoted forward references)
64
+ and already-dotted names are left untouched.
65
+ """
45
66
  if not shadowed:
46
67
  return annotation
47
- for name in shadowed:
48
- annotation = re.sub(rf"\b{name}\b", f"builtins.{name}", annotation)
49
- return annotation
68
+ segments = _STRING_LITERAL_RE.split(annotation)
69
+ for index in range(0, len(segments), 2): # even indices are code, odd are string literals
70
+ segment = segments[index]
71
+ for name in shadowed:
72
+ segment = re.sub(rf"(?<!\.)\b{name}\b", f"builtins.{name}", segment)
73
+ segments[index] = segment
74
+ return "".join(segments)
50
75
 
51
76
 
52
77
  class TypesSerializer(BaseSerializer):
@@ -225,22 +250,27 @@ class TypesSerializer(BaseSerializer):
225
250
  """Whether any property wire_name is a Python keyword or requires functional TypedDict form."""
226
251
  return any(keyword.iskeyword(p.wire_name) or not p.wire_name.isidentifier() for p in model.properties)
227
252
 
228
- @staticmethod
229
- def get_shadowed_builtins(model: ModelType) -> frozenset[str]:
253
+ def get_shadowed_builtins(self, model: ModelType) -> frozenset[str]:
230
254
  """Return the set of builtin type names shadowed by property wire_names in this model.
231
255
 
232
- Only includes a builtin if it is both used as a wire_name AND referenced
233
- in a type annotation within the same model (otherwise no shadowing occurs).
256
+ Only includes a builtin if it is both used as a wire_name AND referenced as a bare
257
+ type in an annotation within the same model (otherwise no shadowing occurs). Detection
258
+ uses the exact ``TYPES_FILE`` annotation emitted for each property so that types which
259
+ change under the types-file serialization (e.g. ``bytes``/``datetime`` -> ``str``,
260
+ ``decimal`` -> ``float``) are evaluated as they actually appear in the output.
234
261
  """
235
262
  wire_builtins = {p.wire_name for p in model.properties if p.wire_name in _BUILTIN_TYPE_NAMES}
236
263
  if not wire_builtins:
237
264
  return frozenset()
238
- # Check which of these builtins actually appear in type annotations
265
+ # Check which of these builtins actually appear in emitted type annotations
239
266
  used = set()
240
267
  for prop in model.properties:
241
- annotation = prop.type_annotation()
268
+ annotation = prop.type_annotation(
269
+ serialize_namespace=self.serialize_namespace,
270
+ serialize_namespace_type=NamespaceType.TYPES_FILE,
271
+ )
242
272
  for name in wire_builtins:
243
- if re.search(rf"\b{name}\b", annotation):
273
+ if _annotation_references_builtin(annotation, name):
244
274
  used.add(name)
245
275
  return frozenset(used)
246
276
 
@@ -161,7 +161,15 @@ def _is_readonly(p):
161
161
 
162
162
 
163
163
  class SdkJSONEncoder(JSONEncoder):
164
- """A JSON encoder that's capable of serializing datetime objects and bytes."""
164
+ """A JSON encoder that's capable of serializing datetime objects and bytes.
165
+
166
+ :param args: Additional positional arguments passed to the base ``JSONEncoder``.
167
+ :type args: typing.Any
168
+ :keyword exclude_readonly: Whether to exclude readonly properties. Defaults to False.
169
+ :paramtype exclude_readonly: bool
170
+ :keyword format: The format to use for serialization. Defaults to None.
171
+ :paramtype format: typing.Optional[str]
172
+ """
165
173
 
166
174
  def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs):
167
175
  super().__init__(*args, **kwargs)
@@ -348,6 +356,12 @@ def _deserialize_int_as_str(attr):
348
356
  return int(attr)
349
357
 
350
358
 
359
+ def _deserialize_bool_as_str(attr):
360
+ if isinstance(attr, bool):
361
+ return attr
362
+ return attr.lower() == "true"
363
+
364
+
351
365
  _DESERIALIZE_MAPPING = {
352
366
  datetime: _deserialize_datetime,
353
367
  date: _deserialize_date,
@@ -375,6 +389,8 @@ _DESERIALIZE_MAPPING_WITHFORMAT = {
375
389
  def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None):
376
390
  if annotation is int and rf and rf._format == "str":
377
391
  return _deserialize_int_as_str
392
+ if annotation is bool and rf and rf._format == "str":
393
+ return _deserialize_bool_as_str
378
394
  if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING:
379
395
  return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format])
380
396
  if rf and rf._format:
@@ -476,7 +476,11 @@ def _decode_attribute_map_key(key):
476
476
 
477
477
 
478
478
  class Serializer: # pylint: disable=too-many-public-methods
479
- """Request object model serializer."""
479
+ """Request object model serializer.
480
+
481
+ :param classes: Mapping of model names to model types, used to resolve models during serialization.
482
+ :type classes: typing.Optional[typing.Mapping[str, type]]
483
+ """
480
484
 
481
485
  basic_types = {str: "str", int: "int", bool: "bool", float: "float"}
482
486
 
@@ -399,6 +399,25 @@ class PreProcessPlugin(YamlUpdatePlugin):
399
399
  code_model: dict[str, Any],
400
400
  body_parameter: dict[str, Any],
401
401
  ):
402
+ # For a binary `bytes` body (e.g. content type application/octet-stream or a custom
403
+ # binary media type), add an IO overload alongside the `bytes` one. This keeps backward
404
+ # compatibility for services migrating from swagger, whose binary bodies were typed as IO.
405
+ is_multipart = self.is_tsp and has_multi_part_content_type(body_parameter)
406
+ is_binary_body = not has_json_content_type(body_parameter) and not is_multipart
407
+ if (
408
+ body_parameter
409
+ and body_parameter["type"]["type"] == "bytes"
410
+ and is_binary_body
411
+ and self.options["models-mode"] != "typeddict"
412
+ and not any(t for t in ["flattened", "groupedBy"] if body_parameter.get(t))
413
+ ):
414
+ body_parameter["type"] = {
415
+ "type": "combined",
416
+ "types": [body_parameter["type"], KNOWN_TYPES["binary"]],
417
+ }
418
+ code_model["types"].append(body_parameter["type"])
419
+ return
420
+
402
421
  # only add overload for special content type
403
422
  if ( # pylint: disable=too-many-boolean-expressions
404
423
  body_parameter
@@ -109,7 +109,7 @@ def parse_args(
109
109
 
110
110
 
111
111
  def get_body_type_for_description(body_parameter: dict[str, Any]) -> str:
112
- if body_parameter["type"]["type"] == "binary":
112
+ if body_parameter["type"]["type"] in ("binary", "bytes"):
113
113
  return "binary"
114
114
  if body_parameter["type"]["type"] == "string":
115
115
  return "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typespec/http-client-python",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "author": "Microsoft Corporation",
5
5
  "description": "TypeSpec emitter for Python SDKs",
6
6
  "homepage": "https://typespec.io",
@@ -54,7 +54,8 @@
54
54
  "ci:generated": "tsx ./eng/scripts/ci/run-tests.ts --generator --env=ci",
55
55
  "change:version": "pnpm chronus version --ignore-policies --only @typespec/http-client-python",
56
56
  "change:add": "pnpm chronus add",
57
- "regen-docs": "node ../../packages/tspd/cmd/tspd.js doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/clients/http-client-python/reference --skip-js"
57
+ "regen-docs": "node ../../packages/tspd/cmd/tspd.js doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/clients/http-client-python/reference --skip-js",
58
+ "diff-spector-tests": "node ../../eng/emitter-diff/src/cli.ts --emitter python"
58
59
  },
59
60
  "files": [
60
61
  "dist/**",