Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from itertools import chain
3from typing import Iterable
4from typing import List
5from typing import Union
7from .utils import assure_str
10class NestedCommentError(Exception):
11 """Comments inside of comments are not allowed."""
14class NoCommentStartError(Exception):
15 """Comments must be started before they are ended."""
18class CodeWriterLite:
19 """Simple container for C code, with indentation and blocks.
21 Warning:
22 You probably don't want to use :class:`CodeWriterLite` for code
23 generation, but its subclass, :class:`CodeWriter`.
24 :class:`CodeWriterLite` is meant for csnake internal use and is
25 normally not accessible at package level.
27 CodeWriterLite is used internally in cconstructs and also as a parent class
28 of CodeWriter.
30 CodeWriterLite (abbreviated CWL or cwl) is a container class for C code
31 generation, which implements indentation and block ({}) opening and
32 closing.
34 The class supports str, len, getitem, iter, and contains, making it a
35 virtual subclass of Collection.
38 Args:
39 lf: linefeed character used to separate lines of code when
40 rendering code with :func:`str` or :attr:`code`.
41 indent: indentation unit string or :class:`int` number of spaces,
42 used to indent code.
44 Attributes:
46 lf(str): linefeed character used to separate lines of code when
47 rendering code with :func:`str` or :attr:`code`.
49 lines(list(str)): lines of code.
50 """
52 __slots__ = ("lf", "_indent_unit", "_indent_level", "_in_comment", "lines")
54 def __init__(self, lf: str = None, indent: Union[int, str] = 4) -> None:
56 if lf is None:
57 lf = "\n"
58 self.lf: str = assure_str(lf)
60 if isinstance(indent, int):
61 self._indent_unit = " " * indent
62 else:
63 self._indent_unit = assure_str(indent)
65 # initialize values
66 self._indent_level = 0
67 self._in_comment = False
69 self.lines: List[str] = []
71 @property
72 def _current_indent(self) -> str:
73 return self._indent_unit * self._indent_level
75 @property
76 def code(self) -> str:
77 """str: The contents of the codewriter rendered to a str, using :attr:`lf`
78 as the linebreak.
79 """
80 return self.lf.join(self.lines)
82 def __str__(self) -> str:
83 """Return the code joined by :attr:`lf`."""
84 return self.code
86 def __iter__(self) -> Iterable[str]:
87 """Return an iterator that iterates over the lines in the codewriter."""
88 return iter(self.lines)
90 def __getitem__(self, key) -> str:
91 """Return the nth line of code."""
92 return self.lines.__getitem__(key)
94 def __len__(self):
95 """Return the number of lines in the codewriter."""
96 return self.lines.__len__()
98 def __contains__(self, item) -> bool:
99 """Return whether a :class:`str` is a substring of the codewriter.
101 Args:
102 item(str): a potential substring of the codewriter.
104 Returns:
105 Whether or not `item` is a substring of the :func:`str`
106 representation of the codewriter.
107 """
108 return self.code.__contains__(item)
110 def indent(self) -> None:
111 """Increase the indentation level by 1 unit.
113 Examples:
114 :meth:`indent` and :meth:`dedent` in action:
116 >>> from csnake import CodeWriter
117 >>> cwr = CodeWriter()
118 >>> cwr.add_line('line_1;')
119 >>> cwr.indent()
120 >>> cwr.add_line('line_2;')
121 >>> cwr.add_line('line_3;')
122 >>> cwr.dedent()
123 >>> cwr.add_line('line_4;')
124 >>> print(cwr)
125 line_1;
126 line_2;
127 line_3;
128 line_4;
129 """
130 self._indent_level += 1
132 def dedent(self) -> None:
133 """Decrease the indentation level by 1 unit.
135 Examples:
136 See :meth:`indent`.
137 """
139 if self._indent_level > 0:
140 self._indent_level -= 1
142 def zero_indent(self) -> None:
143 """Set indentation level to zero."""
144 self._indent_level = 0
146 def start_comment(self, ignore_indent: bool = False) -> None:
147 """Start a comment block.
149 All of the lines added after this are in a block comment, until the
150 block is finished by :meth:`end_comment`.
152 Args:
153 ignore_indent: whether to ignore the indentation and just start the
154 comment from the beginning of the next line.
156 Raises:
157 NestedCommentError: if you attempt to start a comment block when a
158 comment block has already been started.
160 Examples:
161 Starting and ending a comment block with :meth:`start_comment` and
162 :meth:`end_comment`:
164 >>> from csnake import CodeWriter
165 >>> cwr = CodeWriter()
166 >>> cwr.add_line('line_1;')
167 >>> cwr.start_comment()
168 >>> cwr.add_line('line_2;')
169 >>> cwr.add_line('line_3;')
170 >>> cwr.end_comment()
171 >>> cwr.add_line('line_4;')
172 >>> print(cwr)
173 line_1;
174 /*
175 * line_2;
176 * line_3;
177 */
178 line_4;
179 """
180 if self._in_comment:
181 raise NestedCommentError()
182 self.add_line("/*", ignore_indent=ignore_indent)
183 self._in_comment = True
185 def end_comment(self, ignore_indent: bool = False) -> None:
186 """End a comment block.
188 A comment block previously started by :meth:`start_comment` is ended.
189 The following code is not inside a block comment.
191 Raises:
192 NoCommentStartError: if you attempt to end a comment block when a
193 comment block hasn't been started.
195 Examples:
196 See :meth:`start_comment`.
197 """
198 if not self._in_comment:
199 raise NoCommentStartError()
200 self._in_comment = False
201 self.add_line("*/", ignore_indent=ignore_indent)
203 def open_brace(self) -> None:
204 """Add an open brace and increase indentation for subsequent lines.
206 See :meth:`indent` for more information on how indentation is done.
208 Examples:
209 Using :meth:`open_brace` and :meth:`close_brace`.
211 >>> from csnake import CodeWriter
212 >>> cwr = CodeWriter()
213 >>> cwr.add_line('line_1;')
214 >>> cwr.open_brace()
215 >>> cwr.add_line('line_2;')
216 >>> cwr.add_line('line_3;')
217 >>> cwr.close_brace()
218 >>> cwr.add_line('line_4;')
219 >>> print(cwr)
220 line_1;
221 {
222 line_2;
223 line_3;
224 }
225 line_4;
226 """
227 self.add_line("{")
228 self.indent()
230 def close_brace(self) -> None:
231 """Add a closed brace and decrease indentation for subsequent lines.
233 See :meth:`dedent` for more information on how dedentation is done.
235 Examples:
236 See :meth:`open_brace`.
237 """
238 self.dedent()
239 self.add_line("}")
241 def add(self, text: str) -> None:
242 """Append text to the end of last line.
244 Args:
245 text: :obj:`str` to append to the last line.
247 Raises:
248 ValueError: if :obj:`text` is not a :class:`str`.
249 """
250 try:
251 self.lines[-1]
252 except IndexError:
253 self.lines.append("")
255 self.lines[-1] += text
257 def add_line(
258 self,
259 text: str = None,
260 comment: str = None,
261 ignore_indent: bool = False,
262 ) -> None:
263 """Add a line of text to the container after the last line.
265 Args:
266 text: :obj:`str` to add
267 comment: optional comment
268 ignore_indent: switch to ignore indentation and insert the code
269 with indentation 0
271 Raises:
272 ValueError: if text or comment isn't a :class:`str`
273 """
275 # empty line
276 if text is None and not comment and not self._in_comment:
277 self.lines.append("")
278 return
280 current_line = ""
282 if not ignore_indent:
283 current_line += self._current_indent
285 if self._in_comment:
286 current_line += "*"
288 # add the text (if appropriate)
289 if text:
290 text = assure_str(text)
291 if "\n" in text:
292 raise ValueError("text mustn't contain multiple lines.")
293 if self._in_comment:
294 current_line += " "
295 current_line += text
297 if comment:
298 comment = assure_str(comment)
299 # add a space if there is text
300 if text:
301 current_line += " "
302 if self._in_comment:
303 current_line += f"{comment}"
304 else:
305 current_line += f"/* {comment} */"
307 self.lines.append(current_line)
309 def add_lines(
310 self, lines: Union[Iterable[str], str], ignore_indent: bool = False
311 ) -> None:
312 """Append code lines from an iterable or a multi-line str.
314 If `lines` is an iterable iterating over :class:`str` objects, adds the
315 lines to the codewriter in order.
317 If `lines` is a multi-line string, adds its lines to the codewriter.
319 Args:
320 lines: an :class:`Iterable` of :class:`str` or a multi-line `str`
321 to add to codewriter.
322 ignore_indent: whether to ignore the current indentation level or
323 not. If `ignore_indent` is true, the lines will be appended
324 without indentation.
326 Raises:
327 ValueError: if one of the arguments is of the wrong type
329 Examples:
330 Adding a list of lines:
332 >>> from csnake import CodeWriter
333 >>> cwr = CodeWriter()
334 >>> lol = ['list', 'of', 'lines']
335 >>> cwr.add_lines(lol)
336 >>> print(cwr)
337 list
338 of
339 lines
340 """
342 if isinstance(lines, str):
343 lines = lines.splitlines()
344 else:
345 lines = chain.from_iterable(
346 supposed_line.splitlines() for supposed_line in lines
347 )
349 for line in lines:
350 self.add_line(line, ignore_indent=ignore_indent)
353assert issubclass(CodeWriterLite, Iterable)