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

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 category: typing.Type[Warning] = PendingDeprecationWarning, 

110 stacklevel: int = 1): 

111 """Mark supported_number of positional arguments as the maximum. 

112 

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

120 

121 Returns: 

122 Return a decorator raising a category warning 

123 on more than supported_number positional args. 

124 

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

131 

132 if category is None: 

133 def nulldecorator(func): 

134 """Return the undecorated function.""" 

135 return func 

136 

137 return nulldecorator 

138 

139 assert issubclass(category, Warning) 

140 

141 stacklevel += 1 

142 

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:]) 

150 

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) 

170 

171 return func(*args, **kwargs) 

172 

173 return wrapper 

174 

175 return decorator