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."""
3from collections.abc import Callable, Iterator, Mapping
4import functools
5import inspect
6import itertools
7import logging
8import os
9import pathlib
10from typing import Any, overload
11import warnings
13__all__ = ['attach',
14 'mkdirs',
15 'mapping_items',
16 'promote_pathlike',
17 'promote_pathlike_directory',
18 'deprecate_positional_args']
21log = logging.getLogger(__name__)
24def attach(object: Any, /, name: str) -> Callable:
25 """Return a decorator doing ``setattr(object, name)`` with its argument.
27 >>> spam = type('Spam', (object,), {})() # doctest: +NO_EXE
29 >>> @attach(spam, 'eggs')
30 ... def func():
31 ... pass
33 >>> spam.eggs # doctest: +ELLIPSIS
34 <function func at 0x...>
35 """
36 def decorator(func):
37 setattr(object, name, func)
38 return func
40 return decorator
43def mkdirs(filename: os.PathLike[str] | str, /, *, mode: int = 0o777) -> None:
44 """Recursively create directories up to the path of ``filename``
45 as needed."""
46 dirname = os.path.dirname(filename)
47 if not dirname:
48 return
49 log.debug('os.makedirs(%r)', dirname)
50 os.makedirs(dirname, mode=mode, exist_ok=True)
53def mapping_items(mapping: Mapping[Any, Any], /) -> Iterator[tuple[Any, Any]]:
54 """Return an iterator over the ``mapping`` items,
55 sort if it's a plain dict.
57 >>> list(mapping_items({'spam': 0, 'ham': 1, 'eggs': 2})) # doctest: +NO_EXE
58 [('eggs', 2), ('ham', 1), ('spam', 0)]
60 >>> from collections import OrderedDict
61 >>> list(mapping_items(OrderedDict(enumerate(['spam', 'ham', 'eggs']))))
62 [(0, 'spam'), (1, 'ham'), (2, 'eggs')]
63 """
64 result = iter(mapping.items())
65 if type(mapping) is dict:
66 result = iter(sorted(result))
67 return result
70@overload
71def promote_pathlike(filepath: os.PathLike[str] | str, /) -> pathlib.Path:
72 """Return path object for path-like-object."""
75@overload
76def promote_pathlike(filepath: None, /) -> None:
77 """Return None for None."""
80@overload
81def promote_pathlike(filepath: os.PathLike[str] | str | None, /,
82 ) -> pathlib.Path | None:
83 """Return path object or ``None`` depending on ``filepath``."""
86def promote_pathlike(filepath: os.PathLike[str] | str | None
87 ) -> pathlib.Path | None:
88 """Return path-like object ``filepath`` promoted into a path object.
90 See also:
91 https://docs.python.org/3/glossary.html#term-path-like-object
92 """
93 return pathlib.Path(filepath) if filepath is not None else None
96def promote_pathlike_directory(directory: os.PathLike[str] | str | None, /, *,
97 default: os.PathLike[str] | str | None = None,
98 ) -> pathlib.Path:
99 """Return path-like object ``directory`` promoted into a path object (default to ``os.curdir``).
101 See also:
102 https://docs.python.org/3/glossary.html#term-path-like-object
103 """
104 return pathlib.Path(directory if directory is not None
105 else default or os.curdir)
108def deprecate_positional_args(*,
109 supported_number: int,
110 ignore_arg: str | None = None,
111 category: type[Warning] = PendingDeprecationWarning,
112 stacklevel: int = 1):
113 """Mark supported_number of positional arguments as the maximum.
115 Args:
116 supported_number: Number of positional arguments
117 for which no warning is raised.
118 ignore_arg: Name of positional argument to ignore.
119 category: Type of Warning to raise
120 or None to return a nulldecorator
121 returning the undecorated function.
122 stacklevel: See :func:`warning.warn`.
124 Returns:
125 Return a decorator raising a category warning
126 on more than supported_number positional args.
128 See also:
129 https://docs.python.org/3/library/exceptions.html#FutureWarning
130 https://docs.python.org/3/library/exceptions.html#DeprecationWarning
131 https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning
132 """
133 assert supported_number >= 0, f'supported_number => 0: {supported_number!r}'
135 if category is None:
136 def nulldecorator(func):
137 """Return the undecorated function."""
138 return func
140 return nulldecorator
142 assert issubclass(category, Warning)
144 stacklevel += 1
146 def decorator(func):
147 signature = inspect.signature(func)
148 argnames = [name for name, param in signature.parameters.items()
149 if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
150 check_number = supported_number
151 if ignore_arg is not None:
152 ignored = [name for name in argnames if name == ignore_arg]
153 assert ignored, 'ignore_arg must be a positional arg'
154 check_number += len(ignored)
155 qualification = f' (ignoring {ignore_arg}))'
156 else:
157 qualification = ''
159 deprecated = argnames[supported_number:]
160 assert deprecated
161 log.debug('deprecate positional args: %s.%s(%r)',
162 func.__module__, func.__qualname__, deprecated)
164 # mangle function name in message for this package
165 func_name = func.__name__.lstrip('_')
166 func_name, sep, rest = func_name.partition('_legacy')
167 assert func_name and (not sep or not rest)
169 s_ = 's' if supported_number > 1 else ''
171 @functools.wraps(func)
172 def wrapper(*args, **kwargs):
173 if len(args) > check_number:
174 call_args = zip(argnames, args)
175 supported = dict(itertools.islice(call_args, check_number))
176 deprecated = dict(call_args)
177 assert deprecated
178 wanted = ', '.join(f'{name}={value!r}'
179 for name, value in deprecated.items())
180 warnings.warn(f'The signature of {func_name} will be reduced'
181 f' to {supported_number} positional arg{s_}{qualification}'
182 f' {list(supported)}: pass {wanted} as keyword arg{s_}',
183 stacklevel=stacklevel,
184 category=category)
186 return func(*args, **kwargs)
188 return wrapper
190 return decorator