feat(python): support a custom license_header in generated files - #17604
feat(python): support a custom license_header in generated files#17604devin-ai-integration[bot] wants to merge 2 commits into
Conversation
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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
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.
| return "\n".join( | ||
| line if line.startswith("#") else (f"# {line}" if line.strip() else "#") | ||
| for line in text.rstrip("\n").split("\n") | ||
| ) |
There was a problem hiding this comment.
🔵 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():
| 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, |
There was a problem hiding this comment.
🟡 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.
| 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") |
There was a problem hiding this comment.
🔵 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.
…telabel init Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
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 |
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_headercustom 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: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
codegen/file_header.pycentralizes header construction (Fern / whitelabel notice + optional license text);WriterImpland the hardcoded copy inProject._create_package_path_init_filesboth now use it, so__init__.pyfiles at every level get the header too.license_headerfromcustom_configthroughAbstractGenerator→Project→SourceFileImpl/NodeWriterImpl/ModuleManager.license_headertoSDKCustomConfig(required since it forbids extra fields).Testing
tests/codegen/test_file_header.pyexhaustive/license-headerseed fixture (combined withpackage_pathso the intermediate__init__.pypath is covered); diffing it againstexhaustive/package-pathshows only the added header lines.poetry run mypy .clean,poetry run pre-commit run -aclean, fullexhaustiveseed 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