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
22 changes: 19 additions & 3 deletions scripts/patch_generated_score_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations

import argparse
import ast
from pathlib import Path

CLIENT_IMPORTS = """\
Expand Down Expand Up @@ -141,10 +142,25 @@ def _append_type_exports(path: Path, imports_by_type: dict[str, str]) -> None:
for type_name, module_name in imports_by_type.items()
)
names = ", ".join(repr(type_name) for type_name in TYPE_NAMES)
path.write_text(
f"{contents.rstrip()}\n\n{exports_marker}\n{imports}\n"
f"__all__ = [*__all__, {names}]\n"
defines_all = any(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Previously patched exports remain broken

When this version runs on output patched by the previous postprocessor, the existing compatibility marker returns before this new detection executes, leaving __all__ = [*__all__, ...] unchanged in modules without an earlier binding and causing import-time NameError. The postprocessor supports reruns, so it needs to repair the old marked output rather than treating every marker as proof that no patch is needed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/patch_generated_score_compat.py
Line: 145

Comment:
**Previously patched exports remain broken**

When this version runs on output patched by the previous postprocessor, the existing compatibility marker returns before this new detection executes, leaving `__all__ = [*__all__, ...]` unchanged in modules without an earlier binding and causing import-time `NameError`. The postprocessor supports reruns, so it needs to repair the old marked output rather than treating every marker as proof that no patch is needed.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

(
isinstance(node, ast.Assign)
and any(
isinstance(target, ast.Name) and target.id == "__all__"
for target in node.targets
)
)
or (
isinstance(node, ast.AnnAssign)
and isinstance(node.target, ast.Name)
and node.target.id == "__all__"
)
Comment on lines +153 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat annotation-only all declarations as undefined

When a generated initializer contains an annotation without a value, such as __all__: list[str], this AnnAssign branch sets defines_all even though Python has not bound __all__. The postprocessor then emits __all__ = [*__all__, ...], reproducing the import-time NameError this change is intended to prevent. Require node.value is not None before treating an annotated assignment as initialized.

Useful? React with 👍 / 👎.

for node in ast.parse(contents).body
)
exports = (
f"__all__ = [*__all__, {names}]" if defines_all else f"__all__ = [{names}]"
)
path.write_text(f"{contents.rstrip()}\n\n{exports_marker}\n{imports}\n{exports}\n")


def _create_type_aliases(types_dir: Path) -> None:
Expand Down
7 changes: 6 additions & 1 deletion tests/unit/test_patch_generated_score_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def _write_generated_fixture(api_root: Path, *, canonical_has_create: bool) -> N
" async def delete(self):\n"
" pass\n"
)
(legacy_dir / "__init__.py").write_text("__all__ = []\n")
(legacy_dir / "__init__.py").write_text(
"# This file was auto-generated by Fern from our API Definition.\n"
)
(legacy_dir.parent / "__init__.py").write_text("__all__ = []\n")
(legacy_types_dir / "__init__.py").write_text("__all__ = []\n")

Expand Down Expand Up @@ -78,6 +80,9 @@ def test_postprocessor_adds_delegating_methods_and_type_aliases(tmp_path: Path)
"CreateScoreResponse"
in (api_root / "legacy" / "score_v1" / "__init__.py").read_text()
)
legacy_package = (api_root / "legacy" / "score_v1" / "__init__.py").read_text()
assert "__all__ = [" in legacy_package
assert "[*__all__" not in legacy_package
assert "CreateScoreResponse" in (api_root / "legacy" / "__init__.py").read_text()

second_result = _run_postprocessor(api_root)
Expand Down