Skip to content

feat(python): support a custom license_header in generated files - #17604

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1788273809-python-license-header
Open

feat(python): support a custom license_header in generated files#17604
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1788273809-python-license-header

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Description

Requested by a customer who must ship a license notice in every file of their published SDK repo. Today the only knob is whitelabel, which just swaps the Fern notice text; there is no way to add a header.

Adds a license_header custom config to the Python SDK generator. The configured text is emitted as a comment block above the existing auto-generated notice in every generated Python file:

config:
  license_header: |
    Copyright 2024 Acme, Inc.
    Licensed under the Apache License, Version 2.0.
# Copyright 2024 Acme, Inc.
# Licensed under the Apache License, Version 2.0.

# This file was auto-generated by Fern from our API Definition.

Lines that already start with # are passed through, so pre-commented text works too. Output is byte-identical to before when the option is omitted.

Changes Made

  • New codegen/file_header.py centralizes header construction (Fern / whitelabel notice + optional license text); WriterImpl and the hardcoded copy in Project._create_package_path_init_files both now use it, so __init__.py files at every level get the header too.
  • Threaded license_header from custom_config through AbstractGeneratorProjectSourceFileImpl/NodeWriterImpl/ModuleManager.
  • Added license_header to SDKCustomConfig (required since it forbids extra fields).

Testing

  • Unit tests added/updated — tests/codegen/test_file_header.py
  • Manual testing completed — new exhaustive/license-header seed fixture (combined with package_path so the intermediate __init__.py path is covered); diffing it against exhaustive/package-path shows only the added header lines. poetry run mypy . clean, poetry run pre-commit run -a clean, full exhaustive seed run 36/36 passing.

Link to Devin session: https://app.devin.ai/sessions/37b1ba4f0fc247f8913d7d4e0cb21551
Open in Devin Desktop: https://app.devin.ai/desktop/session/37b1ba4f0fc247f8913d7d4e0cb21551?variant=devin


Devin Review

Emit a configurable license notice above the auto-generated header in generated Python files.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot 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.

AI Review Summary

Adds a license_header custom config threaded through the Python generator's codegen layer, with a shared get_file_header helper. Implementation is straightforward; two issues worth addressing: the license header is applied to snippet writers (which likely shouldn't carry it), and the _as_comment handling of \r\n / trailing whitespace is slightly fragile. Also the config is read via raw dict access in abstract_generator rather than the typed SDKCustomConfig.

  • 🟡 1 warning(s)
  • 🔵 2 suggestion(s)

To request another review, comment /ai-review on this pull request.

Comment on lines +21 to +24
return "\n".join(
line if line.startswith("#") else (f"# {line}" if line.strip() else "#")
for line in text.rstrip("\n").split("\n")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

split("\n") leaves a trailing \r on CRLF input, producing # foo\r lines. Also line.startswith("#") misses indented comments (e.g. # foo), which is probably fine but worth being deliberate about. Consider splitlines():

Suggested change
return "\n".join(
line if line.startswith("#") else (f"# {line}" if line.strip() else "#")
for line in text.rstrip("\n").split("\n")
)
return "\n".join(
line if line.lstrip().startswith("#") else (f"# {line}" if line.strip() else "#")
for line in text.rstrip().splitlines()
)

should_include_header: bool = True,
reference_resolver: ReferenceResolver,
whitelabel: bool = False,
license_header: Optional[str] = None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

NodeWriterImpl is also used for snippet generation (should_format_as_snippet / should_include_header=False paths, and Project snippet writers). Confirm that generated code snippets in snippet.json / reference.md don't end up prefixed with the customer's license text — headers there would be noise. If snippets already pass should_include_header=False, ignore; the to_str guard covers it.

Comment on lines +151 to +153
license_header = None
if generator_config.custom_config is not None and "license_header" in generator_config.custom_config:
license_header = generator_config.custom_config.get("license_header")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

Raw dict access bypasses the typed SDKCustomConfig.license_header you just added, so a non-string value (e.g. a list or int in YAML) flows straight into _as_comment and blows up with an unclear AttributeError. The import_paths block above has the same smell, but for a new field consider validating the type here (or reading from the parsed custom config) so a misconfigured license_header yields a sensible error.

Note also this is generic to all generators (abstract_generator), while the config field is only declared on SDKCustomConfig — for pydantic-based generators with extra="forbid", other generators' configs will reject license_header before this code runs.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Devin Review

Comment thread generators/python/src/fern_python/codegen/project.py
Comment thread generators/python/src/fern_python/codegen/project.py Outdated
…telabel init

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-09-01T04:06:34Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
python-sdk square 149s (n=5) 254s (n=5) 141s -8s (-5.4%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-09-01T04:06:34Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-09-01 14:59 UTC

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