You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+18-7Lines changed: 18 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -250,11 +250,15 @@ With the `@superfunction` decorator, you no longer need to call special methods
250
250
251
251
If you use it as a regular function, a regular function will be created "under the hood" based on the template and then called:
252
252
253
+
To call a superfunction like a regular function, you need to use a special tilde syntax:
254
+
253
255
```python
254
-
my_superfunction()
256
+
~my_superfunction()
255
257
#> so, it's just usual function!
256
258
```
257
259
260
+
Yes, the tilde syntax simply means putting the `~` symbol in front of the function name when calling it.
261
+
258
262
If you use `asyncio.run` or the `await` keyword when calling, the async version of the function will be automatically generated and called:
259
263
260
264
```python
@@ -273,15 +277,22 @@ list(my_superfunction())
273
277
274
278
How does it work? In fact, `my_superfunction` returns some kind of intermediate object that can be both a coroutine and a generator and an ordinary function. Depending on how it is handled, it lazily code-generates the desired version of the function from a given template and uses it.
275
279
276
-
Separately, it is worth considering how the superfunction works in the normal function mode. The point is that we need to somehow distinguish a call wrapped with an `await` statement or iteration from a call in which we use a function as a regular function. To do this, a special trick is used by default: assigning a finalizer to reset the reference counter to a variable. When the reference count is zero, the normal (synchronous) implementation of the function is automatically called. However, this imposes 2 restrictions:
280
+
By default, a superfunction is called as a regular function using tilde syntax, but there is another mode. To enable it, use the appropriate flag in the decorator:
277
281
278
-
- You cannot use the return values from this function in any way. This works in the coroutine function mode, but not in the regular mode. If you try to save the result of a function call to a variable, the reference counter to the returned object will not reset while this variable exists, and accordingly the function will not actually be called.
279
-
- Exceptions will not work normally inside this function. Rather, they can be picked up and intercepted in [`sys.unraisablehook`](https://docs.python.org/3/library/sys.html#sys.unraisablehook), but they will not go up the stack above this function. This is due to a feature of CPython: exceptions that occur inside callbacks for finalizing objects are completely escaped.
282
+
```python
283
+
@superfunction(tilde_syntax=False)
284
+
```
280
285
281
-
To get around both of these problems, you can use a special syntactic trick: put the `~` symbol before calling the function. Like this:
286
+
In this case, the superfunction can be called in exactly the same way as a regular function:
282
287
283
288
```python
284
-
~my_superfunction()
289
+
my_superfunction()
290
+
#> so, it's just usual function!
285
291
```
286
292
287
-
In this case, the behavior of the superfunction will be completely indistinguishable from the behavior of a regular function. Return expressions and exceptions will work exactly as you expect them to.
293
+
However, it is not completely free. The fact is that this mode uses a special trick with a reference counter, a special mechanism inside the interpreter that cleans up memory. When there is no reference to an object, the interpreter deletes it, and you can link your callback to this process. It is inside such a callback that the contents of your function are actually executed. This imposes some restrictions on you:
294
+
295
+
- You cannot use the return values from this function in any way. If you try to save the result of a function call to a variable, the reference counter to the returned object will not reset while this variable exists, and accordingly the function will not actually be called.
296
+
- Exceptions will not work normally inside this function. Rather, they can be picked up and intercepted in [`sys.unraisablehook`](https://docs.python.org/3/library/sys.html#sys.unraisablehook), but they will not go up the stack above this function. This is due to a feature of CPython: exceptions that occur inside callbacks for finalizing objects are completely escaped.
297
+
298
+
This mode is well suited for functions such as logging or sending statistics from your code: simple functions from which no exceptions or return values are expected. In all other cases, I recommend using the tilde syntax.
Copy file name to clipboardExpand all lines: tests/units/decorators/test_superfunction.py
+99-2Lines changed: 99 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,5 @@
1
1
importio
2
+
importsys
2
3
fromasyncioimportrun
3
4
fromcontextlibimportredirect_stdout
4
5
@@ -22,7 +23,7 @@
22
23
"""
23
24
24
25
25
-
deftest_just_sync_call():
26
+
deftest_just_sync_call_without_breackets():
26
27
@superfunction
27
28
deffunction():
28
29
withsync_context:
@@ -32,12 +33,44 @@ def function():
32
33
withgenerator_context:
33
34
yieldfrom [1, 2, 3]
34
35
36
+
buffer=io.StringIO()
37
+
withredirect_stdout(buffer):
38
+
~function()
39
+
assertbuffer.getvalue() =="1\n"
40
+
41
+
42
+
deftest_just_sync_call_without_tilde_syntax():
43
+
@superfunction(tilde_syntax=False)
44
+
deffunction():
45
+
withsync_context:
46
+
print(1)
47
+
withasync_context:
48
+
print(2)
49
+
withgenerator_context:
50
+
yieldfrom [1, 2, 3]
51
+
35
52
buffer=io.StringIO()
36
53
withredirect_stdout(buffer):
37
54
function()
38
55
assertbuffer.getvalue() =="1\n"
39
56
40
57
58
+
deftest_just_sync_call_with_tilde_syntax():
59
+
@superfunction(tilde_syntax=True)
60
+
deffunction():
61
+
withsync_context:
62
+
print(1)
63
+
withasync_context:
64
+
print(2)
65
+
withgenerator_context:
66
+
yieldfrom [1, 2, 3]
67
+
68
+
buffer=io.StringIO()
69
+
withredirect_stdout(buffer):
70
+
~function()
71
+
assertbuffer.getvalue() =="1\n"
72
+
73
+
41
74
deftest_just_async_call():
42
75
@superfunction
43
76
deffunction():
@@ -84,7 +117,7 @@ def function(a, b):
84
117
85
118
buffer=io.StringIO()
86
119
withredirect_stdout(buffer):
87
-
function(1, 2)
120
+
~function(1, 2)
88
121
assertbuffer.getvalue() =="1\n"
89
122
90
123
@@ -248,3 +281,67 @@ def template():
248
281
249
282
withpytest.raises(WrongDecoratorSyntaxError, match=full_match('The @superfunction decorator cannot be used in conjunction with other decorators.')):
250
283
~template()
284
+
285
+
286
+
deftest_pass_coroutine_function_to_decorator():
287
+
withpytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @superfunction. You can't use async functions.")):
288
+
@superfunction
289
+
asyncdeffunction_maker():
290
+
return4
291
+
292
+
293
+
deftest_pass_not_function_to_decorator():
294
+
withpytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @superfunction.")):
295
+
superfunction(1)
296
+
297
+
298
+
deftest_try_to_pass_lambda_to_decorator():
299
+
withpytest.raises(ValueError, match=full_match("Only regular or generator functions can be used as a template for @superfunction. Don't use lambdas here.")):
300
+
superfunction(lambdax: x)
301
+
302
+
303
+
deftest_choose_tilde_syntax_off_and_use_tilde():
304
+
@superfunction(tilde_syntax=False)
305
+
deffunction():
306
+
pass
307
+
308
+
withpytest.raises(NotImplementedError, match=full_match('The syntax with ~ is disabled for this superfunction. Call it with simple breackets.')):
raiseNotImplementedError(f'The tilde-syntax is enabled for the "{transformer.function.__name__}" function. Call it like this: ~{transformer.function.__name__}().')
raiseWrongDecoratorSyntaxError(f"The @{decorator_name} decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")
137
137
138
138
fordecoratorinnode.decorator_list:
139
+
ifisinstance(decorator, Call):
140
+
decorator=decorator.func
139
141
ifdecorator.id!=decorator_name:
140
142
raiseWrongDecoratorSyntaxError(f'The @{decorator_name} decorator cannot be used in conjunction with other decorators.')
0 commit comments