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

domdfcoding / domdf_python_tools / 7170146041

11 Dec 2023 04:07PM CUT coverage: 97.456%. Remained the same
7170146041

push

github

domdfcoding
Bump version v3.8.0.post1 -> v3.8.0.post2

1 of 1 new or added line in 1 file covered. (100.0%)

2145 of 2201 relevant lines covered (97.46%)

0.97 hits per line

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

94.74
/domdf_python_tools/utils.py
1
#!/usr/bin/env python
2
# cython: language_level=3
3
#
4
#  utils.py
5
"""
1✔
6
General utility functions.
7

8
.. versionchanged:: 1.0.0
9

10
        * Removed ``tuple2str`` and ``list2string``.
11
          Use :func:`domdf_python_tools.utils.list2str` instead.
12
        * Removed ``as_text`` and ``word_join``.
13
          Import from :mod:`domdf_python_tools.words` instead.
14
        * Removed ``splitLen``.
15
          Use :func:`domdf_python_tools.iterative.split_len` instead.
16

17
.. versionchanged:: 2.0.0
18

19
        :func:`~domdf_python_tools.iterative.chunks`,
20
        :func:`~domdf_python_tools.iterative.permutations`,
21
        :func:`~domdf_python_tools.iterative.split_len`,
22
        :func:`~domdf_python_tools.iterative.Len`, and
23
        :func:`~domdf_python_tools.iterative.double_chain`
24
        moved to :func:`domdf_python_tools.iterative`.
25

26
.. versionchanged:: 2.3.0
27

28
        Removed :func:`domdf_python_tools.utils.deprecated`.
29
        Use the new `deprecation-alias <https://pypi.org/project/deprecation-alias/>`_ package instead.
30

31
"""
32
#
33
#  Copyright © 2018-2022 Dominic Davis-Foster <dominic@davis-foster.co.uk>
34
#
35
#  Permission is hereby granted, free of charge, to any person obtaining a copy
36
#  of this software and associated documentation files (the "Software"), to deal
37
#  in the Software without restriction, including without limitation the rights
38
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
39
#  copies of the Software, and to permit persons to whom the Software is
40
#  furnished to do so, subject to the following conditions:
41
#
42
#  The above copyright notice and this permission notice shall be included in all
43
#  copies or substantial portions of the Software.
44
#
45
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
46
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
47
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
48
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
49
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
50
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
51
#  OR OTHER DEALINGS IN THE SOFTWARE.
52
#
53
#  as_text from https://stackoverflow.com/a/40935194
54
#                 Copyright © 2016 User3759685
55
#                 Available under the MIT License
56
#
57
#  strtobool based on the "distutils" module from CPython.
58
#  Some docstrings based on the Python documentation.
59
#  Licensed under the Python Software Foundation License Version 2.
60
#  Copyright © 2001-2020 Python Software Foundation. All rights reserved.
61
#  Copyright © 2000 BeOpen.com. All rights reserved.
62
#  Copyright © 1995-2000 Corporation for National Research Initiatives. All rights reserved.
63
#  Copyright © 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
64
#
65

66
# stdlib
67
import contextlib
1✔
68
import inspect
1✔
69
import json
1✔
70
import re
1✔
71
import sys
1✔
72
from io import StringIO
1✔
73
from math import log10
1✔
74
from pprint import pformat
1✔
75
from types import MethodType
1✔
76
from typing import (
1✔
77
                IO,
78
                TYPE_CHECKING,
79
                Any,
80
                Callable,
81
                Dict,
82
                Iterable,
83
                Iterator,
84
                List,
85
                Optional,
86
                Pattern,
87
                Set,
88
                Tuple,
89
                TypeVar,
90
                Union,
91
                overload
92
                )
93

94
# this package
95
import domdf_python_tools.words
1✔
96
from domdf_python_tools.typing import HasHead, String, SupportsLessThan
1✔
97

