Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba0cfb5
feat(seed): augment server-sent-events-openapi fixture with x-fern-st…
jsklan Apr 9, 2026
68551ae
chore(seed): add debug script for SSE pipeline stage inspection
jsklan Apr 10, 2026
7e304f1
chore(seed): comment out endpoint 9 (x-fern-type-name collision)
jsklan Apr 10, 2026
fe0503a
fix: deduplicate stream condition properties in discriminated union v…
devin-ai-integration[bot] Apr 10, 2026
71a5500
chore: update ir-to-jsonschema snapshots for server-sent-events-opena…
devin-ai-integration[bot] Apr 10, 2026
48bcdbb
chore: add server-sent-events-openapi to go-sdk allowed failures
devin-ai-integration[bot] Apr 10, 2026
29f8a56
fix: preserve AST references for streaming endpoint imports in wire t…
devin-ai-integration[bot] Apr 10, 2026
8dab51a
style: fix biome formatting in WireTestGenerator.ts
devin-ai-integration[bot] Apr 10, 2026
8ce469d
fix: escape triple quotes in docstrings and suppress mypy overload er…
devin-ai-integration[bot] Apr 10, 2026
f58bef3
chore: update seed snapshot for exported client type ignore comment
devin-ai-integration[bot] Apr 10, 2026
b454d17
chore: merge main into jsklan/fix-streaming-parsing to resolve conflicts
devin-ai-integration[bot] Apr 10, 2026
c367bf3
fix: use actual stream condition property name in WireMock body patterns
devin-ai-integration[bot] Apr 10, 2026
de13678
fix: use explicit None check for mypy type narrowing in wrapper class
devin-ai-integration[bot] Apr 10, 2026
bc80ede
fix: update CLI changelog version ordering and regenerate IR test sna…
devin-ai-integration[bot] Apr 10, 2026
bb1717f
chore: merge main into branch to resolve changelog conflict
devin-ai-integration[bot] Apr 10, 2026
eac5ea7
Merge branch 'main' into jsklan/fix-streaming-parsing
jsklan Apr 10, 2026
44b5987
nit
jsklan Apr 10, 2026
b2bbfa6
fix(python): replace type: ignore with proper @overload signatures in…
jsklan Apr 10, 2026
38bc0b7
Update packages/commons/mock-utils/index.ts
jsklan Apr 10, 2026
1122661
format
jsklan Apr 10, 2026
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 @@ -271,7 +271,12 @@ export class DynamicTypeLiteralMapper {
object_: named,
value: discriminatedUnionTypeInstance.value
});
return [...baseFields, ...objectEntries];
// Skip object entries that overlap with base fields to avoid
// duplicate keyword arguments (e.g. stream condition properties
// inherited via extends that are already pinned as literals).
const baseFieldNames = new Set(baseFields.map((f) => f.name));
const filteredObjectEntries = objectEntries.filter((entry) => !baseFieldNames.has(entry.name));
return [...baseFields, ...filteredObjectEntries];
}
case "singleProperty": {
try {
Expand Down
26 changes: 18 additions & 8 deletions generators/python-v2/sdk/src/wire-tests/WireTestGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,22 +368,32 @@ export class WireTestGenerator {
if (isErrorResponse) {
// For streaming endpoints, we need to consume the iterator inside pytest.raises
if (this.isStreamingEndpoint(endpoint)) {
statements.push(
python.codeBlock(
`with pytest.raises(ApiError):\n for _ in ${apiCallAst.toString()}:\n pass`
)
const block = python.codeBlock(
`with pytest.raises(ApiError):\n for _ in ${apiCallAst.toString()}:\n pass`
);
// Preserve import references from the AST that are lost during toString()
for (const ref of apiCallAst.getReferences()) {
block.addReference(ref);
}
statements.push(block);
} else {
statements.push(
python.codeBlock(`with pytest.raises(ApiError):\n ${apiCallAst.toString()}`)
);
const block = python.codeBlock(`with pytest.raises(ApiError):\n ${apiCallAst.toString()}`);
for (const ref of apiCallAst.getReferences()) {
block.addReference(ref);
}
statements.push(block);
}
} else {
// For streaming endpoints, wrap the call in a for loop to consume the iterator
// This is necessary because streaming methods return lazy generators that don't
// execute the HTTP request until iterated
if (this.isStreamingEndpoint(endpoint)) {
statements.push(python.codeBlock(`for _ in ${apiCallAst.toString()}:`));
const block = python.codeBlock(`for _ in ${apiCallAst.toString()}:`);
// Preserve import references from the AST that are lost during toString()
for (const ref of apiCallAst.getReferences()) {
block.addReference(ref);
}
statements.push(block);
statements.push(python.codeBlock(" pass"));
} else {
statements.push(apiCallAst);
Expand Down
21 changes: 21 additions & 0 deletions generators/python/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
# For unreleased changes, use unreleased.yml
- version: 5.3.11
changelogEntry:
- summary: |
Fix duplicate keyword arguments in generated code for discriminated union
request bodies with stream condition properties. When a union variant inherits
the stream condition field from a base schema via extends, the property was
emitted twice — once from the union's base properties and once from the
variant's extended properties — causing SyntaxError in generated Python code.
type: fix
- summary: |
Escape triple quotes in docstrings to prevent premature docstring termination
when OpenAPI descriptions contain Python code examples with triple-quoted strings.
type: fix
- summary: |
Fix mypy call-overload errors in exported client wrapper by mirroring the
base client's @overload signatures and using **kwargs pass-through, instead
of suppressing the error with a type: ignore comment.
type: fix
createdAt: "2026-04-10"
irVersion: 65

- version: 5.3.10
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

def escape_docstring(text: str) -> str:
"""
Escape backslashes in docstrings to avoid SyntaxWarning for invalid escape sequences.
This is needed when docstrings contain backslashes in source text (e.g., FOO\\_BAR)
that would otherwise produce invalid escape sequences.
Escape special characters in docstrings to avoid syntax errors.
- Backslashes are escaped to prevent invalid escape sequences (e.g., FOO\\_BAR).
- Triple quotes are escaped to prevent premature docstring termination when
descriptions contain code examples with triple-quoted strings.
"""
return text.replace("\\", "\\\\")
result = text.replace("\\", "\\\\")
result = result.replace('"""', '\\"""')
return result


class Docstring(CodeWriter):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,15 @@ def generate(self) -> None:
)
]
discriminant_wire_value = self._union.discriminant.wire_value
base_property_wire_values = {bp.name.wire_value for bp in self._union.base_properties}
object_properties = self._context.get_all_properties_including_extensions(shape.type_id)
for object_property in object_properties:
# Skip properties that match the discriminant field to avoid duplicate fields
if object_property.name.wire_value == discriminant_wire_value:
continue
# Skip properties already declared in the union's base properties
if object_property.name.wire_value in base_property_wire_values:
continue
self._all_referenced_types.append(object_property.value_type)
same_properties_as_object_property_fields.append(
FernAwarePydanticField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class RootClient:
class_reference: AST.ClassReference
parameters: List[ConstructorParameter]
init_parameters: Optional[List[ConstructorParameter]] = field(default=None)
constructor_overloads: Optional[List[AST.FunctionSignature]] = field(default=None)


@dataclass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ def __init__(
base_url_example_value=base_url_example_value,
sync_init_parameters=self._get_constructor_parameters(is_async=False),
async_init_parameters=self._get_constructor_parameters(is_async=True),
sync_constructor_overloads=self._get_constructor_overloads(is_async=False),
async_constructor_overloads=self._get_constructor_overloads(is_async=True),
)
self._generated_root_client = root_client_builder.build()

Expand Down Expand Up @@ -1650,6 +1652,8 @@ def __init__(
base_url_example_value: Optional[AST.Expression] = None,
sync_init_parameters: Optional[Sequence[ConstructorParameter]] = None,
async_init_parameters: Optional[Sequence[ConstructorParameter]] = None,
sync_constructor_overloads: Optional[List[AST.FunctionSignature]] = None,
async_constructor_overloads: Optional[List[AST.FunctionSignature]] = None,
):
self._module_path = module_path
self._class_name = class_name
Expand All @@ -1664,6 +1668,8 @@ def __init__(
self._oauth_token_override = oauth_token_override
self._use_kwargs_snippets = use_kwargs_snippets
self._base_url_example_value = base_url_example_value
self._sync_constructor_overloads = sync_constructor_overloads
self._async_constructor_overloads = async_constructor_overloads

def build(self) -> GeneratedRootClient:
def create_class_reference(class_name: str) -> AST.ClassReference:
Expand Down Expand Up @@ -1831,11 +1837,13 @@ def build_default_snippet_kwargs() -> List[typing.Tuple[str, AST.Expression]]:
class_reference=async_class_reference,
parameters=self._constructor_parameters,
init_parameters=self._async_init_parameters,
constructor_overloads=self._async_constructor_overloads,
),
sync_instantiations=sync_instantiations,
sync_client=RootClient(
class_reference=sync_class_reference,
parameters=self._constructor_parameters,
init_parameters=self._sync_init_parameters,
constructor_overloads=self._sync_constructor_overloads,
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,23 @@ def _create_wrapper_class_declaration(
) -> AST.ClassDeclaration:
params = root_client.init_parameters if root_client.init_parameters is not None else root_client.parameters

if root_client.constructor_overloads is not None:
# When the base class has overloaded __init__ (e.g. OAuth + token),
# mirror the overloads on the wrapper and use **kwargs pass-through
# so the call to super().__init__() satisfies mypy without type: ignore.
def write_kwargs_super_init(writer: AST.NodeWriter) -> None:
writer.write_line("super().__init__(**kwargs)")

return AST.ClassDeclaration(
name=class_name,
extends=[base_class_ref],
constructor=AST.ClassConstructor(
signature=AST.FunctionSignature(include_kwargs=True),
body=AST.CodeWriter(write_kwargs_super_init),
overloads=root_client.constructor_overloads,
),
)

named_params = [
AST.NamedFunctionParameter(
name=param.constructor_parameter_name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"type": "object",
"properties": {
"answer": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"finishReason": {
"oneOf": [
{
"$ref": "#/definitions/CompletionFullResponseFinishReason"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"CompletionFullResponseFinishReason": {
"type": "string",
"enum": [
"complete",
"length",
"error"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "string",
"enum": [
"complete",
"length",
"error"
],
"definitions": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"type": "object",
"properties": {
"query": {
"type": "string"
},
"stream": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"query"
],
"additionalProperties": false,
"definitions": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"type": "object",
"properties": {
"delta": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"tokens": {
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"type": "object",
"properties": {
"query": {
"type": "string"
},
"stream": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"query"
],
"additionalProperties": false,
"definitions": {}
}
Loading
Loading