@typespec/http-client-python 0.32.0 → 0.34.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/emitter.js +10 -2
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/lib.d.ts +6 -0
- package/dist/emitter/lib.d.ts.map +1 -1
- package/dist/emitter/lib.js +29 -0
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +2 -0
- package/dist/emitter/types.js.map +1 -1
- package/emitter/src/emitter.ts +9 -1
- package/emitter/src/lib.ts +36 -0
- package/emitter/src/types.ts +2 -0
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/eng/scripts/ci/regenerate-common.ts +40 -15
- 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/__init__.py +4 -3
- package/generator/build/lib/pygen/codegen/models/__init__.py +4 -2
- package/generator/build/lib/pygen/codegen/models/code_model.py +27 -5
- package/generator/build/lib/pygen/codegen/models/combined_type.py +3 -3
- package/generator/build/lib/pygen/codegen/models/enum_type.py +75 -25
- package/generator/build/lib/pygen/codegen/models/list_type.py +10 -4
- package/generator/build/lib/pygen/codegen/models/model_type.py +145 -4
- package/generator/build/lib/pygen/codegen/models/operation.py +1 -0
- package/generator/build/lib/pygen/codegen/models/paging_operation.py +5 -1
- package/generator/build/lib/pygen/codegen/models/parameter.py +2 -2
- package/generator/build/lib/pygen/codegen/models/primitive_types.py +30 -6
- package/generator/build/lib/pygen/codegen/models/property.py +21 -8
- package/generator/build/lib/pygen/codegen/models/response.py +16 -3
- package/generator/build/lib/pygen/codegen/models/utils.py +12 -10
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +46 -11
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +48 -15
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +32 -2
- package/generator/build/lib/pygen/codegen/serializers/model_init_serializer.py +1 -1
- package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +127 -3
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +263 -12
- package/generator/build/lib/pygen/codegen/serializers/unions_serializer.py +44 -0
- package/generator/build/lib/pygen/codegen/templates/model_container.py.jinja2 +1 -1
- package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +30 -2
- package/generator/build/lib/pygen/codegen/templates/types.py.jinja2 +51 -2
- package/generator/build/lib/pygen/codegen/templates/unions.py.jinja2 +9 -0
- package/generator/build/lib/pygen/preprocess/__init__.py +104 -13
- package/generator/build/lib/pygen/utils.py +16 -0
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/__init__.py +4 -3
- package/generator/pygen/codegen/models/__init__.py +4 -2
- package/generator/pygen/codegen/models/code_model.py +27 -5
- package/generator/pygen/codegen/models/combined_type.py +3 -3
- package/generator/pygen/codegen/models/enum_type.py +75 -25
- package/generator/pygen/codegen/models/list_type.py +10 -4
- package/generator/pygen/codegen/models/model_type.py +145 -4
- package/generator/pygen/codegen/models/operation.py +1 -0
- package/generator/pygen/codegen/models/paging_operation.py +5 -1
- package/generator/pygen/codegen/models/parameter.py +2 -2
- package/generator/pygen/codegen/models/primitive_types.py +30 -6
- package/generator/pygen/codegen/models/property.py +21 -8
- package/generator/pygen/codegen/models/response.py +16 -3
- package/generator/pygen/codegen/models/utils.py +12 -10
- package/generator/pygen/codegen/serializers/__init__.py +46 -11
- package/generator/pygen/codegen/serializers/builder_serializer.py +48 -15
- package/generator/pygen/codegen/serializers/general_serializer.py +32 -2
- package/generator/pygen/codegen/serializers/model_init_serializer.py +1 -1
- package/generator/pygen/codegen/serializers/model_serializer.py +127 -3
- package/generator/pygen/codegen/serializers/types_serializer.py +263 -12
- package/generator/pygen/codegen/serializers/unions_serializer.py +44 -0
- package/generator/pygen/codegen/templates/model_container.py.jinja2 +1 -1
- package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +30 -2
- package/generator/pygen/codegen/templates/types.py.jinja2 +51 -2
- package/generator/pygen/codegen/templates/unions.py.jinja2 +9 -0
- package/generator/pygen/preprocess/__init__.py +104 -13
- package/generator/pygen/utils.py +16 -0
- package/generator/pygen.egg-info/SOURCES.txt +2 -0
- package/package.json +5 -5
|
@@ -196,6 +196,16 @@ def is_json_model_type(parameters: ParameterListType) -> bool:
|
|
|
196
196
|
)
|
|
197
197
|
|
|
198
198
|
|
|
199
|
+
def _is_dpg_or_typeddict_body(body_param: BodyParameter) -> bool:
|
|
200
|
+
"""Check if a body parameter is a DPG model or a CombinedType wrapping one."""
|
|
201
|
+
body_type = body_param.type
|
|
202
|
+
if isinstance(body_type, DPGModelType):
|
|
203
|
+
return True
|
|
204
|
+
if isinstance(body_type, CombinedType):
|
|
205
|
+
return body_type.target_model_subtype((DPGModelType,)) is not None
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
199
209
|
class _BuilderBaseSerializer(Generic[BuilderType]):
|
|
200
210
|
def __init__(self, code_model: CodeModel, async_mode: bool, client_namespace: str) -> None:
|
|
201
211
|
self.code_model = code_model
|
|
@@ -712,8 +722,20 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
712
722
|
f"_{body_kwarg_name} = self._serialize.body({body_param.client_name}, "
|
|
713
723
|
f"'{serialization_type}'{is_xml_cmd}{serialization_ctxt_cmd})"
|
|
714
724
|
)
|
|
725
|
+
elif self.code_model.options["models-mode"] == "typeddict":
|
|
726
|
+
# TypedDict-only models are plain dicts — no serialization needed
|
|
727
|
+
create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
|
|
715
728
|
elif self.code_model.options["models-mode"] == "dpg":
|
|
716
|
-
if
|
|
729
|
+
# Check if this is a typeddict-only model within dpg mode — skip serialization
|
|
730
|
+
body_model_type = body_param.type
|
|
731
|
+
if isinstance(body_model_type, CombinedType):
|
|
732
|
+
body_model_type = body_model_type.target_model_subtype((DPGModelType,))
|
|
733
|
+
is_typeddict_only_body = isinstance(body_model_type, DPGModelType) and getattr(
|
|
734
|
+
body_model_type, "is_typed_dict_only", False
|
|
735
|
+
)
|
|
736
|
+
if is_typeddict_only_body:
|
|
737
|
+
create_body_call = f"_{body_kwarg_name} = {body_param.client_name}"
|
|
738
|
+
elif json_serializable(body_param.default_content_type):
|
|
717
739
|
if hasattr(body_param.type, "encode") and body_param.type.encode: # type: ignore
|
|
718
740
|
create_body_call = (
|
|
719
741
|
f"_{body_kwarg_name} = json.dumps({body_param.client_name}, "
|
|
@@ -791,9 +813,9 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
791
813
|
overload.request_builder.parameters.body_parameter.client_name for overload in builder.overloads
|
|
792
814
|
]
|
|
793
815
|
all_dpg_model_overloads = False
|
|
794
|
-
if self.code_model.options["models-mode"]
|
|
816
|
+
if self.code_model.options["models-mode"] in ("dpg", "typeddict") and builder.overloads:
|
|
795
817
|
all_dpg_model_overloads = all(
|
|
796
|
-
|
|
818
|
+
_is_dpg_or_typeddict_body(o.parameters.body_parameter) for o in builder.overloads
|
|
797
819
|
)
|
|
798
820
|
if not all_dpg_model_overloads:
|
|
799
821
|
for v in sorted(set(client_names), key=client_names.index):
|
|
@@ -1002,6 +1024,12 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1002
1024
|
elif self.code_model.options["models-mode"] == "dpg":
|
|
1003
1025
|
if builder.has_stream_response:
|
|
1004
1026
|
deserialize_code.append("deserialized = response.content")
|
|
1027
|
+
elif isinstance(response.type, ModelType) and response.type.is_typed_dict_only:
|
|
1028
|
+
# Typed-dict-only models skip deserialization — return raw JSON
|
|
1029
|
+
deserialize_code.append("if response.content:")
|
|
1030
|
+
deserialize_code.append(" deserialized = response.json()")
|
|
1031
|
+
deserialize_code.append("else:")
|
|
1032
|
+
deserialize_code.append(" deserialized = None")
|
|
1005
1033
|
else:
|
|
1006
1034
|
format_filed = (
|
|
1007
1035
|
f', format="{response.type.encode}"'
|
|
@@ -1437,18 +1465,23 @@ class _PagingOperationSerializer(_OperationSerializer[PagingOperationType]):
|
|
|
1437
1465
|
)
|
|
1438
1466
|
pylint_disable = ""
|
|
1439
1467
|
if self.code_model.options["models-mode"] == "dpg":
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1468
|
+
is_item_typed_dict_only = isinstance(builder.item_type, ModelType) and builder.item_type.is_typed_dict_only
|
|
1469
|
+
if is_item_typed_dict_only:
|
|
1470
|
+
# Typed-dict-only models skip deserialization — return raw JSON items
|
|
1471
|
+
list_of_elem_deserialized = [f"deserialized{access}"]
|
|
1472
|
+
else:
|
|
1473
|
+
item_type = builder.item_type.type_annotation(
|
|
1474
|
+
is_operation_file=True, serialize_namespace=self.serialize_namespace
|
|
1475
|
+
)
|
|
1476
|
+
pylint_disable = (
|
|
1477
|
+
" # pylint: disable=protected-access" if getattr(builder.item_type, "internal", False) else ""
|
|
1478
|
+
)
|
|
1479
|
+
list_of_elem_deserialized = [
|
|
1480
|
+
"_deserialize(",
|
|
1481
|
+
f"{item_type},{pylint_disable}",
|
|
1482
|
+
f"deserialized{access},",
|
|
1483
|
+
")",
|
|
1484
|
+
]
|
|
1452
1485
|
else:
|
|
1453
1486
|
list_of_elem_deserialized = [f"deserialized{access}"]
|
|
1454
1487
|
list_of_elem_deserialized_str = "\n ".join(list_of_elem_deserialized)
|
|
@@ -66,7 +66,26 @@ class GeneralSerializer(BaseSerializer):
|
|
|
66
66
|
if dep_version > default_version:
|
|
67
67
|
version_map[dep_name] = str(dep_version)
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
# Project-level pyproject.toml fields that may be preserved across regeneration
|
|
70
|
+
# via the "keep-pyproject-fields" option.
|
|
71
|
+
KEEPABLE_PROJECT_FIELDS = ("authors", "description", "classifiers", "urls")
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _parse_keep_pyproject_fields(value: Any) -> tuple[str, ...]:
|
|
75
|
+
# The "keep-pyproject-fields" option may be a comma-separated string (e.g.
|
|
76
|
+
# "authors,description") or an already-parsed sequence of field names.
|
|
77
|
+
if not value:
|
|
78
|
+
return ()
|
|
79
|
+
if isinstance(value, str):
|
|
80
|
+
return tuple(field.strip() for field in value.split(",") if field.strip())
|
|
81
|
+
return tuple(value)
|
|
82
|
+
|
|
83
|
+
def external_lib_version_map(
|
|
84
|
+
self,
|
|
85
|
+
file_content: str,
|
|
86
|
+
additional_version_map: dict[str, str],
|
|
87
|
+
keep_pyproject_fields: tuple[str, ...] = (),
|
|
88
|
+
) -> dict:
|
|
70
89
|
# Load the pyproject.toml file if it exists and extract fields to keep.
|
|
71
90
|
result: dict = {"KEEP_FIELDS": {}}
|
|
72
91
|
try:
|
|
@@ -85,6 +104,14 @@ class GeneralSerializer(BaseSerializer):
|
|
|
85
104
|
|
|
86
105
|
# Process dependencies
|
|
87
106
|
if "project" in loaded_pyproject_toml:
|
|
107
|
+
project = loaded_pyproject_toml["project"]
|
|
108
|
+
|
|
109
|
+
# Keep manually customized project fields the emitter would otherwise overwrite.
|
|
110
|
+
# Only the fields explicitly listed in the "keep-pyproject-fields" option are preserved.
|
|
111
|
+
for field in keep_pyproject_fields:
|
|
112
|
+
if field in self.KEEPABLE_PROJECT_FIELDS and field in project:
|
|
113
|
+
result["KEEP_FIELDS"][f"project.{field}"] = project[field]
|
|
114
|
+
|
|
88
115
|
# Handle main dependencies
|
|
89
116
|
if "dependencies" in loaded_pyproject_toml["project"]:
|
|
90
117
|
kept_deps = []
|
|
@@ -127,7 +154,10 @@ class GeneralSerializer(BaseSerializer):
|
|
|
127
154
|
|
|
128
155
|
# Add fields to keep from an existing pyproject.toml
|
|
129
156
|
if template_name == "pyproject.toml.jinja2":
|
|
130
|
-
|
|
157
|
+
keep_pyproject_fields = self._parse_keep_pyproject_fields(
|
|
158
|
+
self.code_model.options.get("keep-pyproject-fields")
|
|
159
|
+
)
|
|
160
|
+
params = self.external_lib_version_map(file_content, additional_version_map, keep_pyproject_fields)
|
|
131
161
|
else:
|
|
132
162
|
params = {}
|
|
133
163
|
|
|
@@ -32,7 +32,7 @@ class ModelInitSerializer:
|
|
|
32
32
|
", ".join(model_enum_name_intersection)
|
|
33
33
|
)
|
|
34
34
|
)
|
|
35
|
-
has_models = self.models
|
|
35
|
+
has_models = self.code_model.has_non_json_models(self.models)
|
|
36
36
|
has_enums = self.enums
|
|
37
37
|
template = self.env.get_template("model_init.py.jinja2")
|
|
38
38
|
return template.render(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# --------------------------------------------------------------------------
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Any, Optional
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
|
|
9
9
|
from ..models import ModelType, Property, ConstantType, EnumValue, EnumType
|
|
@@ -73,12 +73,19 @@ def _get_xml_deserializer_enum_type(prop: Property) -> Optional[EnumType]:
|
|
|
73
73
|
return prop_type if isinstance(prop_type, EnumType) else None
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def _documentation_string(
|
|
76
|
+
def _documentation_string(
|
|
77
|
+
prop: Property, description_keyword: str, docstring_type_keyword: str, **kwargs: Any
|
|
78
|
+
) -> list[str]:
|
|
77
79
|
retval: list[str] = []
|
|
78
80
|
sphinx_prefix = f":{description_keyword} {prop.client_name}:"
|
|
79
81
|
description = prop.description(is_operation_file=False).replace("\\", "\\\\")
|
|
80
82
|
retval.append(f"{sphinx_prefix} {description}" if description else sphinx_prefix)
|
|
81
|
-
|
|
83
|
+
# In the types file, use type_annotation (the serialized form) for docstrings
|
|
84
|
+
if kwargs.get("serialize_namespace_type") == NamespaceType.TYPES_FILE:
|
|
85
|
+
doc_type = prop.type.type_annotation(**kwargs)
|
|
86
|
+
else:
|
|
87
|
+
doc_type = prop.type.docstring_type(**kwargs)
|
|
88
|
+
retval.append(f":{docstring_type_keyword} {prop.client_name}: {doc_type}")
|
|
82
89
|
return retval
|
|
83
90
|
|
|
84
91
|
|
|
@@ -494,3 +501,120 @@ class DpgModelSerializer(_ModelSerializer):
|
|
|
494
501
|
if final_result:
|
|
495
502
|
return "# pylint: disable=" + ", ".join(final_result)
|
|
496
503
|
return ""
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class TypedDictModelSerializer(_ModelSerializer):
|
|
507
|
+
def _is_parent_discriminated_base(self, model: ModelType) -> bool:
|
|
508
|
+
"""Check if any parent of this model is a discriminated base (has discriminated_subtypes)."""
|
|
509
|
+
return any(p.discriminated_subtypes for p in model.parents)
|
|
510
|
+
|
|
511
|
+
def _reorder_models(self, models: list[ModelType]) -> list[ModelType]:
|
|
512
|
+
"""Reorder so discriminated base Union aliases come after all their subtypes."""
|
|
513
|
+
bases = [m for m in models if m.discriminated_subtypes]
|
|
514
|
+
non_bases = [m for m in models if not m.discriminated_subtypes]
|
|
515
|
+
return non_bases + bases
|
|
516
|
+
|
|
517
|
+
def serialize(self) -> str:
|
|
518
|
+
template = self.env.get_template("model_container.py.jinja2")
|
|
519
|
+
return template.render(
|
|
520
|
+
code_model=self.code_model,
|
|
521
|
+
imports=FileImportSerializer(self.imports()),
|
|
522
|
+
str=str,
|
|
523
|
+
serializer=self,
|
|
524
|
+
models=self._reorder_models(self.models),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def imports(self) -> FileImport:
|
|
528
|
+
file_import = FileImport(self.code_model)
|
|
529
|
+
has_required = False
|
|
530
|
+
has_discriminated_union = False
|
|
531
|
+
for model in self.models:
|
|
532
|
+
if model.base == "json":
|
|
533
|
+
continue
|
|
534
|
+
if model.discriminated_subtypes:
|
|
535
|
+
has_discriminated_union = True
|
|
536
|
+
file_import.merge(
|
|
537
|
+
model.imports(
|
|
538
|
+
is_operation_file=False,
|
|
539
|
+
serialize_namespace=self.serialize_namespace,
|
|
540
|
+
serialize_namespace_type=NamespaceType.MODEL,
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
for prop in model.properties:
|
|
544
|
+
file_import.merge(
|
|
545
|
+
prop.imports(
|
|
546
|
+
serialize_namespace=self.serialize_namespace,
|
|
547
|
+
serialize_namespace_type=NamespaceType.MODEL,
|
|
548
|
+
called_by_property=True,
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
if not (prop.optional or prop.client_default_value is not None):
|
|
552
|
+
has_required = True
|
|
553
|
+
for parent in model.parents:
|
|
554
|
+
if parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
|
|
555
|
+
file_import.add_submodule_import(
|
|
556
|
+
self.code_model.get_relative_import_path(
|
|
557
|
+
self.serialize_namespace,
|
|
558
|
+
self.code_model.get_imported_namespace_for_model(parent.client_namespace),
|
|
559
|
+
),
|
|
560
|
+
parent.name,
|
|
561
|
+
ImportType.LOCAL,
|
|
562
|
+
)
|
|
563
|
+
file_import.add_submodule_import("typing_extensions", "TypedDict", ImportType.STDLIB)
|
|
564
|
+
if has_required:
|
|
565
|
+
file_import.add_submodule_import("typing_extensions", "Required", ImportType.STDLIB)
|
|
566
|
+
if has_discriminated_union:
|
|
567
|
+
file_import.add_submodule_import("typing", "Union", ImportType.STDLIB)
|
|
568
|
+
return file_import
|
|
569
|
+
|
|
570
|
+
def declare_model(self, model: ModelType) -> str:
|
|
571
|
+
# If the model's parent is a discriminated base, don't inherit from it
|
|
572
|
+
non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
|
|
573
|
+
if non_discriminated_parents:
|
|
574
|
+
basename = ", ".join([m.name for m in non_discriminated_parents])
|
|
575
|
+
return f"class {model.name}({basename}):{model.pylint_disable()}"
|
|
576
|
+
return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
|
|
577
|
+
|
|
578
|
+
@staticmethod
|
|
579
|
+
def get_properties_to_declare(model: ModelType) -> list[Property]:
|
|
580
|
+
# Only exclude inherited properties from non-discriminated parents
|
|
581
|
+
non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
|
|
582
|
+
if non_discriminated_parents:
|
|
583
|
+
parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
|
|
584
|
+
properties_to_declare = [
|
|
585
|
+
p
|
|
586
|
+
for p in model.properties
|
|
587
|
+
if not any(
|
|
588
|
+
p.client_name == pp.client_name
|
|
589
|
+
and p.type_annotation() == pp.type_annotation()
|
|
590
|
+
and not p.is_base_discriminator
|
|
591
|
+
for pp in parent_properties
|
|
592
|
+
)
|
|
593
|
+
]
|
|
594
|
+
else:
|
|
595
|
+
properties_to_declare = model.properties
|
|
596
|
+
return properties_to_declare
|
|
597
|
+
|
|
598
|
+
def declare_property(self, prop: Property) -> str:
|
|
599
|
+
type_annotation = prop.type_annotation(serialize_namespace=self.serialize_namespace)
|
|
600
|
+
is_optional = prop.optional or prop.client_default_value is not None
|
|
601
|
+
if is_optional:
|
|
602
|
+
return f"{prop.wire_name}: {type_annotation}"
|
|
603
|
+
return f"{prop.wire_name}: Required[{type_annotation}]"
|
|
604
|
+
|
|
605
|
+
def initialize_properties(self, model: ModelType) -> list[str]:
|
|
606
|
+
return []
|
|
607
|
+
|
|
608
|
+
def need_init(self, model: ModelType) -> bool:
|
|
609
|
+
return False
|
|
610
|
+
|
|
611
|
+
def discriminated_subtypes_union(self, model: ModelType) -> str:
|
|
612
|
+
subtypes = list(model.discriminated_subtypes.values())
|
|
613
|
+
subtype_names = [s.name for s in subtypes]
|
|
614
|
+
return f"{model.name} = Union[{', '.join(subtype_names)}]"
|
|
615
|
+
|
|
616
|
+
def is_discriminated_base(self, model: ModelType) -> bool:
|
|
617
|
+
return bool(model.discriminated_subtypes)
|
|
618
|
+
|
|
619
|
+
def global_pylint_disables(self) -> str:
|
|
620
|
+
return ""
|
|
@@ -3,34 +3,285 @@
|
|
|
3
3
|
# Licensed under the MIT License. See License.txt in the project root for
|
|
4
4
|
# license information.
|
|
5
5
|
# --------------------------------------------------------------------------
|
|
6
|
+
import keyword
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from ..models import ModelType, CodeModel
|
|
10
|
+
from ..models.enum_type import EnumType
|
|
6
11
|
from ..models.imports import FileImport, ImportType
|
|
7
12
|
from ..models.utils import NamespaceType
|
|
13
|
+
from ..models.property import Property
|
|
14
|
+
from .model_serializer import _documentation_string
|
|
8
15
|
from .import_serializer import FileImportSerializer
|
|
9
16
|
from .base_serializer import BaseSerializer
|
|
10
17
|
|
|
18
|
+
# Python builtin type names that can be shadowed by TypedDict field wire_names.
|
|
19
|
+
# When a field name matches one of these, all references to that builtin in type
|
|
20
|
+
# annotations within the same class are qualified as builtins.X.
|
|
21
|
+
_BUILTIN_TYPE_NAMES = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"int",
|
|
24
|
+
"str",
|
|
25
|
+
"float",
|
|
26
|
+
"bool",
|
|
27
|
+
"list",
|
|
28
|
+
"dict",
|
|
29
|
+
"tuple",
|
|
30
|
+
"set",
|
|
31
|
+
"bytes",
|
|
32
|
+
"type",
|
|
33
|
+
"object",
|
|
34
|
+
"complex",
|
|
35
|
+
"frozenset",
|
|
36
|
+
"bytearray",
|
|
37
|
+
"memoryview",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _qualify_shadowed_builtins(annotation: str, shadowed: frozenset[str]) -> str:
|
|
43
|
+
"""Replace bare builtin type references with builtins.X when shadowed by a field name."""
|
|
44
|
+
if not shadowed:
|
|
45
|
+
return annotation
|
|
46
|
+
for name in shadowed:
|
|
47
|
+
annotation = re.sub(rf"\b{name}\b", f"builtins.{name}", annotation)
|
|
48
|
+
return annotation
|
|
49
|
+
|
|
11
50
|
|
|
12
51
|
class TypesSerializer(BaseSerializer):
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
code_model: CodeModel,
|
|
55
|
+
env,
|
|
56
|
+
client_namespace: Optional[str] = None,
|
|
57
|
+
models: Optional[list[ModelType]] = None,
|
|
58
|
+
enums: Optional[list["EnumType"]] = None,
|
|
59
|
+
):
|
|
60
|
+
super().__init__(code_model=code_model, env=env, client_namespace=client_namespace)
|
|
61
|
+
self._models = models or []
|
|
62
|
+
self._enums = enums or []
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def literal_enums(self) -> list[EnumType]:
|
|
66
|
+
"""Enums to render as Literal type aliases in typeddict mode."""
|
|
67
|
+
return sorted(self._enums)
|
|
68
|
+
|
|
69
|
+
def declare_literal_enum(self, enum: EnumType) -> str:
|
|
70
|
+
"""Generate a Literal type alias for an enum, e.g. MyColor = Literal["red", "blue"]."""
|
|
71
|
+
values = [enum.get_declaration(v.value) for v in enum.values]
|
|
72
|
+
return f"{enum.name} = Literal[{', '.join(values)}]"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def typeddict_models(self) -> list[ModelType]:
|
|
76
|
+
"""Models that should be rendered as TypedDicts (excluding discriminated bases which become unions).
|
|
77
|
+
|
|
78
|
+
When both a dpg model and its typeddict copy exist (same crossLanguageDefinitionId),
|
|
79
|
+
prefer the dpg model (it already renders as a TypedDict in types.py) and skip the copy.
|
|
80
|
+
"""
|
|
81
|
+
candidates = [m for m in self._models if m.base != "json" and not m.discriminated_subtypes]
|
|
82
|
+
seen_ids: dict[str, "ModelType"] = {}
|
|
83
|
+
result: list["ModelType"] = []
|
|
84
|
+
for m in candidates:
|
|
85
|
+
clid = m.yaml_data.get("crossLanguageDefinitionId")
|
|
86
|
+
if clid and clid in seen_ids:
|
|
87
|
+
# Prefer the dpg model over the typeddict copy
|
|
88
|
+
if m.base == "dpg" and seen_ids[clid].base == "typeddict":
|
|
89
|
+
# Replace the typeddict copy with the dpg model
|
|
90
|
+
result = [r if r is not seen_ids[clid] else m for r in result]
|
|
91
|
+
seen_ids[clid] = m
|
|
92
|
+
# Otherwise skip this duplicate
|
|
93
|
+
continue
|
|
94
|
+
if clid:
|
|
95
|
+
seen_ids[clid] = m
|
|
96
|
+
result.append(m)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def discriminated_base_models(self) -> list[ModelType]:
|
|
101
|
+
"""Discriminated base models that become Union type aliases in types.py.
|
|
102
|
+
|
|
103
|
+
Topologically sorted so that nested discriminated bases (e.g. Shark)
|
|
104
|
+
are defined before their parents (e.g. Fish = Union[Salmon, Shark]).
|
|
105
|
+
"""
|
|
106
|
+
bases = [m for m in self._models if m.base != "json" and m.discriminated_subtypes]
|
|
107
|
+
base_names = {m.name for m in bases}
|
|
108
|
+
sorted_bases: list[ModelType] = []
|
|
109
|
+
visited: set[str] = set()
|
|
110
|
+
|
|
111
|
+
def visit(model: ModelType) -> None:
|
|
112
|
+
if model.name in visited:
|
|
113
|
+
return
|
|
114
|
+
visited.add(model.name)
|
|
115
|
+
for subtype in model.discriminated_subtypes.values():
|
|
116
|
+
if subtype.name in base_names:
|
|
117
|
+
visit(subtype)
|
|
118
|
+
sorted_bases.append(model)
|
|
119
|
+
|
|
120
|
+
for m in bases:
|
|
121
|
+
visit(m)
|
|
122
|
+
return sorted_bases
|
|
123
|
+
|
|
124
|
+
def discriminated_subtypes_union(self, model: ModelType) -> str:
|
|
125
|
+
"""Generate a Union alias for a discriminated base using TypedDict subtype names."""
|
|
126
|
+
subtypes = list(model.discriminated_subtypes.values())
|
|
127
|
+
subtype_names = [s.name for s in subtypes]
|
|
128
|
+
return f"{model.name} = Union[{', '.join(subtype_names)}]"
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def has_keyword_wire_names(model: ModelType) -> bool:
|
|
132
|
+
"""Whether any property wire_name is a Python keyword or requires functional TypedDict form."""
|
|
133
|
+
return any(keyword.iskeyword(p.wire_name) or not p.wire_name.isidentifier() for p in model.properties)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def get_shadowed_builtins(model: ModelType) -> frozenset[str]:
|
|
137
|
+
"""Return the set of builtin type names shadowed by property wire_names in this model.
|
|
138
|
+
|
|
139
|
+
Only includes a builtin if it is both used as a wire_name AND referenced
|
|
140
|
+
in a type annotation within the same model (otherwise no shadowing occurs).
|
|
141
|
+
"""
|
|
142
|
+
wire_builtins = {p.wire_name for p in model.properties if p.wire_name in _BUILTIN_TYPE_NAMES}
|
|
143
|
+
if not wire_builtins:
|
|
144
|
+
return frozenset()
|
|
145
|
+
# Check which of these builtins actually appear in type annotations
|
|
146
|
+
used = set()
|
|
147
|
+
for prop in model.properties:
|
|
148
|
+
annotation = prop.type_annotation()
|
|
149
|
+
for name in wire_builtins:
|
|
150
|
+
if re.search(rf"\b{name}\b", annotation):
|
|
151
|
+
used.add(name)
|
|
152
|
+
return frozenset(used)
|
|
153
|
+
|
|
13
154
|
def imports(self) -> FileImport:
|
|
14
155
|
file_import = FileImport(self.code_model)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
156
|
+
|
|
157
|
+
literal_enums = self.literal_enums
|
|
158
|
+
if literal_enums:
|
|
159
|
+
file_import.add_submodule_import("typing", "Literal", ImportType.STDLIB)
|
|
160
|
+
|
|
161
|
+
td_models = self.typeddict_models
|
|
162
|
+
if td_models or self.discriminated_base_models:
|
|
163
|
+
if td_models:
|
|
164
|
+
file_import.add_submodule_import("typing_extensions", "TypedDict", ImportType.STDLIB)
|
|
165
|
+
if self.discriminated_base_models:
|
|
166
|
+
file_import.add_submodule_import("typing", "Union", ImportType.STDLIB)
|
|
167
|
+
has_required = False
|
|
168
|
+
needs_builtins = False
|
|
169
|
+
for model in td_models:
|
|
170
|
+
file_import.merge(
|
|
171
|
+
model.imports(
|
|
172
|
+
is_operation_file=False,
|
|
173
|
+
serialize_namespace=self.serialize_namespace,
|
|
174
|
+
serialize_namespace_type=NamespaceType.TYPES_FILE,
|
|
175
|
+
)
|
|
25
176
|
)
|
|
26
|
-
|
|
177
|
+
for prop in model.properties:
|
|
178
|
+
file_import.merge(
|
|
179
|
+
prop.imports(
|
|
180
|
+
serialize_namespace=self.serialize_namespace,
|
|
181
|
+
serialize_namespace_type=NamespaceType.TYPES_FILE,
|
|
182
|
+
called_by_property=True,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
if not (prop.optional or prop.client_default_value is not None):
|
|
186
|
+
has_required = True
|
|
187
|
+
if self.get_shadowed_builtins(model):
|
|
188
|
+
needs_builtins = True
|
|
189
|
+
for parent in model.parents:
|
|
190
|
+
if parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
|
|
191
|
+
# Import parent class from sibling namespace's types module
|
|
192
|
+
file_import.add_submodule_import(
|
|
193
|
+
self.code_model.get_relative_import_path(
|
|
194
|
+
self.serialize_namespace,
|
|
195
|
+
parent.client_namespace,
|
|
196
|
+
module_name="types",
|
|
197
|
+
),
|
|
198
|
+
parent.name,
|
|
199
|
+
ImportType.LOCAL,
|
|
200
|
+
)
|
|
201
|
+
if has_required:
|
|
202
|
+
file_import.add_submodule_import("typing_extensions", "Required", ImportType.STDLIB)
|
|
203
|
+
if needs_builtins:
|
|
204
|
+
file_import.add_import("builtins", ImportType.STDLIB)
|
|
27
205
|
return file_import
|
|
28
206
|
|
|
207
|
+
def declare_model(self, model: ModelType) -> str:
|
|
208
|
+
"""Generate the class declaration or functional form for a TypedDict model.
|
|
209
|
+
|
|
210
|
+
Uses functional form when any property wire_name is a Python keyword
|
|
211
|
+
(e.g. 'and', 'class') since keywords can't be identifiers in class bodies.
|
|
212
|
+
"""
|
|
213
|
+
if self.has_keyword_wire_names(model):
|
|
214
|
+
return "" # functional form is rendered separately
|
|
215
|
+
non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
|
|
216
|
+
if non_discriminated_parents:
|
|
217
|
+
basename = ", ".join([m.name for m in non_discriminated_parents])
|
|
218
|
+
return f"class {model.name}({basename}):{model.pylint_disable()}"
|
|
219
|
+
return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
|
|
220
|
+
|
|
221
|
+
def declare_functional_model(self, model: ModelType) -> str:
|
|
222
|
+
"""Generate a functional-form TypedDict for models with keyword wire_names.
|
|
223
|
+
|
|
224
|
+
Functional form is required when any field name is a Python keyword.
|
|
225
|
+
All fields (including inherited) are included since functional form
|
|
226
|
+
can't specify a base class.
|
|
227
|
+
"""
|
|
228
|
+
shadowed = self.get_shadowed_builtins(model)
|
|
229
|
+
entries: list[str] = []
|
|
230
|
+
for prop in model.properties:
|
|
231
|
+
type_annotation = prop.type_annotation(
|
|
232
|
+
serialize_namespace=self.serialize_namespace,
|
|
233
|
+
serialize_namespace_type=NamespaceType.TYPES_FILE,
|
|
234
|
+
)
|
|
235
|
+
type_annotation = _qualify_shadowed_builtins(type_annotation, shadowed)
|
|
236
|
+
is_optional = prop.optional or prop.client_default_value is not None
|
|
237
|
+
if is_optional:
|
|
238
|
+
entries.append(f' "{prop.wire_name}": {type_annotation},')
|
|
239
|
+
else:
|
|
240
|
+
entries.append(f' "{prop.wire_name}": Required[{type_annotation}],')
|
|
241
|
+
fields = "\n".join(entries)
|
|
242
|
+
return f'{model.name} = TypedDict("{model.name}", {{\n{fields}\n}}, total=False)'
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def get_properties_to_declare(model: ModelType) -> list[Property]:
|
|
246
|
+
if TypesSerializer.has_keyword_wire_names(model):
|
|
247
|
+
return [] # functional form handles all properties
|
|
248
|
+
non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
|
|
249
|
+
if non_discriminated_parents:
|
|
250
|
+
parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
|
|
251
|
+
return [
|
|
252
|
+
p
|
|
253
|
+
for p in model.properties
|
|
254
|
+
if not any(
|
|
255
|
+
p.client_name == pp.client_name
|
|
256
|
+
and p.type_annotation() == pp.type_annotation()
|
|
257
|
+
and not p.is_base_discriminator
|
|
258
|
+
for pp in parent_properties
|
|
259
|
+
)
|
|
260
|
+
]
|
|
261
|
+
return list(model.properties)
|
|
262
|
+
|
|
263
|
+
def declare_property(self, prop: Property, shadowed_builtins: frozenset[str]) -> str:
|
|
264
|
+
type_annotation = prop.type_annotation(
|
|
265
|
+
serialize_namespace=self.serialize_namespace,
|
|
266
|
+
serialize_namespace_type=NamespaceType.TYPES_FILE,
|
|
267
|
+
)
|
|
268
|
+
type_annotation = _qualify_shadowed_builtins(type_annotation, shadowed_builtins)
|
|
269
|
+
is_optional = prop.optional or prop.client_default_value is not None
|
|
270
|
+
if is_optional:
|
|
271
|
+
return f"{prop.wire_name}: {type_annotation}"
|
|
272
|
+
return f"{prop.wire_name}: Required[{type_annotation}]"
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def variable_documentation_string(prop: Property) -> list[str]:
|
|
276
|
+
return _documentation_string(prop, "ivar", "vartype", serialize_namespace_type=NamespaceType.TYPES_FILE)
|
|
277
|
+
|
|
29
278
|
def serialize(self) -> str:
|
|
30
|
-
# Generate the models
|
|
31
279
|
template = self.env.get_template("types.py.jinja2")
|
|
32
280
|
return template.render(
|
|
33
281
|
code_model=self.code_model,
|
|
34
282
|
imports=FileImportSerializer(self.imports()),
|
|
35
283
|
serializer=self,
|
|
284
|
+
literal_enums=self.literal_enums,
|
|
285
|
+
models=self.typeddict_models,
|
|
286
|
+
discriminated_bases=self.discriminated_base_models,
|
|
36
287
|
)
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
from ..models import CodeModel
|
|
7
|
+
from ..models.imports import FileImport, ImportType
|
|
8
|
+
from ..models.utils import NamespaceType
|
|
9
|
+
from .import_serializer import FileImportSerializer
|
|
10
|
+
from .base_serializer import BaseSerializer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnionsSerializer(BaseSerializer):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
code_model: CodeModel,
|
|
17
|
+
env,
|
|
18
|
+
):
|
|
19
|
+
super().__init__(code_model=code_model, env=env)
|
|
20
|
+
|
|
21
|
+
def imports(self) -> FileImport:
|
|
22
|
+
file_import = FileImport(self.code_model)
|
|
23
|
+
if self.code_model.named_unions:
|
|
24
|
+
file_import.add_submodule_import(
|
|
25
|
+
"typing",
|
|
26
|
+
"Union",
|
|
27
|
+
ImportType.STDLIB,
|
|
28
|
+
)
|
|
29
|
+
for nu in self.code_model.named_unions:
|
|
30
|
+
file_import.merge(
|
|
31
|
+
nu.imports(
|
|
32
|
+
serialize_namespace=self.serialize_namespace,
|
|
33
|
+
serialize_namespace_type=NamespaceType.UNIONS_FILE,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
return file_import
|
|
37
|
+
|
|
38
|
+
def serialize(self) -> str:
|
|
39
|
+
template = self.env.get_template("unions.py.jinja2")
|
|
40
|
+
return template.render(
|
|
41
|
+
code_model=self.code_model,
|
|
42
|
+
imports=FileImportSerializer(self.imports()),
|
|
43
|
+
serializer=self,
|
|
44
|
+
)
|