⬅ decopatch/utils_modes.py source

1 from makefun import remove_signature_parameters, with_signature, wraps
2  
3 try: # python 3.3+
4 from inspect import signature, Parameter
5 funcsigs_used = False
6 except ImportError:
7 from funcsigs import signature, Parameter
8 funcsigs_used = True
9  
10  
11 class _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_', )
17  
18 def __init__(self, repr_):
19 self.repr_ = repr_
20  
21 def __repr__(self):
22 return self.repr_
23  
24  
25 DECORATED = _Symbol('DECORATED')
26 # A symbol used in flat-mode signatures to declare where the decorated function should be injected
27  
28  
29 WRAPPED = _Symbol('WRAPPED')
30 # A symbol used in double flat-mode signatures to declare where the wrapped function should be injected
31  
32  
33 F_ARGS = _Symbol('F_ARGS')
34 # A symbol used in your double flat-mode signatures to declare where the wrapper args should be injected
35  
36  
37 F_KWARGS = _Symbol('F_KWARGS')
38 # A symbol used in your double flat-mode signatures to declare where the wrapper kwargs should be injected
39  
40  
41 def make_decorator_spec(impl_function,
42 flat_mode_decorated_name=None # type: str
43 ):
44 """
45 Analyzes the implementation function
46  
47  
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.
50  
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)
57  
58 # determine the mode (nested, flat, double-flat) and check signature
59 mode, injected_name, contains_varpositional, injected_pos, \
  • E122 Continuation line missing indentation or outdented
60 injected_arg, f_args_name, f_kwargs_name = extract_mode_info(implementors_signature, flat_mode_decorated_name)
61  
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
68  
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)
72  
73 # use the original function for the docstring/module metadata
74 function_for_metadata = impl_function
75  
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)
79  
80 elif mode is WRAPPED:
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)
85  
86 # use the original function for the docstring/module metadata
87 function_for_metadata = impl_function
88  
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)
92  
93 else:
94 raise ValueError("Unknown mode: %s" % mode)
95  
96 # create an object to easily access the exposed signature information afterwards
97 sig_info = SignatureInfo(exposed_signature, contains_varpositional, injected_pos)
98  
99 return sig_info, function_for_metadata, nested_impl_function
100  
101  
102 def 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.
105  
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.
109  
110 :param decorator_signature:
111 :param user_provided_applier:
112 :param injected_name:
113 :param argnames_before_varpos_arg:
114 :return:
115 """
116  
117 @with_signature(decorator_signature)
118 def _decorator(*args, **kwargs):
119 """ The decorator. Its signature will be overriden by `generated_signature` """
120  
121 def _apply_decorator(decorated):
122 """ This is called when the decorator is applied to an object `decorated` """
123  
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
131  
132 return user_provided_applier(*new_args, **kwargs)
133  
134 return _apply_decorator
135  
136 return _decorator
137  
138  
139 def 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.
143  
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.
147  
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 """
155  
156 @with_signature(decorator_signature)
157 def _decorator(*args, **kwargs):
158 """ The decorator. Its signature will be overriden by `generated_signature` """
159  
160 def _apply_decorator(decorated):
161 """ This is called when the decorator is applied to an object `decorated` """
162  
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
170  
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:
177 kwargs[f_args_name] = f_args
178 if f_kwargs_name is not None:
179 kwargs[f_kwargs_name] = f_kwargs
180  
181 # finally call the user-provided implementation
182 return user_provided_wrapper(*new_args, **kwargs)
183  
184 return wrapper
185  
186 return _apply_decorator
187  
188 return _decorator
189  
190  
191 class 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
198  
199  
  • F821 Undefined name 'Signature'
200 def 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
205  
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
217  
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):
221 raise InvalidSignatureError("'flat_mode_decorated_name' argument should be a string with the argument name "
222 "where the wrapped object should be injected")
223  
224 mode = DECORATED
225  
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
234  
235 if injected is None:
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}:
239 raise InvalidSignatureError("`flat_mode_decorated_name` cannot correspond to a Var-pos nor Var-kw")
240 else:
241 # analyze signature to detect
  • B007 Loop control variable 'p_name' not used within the loop body. If this is intended, start the name with an underscore.
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:
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:
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
265  
266 if mode in {None, DECORATED} and (f_args is not None or f_kwargs is not None):
267 raise InvalidSignatureError("`F_ARGS` or `F_KWARGS` should only be used if you use `WRAPPED`")
268  
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()
276  
277 contains_varpositional = position_of_varpos >= 0
278  
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
282  
283 return mode, (injected.name if injected is not None else None), contains_varpositional, injected_pos, \
  • E127 Continuation line over-indented for visual indent
284 injected, (f_args.name if f_args is not None else None), (f_kwargs.name if f_kwargs is not None else None)
285  
286  
287 # -----------
288  
289  
290 class 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'
297  
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
304  
305 # --
306  
307 @property
308 def use_signature_trick(self):
309 return self._use_signature_trick
310  
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
316  
317 # --
318  
319 @property
320 def exposed_signature(self):
321 return self._exposed_signature
322  
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")
333  
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)
337  
338 # --
339  
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
343  
344 @property
345 def first_arg_name_with_possible_star(self):
346 return ('*' if self.is_first_arg_varpositional else '') + self.first_arg_name
347  
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
351  
352 @property
353 def is_first_arg_keyword_only(self):
354 return self.first_arg_kind in {Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD}
355  
356 @property
357 def is_first_arg_varpositional(self):
358 return self.first_arg_kind is Parameter.VAR_POSITIONAL
359  
360 @property
361 def is_first_arg_positional_only(self):
362 return self.first_arg_kind is Parameter.POSITIONAL_ONLY
363  
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}
368  
369  
  • F821 Undefined name 'Signature'
370 def get_first_parameter(ds # type: Signature
371 ):
372 """
373 Returns the (name, Parameter) for the first parameter in the signature
374  
375 :param ds:
376 :return:
377 """
378 try:
379 return next(iter(ds.parameters.items()))
380 except StopIteration:
381 return None, None