transfunctions 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transfunctions-0.0.1/LICENSE +21 -0
- transfunctions-0.0.1/PKG-INFO +103 -0
- transfunctions-0.0.1/README.md +76 -0
- transfunctions-0.0.1/pyproject.toml +46 -0
- transfunctions-0.0.1/setup.cfg +4 -0
- transfunctions-0.0.1/transfunctions/__init__.py +14 -0
- transfunctions-0.0.1/transfunctions/decorator.py +6 -0
- transfunctions-0.0.1/transfunctions/errors.py +10 -0
- transfunctions-0.0.1/transfunctions/markers.py +24 -0
- transfunctions-0.0.1/transfunctions/py.typed +0 -0
- transfunctions-0.0.1/transfunctions/transformer.py +196 -0
- transfunctions-0.0.1/transfunctions.egg-info/PKG-INFO +103 -0
- transfunctions-0.0.1/transfunctions.egg-info/SOURCES.txt +13 -0
- transfunctions-0.0.1/transfunctions.egg-info/dependency_links.txt +1 -0
- transfunctions-0.0.1/transfunctions.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 pomponchik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: transfunctions
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Say NO to Python fragmentation on sync and async
|
|
5
|
+
Author-email: Evgeniy Blinov <zheni-b@yandex.ru>
|
|
6
|
+
Project-URL: Source, https://github.com/pomponchik/transfunctions
|
|
7
|
+
Project-URL: Tracker, https://github.com/pomponchik/transfunctions/issues
|
|
8
|
+
Keywords: async,sync to async,async to sync
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
|
|
28
|
+
# transfunctions
|
|
29
|
+
|
|
30
|
+
[](https://pepy.tech/project/transfunctions)
|
|
31
|
+
[](https://pepy.tech/project/transfunctions)
|
|
32
|
+
[](https://coveralls.io/github/pomponchik/transfunctions?branch=develop)
|
|
33
|
+
[](https://github.com/boyter/scc/)
|
|
34
|
+
[](https://hitsofcode.com/github/pomponchik/transfunctions/view?branch=main)
|
|
35
|
+
[](https://github.com/pomponchik/transfunctions/actions/workflows/tests_and_coverage.yml)
|
|
36
|
+
[](https://pypi.python.org/pypi/transfunctions)
|
|
37
|
+
[](https://badge.fury.io/py/transfunctions)
|
|
38
|
+
[](https://github.com/astral-sh/ruff)
|
|
39
|
+
|
|
40
|
+
This library is designed to solve one of the most important problems in python programming - dividing all written code into 2 camps: sync and async. We get rid of code duplication by using templates.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Table of contents
|
|
44
|
+
|
|
45
|
+
- [**Quick start**](#quick-start)
|
|
46
|
+
- [**The problem**](#the-problem)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
Install it:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install transfunctions
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
And use:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from asyncio import run
|
|
61
|
+
from transfunctions import (
|
|
62
|
+
transfunction,
|
|
63
|
+
sync_context,
|
|
64
|
+
async_context,
|
|
65
|
+
generator_context,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@transfunction
|
|
69
|
+
def template():
|
|
70
|
+
print('so, ', end='')
|
|
71
|
+
with sync_context:
|
|
72
|
+
print("it's just usual function!")
|
|
73
|
+
with async_context:
|
|
74
|
+
print("it's an async function!")
|
|
75
|
+
with generator_context:
|
|
76
|
+
print("it's a generator function!")
|
|
77
|
+
yield
|
|
78
|
+
|
|
79
|
+
function = template.get_usual_function()
|
|
80
|
+
function()
|
|
81
|
+
#> so, it's just usual function!
|
|
82
|
+
|
|
83
|
+
async_function = template.get_async_function()
|
|
84
|
+
run(async_function())
|
|
85
|
+
#> so, it's an async function!
|
|
86
|
+
|
|
87
|
+
generator_function = template.get_generator_function()
|
|
88
|
+
list(generator_function())
|
|
89
|
+
#> so, it's a generator function!
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
As you can see, in this case, 3 different functions were created based on the template, including both common parts and unique ones for a specific type of function.
|
|
93
|
+
|
|
94
|
+
You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
## The problem
|
|
98
|
+
|
|
99
|
+
Since the `asyncio` module appeared in Python more than 10 years ago, many well-known libraries have received their asynchronous alternates. A lot of the code in the Python ecosystem has been [duplicated](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), and you probably know many such examples.
|
|
100
|
+
|
|
101
|
+
The reason for this problem is that the Python community has chosen a way to implement asynchrony expressed through syntax. There are new keywords in the language, such as `async` and `await`. Their use makes the code so-called "[multicolored](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)": all the functions in it can be red or blue, and depending on the color, the rules for calling them are different. You can only call blue functions from red ones, but not vice versa.
|
|
102
|
+
|
|
103
|
+
I must say that implementing asynchronous calls using a special syntax is not the only solution. There are languages like Go where runtime can independently determine "under the hood" where a function should be asynchronous and where not, and choose the correct way to call it. A programmer does not need to manually "colorize" their functions there. Personally, I think that choosing a different path is the mistake of the Python community, but that's not what we're discussing here.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# transfunctions
|
|
2
|
+
|
|
3
|
+
[](https://pepy.tech/project/transfunctions)
|
|
4
|
+
[](https://pepy.tech/project/transfunctions)
|
|
5
|
+
[](https://coveralls.io/github/pomponchik/transfunctions?branch=develop)
|
|
6
|
+
[](https://github.com/boyter/scc/)
|
|
7
|
+
[](https://hitsofcode.com/github/pomponchik/transfunctions/view?branch=main)
|
|
8
|
+
[](https://github.com/pomponchik/transfunctions/actions/workflows/tests_and_coverage.yml)
|
|
9
|
+
[](https://pypi.python.org/pypi/transfunctions)
|
|
10
|
+
[](https://badge.fury.io/py/transfunctions)
|
|
11
|
+
[](https://github.com/astral-sh/ruff)
|
|
12
|
+
|
|
13
|
+
This library is designed to solve one of the most important problems in python programming - dividing all written code into 2 camps: sync and async. We get rid of code duplication by using templates.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Table of contents
|
|
17
|
+
|
|
18
|
+
- [**Quick start**](#quick-start)
|
|
19
|
+
- [**The problem**](#the-problem)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
Install it:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install transfunctions
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
And use:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from asyncio import run
|
|
34
|
+
from transfunctions import (
|
|
35
|
+
transfunction,
|
|
36
|
+
sync_context,
|
|
37
|
+
async_context,
|
|
38
|
+
generator_context,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@transfunction
|
|
42
|
+
def template():
|
|
43
|
+
print('so, ', end='')
|
|
44
|
+
with sync_context:
|
|
45
|
+
print("it's just usual function!")
|
|
46
|
+
with async_context:
|
|
47
|
+
print("it's an async function!")
|
|
48
|
+
with generator_context:
|
|
49
|
+
print("it's a generator function!")
|
|
50
|
+
yield
|
|
51
|
+
|
|
52
|
+
function = template.get_usual_function()
|
|
53
|
+
function()
|
|
54
|
+
#> so, it's just usual function!
|
|
55
|
+
|
|
56
|
+
async_function = template.get_async_function()
|
|
57
|
+
run(async_function())
|
|
58
|
+
#> so, it's an async function!
|
|
59
|
+
|
|
60
|
+
generator_function = template.get_generator_function()
|
|
61
|
+
list(generator_function())
|
|
62
|
+
#> so, it's a generator function!
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
As you can see, in this case, 3 different functions were created based on the template, including both common parts and unique ones for a specific type of function.
|
|
66
|
+
|
|
67
|
+
You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## The problem
|
|
71
|
+
|
|
72
|
+
Since the `asyncio` module appeared in Python more than 10 years ago, many well-known libraries have received their asynchronous alternates. A lot of the code in the Python ecosystem has been [duplicated](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), and you probably know many such examples.
|
|
73
|
+
|
|
74
|
+
The reason for this problem is that the Python community has chosen a way to implement asynchrony expressed through syntax. There are new keywords in the language, such as `async` and `await`. Their use makes the code so-called "[multicolored](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)": all the functions in it can be red or blue, and depending on the color, the rules for calling them are different. You can only call blue functions from red ones, but not vice versa.
|
|
75
|
+
|
|
76
|
+
I must say that implementing asynchronous calls using a special syntax is not the only solution. There are languages like Go where runtime can independently determine "under the hood" where a function should be asynchronous and where not, and choose the correct way to call it. A programmer does not need to manually "colorize" their functions there. Personally, I think that choosing a different path is the mistake of the Python community, but that's not what we're discussing here.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==68.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "transfunctions"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Evgeniy Blinov", email="zheni-b@yandex.ru" },
|
|
10
|
+
]
|
|
11
|
+
description = 'Say NO to Python fragmentation on sync and async'
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
'Operating System :: MacOS :: MacOS X',
|
|
17
|
+
'Operating System :: Microsoft :: Windows',
|
|
18
|
+
'Operating System :: POSIX',
|
|
19
|
+
'Operating System :: POSIX :: Linux',
|
|
20
|
+
'Programming Language :: Python',
|
|
21
|
+
'Programming Language :: Python :: 3.8',
|
|
22
|
+
'Programming Language :: Python :: 3.9',
|
|
23
|
+
'Programming Language :: Python :: 3.10',
|
|
24
|
+
'Programming Language :: Python :: 3.11',
|
|
25
|
+
'Programming Language :: Python :: 3.12',
|
|
26
|
+
'Programming Language :: Python :: 3.13',
|
|
27
|
+
'License :: OSI Approved :: MIT License',
|
|
28
|
+
'Intended Audience :: Developers',
|
|
29
|
+
'Topic :: Software Development :: Libraries',
|
|
30
|
+
]
|
|
31
|
+
keywords = [
|
|
32
|
+
'async',
|
|
33
|
+
'sync to async',
|
|
34
|
+
'async to sync',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
"transfunctions" = ["py.typed"]
|
|
39
|
+
|
|
40
|
+
[tool.mutmut]
|
|
41
|
+
paths_to_mutate="transfunctions"
|
|
42
|
+
runner="pytest"
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
'Source' = 'https://github.com/pomponchik/transfunctions'
|
|
46
|
+
'Tracker' = 'https://github.com/pomponchik/transfunctions/issues'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from transfunctions.decorator import transfunction as transfunction
|
|
2
|
+
|
|
3
|
+
from transfunctions.markers import (
|
|
4
|
+
async_context as async_context,
|
|
5
|
+
sync_context as sync_context,
|
|
6
|
+
generator_context as generator_context,
|
|
7
|
+
await_it as await_it,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from transfunctions.errors import (
|
|
11
|
+
CallTransfunctionDirectlyError as CallTransfunctionDirectlyError,
|
|
12
|
+
DualUseOfDecoratorError as DualUseOfDecoratorError,
|
|
13
|
+
WrongDecoratorSyntaxError as WrongDecoratorSyntaxError,
|
|
14
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@contextmanager
|
|
6
|
+
def create_async_context():
|
|
7
|
+
yield
|
|
8
|
+
|
|
9
|
+
@contextmanager
|
|
10
|
+
def create_sync_context():
|
|
11
|
+
yield
|
|
12
|
+
|
|
13
|
+
@contextmanager
|
|
14
|
+
def create_generator_context():
|
|
15
|
+
yield
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async_context = create_async_context()
|
|
19
|
+
sync_context = create_sync_context()
|
|
20
|
+
generator_context = create_generator_context()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def await_it(some_expression: Any):
|
|
24
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from sys import version_info
|
|
2
|
+
from typing import Optional, Union, List, Any
|
|
3
|
+
from types import FunctionType
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from inspect import isfunction, iscoroutinefunction, getsource, getfile
|
|
6
|
+
from ast import parse, NodeTransformer, Expr, AST, FunctionDef, AsyncFunctionDef, increment_lineno, Await, Return, Name, Load, Assign, Constant, Store, arguments
|
|
7
|
+
from functools import wraps, update_wrapper
|
|
8
|
+
|
|
9
|
+
from transfunctions.errors import CallTransfunctionDirectlyError, DualUseOfDecoratorError, WrongDecoratorSyntaxError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FunctionTransformer:
|
|
13
|
+
def __init__(self, function: Callable, decorator_lineno: int) -> None:
|
|
14
|
+
if isinstance(function, type(self)):
|
|
15
|
+
raise DualUseOfDecoratorError("You cannot use the 'transfunction' decorator twice for the same function.")
|
|
16
|
+
if not isfunction(function):
|
|
17
|
+
raise ValueError("Only regular or generator functions can be used as a template for @transfunction.")
|
|
18
|
+
if iscoroutinefunction(function):
|
|
19
|
+
raise ValueError("Only regular or generator functions can be used as a template for @transfunction. You can't use async functions.")
|
|
20
|
+
if self.is_lambda(function):
|
|
21
|
+
raise ValueError("Only regular or generator functions can be used as a template for @transfunction. Don't use lambdas here.")
|
|
22
|
+
|
|
23
|
+
self.function = function
|
|
24
|
+
self.decorator_lineno = decorator_lineno
|
|
25
|
+
|
|
26
|
+
def __call__(self, *args: Any, **kwargs: Any) -> None:
|
|
27
|
+
raise CallTransfunctionDirectlyError("You can't call a transfunction object directly, create a function, a generator function or a coroutine function from it.")
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def is_lambda(function: Callable) -> bool:
|
|
31
|
+
# https://stackoverflow.com/a/3655857/14522393
|
|
32
|
+
lambda_example = lambda: 0 # noqa: E731
|
|
33
|
+
return isinstance(function, type(lambda_example)) and function.__name__ == lambda_example.__name__
|
|
34
|
+
|
|
35
|
+
def get_usual_function(self):
|
|
36
|
+
return self.extract_context('sync_context')
|
|
37
|
+
|
|
38
|
+
def get_async_function(self):
|
|
39
|
+
original_function = self.function
|
|
40
|
+
|
|
41
|
+
class ConvertSyncFunctionToAsync(NodeTransformer):
|
|
42
|
+
def visit_FunctionDef(self, node: Expr) -> Optional[Union[AST, List[AST]]]:
|
|
43
|
+
if node.name == original_function.__name__:
|
|
44
|
+
return AsyncFunctionDef(
|
|
45
|
+
name=original_function.__name__,
|
|
46
|
+
args=node.args,
|
|
47
|
+
body=node.body,
|
|
48
|
+
decorator_list=node.decorator_list,
|
|
49
|
+
lineno=node.lineno,
|
|
50
|
+
col_offset=node.col_offset,
|
|
51
|
+
)
|
|
52
|
+
return node
|
|
53
|
+
|
|
54
|
+
class ExtractAwaitExpressions(NodeTransformer):
|
|
55
|
+
def visit_Call(self, node: Expr) -> Optional[Union[AST, List[AST]]]:
|
|
56
|
+
if node.func.id == 'await_it':
|
|
57
|
+
return Await(
|
|
58
|
+
value=node.args[0],
|
|
59
|
+
lineno=node.lineno,
|
|
60
|
+
col_offset=node.col_offset,
|
|
61
|
+
)
|
|
62
|
+
return node
|
|
63
|
+
|
|
64
|
+
return self.extract_context(
|
|
65
|
+
'async_context',
|
|
66
|
+
addictional_transformers=[
|
|
67
|
+
ConvertSyncFunctionToAsync(),
|
|
68
|
+
ExtractAwaitExpressions(),
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def get_generator_function(self):
|
|
73
|
+
return self.extract_context('generator_context')
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def clear_spaces_from_source_code(source_code: str) -> str:
|
|
77
|
+
splitted_source_code = source_code.split('\n')
|
|
78
|
+
|
|
79
|
+
indent = 0
|
|
80
|
+
for letter in splitted_source_code[0]:
|
|
81
|
+
if letter.isspace():
|
|
82
|
+
indent += 1
|
|
83
|
+
else:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
new_splitted_source_code = [x[indent:] for x in splitted_source_code]
|
|
87
|
+
|
|
88
|
+
return '\n'.join(new_splitted_source_code)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def extract_context(self, context_name: str, addictional_transformers: Optional[List[NodeTransformer]] = None):
|
|
92
|
+
source_code = getsource(self.function)
|
|
93
|
+
converted_source_code = self.clear_spaces_from_source_code(source_code)
|
|
94
|
+
tree = parse(converted_source_code)
|
|
95
|
+
original_function = self.function
|
|
96
|
+
transfunction_decorator = None
|
|
97
|
+
|
|
98
|
+
class RewriteContexts(NodeTransformer):
|
|
99
|
+
def visit_With(self, node: Expr) -> Optional[Union[AST, List[AST]]]:
|
|
100
|
+
if len(node.items) == 1 and node.items[0].context_expr.id == context_name:
|
|
101
|
+
return node.body
|
|
102
|
+
elif len(node.items) == 1 and node.items[0].context_expr.id != context_name and context_name in ('async_context', 'sync_context', 'generator_context'):
|
|
103
|
+
return None
|
|
104
|
+
return node
|
|
105
|
+
|
|
106
|
+
class DeleteDecorator(NodeTransformer):
|
|
107
|
+
def visit_FunctionDef(self, node: Expr) -> Optional[Union[AST, List[AST]]]:
|
|
108
|
+
if node.name == original_function.__name__:
|
|
109
|
+
nonlocal transfunction_decorator
|
|
110
|
+
transfunction_decorator = None
|
|
111
|
+
|
|
112
|
+
if not node.decorator_list:
|
|
113
|
+
raise WrongDecoratorSyntaxError("The @transfunction decorator can only be used with the '@' symbol. Don't use it as a regular function. Also, don't rename it.")
|
|
114
|
+
|
|
115
|
+
for decorator in node.decorator_list:
|
|
116
|
+
if decorator.id != 'transfunction':
|
|
117
|
+
raise WrongDecoratorSyntaxError('The @transfunction decorator cannot be used in conjunction with other decorators.')
|
|
118
|
+
else:
|
|
119
|
+
if transfunction_decorator is not None:
|
|
120
|
+
raise DualUseOfDecoratorError("You cannot use the 'transfunction' decorator twice for the same function.")
|
|
121
|
+
transfunction_decorator = decorator
|
|
122
|
+
|
|
123
|
+
node.decorator_list = []
|
|
124
|
+
return node
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
RewriteContexts().visit(tree)
|
|
128
|
+
DeleteDecorator().visit(tree)
|
|
129
|
+
|
|
130
|
+
if addictional_transformers is not None:
|
|
131
|
+
for addictional_transformer in addictional_transformers:
|
|
132
|
+
addictional_transformer.visit(tree)
|
|
133
|
+
|
|
134
|
+
tree = self.wrap_ast_by_closures(tree)
|
|
135
|
+
|
|
136
|
+
if version_info.minor > 10:
|
|
137
|
+
increment_lineno(tree, n=(self.decorator_lineno - transfunction_decorator.lineno))
|
|
138
|
+
else:
|
|
139
|
+
increment_lineno(tree, n=(self.decorator_lineno - transfunction_decorator.lineno - 1))
|
|
140
|
+
|
|
141
|
+
code = compile(tree, filename=getfile(self.function), mode='exec')
|
|
142
|
+
namespace = {}
|
|
143
|
+
exec(code, namespace)
|
|
144
|
+
function_factory = namespace['wrapper']
|
|
145
|
+
result = function_factory()
|
|
146
|
+
result = self.rewrite_globals_and_closure(result)
|
|
147
|
+
result = wraps(self.function)(result)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
def wrap_ast_by_closures(self, tree):
|
|
151
|
+
old_functiondef = tree.body[0]
|
|
152
|
+
|
|
153
|
+
tree.body[0] = FunctionDef(
|
|
154
|
+
name='wrapper',
|
|
155
|
+
body=[Assign(targets=[Name(id=name, ctx=Store(), col_offset=0)], value=Constant(value=None, col_offset=0), col_offset=0) for name in self.function.__code__.co_freevars] + [
|
|
156
|
+
old_functiondef,
|
|
157
|
+
Return(value=Name(id=self.function.__name__, ctx=Load(), col_offset=0), col_offset=0),
|
|
158
|
+
],
|
|
159
|
+
col_offset=0,
|
|
160
|
+
args=arguments(
|
|
161
|
+
posonlyargs=[],
|
|
162
|
+
args=[],
|
|
163
|
+
kwonlyargs=[],
|
|
164
|
+
kw_defaults=[],
|
|
165
|
+
defaults=[],
|
|
166
|
+
),
|
|
167
|
+
decorator_list=[],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return tree
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def rewrite_globals_and_closure(self, function):
|
|
174
|
+
# https://stackoverflow.com/a/13503277/14522393
|
|
175
|
+
all_new_closure_names = set(self.function.__code__.co_freevars)
|
|
176
|
+
|
|
177
|
+
if self.function.__closure__ is not None:
|
|
178
|
+
old_function_closure_variables = {name: cell for name, cell in zip(self.function.__code__.co_freevars, self.function.__closure__)}
|
|
179
|
+
filtered_closure = tuple([cell for name, cell in old_function_closure_variables.items() if name in all_new_closure_names])
|
|
180
|
+
names = tuple([name for name, cell in old_function_closure_variables.items() if name in all_new_closure_names])
|
|
181
|
+
new_code = function.__code__.replace(co_freevars=names)
|
|
182
|
+
else:
|
|
183
|
+
filtered_closure = None
|
|
184
|
+
new_code = function.__code__
|
|
185
|
+
|
|
186
|
+
new_function = FunctionType(
|
|
187
|
+
new_code,
|
|
188
|
+
self.function.__globals__,
|
|
189
|
+
name=self.function.__name__,
|
|
190
|
+
argdefs=self.function.__defaults__,
|
|
191
|
+
closure=filtered_closure,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
new_function = update_wrapper(new_function, function)
|
|
195
|
+
new_function.__kwdefaults__ = function.__kwdefaults__
|
|
196
|
+
return new_function
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: transfunctions
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Say NO to Python fragmentation on sync and async
|
|
5
|
+
Author-email: Evgeniy Blinov <zheni-b@yandex.ru>
|
|
6
|
+
Project-URL: Source, https://github.com/pomponchik/transfunctions
|
|
7
|
+
Project-URL: Tracker, https://github.com/pomponchik/transfunctions/issues
|
|
8
|
+
Keywords: async,sync to async,async to sync
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
|
|
28
|
+
# transfunctions
|
|
29
|
+
|
|
30
|
+
[](https://pepy.tech/project/transfunctions)
|
|
31
|
+
[](https://pepy.tech/project/transfunctions)
|
|
32
|
+
[](https://coveralls.io/github/pomponchik/transfunctions?branch=develop)
|
|
33
|
+
[](https://github.com/boyter/scc/)
|
|
34
|
+
[](https://hitsofcode.com/github/pomponchik/transfunctions/view?branch=main)
|
|
35
|
+
[](https://github.com/pomponchik/transfunctions/actions/workflows/tests_and_coverage.yml)
|
|
36
|
+
[](https://pypi.python.org/pypi/transfunctions)
|
|
37
|
+
[](https://badge.fury.io/py/transfunctions)
|
|
38
|
+
[](https://github.com/astral-sh/ruff)
|
|
39
|
+
|
|
40
|
+
This library is designed to solve one of the most important problems in python programming - dividing all written code into 2 camps: sync and async. We get rid of code duplication by using templates.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Table of contents
|
|
44
|
+
|
|
45
|
+
- [**Quick start**](#quick-start)
|
|
46
|
+
- [**The problem**](#the-problem)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
Install it:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install transfunctions
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
And use:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from asyncio import run
|
|
61
|
+
from transfunctions import (
|
|
62
|
+
transfunction,
|
|
63
|
+
sync_context,
|
|
64
|
+
async_context,
|
|
65
|
+
generator_context,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@transfunction
|
|
69
|
+
def template():
|
|
70
|
+
print('so, ', end='')
|
|
71
|
+
with sync_context:
|
|
72
|
+
print("it's just usual function!")
|
|
73
|
+
with async_context:
|
|
74
|
+
print("it's an async function!")
|
|
75
|
+
with generator_context:
|
|
76
|
+
print("it's a generator function!")
|
|
77
|
+
yield
|
|
78
|
+
|
|
79
|
+
function = template.get_usual_function()
|
|
80
|
+
function()
|
|
81
|
+
#> so, it's just usual function!
|
|
82
|
+
|
|
83
|
+
async_function = template.get_async_function()
|
|
84
|
+
run(async_function())
|
|
85
|
+
#> so, it's an async function!
|
|
86
|
+
|
|
87
|
+
generator_function = template.get_generator_function()
|
|
88
|
+
list(generator_function())
|
|
89
|
+
#> so, it's a generator function!
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
As you can see, in this case, 3 different functions were created based on the template, including both common parts and unique ones for a specific type of function.
|
|
93
|
+
|
|
94
|
+
You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
## The problem
|
|
98
|
+
|
|
99
|
+
Since the `asyncio` module appeared in Python more than 10 years ago, many well-known libraries have received their asynchronous alternates. A lot of the code in the Python ecosystem has been [duplicated](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), and you probably know many such examples.
|
|
100
|
+
|
|
101
|
+
The reason for this problem is that the Python community has chosen a way to implement asynchrony expressed through syntax. There are new keywords in the language, such as `async` and `await`. Their use makes the code so-called "[multicolored](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/)": all the functions in it can be red or blue, and depending on the color, the rules for calling them are different. You can only call blue functions from red ones, but not vice versa.
|
|
102
|
+
|
|
103
|
+
I must say that implementing asynchronous calls using a special syntax is not the only solution. There are languages like Go where runtime can independently determine "under the hood" where a function should be asynchronous and where not, and choose the correct way to call it. A programmer does not need to manually "colorize" their functions there. Personally, I think that choosing a different path is the mistake of the Python community, but that's not what we're discussing here.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
transfunctions/__init__.py
|
|
5
|
+
transfunctions/decorator.py
|
|
6
|
+
transfunctions/errors.py
|
|
7
|
+
transfunctions/markers.py
|
|
8
|
+
transfunctions/py.typed
|
|
9
|
+
transfunctions/transformer.py
|
|
10
|
+
transfunctions.egg-info/PKG-INFO
|
|
11
|
+
transfunctions.egg-info/SOURCES.txt
|
|
12
|
+
transfunctions.egg-info/dependency_links.txt
|
|
13
|
+
transfunctions.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
transfunctions
|