Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/graphviz/_tools.py: 70%
64 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:43 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:43 +0000
1"""Generic re-useable self-contained helper functions."""
3import functools
4import inspect
5import itertools
6import logging
7import os
8import pathlib
9import typing
10import warnings
12__all__ = ['attach',
13 'mkdirs',
14 'mapping_items',
15 'promote_pathlike',
16 'promote_pathlike_directory',
17 'deprecate_positional_args']
20log = logging.getLogger(__name__)
23def attach(object: typing.Any, name: str) -> typing.Callable:
24 """Return a decorator doing ``setattr(object, name)`` with its argument.
26 >>> spam = type('Spam', (object,), {})() # doctest: +NO_EXE
28 >>> @attach(spam, 'eggs')
29 ... def func():
30 ... pass
32 >>> spam.eggs # doctest: +ELLIPSIS
33 <function func at 0x...>
34 """
35 def decorator(func):
36 setattr(object, name, func)
37 return func
39 return decorator
42def mkdirs(filename: typing.Union[os.PathLike, str], *, mode: int = 0o777) -> None:
43 """Recursively create directories up to the path of ``filename``
44 as needed."""
45 dirname = os.path.dirname(filename)
46 if not dirname:
47 return
48 log.debug('os.makedirs(%r)', dirname)
49 os.makedirs(dirname, mode=mode, exist_ok=True)
52def mapping_items(mapping):
53 """Return an iterator over the ``mapping`` items,
54 sort if it's a plain dict.
56 >>> list(mapping_items({'spam': 0, 'ham': 1, 'eggs': 2})) # doctest: +NO_EXE
57 [('eggs', 2), ('ham', 1), ('spam', 0)]
59 >>> from collections import OrderedDict
60 >>> list(mapping_items(OrderedDict(enumerate(['spam', 'ham', 'eggs']))))
61 [(0, 'spam'), (1, 'ham'), (2, 'eggs')]
62 """
63 result = iter(mapping.items())
64 if type(mapping) is dict:
65 result = iter(sorted(result))
66 return result
69@typing.overload
70def promote_pathlike(filepath: typing.Union[os.PathLike, str]) -> pathlib.Path:
71 """Return path object for path-like-object."""
74@typing.overload
75def promote_pathlike(filepath: None) -> None:
76 """Return None for None."""
79@typing.overload
80def promote_pathlike(filepath: typing.Union[os.PathLike, str, None]
81 ) -> typing.Optional[pathlib.Path]:
82 """Return path object or ``None`` depending on ``filepath``."""
85def promote_pathlike(filepath: typing.Union[os.PathLike, str, None]
86 ) -> typing.Optional[pathlib.Path]:
87 """Return path-like object ``filepath`` promoted into a path object.
89 See also:
90 https://docs.python.org/3/glossary.html#term-path-like-object
91 """
92 return pathlib.Path(filepath) if filepath is not None else None
95def promote_pathlike_directory(directory: typing.Union[os.PathLike, str, None], *,
96 default: typing.Union[os.PathLike, str, None] = None,
97 ) -> pathlib.Path:
98 """Return path-like object ``directory`` promoted into a path object (default to ``os.curdir``).
100 See also:
101 https://docs.python.org/3/glossary.html#term-path-like-object
102 """
103 return pathlib.Path(directory if directory is not None
104 else default or os.curdir)
107def deprecate_positional_args(*,
108 supported_number: int,
109 category: typing.Type[Warning] = PendingDeprecationWarning,
110 stacklevel: int = 1):
111 """Mark supported_number of positional arguments as the maximum.
113 Args:
114 supported_number: Number of positional arguments
115 for which no warning is raised.
116 category: Type of Warning to raise
117 or None to return a nulldecorator
118 returning the undecorated function.
119 stacklevel: See :func:`warning.warn`.
121 Returns:
122 Return a decorator raising a category warning
123 on more than supported_number positional args.
125 See also:
126 https://docs.python.org/3/library/exceptions.html#FutureWarning
127 https://docs.python.org/3/library/exceptions.html#DeprecationWarning
128 https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning
129 """
130 assert supported_number > 0, f'supported_number at least one: {supported_number!r}'
132 if category is None:
133 def nulldecorator(func):
134 """Return the undecorated function."""
135 return func
137 return nulldecorator
139 assert issubclass(category, Warning)
141 stacklevel += 1
143 def decorator(func):
144 signature = inspect.signature(func)
145 argnames = [name for name, param in signature.parameters.items()
146 if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
147 log.debug('deprecate positional args: %s.%s(%r)',
148 func.__module__, func.__qualname__,
149 argnames[supported_number:])
151 @functools.wraps(func)
152 def wrapper(*args, **kwargs):
153 if len(args) > supported_number:
154 call_args = zip(argnames, args)
155 supported = itertools.islice(call_args, supported_number)
156 supported = dict(supported)
157 deprecated = dict(call_args)
158 assert deprecated
159 func_name = func.__name__.lstrip('_')
160 func_name, sep, rest = func_name.partition('_legacy')
161 assert not set or not rest
162 wanted = ', '.join(f'{name}={value!r}'
163 for name, value in deprecated.items())
164 warnings.warn(f'The signature of {func.__name__} will be reduced'
165 f' to {supported_number} positional args'
166 f' {list(supported)}: pass {wanted}'
167 ' as keyword arg(s)',
168 stacklevel=stacklevel,
169 category=category)
171 return func(*args, **kwargs)
173 return wrapper
175 return decorator