Coverage for sacred/sacred/config/signature.py: 25%

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

76 statements  

1#!/usr/bin/env python 

2# coding=utf-8 

3 

4import inspect 

5from inspect import Parameter 

6from collections import OrderedDict 

7from sacred.utils import MissingConfigError, SignatureError 

8 

9ARG_TYPES = [ 

10 Parameter.POSITIONAL_ONLY, 

11 Parameter.POSITIONAL_OR_KEYWORD, 

12 Parameter.KEYWORD_ONLY, 

13] 

14POSARG_TYPES = [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD] 

15 

16 

17def get_argspec(f): 

18 sig = inspect.signature(f) 

19 args = [n for n, p in sig.parameters.items() if p.kind in ARG_TYPES] 

20 pos_args = [ 

21 n 

22 for n, p in sig.parameters.items() 

23 if p.kind in POSARG_TYPES and p.default == inspect._empty 

24 ] 

25 varargs = [ 

26 n for n, p in sig.parameters.items() if p.kind == Parameter.VAR_POSITIONAL 

27 ] 

28 # only use first vararg (how on earth would you have more anyways?) 

29 vararg_name = varargs[0] if varargs else None 

30 

31 varkws = [n for n, p in sig.parameters.items() if p.kind == Parameter.VAR_KEYWORD] 

32 # only use first varkw (how on earth would you have more anyways?) 

33 kw_wildcard_name = varkws[0] if varkws else None 

34 kwargs = OrderedDict( 

35 [ 

36 (n, p.default) 

37 for n, p in sig.parameters.items() 

38 if p.default != inspect._empty 

39 ] 

40 ) 

41 

42 return args, vararg_name, kw_wildcard_name, pos_args, kwargs 

43 

44 

45class Signature: 

46 """ 

47 Extracts and stores information about the signature of a function. 

48 

49 name : the functions name 

50 arguments : list of all arguments 

51 vararg_name : name of the *args variable 

52 kw_wildcard_name : name of the **kwargs variable 

53 positional_args : list of all positional-only arguments 

54 kwargs : dict of all keyword arguments mapped to their default 

55 """ 

56 

57 def __init__(self, f): 

58 self.name = f.__name__ 

59 args, vararg_name, kw_wildcard_name, pos_args, kwargs = get_argspec(f) 

60 self.arguments = args 

61 self.vararg_name = vararg_name 

62 self.kw_wildcard_name = kw_wildcard_name 

63 self.positional_args = pos_args 

64 self.kwargs = kwargs 

65 

66 def get_free_parameters(self, args, kwargs, bound=False): 

67 expected_args = self._get_expected_args(bound) 

68 return [a for a in expected_args[len(args) :] if a not in kwargs] 

69 

70 def construct_arguments(self, args, kwargs, options, bound=False): 

71 """ 

72 Construct args list and kwargs dictionary for this signature. 

73 

74 They are created such that: 

75 - the original explicit call arguments (args, kwargs) are preserved 

76 - missing arguments are filled in by name using options (if possible) 

77 - default arguments are overridden by options 

78 - TypeError is thrown if: 

79 * kwargs contains one or more unexpected keyword arguments 

80 * conflicting values for a parameter in both args and kwargs 

81 * there is an unfilled parameter at the end of this process 

82 """ 

83 expected_args = self._get_expected_args(bound) 

84 self._assert_no_unexpected_args(expected_args, args) 

85 self._assert_no_unexpected_kwargs(expected_args, kwargs) 

86 self._assert_no_duplicate_args(expected_args, args, kwargs) 

87 

88 args, kwargs = self._fill_in_options(args, kwargs, options, bound) 

89 

90 self._assert_no_missing_args(args, kwargs, bound) 

91 return args, kwargs 

92 

93 def __str__(self): 

94 pos_args = self.positional_args 

95 varg = ["*" + self.vararg_name] if self.vararg_name else [] 

96 kwargs = ["{}={}".format(n, v.__repr__()) for n, v in self.kwargs.items()] 

97 kw_wc = ["**" + self.kw_wildcard_name] if self.kw_wildcard_name else [] 

98 arglist = pos_args + varg + kwargs + kw_wc 

99 return "{}({})".format(self.name, ", ".join(arglist)) 

100 

101 def __repr__(self): 

102 return "<Signature at 0x{1:x} for '{0}'>".format(self.name, id(self)) 

103 

104 def _get_expected_args(self, bound): 

105 if bound: 

106 # When called as instance method, the instance ('self') will be 

107 # passed as first argument automatically, so the first argument 

108 # should be excluded from the signature during this invocation. 

109 return self.arguments[1:] 

110 else: 

111 return self.arguments 

112 

113 def _assert_no_unexpected_args(self, expected_args, args): 

114 if not self.vararg_name and len(args) > len(expected_args): 

115 unexpected_args = args[len(expected_args) :] 

116 raise SignatureError( 

117 "{} got unexpected argument(s): {}".format(self.name, unexpected_args) 

118 ) 

119 

120 def _assert_no_unexpected_kwargs(self, expected_args, kwargs): 

121 if self.kw_wildcard_name: 

122 return 

123 unexpected_kwargs = set(kwargs) - set(expected_args) 

124 if unexpected_kwargs: 

125 raise SignatureError( 

126 "{} got unexpected kwarg(s): {}".format( 

127 self.name, sorted(unexpected_kwargs) 

128 ) 

129 ) 

130 

131 def _assert_no_duplicate_args(self, expected_args, args, kwargs): 

132 positional_arguments = expected_args[: len(args)] 

133 duplicate_arguments = [v for v in positional_arguments if v in kwargs] 

134 if duplicate_arguments: 

135 raise SignatureError( 

136 "{} got multiple values for argument(s) {}".format( 

137 self.name, duplicate_arguments 

138 ) 

139 ) 

140 

141 def _fill_in_options(self, args, kwargs, options, bound): 

142 free_params = self.get_free_parameters(args, kwargs, bound) 

143 new_kwargs = dict(kwargs) if free_params else kwargs 

144 for param in free_params: 

145 if param in options: 

146 new_kwargs[param] = options[param] 

147 return args, new_kwargs 

148 

149 def _assert_no_missing_args(self, args, kwargs, bound): 

150 free_params = self.get_free_parameters(args, kwargs, bound) 

151 missing_args = [m for m in free_params if m not in self.kwargs] 

152 if missing_args: 

153 raise MissingConfigError( 

154 "{} is missing value(s):".format(self.name), 

155 missing_configs=missing_args, 

156 )