Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changesets/1792768355-9f04a617.yaml
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions templates/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions templates/templates/pythonv2/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions templates/templates/pythonv2/error.py.stmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
4 changes: 2 additions & 2 deletions templates/templates/pythonv2/includes/documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -159,7 +159,7 @@ function templateMethodParametersDocs(operation: Operation): string {
}

contents.push([
`\`${sanitizeFieldName(parameter.Name)}\``,
`\`${sanitizeParameterName(parameter.Name)}\``,
templateTypeMarkdown({
fieldDef: parameter,
}),
Expand Down
25 changes: 18 additions & 7 deletions templates/templates/pythonv2/includes/sanitization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "") {
Expand Down Expand Up @@ -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)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
) {
name += "_";
}

return name;
}
registerTemplateFunc("sanitizeFieldName", sanitizeFieldName);

function sanitizeAttributeName(name: string): string {
name = sanitizeName(name);
if (name.endsWith("_")) {
name = name.substring(0, name.length - 1);
Expand All @@ -326,7 +340,6 @@ function sanitizeFieldName(name: string): string {

return name;
}
registerTemplateFunc("sanitizeFieldName", sanitizeFieldName);

function useConflictResistantModelImports(): boolean {
return !!context.Global.Config.FixFlags?.[
Expand Down Expand Up @@ -413,9 +426,7 @@ function doesModuleNameConflictAcrossNamespaces(

// @ts-ignore
function sanitizeSubSDKFieldName(name: string): string {
name = sanitizeFieldName(name);

return name;
return sanitizeAttributeName(name);
}
registerTemplateFunc("sanitizeSubSDKFieldName", sanitizeSubSDKFieldName);

Expand Down
29 changes: 26 additions & 3 deletions templates/templates/pythonv2/includes/templating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading