Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 43 additions & 5 deletions src/datamodel_code_generator/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from __future__ import annotations

import re
import textwrap
from abc import ABC, abstractmethod
from collections import defaultdict
from copy import deepcopy
Expand Down Expand Up @@ -66,6 +67,40 @@ def escape_docstring(value: str | None) -> str | None:
return value.replace("\\", "\\\\").replace('"""', '\\"\\"\\"')


def format_docstring(value: str | None, indent_spaces: int = 0) -> str:
"""Format *value* as a docstring as per PEP 257.

PEP 257 recommends that docstrings that can fit on one line should be formatted on a
single line, for consistency and readability. Leading and trailing whitespace will
be removed, and if after this the value is falsy, an empty string is returned. It is
assumed that the opening triple-quotes are indented appropriately in the template.
If it's a multi-line docstring, each line including the closing triple-quotes will
be indented as per indent_spaces.

Args:
value: docstring text
indent_spaces: Spaces to indent for all lines after the opening triple-quotes

Returns:
Empty string when `value` is falsy; otherwise the docstring block.
"""
value = (value or "").strip()

if not value:
return ""

escaped = escape_docstring(value)

if len(value.splitlines()) == 1:
if escaped.endswith('"'):
escaped = f"{escaped} "

Comment thread
coderabbitai[bot] marked this conversation as resolved.
return f'"""{escaped}"""'

indent_text = textwrap.indent(f'{escaped}\n"""', max(indent_spaces, 0) * " ")
return f'"""\n{indent_text}'


ALL_MODEL: str = "#all#"
GENERIC_BASE_CLASS_PATH: str = "#/__datamodel_code_generator__/generic_base_class__"
GENERIC_BASE_CLASS_NAME: str = "__generic_base_class__"
Expand Down Expand Up @@ -354,9 +389,10 @@ def inline_field_docstring(self) -> str | None:
"""Get the inline docstring for this field if single-line."""
if self.use_inline_field_description:
description = self.extras.get("description", None)
if description is not None and "\n" not in description:
escaped = escape_docstring(description)
return f'"""{escaped}"""'
docstring = format_docstring(description, 0)
if docstring:
return docstring

return None

@property
Expand Down Expand Up @@ -452,7 +488,8 @@ def _get_environment(template_subdir: Path, custom_template_dir: Path | None) ->
loader=loader,
autoescape=select_autoescape(["html", "xml"]),
)
env.filters["escape_docstring"] = escape_docstring
env.filters["escape_docstring"] = escape_docstring # For old custom templates
env.filters["format_docstring"] = format_docstring
return env


Expand Down Expand Up @@ -485,7 +522,8 @@ def _get_environment_with_absolute_path(absolute_template_dir: Path, builtin_sub
loader=ChoiceLoader(loaders),
autoescape=select_autoescape(["html", "xml"]),
)
env.filters["escape_docstring"] = escape_docstring
env.filters["escape_docstring"] = escape_docstring # For old custom templates
env.filters["format_docstring"] = format_docstring
return env