98
if TYPE_CHECKING or domdf_python_tools.__docs:  # pragma: no cover
99
        # 3rd party
100
        from pandas import DataFrame, Series
101

102
        Series.__module__ = "pandas"
103
        DataFrame.__module__ = "pandas"
104

105
_T = TypeVar("_T")
1✔
106

107
SupportsLessThanT = TypeVar("SupportsLessThanT", bound=SupportsLessThan)
1✔
108

109
__all__ = [
1✔
110
                "pyversion",
111
                "SPACE_PLACEHOLDER",
112
                "cmp",
113
                "list2str",
114
                "printr",
115
                "printt",
116
                "stderr_writer",
117
                "printe",
118
                "str2tuple",
119
                "strtobool",
120
                "enquote_value",
121
                "posargs2kwargs",
122
                "convert_indents",
123
                "etc",
124
                "head",
125
                "magnitude",
126
                "trim_precision",
127
                "double_repr_string",
128
                "redirect_output",
129
                "divide",
130
                "redivide",
131
                "unique_sorted",
132
                "replace_nonprinting",
133
                ]
134

135
#: The current major python version.
136
pyversion: int = int(sys.version_info.major)  # Python Version
1✔
137

138
#: The ``␣`` character.
139
SPACE_PLACEHOLDER = '␣'
1✔
140

141

142
def cmp(x, y) -> int:
1✔
143
        """
144
        Implementation of ``cmp`` for Python 3.
145

146
        Compare the two objects x and y and return an integer according to the outcome.
147

148
        The return value is negative if ``x < y``, zero if ``x == y`` and strictly positive if ``x > y``.
149
        """
150

151
        return int((x > y) - (x < y))
1✔
152

153

154
def list2str(the_list: Iterable[Any], sep: str = ',') -> str:
1✔
155
        """
156
        Convert an iterable, such as a list, to a comma separated string.
157

158
        :param the_list: The iterable to convert to a string.
159
        :param sep: Separator to use for the string.
160

161
        :return: Comma separated string
162
        """
163

164
        return sep.join([str(x) for x in the_list])
1✔
165

166

167
def printr(
1✔
168
                obj: Any,
169
                *values: object,
170
                sep: Optional[str] = ' ',
171
                end: Optional[str] = '\n',
172
                file: Optional[IO] = None,
173
                flush: bool = False,
174
                ) -> None:
175
        r"""
176
        Print the :func:`repr` of an object.
177

178
        If no objects are given, :func:`~.printr` will just write ``end``.
179

180
        :param obj:
181
        :param \*values: Additional values to print. These are printed verbatim.
182
        :param sep: The separator between values.
183
        :param end: The final value to print.
184
                Setting to ``''`` will leave the insertion point at the end of the printed text.
185
        :param file: The file to write to.
186
                If not present or :py:obj:`None`, :py:obj:`sys.stdout` will be used.
187
        :no-default file:
188
        :param flush: If :py:obj:`True` the stream is forcibly flushed after printing.
189
        """
190

191
        print(repr(obj), *values, sep=sep, end=end, file=file, flush=flush)
1✔
192

193

194
def printt(
1✔
195
                obj: Any,
196
                *values: object,
197
                sep: Optional[str] = ' ',
198
                end: Optional[str] = '\n',
199
                file: Optional[IO] = None,
200
                flush: bool = False,
201
                ) -> None:
202
        r"""
203
        Print the type of an object.
204

205
        If no objects are given, :func:`~.printt` will just write ``end``.
206

207
        :param obj:
208
        :param \*values: Additional values to print. These are printed verbatim.
209
        :param sep: The separator between values.
210
        :param end: The final value to print.
211
                Setting to ``''`` will leave the insertion point at the end of the printed text.
212
        :param file: The file to write to.
213
                If not present or :py:obj:`None`, :py:obj:`sys.stdout` will be used.
214
        :no-default file:
215
        :param flush: If :py:obj:`True` the stream is forcibly flushed after printing.
216
        """
