File: _tools.py

package info (click to toggle)
python-graphviz 0.20.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,188 kB
  • sloc: python: 4,098; makefile: 13
file content (180 lines) | stat: -rw-r--r-- 6,382 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""Generic re-useable self-contained helper functions."""

import functools
import inspect
import itertools
import logging
import os
import pathlib
import typing
import warnings

__all__ = ['attach',
           'mkdirs',
           'mapping_items',
           'promote_pathlike',
           'promote_pathlike_directory',
           'deprecate_positional_args']


log = logging.getLogger(__name__)


def attach(object: typing.Any, /, name: str) -> typing.Callable:
    """Return a decorator doing ``setattr(object, name)`` with its argument.

    >>> spam = type('Spam', (object,), {})()  # doctest: +NO_EXE

    >>> @attach(spam, 'eggs')
    ... def func():
    ...     pass

    >>> spam.eggs  # doctest: +ELLIPSIS
    <function func at 0x...>
    """
    def decorator(func):
        setattr(object, name, func)
        return func

    return decorator


def mkdirs(filename: typing.Union[os.PathLike, str], /, *, mode: int = 0o777) -> None:
    """Recursively create directories up to the path of ``filename``
        as needed."""
    dirname = os.path.dirname(filename)
    if not dirname:
        return
    log.debug('os.makedirs(%r)', dirname)
    os.makedirs(dirname, mode=mode, exist_ok=True)


def mapping_items(mapping, /):
    """Return an iterator over the ``mapping`` items,
        sort if it's a plain dict.

    >>> list(mapping_items({'spam': 0, 'ham': 1, 'eggs': 2}))  # doctest: +NO_EXE
    [('eggs', 2), ('ham', 1), ('spam', 0)]

    >>> from collections import OrderedDict
    >>> list(mapping_items(OrderedDict(enumerate(['spam', 'ham', 'eggs']))))
    [(0, 'spam'), (1, 'ham'), (2, 'eggs')]
    """
    result = iter(mapping.items())
    if type(mapping) is dict:
        result = iter(sorted(result))
    return result


@typing.overload
def promote_pathlike(filepath: typing.Union[os.PathLike, str], /) -> pathlib.Path:
    """Return path object for path-like-object."""


@typing.overload
def promote_pathlike(filepath: None, /) -> None:
    """Return None for None."""


@typing.overload
def promote_pathlike(filepath: typing.Union[os.PathLike, str, None], /,
                     ) -> typing.Optional[pathlib.Path]:
    """Return path object or ``None`` depending on ``filepath``."""


def promote_pathlike(filepath: typing.Union[os.PathLike, str, None]
                     ) -> typing.Optional[pathlib.Path]:
    """Return path-like object ``filepath`` promoted into a path object.

    See also:
        https://docs.python.org/3/glossary.html#term-path-like-object
    """
    return pathlib.Path(filepath) if filepath is not None else None


def promote_pathlike_directory(directory: typing.Union[os.PathLike, str, None], /, *,
                               default: typing.Union[os.PathLike, str, None] = None,
                               ) -> pathlib.Path:
    """Return path-like object ``directory`` promoted into a path object (default to ``os.curdir``).

    See also:
        https://docs.python.org/3/glossary.html#term-path-like-object
    """
    return pathlib.Path(directory if directory is not None
                        else default or os.curdir)


def deprecate_positional_args(*,
                              supported_number: int,
                              ignore_argnames: typing.Sequence[str] = ('cls', 'self'),
                              category: typing.Type[Warning] = PendingDeprecationWarning,
                              stacklevel: int = 1):
    """Mark supported_number of positional arguments as the maximum.

    Args:
        supported_number: Number of positional arguments
            for which no warning is raised.
        ignore_argnames: Name(s) of arguments to ignore
            ('cls' and 'self' by default).
        category: Type of Warning to raise
            or None to return a nulldecorator
            returning the undecorated function.
        stacklevel: See :func:`warning.warn`.

    Returns:
        Return a decorator raising a category warning
            on more than supported_number positional args.

    See also:
        https://docs.python.org/3/library/exceptions.html#FutureWarning
        https://docs.python.org/3/library/exceptions.html#DeprecationWarning
        https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning
    """
    assert supported_number > 0, f'supported_number at least one: {supported_number!r}'

    if category is None:
        def nulldecorator(func):
            """Return the undecorated function."""
            return func

        return nulldecorator

    assert issubclass(category, Warning)

    stacklevel += 1

    def decorator(func):
        signature = inspect.signature(func)
        argnames = [name for name, param in signature.parameters.items()
                    if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
                    and name not in ignore_argnames]
        log.debug('deprecate positional args: %s.%s(%r)',
                  func.__module__, func.__qualname__,
                  argnames[supported_number:])

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if len(args) > supported_number:
                call_args = zip(argnames, args)
                supported = itertools.islice(call_args, supported_number)
                supported = dict(supported)
                deprecated = dict(call_args)
                assert deprecated
                func_name = func.__name__.lstrip('_')
                func_name, sep, rest = func_name.partition('_legacy')
                assert not set or not rest
                wanted = ', '.join(f'{name}={value!r}'
                                   for name, value in deprecated.items())
                warnings.warn(f'The signature of {func.__name__} will be reduced'
                              f' to {supported_number} positional arg'
                              f"{'s' if supported_number > 1 else ''}"
                              f' {list(supported)}: pass {wanted}'
                              ' as keyword arg(s)',
                              stacklevel=stacklevel,
                              category=category)

            return func(*args, **kwargs)

        return wrapper

    return decorator