From 6c141ec82f7525a00bb32f1c32bb063d5be0782a Mon Sep 17 00:00:00 2001 From: 2ynn Date: Fri, 23 Oct 2026 13:12:29 -0400 Subject: [PATCH 1/2] feat(python): add modelSerializationMethods option Add python.modelSerializationMethods (default false). When enabled, generated models gain to_dict() and to_json(), which dump with API field names and exclude unset fields by default, and expose Pydantic's exclude_defaults, exclude_none and warnings options plus use_api_names. - model serializers emit Python field names when use_api_names=False, and the helpers leave filtered fields out even with exclude_none - consts are marked as set so exclude_unset keeps them, in models and error data models - the unknown variant of an open union dumps as its raw payload in to_dict() and to_json(); use_api_names and the exclude options do not apply inside the raw payload, which has no schema - model fields named to_dict or to_json gain a trailing underscore; SDK group accessors, global and server-variable constructor arguments and method parameters keep their names - method parameter docs and docstrings use the same sanitizer as the method signature - usage snippets name global parameters like the SDK constructor does The tertiary variant covers the option alone and the quaternary variant covers it together with preserveUnknownFields. --- .changesets/1792768355-9f04a617.yaml | 10 ++ templates/perms.go | 1 + .../types/basemodel.py.stmpl | 74 ++++++++- templates/templates/pythonv2/config.ts | 9 ++ templates/templates/pythonv2/error.py.stmpl | 7 + .../pythonv2/includes/documentation.ts | 4 +- .../pythonv2/includes/sanitization.ts | 25 ++- .../templates/pythonv2/includes/templating.ts | 29 +++- templates/templates/pythonv2/model.py.stmpl | 34 ++++- .../templates/pythonv2/open-union.py.stmpl | 10 ++ .../tests/common/unknown_fields_cases.py | 1 + .../test_unknown_fields_additional.py | 99 +++++++++++- .../test_unknown_fields_additional.py | 5 + .../test_unknown_fields_additional.py | 142 ++++++++++++++++++ .../quaternary/pythonv2/.speakeasy/gen.yaml | 1 + .../review/pythonv2/.speakeasy/gen.yaml | 1 + .../tertiary/pythonv2/.speakeasy/gen.yaml | 1 + .../specs/fragments/uber/unknown-fields.yaml | 74 +++++++-- zSDKs/sdk-pythonv2/.speakeasy/gen.lock | 10 +- zSDKs/sdk-pythonv2/.speakeasy/gen.yaml | 1 + zSDKs/sdk-pythonv2/docs/sdks/tag2/README.md | 2 +- zSDKs/sdk-pythonv2/docs/sdks/tag3/README.md | 2 +- .../src/speakeasy/new_openapi/tag2.py | 4 +- .../src/speakeasy/new_openapi/tag3.py | 4 +- 24 files changed, 504 insertions(+), 46 deletions(-) create mode 100644 .changesets/1792768355-9f04a617.yaml create mode 100644 templates/templates/pythonv2/tests/tertiary/test_unknown_fields_additional.py diff --git a/.changesets/1792768355-9f04a617.yaml b/.changesets/1792768355-9f04a617.yaml new file mode 100644 index 000000000..0b44a5edb --- /dev/null +++ b/.changesets/1792768355-9f04a617.yaml @@ -0,0 +1,10 @@ +id: 1792768355-9f04a617 +features: + - core +targets: + - pythonv2 +type: feat +bump: patch +description: add modelSerializationMethods for to_dict and to_json model helpers +author: 2ynn +date: "2026-10-23" diff --git a/templates/perms.go b/templates/perms.go index 87294a135..cc1d9bb6a 100644 --- a/templates/perms.go +++ b/templates/perms.go @@ -1739,6 +1739,7 @@ var fileMode = map[string]fs.FileMode{ "templates/pythonv2/tests/tertiary/test_body_typeddict_suffix.py": 0o644, "templates/pythonv2/tests/tertiary/test_relative_imports.py": 0o644, "templates/pythonv2/tests/tertiary/test_servers.py": 0o644, + "templates/pythonv2/tests/tertiary/test_unknown_fields_additional.py": 0o644, "templates/pythonv2/testusage": 0o20000000755, "templates/pythonv2/testusage/pylintrc": 0o644, "templates/pythonv2/testusage/pyproject.toml.stmpl": 0o644, diff --git a/templates/templates/pythonv2/auxiliary/{{getSourcePath}}/types/basemodel.py.stmpl b/templates/templates/pythonv2/auxiliary/{{getSourcePath}}/types/basemodel.py.stmpl index 1589c149f..2fa8b3eec 100644 --- a/templates/templates/pythonv2/auxiliary/{{getSourcePath}}/types/basemodel.py.stmpl +++ b/templates/templates/pythonv2/auxiliary/{{getSourcePath}}/types/basemodel.py.stmpl @@ -11,6 +11,48 @@ class BaseModel(PydanticBaseModel): model_config = ConfigDict( populate_by_name=True, arbitrary_types_allowed=True, protected_namespaces=(){{if .Global.Config.PreserveUnknownFields}}, extra="allow"{{end}} ) +{{- if .Global.Config.ModelSerializationMethods}} + + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, Any]: + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + context={SERIALIZATION_METHODS_DUMP: True}, + ) + + def to_json( + self, + *, + indent: Optional[int] = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + context={SERIALIZATION_METHODS_DUMP: True}, + ) +{{- end}} class Unset(BaseModel): @@ -27,6 +69,9 @@ class Unset(BaseModel): UNSET = Unset() UNSET_SENTINEL = "~?~unset~?~sentinel~?~" +{{- if .Global.Config.ModelSerializationMethods}} +SERIALIZATION_METHODS_DUMP = "speakeasy_serialization_methods_dump" +{{- end}} T = TypeVar("T") @@ -41,22 +86,36 @@ else: _exclude_none: ContextVar[Optional[bool]] = ContextVar("exclude_none", default=None) -{{- if .Global.Config.PreserveUnknownFields}} +{{- $exposeByAlias := or .Global.Config.PreserveUnknownFields .Global.Config.ModelSerializationMethods}} +{{- if $exposeByAlias}} _by_alias: ContextVar[Optional[bool]] = ContextVar("by_alias", default=None) {{- end}} +{{- if .Global.Config.ModelSerializationMethods}} +_serialization_methods_dump: ContextVar[bool] = ContextVar( + "serialization_methods_dump", default=False +) +{{- end}} def serialize_with_info( serialize: Callable[[Any], Any], handler: Any, info: core_schema.SerializationInfo ) -> Any: token = _exclude_none.set(info.exclude_none) -{{- if .Global.Config.PreserveUnknownFields}} +{{- if $exposeByAlias}} by_alias_token = _by_alias.set(info.by_alias) +{{- end}} +{{- if .Global.Config.ModelSerializationMethods}} + serialization_methods_dump_token = _serialization_methods_dump.set( + isinstance(info.context, dict) and SERIALIZATION_METHODS_DUMP in info.context + ) {{- end}} try: return serialize(handler) finally: -{{- if .Global.Config.PreserveUnknownFields}} +{{- if .Global.Config.ModelSerializationMethods}} + _serialization_methods_dump.reset(serialization_methods_dump_token) +{{- end}} +{{- if $exposeByAlias}} _by_alias.reset(by_alias_token) {{- end}} _exclude_none.reset(token) @@ -65,13 +124,20 @@ def serialize_with_info( def current_exclude_none() -> Optional[bool]: """Return the exclude_none option of the enclosing serialize_with_info call, or None outside of one.""" return _exclude_none.get() -{{- if .Global.Config.PreserveUnknownFields}} +{{- if $exposeByAlias}} def current_by_alias() -> Optional[bool]: """Return the by_alias option of the enclosing serialize_with_info call, or None outside of one.""" return _by_alias.get() {{- end}} +{{- if .Global.Config.ModelSerializationMethods}} + + +def current_serialization_methods_dump() -> bool: + """Return whether the enclosing serialize_with_info call comes from to_dict() or to_json().""" + return _serialization_methods_dump.get() +{{- end}} def _unrecognized_serialization(cls: Type[Union[str, int]]) -> core_schema.PlainSerializerFunctionSerSchema: diff --git a/templates/templates/pythonv2/config.ts b/templates/templates/pythonv2/config.ts index e776639a4..fcc77a449 100644 --- a/templates/templates/pythonv2/config.ts +++ b/templates/templates/pythonv2/config.ts @@ -345,6 +345,15 @@ function getConfigFields( ValidationRegex: /^(true|false)$/.source, ValidationMessage: "true or false only", }, + modelSerializationMethods: { + Name: "modelSerializationMethods", + Required: false, + DefaultValue: false, + Description: + "Add to_dict and to_json convenience methods to models, using API field names and excluding unset fields by default.", + ValidationRegex: /^(true|false)$/.source, + ValidationMessage: "true or false only", + }, preserveUnknownFields: { Name: "preserveUnknownFields", Required: false, diff --git a/templates/templates/pythonv2/error.py.stmpl b/templates/templates/pythonv2/error.py.stmpl index 8bb25b386..2a6b5edf5 100644 --- a/templates/templates/pythonv2/error.py.stmpl +++ b/templates/templates/pythonv2/error.py.stmpl @@ -20,6 +20,13 @@ class {{$className}}Data(BaseModel): {{- else}} pass {{- end}} + {{- $constFieldAttributes := templateConstFieldAttributeList $type}} + {{- if and .Global.Config.ModelSerializationMethods $constFieldAttributes}} + {{- addImport "typing" "Any"}} + + def model_post_init(self, context: Any, /) -> None: + self.__pydantic_fields_set__.update([{{$constFieldAttributes}}]) + {{- end}} {{- else}} {{- if and $type.Discriminator $type.IsUnionOpen }} {{templateString "open-union.py.stmpl" $local}} diff --git a/templates/templates/pythonv2/includes/documentation.ts b/templates/templates/pythonv2/includes/documentation.ts index 09d99abe8..d48efc7bc 100644 --- a/templates/templates/pythonv2/includes/documentation.ts +++ b/templates/templates/pythonv2/includes/documentation.ts @@ -66,7 +66,7 @@ function templateMethodParametersDocsSDKs(operation: Operation): SDKDocType[] { } contents.push({ - Name: `${sanitizeFieldName(parameter.Name)}`, + Name: `${sanitizeParameterName(parameter.Name)}`, Type: templateTypeMarkdown({ fieldDef: parameter, source: GeneratedDocSource.SDKDOCS, @@ -159,7 +159,7 @@ function templateMethodParametersDocs(operation: Operation): string { } contents.push([ - `\`${sanitizeFieldName(parameter.Name)}\``, + `\`${sanitizeParameterName(parameter.Name)}\``, templateTypeMarkdown({ fieldDef: parameter, }), diff --git a/templates/templates/pythonv2/includes/sanitization.ts b/templates/templates/pythonv2/includes/sanitization.ts index 2d9bf6bfc..6cba8e67b 100644 --- a/templates/templates/pythonv2/includes/sanitization.ts +++ b/templates/templates/pythonv2/includes/sanitization.ts @@ -257,12 +257,12 @@ function sanitizeSDKAccess(sdk: SDK): string { let formattedName = ""; if (group.includes(".") && ".".repeat(group.length) !== group) { const pieces = group.split("."); - formattedName = sanitizeFieldName(pieces[0]); + formattedName = sanitizeSubSDKFieldName(pieces[0]); for (const i of pieces.slice(1)) { - formattedName += "." + sanitizeFieldName(i); + formattedName += "." + sanitizeSubSDKFieldName(i); } } else { - formattedName = sanitizeFieldName(group); + formattedName = sanitizeSubSDKFieldName(group); } if (formattedName != "") { @@ -311,6 +311,20 @@ registerTemplateFunc("sanitizeSDKFileName", sanitizeSDKFileName); // @ts-ignore function sanitizeFieldName(name: string): string { + name = sanitizeAttributeName(name); + + if ( + context.Global.Config.ModelSerializationMethods && + ["to_dict", "to_json"].includes(name) + ) { + name += "_"; + } + + return name; +} +registerTemplateFunc("sanitizeFieldName", sanitizeFieldName); + +function sanitizeAttributeName(name: string): string { name = sanitizeName(name); if (name.endsWith("_")) { name = name.substring(0, name.length - 1); @@ -326,7 +340,6 @@ function sanitizeFieldName(name: string): string { return name; } -registerTemplateFunc("sanitizeFieldName", sanitizeFieldName); function useConflictResistantModelImports(): boolean { return !!context.Global.Config.FixFlags?.[ @@ -413,9 +426,7 @@ function doesModuleNameConflictAcrossNamespaces( // @ts-ignore function sanitizeSubSDKFieldName(name: string): string { - name = sanitizeFieldName(name); - - return name; + return sanitizeAttributeName(name); } registerTemplateFunc("sanitizeSubSDKFieldName", sanitizeSubSDKFieldName); diff --git a/templates/templates/pythonv2/includes/templating.ts b/templates/templates/pythonv2/includes/templating.ts index 4ed12dd07..712bde735 100644 --- a/templates/templates/pythonv2/includes/templating.ts +++ b/templates/templates/pythonv2/includes/templating.ts @@ -922,7 +922,7 @@ function templateSDKInitFieldName(name: string, suffix: string): string { reserved.push("server_idx"); } - let fieldName = sanitizeFieldName(name); + let fieldName = sanitizeAttributeName(name); if (reserved.includes(name)) { fieldName += "_" + suffix; @@ -1096,7 +1096,12 @@ function templateUsageSDKParams(local: UsageContext): string { return; } - params.push(`${sanitizeFieldName(parameter.field.Name)}=${value},`); + params.push( + `${templateSDKInitFieldName( + parameter.field.Name, + "global", + )}=${value},`, + ); break; } } @@ -1376,7 +1381,7 @@ function getMethodParamDescriptions( const descriptions = fields.map((field) => { let description = ""; - const fieldName = sanitizeFieldName(field.Name); + const fieldName = sanitizeParameterName(field.Name); if (fieldName === "request") { description = "The request object to send."; } else if (field.Comments && field.Comments.Description) { @@ -1850,6 +1855,24 @@ function templateOptionalFieldList(typeDef: TypeDef): string { } registerTemplateFunc("templateOptionalFieldList", templateOptionalFieldList); +function templateConstFieldAttributeList(typeDef: TypeDef): string { + return typeDef.Fields.filter( + (f) => + f.Const && + !(f.Optional && f.Const.Value === null) && + !f.IsAdditionalProperties, + ) + .map((f) => { + const fieldName = sanitizeFieldName(f.Name); + return `"${useUpperCaseConsts() ? fieldName.toUpperCase() : fieldName}"`; + }) + .join(", "); +} +registerTemplateFunc( + "templateConstFieldAttributeList", + templateConstFieldAttributeList, +); + function templateNullableFieldList(typeDef: TypeDef): string { return templateFieldList(typeDef, (f) => f.Nullable); } diff --git a/templates/templates/pythonv2/model.py.stmpl b/templates/templates/pythonv2/model.py.stmpl index 56599e75c..d2e537def 100644 --- a/templates/templates/pythonv2/model.py.stmpl +++ b/templates/templates/pythonv2/model.py.stmpl @@ -8,6 +8,13 @@ {{- $hasNullableField := gt (len $nullableFields) 0 -}} {{- $hasAdditionalProperties := hasAdditionalPropertiesField $type -}} {{- $preserveUnknownFields := .Global.Config.PreserveUnknownFields -}} +{{- $modelSerializationMethods := .Global.Config.ModelSerializationMethods -}} +{{- $outputKey := "k" -}} +{{- $skipFilteredFieldsWhen := "exclude_none is False" -}} +{{- if $modelSerializationMethods}} +{{- $outputKey = "n if use_field_names else k"}} +{{- $skipFilteredFieldsWhen = "(serialization_methods_dump or exclude_none is False)"}} +{{- end -}} {{- if $hasAdditionalProperties}}{{addImport "pydantic" "ConfigDict"}}{{end -}} {{- $fields := (sortFields $type.Fields) -}} {{- $className := sanitizeClassName $type.Name }} @@ -57,6 +64,14 @@ class {{$className}}(BaseModel): self.__pydantic_extra__ = value # pyright: ignore[reportIncompatibleVariableOverride] {{end}} + {{- $constFieldAttributes := templateConstFieldAttributeList $type}} + {{- if and $modelSerializationMethods $constFieldAttributes}} + {{- addImport "typing" "Any"}} + + def model_post_init(self, context: Any, /) -> None: + self.__pydantic_fields_set__.update([{{$constFieldAttributes}}]) + {{- end}} + {{- if hasOpenEnumField $type}} {{- range openEnums $type}} {{- if and (not .Const) (not (eq (getEnumFormat .Type) "union"))}} @@ -78,9 +93,12 @@ class {{$className}}(BaseModel): {{- addImport "types" "UNSET_SENTINEL" true}} {{- addImport "types" "serialize_with_info" true}} {{- addImport "types" "current_exclude_none" true}} - {{- if $preserveUnknownFields}} + {{- if or $preserveUnknownFields $modelSerializationMethods}} {{- addImport "types.basemodel" "current_by_alias" true}} {{- end}} + {{- if $modelSerializationMethods}} + {{- addImport "types.basemodel" "current_serialization_methods_dump" true}} + {{- end}} {{- $serializeMethodName := sanitizeSerializeMethodName $type}} @model_serializer(mode="wrap") def _serialize_model_with_info(self, handler, info): @@ -99,9 +117,13 @@ class {{$className}}(BaseModel): {{- end}} {{- end}} exclude_none = current_exclude_none() - {{- if $preserveUnknownFields}} + {{- if or $preserveUnknownFields $modelSerializationMethods}} by_alias = current_by_alias() {{- end}} + {{- if $modelSerializationMethods}} + use_field_names = by_alias is False + serialization_methods_dump = current_serialization_methods_dump() + {{- end}} serialized = handler(self) m = {} @@ -111,11 +133,11 @@ class {{$className}}(BaseModel): {{- /* Pydantic writes extras after declared fields, so an extra under a field's emitted key replaces that field's value */}} {{- /* A nested model is serialized again from its own output, which may already use aliases */}} key = k if by_alias or k in serialized else n - if key not in serialized and exclude_none is False: + if key not in serialized and {{$skipFilteredFieldsWhen}}: continue val = serialized.pop(key, None) {{- else}} - if k not in serialized and n not in serialized and exclude_none is False: + if k not in serialized and n not in serialized and {{$skipFilteredFieldsWhen}}: continue val = serialized.get(k, serialized.get(n)) {{- if $hasAdditionalProperties}} @@ -137,7 +159,7 @@ class {{$className}}(BaseModel): if val != UNSET_SENTINEL: {{- if not $hasOptionalField}} - m[k] = val + m[{{$outputKey}}] = val {{- else }} if ( val is not None @@ -146,7 +168,7 @@ class {{$className}}(BaseModel): or is_nullable_and_explicitly_set {{- end}} ): - m[k] = val + m[{{$outputKey}}] = val {{- end}} {{- if or $hasAdditionalProperties $preserveUnknownFields}} diff --git a/templates/templates/pythonv2/open-union.py.stmpl b/templates/templates/pythonv2/open-union.py.stmpl index 406601923..7d95e3e34 100644 --- a/templates/templates/pythonv2/open-union.py.stmpl +++ b/templates/templates/pythonv2/open-union.py.stmpl @@ -12,6 +12,16 @@ class {{openUnionUnknownClassName $type}}(BaseModel): is_unknown: Literal[True] = True model_config = ConfigDict(frozen=True) + {{- if .Global.Config.ModelSerializationMethods}} + {{- addImport "pydantic" "model_serializer"}} + {{- addImport "types.basemodel" "SERIALIZATION_METHODS_DUMP" true}} + + @model_serializer(mode="wrap") + def serialize_model(self, handler, info): + if isinstance(info.context, dict) and SERIALIZATION_METHODS_DUMP in info.context: + return self.raw + return handler(self) + {{- end}} {{openUnionVariantsDictName $type}}: dict[str, Any] = { diff --git a/templates/templates/pythonv2/tests/common/unknown_fields_cases.py b/templates/templates/pythonv2/tests/common/unknown_fields_cases.py index 2d5aac5d6..febc4f7e1 100644 --- a/templates/templates/pythonv2/tests/common/unknown_fields_cases.py +++ b/templates/templates/pythonv2/tests/common/unknown_fields_cases.py @@ -1,5 +1,6 @@ UNKNOWN_FIELD_CASES = [ ({"futureKind": "third", "futureCode": 3}, {"futureKind": "third", "futureCode": 3}), + ({"methodNames": {"toDict": "dictionary", "toJson": "json"}}, {"methodNames": {"toDict": "dictionary", "toJson": "json"}}), ({"future": "value"}, {}), ({"future": False}, {}), ({"empty": {"future": True}}, {"empty": {}}), diff --git a/templates/templates/pythonv2/tests/quaternary/test_unknown_fields_additional.py b/templates/templates/pythonv2/tests/quaternary/test_unknown_fields_additional.py index a5927e445..d23519773 100644 --- a/templates/templates/pythonv2/tests/quaternary/test_unknown_fields_additional.py +++ b/templates/templates/pythonv2/tests/quaternary/test_unknown_fields_additional.py @@ -1,9 +1,16 @@ +import json +import warnings + +import pydantic import pytest -from openapi import SDK, AsyncSDK +from openapi import SDK, AsyncSDK, utils from openapi.models import shared +from openapi.types import UnrecognizedInt, UnrecognizedStr +from openapi.utils import ALLOW_UNKNOWN_UNION_VARIANTS from .common_helpers import HTTPBIN_URL, record_test from .unknown_fields import drop_unexpected_fields +from .unknown_fields_cases import UNKNOWN_FIELD_CASES @pytest.mark.parametrize("child", [ @@ -18,7 +25,8 @@ def test_flattened_request_keeps_nested_unknown_fields(child): response = sdk.echo_unknown_fields(label="root", child=child) payload = {"label": "root", "child": child} assert response.raw_response.json()["json"] == payload - assert response.object.json_.model_dump(mode="json", by_alias=True) == payload + assert response.object.json_.to_dict() == payload + assert json.loads(response.object.json_.to_json()) == payload @pytest.mark.asyncio() @@ -28,7 +36,8 @@ async def test_flattened_request_keeps_nested_unknown_fields_async(): response = await sdk.echo_unknown_fields(label="root", child=child) payload = {"label": "root", "child": child} assert response.raw_response.json()["json"] == payload - assert response.object.json_.model_dump(mode="json", by_alias=True) == payload + assert response.object.json_.to_dict() == payload + assert json.loads(response.object.json_.to_json()) == payload def test_flattened_request_keeps_unknown_fields_on_nested_models(): @@ -44,6 +53,36 @@ def test_flattened_request_has_no_argument_for_top_level_unknown_fields(): sdk.echo_flattened_unknown_fields(label="root", future=True) # type: ignore[call-arg] +@pytest.mark.parametrize("fields", [fields for fields, _ in UNKNOWN_FIELD_CASES]) +def test_to_dict_round_trips_unknown_fields(fields): + payload = {"label": "root", **fields} + model = shared.FieldEnvelopeInput.model_validate(payload) + assert model.to_dict(mode="json") == payload + assert json.loads(model.to_json()) == payload + assert shared.FieldEnvelopeInput.model_validate(model.to_dict()).to_dict(mode="json") == payload + + +@pytest.mark.parametrize("options, expected", [ + ({}, {"displayName": "child", "nullable": None, "future": None}), + ({"use_api_names": False}, {"display_name": "child", "nullable": None, "future": None}), + ({"exclude_unset": False}, {"displayName": "child", "nullable": None, "future": None}), + ({"exclude_defaults": True}, {"displayName": "child", "nullable": None, "future": None}), + ({"exclude_none": True}, {"displayName": "child"}), +]) +def test_model_serialization_methods(options, expected): + model = shared.FieldNode.model_validate({"displayName": "child", "nullable": None, "future": None}) + assert model.to_dict(**options) == expected + assert model.to_dict(mode="json", **options) == expected + assert json.loads(model.to_json(**options)) == expected + + +@pytest.mark.parametrize("options", [{"exclude_none": True}, {"exclude_defaults": True}]) +def test_excluded_field_keeps_unknown_field_named_like_it(options): + model = shared.FieldEnvelopeInput.model_validate({"label": "root", "plainChoice": None, "plain_choice": "unknown"}) + assert model.to_dict(**options) == {"label": "root", "plain_choice": "unknown"} + assert model.model_dump(by_alias=True, **options) == {"label": "root", "plain_choice": "unknown"} + + def test_drop_unexpected_fields_ignores_only_undeclared_fields(): expected = shared.TypedFields(label="typed") assert drop_unexpected_fields(shared.TypedFields.model_validate({"label": "typed", "future": "value"}), expected) != expected @@ -58,3 +97,57 @@ def test_drop_unexpected_fields_in_typed_additional_properties(): assert drop_unexpected_fields(nested_unknown, expected) == expected assert drop_unexpected_fields(extra_entry, expected) != expected assert drop_unexpected_fields(changed_value, expected) != expected + + +def test_model_serialization_methods_field_names(): + model = shared.FieldEnvelopeInput.model_validate({"label": "root", "child": {"displayName": "child", "child": {"displayName": "grandchild"}}, "futureKind": "third", "plainChoice": {"text": "t"}, "future": True}) + api_names = {"label": "root", "child": {"displayName": "child", "child": {"displayName": "grandchild"}}, "futureKind": "third", "plainChoice": {"text": "t"}, "future": True} + field_names = {"label": "root", "child": {"display_name": "child", "child": {"display_name": "grandchild"}}, "future_kind": "third", "plain_choice": {"text": "t"}, "future": True} + assert model.model_dump(exclude_unset=True) == api_names + assert model.to_dict() == api_names + assert model.to_dict(use_api_names=False) == field_names + assert json.loads(model.to_json(use_api_names=False)) == field_names + assert shared.FieldEnvelopeInput.model_validate(model.to_dict(use_api_names=False)).to_dict() == api_names + + +@pytest.mark.parametrize("typ, value", [(UnrecognizedStr, "future"), (UnrecognizedInt, 3)]) +def test_unrecognized_enum_serialization(typ, value): + adapter = pydantic.TypeAdapter(typ) + enum_value = adapter.validate_python(value) + assert isinstance(enum_value, typ) + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert adapter.dump_python(enum_value) is enum_value + assert adapter.dump_python(enum_value, mode="json") == value + assert json.loads(adapter.dump_json(enum_value)) == value + with pytest.raises(pydantic.ValidationError): + adapter.validate_python(value, strict=True) + + +def test_unrecognized_enum_generated_model(): + model = shared.FieldEnvelope.model_validate({"label": "root", "futureKind": "third", "futureCode": 3}) + with warnings.catch_warnings(): + warnings.simplefilter("error") + dumped = model.model_dump(by_alias=True) + assert type(dumped["futureKind"]) is UnrecognizedStr + assert type(dumped["futureCode"]) is UnrecognizedInt + assert json.loads(utils.marshal_json(model, shared.FieldEnvelope)) == {"label": "root", "futureKind": "third", "futureCode": 3} + + +def test_flattened_parameter_named_like_helper(): + with SDK(server_url=HTTPBIN_URL) as sdk: + response = sdk.to_json.echo_grouped_fields(to_json="j") + assert response.raw_response.request.url.params["toJson"] == "j" + + +def test_serialization_methods_return_raw_payload_of_nested_unknown_variants(): + choice = {"kind": "future", "weight": 9000} + payload = {"label": "root", "choice": choice, "choices": [{"kind": "text", "text": "t", "future": True}, choice], "future": True} + model = shared.FieldEnvelope.model_validate(payload, context={ALLOW_UNKNOWN_UNION_VARIANTS: True}) + assert model.choice.is_unknown and model.choices[1].is_unknown + assert model.to_dict() == model.to_dict(mode="json") == payload + assert json.loads(model.to_json()) == payload + assert model.model_dump(by_alias=True)["choice"] == {"kind": "UNKNOWN", "raw": choice, "is_unknown": True} + assert shared.FieldEnvelope.model_validate(model.to_dict(), context={ALLOW_UNKNOWN_UNION_VARIANTS: True}).to_dict() == payload + with pytest.raises(pydantic.ValidationError, match="unrecognized kind value 'future'"): + shared.FieldEnvelope.model_validate(model.to_dict()) diff --git a/templates/templates/pythonv2/tests/secondary/test_unknown_fields_additional.py b/templates/templates/pythonv2/tests/secondary/test_unknown_fields_additional.py index 5b7c88ae8..6aa3af1e5 100644 --- a/templates/templates/pythonv2/tests/secondary/test_unknown_fields_additional.py +++ b/templates/templates/pythonv2/tests/secondary/test_unknown_fields_additional.py @@ -101,6 +101,11 @@ def test_invalid_nullable_value_becomes_unset_like_default_policy(): assert node.nullable == UNSET +def test_helper_names_not_reserved_without_methods(): + assert set(shared.MethodNames.model_fields) == {"to_dict", "to_json"} + assert not hasattr(shared.FieldEnvelope, "to_dict") + + @pytest.mark.parametrize("typ", [Optional[int], Optional[List[int]], Optional[shared.FieldNode], Any]) def test_top_level_none_serialization(typ): assert utils.marshal_json(None, typ) == "" diff --git a/templates/templates/pythonv2/tests/tertiary/test_unknown_fields_additional.py b/templates/templates/pythonv2/tests/tertiary/test_unknown_fields_additional.py new file mode 100644 index 000000000..13e19e992 --- /dev/null +++ b/templates/templates/pythonv2/tests/tertiary/test_unknown_fields_additional.py @@ -0,0 +1,142 @@ +import json +from typing import List + +import pydantic +import pytest + +from openapi import SDK, utils +from openapi.models import errors, shared +from openapi.models.shared.vehicle import UnknownVehicle, Vehicle +from openapi.utils import ALLOW_UNKNOWN_UNION_VARIANTS +from .common_helpers import HTTPBIN_URL +from .unknown_fields_cases import UNKNOWN_FIELD_CASES + + +@pytest.mark.parametrize("fields, known_fields", UNKNOWN_FIELD_CASES) +def test_to_dict_drops_unknown_fields(fields, known_fields): + model = shared.FieldEnvelopeInput.model_validate({"label": "root", **fields}) + expected = {"label": "root", **known_fields} + assert model.to_dict(mode="json") == expected + assert json.loads(model.to_json()) == expected + assert shared.FieldEnvelopeInput.model_validate(model.to_dict()).to_dict(mode="json") == expected + + +def test_flattened_request_drops_nested_unknown_fields(): + with SDK(server_url=HTTPBIN_URL) as sdk: + response = sdk.echo_flattened_unknown_fields(label="root", child={"displayName": "child", "future": True}) + assert response.raw_response.json()["json"] == {"label": "root", "child": {"displayName": "child"}} + assert response.object.json_.to_dict() == {"label": "root", "child": {"displayName": "child"}} + assert "headers" not in response.object.to_dict() + + +@pytest.mark.parametrize("options, expected", [ + ({}, {"displayName": "child", "nullable": None}), + ({"use_api_names": False}, {"display_name": "child", "nullable": None}), + ({"exclude_unset": False}, {"displayName": "child", "nullable": None}), + ({"exclude_defaults": True}, {"displayName": "child", "nullable": None}), + ({"exclude_none": True}, {"displayName": "child"}), +]) +def test_model_serialization_methods(options, expected): + model = shared.FieldNode.model_validate({"displayName": "child", "nullable": None}) + assert model.to_dict(**options) == expected + assert model.to_dict(mode="json", **options) == expected + assert json.loads(model.to_json(**options)) == expected + + +def test_exclude_none_keeps_explicit_nulls_on_the_wire(): + model = shared.FieldNode(display_name="child", nullable=None) + assert model.to_dict(exclude_none=True) == {"displayName": "child"} + assert json.loads(utils.marshal_json(model, shared.FieldNode)) == {"displayName": "child", "nullable": None} + + +def test_excluded_fields_stay_out_of_serialization_methods(): + model = shared.DefaultsAndConsts(normal_field="x") + assert "defaultStrNullable" not in model.to_dict() + assert model.to_dict(exclude_unset=False)["defaultStrNullable"] is None + assert shared.DefaultsAndConsts(normal_field="x", default_str_nullable=None).to_dict()["defaultStrNullable"] is None + assert json.loads(utils.marshal_json(model, shared.DefaultsAndConsts))["defaultStrNullable"] is None + node = shared.FieldNode(display_name="child", nullable=None) + assert node.model_dump(by_alias=True, exclude={"display_name"}) == {"nullable": None} + assert node.model_dump(by_alias=True, exclude_none=True) == {"displayName": "child", "nullable": None} + + +def test_model_serialization_methods_field_names(): + model = shared.FieldEnvelopeInput.model_validate({"label": "root", "child": {"displayName": "child"}, "futureKind": "third", "plainChoice": {"text": "t"}}) + api_names = {"label": "root", "child": {"displayName": "child"}, "futureKind": "third", "plainChoice": {"text": "t"}} + field_names = {"label": "root", "child": {"display_name": "child"}, "future_kind": "third", "plain_choice": {"text": "t"}} + assert model.to_dict() == api_names + assert model.to_dict(use_api_names=False) == field_names + assert json.loads(model.to_json(use_api_names=False)) == field_names + assert shared.FieldEnvelopeInput.model_validate(model.to_dict(use_api_names=False)).to_dict() == api_names + + +def test_default_dump_keeps_nested_api_names(): + model = shared.FieldEnvelopeInput.model_validate({"label": "root", "child": {"displayName": "child", "child": {"displayName": "grandchild"}}, "children": [{"displayName": "item"}]}) + expected = {"label": "root", "child": {"displayName": "child", "child": {"displayName": "grandchild"}}, "children": [{"displayName": "item"}]} + assert model.model_dump() == expected + assert json.loads(model.model_dump_json()) == expected + assert pydantic.TypeAdapter(List[shared.FieldNode]).dump_python([shared.FieldNode(display_name="item")]) == [{"displayName": "item"}] + + +def test_helper_names_reserved_only_on_models(): + assert set(shared.MethodNames.model_fields) == {"to_dict_", "to_json_"} + model = shared.MethodNames.model_validate({"toDict": "d", "toJson": "j"}) + assert model.to_dict() == {"toDict": "d", "toJson": "j"} + with SDK(server_url=HTTPBIN_URL) as sdk: + response = sdk.to_json.echo_grouped_fields() + assert response.status_code == 200 + + +def test_model_serialization_methods_keep_consts(): + model = shared.FieldEnvelopeInput(label="root", choice=shared.TextChoice(text="t"), constant=shared.ConstFields()) + expected = {"label": "root", "choice": {"kind": "text", "text": "t"}, "constant": {"kind": "fixed"}} + wire = {"label": "root", "choice": {"kind": "text", "text": "t"}, "constant": {"kind": "fixed", "priority": 1}} + assert json.loads(utils.marshal_json(model, shared.FieldEnvelopeInput)) == wire + assert model.to_dict() == expected + assert model.to_dict(exclude_unset=False) == wire + assert json.loads(model.to_json()) == expected + assert shared.FieldEnvelopeInput.model_validate(model.to_dict()).to_dict() == expected + assert shared.FieldEnvelopeInput.model_validate(wire).to_dict() == wire + assert shared.ConstFields(priority=1).to_dict() == {"kind": "fixed", "priority": 1} + assert shared.ConstFields(priority=1).to_dict(exclude_defaults=True) == {} + assert shared.ConstFields(priority=2).to_dict(exclude_defaults=True) == {"priority": 2} + + +def test_model_serialization_methods_keep_error_data_consts(): + data = errors.EchoUnknownFieldsResponseBodyData(message="bad") + assert data.model_dump(by_alias=True) == {"message": "bad", "kind": "invalid", "detail": None} + assert data.to_dict() == {"message": "bad", "kind": "invalid"} + assert json.loads(data.to_json()) == {"message": "bad", "kind": "invalid"} + assert data.to_dict(exclude_defaults=True) == {"message": "bad"} + + +def test_optional_null_consts_keep_wire_output(): + model = shared.ObjectWithNullEnums() + assert json.loads(utils.marshal_json(model, shared.ObjectWithNullEnums)) == {"nullEnum": None} + assert model.to_dict() == {"nullEnum": None} + explicit = shared.ObjectWithNullEnums(opt_null_enum=None) + assert json.loads(utils.marshal_json(explicit, shared.ObjectWithNullEnums)) == {"nullEnum": None, "optNullEnum": None} + assert explicit.to_dict() == {"nullEnum": None, "optNullEnum": None} + + +def test_serialization_methods_return_raw_payload_of_unknown_variants(): + vehicle = {"vehicleType": "spaceship", "thrust": 9000} + unknown = pydantic.TypeAdapter(Vehicle).validate_python(vehicle, context={ALLOW_UNKNOWN_UNION_VARIANTS: True}) + assert isinstance(unknown, UnknownVehicle) + assert unknown.to_dict() == vehicle + assert json.loads(unknown.to_json()) == vehicle + assert unknown.model_dump(by_alias=True) == {"vehicle_type": "UNKNOWN", "raw": vehicle, "is_unknown": True} + + +def test_serialization_methods_return_raw_payload_of_nested_unknown_variants(): + choice = {"kind": "future", "weight": 9000} + payload = {"label": "root", "choice": choice, "choices": [{"kind": "text", "text": "t"}, choice]} + model = shared.FieldEnvelope.model_validate(payload, context={ALLOW_UNKNOWN_UNION_VARIANTS: True}) + assert model.choice.is_unknown and model.choices[1].is_unknown + assert model.to_dict() == model.to_dict(mode="json") == payload + assert json.loads(model.to_json()) == payload + assert model.to_dict(use_api_names=False)["choices"] == payload["choices"] + assert model.model_dump(by_alias=True)["choice"] == {"kind": "UNKNOWN", "raw": choice, "is_unknown": True} + assert shared.FieldEnvelope.model_validate(model.to_dict(), context={ALLOW_UNKNOWN_UNION_VARIANTS: True}).to_dict() == payload + with pytest.raises(pydantic.ValidationError, match="unrecognized kind value 'future'"): + shared.FieldEnvelope.model_validate(model.to_dict()) diff --git a/tests/config/quaternary/pythonv2/.speakeasy/gen.yaml b/tests/config/quaternary/pythonv2/.speakeasy/gen.yaml index 98733ed2e..b8c4abd36 100644 --- a/tests/config/quaternary/pythonv2/.speakeasy/gen.yaml +++ b/tests/config/quaternary/pythonv2/.speakeasy/gen.yaml @@ -36,6 +36,7 @@ python: responseSchemaValidation: lenient flattenRequests: true preserveUnknownFields: true + modelSerializationMethods: true pytestFilterWarnings: - error::RuntimeWarning # Ensure __del__ method warnings are raised diff --git a/tests/config/review/pythonv2/.speakeasy/gen.yaml b/tests/config/review/pythonv2/.speakeasy/gen.yaml index 357bc40e1..b06dd1876 100644 --- a/tests/config/review/pythonv2/.speakeasy/gen.yaml +++ b/tests/config/review/pythonv2/.speakeasy/gen.yaml @@ -88,6 +88,7 @@ python: methodArguments: infer-optional-args methodTimeoutArgument: timeout-ms methodTimeoutUnits: milliseconds + modelSerializationMethods: false moduleName: speakeasy.new_openapi multipartArrayFormat: standard optionalDependencies: diff --git a/tests/config/tertiary/pythonv2/.speakeasy/gen.yaml b/tests/config/tertiary/pythonv2/.speakeasy/gen.yaml index ffe89c4a4..41aa32644 100644 --- a/tests/config/tertiary/pythonv2/.speakeasy/gen.yaml +++ b/tests/config/tertiary/pythonv2/.speakeasy/gen.yaml @@ -32,6 +32,7 @@ python: relative: true maxMethodParams: 0 flattenRequests: true + modelSerializationMethods: true packageName: openapi responseFormat: envelope inputTypedDictSuffix: Param diff --git a/tests/specs/fragments/uber/unknown-fields.yaml b/tests/specs/fragments/uber/unknown-fields.yaml index 8ed2b359c..c1434eff7 100644 --- a/tests/specs/fragments/uber/unknown-fields.yaml +++ b/tests/specs/fragments/uber/unknown-fields.yaml @@ -8,6 +8,29 @@ security: - accessToken: [] - {} paths: + /anything/unknown-fields-group: + get: + operationId: echoGroupedFields + x-speakeasy-group: toJson + x-speakeasy-exclude-test-sdk: [ruby] + description: Belongs to a group named like a common model serialization method, so the group accessor must keep its name when models reserve that method name. Its query parameter has the same name, so method arguments and their documentation must keep it too. + parameters: + - name: toJson + in: query + required: false + description: Echoed back in the request URL. + schema: + type: string + responses: + '200': + description: The echoed request URL. + content: + application/json: + schema: + type: object + properties: + url: + type: string /anything/unknown-fields-query: get: operationId: echoOptionalJsonQuery @@ -57,15 +80,18 @@ paths: json: $ref: '#/components/schemas/FieldEnvelope' '400': - description: A structured error with additional server fields. + description: A structured error with additional server fields and a required const populated by its default. content: application/json: schema: type: object - required: [message] + required: [message, kind] properties: message: type: string + kind: + type: string + const: invalid detail: $ref: '#/components/schemas/FieldNode' /anything/unknown-fields-flattened: @@ -108,14 +134,11 @@ components: additionalProperties: $ref: '#/components/schemas/FieldNode' choice: - oneOf: - - $ref: '#/components/schemas/TextChoice' - - $ref: '#/components/schemas/NumberChoice' - discriminator: - propertyName: kind - mapping: - text: '#/components/schemas/TextChoice' - number: '#/components/schemas/NumberChoice' + $ref: '#/components/schemas/Choice' + choices: + type: array + items: + $ref: '#/components/schemas/Choice' plainChoice: anyOf: - $ref: '#/components/schemas/TextFields' @@ -141,6 +164,8 @@ components: $ref: '#/components/schemas/TypedNodes' closed: $ref: '#/components/schemas/ClosedFields' + constant: + $ref: '#/components/schemas/ConstFields' arbitrary: {} futureKind: type: string @@ -150,6 +175,13 @@ components: type: integer enum: [1, 2] x-speakeasy-unknown-values: allow + methodNames: + type: object + properties: + toDict: + type: string + toJson: + type: string FieldNode: type: object required: [displayName] @@ -160,6 +192,15 @@ components: type: [string, 'null'] child: $ref: '#/components/schemas/FieldNode' + Choice: + oneOf: + - $ref: '#/components/schemas/TextChoice' + - $ref: '#/components/schemas/NumberChoice' + discriminator: + propertyName: kind + mapping: + text: '#/components/schemas/TextChoice' + number: '#/components/schemas/NumberChoice' TextChoice: type: object required: [kind, text] @@ -245,6 +286,19 @@ components: EmptyFields: type: object additionalProperties: false + ConstFields: + type: object + description: A required const alongside optional fields, one of them with a default, so values are populated by their defaults rather than by the caller. + required: [kind] + properties: + kind: + type: string + const: fixed + label: + type: string + priority: + type: integer + default: 1 FlattenedFields: type: object description: A body with few enough properties to be flattened into operation arguments. diff --git a/zSDKs/sdk-pythonv2/.speakeasy/gen.lock b/zSDKs/sdk-pythonv2/.speakeasy/gen.lock index 43a08637a..40b3f3111 100644 --- a/zSDKs/sdk-pythonv2/.speakeasy/gen.lock +++ b/zSDKs/sdk-pythonv2/.speakeasy/gen.lock @@ -6,7 +6,7 @@ management: speakeasyVersion: internal generationVersion: internal releaseVersion: 0.0.1 - configChecksum: 908ffac3f2935a4cd93222913042d257 + configChecksum: 3b5207abcf7df0a028f020866f6e1112 repoURL: https://github.com/speakeasy-sdks/test-sdk installationURL: https://github.com/speakeasy-sdks/test-sdk.git features: @@ -450,9 +450,9 @@ trackedFiles: docs/sdks/tag1/README.md: last_write_checksum: sha1:75641bf55bd49e50334b7e99fd54a63378c80cbd docs/sdks/tag2/README.md: - last_write_checksum: sha1:28127913956534d2fb17bdf63dba1ca09f422cf2 + last_write_checksum: sha1:90953e2716017e6d416fe5e9ea4620d3754173a2 docs/sdks/tag3/README.md: - last_write_checksum: sha1:3084c4529e6c62d2aae53aedf4e312c9c63b4311 + last_write_checksum: sha1:021701a07814c3368b317b05be9b844c372ad5d9 docs/sdks/tail/README.md: last_write_checksum: sha1:85301e5b05d24da65512656a510aeb15033c1551 docs/sdks/types/README.md: @@ -734,9 +734,9 @@ trackedFiles: src/speakeasy/new_openapi/tag1.py: last_write_checksum: sha1:e7611f7deb992e771ce1117040bb40d21305f466 src/speakeasy/new_openapi/tag2.py: - last_write_checksum: sha1:66f5cc660661e3bde0acb7a0389c5ef9d58665d5 + last_write_checksum: sha1:e185a5357b789a9ad8654526dca639e034a123ca src/speakeasy/new_openapi/tag3.py: - last_write_checksum: sha1:f53691aa2a52f6a5b9474e7801c82b85c27cc48f + last_write_checksum: sha1:a45f32697dcdb2ec5679117a2fa5f0063a017e39 src/speakeasy/new_openapi/tail.py: last_write_checksum: sha1:7565b3fcac7390be59d87d4bb56b9db60e1b2e3c src/speakeasy/new_openapi/testgroup.py: diff --git a/zSDKs/sdk-pythonv2/.speakeasy/gen.yaml b/zSDKs/sdk-pythonv2/.speakeasy/gen.yaml index 357bc40e1..b06dd1876 100644 --- a/zSDKs/sdk-pythonv2/.speakeasy/gen.yaml +++ b/zSDKs/sdk-pythonv2/.speakeasy/gen.yaml @@ -88,6 +88,7 @@ python: methodArguments: infer-optional-args methodTimeoutArgument: timeout-ms methodTimeoutUnits: milliseconds + modelSerializationMethods: false moduleName: speakeasy.new_openapi multipartArrayFormat: standard optionalDependencies: diff --git a/zSDKs/sdk-pythonv2/docs/sdks/tag2/README.md b/zSDKs/sdk-pythonv2/docs/sdks/tag2/README.md index 9726ef1aa..b0a097456 100644 --- a/zSDKs/sdk-pythonv2/docs/sdks/tag2/README.md +++ b/zSDKs/sdk-pythonv2/docs/sdks/tag2/README.md @@ -145,7 +145,7 @@ asyncio.run(main()) | `obj` | [models.ExhaustiveObject](../../models/exhaustiveobject.md) | :heavy_check_mark: | A simple object that uses all our supported primitive types and enums and has optional properties.