217

218
        print(type(obj), *values, sep=sep, end=end, file=file, flush=flush)
1✔
219

220

221
def stderr_writer(
1✔
222
                *values: object,
223
                sep: Optional[str] = ' ',
224
                end: Optional[str] = '\n',
225
                ) -> None:
226
        r"""
227
        Print ``*values`` to :py:obj:`sys.stderr`, separated by ``sep`` and followed by ``end``.
228

229
        :py:obj:`sys.stdout` is flushed before printing, and :py:obj:`sys.stderr` is flushed afterwards.
230

231
        If no objects are given, :func:`~.stderr_writer` will just write ``end``.
232

233
        :param \*values:
234
        :param sep: The separator between values.
235
        :param end: The final value to print.
236
                Setting to ``''`` will leave the insertion point at the end of the printed text.
237

238
        :rtype:
239

240
        .. versionchanged:: 3.0.0
241

242
                The only permitted keyword arguments are ``sep`` and ``end``.
243
                Previous versions allowed other keywords arguments supported by :func:`print` but they had no effect.
244
        """
245

246
        sys.stdout.flush()
1✔
247
        print(*values, sep=sep, end=end, file=sys.stderr, flush=True)
1✔
248
        sys.stderr.flush()
1✔
249

250

251
#: Alias of :func:`~.stderr_writer`
252
printe = stderr_writer
1✔
253

254

255
def str2tuple(input_string: str, sep: str = ',') -> Tuple[int, ...]:
1✔
256
        """
257
        Convert a comma-separated string of integers into a tuple.
258

259
        .. latex:vspace:: -10px
260
        .. important:: The input string must represent a comma-separated series of integers.
261
        .. TODO:: Allow custom types, not just :class:`int` (making :class:`int` the default)
262
        .. latex:vspace:: -20px
263

264
        :param input_string: The string to be converted into a tuple
265
        :param sep: The separator in the string.
266
        """
267

268
        return tuple(int(x) for x in input_string.split(sep))
1✔
269

270

271
def strtobool(val: Union[str, int]) -> bool:
1✔
272
        """
273
        Convert a string representation of truth to :py:obj:`True` or :py:obj:`False`.
274

275
        If val is an integer then its boolean representation is returned. If val is a boolean it is returned as-is.
276

277
        :py:obj:`True` values are ``'y'``, ``'yes'``, ``'t'``, ``'true'``, ``'on'``, ``'1'``, and ``1``.
278

279
        :py:obj:`False` values are ``'n'``, ``'no'``, ``'f'``, ``'false'``, ``'off'``, ``'0'``, and ``0``.
280

281
        :raises: :py:exc:`ValueError` if ``val`` is anything else.
282
        """
283

284
        if isinstance(val, int):
1✔
285
                return bool(val)
1✔
286

287
        val = val.lower()
1✔
288
        if val in {'y', "yes", 't', "true", "on", '1'}:
1✔
289
                return True
1✔
290
        elif val in {'n', "no", 'f', "false", "off", '0'}:
1✔
291
                return False
1✔
292
        else:
293
                raise ValueError(f"invalid truth value {val!r}")
1✔
294

295

296
def enquote_value(value: Any) -> Union[str, bool, float]:
1✔
297
        """
298
        Adds single quotes (``'``) to the given value, suitable for use in a templating system such as Jinja2.
299

300
        :class:`Floats <float>`, :class:`integers <int>`, :class:`booleans <bool>`, :py:obj:`None`,
301
        and the strings ``'True'``, ``'False'`` and ``'None'`` are returned as-is.
302

303
        :param value: The value to enquote
304
        """
305

306
        if value in {"True", "False", "None", True, False, None}:
1✔
307
                return value
1✔
308
        elif isinstance(value, (int, float)):
1✔
309
                return value
1✔
310
        elif isinstance(value, str):
1✔
311
                return repr(value)
