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.
Files changed (122) hide show
  1. pyopenapi_gen/__init__.py +114 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +86 -0
  4. pyopenapi_gen/context/file_manager.py +52 -0
  5. pyopenapi_gen/context/import_collector.py +382 -0
  6. pyopenapi_gen/context/render_context.py +630 -0
  7. pyopenapi_gen/core/__init__.py +0 -0
  8. pyopenapi_gen/core/auth/base.py +22 -0
  9. pyopenapi_gen/core/auth/plugins.py +89 -0
  10. pyopenapi_gen/core/exceptions.py +25 -0
  11. pyopenapi_gen/core/http_transport.py +219 -0
  12. pyopenapi_gen/core/loader/__init__.py +12 -0
  13. pyopenapi_gen/core/loader/loader.py +158 -0
  14. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  15. pyopenapi_gen/core/loader/operations/parser.py +155 -0
  16. pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
  17. pyopenapi_gen/core/loader/operations/request_body.py +85 -0
  18. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  19. pyopenapi_gen/core/loader/parameters/parser.py +121 -0
  20. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  21. pyopenapi_gen/core/loader/responses/parser.py +104 -0
  22. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  23. pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
  24. pyopenapi_gen/core/pagination.py +64 -0
  25. pyopenapi_gen/core/parsing/__init__.py +13 -0
  26. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  27. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  28. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  29. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  30. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  37. pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
  38. pyopenapi_gen/core/parsing/context.py +184 -0
  39. pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
  40. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  41. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
  42. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
  43. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
  44. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
  45. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  46. pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
  47. pyopenapi_gen/core/parsing/schema_parser.py +610 -0
  48. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  49. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  50. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
  51. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  52. pyopenapi_gen/core/postprocess_manager.py +161 -0
  53. pyopenapi_gen/core/schemas.py +40 -0
  54. pyopenapi_gen/core/streaming_helpers.py +86 -0
  55. pyopenapi_gen/core/telemetry.py +67 -0
  56. pyopenapi_gen/core/utils.py +409 -0
  57. pyopenapi_gen/core/warning_collector.py +83 -0
  58. pyopenapi_gen/core/writers/code_writer.py +135 -0
  59. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  60. pyopenapi_gen/core/writers/line_writer.py +217 -0
  61. pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
  62. pyopenapi_gen/core_package_template/README.md +21 -0
  63. pyopenapi_gen/emit/models_emitter.py +143 -0
  64. pyopenapi_gen/emitters/client_emitter.py +51 -0
  65. pyopenapi_gen/emitters/core_emitter.py +181 -0
  66. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  67. pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
  68. pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
  69. pyopenapi_gen/emitters/models_emitter.py +428 -0
  70. pyopenapi_gen/generator/client_generator.py +562 -0
  71. pyopenapi_gen/helpers/__init__.py +1 -0
  72. pyopenapi_gen/helpers/endpoint_utils.py +552 -0
  73. pyopenapi_gen/helpers/type_cleaner.py +341 -0
  74. pyopenapi_gen/helpers/type_helper.py +112 -0
  75. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  76. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  77. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  78. pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
  79. pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
  80. pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
  81. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
  82. pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
  83. pyopenapi_gen/helpers/url_utils.py +14 -0
  84. pyopenapi_gen/http_types.py +20 -0
  85. pyopenapi_gen/ir.py +167 -0
  86. pyopenapi_gen/py.typed +1 -0
  87. pyopenapi_gen/types/__init__.py +11 -0
  88. pyopenapi_gen/types/contracts/__init__.py +13 -0
  89. pyopenapi_gen/types/contracts/protocols.py +106 -0
  90. pyopenapi_gen/types/contracts/types.py +30 -0
  91. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  92. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  93. pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
  94. pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
  95. pyopenapi_gen/types/services/__init__.py +5 -0
  96. pyopenapi_gen/types/services/type_service.py +133 -0
  97. pyopenapi_gen/visit/client_visitor.py +228 -0
  98. pyopenapi_gen/visit/docs_visitor.py +38 -0
  99. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  100. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
  101. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  102. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
  103. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
  104. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  105. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
  106. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
  107. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
  108. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  109. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
  110. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  111. pyopenapi_gen/visit/exception_visitor.py +52 -0
  112. pyopenapi_gen/visit/model/__init__.py +0 -0
  113. pyopenapi_gen/visit/model/alias_generator.py +89 -0
  114. pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
  115. pyopenapi_gen/visit/model/enum_generator.py +200 -0
  116. pyopenapi_gen/visit/model/model_visitor.py +197 -0
  117. pyopenapi_gen/visit/visitor.py +97 -0
  118. pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
  119. pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
  120. pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
  121. pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
  122. 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)