• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

domdfcoding / domdf_python_tools / 3732685666

pending completion
3732685666

push

github

Dominic Davis-Foster
Bump version v3.5.0 -> v3.5.1

2143 of 2200 relevant lines covered (97.41%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.45
/domdf_python_tools/stringlist.py
1
#!/usr/bin/env python
2
#
3
#  stringlist.py
4
"""
1✔
5
A list of strings that represent lines in a multiline string.
6

7
.. versionchanged:: 1.0.0
8

9
        :class:`~domdf_python_tools.typing.String` should now be imported from :mod:`domdf_python_tools.typing`.
10
"""
11
#
12
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
13
#
14
#  Permission is hereby granted, free of charge, to any person obtaining a copy
15
#  of this software and associated documentation files (the "Software"), to deal
16
#  in the Software without restriction, including without limitation the rights
17
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
#  copies of the Software, and to permit persons to whom the Software is
19
#  furnished to do so, subject to the following conditions:
20
#
21
#  The above copyright notice and this permission notice shall be included in all
22
#  copies or substantial portions of the Software.
23
#
24
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
28
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
29
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
30
#  OR OTHER DEALINGS IN THE SOFTWARE.
31
#
32

33
# stdlib
34
from contextlib import contextmanager
1✔
35
from itertools import chain
1✔
36
from typing import Any, Iterable, Iterator, List, Reversible, Tuple, TypeVar, Union, cast, overload
1✔
37

38
# this package
39
from domdf_python_tools.doctools import prettify_docstrings
1✔
40
from domdf_python_tools.typing import String, SupportsIndex
1✔
41
from domdf_python_tools.utils import convert_indents
1✔
42

43
__all__ = ["Indent", "StringList", "DelimitedList", "_SL", "splitlines", "joinlines"]
1✔
44

45
_S = TypeVar("_S")
1✔
46
_SL = TypeVar("_SL", bound="StringList")
1✔
47

48

49
@prettify_docstrings
1✔
50
class Indent:
1✔
51
        """
52
        Represents an indent, having a symbol/type and a size.
53

54
        :param size: The indent size.
55
        :param type: The indent character.
56
        """
57

58
        def __init__(self, size: int = 0, type: str = '\t'):  # noqa: A002  # pylint: disable=redefined-builtin
1✔
59
                self.size = int(size)
1✔
60
                self.type = str(type)
1✔
61

62
        def __iter__(self) -> Iterator[Union[str, Any]]:
1✔
63
                """
64
                Returns the size and type of the :class:`~domdf_python_tools.stringlist.Indent`.
65
                """
66

67
                yield self.size
1✔
68
                yield self.type
1✔
69

70
        @property
1✔
71
        def size(self) -> int:
1✔
72
                """
73
                The indent size.
74
                """
75

76
                return self._size
1✔
77

78
        @size.setter
1✔
79
        def size(self, size: int) -> None:
1✔
80
                self._size = int(size)
1✔
81

82
        @property  # noqa: A003  # pylint: disable=redefined-builtin
1✔
83
        def type(self) -> str:
1✔
84
                """
85
                The indent character.
86
                """
87

88
                return self._type
1✔
89

90
        @type.setter  # noqa: A003  # pylint: disable=redefined-builtin
1✔
91
        def type(self, type: str) -> None:  # noqa: A002  # pylint: disable=redefined-builtin
1✔
92
                if not str(type):
1✔
93
                        raise ValueError("'type' cannot an empty string.")
1✔
94

95
                self._type = str(type)
1✔
96

97
        def __str__(self) -> str:
1✔
98
                """
99
                Returns the :class:`~domdf_python_tools.stringlist.Indent` as a string.
100
                """
101

102
                return self.type * self.size
1✔
103

104
        def __repr__(self) -> str:
1✔
105
                """
106
                Returns the string representation of the :class:`~domdf_python_tools.stringlist.Indent`.
107
                """
108

109
                return f"{type(self).__name__}(size={self.size}, type={self.type!r})"
1✔
110

111
        def __eq__(self, other):
1✔
112
                if isinstance(other, Indent):
1✔
113
                        return other.size == self.size and other.type == self.type
1✔
114
                elif isinstance(other, str):
1✔
115
                        return str(self) == other
1✔
116
                elif isinstance(other, tuple):
1✔
117
                        return tuple(self) == other
1✔
118
                else:
119
                        return NotImplemented
1✔
120

121

122
class StringList(List[str]):
1✔
123
        """
124
        A list of strings that represent lines in a multiline string.
125

126
        :param iterable: Content to populate the StringList with.
127
        :param convert_indents: Whether indents at the start of lines should be converted.
128
        """
129

130
        #: The indent to insert at the beginning of new lines.
131
        indent: Indent
1✔
132

133
        convert_indents: bool
1✔
134
        """
135
        Whether indents at the start of lines should be converted.
136

137
        Only applies to lines added after this is enabled/disabled.
138

139
        Can only be used when the indent is ``'\\t'`` or ``'␣'``.
140
        """
141

142
        def __init__(
1✔
143
                        self,
144
                        iterable: Iterable[String] = (),
145
                        convert_indents: bool = False,
146
                        ) -> None:
147

148
                if isinstance(iterable, str):
1✔
149
                        iterable = iterable.split('\n')
1✔
150

151
                self.indent = Indent()
1✔
152
                self.convert_indents = convert_indents
1✔
153
                super().__init__([self._make_line(str(x)) for x in iterable])
1✔
154

155
        def _make_line(self, line: str) -> str:
1✔
156
                if not str(self.indent_type).strip(" \t") and self.convert_indents:
1✔
157
                        if self.indent_type == '\t':
1✔
158
                                line = convert_indents(line, tab_width=1, from_="    ", to='\t')
1✔
159
                        else:  # pragma: no cover
160
                                line = convert_indents(line, tab_width=1, from_='\t', to=self.indent_type)
161

162
                return f"{self.indent}{line}".rstrip()
1✔
163

164
        def append(self, line: String) -> None:
1✔
165
                """
166
                Append a line to the end of the :class:`~domdf_python_tools.stringlist.StringList`.
167

168
                :param line:
169
                """
170

171
                for inner_line in str(line).split('\n'):
1✔
172
                        super().append(self._make_line(inner_line))
1✔
173

174
        def extend(self, iterable: Iterable[String]) -> None:
1✔
175
                """
176
                Extend the :class:`~domdf_python_tools.stringlist.StringList` with lines from ``iterable``.
177

178
                :param iterable: An iterable of string-like objects to add to the end of the
179
                        :class:`~domdf_python_tools.stringlist.StringList`.
180
                """
181

182
                for line in iterable:
1✔
183
                        self.append(line)
1✔
184

185
        def copy(self: _SL) -> _SL:
1✔
186
                """
187
                Returns a shallow copy of the :class:`~domdf_python_tools.stringlist.StringList`.
188

189
                Equivalent to ``a[:]``.
190

191
                :rtype: :class:`~domdf_python_tools.stringlist.StringList`
192
                """
193

194
                return self.__class__(super().copy())
1✔
195

196
        def count_blanklines(self) -> int:
1✔
197
                """
198
                Returns a count of the blank lines in the :class:`~domdf_python_tools.stringlist.StringList`.
199

200
                .. versionadded:: 0.7.1
201
                """
202

203
                return self.count('')
1✔
204

205
        def insert(self, index: SupportsIndex, line: String) -> None:
1✔
206
                """
207
                Insert a line into the :class:`~domdf_python_tools.stringlist.StringList` at the given position.
208

209
                :param index:
210
                :param line:
211

212
                .. versionchanged:: 3.2.0  Changed :class:`int` in the type annotation to :protocol:`~.SupportsIndex`.
213
                """
214

215
                lines: List[str]
216

217
                index = index.__index__()
1✔
218

219
                if index < 0 or index > len(self):
1✔
220
                        lines = str(line).split('\n')
1✔
221
                else:
222
                        lines = cast(list, reversed(str(line).split('\n')))
1✔
223

224
                for inner_line in lines:
1✔
225
                        super().insert(index, self._make_line(inner_line))
1✔
226

227
        @overload
1✔
228
        def __setitem__(self, index: SupportsIndex, line: String) -> None: ...
1✔
229

230
        @overload
1✔
231
        def __setitem__(self, index: slice, line: Iterable[String]) -> None: ...
1✔
232

233
        def __setitem__(self, index: Union[SupportsIndex, slice], line: Union[String, Iterable[String]]):
1✔
234
                """
235
                Replaces the given line with new content.
236

237
                If the new content consists of multiple lines subsequent content in the
238
                :class:`~domdf_python_tools.stringlist.StringList` will be shifted down.
239

240
                :param index:
241
                :param line:
242

243
                .. versionchanged:: 3.2.0  Changed :class:`int` in the type annotation to :protocol:`~.SupportsIndex`.
244
                """
245

246
                if isinstance(index, slice):
1✔
247
                        line = cast(Iterable[String], line)
1✔
248

249
                        if not isinstance(line, Reversible):
1✔
250
                                line = tuple(line)
×
251

252
                        for lline, idx in zip(
1✔
253
                                reversed(line),
254
                                reversed(range(index.start or 0, index.stop + 1, index.step or 1)),
255
                                ):
256
                                self[idx] = lline
1✔
257
                else:
258
                        line = cast(String, line)
1✔
259
                        index = index.__index__()
1✔
260

261
                        if self and index < len(self):
1✔
262
                                self.pop(index)
1✔
263
                        if index < 0:
1✔
264
                                index = len(self) + index + 1
1✔
265

266
                        self.insert(index, line)
1✔
267

268
        @overload
1✔
269
        def __getitem__(self, index: SupportsIndex) -> str: ...
1✔
270

271
        @overload
1✔
272
        def __getitem__(self: _SL, index: slice) -> _SL: ...
1✔
273

274
        def __getitem__(self: _SL, index: Union[SupportsIndex, slice]) -> Union[str, _SL]:
1✔
275
                r"""
276
                Returns the line with the given index.
277

278
                :param index:
279

280
                :rtype: :py:obj:`~typing.Union`\[:class:`str`, :class:`~domdf_python_tools.stringlist.StringList`\]
281

282
                .. versionchanged:: 1.8.0
283

284
                        Now returns a :class:`~domdf_python_tools.stringlist.StringList` when ``index`` is a :class:`slice`.
285

286
                .. versionchanged:: 3.2.0  Changed :class:`int` in the type annotation to :protocol:`~.SupportsIndex`.
287
                """
288

289
                if isinstance(index, slice):
1✔
290
                        return self.__class__(super().__getitem__(index))
1✔
291
                else:
292
                        return super().__getitem__(index)
1✔
293

294
        def blankline(self, ensure_single: bool = False):
1✔
295
                """
296
                Append a blank line to the end of the :class:`~domdf_python_tools.stringlist.StringList`.
297

298
                :param ensure_single: Ensure only a single blank line exists after the previous line of text.
299
                """
300

301
                if ensure_single:
1✔
302
                        while self and not self[-1]:
1✔
303
                                self.pop(-1)
1✔
304

305
                self.append('')
1✔
306

307
        def set_indent_size(self, size: int = 0):
1✔
308
                """
309
                Sets the size of the indent to insert at the beginning of new lines.
310

311
                :param size: The indent size to use for new lines.
312
                """
313

314
                self.indent.size = int(size)
1✔
315

316
        def set_indent_type(self, indent_type: str = '\t'):
1✔
317
                """
318
                Sets the type of the indent to insert at the beginning of new lines.
319

320
                :param indent_type: The type of indent to use for new lines.
321
                """
322

323
                self.indent.type = str(indent_type)
1✔
324

325
        def set_indent(self, indent: Union[String, Indent], size: int = 0):
1✔
326
                """
327
                Sets the indent to insert at the beginning of new lines.
328

329
                :param indent: The :class:`~.Indent` to use for new lines, or the indent type.
330
                :param size: If ``indent`` is an indent type, the indent size to use for new lines.
331
                """
332

333
                if isinstance(indent, Indent):
1✔
334
                        if size:
1✔
335
                                raise TypeError("'size' argument cannot be used when providing an 'Indent' object.")
1✔
336

337
                        self.indent = indent
1✔
338
                else:
339
                        self.indent = Indent(int(size), str(indent))
1✔
340

341
        @property
1✔
342
        def indent_size(self) -> int:
1✔
343
                """
344
                The current indent size.
345
                """
346

347
                return int(self.indent.size)
1✔
348

349
        @indent_size.setter
1✔
350
        def indent_size(self, size: int) -> None:
1✔
351
                """
352
                Sets the indent size.
353
                """
354

355
                self.indent.size = int(size)
1✔
356

357
        @property
1✔
358
        def indent_type(self) -> str:
1✔
359
                """
360
                The current indent type.
361
                """
362

363
                return str(self.indent.type)
1✔
364

365
        @indent_type.setter
1✔
366
        def indent_type(self, type: str) -> None:  # noqa: A002  # pylint: disable=redefined-builtin
1✔
367
                """
368
                Sets the indent type.
369
                """
370

371
                self.indent.type = str(type)
1✔
372

373
        def __str__(self) -> str:
1✔
374
                """
375
                Returns the :class:`~domdf_python_tools.stringlist.StringList` as a string.
376
                """
377

378
                return '\n'.join(self)
1✔
379

380
        def __bytes__(self) -> bytes:
1✔
381
                """
382
                Returns the :class:`~domdf_python_tools.stringlist.StringList` as bytes.
383

384
                .. versionadded:: 2.1.0
385
                """
386

387
                return str(self).encode("UTF-8")
1✔
388

389
        def __eq__(self, other) -> bool:
1✔
390
                """
391
                Returns whether the other object is equal to this :class:`~domdf_python_tools.stringlist.StringList`.
392
                """
393

394
                if isinstance(other, str):
1✔
395
                        return str(self) == other
1✔
396
                else:
397
                        return super().__eq__(other)
1✔
398

399
        @contextmanager
1✔
400
        def with_indent(self, indent: Union[String, Indent], size: int = 0):
1✔
401
                """
402
                Context manager to temporarily use a different indent.
403

404
                .. code-block:: python
405

406
                        >>> sl = StringList()
407
                        >>> with sl.with_indent("    ", 1):
408
                        ...     sl.append("Hello World")
409

410
                :param indent: The :class:`~.Indent` to use within the ``with`` block, or the indent type.
411
                :param size: If ``indent`` is an indent type, the indent size to use within the ``with`` block.
412
                """
413

414
                original_indent: Tuple[int, str] = tuple(self.indent)  # type: ignore
1✔
415

416
                try:
1✔
417
                        self.set_indent(indent, size)
1✔
418
                        yield
1✔
419
                finally:
420
                        self.indent = Indent(*original_indent)
1✔
421

422
        @contextmanager
1✔
423
        def with_indent_size(self, size: int = 0):
1✔
424
                """
425
                Context manager to temporarily use a different indent size.
426

427
                .. code-block:: python
428

429
                        >>> sl = StringList()
430
                        >>> with sl.with_indent_size(1):
431
                        ...     sl.append("Hello World")
432

433
                :param size: The indent size to use within the ``with`` block.
434
                """
435

436
                original_indent_size = self.indent_size
1✔
437

438
                try:
1✔
439
                        self.indent_size = size
1✔
440
                        yield
1✔
441
                finally:
442
                        self.indent_size = original_indent_size
1✔
443

444
        @contextmanager
1✔
445
        def with_indent_type(self, indent_type: str = '\t'):
1✔
446
                """
447
                Context manager to temporarily use a different indent type.
448

449
                .. code-block:: python
450

451
                        >>> sl = StringList()
452
                        >>> with sl.with_indent_type("    "):
453
                        ...     sl.append("Hello World")
454

455
                :param indent_type: The type of indent to use within the ``with`` block.
456
                """
457

458
                original_indent_type = self.indent_type
1✔
459

460
                try:
1✔
461
                        self.indent_type = indent_type
1✔
462
                        yield
1✔
463
                finally:
464
                        self.indent_type = original_indent_type
1✔
465

466

467
class DelimitedList(List[_S]):
1✔
468
        """
469
        Subclass of :class:`list` that supports custom delimiters in format strings.
470

471
        **Example:**
472

473
        .. code-block:: python
474

475
                >>> l = DelimitedList([1, 2, 3, 4, 5])
476
                >>> format(l, ", ")
477
                '1, 2, 3, 4, 5'
478
                >>> f"Numbers: {l:, }"
479
                'Numbers: 1, 2, 3, 4, 5'
480

481
        .. autoclasssumm:: DelimitedList
482
                :autosummary-sections: ;;
483

484
        .. versionadded:: 1.1.0
485
        """
486

487
        def __format__(self, format_spec: str) -> str:
1✔
488
                return format_spec.join([str(x) for x in self])  # pylint: disable=not-an-iterable
1✔
489

490

491
def splitlines(string: str) -> List[Tuple[str, str]]:
1✔
492
        """
493
        Split ``string`` into a list of two-element tuples,
494
        containing the line content and the newline character(s), if any.
495

496
        .. versionadded:: 3.2.0
497

498
        :param string:
499

500
        :rtype:
501

502
        .. seealso:: :meth:`str.splitlines` and :func:`~.stringlist.joinlines`
503
        """  # noqa: D400
504

505
        # Translated and adapted from https://github.com/python/cpython/blob/main/Objects/stringlib/split.h
506

507
        str_len: int = len(string)
1✔
508
        i: int = 0
1✔
509
        j: int = 0
1✔
510
        eol: int
511
        the_list: List[Tuple[str, str]] = []
1✔
512

513
        while i < str_len:
1✔
514

515
                # Find a line and append it
516
                while i < str_len and not string[i] in "\n\r":
1✔
517
                        i += 1
1✔
518

519
                # Skip the line break reading CRLF as one line break
520
                eol = i
1✔
521
                if i < str_len:
1✔
522
                        if (string[i] == '\r') and (i + 1 < str_len) and (string[i + 1] == '\n'):
1✔
523
                                i += 2
1✔
524
                        else:
525
                                i += 1
1✔
526

527
                if j == 0 and eol == str_len and type(string) is str:  # pylint: disable=unidiomatic-typecheck
1✔
528
                        # No whitespace in string, so just use it as the_list[0]
529
                        the_list.append((string, ''))
1✔
530
                        break
1✔
531

532
                the_list.append((string[j:eol], string[eol:i]))
1✔
533
                j = i
1✔
534

535
        return the_list
1✔
536

537

538
def joinlines(lines: List[Tuple[str, str]]) -> str:
1✔
539
        """
540
        Given a list of two-element tuples, each containing a line and a newline character (or empty string),
541
        return a single string.
542

543
        .. versionadded:: 3.2.0
544

545
        :param lines:
546

547
        :rtype:
548

549
        .. seealso:: :func:`~.stringlist.splitlines`
550
        """  # noqa: D400
551

552
        return ''.join(chain.from_iterable(lines))
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc