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

74 statements  

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

2 

3import functools 

4import inspect 

5import itertools 

6import logging 

7import os 

8import pathlib 

9import typing 

10import warnings 

11 

12__all__ = ['attach', 

13 'mkdirs', 

14 'mapping_items', 

15 'promote_pathlike', 

16 'promote_pathlike_directory', 

17 'deprecate_positional_args'] 

18 

19 

20log = logging.getLogger(__name__) 

21 

22 

23def attach(object: typing.Any, /, name: str) -> typing.Callable: 

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

25 

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

27 

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

29 ... def func(): 

30 ... pass 

31 

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

33 <function func at 0x...> 

34 """ 

35 def decorator(func): 

36 setattr(object, name, func) 

37 return func 

38 

39 return decorator 

40 

41 

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) 

50 

51 

52def mapping_items(mapping, /): 

53 """Return an iterator over the ``mapping`` items, 

54 sort if it's a plain dict. 

55 

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

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

58 

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 

67 

68 

69@typing.overload 

70def promote_pathlike(filepath: typing.Union[os.PathLike, str], /) -> pathlib.Path: 

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

72 

73 

74@typing.overload 

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

76 """Return None for None.""" 

77 

78 

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

83 

84 

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. 

88 

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 

93 

94 

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

99 

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) 

105 

106 

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. 

113 

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

122 

123 Returns: 

124 Return a decorator raising a category warning 

125 on more than supported_number positional args. 

126 

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

133 

134 if category is None: 

135 def nulldecorator(func): 

136 """Return the undecorated function.""" 

137 return func 

138 

139 return nulldecorator 

140 

141 assert issubclass(category, Warning) 

142 

143 stacklevel += 1 

144 

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

157 

158 deprecated = argnames[supported_number:] 

159 assert deprecated 

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

161 func.__module__, func.__qualname__, deprecated) 

162 

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) 

167 

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

169 

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) 

184 

185 return func(*args, **kwargs) 

186 

187 return wrapper 

188 

189 return decorator