⬅ decopatch/main.py source

1 from makefun import with_signature, add_signature_parameters
2 from decopatch.utils_modes import SignatureInfo, make_decorator_spec
3 from decopatch.utils_disambiguation import create_single_arg_callable_or_class_disambiguator, disambiguate_call, \
4 DecoratorUsageInfo, can_arg_be_a_decorator_target
5 from decopatch.utils_calls import with_parenthesis_usage, no_parenthesis_usage, call_in_appropriate_mode
6  
7 try: # python 3.3+
8 from inspect import signature, Parameter
9 except ImportError:
  • F401 'funcsigs.signature' imported but unused
10 from funcsigs import signature, Parameter
11  
12 try: # python 3.5+
13 from typing import Callable, Any, Optional
14 except ImportError:
15 pass
16  
17  
18 def function_decorator(enable_stack_introspection=False, # type: bool
  • F821 Undefined name 'FirstArgDisambiguation'
19 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation]
20 flat_mode_decorated_name=None, # type: Optional[str]
21 ):
22 """
23 A decorator to create function decorators.
24 Equivalent to
25  
26 decorator(is_function_decorator=True, is_class_decorator=False)
27  
28 :param enable_stack_introspection:
29 :param custom_disambiguator:
30 :param flat_mode_decorated_name:
31 :return:
32 """
33 if callable(enable_stack_introspection):
34 # no-parenthesis call
35 f = enable_stack_introspection
36 return decorator(is_function_decorator=True,
37 is_class_decorator=False)(f)
38 else:
39 return decorator(is_function_decorator=True,
40 is_class_decorator=False,
41 enable_stack_introspection=enable_stack_introspection,
42 custom_disambiguator=custom_disambiguator,
43 flat_mode_decorated_name=flat_mode_decorated_name)
44  
45  
46 def class_decorator(enable_stack_introspection=False, # type: bool
  • F821 Undefined name 'FirstArgDisambiguation'
47 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation]
48 flat_mode_decorated_name=None, # type: Optional[str]
49 ):
50 """
51 A decorator to create class decorators
52 Equivalent to
53  
54 decorator(is_function_decorator=False, is_class_decorator=True)
55  
56 :param enable_stack_introspection:
57 :param custom_disambiguator:
58 :param flat_mode_decorated_name:
59 :return:
60 """
61 if callable(enable_stack_introspection):
62 # no-parenthesis call
63 f = enable_stack_introspection
64 return decorator(is_function_decorator=False,
65 is_class_decorator=True)(f)
66 else:
67 return decorator(is_function_decorator=False,
68 is_class_decorator=True,
69 enable_stack_introspection=enable_stack_introspection,
70 custom_disambiguator=custom_disambiguator,
71 flat_mode_decorated_name=flat_mode_decorated_name)
72  
73  
74 def decorator(is_function_decorator=True, # type: bool
75 is_class_decorator=True, # type: bool
76 enable_stack_introspection=False, # type: bool
  • F821 Undefined name 'FirstArgDisambiguation'
77 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation]
78 use_signature_trick=True, # type: bool
79 flat_mode_decorated_name=None, # type: str
80 ):
81 """
82 A decorator to create decorators.
83  
84 It support two main modes: "nested", and "flat".
85  
86 In "flat" mode your implementation is flat:
87  
88 ```python
89 def my_decorator(a, b, f=DECORATED):
90 # ...
91 return <replacement for f>
92 ```
93  
94 For this mode to be automatically detected, your implementation has to have an argument with default value
95 `DECORATED`, or a non-None `decorated` argument name should be provided. This argument will be injected with the
96 decorated target when your decorator is used.
97  
98 Otherwise the "nested" mode is activated. In this mode your implementation is nested, as usual in python:
99  
100 ```python
101 def my_decorator(a, b):
102 def replace_f(f):
103 # ...
104 return <replacement for f>
105 return replace_f
106 ```
107  
108 In both modes, because python language does not redirect no-parenthesis usages (@my_decorator) to no-args usages
109 (@my_decorator()), `decopatch` tries to disambiguate automatically the type of call. See documentation for details.
110  
111 Finally you can use this function to directly create a "signature preserving function wrapper" decorator. This mode
112 is called "double flat" because it saves you from 2 levels of nesting. For this, use the `WRAPPED` default value
113 instead of `DECORATED`, and include two arguments with default values `F_ARGS` and `F_KWARGS`:
114  
115 ```python
116 @function_decorator
117 def say_hello(person="world", f=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS):
118 '''
119 This decorator wraps the decorated function so that a nice hello
120 message is printed before each call.
121  
122 :param person: the person name in the print message. Default = "world"
123 '''
124 print("hello, %s !" % person) # say hello
125 return f(*f_args, **f_kwargs) # call f
126 ```
127  
128 :param is_function_decorator:
129 :param is_class_decorator:
130 :param enable_stack_introspection:
131 :param custom_disambiguator:
132 :param use_signature_trick: if set to `True`, generated decorators will have a generic signature but the `help`
133 and `signature` modules will still think that they have the specific signature, because by default they
134 follow the `__wrapped__` attribute if it is set. See
135 https://docs.python.org/3/library/inspect.html#inspect.signature for details.
136 :param flat_mode_decorated_name:
137 :return:
138 """
139  
140 if callable(is_function_decorator):
141 # called without argument: the first argument is actually the decorated function
142 f = is_function_decorator
143 return create_decorator(f)
144 else:
145 # called with argument. Return a decorator function
146 def _apply_on(f):
147 return create_decorator(f,
148 is_function_decorator=is_function_decorator,
149 is_class_decorator=is_class_decorator,
150 enable_stack_introspection=enable_stack_introspection,
151 custom_disambiguator=custom_disambiguator,
152 flat_mode_decorated_name=flat_mode_decorated_name,
153 use_signature_trick=use_signature_trick)
154 return _apply_on
155  
156  
157 def create_decorator(impl_function,
158 is_function_decorator=True, # type: bool
159 is_class_decorator=True, # type: bool
160 enable_stack_introspection=False, # type: bool
  • F821 Undefined name 'FirstArgDisambiguation'
161 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation]
162 use_signature_trick=True, # type: bool
163 flat_mode_decorated_name=None, # type: Optional[str]
164 ):
165 """
166 Main function to create a decorator implemented with the `decorator_function` implementation.
167  
168 :param impl_function:
169 :param is_function_decorator:
170 :param is_class_decorator:
171 :param enable_stack_introspection:
172 :param custom_disambiguator:
173 :param use_signature_trick:
174 :param flat_mode_decorated_name:
175 :return:
176 """
177 # input checks
178 if not is_function_decorator and not is_class_decorator:
179 raise ValueError("At least one of `is_function_decorator` and `is_class_decorator` must be True")
180  
181 # (1) --- Detect mode and prepare signature to generate --------
182 sig_info, f_for_metadata, nested_impl_function = make_decorator_spec(impl_function, flat_mode_decorated_name)
183 sig_info.use_signature_trick = use_signature_trick
184  
185 # (2) --- Generate according to the situation--------
186 # check if the resulting function has any parameter at all
187 if len(sig_info.exposed_signature.parameters) == 0:
188 # (A) no argument at all. Special handling.
189 return create_no_args_decorator(nested_impl_function, function_for_metadata=f_for_metadata)
190  
191 else:
192 # (B) general case: at least one argument
193  
194 # if the decorator has at least 1 mandatory argument, we allow it to be created but its default behaviour
195 # is to raise errors only on ambiguous cases. Usually ambiguous cases are rare (not nominal cases)
196 disambiguator = create_single_arg_callable_or_class_disambiguator(nested_impl_function,
197 is_function_decorator,
198 is_class_decorator,
199 custom_disambiguator,
200 enable_stack_introspection,
201 signature_knowledge=sig_info)
202  
203 if sig_info.is_first_arg_keyword_only:
204 # in this case the decorator *can* be used without arguments but *cannot* with one positional argument,
205 # which will happen in the no-parenthesis case. We have to modify the signature to allow no-parenthesis
206 return create_kwonly_decorator(sig_info, nested_impl_function, disambiguator,
207 function_for_metadata=f_for_metadata)
208  
209 # general case
210 return create_general_case_decorator(sig_info, nested_impl_function, disambiguator,
211 function_for_metadata=f_for_metadata)
212  
213  
214 def create_no_args_decorator(decorator_function,
215 function_for_metadata=None,
216 ):
217 """
218 Utility method to create a decorator that has no arguments at all and is implemented by `decorator_function`, in
219 implementation-first mode or usage-first mode.
220  
221 The created decorator is a function with var-args. When called it checks the length
222 (0=called with parenthesis, 1=called without, 2=error).
223  
224 Note: we prefer to use this var-arg signature rather than a "(_=None)" signature, because it is more readable for
225 the decorator's help.
226  
227 :param decorator_function:
228 :param function_for_metadata: an alternate function to use for the documentation and module metadata of the
229 generated function
230 :return:
231 """
232 if function_for_metadata is None:
233 function_for_metadata = decorator_function
234  
235 @with_signature(None,
236 func_name=function_for_metadata.__name__,
237 doc=function_for_metadata.__doc__,
238 module_name=function_for_metadata.__module__)
239 def new_decorator(*_):
240 """
241 Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis
242 """
243 if len(_) == 0:
244 # called with no args BUT parenthesis: @foo_decorator().
245 return with_parenthesis_usage(decorator_function, *_)
246  
247 elif len(_) == 1:
248 first_arg_value = _[0]
249 if can_arg_be_a_decorator_target(first_arg_value):
250 # called with no arg NOR parenthesis: @foo_decorator
251 return no_parenthesis_usage(decorator_function, first_arg_value)
252  
253 # more than 1 argument or non-decorable argument: not possible
254 raise TypeError("Decorator function '%s' does not accept any argument."
255 "" % decorator_function.__name__)
256  
257 return new_decorator
258  
259  
260 _GENERATED_VARPOS_NAME = '_'
261  
262  
263 def create_kwonly_decorator(sig_info, # type: SignatureInfo
264 decorator_function,
265 disambiguator,
266 function_for_metadata,
267 ):
268 """
269 Utility method to create a decorator that has only keyword arguments and is implemented by `decorator_function`, in
270 implementation-first mode or usage-first mode.
271  
272 When the decorator to create has a mandatory argument, it is exposed "as-is" because it is directly protected.
273  
274 Otherwise (if all arguments are optional and keyword-only), we modify the created decorator's signature to add a
275 leading var-args, so that users will be able to call the decorator without parenthesis.
276 When called it checks the length of the var-positional received:
277 - 0 positional=called with parenthesis,
278 - 1 and the positional argument is not a callable/class : called with parenthesis
279 - 1 and the positional argument is a callable/class: disambiguation is required to know if this is without
280 parenthesis or with positional arg
281 - 2 positional=error).
282  
283 Note: we prefer to use this var-arg signature rather than a "(_=None)" signature, because it is more readable for
284 the decorator's help.
285  
286 :param sig_info:
287 :param decorator_function:
288 :param function_for_metadata: an alternate function to use for the documentation and module metadata of the
289 generated function
290 :return:
291 """
292 if sig_info.is_first_arg_mandatory:
293 # The first argument is mandatory AND keyword. So we do not need to change the signature to be fully protected
294 # indeed python will automatically raise a `TypeError` when users will use this decorator without parenthesis
295 # or with positional arguments.
296 @with_signature(sig_info.exposed_signature,
297 func_name=function_for_metadata.__name__,
298 doc=function_for_metadata.__doc__,
299 modulename=function_for_metadata.__module__)
300 def new_decorator(*no_args, **kwargs):
301 """
302 Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis
303 """
304 # this is a parenthesis call, because otherwise a `TypeError` would already have been raised by python.
305 return with_parenthesis_usage(decorator_function, *no_args, **kwargs)
306  
307 return new_decorator
308 elif sig_info.use_signature_trick:
309 # no need to modify the signature, we will expose *args, **kwargs
310 pass
311 else:
312 # modify the signature to add a var-positional first
313 gen_varpos_param = Parameter(_GENERATED_VARPOS_NAME, kind=Parameter.VAR_POSITIONAL)
314 sig_info.exposed_signature = add_signature_parameters(sig_info.exposed_signature, first=[gen_varpos_param])
315  
316 # we can fallback to the same case than varpositional
317 return create_general_case_decorator(sig_info, decorator_function, disambiguator,
318 function_for_metadata=function_for_metadata)
319  
320  
321 def create_general_case_decorator(sig_info, # type: SignatureInfo
322 impl_function,
323 disambiguator,
324 function_for_metadata,
325 ):
326 """
327 This method supports both with-trick and without-trick
328  
329 :param sig_info:
330 :param impl_function:
331 :param disambiguator:
332 :param function_for_metadata: an alternate function to use for the documentation and module metadata of the
333 generated function
334 :return:
335 """
336 # Note: since we expose a decorator with a preserved signature and not (*args, **kwargs)
337 # we lose the information about the number of arguments *actually* provided.
338 # `@with_signature` will send us all arguments, including the defaults (because it has no way to
339 # determine what was actually provided by the user and what is just the default). So in this decorator we may
340 # receive several kwargs
341 # - even if user did not provide them
342 # - and even if user provided them as positional !! (except for var-positional and fuutre positional-only args)
343  
344 @with_signature(None if sig_info.use_signature_trick else sig_info.exposed_signature,
345 func_name=function_for_metadata.__name__,
346 doc=function_for_metadata.__doc__,
347 module_name=function_for_metadata.__module__)
348 def new_decorator(*args, **kwargs):
349 """
350 Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis
351 """
352 # disambiguate
353 dk = DecoratorUsageInfo(sig_info, args, kwargs)
354 disambiguation_result = disambiguate_call(dk, disambiguator)
355  
356 # call
357 return call_in_appropriate_mode(impl_function, dk, disambiguation_result)
358  
359 # trick to declare that our true signature is different than our actual one
360 if sig_info.use_signature_trick:
361 # thanks to setting this field, python help() and signature() will be tricked without compromising the
362 # actual code signature (so, no dynamic function creation in @with_signature above).
363 # Indeed by default they follow the `__wrapped__` attribute if it is set. See
364 # https://docs.python.org/3/library/inspect.html#inspect.signature for details.
365 new_decorator.__wrapped__ = impl_function
366  
367 return new_decorator