Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/graphviz/_tools.py: 80%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 ignore_arg: typing.Optional[str] = None,
110 category: typing.Type[Warning] = PendingDeprecationWarning,
111 stacklevel: int = 1):
112 """Mark supported_number of positional arguments as the maximum.
114 Args:
115 supported_number: Number of positional arguments
116 for which no warning is raised.
117 ignore_arg: Name of positional argument to ignore.
118 category: Type of Warning to raise
119 or None to return a nulldecorator
120 returning the undecorated function.
121 stacklevel: See :func:`warning.warn`.
123 Returns:
124 Return a decorator raising a category warning
125 on more than supported_number positional args.
127 See also:
128 https://docs.python.org/3/library/exceptions.html#FutureWarning
129 https://docs.python.org/3/library/exceptions.html#DeprecationWarning
130 https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning
131 """
132 assert supported_number >= 0, f'supported_number => 0: {supported_number!r}'
134 if category is None:
135 def nulldecorator(func):
136 """Return the undecorated function."""
137 return func
139 return nulldecorator
141 assert issubclass(category, Warning)
143 stacklevel += 1
145 def decorator(func):
146 signature = inspect.signature(func)
147 argnames = [name for name, param in signature.parameters.items()
148 if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
149 check_number = supported_number
150 if ignore_arg is not None:
151 ignored = [name for name in argnames if name == ignore_arg]
152 assert ignored, 'ignore_arg must be a positional arg'
153 check_number += len(ignored)
154 qualification = f' (ignoring {ignore_arg}))'
155 else:
156 qualification = ''
158 deprecated = argnames[supported_number:]
159 assert deprecated
160 log.debug('deprecate positional args: %s.%s(%r)',
161 func.__module__, func.__qualname__, deprecated)
163 # mangle function name in message for this package
164 func_name = func.__name__.lstrip('_')
165 func_name, sep, rest = func_name.partition('_legacy')
166 assert func_name and (not sep or not rest)
168 s_ = 's' if supported_number > 1 else ''
170 @functools.wraps(func)
171 def wrapper(*args, **kwargs):
172 if len(args) > check_number:
173 call_args = zip(argnames, args)
174 supported = dict(itertools.islice(call_args, check_number))
175 deprecated = dict(call_args)
176 assert deprecated
177 wanted = ', '.join(f'{name}={value!r}'
178 for name, value in deprecated.items())
179 warnings.warn(f'The signature of {func_name} will be reduced'
180 f' to {supported_number} positional arg{s_}{qualification}'
181 f' {list(supported)}: pass {wanted} as keyword arg{s_}',
182 stacklevel=stacklevel,
183 category=category)
185 return func(*args, **kwargs)
187 return wrapper
189 return decorator