Skip to content

fix(api): initialize compatibility exports safely - #1789

Merged
hassiebp merged 1 commit into
mainfrom
hassiebbot/fix-python-score-compat-all-export
Jul 29, 2026
Merged

fix(api): initialize compatibility exports safely#1789
hassiebp merged 1 commit into
mainfrom
hassiebbot/fix-python-score-compat-all-export

Conversation

@hassiebp

@hassiebp hassiebp commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes the score-create compatibility postprocessor when Fern generates a package __init__.py without an existing __all__.

The postprocessor previously always emitted:

__all__ = [*__all__, ...]

That is invalid when the generated module has no prior __all__, causing the generated API PR's mypy failure and a potential import-time NameError. The postprocessor now inspects the module AST and either extends an existing assignment or creates a fresh export list.

CI failure: https://github.com/langfuse/langfuse-python/actions/runs/30450770977/job/90572011905?pr=1788
Generated API PR: #1788
Linear: https://linear.app/clickhouse/issue/LFE-10397

After this PR merges, the core SDK-generation workflow should be rerun from fresh Fern output to replace/update #1788.

Impacted packages

  • langfuse generated API compatibility tooling

Verification

  • Regression reproduced before the fix: 1 failed, 2 passed
  • uv run --frozen pytest tests/unit/test_patch_generated_score_compat.py3 passed in 0.12s
  • uv run --frozen ruff check .All checks passed!
  • uv run --frozen ruff format --check .617 files already formatted
  • uv run --frozen mypy langfuse --no-error-summary — exit code 0

Full E2E and live-provider suites were not run because this change only affects the deterministic generation postprocessor and its fixture; it does not change runtime SDK behavior until API generation runs.

Greptile Summary

Safely initializes compatibility exports when generated modules do not already define __all__.

  • Parses generated package initializers to choose between creating and extending __all__.
  • Updates the regression fixture and assertions for Fern output without an existing export list.

Confidence Score: 3/5

The previously generated broken compatibility output must be repaired before this change is safe to merge.

The new AST-based initialization works on fresh Fern output, but the existing marker guard skips the logic when rerunning over output produced by the previous postprocessor, preserving the import-time NameError.

Files Needing Attention: scripts/patch_generated_score_compat.py

Prompt To Fix All With AI
### Issue 1
scripts/patch_generated_score_compat.py:145
**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.

Reviews (1): Last reviewed commit: "fix(api): initialize compatibility expor..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@hassiebp
hassiebp marked this pull request as ready for review July 29, 2026 12:28
@hassiebp
hassiebp merged commit 370039d into main Jul 29, 2026
22 checks passed
@hassiebp
hassiebp deleted the hassiebbot/fix-python-score-compat-all-export branch July 29, 2026 12:28
@github-actions

Copy link
Copy Markdown

@claude review

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8dfba165f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

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 👍 / 👎.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — narrow, correct fix to the codegen postprocessor's AST-based __all__ handling.

What was reviewed: the ast-based detection of an existing __all__ assignment (both Assign and AnnAssign forms), the fallback path when no __all__ exists yet, and idempotency on a second run. Manually reproduced the fixture and confirmed the generated __init__.py is valid Python and the postprocessor is a no-op on rerun.

Extended reasoning...

Overview

This PR touches only scripts/patch_generated_score_compat.py (a build-time codegen postprocessor, not runtime SDK code) and its unit test. It fixes a bug where the postprocessor unconditionally emitted __all__ = [*__all__, ...] even when the generated module had no prior __all__ definition, which produced invalid/broken generated code (mypy failure, potential NameError). The fix uses ast.parse to detect whether the module already defines __all__ (via Assign or AnnAssign) and branches accordingly.

Security risks

None. This script only runs during local/CI code generation against Fern-generated fixture files; it does not process user input or run in production request paths.

Level of scrutiny

Low-to-moderate is appropriate: this is internal tooling with narrow blast radius (only affects the generated langfuse/api compatibility shims), and it ships with direct test coverage for the exact regression being fixed. I manually re-ran the fixture-based postprocessor logic (network access for uv/pytest was unavailable in this sandbox) and confirmed: (1) the AST detection correctly falls back to __all__ = [...] when no prior assignment exists, (2) the output is syntactically valid, and (3) a second run is a correct no-op via the existing marker-based idempotency check.

Other factors

The diff is small, mechanical, and matches its stated purpose exactly — no scope creep. Test additions directly assert the new behavior (__all__ = [ present, [*__all__ absent) and the fixture was updated to reflect Fern's actual no-__all__ output shape. No outstanding review comments in the timeline beyond an automated @claude review trigger.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant