diff --git a/docs/cli-reference/index.md b/docs/cli-reference/index.md index 757997b9e..bdc4c3960 100644 --- a/docs/cli-reference/index.md +++ b/docs/cli-reference/index.md @@ -12,7 +12,7 @@ This documentation is auto-generated from test cases. | 🔧 [Typing Customization](typing-customization.md) | 16 | Type annotation and import behavior | | 🏷️ [Field Customization](field-customization.md) | 20 | Field naming and docstring behavior | | 🏗️ [Model Customization](model-customization.md) | 26 | Model generation behavior | -| 🎨 [Template Customization](template-customization.md) | 15 | Output formatting and custom rendering | +| 🎨 [Template Customization](template-customization.md) | 16 | Output formatting and custom rendering | | 📘 [OpenAPI-only Options](openapi-only-options.md) | 5 | OpenAPI-specific features | | ⚙️ [General Options](general-options.md) | 13 | Utilities and meta options | | 📝 [Utility Options](utility-options.md) | 5 | Help, version, debug options | @@ -60,6 +60,7 @@ This documentation is auto-generated from test cases. ### E {#e} - [`--empty-enum-field-name`](field-customization.md#empty-enum-field-name) +- [`--enable-command-header`](template-customization.md#enable-command-header) - [`--enable-faux-immutability`](model-customization.md#enable-faux-immutability) - [`--enable-version-header`](template-customization.md#enable-version-header) - [`--encoding`](base-options.md#encoding) diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index 3624045ff..f01ee3eba 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -111,6 +111,7 @@ datamodel-codegen [OPTIONS] | [`--custom-template-dir`](template-customization.md#custom-template-dir) | Use custom Jinja2 templates for model generation. | | [`--disable-appending-item-suffix`](template-customization.md#disable-appending-item-suffix) | Disable appending 'Item' suffix to array item types. | | [`--disable-timestamp`](template-customization.md#disable-timestamp) | Disable timestamp in generated file header for reproducible output. | +| [`--enable-command-header`](template-customization.md#enable-command-header) | Include command-line options in file header for reproducibility. | | [`--enable-version-header`](template-customization.md#enable-version-header) | Include tool version information in file header. | | [`--extra-template-data`](template-customization.md#extra-template-data) | Pass custom template variables from JSON file for code generation. | | [`--formatters`](template-customization.md#formatters) | Specify code formatters to apply to generated output. | @@ -187,6 +188,7 @@ All options sorted alphabetically: - [`--disable-timestamp`](template-customization.md#disable-timestamp) - Disable timestamp in generated file header for reproducible ... - [`--disable-warnings`](general-options.md#disable-warnings) - Suppress warning messages during code generation. - [`--empty-enum-field-name`](field-customization.md#empty-enum-field-name) - Name for empty string enum field values. +- [`--enable-command-header`](template-customization.md#enable-command-header) - Include command-line options in file header for reproducibil... - [`--enable-faux-immutability`](model-customization.md#enable-faux-immutability) - Enable faux immutability in Pydantic v1 models (allow_mutati... - [`--enable-version-header`](template-customization.md#enable-version-header) - Include tool version information in file header. - [`--encoding`](base-options.md#encoding) - Specify character encoding for input and output files. diff --git a/docs/cli-reference/template-customization.md b/docs/cli-reference/template-customization.md index 5d564a908..1796b65bb 100644 --- a/docs/cli-reference/template-customization.md +++ b/docs/cli-reference/template-customization.md @@ -12,6 +12,7 @@ | [`--custom-template-dir`](#custom-template-dir) | Use custom Jinja2 templates for model generation. | | [`--disable-appending-item-suffix`](#disable-appending-item-suffix) | Disable appending 'Item' suffix to array item types. | | [`--disable-timestamp`](#disable-timestamp) | Disable timestamp in generated file header for reproducible ... | +| [`--enable-command-header`](#enable-command-header) | Include command-line options in file header for reproducibil... | | [`--enable-version-header`](#enable-version-header) | Include tool version information in file header. | | [`--extra-template-data`](#extra-template-data) | Pass custom template variables from JSON file for code gener... | | [`--formatters`](#formatters) | Specify code formatters to apply to generated output. | @@ -1286,6 +1287,282 @@ The `--disable-timestamp` flag configures the code generation behavior. --- +## `--enable-command-header` {#enable-command-header} + +Include command-line options in file header for reproducibility. + +The `--enable-command-header` flag adds the full command-line used to generate +the file to the header, making it easy to reproduce the generation. + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --enable-command-header # (1)! + ``` + + 1. :material-arrow-left: `--enable-command-header` - the option documented here + +??? example "Input Schema" + + ```yaml + openapi: "3.0.0" + info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT + servers: + - url: http://petstore.swagger.io/v1 + paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + default: 1 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Users: + type: array + items: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Id: + type: string + Rules: + type: array + items: + type: string + Error: + description: error result + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + apis: + type: array + items: + type: object + properties: + apiKey: + type: string + description: To be used as a dataset parameter value + apiVersionNumber: + type: string + description: To be used as a version parameter value + apiUrl: + type: string + format: uri + description: "The URL describing the dataset's fields" + apiDocumentationUrl: + type: string + format: uri + description: A URL to the API console for each API + Event: + type: object + description: Event object + properties: + name: + type: string + Result: + type: object + properties: + event: + $ref: '#/components/schemas/Event' + ``` + +??? example "Output" + + ```python + # generated by datamodel-codegen: + # filename: api.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + # command: datamodel-codegen [COMMAND] + + from __future__ import annotations + + from typing import List, Optional + + from pydantic import AnyUrl, BaseModel, Field + + + class Pet(BaseModel): + id: int + name: str + tag: Optional[str] = None + + + class Pets(BaseModel): + __root__: List[Pet] + + + class User(BaseModel): + id: int + name: str + tag: Optional[str] = None + + + class Users(BaseModel): + __root__: List[User] + + + class Id(BaseModel): + __root__: str + + + class Rules(BaseModel): + __root__: List[str] + + + class Error(BaseModel): + code: int + message: str + + + class Api(BaseModel): + apiKey: Optional[str] = Field( + None, description='To be used as a dataset parameter value' + ) + apiVersionNumber: Optional[str] = Field( + None, description='To be used as a version parameter value' + ) + apiUrl: Optional[AnyUrl] = Field( + None, description="The URL describing the dataset's fields" + ) + apiDocumentationUrl: Optional[AnyUrl] = Field( + None, description='A URL to the API console for each API' + ) + + + class Apis(BaseModel): + __root__: List[Api] + + + class Event(BaseModel): + name: Optional[str] = None + + + class Result(BaseModel): + event: Optional[Event] = None + ``` + +--- + ## `--enable-version-header` {#enable-version-header} Include tool version information in file header. diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index 15a9a9b3e..5d2bcba3d 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -389,6 +389,8 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915 aliases: Mapping[str, str] | None = None, disable_timestamp: bool = False, enable_version_header: bool = False, + enable_command_header: bool = False, + command_line: str | None = None, allow_population_by_field_name: bool = False, allow_extra_fields: bool = False, extra_fields: str | None = None, @@ -765,6 +767,9 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]: header += f"\n# timestamp: {timestamp}" if enable_version_header: header += f"\n# version: {get_version()}" + if enable_command_header and command_line: + safe_command_line = command_line.replace("\n", " ").replace("\r", " ") + header += f"\n# command: {safe_command_line}" file: IO[Any] | None for path, (body, future_imports, filename) in modules.items(): diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index 287a6dfd9..51beedd52 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -4,6 +4,7 @@ import difflib import json +import shlex import signal import sys import tempfile @@ -385,6 +386,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict aliases: Optional[TextIOBase] = None # noqa: UP045 disable_timestamp: bool = False enable_version_header: bool = False + enable_command_header: bool = False allow_population_by_field_name: bool = False allow_extra_fields: bool = False extra_fields: Optional[str] = None # noqa: UP045 @@ -662,6 +664,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917 output: Path | None, extra_template_data: dict[str, Any] | None, aliases: dict[str, str] | None, + command_line: str | None, custom_formatters_kwargs: dict[str, str] | None, settings_path: Path | None = None, ) -> None: @@ -683,6 +686,8 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917 aliases=aliases, disable_timestamp=config.disable_timestamp, enable_version_header=config.enable_version_header, + enable_command_header=config.enable_command_header, + command_line=command_line, allow_population_by_field_name=config.allow_population_by_field_name, allow_extra_fields=config.allow_extra_fields, extra_fields=config.extra_fields, @@ -946,6 +951,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912, output=generate_output, extra_template_data=extra_template_data, aliases=aliases, + command_line=shlex.join(["datamodel-codegen", *args]) if config.enable_command_header else None, custom_formatters_kwargs=custom_formatters_kwargs, settings_path=config.output if config.check else None, ) diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py index 21f18a875..fedd94d94 100644 --- a/src/datamodel_code_generator/arguments.py +++ b/src/datamodel_code_generator/arguments.py @@ -198,6 +198,12 @@ def start_section(self, heading: str | None) -> None: action="store_true", default=None, ) +model_options.add_argument( + "--enable-command-header", + help="Enable command-line options on file headers for reproducibility", + action="store_true", + default=None, +) extra_fields_model_options.add_argument( "--extra-fields", help="Set the generated models to allow, forbid, or ignore extra fields.", diff --git a/src/datamodel_code_generator/cli_options.py b/src/datamodel_code_generator/cli_options.py index 0f6274355..6ada759ec 100644 --- a/src/datamodel_code_generator/cli_options.py +++ b/src/datamodel_code_generator/cli_options.py @@ -172,6 +172,7 @@ class CLIOptionMeta: "--treat-dot-as-module": CLIOptionMeta(name="--treat-dot-as-module", category=OptionCategory.TEMPLATE), "--disable-timestamp": CLIOptionMeta(name="--disable-timestamp", category=OptionCategory.TEMPLATE), "--enable-version-header": CLIOptionMeta(name="--enable-version-header", category=OptionCategory.TEMPLATE), + "--enable-command-header": CLIOptionMeta(name="--enable-command-header", category=OptionCategory.TEMPLATE), "--formatters": CLIOptionMeta(name="--formatters", category=OptionCategory.TEMPLATE), "--custom-formatters": CLIOptionMeta(name="--custom-formatters", category=OptionCategory.TEMPLATE), "--custom-formatters-kwargs": CLIOptionMeta(name="--custom-formatters-kwargs", category=OptionCategory.TEMPLATE), diff --git a/src/datamodel_code_generator/watch.py b/src/datamodel_code_generator/watch.py index d6c9fa742..91ed70b18 100644 --- a/src/datamodel_code_generator/watch.py +++ b/src/datamodel_code_generator/watch.py @@ -53,6 +53,7 @@ def watch_and_regenerate( output=config.output, extra_template_data=extra_template_data, aliases=aliases, + command_line=None, custom_formatters_kwargs=custom_formatters_kwargs, ) print("Done.") # noqa: T201 diff --git a/tests/data/expected/main/openapi/enable_command_header.py b/tests/data/expected/main/openapi/enable_command_header.py new file mode 100644 index 000000000..f7dfd5202 --- /dev/null +++ b/tests/data/expected/main/openapi/enable_command_header.py @@ -0,0 +1,70 @@ +# generated by datamodel-codegen: +# filename: api.yaml +# timestamp: 2019-07-26T00:00:00+00:00 +# command: datamodel-codegen [COMMAND] + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import AnyUrl, BaseModel, Field + + +class Pet(BaseModel): + id: int + name: str + tag: Optional[str] = None + + +class Pets(BaseModel): + __root__: List[Pet] + + +class User(BaseModel): + id: int + name: str + tag: Optional[str] = None + + +class Users(BaseModel): + __root__: List[User] + + +class Id(BaseModel): + __root__: str + + +class Rules(BaseModel): + __root__: List[str] + + +class Error(BaseModel): + code: int + message: str + + +class Api(BaseModel): + apiKey: Optional[str] = Field( + None, description='To be used as a dataset parameter value' + ) + apiVersionNumber: Optional[str] = Field( + None, description='To be used as a version parameter value' + ) + apiUrl: Optional[AnyUrl] = Field( + None, description="The URL describing the dataset's fields" + ) + apiDocumentationUrl: Optional[AnyUrl] = Field( + None, description='A URL to the API console for each API' + ) + + +class Apis(BaseModel): + __root__: List[Api] + + +class Event(BaseModel): + name: Optional[str] = None + + +class Result(BaseModel): + event: Optional[Event] = None diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 67d419379..6aae90f1b 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -5,6 +5,7 @@ import contextlib import json import platform +import re import warnings from collections import defaultdict from pathlib import Path @@ -913,6 +914,34 @@ def test_enable_version_header(output_file: Path) -> None: ) +@pytest.mark.cli_doc( + options=["--enable-command-header"], + input_schema="openapi/api.yaml", + cli_args=["--enable-command-header"], + golden_output="openapi/enable_command_header.py", +) +def test_enable_command_header(output_file: Path) -> None: + """Include command-line options in file header for reproducibility. + + The `--enable-command-header` flag adds the full command-line used to generate + the file to the header, making it easy to reproduce the generation. + """ + + def normalize_command(s: str) -> str: + # Replace the actual command line with a placeholder for consistent testing + return re.sub(r"# command: datamodel-codegen .*", "# command: datamodel-codegen [COMMAND]", s) + + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "api.yaml", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="enable_command_header.py", + extra_args=["--enable-command-header"], + transform=normalize_command, + ) + + @pytest.mark.parametrize( ("output_model", "expected_output"), [