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 Optional
6from typing import Union
8from .utils import assure_str
11class NestedCommentError(Exception):
12 """Comments inside of comments are not allowed."""
15class NoCommentStartError(Exception):
16 """Comments must be started before they are ended."""
19class CodeWriterLite:
20 """Simple container for C code, with indentation and blocks.
22 Warning:
23 You probably don't want to use :class:`CodeWriterLite` for code
24 generation, but its subclass, :class:`CodeWriter`.
25 :class:`CodeWriterLite` is meant for csnake internal use and is
26 normally not accessible at package level.
28 CodeWriterLite is used internally in cconstructs and also as a parent class
29 of CodeWriter.
31 CodeWriterLite (abbreviated CWL or cwl) is a container class for C code
32 generation, which implements indentation and block ({}) opening and
33 closing.
35 The class supports str, len, getitem, iter, and contains, making it a
36 virtual subclass of Collection.
39 Args:
40 lf: linefeed character used to separate lines of code when
41 rendering code with :func:`str` or :attr:`code`.
42 indent: indentation unit string or :class:`int` number of spaces,
43 used to indent code.
45 Attributes:
47 lf(str): linefeed character used to separate lines of code when
48 rendering code with :func:`str` or :attr:`code`.
50 lines(list(str)): lines of code.
51 """
53 __slots__ = ("lf", "_indent_unit", "_indent_level", "_in_comment", "lines")
55 def __init__(
56 self, lf: Optional[str] = None, indent: Union[int, str] = 4
57 ) -> None:
58 if lf is None:
59 lf = "\n"
60 self.lf: str = assure_str(lf)
62 if isinstance(indent, int):
63 self._indent_unit = " " * indent
64 else:
65 self._indent_unit = assure_str(indent)
67 # initialize values
68 self._indent_level = 0
69 self._in_comment = False
71 self.lines: List[str] = []
73 @property
74 def _current_indent(self) -> str:
75 return self._indent_unit * self._indent_level
77 @property
78 def code(self) -> str:
79 """str: The contents of the codewriter rendered to a str, using :attr:`lf`
80 as the linebreak.
81 """
82 return self.lf.join(self.lines)
84 def __str__(self) -> str:
85 """Return the code joined by :attr:`lf`."""
86 return self.code
88 def __iter__(self) -> Iterable[str]:
89 """Return an iterator that iterates over the lines in the codewriter."""
90 return iter(self.lines)
92 def __getitem__(self, key) -> str:
93 """Return the nth line of code."""
94 return self.lines.__getitem__(key)
96 def __len__(self):
97 """Return the number of lines in the codewriter."""
98 return self.lines.__len__()
100 def __contains__(self, item) -> bool:
101 """Return whether a :class:`str` is a substring of the codewriter.
103 Args:
104 item(str): a potential substring of the codewriter.
106 Returns:
107 Whether or not `item` is a substring of the :func:`str`
108 representation of the codewriter.
109 """
110 return self.code.__contains__(item)
112 def indent(self) -> None:
113 """Increase the indentation level by 1 unit.
115 Examples:
116 :meth:`indent` and :meth:`dedent` in action:
118 >>> from csnake import CodeWriter
119 >>> cwr = CodeWriter()
120 >>> cwr.add_line('line_1;')
121 >>> cwr.indent()
122 >>> cwr.add_line('line_2;')
123 >>> cwr.add_line('line_3;')
124 >>> cwr.dedent()
125 >>> cwr.add_line('line_4;')
126 >>> print(cwr)
127 line_1;
128 line_2;
129 line_3;
130 line_4;
131 """
132 self._indent_level += 1
134 def dedent(self) -> None:
135 """Decrease the indentation level by 1 unit.
137 Examples:
138 See :meth:`indent`.
139 """
141 if self._indent_level > 0:
142 self._indent_level -= 1
144 def zero_indent(self) -> None:
145 """Set indentation level to zero."""
146 self._indent_level = 0
148 def start_comment(self, ignore_indent: bool = False) -> None:
149 """Start a comment block.
151 All of the lines added after this are in a block comment, until the
152 block is finished by :meth:`end_comment`.
154 Args:
155 ignore_indent: whether to ignore the indentation and just start the
156 comment from the beginning of the next line.
158 Raises:
159 NestedCommentError: if you attempt to start a comment block when a
160 comment block has already been started.
162 Examples:
163 Starting and ending a comment block with :meth:`start_comment` and
164 :meth:`end_comment`:
166 >>> from csnake import CodeWriter
167 >>> cwr = CodeWriter()
168 >>> cwr.add_line('line_1;')
169 >>> cwr.start_comment()
170 >>> cwr.add_line('line_2;')
171 >>> cwr.add_line('line_3;')
172 >>> cwr.end_comment()
173 >>> cwr.add_line('line_4;')
174 >>> print(cwr)
175 line_1;
176 /*
177 * line_2;
178 * line_3;
179 */
180 line_4;
181 """
182 if self._in_comment:
183 raise NestedCommentError
184 self.add_line("/*", ignore_indent=ignore_indent)
185 self._in_comment = True
187 def end_comment(self, ignore_indent: bool = False) -> None:
188 """End a comment block.
190 A comment block previously started by :meth:`start_comment` is ended.
191 The following code is not inside a block comment.
193 Raises:
194 NoCommentStartError: if you attempt to end a comment block when a
195 comment block hasn't been started.
197 Examples:
198 See :meth:`start_comment`.
199 """
200 if not self._in_comment:
201 raise NoCommentStartError
202 self._in_comment = False
203 self.add_line("*/", ignore_indent=ignore_indent)
205 def open_brace(self) -> None:
206 """Add an open brace and increase indentation for subsequent lines.
208 See :meth:`indent` for more information on how indentation is done.
210 Examples:
211 Using :meth:`open_brace` and :meth:`close_brace`.
213 >>> from csnake import CodeWriter
214 >>> cwr = CodeWriter()
215 >>> cwr.add_line('line_1;')
216 >>> cwr.open_brace()
217 >>> cwr.add_line('line_2;')
218 >>> cwr.add_line('line_3;')
219 >>> cwr.close_brace()
220 >>> cwr.add_line('line_4;')
221 >>> print(cwr)
222 line_1;
223 {
224 line_2;
225 line_3;
226 }
227 line_4;
228 """
229 self.add_line("{")
230 self.indent()
232 def close_brace(self) -> None:
233 """Add a closed brace and decrease indentation for subsequent lines.
235 See :meth:`dedent` for more information on how dedentation is done.
237 Examples:
238 See :meth:`open_brace`.
239 """
240 self.dedent()
241 self.add_line("}")
243 def add(self, text: str) -> None:
244 """Append text to the end of last line.
246 Args:
247 text: :obj:`str` to append to the last line.
249 Raises:
250 ValueError: if :obj:`text` is not a :class:`str`.
251 """
252 try:
253 self.lines[-1]
254 except IndexError:
255 self.lines.append("")
257 self.lines[-1] += text
259 def add_line(
260 self,
261 text: Optional[str] = None,
262 comment: Optional[str] = None,
263 ignore_indent: bool = False,
264 ) -> None:
265 """Add a line of text to the container after the last line.
267 Args:
268 text: :obj:`str` to add
269 comment: optional comment
270 ignore_indent: switch to ignore indentation and insert the code
271 with indentation 0
273 Raises:
274 ValueError: if text or comment isn't a :class:`str`
275 """
277 # empty line
278 if text is None and not comment and not self._in_comment:
279 self.lines.append("")
280 return
282 current_line = ""
284 if not ignore_indent:
285 current_line += self._current_indent
287 if self._in_comment:
288 current_line += "*"
290 # add the text (if appropriate)
291 if text:
292 text = assure_str(text)
293 if "\n" in text:
294 raise ValueError("text mustn't contain multiple lines.")
295 if self._in_comment:
296 current_line += " "
297 current_line += text
299 if comment:
300 comment = assure_str(comment)
301 # add a space if there is text
302 if text:
303 current_line += " "
304 if self._in_comment:
305 current_line += f"{comment}"
306 else:
307 current_line += f"/* {comment} */"
309 self.lines.append(current_line)
311 def add_lines(
312 self, lines: Union[Iterable[str], str], ignore_indent: bool = False
313 ) -> None:
314 """Append code lines from an iterable or a multi-line str.
316 If `lines` is an iterable iterating over :class:`str` objects, adds the
317 lines to the codewriter in order.
319 If `lines` is a multi-line string, adds its lines to the codewriter.
321 Args:
322 lines: an :class:`Iterable` of :class:`str` or a multi-line `str`
323 to add to codewriter.
324 ignore_indent: whether to ignore the current indentation level or
325 not. If `ignore_indent` is true, the lines will be appended
326 without indentation.
328 Raises:
329 ValueError: if one of the arguments is of the wrong type
331 Examples:
332 Adding a list of lines:
334 >>> from csnake import CodeWriter
335 >>> cwr = CodeWriter()
336 >>> lol = ['list', 'of', 'lines']
337 >>> cwr.add_lines(lol)
338 >>> print(cwr)
339 list
340 of
341 lines
342 """
344 if isinstance(lines, str):
345 lines = lines.splitlines()
346 else:
347 lines = chain.from_iterable(
348 supposed_line.splitlines() for supposed_line in lines
349 )
351 for line in lines:
352 self.add_line(line, ignore_indent=ignore_indent)
355assert issubclass(CodeWriterLite, Iterable)