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

75 statements  

1"""Generic re-useable self-contained helper functions.""" 

2 

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 

12 

13__all__ = ['attach', 

14 'mkdirs', 

15 'mapping_items', 

16 'promote_pathlike', 

17 'promote_pathlike_directory', 

18 'deprecate_positional_args'] 

19 

20 

21log = logging.getLogger(__name__) 

22 

23 

24def attach(object: Any, /, name: str) -> Callable: 

25 """Return a decorator doing ``setattr(object, name)`` with its argument. 

26 

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

28 

29 >>> @attach(spam, 'eggs') 

30 ... def func(): 

31 ... pass 

32 

33 >>> spam.eggs # doctest: +ELLIPSIS 

34 <function func at 0x...> 

35 """ 

36 def decorator(func): 

37 setattr(object, name, func) 

38 return func 

39 

40 return decorator 

41 

42 

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) 

51 

52 

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. 

56 

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

58 [('eggs', 2), ('ham', 1), ('spam', 0)] 

59 

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 

68 

69 

70@overload 

71def promote_pathlike(filepath: os.PathLike[str] | str, /) -> pathlib.Path: 

72 """Return path object for path-like-object.""" 

73 

74 

75@overload 

76def promote_pathlike(filepath: None, /) -> None: 

77 """Return None for None.""" 

78 

79 

80@overload 

81def promote_pathlike(filepath: os.PathLike[str] | str | None, /, 

82 ) -> pathlib.Path | None: 

83 """Return path object or ``None`` depending on ``filepath``.""" 

84 

85 

86def promote_pathlike(filepath: os.PathLike[str] | str | None 

87 ) -> pathlib.Path | None: 

88 """Return path-like object ``filepath`` promoted into a path object. 

89 

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 

94 

95 

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``). 

100 

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) 

106 

107 

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. 

114 

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`. 

123 

124 Returns: 

125 Return a decorator raising a category warning 

126 on more than supported_number positional args. 

127 

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}' 

134 

135 if category is None: 

136 def nulldecorator(func): 

137 """Return the undecorated function.""" 

138 return func 

139 

140 return nulldecorator 

141 

142 assert issubclass(category, Warning) 

143 

144 stacklevel += 1 

145 

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 = '' 

158 

159 deprecated = argnames[supported_number:] 

160 assert deprecated 

161 log.debug('deprecate positional args: %s.%s(%r)', 

162 func.__module__, func.__qualname__, deprecated) 

163 

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) 

168 

169 s_ = 's' if supported_number > 1 else '' 

170 

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) 

185 

186 return func(*args, **kwargs) 

187 

188 return wrapper 

189 

190 return decorator