Expand Down
8 changes: 2 additions & 6 deletions src/datamodel_code_generator/model/template/Enum.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@
{% endfor -%}
class {{ class_name }}({{ base_class }}):
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{%- for field in fields %}
{{ field.name }} = {{ field.default }}
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{{ class_name }}: TypeAlias = {{ py_type }}
{%- if description %}
"""
{{ description }}
"""
{{ description | format_docstring(0) }}
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{{ class_name }} = TypeAliasType("{{ class_name }}", {{ py_type }})
{%- if description %}
"""
{{ description }}
"""
{{ description | format_docstring(0) }}
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
type {{ class_name }} = {{ py_type }}
{%- if description %}
"""
{{ description }}
"""
{%- endif %}
{{ description | format_docstring(0) }}
{%- endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,13 @@ Annotated[{{ _field.type_hint }}, {{ _field.field }}]
{%- if fields %}
{{ class_name }}: TypeAlias = {{ get_type_annotation(fields[0]) }}{% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(0) }}
"""
{{ description | format_docstring(0) }}
{%- elif fields[0].docstring %}
"""
{{ fields[0].docstring | escape_docstring | indent(0) }}
"""
{{ fields[0].docstring | format_docstring(0) }}
{%- endif %}
{%- else %}
{{ class_name }}: TypeAlias = {{ base_class }}{% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(0) }}
"""
{{ description | format_docstring(0) }}
{%- endif %}
{%- endif %}
12 changes: 3 additions & 9 deletions src/datamodel_code_generator/model/template/TypeAliasType.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,13 @@ Annotated[{{ _field.type_hint }}, {{ _field.field }}]
{%- if fields %}
{{ class_name }} = TypeAliasType("{{ class_name }}", {{ get_type_annotation(fields[0]) }}){% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(0) }}
"""
{{ description | format_docstring(0) }}
{%- elif fields[0].docstring %}
"""
{{ fields[0].docstring | escape_docstring | indent(0) }}
"""
{{ fields[0].docstring | format_docstring(0) }}
{%- endif %}
{%- else %}
{{ class_name }} = TypeAliasType("{{ class_name }}", {{ base_class }}){% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(0) }}
"""
{{ description | format_docstring(0) }}
{%- endif %}
{%- endif %}
14 changes: 4 additions & 10 deletions src/datamodel_code_generator/model/template/TypeStatement.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,13 @@ Annotated[{{ _field.type_hint }}, {{ _field.field }}]
{%- if fields %}
type {{ class_name }} = {{ get_type_annotation(fields[0]) }}{% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(0) }}
"""
{{ description | format_docstring(0) }}
{%- elif fields[0].docstring %}
"""
{{ fields[0].docstring | escape_docstring | indent(0) }}
"""
{{ fields[0].docstring | format_docstring(0) }}
{%- endif %}
{%- else %}
type {{ class_name }} = {{ base_class }}{% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(0) }}
"""
{{ description | format_docstring(0) }}
{%- endif %}
{%- endif %}
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
class {{ class_name }}({{ base_class }}):
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{%- if not fields and not description %}
pass
{%- endif %}
{%- for field in fields %}
{{ field.name }}: {{ field.type_hint }}
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{{ class_name }} = TypedDict('{{ class_name }}', {
{%- for field in all_fields %}
'{{ field.key }}': {{ field.type_hint }},
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
Expand Down
8 changes: 2 additions & 6 deletions src/datamodel_code_generator/model/template/dataclass.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ class {{ class_name }}({{ base_class }}):
class {{ class_name }}:
{%- endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{%- if not fields and not description %}
pass
Expand All @@ -35,9 +33,7 @@ class {{ class_name }}:
{%- endif -%}
{%- endif %}
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
Expand Down
8 changes: 2 additions & 6 deletions src/datamodel_code_generator/model/template/msgspec.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ class {{ class_name }}({{ base_class }}{%- for key, value in (base_class_kwargs|
class {{ class_name }}:
{%- endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{%- set ns = namespace(has_rendered_field=false) -%}
{%- for field in fields -%}
Expand All @@ -37,9 +35,7 @@ class {{ class_name }}:


{%- if not field.extras.get('is_classvar') and field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
{% endfor -%}
class {{ class_name }}({{ base_class }}):{% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{%- if not fields and not description and not config and not class_body_lines %}
pass
Expand All @@ -32,9 +30,7 @@ class {{ class_name }}({{ base_class }}):{% if comment is defined %} # {{ comme
{%- endif -%}
{%- endif %}
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
{%- set use_base_type = config and config.regex_engine -%}
class {{ class_name }}({{ base_class }}{%- if fields -%}[{{get_type_hint(fields, use_base_type)}}]{%- endif -%}):{% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{%- if config %}
{%- filter indent(4) %}
Expand All @@ -47,9 +45,7 @@ class {{ class_name }}({{ base_class }}{%- if fields -%}[{{get_type_hint(fields,
{%- endif -%}
{%- endif %}
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- elif field.inline_field_docstring %}
{{ field.inline_field_docstring }}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@
{%- set use_base_type = config and config.regex_engine -%}
{{ class_name }} = RootModel[{{get_type_hint(fields, use_base_type)}}]{% if comment is defined %} # {{ comment }}{% endif %}
{%- if description %}
"""
{{ description }}
"""
{{ description | format_docstring(0) }}
{%- elif fields and fields[0].docstring %}
"""
{{ fields[0].docstring }}
"""
{{ fields[0].docstring | format_docstring(0) }}
{%- endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ class {{ class_name }}({{ base_class }}):
class {{ class_name }}:
{%- endif %}
{%- if description %}
"""
{{ description | escape_docstring | indent(4) }}
"""
{{ description | format_docstring(4) }}
{%- endif %}
{%- if not fields and not description %}
pass
Expand All @@ -46,9 +44,7 @@ class {{ class_name }}:
{%- endif -%}
{%- endif %}
{%- if field.docstring %}
"""
{{ field.docstring | escape_docstring | indent(4) }}
"""
{{ field.docstring | format_docstring(4) }}
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
Expand Down
12 changes: 3 additions & 9 deletions tests/data/expected/main/graphql/additional_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,21 @@
from typing_extensions import TypeAliasType

Boolean = TypeAliasType("Boolean", bool)
"""
The `Boolean` scalar type represents `true` or `false`.
"""
"""The `Boolean` scalar type represents `true` or `false`."""


Date = TypeAliasType("Date", date)


DateTime = TypeAliasType("DateTime", datetime)
"""
DateTime (ISO8601, example: 2020-01-01T10:11:12+00:00)
"""
"""DateTime (ISO8601, example: 2020-01-01T10:11:12+00:00)"""


MyCustomClass = TypeAliasType("MyCustomClass", MyCustomPythonClass)


String = TypeAliasType("String", str)
"""
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
"""
"""The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."""


class A(BaseModel):
Expand Down
Loading
Loading