@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.
Files changed (58) hide show
  1. package/dist/emitter/emitter.d.ts.map +1 -1
  2. package/dist/emitter/emitter.js +6 -5
  3. package/dist/emitter/emitter.js.map +1 -1
  4. package/dist/emitter/external-process.d.ts +0 -14
  5. package/dist/emitter/external-process.d.ts.map +1 -1
  6. package/dist/emitter/external-process.js +1 -17
  7. package/dist/emitter/external-process.js.map +1 -1
  8. package/dist/emitter/http.d.ts +44 -1
  9. package/dist/emitter/http.d.ts.map +1 -1
  10. package/dist/emitter/http.js +96 -0
  11. package/dist/emitter/http.js.map +1 -1
  12. package/dist/emitter/lib.d.ts +5 -5
  13. package/dist/emitter/lib.js +2 -2
  14. package/dist/emitter/lib.js.map +1 -1
  15. package/dist/emitter/yaml-utils.d.ts +19 -0
  16. package/dist/emitter/yaml-utils.d.ts.map +1 -0
  17. package/dist/emitter/yaml-utils.js +22 -0
  18. package/dist/emitter/yaml-utils.js.map +1 -0
  19. package/emitter/src/emitter.ts +7 -5
  20. package/emitter/src/external-process.ts +1 -18
  21. package/emitter/src/http.ts +154 -0
  22. package/emitter/src/lib.ts +2 -2
  23. package/emitter/src/yaml-utils.ts +22 -0
  24. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  25. package/emitter/test/emitter.test.ts +32 -0
  26. package/emitter/test/streaming.test.ts +155 -0
  27. package/emitter/test/test-host.ts +83 -0
  28. package/emitter/test/{external-process.test.ts → yaml-utils.test.ts} +2 -2
  29. package/eng/scripts/ci/regenerate-common.ts +1 -5
  30. package/eng/scripts/ci/run_pylint.py +1 -0
  31. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  32. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  33. package/generator/build/lib/pygen/codegen/models/code_model.py +22 -8
  34. package/generator/build/lib/pygen/codegen/models/enum_type.py +14 -2
  35. package/generator/build/lib/pygen/codegen/models/operation.py +6 -1
  36. package/generator/build/lib/pygen/codegen/models/response.py +152 -8
  37. package/generator/build/lib/pygen/codegen/serializers/__init__.py +7 -0
  38. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +197 -58
  39. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +4 -0
  40. package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +27 -0
  41. package/generator/build/lib/pygen/codegen/serializers/types_serializer.py +40 -3
  42. package/generator/build/lib/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
  43. package/generator/build/lib/pygen/preprocess/__init__.py +2 -0
  44. package/generator/component-detection-pip-report.json +5 -5
  45. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  46. package/generator/pygen/codegen/models/code_model.py +22 -8
  47. package/generator/pygen/codegen/models/enum_type.py +14 -2
  48. package/generator/pygen/codegen/models/operation.py +6 -1
  49. package/generator/pygen/codegen/models/response.py +152 -8
  50. package/generator/pygen/codegen/serializers/__init__.py +7 -0
  51. package/generator/pygen/codegen/serializers/builder_serializer.py +197 -58
  52. package/generator/pygen/codegen/serializers/general_serializer.py +4 -0
  53. package/generator/pygen/codegen/serializers/import_serializer.py +27 -0
  54. package/generator/pygen/codegen/serializers/types_serializer.py +40 -3
  55. package/generator/pygen/codegen/templates/streaming_base.py.jinja2 +767 -0
  56. package/generator/pygen/preprocess/__init__.py +2 -0
  57. package/generator/pygen.egg-info/SOURCES.txt +1 -0
  58. 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
- return self._call_request_builder_helper(builder, builder.request_builder, is_paging=is_paging)
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
- # add build-in error type
1171
- # TODO: we should decide whether need to this wrapper for customized error type
1172
- status_code_error_map = {
1173
- 401: "ClientAuthenticationError",
1174
- 404: "ResourceNotFoundError",
1175
- 409: "ResourceExistsError",
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
- def handle_response(self, builder: OperationType) -> list[str]:
1259
- retval: list[str] = ["response = pipeline_response.http_response"]
1260
- retval.append("")
1261
- retval.extend(self.handle_error_response(builder))
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
- if builder.has_optional_return_type:
1264
- retval.append("deserialized = None")
1265
- if builder.any_response_has_headers:
1266
- retval.append("response_headers = {}")
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
- if builder.non_default_errors and (
1342
- self.code_model.options["models-mode"] or self.code_model.generate_typeddict_only
1343
- ):
1344
- # TODO: we should decide whether to add the build-in error map when there is a customized default error type
1345
- if self._need_specific_error_map(401, builder):
1346
- retval.append(" 401: ClientAuthenticationError,")
1347
- if self._need_specific_error_map(404, builder):
1348
- retval.append(" 404: ResourceNotFoundError,")
1349
- if self._need_specific_error_map(409, builder):
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 parent.client_namespace != model.client_namespace and not parent.discriminated_subtypes:
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