1✔
312
        else:
313
                return f"'{value}'"
1✔
314

315

316
def posargs2kwargs(
1✔
317
                args: Iterable[Any],
318
                posarg_names: Union[Iterable[str], Callable],
319
                kwargs: Optional[Dict[str, Any]] = None,
320
                ) -> Dict[str, Any]:
321
        """
322
        Convert the positional args in ``args`` to kwargs, based on the relative order of ``args`` and ``posarg_names``.
323

324
        .. important:: Python 3.8's Positional-Only Parameters (:pep:`570`) are not supported.
325

326
        .. versionadded:: 0.4.10
327

328
        :param args: List of positional arguments provided to a function.
329
        :param posarg_names: Either a list of positional argument names for the function, or the function object.
330
        :param kwargs: Optional mapping of keyword argument names to values.
331
                The arguments will be added to this dictionary if provided.
332
        :default kwargs: ``{}``
333

334
        :return: Dictionary mapping argument names to values.
335

336
        .. versionchanged:: 2.8.0
337

338
                The "self" argument for bound methods is ignored.
339
                For unbound methods (which are just functions) the behaviour is unchanged.
340
        """
341

342
        if kwargs is None:
1✔
343
                kwargs = {}
1✔
344

345
        self_arg = None
1✔
346

347
        if isinstance(posarg_names, MethodType):
1✔
348
                self_arg, *posarg_names = inspect.getfullargspec(posarg_names).args
1✔
349
        elif callable(posarg_names):
1✔
350
                posarg_names = inspect.getfullargspec(posarg_names).args
1✔
351

352
        for name, arg_value in zip(posarg_names, args):
1✔
353
                if name in kwargs:
1✔
354
                        if isinstance(posarg_names, MethodType):
×
355
                                raise TypeError(f"{posarg_names.__name__}(): got multiple values for argument '{name}'")
×
356
                        else:
357
                                raise TypeError(f"got multiple values for argument '{name}'")
×
358

359
        kwargs.update(zip(posarg_names, args))
1✔
360

361
        if self_arg is not None and self_arg in kwargs:
1✔
362
                del kwargs[self_arg]
×
363

364
        # TODO: positional only arguments
365

366
        return kwargs
1✔
367

368

369
def convert_indents(text: str, tab_width: int = 4, from_: str = '\t', to: str = ' ') -> str:
1✔
370
        r"""
371
        Convert indentation at the start of lines in ``text`` from tabs to spaces.
372

373
        :param text: The text to convert indents in.
374
        :param tab_width: The number of spaces per tab.
375
        :param from\_: The indent to convert from.
376
        :param to: The indent to convert to.
377
        """
378

379
        output = []
1✔
380
        tab = to * tab_width
1✔
381
        from_size = len(from_)
1✔
382

383
        for line in text.splitlines():
1✔
384
                indent_count = 0
1✔
385

386
                while line.startswith(from_):
1✔
387
                        indent_count += 1
1✔
388
                        line = line[from_size:]
1✔
389

390
                output.append(f"{tab * indent_count}{line}")
1✔
391

392
        return '\n'.join(output)
1✔
393

394

395
class _Etcetera(str):
1✔
396

397
        __slots__ = ()
1✔
398

399
        def __new__(cls):
1✔
400
                return str.__new__(cls, "...")
1✔
401

402
        def __repr__(self) -> str:
1✔
403
                return str(self)
1✔
404

405

406
etc = _Etcetera()
1✔
407
"""
408
Object that provides an ellipsis string
409

410
.. versionadded:: 0.8.0
411
"""
412

413

414
def head(obj: Union[Tuple, List, "DataFrame", "Series", String, HasHead], n: int = 10) -> Optional[str]:
1✔
415
        """
416
        Returns the head of the given object.
417

418
        .. versionadded:: 0.8.0
419

420
        :param obj:
421
        :param n: Show the first ``n`` items of ``obj``.
422

423
        .. seealso::
424

425
                * :func:`textwrap.shorten`, which truncates a string to fit within a given number of characters.
426
                * :func:`itertools.islice`, which returns the first ``n`` elements from an iterator.
427
        """
428

429
        if isinstance(obj, tuple) and hasattr(obj, "_fields"):
1✔
430
                # Likely a namedtuple
431
                if len(obj) <= n:
1✔
432
                        return repr(obj)
1✔
433
                else:
434
                        head_of_namedtuple = {k: v for k, v in zip(obj._fields[:n], obj[:n])}  # type: ignore
1✔
435
                        repr_fmt = '(' + ", ".join(f"{k}={v!r}" for k, v in head_of_namedtuple.items()) + f", {etc})"
1✔
436
                        return obj.__class__.__name__ + repr_fmt
1✔
437

438
        elif isinstance(obj, (list, tuple)):
1✔
439
                if len(obj) > n:
1✔
440
                        return pformat(obj.__class__((*obj[:n], etc)))
1✔
441
                else:
442
                        return pformat(obj)
1✔
443

444
        elif isinstance(obj, HasHead):
1✔
445
                return obj.head(n).to_string()
1✔
446

447
        elif len(obj) <= n:  # type: ignore
1✔
448
                return str(obj)
1✔
449

450
        else:
451
                return str(obj[:n]) + etc  # type: ignore
1✔
452

453

454
def magnitude(x: float) -> int:
1✔
455
        """
456
        Returns the magnitude of the given value.
457

458
        * For negative numbers the absolute magnitude is returned.
459
        * For decimal numbers below ``1`` the magnitude will be negative.
460

461
        .. versionadded:: 2.0.0
462

463
        :param x: Numerical value to find the magnitude of.
464
        """
465

466
        if x > 0.0:
1✔
467
                return int(log10(x))
1✔
468
        elif x < 0.0:
×
469
                return int(log10(abs(x)))
×
470
        else:
471
                return 0
×
472

473

474
def trim_precision(value: float, precision: int = 4) -> float:
1✔
475
        """
476
        Trim the precision of the given floating point value.
477

478
        For example, if you have the value `170.10000000000002` but really only care about it being
479
        ``\u2248 179.1``:
480

481
        .. code-block:: python
482

483
                >>> trim_precision(170.10000000000002, 2)
484
                170.1
485
                >>> type(trim_precision(170.10000000000002, 2))
486
                <class 'float'>
487

488
        .. versionadded:: 2.0.0
489

490
        :param value:
491
        :param precision: The number of decimal places to leave in the output.
492
        """
493

494
        return float(format(value, f"0.{precision}f"))
1✔
495

496

497
def double_repr_string(string: str) -> str:
1✔
498
        """
499
        Like :func:`repr(str) <repr>`, but tries to use double quotes instead.
500

501
        .. versionadded:: 2.5.0
502

503
        :param string:
504
        """
505

506
        # figure out which quote to use; double is preferred
507
        if '"' in string and "'" not in string:
1✔
508
                return repr(string)
×
509
        else:
510
                return json.dumps(string, ensure_ascii=False)
1✔
511

512

513
@contextlib.contextmanager
1✔
514
def redirect_output(combine: bool = False) -> Iterator[Tuple[StringIO, StringIO]]:
1✔
515
        """
516
        Context manager to redirect stdout and stderr to two :class:`io.StringIO` objects.
517

518
        These are assigned (as a :class:`tuple`) to the target the :keyword:`as` expression.
519

520
        Example:
521

522
        .. code-block:: python
523

524
                with redirect_output() as (stdout, stderr):
525
                        ...
526

527
        .. versionadded:: 2.6.0
528

529
        :param combine: If :py:obj:`True` ``stderr`` is combined with ``stdout``.
530
        """
531

532
        if combine:
1✔
533
                stdout = stderr = StringIO()
1✔
534
        else:
535
                stdout = StringIO()
1✔
536
                stderr = StringIO()
1✔
537

538
        with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
1✔
539
                yield stdout, stderr
1✔
540

541

542
def divide(string: str, sep: str) -> Tuple[str, str]:
1✔
543
        """
544
        Divide a string into two parts, about the given string.
545

546
        .. versionadded:: 2.7.0
547

548
        :param string:
549
        :param sep: The separator to split at.
550
        """
551

552
        if sep not in string:
1✔
553
                raise ValueError(f"{sep!r} not in {string!r}")
1✔
554

555
        parts = string.split(sep, 1)
1✔
556
        return tuple(parts)  # type: ignore
1✔
557

558

559
def redivide(string: str, pat: Union[str, Pattern]) -> Tuple[str, str]:
1✔
560
        """
561
        Divide a string into two parts, splitting on the given regular expression.
562

563
        .. versionadded:: 2.7.0
564

565
        :param string:
566
        :param pat:
567

568
        :rtype:
569

570
        .. latex:clearpage::
571
        """
572

573
        if isinstance(pat, str):
1✔
574
                pat = re.compile(pat)
1✔
575

576
        if not pat.search(string):
1✔
577
                raise ValueError(f"{pat!r} has no matches in {string!r}")
1✔
578

579
        parts = pat.split(string, 1)
1✔
580
        return tuple(parts)  # type: ignore
1✔
581

582

583
@overload
1✔
584
def unique_sorted(
1✔
585
                elements: Iterable[SupportsLessThanT],
586
                *,
587
                key: None = ...,
588
                reverse: bool = ...,
589
                ) -> List[SupportsLessThanT]: ...
590

591

592
@overload
1✔
593
def unique_sorted(
1✔
594
                elements: Iterable[_T],
595
                *,
596
                key: Callable[[_T], SupportsLessThan],
597
                reverse: bool = ...,
598
                ) -> List[_T]: ...
599

600

601
def unique_sorted(
1✔
602
                elements: Iterable,
603
                *,
604
                key: Optional[Callable] = None,
605
                reverse: bool = False,
606
                ) -> List:
607
        """
608
        Returns an ordered list of unique items from ``elements``.
609

610
        .. versionadded:: 3.0.0
611

612
        :param elements:
613
        :param key: A function of one argument used to extract a comparison key from each item when sorting.
614
                For example, :meth:`key=str.lower <str.lower>`.
615
                The default value is :py:obj:`None`, which will compare the elements directly.
616
        :param reverse: If :py:obj:`True` the list elements are sorted as if each comparison were reversed.
617

618
        .. seealso:: :class:`set` and :func:`sorted`
619
        """
620

621
        return sorted(set(elements), key=key, reverse=reverse)
1✔
622

623

624
def replace_nonprinting(string: str, exclude: Optional[Set[int]] = None) -> str:
1✔
625
        """
626
        Replace nonprinting (control) characters in ``string`` with ``^`` and ``M-`` notation.
627

628
        .. versionadded:: 3.3.0
629

630
        :param string:
631
        :param exclude: A set of codepoints to exclude.
632

633
        :rtype:
634

635
        .. seealso:: :wikipedia:`C0 and C1 control codes` on Wikipedia
636
        """
637

638
        # https://stackoverflow.com/a/44952259
639

640
        if exclude is None:
1✔
641
                exclude = set()
1✔
642

643
        translation_map = {}
1✔
644

645
        for codepoint in range(32):
1✔
646
                if codepoint not in exclude:
1✔
647
                        translation_map[codepoint] = f"^{chr(64 + codepoint)}"
1✔
648

649
        if 127 not in exclude:
1✔
650
                translation_map[127] = "^?"
1✔
651

652
        for codepoint in range(128, 256):
1✔
653
                if codepoint not in exclude:
1✔
654
                        translation_map[codepoint] = f"M+{chr(codepoint-64)}"
1✔
655

656
        return string.translate(translation_map)
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