Skip to content

Commit d81401b

Browse files
feat(python): add Python 3.11+ StrEnum compatibility (#11105)
* feat(python): add Python 3.11+ StrEnum compatibility Generated enums now use StrEnum for Python >= 3.11 and the (str, Enum) mixin for older versions, fixing compatibility issues with Python 3.11's stricter enum mixin handling. - Add fern_enum.py utility with version-aware enum base class - Update pydantic CoreUtilities to copy fern_enum.py - Update SDK CoreUtilities to copy fern_enum.py - Update enum generator to use FernEnum base class Co-Authored-By: [email protected] <[email protected]> * chore(python): address PR feedback - rename to StrEnum, rename file to enum.py, add comments, don't re-export - Rename FernEnum to StrEnum (use native StrEnum for Python >= 3.11) - Rename fern_enum.py to enum.py - Add docstring explaining the version-aware enum base class - Don't re-export StrEnum from __init__.py (internal use only) Co-Authored-By: [email protected] <[email protected]> * chore(python): add StrEnum support to fastapi and pydantic generators - Update fastapi versions.yml with changelog entry for v1.12.2 - Update pydantic versions.yml with changelog entry for v1.10.2 - Add generated enum.py to pydantic seed output Co-Authored-By: [email protected] <[email protected]> * fix(python): change changelog type from feat to fix for fastapi and pydantic The semver validator requires patch version bumps to use 'fix' type, not 'feat'. This change is semantically correct since we're fixing Python 3.11 compatibility. Co-Authored-By: [email protected] <[email protected]> * chore(python): use module import pattern for StrEnum to avoid naming conflicts Change import from 'from ..core.enum import StrEnum' to 'from ..core import enum' so generated enums use 'enum.StrEnum' instead of 'StrEnum' directly. This avoids potential naming conflicts with user-defined classes named StrEnum. Co-Authored-By: [email protected] <[email protected]> * chore(python): only copy enum.py when generating actual enum classes Co-Authored-By: [email protected] <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 774f6e2 commit d81401b

File tree

23 files changed

+191
-31
lines changed

23 files changed

+191
-31
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Provides a StrEnum base class that works across Python versions.
3+
4+
For Python >= 3.11, this re-exports the standard library enum.StrEnum.
5+
For older Python versions, this defines a compatible StrEnum using the
6+
(str, Enum) mixin pattern so that generated SDKs can use a single base
7+
class in all supported Python versions.
8+
"""
9+
10+
import enum
11+
import sys
12+
13+
if sys.version_info >= (3, 11):
14+
from enum import StrEnum
15+
else:
16+
17+
class StrEnum(str, enum.Enum):
18+
pass
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Provides a StrEnum base class that works across Python versions.
3+
4+
For Python >= 3.11, this re-exports the standard library enum.StrEnum.
5+
For older Python versions, this defines a compatible StrEnum using the
6+
(str, Enum) mixin pattern so that generated SDKs can use a single base
7+
class in all supported Python versions.
8+
"""
9+
10+
import enum
11+
import sys
12+
13+
if sys.version_info >= (3, 11):
14+
from enum import StrEnum
15+
else:
16+
17+
class StrEnum(str, enum.Enum):
18+
pass

generators/python/fastapi/versions.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
22
# For unreleased changes, use unreleased.yml
3+
- version: 1.12.2
4+
changelogEntry:
5+
- summary: |
6+
Fix Python 3.11+ enum compatibility by using StrEnum for Python >= 3.11 and the (str, Enum)
7+
mixin for older versions. This resolves compatibility issues with Python 3.11's stricter
8+
enum mixin handling.
9+
type: fix
10+
createdAt: "2025-12-08"
11+
irVersion: 61
12+
313
- version: 1.12.1
414
changelogEntry:
515
- summary: Fix discriminated union Field(discriminator=...) and UnionMetadata(discriminant=...) to use Python field names instead of JSON aliases for Pydantic v2 compatibility.

generators/python/pydantic/versions.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
22
# For unreleased changes, use unreleased.yml
3+
- version: 1.10.2
4+
changelogEntry:
5+
- summary: |
6+
Fix Python 3.11+ enum compatibility by using StrEnum for Python >= 3.11 and the (str, Enum)
7+
mixin for older versions. This resolves compatibility issues with Python 3.11's stricter
8+
enum mixin handling.
9+
type: fix
10+
createdAt: "2025-12-08"
11+
irVersion: 61
12+
313
- version: 1.10.1
414
changelogEntry:
515
- summary: Fix discriminated union Field(discriminator=...) and UnionMetadata(discriminant=...) to use Python field names instead of JSON aliases for Pydantic v2 compatibility.

generators/python/sdk/versions.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
22
# For unreleased changes, use unreleased.yml
3+
- version: 4.44.0
4+
changelogEntry:
5+
- summary: |
6+
Add Python 3.11+ StrEnum compatibility. Generated enums now use StrEnum for Python >= 3.11
7+
and the (str, Enum) mixin for older versions, fixing compatibility issues with Python 3.11's
8+
stricter enum mixin handling.
9+
type: feat
10+
createdAt: "2025-12-08"
11+
irVersion: 61
12+
313
- version: 4.43.0
414
changelogEntry:
515
- summary: |

generators/python/src/fern_python/generators/core_utilities/core_utilities.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ def copy_to_project(self, *, project: Project) -> None:
4242
exports={"serialize_datetime"},
4343
)
4444

