Coverage for src/decopatch/main.py: 91%
67 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 with_signature, add_signature_parameters
2from decopatch.utils_modes import SignatureInfo, make_decorator_spec
3from decopatch.utils_disambiguation import create_single_arg_callable_or_class_disambiguator, disambiguate_call, \
4 DecoratorUsageInfo, can_arg_be_a_decorator_target
5from decopatch.utils_calls import with_parenthesis_usage, no_parenthesis_usage, call_in_appropriate_mode
7try: # python 3.3+
8 from inspect import signature, Parameter
9except ImportError:
10 from funcsigs import signature, Parameter
12try: # python 3.5+
13 from typing import Callable, Any, Optional
14except ImportError:
15 pass
18def function_decorator(enable_stack_introspection=False, # type: bool
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
26 decorator(is_function_decorator=True, is_class_decorator=False)
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)
46def class_decorator(enable_stack_introspection=False, # type: bool
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
54 decorator(is_function_decorator=False, is_class_decorator=True)
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)
74def decorator(is_function_decorator=True, # type: bool
75 is_class_decorator=True, # type: bool
76 enable_stack_introspection=False, # type: bool
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.
84 It support two main modes: "nested", and "flat".
86 In "flat" mode your implementation is flat:
88 ```python
89 def my_decorator(a, b, f=DECORATED):
90 # ...
91 return <replacement for f>
92 ```
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.
98 Otherwise the "nested" mode is activated. In this mode your implementation is nested, as usual in python:
100 ```python
101 def my_decorator(a, b):
102 def replace_f(f):
103 # ...
104 return <replacement for f>
105 return replace_f
106 ```
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.
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`:
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.
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 ```
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 """
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
157def 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
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.
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: 178 ↛ 179line 178 didn't jump to line 179, because the condition on line 178 was never true
179 raise ValueError("At least one of `is_function_decorator` and `is_class_decorator` must be True")
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
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)
191 else:
192 # (B) general case: at least one argument
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)
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)
209 # general case
210 return create_general_case_decorator(sig_info, nested_impl_function, disambiguator,
211 function_for_metadata=f_for_metadata)
214def 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.
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).
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.
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: 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true
233 function_for_metadata = decorator_function
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, *_)
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)
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__)
257 return new_decorator
260_GENERATED_VARPOS_NAME = '_'
263def 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.
272 When the decorator to create has a mandatory argument, it is exposed "as-is" because it is directly protected.
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).
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.
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)
307 return new_decorator
308 elif sig_info.use_signature_trick: 308 ↛ 313line 308 didn't jump to line 313, because the condition on line 308 was never false
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])
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)
321def 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
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)
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)
356 # call
357 return call_in_appropriate_mode(impl_function, dk, disambiguation_result)
359 # trick to declare that our true signature is different than our actual one
360 if sig_info.use_signature_trick: 360 ↛ 367line 360 didn't jump to line 367, because the condition on line 360 was never false
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
367 return new_decorator