Skip to content

Commit 2bf17c2

Browse files
committed
eng: add option to put docstrings on model attributes BNCH-114718
1 parent 082fd18 commit 2bf17c2

File tree

6 files changed

+73
-7
lines changed

6 files changed

+73
-7
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ class_overrides:
100100
101101
The easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the `models` folder.
102102

103+
### docstrings_on_attributes
104+
105+
By default, when `openapi-python-client` generates a model class, it includes a list of attributes and their
106+
descriptions in the docstring for the class. If you set this option to `true`, then the attribute descriptions
107+
will be put in docstrings for the attributes themselves, and will not be in the class docstring.
108+
109+
```yaml
110+
docstrings_on_attributes: true
111+
```
112+
103113
### literal_enums
104114

105115
By default, `openapi-python-client` generates classes inheriting for `Enum` for enums. It can instead use `Literal`

end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import re
12
from typing import Any, List
23

34
from end_to_end_tests.functional_tests.helpers import (
45
with_generated_code_import,
56
with_generated_client_fixture,
67
)
8+
from end_to_end_tests.generated_client import GeneratedClientContext
79

810

911
class DocstringParser:
@@ -36,18 +38,57 @@ def get_section(self, header_line: str) -> List[str]:
3638
required: ["reqStr", "undescribedProp"]
3739
""")
3840
@with_generated_code_import(".models.MyModel")
39-
class TestSchemaDocstrings:
41+
class TestSchemaDocstringsDefaultBehavior:
4042
def test_model_description(self, MyModel):
4143
assert DocstringParser(MyModel).lines[0] == "I like this type."
4244

43-
def test_model_properties(self, MyModel):
45+
def test_model_properties_in_model_description(self, MyModel):
4446
assert set(DocstringParser(MyModel).get_section("Attributes:")) == {
4547
"req_str (str): This is necessary.",
4648
"opt_str (Union[Unset, str]): This isn't necessary.",
4749
"undescribed_prop (str):",
4850
}
4951

5052

53+
@with_generated_client_fixture(
54+
"""
55+
components:
56+
schemas:
57+
MyModel:
58+
description: I like this type.
59+
type: object
60+
properties:
61+
prop1:
62+
type: string
63+
description: This attribute has a description
64+
prop2:
65+
type: string # no description for this one
66+
required: ["prop1", "prop2"]
67+
""",
68+
config="docstrings_on_attributes: true",
69+
)
70+
@with_generated_code_import(".models.MyModel")
71+
class TestSchemaWithDocstringsOnAttributesOption:
72+
def test_model_description_is_entire_docstring(self, MyModel):
73+
assert MyModel.__doc__.strip() == "I like this type."
74+
75+
def test_attrs_have_docstrings(self, generated_client: GeneratedClientContext):
76+
# A docstring that appears after an attribute is *not* stored in __doc__ anywhere
77+
# by the interpreter, so we can't inspect it that way-- but it's still valid for it
78+
# to appear there, and it will be recognized by documentation tools. So we'll assert
79+
# that these strings appear in the source code. The code should look like this:
80+
# class MyModel:
81+
# """I like this type."
82+
# prop1: str
83+
# """This attribute has a description"
84+
# prop2: str
85+
#
86+
source_file_path = generated_client.output_path / generated_client.base_module / "models" / "my_model.py"
87+
content = source_file_path.read_text()
88+
assert re.search('\n *prop1: *str\n *""" *This attribute has a description *"""\n', content)
89+
assert re.search('\n *prop2: *str\n *[^"]', content)
90+
91+
5192
@with_generated_client_fixture(
5293
"""
5394
tags:

openapi_python_client/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def __init__(
9595

9696
self.env.filters.update(TEMPLATE_FILTERS)
9797
self.env.globals.update(
98+
config=config,
9899
utils=utils,
99100
python_identifier=lambda x: utils.PythonIdentifier(x, config.field_prefix),
100101
class_name=lambda x: utils.ClassName(x, config.field_prefix),

openapi_python_client/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class ConfigFile(BaseModel):
4141
package_version_override: Optional[str] = None
4242
use_path_prefixes_for_title_model_names: bool = True
4343
post_hooks: Optional[list[str]] = None
44+
docstrings_on_attributes: bool = False
4445
field_prefix: str = "field_"
4546
generate_all_tags: bool = False
4647
http_timeout: int = 5
@@ -70,6 +71,7 @@ class Config:
7071
package_version_override: Optional[str]
7172
use_path_prefixes_for_title_model_names: bool
7273
post_hooks: list[str]
74+
docstrings_on_attributes: bool
7375
field_prefix: str
7476
generate_all_tags: bool
7577
http_timeout: int
@@ -111,6 +113,7 @@ def from_sources(
111113
package_version_override=config_file.package_version_override,
112114
use_path_prefixes_for_title_model_names=config_file.use_path_prefixes_for_title_model_names,
113115
post_hooks=post_hooks,
116+
docstrings_on_attributes=config_file.docstrings_on_attributes,
114117
field_prefix=config_file.field_prefix,
115118
generate_all_tags=config_file.generate_all_tags,
116119
http_timeout=config_file.http_timeout,
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
{% macro safe_docstring(content) %}
1+
{% macro safe_docstring(content, omit_if_empty=False) %}
22
{# This macro returns the provided content as a docstring, set to a raw string if it contains a backslash #}
3+
{% if (not omit_if_empty) or (content | trim) %}
34
{% if '\\' in content -%}
45
r""" {{ content }} """
56
{%- else -%}
67
""" {{ content }} """
78
{%- endif -%}
9+
{% endif %}
810
{% endmacro %}

openapi_python_client/templates/model.py.jinja

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,34 @@ T = TypeVar("T", bound="{{ class_name }}")
4747
{{ model.example | string | wordwrap(112) | indent(12) }}
4848

4949
{% endif %}
50-
{% if model.required_properties or model.optional_properties %}
50+
{% if (not config.docstrings_on_attributes) and (model.required_properties or model.optional_properties) %}
5151
Attributes:
5252
{% for property in model.required_properties + model.optional_properties %}
5353
{{ property.to_docstring() | wordwrap(112) | indent(12) }}
5454
{% endfor %}{% endif %}
5555
{% endmacro %}
5656

57+
{% macro declare_property(property) %}
58+
{%- if config.docstrings_on_attributes and property.description -%}
59+
{{ property.to_string() }}
60+
{{ safe_docstring(property.description, omit_if_empty=True) | wordwrap(112) }}
61+
{%- else -%}
62+
{{ property.to_string() }}
63+
{%- endif -%}
64+
{% endmacro %}
65+
5766
@_attrs_define
5867
class {{ class_name }}:
59-
{{ safe_docstring(class_docstring_content(model)) | indent(4) }}
68+
{{ safe_docstring(class_docstring_content(model), omit_if_empty=config.docstrings_on_attributes) | indent(4) }}
6069

6170
{% for property in model.required_properties + model.optional_properties %}
6271
{% if property.default is none and property.required %}
63-
{{ property.to_string() }}
72+
{{ declare_property(property) | indent(4) }}
6473
{% endif %}
6574
{% endfor %}
6675
{% for property in model.required_properties + model.optional_properties %}
6776
{% if property.default is not none or not property.required %}
68-
{{ property.to_string() }}
77+
{{ declare_property(property) | indent(4) }}
6978
{% endif %}
7079
{% endfor %}
7180
{% if model.additional_properties %}

0 commit comments

Comments
 (0)