Coverage for src/decopatch/utils_modes.py: 87%
148 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-06 15:13 +0000
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-06 15:13 +0000
1from makefun import remove_signature_parameters, with_signature, wraps
3try: # python 3.3+
4 from inspect import signature, Parameter
5 funcsigs_used = False
6except ImportError:
7 from funcsigs import signature, Parameter
8 funcsigs_used = True
11class _Symbol:
12 """
13 Symbols used in your (double) flat-mode signatures to declare where the various objects should be injected
14 These symbols have a nice representation.
15 """
16 __slots__ = ('repr_', )
18 def __init__(self, repr_):
19 self.repr_ = repr_
21 def __repr__(self):
22 return self.repr_
25DECORATED = _Symbol('DECORATED')
26# A symbol used in flat-mode signatures to declare where the decorated function should be injected
29WRAPPED = _Symbol('WRAPPED')
30# A symbol used in double flat-mode signatures to declare where the wrapped function should be injected
33F_ARGS = _Symbol('F_ARGS')
34# A symbol used in your double flat-mode signatures to declare where the wrapper args should be injected
37F_KWARGS = _Symbol('F_KWARGS')
38# A symbol used in your double flat-mode signatures to declare where the wrapper kwargs should be injected
41def make_decorator_spec(impl_function,
42 flat_mode_decorated_name=None # type: str
43 ):
44 """
45 Analyzes the implementation function
48 If `flat_mode_decorated_name` is set, this is a shortcut for flat mode. In that case the implementation function
49 is not analyzed.
51 :param impl_function:
52 :param flat_mode_decorated_name:
53 :return: sig_info, function_for_metadata, nested_impl_function
54 """
55 # extract the implementation's signature
56 implementors_signature = signature(impl_function)
58 # determine the mode (nested, flat, double-flat) and check signature
59 mode, injected_name, contains_varpositional, injected_pos, \
60 injected_arg, f_args_name, f_kwargs_name = extract_mode_info(implementors_signature, flat_mode_decorated_name)
62 # create the signature of the decorator function to create, according to mode
63 if mode is None:
64 # *nested: keep the signature 'as is'
65 exposed_signature = implementors_signature
66 function_for_metadata = impl_function
67 nested_impl_function = impl_function
69 elif mode is DECORATED: # flat mode
70 # use the same signature, but remove the injected arg.
71 exposed_signature = remove_signature_parameters(implementors_signature, injected_name)
73 # use the original function for the docstring/module metadata
74 function_for_metadata = impl_function
76 # generate the corresponding nested decorator
77 nested_impl_function = make_nested_impl_for_flat_mode(exposed_signature, impl_function, injected_name,
78 injected_pos)
80 elif mode is WRAPPED: 80 ↛ 94line 80 didn't jump to line 94, because the condition on line 80 was never false
81 # *double-flat: the same signature, but we remove the injected args.
82 args_to_remove = (injected_name,) + ((f_args_name,) if f_args_name is not None else ()) \
83 + ((f_kwargs_name,) if f_kwargs_name is not None else ())
84 exposed_signature = remove_signature_parameters(implementors_signature, *args_to_remove)
86 # use the original function for the docstring/module metadata
87 function_for_metadata = impl_function
89 # generate the corresponding nested decorator
90 nested_impl_function = make_nested_impl_for_doubleflat_mode(exposed_signature, impl_function, injected_name,
91 f_args_name, f_kwargs_name, injected_pos)
93 else:
94 raise ValueError("Unknown mode: %s" % mode)
96 # create an object to easily access the exposed signature information afterwards
97 sig_info = SignatureInfo(exposed_signature, contains_varpositional, injected_pos)
99 return sig_info, function_for_metadata, nested_impl_function
102def make_nested_impl_for_flat_mode(decorator_signature, user_provided_applier, injected_name, injected_pos):
103 """
104 Creates the nested-mode decorator to be used when the implementation is provided in flat mode.
106 Note: we set the signature correctly so that this behaves exactly like a nested implementation in terms of
107 exceptions raised when the arguments are incorrect. Since the external method is called only once per decorator
108 usage and does not impact the decorated object we can afford.
110 :param decorator_signature:
111 :param user_provided_applier:
112 :param injected_name:
113 :param argnames_before_varpos_arg:
114 :return:
115 """
117 @with_signature(decorator_signature)
118 def _decorator(*args, **kwargs):
119 """ The decorator. Its signature will be overriden by `generated_signature` """
121 def _apply_decorator(decorated):
122 """ This is called when the decorator is applied to an object `decorated` """
124 # inject `decorated` under the correct name
125 # fix in case of var-positional arguments
126 if injected_pos >= 0:
127 new_args = args[:injected_pos] + (decorated, ) + args[injected_pos:]
128 else:
129 new_args = args
130 kwargs[injected_name] = decorated
132 return user_provided_applier(*new_args, **kwargs)
134 return _apply_decorator
136 return _decorator
139def make_nested_impl_for_doubleflat_mode(decorator_signature, user_provided_wrapper, injected_name,
140 f_args_name, f_kwargs_name, injected_pos):
141 """
142 Creates the nested-mode decorator to be used when the implementation is provided in double-flat mode.
144 Note: we set the signature correctly so that this behaves exactly like a nested implementation in terms of
145 exceptions raised when the arguments are incorrect. Since the external method is called only once per decorator
146 usage and does not impact the decorated object / created wrappe, we can afford.
148 :param decorator_signature:
149 :param user_provided_wrapper:
150 :param injected_name:
151 :param f_args_name:
152 :param f_kwargs_name:
153 :return:
154 """
156 @with_signature(decorator_signature)
157 def _decorator(*args, **kwargs):
158 """ The decorator. Its signature will be overriden by `generated_signature` """
160 def _apply_decorator(decorated):
161 """ This is called when the decorator is applied to an object `decorated` """
163 # inject `decorated` under the correct name
164 # fix in case of var-positional arguments
165 if injected_pos >= 0:
166 new_args = args[:injected_pos] + (decorated,) + args[injected_pos:]
167 else:
168 new_args = args
169 kwargs[injected_name] = decorated
171 # create a signature-preserving wrapper using `makefun.wraps`
172 @wraps(decorated)
173 def wrapper(*f_args, **f_kwargs):
174 # if the user wishes us to inject the actual args and kwargs, let's inject them
175 # note: for these it is always keyword-based.
176 if f_args_name is not None: 176 ↛ 178line 176 didn't jump to line 178, because the condition on line 176 was never false
177 kwargs[f_args_name] = f_args
178 if f_kwargs_name is not None: 178 ↛ 182line 178 didn't jump to line 182, because the condition on line 178 was never false
179 kwargs[f_kwargs_name] = f_kwargs
181 # finally call the user-provided implementation
182 return user_provided_wrapper(*new_args, **kwargs)
184 return wrapper
186 return _apply_decorator
188 return _decorator
191class InvalidSignatureError(Exception):
192 """
193 Exception raised when a decorator signature is invalid with respect to the selected mode.
194 Typically when you use flat-mode or wrapped-mode symbols but your signature does not allow them to be safely
195 injected as keyword because they are followed by a var-positional argument.
196 """
197 pass
200def extract_mode_info(impl_sig, # type: Signature
201 flat_mode_decorated_name=None # type: str
202 ):
203 """
204 Returns the (name, Parameter) for the parameter with default value DECORATED
206 :param impl_sig: the implementing function's signature
207 :param flat_mode_decorated_name: an optional name of decorated argument. If provided a "flat mode" is automatically
208 set
209 :return:
210 """
211 mode = None
212 injected = None
213 injected_pos = None
214 position_of_varpos = -1
215 f_args = None
216 f_kwargs = None
218 if flat_mode_decorated_name is not None:
219 # validate that the 'decorated' parameter is a string representing a real parameter of the function
220 if not isinstance(flat_mode_decorated_name, str): 220 ↛ 221line 220 didn't jump to line 221, because the condition on line 220 was never true
221 raise InvalidSignatureError("'flat_mode_decorated_name' argument should be a string with the argument name "
222 "where the wrapped object should be injected")
224 mode = DECORATED
226 # analyze signature to detect injected arg and potentially varpositional
227 for i, (k, p) in enumerate(impl_sig.parameters.items()):
228 if k == flat_mode_decorated_name:
229 # this is the injected parameter
230 injected = p
231 injected_pos = i
232 elif p.kind is Parameter.VAR_POSITIONAL:
233 position_of_varpos = i
235 if injected is None: 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true
236 return ValueError("Function '%s' does not have an argument named '%s'" % (impl_sig.__name__,
237 flat_mode_decorated_name))
238 if injected.kind in {Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD}: 238 ↛ 239line 238 didn't jump to line 239, because the condition on line 238 was never true
239 raise InvalidSignatureError("`flat_mode_decorated_name` cannot correspond to a Var-pos nor Var-kw")
240 else:
241 # analyze signature to detect
242 for i, (p_name, p) in enumerate(impl_sig.parameters.items()):
243 if p.kind is Parameter.VAR_POSITIONAL:
244 position_of_varpos = i
245 if f_args is not None or f_kwargs is not None:
246 raise InvalidSignatureError("f_args and f_kwargs can only be used *after* var-positional arguments")
247 elif p.default is DECORATED:
248 if mode is not None: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true
249 raise InvalidSignatureError("only one of `DECORATED` or `WRAPPED` can be used in your signature")
250 else:
251 mode = DECORATED
252 injected = p
253 injected_pos = i
254 elif p.default is WRAPPED:
255 if mode is not None: 255 ↛ 256line 255 didn't jump to line 256, because the condition on line 255 was never true
256 raise InvalidSignatureError("only one of `DECORATED` or `WRAPPED` can be used in your signature")
257 else:
258 mode = WRAPPED
259 injected = p
260 injected_pos = i
261 elif p.default is F_ARGS:
262 f_args = p
263 elif p.default is F_KWARGS:
264 f_kwargs = p
266 if mode in {None, DECORATED} and (f_args is not None or f_kwargs is not None): 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true
267 raise InvalidSignatureError("`F_ARGS` or `F_KWARGS` should only be used if you use `WRAPPED`")
269 # argnames_before_varpos_arg = None
270 # if position_of_varpos > 0:
271 # # if there is a var-positional we will have to inject arguments before it manually
272 # argnames_before_varpos_arg = tuple(k for k in list(impl_sig.parameters.keys())[0:position_of_varpos])
273 #
274 # if argnames_before_varpos_arg is None:
275 # argnames_before_varpos_arg = tuple()
277 contains_varpositional = position_of_varpos >= 0
279 if not contains_varpositional or (injected_pos is not None and position_of_varpos < injected_pos):
280 # do not inject as positional but as keyword argument
281 injected_pos = -1
283 return mode, (injected.name if injected is not None else None), contains_varpositional, injected_pos, \
284 injected, (f_args.name if f_args is not None else None), (f_kwargs.name if f_kwargs is not None else None)
287# -----------
290class SignatureInfo(object):
291 """
292 Represents the knowledge we have on the decorator signature.
293 Provides handy properties to separate the code requirements from the implementation (and possibly cache).
294 """
295 __slots__ = '_exposed_signature', 'first_arg_def', '_use_signature_trick', 'contains_varpositional', \
296 'injected_pos'
298 def __init__(self, decorator_signature, contains_varpositional, injected_pos):
299 self._exposed_signature = decorator_signature
300 _, self.first_arg_def = get_first_parameter(decorator_signature)
301 self._use_signature_trick = False
302 self.contains_varpositional = contains_varpositional
303 self.injected_pos = injected_pos
305 # --
307 @property
308 def use_signature_trick(self):
309 return self._use_signature_trick
311 @use_signature_trick.setter
312 def use_signature_trick(self, use_signature_trick):
313 # note: as of today python 2.7 backport does not handle it properly, but hopefully it will :)
314 # see https://github.com/testing-cabal/funcsigs/issues/33.
315 self._use_signature_trick = use_signature_trick and not funcsigs_used
317 # --
319 @property
320 def exposed_signature(self):
321 return self._exposed_signature
323 @exposed_signature.setter
324 def exposed_signature(self, new_sig):
325 """
326 If the signature is changed then we should be careful..
327 :param new_sig:
328 :return:
329 """
330 # this currently only happen in a single specific case, control that to avoid future mistakes
331 if self.first_arg_kind is not Parameter.VAR_KEYWORD or len(self._exposed_signature.parameters) != 1:
332 raise NotImplementedError("This case should not happen")
334 self._exposed_signature = new_sig
335 self.contains_varpositional = any(p.kind is Parameter.VAR_POSITIONAL for p in new_sig.parameters.values())
336 _, self.first_arg_def = get_first_parameter(new_sig)
338 # --
340 @property
341 def first_arg_name(self):
342 return self.first_arg_def.name # if self.first_arg_def is not None else None
344 @property
345 def first_arg_name_with_possible_star(self):
346 return ('*' if self.is_first_arg_varpositional else '') + self.first_arg_name
348 @property
349 def first_arg_kind(self):
350 return self.first_arg_def.kind # if self.first_arg_def is not None else None
352 @property
353 def is_first_arg_keyword_only(self):
354 return self.first_arg_kind in {Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD}
356 @property
357 def is_first_arg_varpositional(self):
358 return self.first_arg_kind is Parameter.VAR_POSITIONAL
360 @property
361 def is_first_arg_positional_only(self):
362 return self.first_arg_kind is Parameter.POSITIONAL_ONLY
364 @property
365 def is_first_arg_mandatory(self):
366 return self.first_arg_def.default is Parameter.empty and self.first_arg_kind not in {Parameter.VAR_POSITIONAL,
367 Parameter.VAR_KEYWORD}
370def get_first_parameter(ds # type: Signature
371 ):
372 """
373 Returns the (name, Parameter) for the first parameter in the signature
375 :param ds:
376 :return:
377 """
378 try:
379 return next(iter(ds.parameters.items()))
380 except StopIteration:
381 return None, None