pyopenapi-gen 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi_gen/__init__.py +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +30 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,222 @@
|
|
1
|
+
"""
|
2
|
+
DocumentationWriter: Utility for generating well-formatted, Google-style Python docstrings.
|
3
|
+
|
4
|
+
This module provides the DocumentationWriter and DocumentationBlock classes, which are responsible
|
5
|
+
for building comprehensive, type-rich docstrings for generated Python code. It supports argument
|
6
|
+
alignment, line wrapping, and section formatting for Args, Returns, and Raises.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import List, Optional, Tuple, Union
|
10
|
+
|
11
|
+
from .line_writer import LineWriter
|
12
|
+
|
13
|
+
|
14
|
+
class DocumentationBlock:
|
15
|
+
"""
|
16
|
+
Data container for docstring content.
|
17
|
+
|
18
|
+
Attributes:
|
19
|
+
summary (Optional[str]): The summary line for the docstring.
|
20
|
+
description (Optional[str]): The detailed description.
|
21
|
+
args (Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]]):
|
22
|
+
List of arguments as (name, type, desc) or (type, desc) tuples.
|
23
|
+
returns (Optional[Tuple[str, str]]): The return type and description.
|
24
|
+
raises (Optional[List[Tuple[str, str]]]): List of (exception type, description) tuples.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
summary: Optional[str] = None,
|
30
|
+
description: Optional[str] = None,
|
31
|
+
args: Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]] = None,
|
32
|
+
returns: Optional[Tuple[str, str]] = None,
|
33
|
+
raises: Optional[List[Tuple[str, str]]] = None,
|
34
|
+
) -> None:
|
35
|
+
"""
|
36
|
+
Initialize a DocumentationBlock.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
summary (Optional[str]): The summary line.
|
40
|
+
description (Optional[str]): The detailed description.
|
41
|
+
args (Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]]): Arguments.
|
42
|
+
returns (Optional[Tuple[str, str]]): Return type and description.
|
43
|
+
raises (Optional[List[Tuple[str, str]]]): Exceptions.
|
44
|
+
"""
|
45
|
+
self.summary: Optional[str] = summary
|
46
|
+
self.description: Optional[str] = description
|
47
|
+
self.args: List[Union[Tuple[str, str, str], Tuple[str, str]]] = args or []
|
48
|
+
self.returns: Optional[Tuple[str, str]] = returns
|
49
|
+
self.raises: List[Tuple[str, str]] = raises or []
|
50
|
+
|
51
|
+
|
52
|
+
class DocumentationFormatter:
|
53
|
+
"""
|
54
|
+
Handles low-level formatting, wrapping, and alignment for docstring lines using LineWriter.
|
55
|
+
"""
|
56
|
+
|
57
|
+
def __init__(self, width: int = 88, min_desc_col: int = 30) -> None:
|
58
|
+
self.width: int = width
|
59
|
+
self.min_desc_col: int = min_desc_col
|
60
|
+
|
61
|
+
def wrap(self, text: str, indent: int, prefix: Optional[str] = None) -> List[str]:
|
62
|
+
if not text:
|
63
|
+
return []
|
64
|
+
writer = LineWriter(max_width=self.width)
|
65
|
+
for _ in range(indent // len(writer.indent_str)):
|
66
|
+
writer.indent()
|
67
|
+
if prefix is not None:
|
68
|
+
writer.append(prefix)
|
69
|
+
writer.append_wrapped(text)
|
70
|
+
return writer.getvalue().splitlines()
|
71
|
+
|
72
|
+
def get_arg_prefix(self, arg: Union[Tuple[str, str, str], Tuple[str, str]]) -> str:
|
73
|
+
if len(arg) == 3:
|
74
|
+
name, typ, _ = arg
|
75
|
+
return f"{name} ({typ})"
|
76
|
+
return f"{arg[0]}"
|
77
|
+
|
78
|
+
def render_short_prefix_arg(self, prefix: str, desc: str, indent: int, desc_col: int) -> List[str]:
|
79
|
+
writer = LineWriter(max_width=self.width)
|
80
|
+
for _ in range(indent // len(writer.indent_str)):
|
81
|
+
writer.indent()
|
82
|
+
writer.append(prefix)
|
83
|
+
writer.move_to_column(desc_col)
|
84
|
+
writer.append(": ")
|
85
|
+
writer.append_wrapped(desc)
|
86
|
+
return writer.getvalue().splitlines()
|
87
|
+
|
88
|
+
def render_long_prefix_arg(
|
89
|
+
self,
|
90
|
+
prefix: str,
|
91
|
+
desc: str,
|
92
|
+
indent: int,
|
93
|
+
min_col: int,
|
94
|
+
) -> List[str]:
|
95
|
+
writer = LineWriter(max_width=self.width)
|
96
|
+
for _ in range(indent // len(writer.indent_str)):
|
97
|
+
writer.indent()
|
98
|
+
writer.append(prefix)
|
99
|
+
writer.newline()
|
100
|
+
writer.move_to_column(min_col)
|
101
|
+
writer.append(": ")
|
102
|
+
# writer.move_to_column(min_col + 2)
|
103
|
+
writer.append_wrapped(desc)
|
104
|
+
return writer.getvalue().splitlines()
|
105
|
+
|
106
|
+
|
107
|
+
class DocstringSectionRenderer:
|
108
|
+
"""
|
109
|
+
Renders Args, Returns, and Raises sections for Google-style docstrings using LineWriter.
|
110
|
+
"""
|
111
|
+
|
112
|
+
def __init__(self, formatter: DocumentationFormatter) -> None:
|
113
|
+
self.formatter = formatter
|
114
|
+
|
115
|
+
def _render_short_prefix_arg(self, prefix: str, desc: str, indent: int, min_col: int) -> list[str]:
|
116
|
+
return self.formatter.render_short_prefix_arg(prefix, desc, indent, min_col)
|
117
|
+
|
118
|
+
def _render_exact_prefix_arg(self, prefix: str, desc: str, indent: int, min_col: int) -> list[str]:
|
119
|
+
return self.formatter.render_short_prefix_arg(prefix, desc, indent, min_col)
|
120
|
+
|
121
|
+
def _render_long_prefix_arg(self, prefix: str, desc: str, indent: int, min_col: int) -> list[str]:
|
122
|
+
return self.formatter.render_long_prefix_arg(prefix, desc, indent, min_col)
|
123
|
+
|
124
|
+
def render_args(self, args: List[Union[Tuple[str, str, str], Tuple[str, str]]], indent: int) -> List[str]:
|
125
|
+
lines: List[str] = []
|
126
|
+
min_col = self.formatter.min_desc_col
|
127
|
+
for arg in args:
|
128
|
+
prefix = self.formatter.get_arg_prefix(arg)
|
129
|
+
desc = arg[2] if len(arg) == 3 else arg[1]
|
130
|
+
prefix_len = indent + len(prefix)
|
131
|
+
if prefix_len < min_col:
|
132
|
+
lines.extend(self._render_short_prefix_arg(prefix, desc, indent, min_col))
|
133
|
+
elif prefix_len == min_col:
|
134
|
+
lines.extend(self._render_exact_prefix_arg(prefix, desc, indent, min_col))
|
135
|
+
else:
|
136
|
+
lines.extend(self._render_long_prefix_arg(prefix, desc, indent, min_col))
|
137
|
+
return lines
|
138
|
+
|
139
|
+
def render_returns(self, returns: Tuple[str, str], indent: int) -> List[str]:
|
140
|
+
typ, desc = returns
|
141
|
+
prefix = f"{typ}:"
|
142
|
+
writer = LineWriter(max_width=self.formatter.width)
|
143
|
+
for _ in range(indent // len(writer.indent_str)):
|
144
|
+
writer.indent()
|
145
|
+
writer.append(prefix)
|
146
|
+
writer.append(" ")
|
147
|
+
writer.append_wrapped(desc)
|
148
|
+
return writer.getvalue().splitlines()
|
149
|
+
|
150
|
+
def render_raises(self, raises: List[Tuple[str, str]], indent: int) -> List[str]:
|
151
|
+
writer = LineWriter(max_width=self.formatter.width)
|
152
|
+
for _ in range(indent // len(writer.indent_str)):
|
153
|
+
writer.indent()
|
154
|
+
if not raises:
|
155
|
+
return writer.lines
|
156
|
+
writer.append("HttpError:")
|
157
|
+
for code, desc in raises:
|
158
|
+
writer.newline()
|
159
|
+
writer.append(f" {code}:")
|
160
|
+
if desc.strip():
|
161
|
+
writer.append(" ")
|
162
|
+
writer.append_wrapped(desc)
|
163
|
+
return writer.getvalue().splitlines()
|
164
|
+
|
165
|
+
|
166
|
+
class DocumentationWriter:
|
167
|
+
"""
|
168
|
+
Renders a DocumentationBlock into a Google-style Python docstring.
|
169
|
+
Delegates all formatting to DocumentationFormatter and section rendering to DocstringSectionRenderer.
|
170
|
+
"""
|
171
|
+
|
172
|
+
def __init__(self, width: int = 88, min_desc_col: int = 30) -> None:
|
173
|
+
"""
|
174
|
+
Initialize a DocumentationWriter.
|
175
|
+
|
176
|
+
Args:
|
177
|
+
width (int): The maximum line width for wrapping (default: 88).
|
178
|
+
min_desc_col (int): The minimum column for aligning descriptions (default: 30).
|
179
|
+
"""
|
180
|
+
self.formatter = DocumentationFormatter(width=width, min_desc_col=min_desc_col)
|
181
|
+
self.section_renderer = DocstringSectionRenderer(self.formatter)
|
182
|
+
self.width = width
|
183
|
+
self.min_desc_col = min_desc_col
|
184
|
+
|
185
|
+
def render_docstring(self, doc: DocumentationBlock, indent: int = 0) -> str:
|
186
|
+
"""
|
187
|
+
Render a Google-style docstring from a DocumentationBlock.
|
188
|
+
|
189
|
+
Args:
|
190
|
+
doc (DocumentationBlock): The docstring structure to render.
|
191
|
+
indent (int): The indentation level (in spaces) for the docstring block.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
str: The formatted docstring as a string.
|
195
|
+
"""
|
196
|
+
lines: List[str] = []
|
197
|
+
lines.append(f'"""')
|
198
|
+
# Summary
|
199
|
+
if doc.summary:
|
200
|
+
lines.extend(self.formatter.wrap(doc.summary, indent))
|
201
|
+
# Description
|
202
|
+
if doc.description:
|
203
|
+
if doc.summary:
|
204
|
+
lines.append("")
|
205
|
+
lines.extend(self.formatter.wrap(doc.description, indent))
|
206
|
+
# Args
|
207
|
+
if doc.args:
|
208
|
+
lines.append("")
|
209
|
+
lines.append("Args:")
|
210
|
+
lines.extend(self.section_renderer.render_args(doc.args, indent + 4))
|
211
|
+
# Returns
|
212
|
+
if doc.returns:
|
213
|
+
lines.append("")
|
214
|
+
lines.append("Returns:")
|
215
|
+
lines.extend(self.section_renderer.render_returns(doc.returns, indent + 4))
|
216
|
+
# Raises
|
217
|
+
if doc.raises:
|
218
|
+
lines.append("")
|
219
|
+
lines.append("Raises:")
|
220
|
+
lines.extend(self.section_renderer.render_raises(doc.raises, indent + 4))
|
221
|
+
lines.append('"""')
|
222
|
+
return "\n".join(lines)
|
@@ -0,0 +1,217 @@
|
|
1
|
+
"""
|
2
|
+
LineWriter: Utility for building and formatting lines of text with precise control over indentation and width.
|
3
|
+
|
4
|
+
This class is designed for use in both code and documentation generation, providing methods to append text, start
|
5
|
+
new lines, and query the current line's width.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import List, Optional
|
9
|
+
|
10
|
+
|
11
|
+
class LineWriter:
|
12
|
+
"""
|
13
|
+
Utility for building lines of text with indentation and width tracking.
|
14
|
+
|
15
|
+
Attributes:
|
16
|
+
lines (List[str]): The accumulated lines of text.
|
17
|
+
indent_level (int): The current indentation level.
|
18
|
+
indent_str (str): The string used for one indentation level (default: 4 spaces).
|
19
|
+
max_width (int): The maximum line width for wrapping.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, indent_str: str = " ", max_width: int = 80) -> None:
|
23
|
+
"""
|
24
|
+
Initialize a new LineWriter with a single empty line, indentation state, and max line width.
|
25
|
+
Args:
|
26
|
+
indent_str (str): The string used for one indentation level (default: 4 spaces).
|
27
|
+
max_width (int): The maximum line width for wrapping (default: 80).
|
28
|
+
"""
|
29
|
+
self.lines: List[str] = [""]
|
30
|
+
self.indent_level: int = 0
|
31
|
+
self.indent_str: str = indent_str
|
32
|
+
self._just_newlined: bool = True
|
33
|
+
self.max_width: int = max_width
|
34
|
+
|
35
|
+
def indent(self) -> None:
|
36
|
+
"""
|
37
|
+
Increase the indentation level by one.
|
38
|
+
"""
|
39
|
+
self.indent_level += 1
|
40
|
+
|
41
|
+
def dedent(self) -> None:
|
42
|
+
"""
|
43
|
+
Decrease the indentation level by one (never below zero).
|
44
|
+
"""
|
45
|
+
self.indent_level = max(0, self.indent_level - 1)
|
46
|
+
|
47
|
+
def append(self, text: str) -> None:
|
48
|
+
"""
|
49
|
+
Append text to the current line, respecting current indentation if the line is empty and was just newlined.
|
50
|
+
Args:
|
51
|
+
text (str): The text to append.
|
52
|
+
"""
|
53
|
+
if self.lines[-1] == "" and self._just_newlined:
|
54
|
+
self.lines[-1] = self.indent_str * self.indent_level + text
|
55
|
+
else:
|
56
|
+
self.lines[-1] += text
|
57
|
+
self._just_newlined = False
|
58
|
+
|
59
|
+
def newline(self) -> None:
|
60
|
+
"""
|
61
|
+
Start a new line, with current indentation.
|
62
|
+
"""
|
63
|
+
self.lines.append("")
|
64
|
+
self._just_newlined = True
|
65
|
+
|
66
|
+
def current_width(self) -> int:
|
67
|
+
"""
|
68
|
+
Get the width (number of characters) of the current line.
|
69
|
+
Returns:
|
70
|
+
int: The width of the current line.
|
71
|
+
"""
|
72
|
+
return len(self.lines[-1])
|
73
|
+
|
74
|
+
def current_line(self) -> str:
|
75
|
+
"""
|
76
|
+
Get the current line.
|
77
|
+
"""
|
78
|
+
return self.lines[-1]
|
79
|
+
|
80
|
+
def getvalue(self) -> str:
|
81
|
+
"""
|
82
|
+
Get the full text as a single string, joined by newlines.
|
83
|
+
Returns:
|
84
|
+
str: The full text.
|
85
|
+
"""
|
86
|
+
return "\n".join(self.lines)
|
87
|
+
|
88
|
+
def wrap_and_append(self, text: str, width: int, prefix: str = "") -> None:
|
89
|
+
"""
|
90
|
+
Wrap the given text to the specified width and append it, using the given prefix for the first line.
|
91
|
+
Args:
|
92
|
+
text (str): The text to wrap and append.
|
93
|
+
width (int): The maximum line width.
|
94
|
+
prefix (str): The prefix for the first line (default: empty).
|
95
|
+
"""
|
96
|
+
import textwrap
|
97
|
+
|
98
|
+
wrapped = textwrap.wrap(text, width=width, initial_indent=prefix, subsequent_indent=" " * len(prefix))
|
99
|
+
for i, line in enumerate(wrapped):
|
100
|
+
if i > 0:
|
101
|
+
self.newline()
|
102
|
+
self.append(line)
|
103
|
+
|
104
|
+
def _get_current_column(self) -> int:
|
105
|
+
"""
|
106
|
+
Get the current column of the cursor.
|
107
|
+
"""
|
108
|
+
|
109
|
+
line_width = self.current_width()
|
110
|
+
if line_width == 0:
|
111
|
+
return self.indent_level * len(self.indent_str)
|
112
|
+
return line_width
|
113
|
+
|
114
|
+
def append_wrapped(self, text: str) -> None:
|
115
|
+
"""
|
116
|
+
Append text to the current line, wrapping as needed.
|
117
|
+
|
118
|
+
The first line wraps at (self.max_width - current cursor position), so wrapping always respects the available
|
119
|
+
space on the current line, including indentation and any already-appended text. Subsequent lines wrap
|
120
|
+
at (self.max_width - current column), and are aligned to the current column at the time of the call.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
text (str): The text to append and wrap.
|
124
|
+
"""
|
125
|
+
import textwrap
|
126
|
+
|
127
|
+
if not text:
|
128
|
+
return
|
129
|
+
wrap_col = self._get_current_column()
|
130
|
+
first_line_width = self.max_width - wrap_col
|
131
|
+
if first_line_width <= 0:
|
132
|
+
self.newline()
|
133
|
+
wrap_col = self._get_current_column()
|
134
|
+
first_line_width = self.max_width - wrap_col
|
135
|
+
|
136
|
+
prefix = self.current_line()
|
137
|
+
while len(prefix) < wrap_col:
|
138
|
+
prefix += " "
|
139
|
+
wrap_col = max(wrap_col, len(prefix))
|
140
|
+
|
141
|
+
wrapper = textwrap.TextWrapper(
|
142
|
+
width=self.max_width,
|
143
|
+
initial_indent="",
|
144
|
+
subsequent_indent=" " * wrap_col,
|
145
|
+
break_long_words=True,
|
146
|
+
break_on_hyphens=True,
|
147
|
+
)
|
148
|
+
wrapped_lines = wrapper.wrap(prefix + text)
|
149
|
+
if wrapped_lines:
|
150
|
+
self.replace_current_line(wrapped_lines[0])
|
151
|
+
for line in wrapped_lines[1:]:
|
152
|
+
self.newline()
|
153
|
+
self.replace_current_line(line)
|
154
|
+
|
155
|
+
def replace_current_line(self, line: str) -> None:
|
156
|
+
"""
|
157
|
+
Replace the current line with the given line.
|
158
|
+
"""
|
159
|
+
self.lines[-1] = line
|
160
|
+
|
161
|
+
def move_to_column(self, col: int) -> None:
|
162
|
+
"""
|
163
|
+
Pad the current line with spaces until the cursor is at column col.
|
164
|
+
Args:
|
165
|
+
col (int): The target column to move the cursor to.
|
166
|
+
"""
|
167
|
+
current = len(self.lines[-1])
|
168
|
+
if current < col:
|
169
|
+
self.lines[-1] += " " * (col - current - 1)
|
170
|
+
# If already at or past col, do nothing
|
171
|
+
|
172
|
+
def append_wrapped_at_column(self, text: str, width: int, col: Optional[int] = None) -> None:
|
173
|
+
"""
|
174
|
+
Append text, wrapping as needed, so that the first line continues from the current position,
|
175
|
+
and all subsequent lines start at column `col`.
|
176
|
+
|
177
|
+
If `col` is None, the current line width at the time of the call is used as the wrap column.
|
178
|
+
This allows ergonomic alignment after any prefix or content.
|
179
|
+
|
180
|
+
Args:
|
181
|
+
text (str) : The text to append and wrap.
|
182
|
+
width (int) : The maximum line width.
|
183
|
+
col (Optional[int]) : The column at which to start wrapped lines. If None, uses current
|
184
|
+
line width.
|
185
|
+
"""
|
186
|
+
import textwrap
|
187
|
+
|
188
|
+
if not text:
|
189
|
+
return # Do nothing for empty text
|
190
|
+
# Determine wrap column
|
191
|
+
wrap_col = self.current_width() if col is None else col
|
192
|
+
# First line: fill up to current position, then append as much as fits
|
193
|
+
current = len(self.lines[-1])
|
194
|
+
available = max(0, width - current)
|
195
|
+
if available <= 0:
|
196
|
+
# No space left, start a new line at wrap_col
|
197
|
+
self.newline()
|
198
|
+
self.move_to_column(wrap_col)
|
199
|
+
current = len(self.lines[-1])
|
200
|
+
available = max(0, width - current)
|
201
|
+
words = text.split()
|
202
|
+
first_line = ""
|
203
|
+
|
204
|
+
while words and len(first_line) + len(words[0]) + (1 if first_line else 0) <= available:
|
205
|
+
if first_line:
|
206
|
+
first_line += " "
|
207
|
+
first_line += words.pop(0)
|
208
|
+
if first_line:
|
209
|
+
self.append(first_line)
|
210
|
+
# Remaining lines: wrap at width, start at wrap_col
|
211
|
+
if words:
|
212
|
+
rest = " ".join(words)
|
213
|
+
wrapped = textwrap.wrap(rest, width=width - wrap_col)
|
214
|
+
for line in wrapped:
|
215
|
+
self.newline()
|
216
|
+
self.move_to_column(wrap_col)
|
217
|
+
self.append(line)
|