@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.
- package/dist/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +5 -1
- package/dist/emitter/types.js.map +1 -1
- package/emitter/src/code-model.ts +1 -2
- package/emitter/src/types.ts +6 -2
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/eng/scripts/ci/config/pylintrc +5 -1
- package/eng/scripts/ci/dev_requirements.txt +3 -3
- package/eng/scripts/ci/regenerate-common.ts +3 -0
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/generator/build/lib/pygen/codegen/models/client.py +15 -0
- package/generator/build/lib/pygen/codegen/models/operation.py +10 -2
- package/generator/build/lib/pygen/codegen/models/operation_group.py +2 -0
- package/generator/build/lib/pygen/codegen/models/primitive_types.py +5 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +50 -5
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +23 -3
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +41 -11
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +17 -1
- package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/build/lib/pygen/preprocess/__init__.py +19 -0
- package/generator/build/lib/pygen/utils.py +1 -1
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/codegen/models/client.py +15 -0
- package/generator/pygen/codegen/models/operation.py +10 -2
- package/generator/pygen/codegen/models/operation_group.py +2 -0
- package/generator/pygen/codegen/models/primitive_types.py +5 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +50 -5
- package/generator/pygen/codegen/serializers/model_serializer.py +23 -3
- package/generator/pygen/codegen/serializers/types_serializer.py +41 -11
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +17 -1
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/pygen/preprocess/__init__.py +19 -0
- package/generator/pygen/utils.py +1 -1
- 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}):{
|
|
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}):{
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
package/generator/pygen/utils.py
CHANGED
|
@@ -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"]
|
|
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.
|
|
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/**",
|