diff --git a/datamodel_code_generator/model/msgspec.py b/datamodel_code_generator/model/msgspec.py index a3f966659..1614fb7ae 100644 --- a/datamodel_code_generator/model/msgspec.py +++ b/datamodel_code_generator/model/msgspec.py @@ -33,7 +33,7 @@ def _has_field_assignment(field: DataModelFieldBase) -> bool: - return bool(field.field) or not ( + return not ( field.required or (field.represented_default == 'None' and field.strip_default_none) ) @@ -48,7 +48,9 @@ def import_extender(cls: Type[DataModelFieldBaseT]) -> Type[DataModelFieldBaseT] @wraps(original_imports.fget) # type: ignore def new_imports(self: DataModelFieldBaseT) -> Tuple[Import, ...]: extra_imports = [] - if self.field: + field = self.field + # TODO: Improve field detection + if field and field.startswith('field('): extra_imports.append(IMPORT_MSGSPEC_FIELD) if self.field and 'lambda: convert' in self.field: extra_imports.append(IMPORT_MSGSPEC_CONVERT) @@ -177,6 +179,8 @@ def __str__(self) -> str: if self.default != UNDEFINED and self.default is not None: data['default'] = self.default + elif not self.required: + data['default'] = None if self.required: data = { diff --git a/datamodel_code_generator/model/template/msgspec.jinja2 b/datamodel_code_generator/model/template/msgspec.jinja2 index 6806975a3..03801adf7 100644 --- a/datamodel_code_generator/model/template/msgspec.jinja2 +++ b/datamodel_code_generator/model/template/msgspec.jinja2 @@ -18,12 +18,14 @@ class {{ class_name }}: {%- if not field.annotated and field.field %} {{ field.name }}: {{ field.type_hint }} = {{ field.field }} {%- else %} - {%- if field.annotated %} + {%- if field.annotated and not field.field %} {{ field.name }}: {{ field.annotated }} + {%- elif field.annotated and field.field %} + {{ field.name }}: {{ field.annotated }} = {{ field.field }} {%- else %} {{ field.name }}: {{ field.type_hint }} {%- endif %} - {%- if not field.required or field.data_type.is_optional or field.nullable + {%- if not field.field and (not field.required or field.data_type.is_optional or field.nullable) %} = {{ field.represented_default }} {%- endif -%} {%- endif %} diff --git a/tests/data/expected/main/main_msgspec_struct_snake_case/output.py b/tests/data/expected/main/main_msgspec_struct_snake_case/output.py new file mode 100644 index 000000000..021392912 --- /dev/null +++ b/tests/data/expected/main/main_msgspec_struct_snake_case/output.py @@ -0,0 +1,66 @@ +# generated by datamodel-codegen: +# filename: api_ordered_required_fields.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from msgspec import Meta, Struct, field +from typing_extensions import Annotated + + +class Pet(Struct): + id: int + name: str + before_tag: str = field(name='beforeTag') + tag: Optional[str] = None + + +Pets = List[Pet] + + +class User(Struct): + id: int + name: str + tag: Optional[str] = None + + +Users = List[User] + + +Id = str + + +Rules = List[str] + + +class Error(Struct): + code: int + message: str + + +class Api(Struct): + api_key: Optional[ + Annotated[str, Meta(description='To be used as a dataset parameter value')] + ] = field(name='apiKey', default=None) + api_version_number: Optional[ + Annotated[str, Meta(description='To be used as a version parameter value')] + ] = field(name='apiVersionNumber', default=None) + api_url: Optional[ + Annotated[str, Meta(description="The URL describing the dataset's fields")] + ] = field(name='apiUrl', default=None) + api_documentation_url: Optional[ + Annotated[str, Meta(description='A URL to the API console for each API')] + ] = field(name='apiDocumentationUrl', default=None) + + +Apis = List[Api] + + +class Event(Struct): + name: Optional[str] = None + + +class Result(Struct): + event: Optional[Event] = None diff --git a/tests/data/expected/main/main_with_aliases_msgspec/output.py b/tests/data/expected/main/main_with_aliases_msgspec/output.py index 448739c48..213ec585b 100644 --- a/tests/data/expected/main/main_with_aliases_msgspec/output.py +++ b/tests/data/expected/main/main_with_aliases_msgspec/output.py @@ -57,7 +57,7 @@ class Api(Struct): class Event(Struct): - name_: Optional[str] = field(name='name') + name_: Optional[str] = field(name='name', default=None) class Result(Struct): diff --git a/tests/data/openapi/api_ordered_required_fields.yaml b/tests/data/openapi/api_ordered_required_fields.yaml new file mode 100644 index 000000000..5588b7551 --- /dev/null +++ b/tests/data/openapi/api_ordered_required_fields.yaml @@ -0,0 +1,182 @@ +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 + - beforeTag + properties: + id: + type: integer + format: int64 + default: 1 + name: + type: string + beforeTag: + 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' diff --git a/tests/test_main.py b/tests/test_main.py index 42c39cbe8..9670478c0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6139,6 +6139,33 @@ def test_main_msgspec_struct(): ) +@freeze_time('2019-07-26') +def test_main_msgspec_struct_snake_case(): + with TemporaryDirectory() as output_dir: + output_file: Path = Path(output_dir) / 'output.py' + return_code: Exit = main( + [ + '--input', + str(OPEN_API_DATA_PATH / 'api_ordered_required_fields.yaml'), + '--output', + str(output_file), + # min msgspec python version is 3.8 + '--target-python-version', + '3.8', + '--snake-case-field', + '--output-model-type', + 'msgspec.Struct', + ] + ) + assert return_code == Exit.OK + assert ( + output_file.read_text() + == ( + EXPECTED_MAIN_PATH / 'main_msgspec_struct_snake_case' / 'output.py' + ).read_text() + ) + + @freeze_time('2019-07-26') @pytest.mark.skipif( black.__version__.split('.')[0] == '19',