Coverage for sacred/sacred/config/signature.py: 100%
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#!/usr/bin/env python
2# coding=utf-8
4import inspect
5from inspect import Parameter
6from collections import OrderedDict
7from sacred.utils import MissingConfigError, SignatureError
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]
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
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 )
42 return args, vararg_name, kw_wildcard_name, pos_args, kwargs
45class Signature:
46 """
47 Extracts and stores information about the signature of a function.
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 """
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
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]
70 def construct_arguments(self, args, kwargs, options, bound=False):
71 """
72 Construct args list and kwargs dictionary for this signature.
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)
88 args, kwargs = self._fill_in_options(args, kwargs, options, bound)
90 self._assert_no_missing_args(args, kwargs, bound)
91 return args, kwargs
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))
101 def __repr__(self):
102 return "<Signature at 0x{1:x} for '{0}'>".format(self.name, id(self))
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
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 )
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 )
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 )
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
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 )