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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ client.service.get_metadata(
exports[`snippets > examples > 'POST /big-entity (simple)' 1`] = `
"from acme import Acme
from acme.types import Actor, ExtendedMovie, Migration
from acme.commons.types import EventInfo_Metadata

client = Acme(
token="<YOUR_API_KEY>",
Expand Down Expand Up @@ -68,15 +69,14 @@ client.service.create_big_entity(
},
revenue=1000000,
),
event_info={
"type": "metadata",
"id": "event-12345",
"data": {
event_info=EventInfo_Metadata(
id="event-12345",
data={
"key1": "val1",
"key2": "val2"
},
"json_string": "abc"
},
json_string="abc",
),
migration=Migration(
name="Migration 31 Aug",
status="RUNNING",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ export class DynamicSnippetsGeneratorContext extends AbstractDynamicSnippetsGene
return python.reference({ name: className, modulePath });
}

public getDiscriminatedUnionVariantClassReference({
unionDeclaration,
discriminantValue
}: {
unionDeclaration: FernIr.dynamic.Declaration;
discriminantValue: FernIr.dynamic.NameAndWireValue;
}): python.Reference {
const unionClassName = this.getClassName(unionDeclaration.name);
const variantSuffix = discriminantValue.name.pascalCase.safeName;
const modulePath = [
...this.getRootModulePath(),
...unionDeclaration.fernFilepath.allParts.map((part) => part.snakeCase.safeName)
];
return python.reference({ name: `${unionClassName}_${variantSuffix}`, modulePath });
}

public useTypedDictRequests(): boolean {
return this.customConfig.use_typeddict_requests === true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,19 @@ export class DynamicTypeLiteralMapper {
if (unionProperties == null) {
return python.TypeInstantiation.nop();
}
const discriminantProperty = {
name: this.context.getPropertyName(discriminatedUnion.discriminant.name),
value: python.TypeInstantiation.str(unionVariant.discriminantValue.wireValue)
};
return python.TypeInstantiation.typedDict([discriminantProperty, ...unionProperties], { multiline: true });
const variantClassReference = this.context.getDiscriminatedUnionVariantClassReference({
unionDeclaration: discriminatedUnion.declaration,
discriminantValue: unionVariant.discriminantValue
});
return python.TypeInstantiation.reference(
python.instantiateClass({
classReference: variantClassReference,
arguments_: unionProperties.map((entry) =>
python.methodArgument({ name: entry.name, value: entry.value })
),
multiline: true
})
);
}

private convertDiscriminatedUnionProperties({
Expand Down Expand Up @@ -387,13 +395,6 @@ export class DynamicTypeLiteralMapper {
value: unknown;
}): python.TypeInstantiation {
const entries = this.convertObjectEntries({ object_, value });

// biome-ignore lint/correctness/useHookAtTopLevel: not a React hook
if (this.context.useTypedDictRequests()) {
return python.TypeInstantiation.typedDict(entries, { multiline: true });
}

// Pydantic model style: ClassName(key=value)
const classReference = this.context.getTypeClassReference(object_.declaration);
return python.TypeInstantiation.reference(
python.instantiateClass({
Expand All @@ -415,10 +416,27 @@ export class DynamicTypeLiteralMapper {
if (enumValue == null) {
return python.TypeInstantiation.nop();
}
return python.TypeInstantiation.str(enumValue);
const enumType = this.context.customConfig.pydantic_config?.enum_type;
if (enumType === "python_enums" || enumType === "forward_compatible_python_enums") {
const classReference = this.context.getTypeClassReference(enum_.declaration);
const memberName = enumValue.name.screamingSnakeCase.safeName;
return python.TypeInstantiation.reference(
python.accessAttribute({
lhs: classReference,
rhs: python.codeBlock(memberName)
})
);
}
return python.TypeInstantiation.str(enumValue.wireValue);
}

private getEnumValue({ enum_, value }: { enum_: FernIr.dynamic.EnumType; value: unknown }): string | undefined {
private getEnumValue({
enum_,
value
}: {
enum_: FernIr.dynamic.EnumType;
value: unknown;
}): FernIr.dynamic.NameAndWireValue | undefined {
if (typeof value !== "string") {
this.context.errors.add({
severity: Severity.Critical,
Expand All @@ -434,7 +452,7 @@ export class DynamicTypeLiteralMapper {
});
return undefined;
}
return value;
return enumValue;
}

private convertUndiscriminatedUnion({
Expand Down Expand Up @@ -586,6 +604,17 @@ export class DynamicTypeLiteralMapper {
if (firstValue == null) {
return python.TypeInstantiation.nop();
}
const enumType = this.context.customConfig.pydantic_config?.enum_type;
if (enumType === "python_enums" || enumType === "forward_compatible_python_enums") {
const classReference = this.context.getTypeClassReference(named.declaration);
const memberName = firstValue.name.screamingSnakeCase.safeName;
return python.TypeInstantiation.reference(
python.accessAttribute({
lhs: classReference,
rhs: python.codeBlock(memberName)
})
);
}
return python.TypeInstantiation.str(firstValue.wireValue);
}
case "object": {
Expand All @@ -605,10 +634,6 @@ export class DynamicTypeLiteralMapper {
});
}
}
// biome-ignore lint/correctness/useHookAtTopLevel: not a React hook
if (this.context.useTypedDictRequests()) {
return python.TypeInstantiation.typedDict(entries, { multiline: true });
}
const classReference = this.context.getTypeClassReference(named.declaration);
return python.TypeInstantiation.reference(
python.instantiateClass({
Expand Down
5 changes: 4 additions & 1 deletion generators/python-v2/sdk/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ export class SdkGeneratorCli extends AbstractPythonGeneratorCli<SdkCustomConfigS
config: {
organization: context.config.organization,
workspaceName: context.config.workspaceName,
customConfig: context.customConfig
// Pass the raw customConfig (not the parsed SdkCustomConfigSchema) so that
// fields consumed by the dynamic snippets generator (e.g. pydantic_config)
// are preserved. SdkCustomConfigSchema.parse() strips unknown keys.
customConfig: context.config.customConfig
} as FernGeneratorExec.GeneratorConfig
});

Expand Down
19 changes: 19 additions & 0 deletions generators/python/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
# For unreleased changes, use unreleased.yml
- version: 5.3.2
changelogEntry:
- summary: |
Fix enum values in reference.md and README snippets being rendered as string
literals instead of enum references when using `python_enums` or
`forward_compatible_python_enums` pydantic config.
type: fix
- summary: |
Fix client_id and client_secret parameters missing from OAuth client credentials
client instantiation examples in docstrings.
type: fix
- summary: |
Fix objects and discriminated unions in reference.md and README snippets being
rendered as dict literals instead of Pydantic model constructors. For example,
`order_type=OrderType_Market()` instead of `order_type={"type": "MARKET"}`.
type: fix
createdAt: "2026-04-06"
irVersion: 65

- version: 5.3.1
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,7 @@ def build_default_snippet_kwargs() -> List[typing.Tuple[str, AST.Expression]]:
- Use kwargs (many root client params are keyword-only).
- Include required parameters (those without defaults) with reasonable placeholders.
- Prefer inferred-auth credentials (e.g. api_key) when present.
- Include client_id/client_secret for OAuth client credentials flows.
"""

required_params = [
Expand Down Expand Up @@ -1737,6 +1738,15 @@ def build_default_snippet_kwargs() -> List[typing.Tuple[str, AST.Expression]]:

kwargs.append((name, AST.Expression(f'"YOUR_{name.upper()}"')))

# For OAuth client credentials, explicitly include client_id and client_secret
# even though they have os.getenv() defaults.
if self._oauth_token_override:
oauth_param_names_in_kwargs = {name for name, _ in kwargs}
if "client_id" not in oauth_param_names_in_kwargs:
kwargs.append(("client_id", AST.Expression('"YOUR_CLIENT_ID"')))
if "client_secret" not in oauth_param_names_in_kwargs:
kwargs.append(("client_secret", AST.Expression('"YOUR_CLIENT_SECRET"')))

return kwargs

if self._use_kwargs_snippets:
Expand Down
Loading