Skip to content

Commit af92ab5

Browse files
committed
Added stringlist module.
1 parent 4041cb6 commit af92ab5

File tree

3 files changed

+648
-0
lines changed

3 files changed

+648
-0
lines changed

doc-source/api/stringlist.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
=====================================
2+
:mod:`domdf_python_tools.stringlist`
3+
=====================================
4+
5+
.. automodule:: domdf_python_tools.stringlist
6+
:special-members:

domdf_python_tools/stringlist.py

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
#!/usr/bin/env python
2+
#
3+
# stringlist.py
4+
"""
5+
A list of strings that represent lines in a multiline string.
6+
"""
7+
#
8+
# Copyright © 2020 Dominic Davis-Foster <[email protected]>
9+
#
10+
# This program is free software; you can redistribute it and/or modify
11+
# it under the terms of the GNU Lesser General Public License as published by
12+
# the Free Software Foundation; either version 3 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU Lesser General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU Lesser General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23+
# MA 02110-1301, USA.
24+
#
25+
26+
# stdlib
27+
from contextlib import contextmanager
28+
from typing import cast, Iterable, List, Tuple, Union, overload
29+
30+
# 3rd party
31+
from typing_extensions import Protocol
32+
33+
# this package
34+
from domdf_python_tools.utils import convert_indents
35+
36+
__all__ = ["String", "Indent", "StringList"]
37+
38+
39+
class String(Protocol):
40+
"""
41+
Protocol for classes that implement ``__str__``.
42+
"""
43+
44+
def __str__(self) -> str:
45+
...
46+
47+
48+
class Indent:
49+
"""
50+
Represents an indent, having a symbol/type and a size.
51+
"""
52+
53+
def __init__(self, size: int = 0, type: str = "\t"):
54+
self.size = int(size)
55+
self.type = str(type)
56+
57+
def __iter__(self) -> Iterable:
58+
"""
59+
Returns the size and type of the :class:`~domdf_python_tools.stringlist.Indent`
60+
"""
61+
62+
yield self.size
63+
yield self.type
64+
65+
@property
66+
def size(self) -> int:
67+
"""
68+
The indent size
69+
"""
70+
return self._size
71+
72+
@size.setter
73+
def size(self, size: int) -> None:
74+
self._size = int(size)
75+
76+
@property
77+
def type(self) -> str:
78+
"""
79+
The indent character
80+
"""
81+
82+
return self._type
83+
84+
@type.setter
85+
def type(self, type: str) -> None:
86+
if not str(type):
87+
raise ValueError("'type' cannot an empty string.")
88+
89+
self._type = str(type)
90+
91+
def __str__(self) -> str:
92+
"""
93+
Returns the :class:`~domdf_python_tools.stringlist.Indent` as a string.
94+
"""
95+
96+
return self.type * self.size
97+
98+
def __repr__(self) -> str:
99+
"""
100+
Returns the string representation of the :class:`~domdf_python_tools.stringlist.Indent`.
101+
"""
102+
103+
return f"{type(self).__name__}(size={self.size}, type={self.type!r})"
104+
105+
106+
class StringList(List[str]):
107+
"""
108+
A list of strings that represent lines in a multiline string.
109+
"""
110+
111+
#: The indent to insert at the beginning of new lines.
112+
indent: Indent
113+
114+
convert_indents: bool
115+
"""
116+
Whether indents at the start of lines should be converted.
117+
118+
Only applies to lines added after this is enabled/disabled.
119+
120+
Can only be used when the indent is ``'\\t'`` or ``'␣'``.
121+
"""
122+
123+
def __init__(self, iterable: Iterable[String] = (), convert_indents: bool = False) -> None:
124+
if isinstance(iterable, str):
125+
iterable = iterable.split("\n")
126+
127+
super().__init__([str(x).strip() for x in iterable])
128+
self.indent = Indent()
129+
self.convert_indents = convert_indents
130+
131+
def _make_line(self, line: str) -> str:
132+
if not str(self.indent).strip(" \t") and self.convert_indents:
133+
if self.indent_type == '\t':
134+
line = convert_indents(line, tab_width=1, from_=' ', to='\t')
135+
else:
136+
line = convert_indents(line, tab_width=1, from_='\t', to=self.indent_type)
137+
138+
return f"{self.indent}{line}".rstrip()
139+
140+
def append(self, line: String) -> None:
141+
"""
142+
Append a line to the end of the StringList.
143+
144+
:param line:
145+
"""
146+
147+
for inner_line in str(line).split("\n"):
148+
super().append(self._make_line(inner_line))
149+
150+
def insert(self, index: int, line: String) -> None:
151+
"""
152+
Insert a line into the StringList at the given position.
153+
154+
:param index:
155+
:param line:
156+
"""
157+
158+
lines: List[str]
159+
160+
if index < 0 or index > len(self):
161+
lines = str(line).split("\n")
162+
else:
163+
lines = cast(list, reversed(str(line).split("\n")))
164+
165+
for inner_line in lines:
166+
super().insert(index, self._make_line(inner_line))
167+
168+
@overload
169+
def __setitem__(self, index: int, line: String) -> None:
170+
... # pragma: no cover
171+
172+
@overload
173+
def __setitem__(self, index: slice, line: Iterable[String]) -> None:
174+
... # pragma: no cover
175+
176+
def __setitem__(self, index: Union[int, slice], line: Union[String, Iterable[String]]):
177+
"""
178+
Replaces the given line with new content.
179+
180+
If the new content consists of multiple lines subsequent content in the
181+
:class:`~domdf_python_tools.stringlist.StringList` will be shifted down.
182+
183+
:param index:
184+
:param line:
185+
"""
186+
187+
if isinstance(index, int):
188+
if self and index < len(self):
189+
self.pop(index)
190+
self.insert(index, line)
191+
192+
elif isinstance(index, slice):
193+
for line, index in zip(
194+
reversed(line), # type: ignore
195+
reversed(range(index.start, index.stop + 1, index.step or 1)),
196+
):
197+
self[index] = line
198+
199+
@overload
200+
def __getitem__(self, index: int) -> str:
201+
... # pragma: no cover
202+
203+
@overload
204+
def __getitem__(self, index: slice) -> List[str]:
205+
... # pragma: no cover
206+
207+
def __getitem__(self, index: Union[int, slice]) -> Union[str, List[str]]:
208+
"""
209+
Returns the line with the given index.
210+
211+
:param index:
212+
"""
213+
214+
return super().__getitem__(index)
215+
216+
def blankline(self, ensure_single: bool = False):
217+
"""
218+
Append a blank line to the end of the :class:`~domdf_python_tools.stringlist.StringList`.
219+
220+
:param ensure_single: Ensure only a single blank line exists after the previous line of text.
221+
"""
222+
223+
if ensure_single:
224+
while self and not self[-1]:
225+
self.pop(-1)
226+
227+
self.append('')
228+
229+
def set_indent_size(self, size: int = 0):
230+
"""
231+
Sets the size of the indent to insert at the beginning of new lines.
232+
233+
:param size:
234+
"""
235+
236+
self.indent.size = int(size)
237+
238+
def set_indent_type(self, indent_type: str = "\t"):
239+
"""
240+
Sets the type of the indent to insert at the beginning of new lines.
241+
242+
:param indent_type:
243+
"""
244+
245+
self.indent.type = str(indent_type)
246+
247+
def set_indent(self, indent: Union[String, Indent], size: int = 0):
248+
"""
249+
Sets the indent to insert at the beginning of new lines.
250+
251+
:param indent:
252+
:param size:
253+
"""
254+
255+
if isinstance(indent, Indent):
256+
if size:
257+
raise TypeError("'size' argument cannot be used when providing an 'Indent' object.")
258+
259+
self.indent = indent
260+
else:
261+
self.indent = Indent(int(size), str(indent))
262+
263+
@property
264+
def indent_size(self) -> int:
265+
"""
266+
The current indent size.
267+
"""
268+
269+
return int(self.indent.size)
270+
271+
@indent_size.setter
272+
def indent_size(self, size: int) -> None:
273+
"""
274+
Sets the indent size.
275+
"""
276+
277+
self.indent.size = int(size)
278+
279+
@property
280+
def indent_type(self) -> str:
281+
"""
282+
The current indent type.
283+
"""
284+
285+
return str(self.indent.type)
286+
287+
@indent_type.setter
288+
def indent_type(self, type: str) -> None:
289+
"""
290+
Sets the indent type.
291+
"""
292+
293+
self.indent.type = str(type)
294+
295+
def __str__(self) -> str:
296+
"""
297+
Returns the :class:`~domdf_python_tools.stringlist.StringList` as a string.
298+
"""
299+
300+
return "\n".join(self)
301+
302+
def __eq__(self, other) -> bool:
303+
"""
304+
Returns whether the other object is equal to this :class:`~domdf_python_tools.stringlist.StringList`.
305+
"""
306+
307+
if isinstance(other, str):
308+
return str(self) == other
309+
else:
310+
return super().__eq__(other)
311+
312+
@contextmanager
313+
def with_indent(self, indent: Union[String, Indent], size: int = 0):
314+
"""
315+
Context manager to temporarily use a different indent.
316+
317+
.. code-block:: python
318+
319+
>>> sl = StringList()
320+
>>> with sl.with_indent(" ", 1):
321+
... sl.append("Hello World")
322+
323+
:param indent:
324+
:param size:
325+
"""
326+
327+
original_indent: Tuple[int, str] = tuple(self.indent) # type: ignore
328+
329+
try:
330+
self.set_indent(indent, size)
331+
yield
332+
finally:
333+
self.indent = Indent(*original_indent)
334+
335+
@contextmanager
336+
def with_indent_size(self, size: int = 0):
337+
"""
338+
Context manager to temporarily use a different indent size.
339+
340+
.. code-block:: python
341+
342+
>>> sl = StringList()
343+
>>> with sl.with_indent_size(1):
344+
... sl.append("Hello World")
345+
346+
:param size:
347+
"""
348+
349+
original_indent_size = self.indent_size
350+
351+
try:
352+
self.indent_size = size
353+
yield
354+
finally:
355+
self.indent_size = original_indent_size
356+
357+
@contextmanager
358+
def with_indent_type(self, indent_type: str = "\t"):
359+
"""
360+
Context manager to temporarily use a different indent type.
361+
362+
.. code-block:: python
363+
364+
>>> sl = StringList()
365+
>>> with sl.with_indent_type(" "):
366+
... sl.append("Hello World")
367+
368+
:param indent_type:
369+
"""
370+
371+
original_indent_type = self.indent_type
372+
373+
try:
374+
self.indent_type = indent_type
375+
yield
376+
finally:
377+
self.indent_type = original_indent_type

0 commit comments

Comments
 (0)