@typespec/http-client-python 0.36.0 → 0.37.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.
- package/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +6 -5
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/external-process.d.ts +0 -14
- package/dist/emitter/external-process.d.ts.map +1 -1
- package/dist/emitter/external-process.js +1 -17
- package/dist/emitter/external-process.js.map +1 -1
- package/dist/emitter/http.d.ts +44 -1
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +96 -0
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/lib.d.ts +5 -5
- package/dist/emitter/lib.js +2 -2
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/yaml-utils.d.ts +19 -0
- package/dist/emitter/yaml-utils.d.ts.map +1 -0
- package/dist/emitter/yaml-utils.js +22 -0
- package/dist/emitter/yaml-utils.js.map +1 -0
- package/emitter/src/emitter.ts +7 -5
- package/emitter/src/external-process.ts +1 -18
- package/emitter/src/http.ts +154 -0
- package/emitter/src/lib.ts +2 -2
- package/emitter/src/yaml-utils.ts +22 -0
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/emitter.test.ts +32 -0
- package/emitter/test/streaming.test.ts +155 -0
- package/emitter/test/test-host.ts +83 -0
- package/emitter/test/{external-process.test.ts → yaml-utils.test.ts} +2 -2
- package/eng/scripts/ci/regenerate-common.ts +1 -5
- package/eng/scripts/ci/run_pylint.py +1 -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/code_model.py +22 -8
- package/generator/build/lib/pygen/codegen/models/enum_type.py +14 -2
- package/generator/build/lib/pygen/codegen/models/operation.py +6 -1
- package/generator/build/lib/pygen/codegen/models/response.py +152 -8
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +7 -0
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +197 -58
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +4 -0
- package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +27 -0
- package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +40 -3
- package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/build/lib/pygen/preprocess/__init__.py +2 -0
- package/generator/component-detection-pip-report.json +5 -5
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/codegen/models/code_model.py +22 -8
- package/generator/pygen/codegen/models/enum_type.py +14 -2
- package/generator/pygen/codegen/models/operation.py +6 -1
- package/generator/pygen/codegen/models/response.py +152 -8
- package/generator/pygen/codegen/serializers/__init__.py +7 -0
- package/generator/pygen/codegen/serializers/builder_serializer.py +197 -58
- package/generator/pygen/codegen/serializers/general_serializer.py +4 -0
- package/generator/pygen/codegen/serializers/import_serializer.py +27 -0
- package/generator/pygen/codegen/serializers/types_serializer.py +40 -3
- package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
- package/generator/pygen/preprocess/__init__.py +2 -0
- package/generator/pygen.egg-info/SOURCES.txt +1 -0
- package/package.json +6 -6
|
@@ -35,12 +35,33 @@ from ..models import (
|
|
|
35
35
|
from ..models.utils import NamespaceType, escape_sphinx_field_name
|
|
36
36
|
from .parameter_serializer import ParameterSerializer, PopKwargType, check_body_optional
|
|
37
37
|
from ..models.parameter_list import ParameterType
|
|
38
|
+
from ..models.response import (
|
|
39
|
+
DEFAULT_SSE_EVENT_TYPE,
|
|
40
|
+
StreamingEvent,
|
|
41
|
+
get_streaming_event_discriminator,
|
|
42
|
+
)
|
|
38
43
|
from . import utils
|
|
39
44
|
from ...utils import xml_serializable, json_serializable
|
|
40
45
|
|
|
41
46
|
T = TypeVar("T")
|
|
42
47
|
OrderedSet = dict[T, None]
|
|
43
48
|
|
|
49
|
+
|
|
50
|
+
def _sse_event_data_expression(payload_content_type: Optional[str]) -> str:
|
|
51
|
+
"""Return the generated expression that decodes an SSE event's UTF-8 data field."""
|
|
52
|
+
media_type = payload_content_type.split(";", 1)[0].strip().lower() if payload_content_type else None
|
|
53
|
+
is_json = media_type is None or media_type == "application/json" or media_type.endswith("+json")
|
|
54
|
+
return "json.loads(_event.data)" if is_json else "_event.data"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sse_fallback_data_expression(events: list[StreamingEvent]) -> str:
|
|
58
|
+
"""Decode fallback events only when every candidate payload is JSON."""
|
|
59
|
+
if not events:
|
|
60
|
+
return "_event.data"
|
|
61
|
+
expressions = {_sse_event_data_expression(event.payload_content_type) for event in events}
|
|
62
|
+
return "json.loads(_event.data)" if expressions == {"json.loads(_event.data)"} else "_event.data"
|
|
63
|
+
|
|
64
|
+
|
|
44
65
|
BuilderType = TypeVar(
|
|
45
66
|
"BuilderType",
|
|
46
67
|
bound=Union[
|
|
@@ -1021,7 +1042,19 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1021
1042
|
return retval
|
|
1022
1043
|
|
|
1023
1044
|
def call_request_builder(self, builder: OperationType, is_paging: bool = False) -> list[str]:
|
|
1024
|
-
|
|
1045
|
+
retval = self._call_request_builder_helper(builder, builder.request_builder, is_paging=is_paging)
|
|
1046
|
+
if builder.has_structured_stream_response and any(
|
|
1047
|
+
response.streaming_kind == "sse" for response in builder.responses
|
|
1048
|
+
):
|
|
1049
|
+
retval.insert(0, '_last_event_id = kwargs.pop("last_event_id", None)')
|
|
1050
|
+
retval.extend(
|
|
1051
|
+
[
|
|
1052
|
+
"",
|
|
1053
|
+
"if _last_event_id is not None:",
|
|
1054
|
+
' _request.headers["Last-Event-ID"] = _last_event_id',
|
|
1055
|
+
]
|
|
1056
|
+
)
|
|
1057
|
+
return retval
|
|
1025
1058
|
|
|
1026
1059
|
def response_headers_and_deserialization(
|
|
1027
1060
|
self,
|
|
@@ -1167,25 +1200,12 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1167
1200
|
" )",
|
|
1168
1201
|
]
|
|
1169
1202
|
)
|
|
1170
|
-
#
|
|
1171
|
-
#
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
304: "ResourceNotModifiedError",
|
|
1177
|
-
}
|
|
1178
|
-
if status_code in status_code_error_map:
|
|
1179
|
-
retval.append(
|
|
1180
|
-
" raise {}(response=response{}{})".format(
|
|
1181
|
-
status_code_error_map[cast(int, status_code)],
|
|
1182
|
-
error_model,
|
|
1183
|
-
(", error_format=ARMErrorFormat" if self.code_model.options["azure-arm"] else ""),
|
|
1184
|
-
)
|
|
1185
|
-
)
|
|
1186
|
-
condition = "if"
|
|
1187
|
-
else:
|
|
1188
|
-
condition = "elif"
|
|
1203
|
+
# The dedicated azure-core error type for a standard status
|
|
1204
|
+
# code is raised by ``map_error`` via the error map, so here
|
|
1205
|
+
# we only deserialize the customized error body (used by the
|
|
1206
|
+
# generic ``HttpResponseError`` fallback for non-standard
|
|
1207
|
+
# status codes within the response).
|
|
1208
|
+
condition = "elif"
|
|
1189
1209
|
# ranged status code only exist in typespec and will not have multiple status codes
|
|
1190
1210
|
else:
|
|
1191
1211
|
retval.append(
|
|
@@ -1255,15 +1275,140 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1255
1275
|
)
|
|
1256
1276
|
return retval
|
|
1257
1277
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1278
|
+
# pylint: disable=too-many-statements
|
|
1279
|
+
def handle_structured_stream_response(self, builder: OperationType) -> list[str]:
|
|
1280
|
+
"""Emit the body for an operation returning a structured (JSONL / SSE) stream.
|
|
1281
|
+
|
|
1282
|
+
Produces a per-event deserialization callback and returns a ``Stream`` /
|
|
1283
|
+
``AsyncStream`` wrapping the streamed HTTP response.
|
|
1284
|
+
"""
|
|
1285
|
+
response = next(r for r in builder.responses if r.is_structured_stream)
|
|
1286
|
+
item_annotation = response.stream_item_annotation(
|
|
1287
|
+
is_operation_file=True, serialize_namespace=self.serialize_namespace
|
|
1288
|
+
)
|
|
1289
|
+
stream_class = response.stream_class_name(self.async_mode) # type: ignore[attr-defined]
|
|
1290
|
+
terminal_event = getattr(response, "terminal_event", None)
|
|
1291
|
+
terminal_event_names = getattr(response, "terminal_event_names", [])
|
|
1292
|
+
streaming_events = getattr(response, "streaming_events", [])
|
|
1293
|
+
unnamed_events = [event for event in streaming_events if event.event_type is None]
|
|
1294
|
+
unnamed_discriminator = get_streaming_event_discriminator(unnamed_events)
|
|
1295
|
+
retval: list[str] = []
|
|
1296
|
+
retval.append("def _callback(_http_response, _event):")
|
|
1297
|
+
if response.streaming_kind == "sse": # type: ignore[attr-defined]
|
|
1298
|
+
named_events = [event for event in streaming_events if event.event_type is not None]
|
|
1299
|
+
|
|
1300
|
+
def emit_deserialized_event(event: StreamingEvent, indent: str) -> None:
|
|
1301
|
+
event_item_type = event.payload_type
|
|
1302
|
+
event_annotation = event_item_type.type_annotation(
|
|
1303
|
+
is_operation_file=True,
|
|
1304
|
+
serialize_namespace=self.serialize_namespace,
|
|
1305
|
+
)
|
|
1306
|
+
if self.code_model.options["models-mode"] == "msrest":
|
|
1307
|
+
serialization_type = event_item_type.serialization_type(
|
|
1308
|
+
serialize_namespace=self.serialize_namespace
|
|
1309
|
+
)
|
|
1310
|
+
retval.append(f"{indent}deserialized = self._deserialize(")
|
|
1311
|
+
retval.append(f"{indent} '{serialization_type}',")
|
|
1312
|
+
retval.append(f"{indent} _event_json")
|
|
1313
|
+
retval.append(f"{indent})")
|
|
1314
|
+
else:
|
|
1315
|
+
retval.append(f"{indent}deserialized = _deserialize({event_annotation}, _event_json)")
|
|
1316
|
+
|
|
1317
|
+
def emit_typed_event(event: StreamingEvent, indent: str) -> None:
|
|
1318
|
+
event_json = _sse_event_data_expression(event.payload_content_type)
|
|
1319
|
+
retval.append(f"{indent}_event_json = {event_json}")
|
|
1320
|
+
emit_deserialized_event(event, indent)
|
|
1321
|
+
|
|
1322
|
+
def emit_multiple_unnamed_events(events: list[StreamingEvent], indent: str) -> None:
|
|
1323
|
+
event_json = _sse_fallback_data_expression(events)
|
|
1324
|
+
retval.append(f"{indent}_event_json = {event_json}")
|
|
1325
|
+
if unnamed_discriminator is None or event_json != "json.loads(_event.data)":
|
|
1326
|
+
retval.append(f"{indent}deserialized = _event_json")
|
|
1327
|
+
return
|
|
1328
|
+
discriminator_name, variants = unnamed_discriminator
|
|
1329
|
+
for index, (discriminator_value, event) in enumerate(variants):
|
|
1330
|
+
keyword = "if" if index == 0 else "elif"
|
|
1331
|
+
retval.append(
|
|
1332
|
+
f"{indent}{keyword} isinstance(_event_json, dict) "
|
|
1333
|
+
f"and _event_json.get({discriminator_name!r}) == {discriminator_value!r}:"
|
|
1334
|
+
)
|
|
1335
|
+
emit_deserialized_event(event, indent + " ")
|
|
1336
|
+
retval.append(f"{indent}else:")
|
|
1337
|
+
retval.append(f"{indent} deserialized = _event_json")
|
|
1338
|
+
|
|
1339
|
+
def emit_message_branch(keyword: str) -> None:
|
|
1340
|
+
retval.append(f" {keyword} _event.event == {DEFAULT_SSE_EVENT_TYPE!r}:")
|
|
1341
|
+
if len(unnamed_events) == 1:
|
|
1342
|
+
emit_typed_event(unnamed_events[0], " ")
|
|
1343
|
+
else:
|
|
1344
|
+
emit_multiple_unnamed_events(unnamed_events, " ")
|
|
1345
|
+
|
|
1346
|
+
if named_events:
|
|
1347
|
+
for index, event in enumerate(named_events):
|
|
1348
|
+
event_type = event.event_type
|
|
1349
|
+
keyword = "if" if index == 0 else "elif"
|
|
1350
|
+
retval.append(f" {keyword} _event.event == {event_type!r}:")
|
|
1351
|
+
emit_typed_event(event, " ")
|
|
1352
|
+
if unnamed_events:
|
|
1353
|
+
emit_message_branch("elif")
|
|
1354
|
+
elif unnamed_events:
|
|
1355
|
+
emit_message_branch("if")
|
|
1356
|
+
|
|
1357
|
+
if named_events or unnamed_events:
|
|
1358
|
+
retval.append(" else:")
|
|
1359
|
+
retval.append(' raise ValueError(f"Unknown SSE event type: {_event.event!r}")')
|
|
1360
|
+
else:
|
|
1361
|
+
retval.append(' raise ValueError(f"Unknown SSE event type: {_event.event!r}")')
|
|
1362
|
+
else:
|
|
1363
|
+
retval.append(" _event_json = _event.json()")
|
|
1364
|
+
if self.code_model.options["models-mode"] == "msrest":
|
|
1365
|
+
serialization_type = response.stream_item_type.serialization_type(
|
|
1366
|
+
serialize_namespace=self.serialize_namespace
|
|
1367
|
+
)
|
|
1368
|
+
retval.append(" deserialized = self._deserialize(")
|
|
1369
|
+
retval.append(f" '{serialization_type}',")
|
|
1370
|
+
retval.append(" _event_json")
|
|
1371
|
+
retval.append(" )")
|
|
1372
|
+
else:
|
|
1373
|
+
retval.append(f" deserialized = _deserialize({item_annotation}, _event_json)")
|
|
1374
|
+
retval.append(" return deserialized")
|
|
1262
1375
|
retval.append("")
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1376
|
+
stream_kwargs = ["response=response", "deserialization_callback=_callback"]
|
|
1377
|
+
if terminal_event is not None:
|
|
1378
|
+
stream_kwargs.append(f"terminal_event={terminal_event!r}")
|
|
1379
|
+
if terminal_event_names:
|
|
1380
|
+
stream_kwargs.append(f"terminal_event_names={terminal_event_names!r}")
|
|
1381
|
+
unnamed_terminal_values = (
|
|
1382
|
+
[discriminator_value for discriminator_value, event in unnamed_discriminator[1] if event.is_terminal]
|
|
1383
|
+
if unnamed_discriminator is not None
|
|
1384
|
+
else []
|
|
1385
|
+
)
|
|
1386
|
+
if unnamed_terminal_values:
|
|
1387
|
+
discriminator_name = unnamed_discriminator[0] # type: ignore[index]
|
|
1388
|
+
retval.append("")
|
|
1389
|
+
retval.append("def _is_terminal_event(_event):")
|
|
1390
|
+
retval.append(f" if _event.event != {DEFAULT_SSE_EVENT_TYPE!r}:")
|
|
1391
|
+
retval.append(" return False")
|
|
1392
|
+
retval.append(" try:")
|
|
1393
|
+
retval.append(" _event_json = json.loads(_event.data)")
|
|
1394
|
+
retval.append(" except (TypeError, ValueError):")
|
|
1395
|
+
retval.append(" return False")
|
|
1396
|
+
retval.append(
|
|
1397
|
+
f" return isinstance(_event_json, dict) "
|
|
1398
|
+
f"and _event_json.get({discriminator_name!r}) in {unnamed_terminal_values!r}"
|
|
1399
|
+
)
|
|
1400
|
+
stream_kwargs.append("terminal_event_predicate=_is_terminal_event")
|
|
1401
|
+
retval.append(
|
|
1402
|
+
f"deserialized: {stream_class}[{item_annotation}] = "
|
|
1403
|
+
f"{stream_class}({', '.join(stream_kwargs)}) # type: ignore"
|
|
1404
|
+
)
|
|
1405
|
+
retval.append("if cls:")
|
|
1406
|
+
retval.append(" return cls(pipeline_response, deserialized, {}) # type: ignore")
|
|
1407
|
+
retval.append("return deserialized")
|
|
1408
|
+
return retval
|
|
1409
|
+
|
|
1410
|
+
def _handle_response_body(self, builder: OperationType) -> list[str]:
|
|
1411
|
+
retval: list[str] = []
|
|
1267
1412
|
if builder.has_response_body or builder.any_response_has_headers: # pylint: disable=too-many-nested-blocks
|
|
1268
1413
|
if len(builder.responses) > 1:
|
|
1269
1414
|
status_codes, res_headers, res_deserialization = [], [], []
|
|
@@ -1298,6 +1443,21 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1298
1443
|
else:
|
|
1299
1444
|
retval.extend(self.response_headers_and_deserialization(builder, builder.responses[0]))
|
|
1300
1445
|
retval.append("")
|
|
1446
|
+
return retval
|
|
1447
|
+
|
|
1448
|
+
def handle_response(self, builder: OperationType) -> list[str]:
|
|
1449
|
+
retval: list[str] = ["response = pipeline_response.http_response"]
|
|
1450
|
+
retval.append("")
|
|
1451
|
+
retval.extend(self.handle_error_response(builder))
|
|
1452
|
+
retval.append("")
|
|
1453
|
+
if builder.has_structured_stream_response:
|
|
1454
|
+
retval.extend(self.handle_structured_stream_response(builder))
|
|
1455
|
+
return retval
|
|
1456
|
+
if builder.has_optional_return_type:
|
|
1457
|
+
retval.append("deserialized = None")
|
|
1458
|
+
if builder.any_response_has_headers:
|
|
1459
|
+
retval.append("response_headers = {}")
|
|
1460
|
+
retval.extend(self._handle_response_body(builder))
|
|
1301
1461
|
if (
|
|
1302
1462
|
builder.has_optional_return_type
|
|
1303
1463
|
or self.code_model.options["models-mode"]
|
|
@@ -1323,38 +1483,17 @@ class _OperationSerializer(_BuilderBaseSerializer[OperationType]):
|
|
|
1323
1483
|
retval.append("return 200 <= response.status_code <= 299")
|
|
1324
1484
|
return retval
|
|
1325
1485
|
|
|
1326
|
-
def _need_specific_error_map(self, code: int, builder: OperationType) -> bool:
|
|
1327
|
-
for non_default_error in builder.non_default_errors:
|
|
1328
|
-
# single status code
|
|
1329
|
-
if code in non_default_error.status_codes:
|
|
1330
|
-
return False
|
|
1331
|
-
# ranged status code
|
|
1332
|
-
if (
|
|
1333
|
-
isinstance(non_default_error.status_codes[0], list)
|
|
1334
|
-
and non_default_error.status_codes[0][0] <= code <= non_default_error.status_codes[0][1]
|
|
1335
|
-
):
|
|
1336
|
-
return False
|
|
1337
|
-
return True
|
|
1338
|
-
|
|
1339
1486
|
def error_map(self, builder: OperationType) -> list[str]:
|
|
1340
1487
|
retval = ["error_map: MutableMapping = {"]
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
)
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
retval.append(" 409: ResourceExistsError,")
|
|
1351
|
-
if self._need_specific_error_map(304, builder):
|
|
1352
|
-
retval.append(" 304: ResourceNotModifiedError,")
|
|
1353
|
-
else:
|
|
1354
|
-
retval.append(
|
|
1355
|
-
" 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, "
|
|
1356
|
-
"304: ResourceNotModifiedError"
|
|
1357
|
-
)
|
|
1488
|
+
# Always map the standard status codes to their dedicated azure-core error
|
|
1489
|
+
# type so ``map_error`` raises the correct semantic error, even when a
|
|
1490
|
+
# customized error model (single, ranged, or default) covers them. The
|
|
1491
|
+
# customized error body is still deserialized in ``handle_error_response``
|
|
1492
|
+
# for the generic ``HttpResponseError`` fallback.
|
|
1493
|
+
retval.append(
|
|
1494
|
+
" 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, "
|
|
1495
|
+
"304: ResourceNotModifiedError"
|
|
1496
|
+
)
|
|
1358
1497
|
retval.append("}")
|
|
1359
1498
|
if builder.has_etag:
|
|
1360
1499
|
retval.extend(
|
|
@@ -318,6 +318,10 @@ class GeneralSerializer(BaseSerializer):
|
|
|
318
318
|
template = self.env.get_template("model_base.py.jinja2")
|
|
319
319
|
return template.render(code_model=self.code_model, file_import=FileImport(self.code_model))
|
|
320
320
|
|
|
321
|
+
def serialize_streaming_base_file(self) -> str:
|
|
322
|
+
template = self.env.get_template("streaming_base.py.jinja2")
|
|
323
|
+
return template.render(code_model=self.code_model, file_import=FileImport(self.code_model))
|
|
324
|
+
|
|
321
325
|
def serialize_validation_file(self) -> str:
|
|
322
326
|
template = self.env.get_template("validation.py.jinja2")
|
|
323
327
|
return template.render(
|
|
@@ -84,6 +84,32 @@ class FileImportSerializer:
|
|
|
84
84
|
if any(self.file_import.get_imports_from_section(TypingSection.TYPING)):
|
|
85
85
|
self.file_import.add_submodule_import("typing", "TYPE_CHECKING", ImportType.STDLIB)
|
|
86
86
|
|
|
87
|
+
def _dedupe_typing_imports(self):
|
|
88
|
+
"""Drop TYPE_CHECKING imports whose bound name is already imported at runtime.
|
|
89
|
+
|
|
90
|
+
A name imported in the regular (runtime) section is also available during type checking, so a
|
|
91
|
+
duplicate import under ``if TYPE_CHECKING:`` is redundant and triggers mypy's ``no-redef``
|
|
92
|
+
error. This can happen when the same symbol is imported from two different modules — e.g. a
|
|
93
|
+
cross-namespace enum imported at runtime from its ``_enums`` submodule and again for its
|
|
94
|
+
annotation from the public ``models`` package. The runtime import is sufficient.
|
|
95
|
+
"""
|
|
96
|
+
regular_bound_names = {
|
|
97
|
+
(i.alias or i.submodule_name)
|
|
98
|
+
for i in self.file_import.get_imports_from_section(TypingSection.REGULAR)
|
|
99
|
+
if i.submodule_name
|
|
100
|
+
}
|
|
101
|
+
if not regular_bound_names:
|
|
102
|
+
return
|
|
103
|
+
self.file_import.imports = [
|
|
104
|
+
i
|
|
105
|
+
for i in self.file_import.imports
|
|
106
|
+
if not (
|
|
107
|
+
i.typing_section == TypingSection.TYPING
|
|
108
|
+
and i.submodule_name
|
|
109
|
+
and (i.alias or i.submodule_name) in regular_bound_names
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
|
|
87
113
|
def _add_sys_import_if_needed(self):
|
|
88
114
|
all_imports = list(self.file_import.get_imports_from_section(TypingSection.REGULAR)) + list(
|
|
89
115
|
self.file_import.get_imports_from_section(TypingSection.TYPING)
|
|
@@ -106,6 +132,7 @@ class FileImportSerializer:
|
|
|
106
132
|
return "\n".join(declarations)
|
|
107
133
|
|
|
108
134
|
def __str__(self) -> str:
|
|
135
|
+
self._dedupe_typing_imports()
|
|
109
136
|
self._add_type_checking_import()
|
|
110
137
|
self._add_sys_import_if_needed()
|
|
111
138
|
regular_imports = ""
|
|
@@ -243,6 +243,39 @@ class TypesSerializer(BaseSerializer):
|
|
|
243
243
|
"""Whether any property wire_name is a Python keyword or requires functional TypedDict form."""
|
|
244
244
|
return any(keyword.iskeyword(p.wire_name) or not p.wire_name.isidentifier() for p in model.properties)
|
|
245
245
|
|
|
246
|
+
@staticmethod
|
|
247
|
+
def needs_flat_typeddict(model: ModelType) -> bool:
|
|
248
|
+
"""Whether a TypedDict must be emitted in flat (non-inheriting) form.
|
|
249
|
+
|
|
250
|
+
PEP 589 forbids changing an inherited TypedDict field's requiredness in a subclass. When a
|
|
251
|
+
child redeclares an inherited field with a different requiredness (e.g. the parent renders it
|
|
252
|
+
as optional via ``total=False`` while the child needs ``Required[...]``), subclassing would
|
|
253
|
+
emit an illegal ``Overwriting TypedDict field ... while extending`` construct. Such models are
|
|
254
|
+
instead rendered as a flat TypedDict that lists every field (inherited + own) directly.
|
|
255
|
+
|
|
256
|
+
Models with keyword wire_names already flatten all fields via the functional form, so they
|
|
257
|
+
never hit this path.
|
|
258
|
+
"""
|
|
259
|
+
if TypesSerializer.has_keyword_wire_names(model):
|
|
260
|
+
return False
|
|
261
|
+
non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
|
|
262
|
+
if not non_discriminated_parents:
|
|
263
|
+
return False
|
|
264
|
+
for parent in non_discriminated_parents:
|
|
265
|
+
for parent_prop in parent.properties:
|
|
266
|
+
child_prop = next(
|
|
267
|
+
(p for p in model.properties if p.client_name == parent_prop.client_name),
|
|
268
|
+
None,
|
|
269
|
+
)
|
|
270
|
+
if child_prop is None or child_prop is parent_prop:
|
|
271
|
+
# Not overridden by the child (same object is reused when inherited unchanged).
|
|
272
|
+
continue
|
|
273
|
+
parent_required = not (parent_prop.optional or parent_prop.client_default_value is not None)
|
|
274
|
+
child_required = not (child_prop.optional or child_prop.client_default_value is not None)
|
|
275
|
+
if parent_required != child_required:
|
|
276
|
+
return True
|
|
277
|
+
return False
|
|
278
|
+
|
|
246
279
|
def get_shadowed_builtins(self, model: ModelType) -> frozenset[str]:
|
|
247
280
|
"""Return the set of builtin type names shadowed by property wire_names in this model.
|
|
248
281
|
|
|
@@ -303,7 +336,11 @@ class TypesSerializer(BaseSerializer):
|
|
|
303
336
|
if self.get_shadowed_builtins(model):
|
|
304
337
|
needs_builtins = True
|
|
305
338
|
for parent in model.parents:
|
|
306
|
-
if
|
|
339
|
+
if (
|
|
340
|
+
parent.client_namespace != model.client_namespace
|
|
341
|
+
and not parent.discriminated_subtypes
|
|
342
|
+
and not self.needs_flat_typeddict(model)
|
|
343
|
+
):
|
|
307
344
|
# Import parent class from sibling namespace's types module
|
|
308
345
|
file_import.add_submodule_import(
|
|
309
346
|
self.code_model.get_relative_import_path(
|
|
@@ -329,7 +366,7 @@ class TypesSerializer(BaseSerializer):
|
|
|
329
366
|
if self.has_keyword_wire_names(model):
|
|
330
367
|
return "" # functional form is rendered separately
|
|
331
368
|
non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
|
|
332
|
-
if non_discriminated_parents:
|
|
369
|
+
if non_discriminated_parents and not self.needs_flat_typeddict(model):
|
|
333
370
|
basename = ", ".join([m.name for m in non_discriminated_parents])
|
|
334
371
|
return f"class {model.name}({basename}):{model.pylint_disable()}"
|
|
335
372
|
return f"class {model.name}(TypedDict, total=False):{model.pylint_disable()}"
|
|
@@ -362,7 +399,7 @@ class TypesSerializer(BaseSerializer):
|
|
|
362
399
|
if TypesSerializer.has_keyword_wire_names(model):
|
|
363
400
|
return [] # functional form handles all properties
|
|
364
401
|
non_discriminated_parents = [p for p in model.parents if not p.discriminated_subtypes]
|
|
365
|
-
if non_discriminated_parents:
|
|
402
|
+
if non_discriminated_parents and not TypesSerializer.needs_flat_typeddict(model):
|
|
366
403
|
parent_properties = [p for bm in non_discriminated_parents for p in bm.properties]
|
|
367
404
|
return [
|
|
368
405
|
p
|