[A link to the external docs.](https://speakeasy.com) | | | `deprecated_query_param1` | *Optional[str]* | :heavy_minus_sign: | :warning: **DEPRECATED**: This will be removed in a future release, please migrate away from it as soon as possible. | some example query param | | `deprecated_query_param2` | *Optional[str]* | :heavy_minus_sign: | :warning: **DEPRECATED**: This will be removed in a future release, please migrate away from it as soon as possible. | some example query param | -| `type` | [Optional[models.Type]](../../models/type.md) | :heavy_minus_sign: | N/A | type1 | +| `type_` | [Optional[models.Type]](../../models/type.md) | :heavy_minus_sign: | N/A | type1 | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | | `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | diff --git a/zSDKs/sdk-pythonv2/docs/sdks/tag3/README.md b/zSDKs/sdk-pythonv2/docs/sdks/tag3/README.md index b328b1ee7..95ae7754f 100644 --- a/zSDKs/sdk-pythonv2/docs/sdks/tag3/README.md +++ b/zSDKs/sdk-pythonv2/docs/sdks/tag3/README.md @@ -145,7 +145,7 @@ asyncio.run(main()) | `obj` | [models.ExhaustiveObject](../../models/exhaustiveobject.md) | :heavy_check_mark: | A simple object that uses all our supported primitive types and enums and has optional properties.

[A link to the external docs.](https://speakeasy.com) | | | `deprecated_query_param1` | *Optional[str]* | :heavy_minus_sign: | :warning: **DEPRECATED**: This will be removed in a future release, please migrate away from it as soon as possible. | some example query param | | `deprecated_query_param2` | *Optional[str]* | :heavy_minus_sign: | :warning: **DEPRECATED**: This will be removed in a future release, please migrate away from it as soon as possible. | some example query param | -| `type` | [Optional[models.Type]](../../models/type.md) | :heavy_minus_sign: | N/A | type1 | +| `type_` | [Optional[models.Type]](../../models/type.md) | :heavy_minus_sign: | N/A | type1 | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | | `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | diff --git a/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag2.py b/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag2.py index e4b1909f5..9d6cf3372 100644 --- a/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag2.py +++ b/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag2.py @@ -32,7 +32,7 @@ def post_test( :param obj: A simple object that uses all our supported primitive types and enums and has optional properties. :param deprecated_query_param1: :param deprecated_query_param2: - :param type: + :param type_: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -190,7 +190,7 @@ async def post_test( :param obj: A simple object that uses all our supported primitive types and enums and has optional properties. :param deprecated_query_param1: :param deprecated_query_param2: - :param type: + :param type_: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds diff --git a/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag3.py b/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag3.py index eaef3b2cc..1f6259031 100644 --- a/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag3.py +++ b/zSDKs/sdk-pythonv2/src/speakeasy/new_openapi/tag3.py @@ -32,7 +32,7 @@ def post_test( :param obj: A simple object that uses all our supported primitive types and enums and has optional properties. :param deprecated_query_param1: :param deprecated_query_param2: - :param type: + :param type_: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -190,7 +190,7 @@ async def post_test( :param obj: A simple object that uses all our supported primitive types and enums and has optional properties. :param deprecated_query_param1: :param deprecated_query_param2: - :param type: + :param type_: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds From c4ba5fbb5782be9309ee1b0569b9166983f5bb8b Mon Sep 17 00:00:00 2001 From: 2ynn Date: Tue, 27 Oct 2026 16:22:25 -0400 Subject: [PATCH 2/2] test(python): update snapshot for sanitized parameter docs Method docstrings now name a parameter as the signature does, so the renamed retries_ parameter is documented as retries_. --- .../namespaces/namespace_collision_stress_py_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/generate/snapshots/namespaces/namespace_collision_stress_py_test.go b/pkg/generate/snapshots/namespaces/namespace_collision_stress_py_test.go index 6eb1fb209..6b401f225 100644 --- a/pkg/generate/snapshots/namespaces/namespace_collision_stress_py_test.go +++ b/pkg/generate/snapshots/namespaces/namespace_collision_stress_py_test.go @@ -2351,7 +2351,7 @@ class SDK(BaseSDK): http_headers: Optional[Mapping[str, str]] = None, ) -> models_.retries.Widget: r""" - :param retries: + :param retries_: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -2434,7 +2434,7 @@ class SDK(BaseSDK): http_headers: Optional[Mapping[str, str]] = None, ) -> models_.retries.Widget: r""" - :param retries: + :param retries_: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds