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

domdfcoding / domdf_python_tools / 14915795784

08 May 2025 08:45PM CUT coverage: 97.313%. Remained the same
14915795784

push

github

web-flow
Updated files with 'repo_helper'. (#134)

Co-authored-by: repo-helper[bot] <74742576+repo-helper[bot]@users.noreply.github.com>

2137 of 2196 relevant lines covered (97.31%)

0.97 hits per line

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

98.23
/domdf_python_tools/iterative.py
1
#!/usr/bin/env python
2
#
3
#  iterative.py
4
"""
5
Functions for iteration, looping etc.
6

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

35
# stdlib
36
import itertools
1✔
37
import textwrap
1✔
38
from operator import itemgetter
1✔
39
from typing import (
1✔
40
                Any,
41
                Callable,
42
                Iterable,
43
                Iterator,
44
                List,
45
                Optional,
46
                Sequence,
47
                Sized,
48
                Tuple,
49
                Type,
50
                TypeVar,
51
                Union,
52
                cast
53
                )
54

55
# 3rd party
56
from natsort import natsorted, ns
1✔
57
from typing_extensions import final
1✔
58

59
# this package
60
from domdf_python_tools.utils import magnitude
1✔
61

62
__all__ = [
1✔
63
                "chunks",
64
                "permutations",
65
                "split_len",
66
                "Len",
67
                "double_chain",
68
                "flatten",
69
                "make_tree",
70
                "natmin",
71
                "natmax",
72
                "groupfloats",
73
                "ranges_from_iterable",
74
                "extend",
75
                "extend_with",
76
                "extend_with_none",
77
                "count",
78
                "AnyNum",
79
                ]
80

81
_T = TypeVar("_T")
1✔
82
AnyNum = TypeVar("AnyNum", float, complex)
1✔
83

84

85
def chunks(l: Sequence[_T], n: int) -> Iterator[Sequence[_T]]:
1✔
86
        """
87
        Yield successive ``n``-sized chunks from ``l``.
88

89
        :param l: The objects to yield chunks from.
90
        :param n: The size of the chunks.
91

92
        :rtype:
93

94
        .. versionchanged:: 1.4.0 Moved from :mod:`domdf_python_tools.utils`
95
        """
96

97
        for i in range(0, len(l), n):
1✔
98
                yield l[i:i + n]
1✔
99

100

101
def permutations(data: Iterable[_T], n: int = 2) -> List[Tuple[_T, ...]]:
1✔
102
        """
103
        Return permutations containing ``n`` items from ``data`` without any reverse duplicates.
104

105
        If ``n`` is equal to or greater than the length of the data an empty list of returned.
106

107
        :param data:
108
        :param n:
109

110
        :rtype:
111

112
        .. versionchanged:: 1.4.0 Moved from :mod:`domdf_python_tools.utils`
113
        .. seealso:: :func:`itertools.permutations` and :func:`itertools.combinations`
114
        .. latex:clearpage::
115
        """
116

117
        if n == 0:
1✔
118
                raise ValueError("'n' cannot be 0")
1✔
119

120
        perms = []
1✔
121
        for i in itertools.permutations(data, n):
1✔
122
                if i[::-1] not in perms:
1✔
123
                        perms.append(i)
1✔
124

125
        return perms
1✔
126

127

128
def split_len(string: str, n: int) -> List[str]:
1✔
129
        """
130
        Split ``string`` every ``n`` characters.
131

132
        :param string:
133
        :param n: The number of characters to split after
134

135
        :return: The split string
136

137
        .. versionchanged:: 1.4.0 Moved from :mod:`domdf_python_tools.utils`
138
        """
139

140
        return [string[i:i + n] for i in range(0, len(string), n)]
1✔
141

142

143
def Len(obj: Sized, start: int = 0, step: int = 1) -> range:
1✔
144
        """
145
        Shorthand for ``range(len(obj))``.
146

147
        Returns an object that produces a sequence of integers from ``start`` (inclusive)
148
        to :func:`len(obj) <len>` (exclusive) by ``step``.
149

150
        .. versionadded:: 0.4.7
151

152
        :param obj: The object to iterate over the length of.
153
        :param start: The start value of the range.
154
        :param step: The step of the range.
155

156
        :rtype:
157

158
        .. versionchanged:: 1.4.0 Moved from :mod:`domdf_python_tools.utils`
159
        """
160

161
        return range(start, len(obj), step)
1✔
162

163

164
def double_chain(iterable: Iterable[Iterable[Iterable[_T]]]) -> Iterator[_T]:
1✔
165
        """
166
        Flatten a list of lists of lists into a single list.
167

168
        Literally just:
169

170
        .. code-block:: python
171

172
                chain.from_iterable(chain.from_iterable(iterable))
173

174
        .. compound::
175

176
                Will convert
177

178
                .. code-block:: python
179

180
                        [[(1, 2), (3, 4)], [(5, 6), (7, 8)]]
181

182
                to
183

184
                .. code-block:: python
185

186
                        [1, 2, 3, 4, 5, 6, 7, 8]
187

188
        .. versionadded:: 0.4.7
189

190
        :param iterable: The iterable to chain.
191

192
        :rtype:
193

194
        .. versionchanged:: 1.4.0 Moved from :mod:`domdf_python_tools.utils`
195
        """
196

197
        return itertools.chain.from_iterable(itertools.chain.from_iterable(iterable))
1✔
198

199

200
def flatten(iterable: Iterable[_T], primitives: Tuple[Type, ...] = (str, int, float)) -> Iterator[_T]:
1✔
201
        """
202
        Flattens a mixed list of primitive types and iterables of those types into a single list,
203
        regardless of nesting.
204

205
        .. versionadded:: 1.4.0
206

207
        :param iterable:
208
        :param primitives: The primitive types to allow.
209
        """  # noqa: D400
210

211
        for item in iterable:
1✔
212
                if isinstance(item, primitives):
1✔
213
                        yield item
1✔
214
                elif isinstance(item, Iterable):
1✔
215
                        yield from flatten(item)
1✔
216
                else:
217
                        raise NotImplementedError
218

219

220
Branch = Union[Sequence[str], Sequence[Union[Sequence[str], Sequence]]]
1✔
221

222

223
def make_tree(tree: Branch) -> Iterator[str]:
1✔
224
        """
225
        Returns the string representation of a mixed list of strings and lists of strings,
226
        similar to :manpage:`tree(1)`.
227

228
        .. versionadded:: 1.4.0
229

230
        :param tree:
231
        """  # noqa: D400
232

233
        last_string = 0
1✔
234
        for idx, entry in enumerate(tree):
1✔
235
                if isinstance(entry, str):
1✔
236
                        last_string = idx
1✔
237

238
        for idx, entry in enumerate(tree[:-1]):
1✔
239
                if isinstance(entry, str):
1✔
240
                        if idx > last_string:
1✔
241
                                yield f"│   {entry}"
×
242
                        elif idx == last_string:
1✔
243
                                yield f"└── {entry}"
1✔
244
                        else:
245
                                yield f"├── {entry}"
1✔
246

247
                elif isinstance(entry, Iterable):
1✔
248
                        for line in make_tree(entry):
1✔
249
                                if idx - 1 == last_string:
1✔
250
                                        yield textwrap.indent(line, "└── ")
×
251
                                else:
252
                                        yield textwrap.indent(line, "│   ")
1✔
253

254
        if tree:
1✔
255
                if isinstance(tree[-1], str):
1✔
256
                        yield f"└── {tree[-1]}"
1✔
257
                elif isinstance(tree[-1], Iterable):
1✔
258
                        for line in make_tree(tree[-1]):
1✔
259
                                yield textwrap.indent(line, "    ")
1✔
260

261

262
def natmin(seq: Iterable[_T], key: Optional[Callable[[Any], Any]] = None, alg: int = ns.DEFAULT) -> _T:
1✔
263
        """
264
        Returns the minimum value from ``seq`` when sorted naturally.
265

266
        .. versionadded:: 1.8.0
267

268
        :param seq:
269
        :param key: A key used to determine how to sort each element of the iterable.
270
                It is **not** applied recursively.
271
                The callable should accept a single argument and return a single value.
272
        :param alg: This option is used to control which algorithm :mod:`natsort` uses when sorting.
273
        """
274

275
        return natsorted(seq, key=key, alg=cast(ns, alg))[0]
1✔
276

277

278
def natmax(seq: Iterable[_T], key: Optional[Callable[[Any], Any]] = None, alg: int = ns.DEFAULT) -> _T:
1✔
279
        """
280
        Returns the maximum value from ``seq`` when sorted naturally.
281

282
        .. versionadded:: 1.8.0
283

284
        :param seq:
285
        :param key: A key used to determine how to sort each element of the iterable.
286
                It is **not** applied recursively.
287
                The callable should accept a single argument and return a single value.
288
        :param alg: This option is used to control which algorithm :mod:`natsort` uses when sorting.
289
        """
290

291
        return natsorted(seq, key=key, alg=cast(ns, alg))[-1]
1✔
292

293

294
_group = Tuple[float, ...]
1✔
295

296

297
def groupfloats(
1✔
298
                iterable: Iterable[float],
299
                step: float = 1,
300
                ) -> Iterable[_group]:
301
        """
302
        Returns an iterator over the discrete ranges of values in ``iterable``.
303

304
        For example:
305

306
        .. code-block:: python
307

308
                >>> list(groupfloats(
309
                ...         [170.0, 170.05, 170.1, 170.15, 171.05, 171.1, 171.15, 171.2],
310
                ...         step=0.05,
311
                ... ))
312
                [(170.0, 170.05, 170.1, 170.15), (171.05, 171.1, 171.15, 171.2)]
313
                >>> list(groupfloats([1, 2, 3, 4, 5, 7, 8, 9, 10]))
314
                [(1, 2, 3, 4, 5), (7, 8, 9, 10)]
315

316
        .. versionadded:: 2.0.0
317

318
        :param iterable:
319
        :param step: The step between values in ``iterable``.
320

321
        :rtype:
322

323
        .. seealso::
324

325
                :func:`~.ranges_from_iterable`, which returns an iterator over the min and max values for each range.
326
        """
327

328
        # Based on https://stackoverflow.com/a/4629241
329
        # By user97370
330
        # CC BY-SA 4.0
331

332
        modifier = 1 / 10**magnitude(step)
1✔
333

334
        a: float
335
        b: Iterable[_group]
336

337
        def key(pair):
1✔
338
                return (pair[1] * modifier) - ((pair[0] * modifier) * step)
1✔
339

340
        for a, b in itertools.groupby(enumerate(iterable), key=key):
1✔
341
                yield tuple(map(itemgetter(1), list(b)))
1✔
342

343

344
def ranges_from_iterable(iterable: Iterable[float], step: float = 1) -> Iterable[Tuple[float, float]]:
1✔
345
        """
346
        Returns an iterator over the minimum and maximum values for each discrete ranges of values in ``iterable``.
347

348
        For example:
349

350
        .. code-block:: python
351

352
                >>> list(ranges_from_iterable([170.0, 170.05, 170.1, 170.15, 171.05, 171.1, 171.15, 171.2], step=0.05))
353
                [(170.0, 170.15), (171.05, 171.2)]
354
                >>> list(ranges_from_iterable([1, 2, 3, 4, 5, 7, 8, 9, 10]))
355
                [(1, 5), (7, 10)]
356

357
        :param iterable:
358
        :param step: The step between values in ``iterable``.
359
        """
360

361
        for group in groupfloats(iterable, step):
1✔
362
                yield group[0], group[-1]
1✔
363

364

365
def extend(sequence: Iterable[_T], minsize: int) -> List[_T]:
1✔
366
        """
367
        Extend ``sequence`` by repetition until it is at least as long as ``minsize``.
368

369
        .. versionadded:: 2.3.0
370

371
        :param sequence:
372
        :param minsize:
373

374
        :rtype:
375

376
        .. seealso:: :func:`~.extend_with` and :func:`~.extend_with_none`
377
        """
378

379
        output = list(sequence)
1✔
380
        cycle = itertools.cycle(output)
1✔
381

382
        while len(output) < minsize:
1✔
383
                output.append(next(cycle))
1✔
384

385
        return output
1✔
386

387

388
def extend_with(sequence: Iterable[_T], minsize: int, with_: _T) -> List[_T]:
1✔
389
        r"""
390
        Extend ``sequence`` by adding ``with\_`` to the right hand end until it is at least as long as ``minsize``.
391

392
        .. versionadded:: 2.3.0
393

394
        :param sequence:
395
        :param minsize:
396
        :param with\_:
397

398
        :rtype:
399

400
        .. seealso:: :func:`~.extend` and :func:`~.extend_with_none`
401
        .. latex:clearpage::
402
        """
403

404
        output = list(sequence)
1✔
405

406
        while len(output) < minsize:
1✔
407
                output.append(with_)
1✔
408

409
        return output
1✔
410

411

412
def extend_with_none(sequence: Iterable[_T], minsize: int) -> Sequence[Optional[_T]]:
1✔
413
        r"""
414
        Extend ``sequence`` by adding :py:obj:`None` to the right hand end until it is at least as long as ``minsize``.
415

416
        .. versionadded:: 2.3.0
417

418
        :param sequence:
419
        :param minsize:
420

421
        :rtype:
422

423
        .. seealso:: :func:`~.extend` and :func:`~.extend_with`
424
        """
425

426
        output: Sequence[Optional[_T]] = list(sequence)
1✔
427
        filler: Sequence[Optional[_T]] = [None] * max(0, minsize - len(output))
1✔
428

429
        return tuple((*output, *filler))
1✔
430

431

432
def count(start: AnyNum = 0, step: AnyNum = 1) -> Iterator[AnyNum]:
1✔
433
        """
434
        Make an iterator which returns evenly spaced values starting with number ``start``.
435

436
        Often used as an argument to :func:`map` to generate consecutive data points.
437
        Can also be used with :func:`zip` to add sequence numbers.
438

439
        .. versionadded:: 2.7.0
440

441
        :param start:
442
        :param step: The step between values.
443

444
        :rtype:
445

446
        .. seealso::
447

448
                :func:`itertools.count`.
449

450
                The difference is that this returns more exact floats,
451
                whereas the values from :func:`itertools.count` drift.
452

453
                .. only:: html
454

455
                        A demonstration of the drift can be seen in this file: :download:`count_demo.py`.
456

457
        .. latex:clearpage::
458
        """
459

460
        if not isinstance(start, (int, float, complex)):
1✔
461
                raise TypeError("a number is required")
1✔
462
        if not isinstance(step, (int, float, complex)):
1✔
463
                raise TypeError("a number is required")
1✔
464

465
        # count(10) --> 10 11 12 13 14 ...
466
        # count(2.5, 0.5) -> 2.5 3.0 3.5 ...
467

468
        pos: int = 0
1✔
469

470
        def get_next():
1✔
471
                if pos:
1✔
472
                        return start + (step * pos)
1✔
473
                else:
474
                        return start
1✔
475

476
        @final
1✔
477
        class count(Iterator[AnyNum]):
1✔
478

479
                def __next__(self):
1✔
480
                        nonlocal pos
481

482
                        val = get_next()
1✔
483
                        pos += 1
1✔
484

485
                        return val
1✔
486

487
                def __iter__(self):
1✔
488
                        return self
1✔
489

490
                if isinstance(step, int) and step == 1:
1✔
491

492
                        def __repr__(self) -> str:
1✔
493
                                return f"{self.__class__.__name__}({get_next()})"
1✔
494
                else:
495

496
                        def __repr__(self) -> str:
1✔
497
                                return f"{self.__class__.__name__}{get_next(), step}"
1✔
498

499
                def __init_subclass__(cls, **kwargs):
1✔
500
                        raise TypeError("type 'domdf_python_tools.iterative.count' is not an acceptable base type")
1✔
501

502
        count.__qualname__ = count.__name__ = "count"
1✔
503

504
        return count()  # type: ignore
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