45+
self._copy_file_to_project(
46+
project=project,
47+
relative_filepath_on_disk="enum.py",
48+
filepath_in_project=Filepath(
49+
directories=self.filepath,
50+
file=Filepath.FilepathPart(module_name="enum"),
51+
),
52+
exports=set(),
53+
)
54+
4555
utilities_path = (
4656
"with_pydantic_v1_on_v2/with_aliases/pydantic_utilities.py"
4757
if is_v1_on_v2 and self._use_pydantic_field_aliases
@@ -119,6 +129,12 @@ def get_serialize_datetime(self) -> AST.Reference:
119129
),
120130
)
121131

132+
def get_fern_enum(self) -> AST.ClassReference:
133+
return AST.ClassReference(
134+
qualified_name_excluding_import=("StrEnum",),
135+
import_=AST.ReferenceImport(module=AST.Module.local(*self._module_path), named_import="enum"),
136+
)
137+
122138
def get_field_metadata(self) -> FieldMetadata:
123139
field_metadata_reference = AST.ClassReference(
124140
qualified_name_excluding_import=(),

generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,7 @@ def generate(self) -> None:
6464
enum_class = AST.ClassDeclaration(
6565
name=self._class_name,
6666
extends=[
67-
AST.ClassReference(
68-
qualified_name_excluding_import=("str",),
69-
),
70-
AST.ClassReference(
71-
import_=AST.ReferenceImport(module=AST.Module.built_in(("enum",))),
72-
qualified_name_excluding_import=("Enum",),
73-
),
67+
self._context.core_utilities.get_fern_enum(),
7468
],
7569
docstring=AST.Docstring(self._docs) if self._docs is not None else None,
7670
snippet=self._snippet,

generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(
4242
self._should_generate_websocket_clients = custom_config.should_generate_websocket_clients
4343
self._exclude_types_from_init_exports = custom_config.exclude_types_from_init_exports
4444
self._custom_pager_base_name = self._sanitize_pager_name(custom_config.custom_pager_name or "CustomPager")
45+
self._use_str_enums = custom_config.pydantic_config.use_str_enums
4546

4647
def copy_to_project(self, *, project: Project) -> None:
4748
self._copy_file_to_project(
@@ -53,6 +54,17 @@ def copy_to_project(self, *, project: Project) -> None:
5354
),
5455
exports={"serialize_datetime"} if not self._exclude_types_from_init_exports else set(),
5556
)
57+
# Only copy enum.py when generating actual enum classes (not string literals)
58+
if not self._use_str_enums:
59+
self._copy_file_to_project(
60+
project=project,
61+
relative_filepath_on_disk="enum.py",
62+
filepath_in_project=Filepath(
63+
directories=self.filepath,
64+
file=Filepath.FilepathPart(module_name="enum"),
65+
),
66+
exports=set(),
67+
)
5668
self._copy_file_to_project(
5769
project=project,
5870
relative_filepath_on_disk="api_error.py",

seed/pydantic/enum/src/seed/enum/core/enum.py

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/python-sdk/enum/real-enum-forward-compat/src/seed/core/enum.py

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)