assign-overload 1.0.0__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.
- assign_overload-1.0.0/PKG-INFO +82 -0
- assign_overload-1.0.0/README.md +73 -0
- assign_overload-1.0.0/pyproject.toml +15 -0
- assign_overload-1.0.0/src/assign_overload/__init__.py +1 -0
- assign_overload-1.0.0/src/assign_overload/magic.py +24 -0
- assign_overload-1.0.0/src/assign_overload/patch.py +52 -0
- assign_overload-1.0.0/src/assign_overload/transformer.py +147 -0
- assign_overload-1.0.0/tests.py +42 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assign-overload
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: overloading assignment operator everywhere
|
|
5
|
+
Project-URL: Homepage, https://github.com/pyhacks/assign-overload
|
|
6
|
+
Author-email: pyhacks <enginyildirim111@gmail.com>
|
|
7
|
+
Keywords: assign,assignment,magic,method,operator,overloading
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# assign-overload
|
|
11
|
+
This library makes it possible to overload assignment (=) operator. Inspired by [assign](https://github.com/RyanKung/assign).
|
|
12
|
+
Along with other things, main difference from that library is [assign](https://github.com/RyanKung/assign) calls the overloaded operator of the right hand side while this library calls of the left hand side.
|
|
13
|
+
|
|
14
|
+
# Installation
|
|
15
|
+
```pip install assign-overload```
|
|
16
|
+
|
|
17
|
+
# How To Use
|
|
18
|
+
First you need to overload the assignment operator using the function ```_assign_```:
|
|
19
|
+
```python
|
|
20
|
+
class T:
|
|
21
|
+
def _assign_(self, value, *annotation):
|
|
22
|
+
print(f"called with {value}")
|
|
23
|
+
return self
|
|
24
|
+
```
|
|
25
|
+
_value_ is the value of right hand side. If the assignment is annotated, _annotation_ will take a single argument carrying the annotation. Return value specifies the value of left hand side.
|
|
26
|
+
|
|
27
|
+
Next, in order to make this method automatically called you should call assign_overload.**patch_and_reload_module**():
|
|
28
|
+
```python
|
|
29
|
+
import assign_overload
|
|
30
|
+
|
|
31
|
+
class T:
|
|
32
|
+
def _assign_(self, value, *annotation):
|
|
33
|
+
print(f"called with {value}")
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
a = T()
|
|
39
|
+
a = 10 # a will keep holding the T object
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if assign_overload.patch_and_reload_module():
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
45
|
+
```
|
|
46
|
+
This function will find and modify the current module's source code, replacing right hand side of assignments with calls to ```_assign_```, then execute the modified code.
|
|
47
|
+
Once called, this function will introduce a new global variable to the current module: ```modified_source```.
|
|
48
|
+
This variable will be accessible while both executing the original module and modified module.
|
|
49
|
+
It represents the source code of the modified module.
|
|
50
|
+
Since module's ```__dict__``` attribute is passed as the globals parameter of ```exec```, names created in the modified module will be reflected on the original module along with their values, creating a reloading effect.
|
|
51
|
+
Return value of this function specifies which module we are currently executing. True means modified module and False means original module.
|
|
52
|
+
Any code outside the first if block will be executed twice.
|
|
53
|
+
Codes before this function call will first be executed while executing the original module and second while executing the modified module.
|
|
54
|
+
Codes after the first if block will first be executed while executing the modified module and second while executing the original module.
|
|
55
|
+
This is especially bad because if a piece of code last executed while executing the original module, any function or class definition will have its original definition, not the modified definition they should have.
|
|
56
|
+
Codes inside the first if block will only be executed while executing the modified module.
|
|
57
|
+
You can wrap all your module code including functions and classes inside a single if block like this:
|
|
58
|
+
```python
|
|
59
|
+
import assign_overload
|
|
60
|
+
|
|
61
|
+
if assign_overload.patch_and_reload_module():
|
|
62
|
+
class T:
|
|
63
|
+
def _assign_(self, value, *annotation):
|
|
64
|
+
print(f"called with {value}")
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
a = T()
|
|
70
|
+
a = 10 # a will keep holding the T object
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
75
|
+
```
|
|
76
|
+
But that doesn't look nice.
|
|
77
|
+
Functions, classes and constant definitions are most probably okay to be executed twice but actual code should be executed once.
|
|
78
|
+
Thus, the location in the first example is the best location for calling this function.
|
|
79
|
+
This function should be called even if this module isn't the ```__main__``` module because function and class definitions should be modified even if no real code is ought to be executed.
|
|
80
|
+
This function should be called once. Consecutive calls doesn't modify and execute the source code again.
|
|
81
|
+
Lastly, this function will introduce 2 other global varriables to the current module: ```patched``` and ```executing_patch```.
|
|
82
|
+
These variables are internally used by the library and they are documented here only to prevent the user from changing them.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# assign-overload
|
|
2
|
+
This library makes it possible to overload assignment (=) operator. Inspired by [assign](https://github.com/RyanKung/assign).
|
|
3
|
+
Along with other things, main difference from that library is [assign](https://github.com/RyanKung/assign) calls the overloaded operator of the right hand side while this library calls of the left hand side.
|
|
4
|
+
|
|
5
|
+
# Installation
|
|
6
|
+
```pip install assign-overload```
|
|
7
|
+
|
|
8
|
+
# How To Use
|
|
9
|
+
First you need to overload the assignment operator using the function ```_assign_```:
|
|
10
|
+
```python
|
|
11
|
+
class T:
|
|
12
|
+
def _assign_(self, value, *annotation):
|
|
13
|
+
print(f"called with {value}")
|
|
14
|
+
return self
|
|
15
|
+
```
|
|
16
|
+
_value_ is the value of right hand side. If the assignment is annotated, _annotation_ will take a single argument carrying the annotation. Return value specifies the value of left hand side.
|
|
17
|
+
|
|
18
|
+
Next, in order to make this method automatically called you should call assign_overload.**patch_and_reload_module**():
|
|
19
|
+
```python
|
|
20
|
+
import assign_overload
|
|
21
|
+
|
|
22
|
+
class T:
|
|
23
|
+
def _assign_(self, value, *annotation):
|
|
24
|
+
print(f"called with {value}")
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
a = T()
|
|
30
|
+
a = 10 # a will keep holding the T object
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if assign_overload.patch_and_reload_module():
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
36
|
+
```
|
|
37
|
+
This function will find and modify the current module's source code, replacing right hand side of assignments with calls to ```_assign_```, then execute the modified code.
|
|
38
|
+
Once called, this function will introduce a new global variable to the current module: ```modified_source```.
|
|
39
|
+
This variable will be accessible while both executing the original module and modified module.
|
|
40
|
+
It represents the source code of the modified module.
|
|
41
|
+
Since module's ```__dict__``` attribute is passed as the globals parameter of ```exec```, names created in the modified module will be reflected on the original module along with their values, creating a reloading effect.
|
|
42
|
+
Return value of this function specifies which module we are currently executing. True means modified module and False means original module.
|
|
43
|
+
Any code outside the first if block will be executed twice.
|
|
44
|
+
Codes before this function call will first be executed while executing the original module and second while executing the modified module.
|
|
45
|
+
Codes after the first if block will first be executed while executing the modified module and second while executing the original module.
|
|
46
|
+
This is especially bad because if a piece of code last executed while executing the original module, any function or class definition will have its original definition, not the modified definition they should have.
|
|
47
|
+
Codes inside the first if block will only be executed while executing the modified module.
|
|
48
|
+
You can wrap all your module code including functions and classes inside a single if block like this:
|
|
49
|
+
```python
|
|
50
|
+
import assign_overload
|
|
51
|
+
|
|
52
|
+
if assign_overload.patch_and_reload_module():
|
|
53
|
+
class T:
|
|
54
|
+
def _assign_(self, value, *annotation):
|
|
55
|
+
print(f"called with {value}")
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
a = T()
|
|
61
|
+
a = 10 # a will keep holding the T object
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
66
|
+
```
|
|
67
|
+
But that doesn't look nice.
|
|
68
|
+
Functions, classes and constant definitions are most probably okay to be executed twice but actual code should be executed once.
|
|
69
|
+
Thus, the location in the first example is the best location for calling this function.
|
|
70
|
+
This function should be called even if this module isn't the ```__main__``` module because function and class definitions should be modified even if no real code is ought to be executed.
|
|
71
|
+
This function should be called once. Consecutive calls doesn't modify and execute the source code again.
|
|
72
|
+
Lastly, this function will introduce 2 other global varriables to the current module: ```patched``` and ```executing_patch```.
|
|
73
|
+
These variables are internally used by the library and they are documented here only to prevent the user from changing them.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "assign-overload"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
dependencies = []
|
|
9
|
+
description = "overloading assignment operator everywhere"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
authors = [{name = "pyhacks", email = "enginyildirim111@gmail.com"}]
|
|
12
|
+
keywords = ["assign", "assignment", "operator", "overloading", "magic", "method"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/pyhacks/assign-overload"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .patch import patch_and_reload_module
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .transformer import AssignTransformer
|
|
2
|
+
from .patch import patch_and_reload_module
|
|
3
|
+
|
|
4
|
+
__all__ = ['custom_import']
|
|
5
|
+
|
|
6
|
+
origin_import = __import__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def custom_import(name, *args, **kwargs):
|
|
10
|
+
module = origin_import(name, *args, **kwargs)
|
|
11
|
+
if not hasattr(module, '__file__'):
|
|
12
|
+
return module
|
|
13
|
+
if module.__name__ == "warnings":
|
|
14
|
+
return module
|
|
15
|
+
try:
|
|
16
|
+
patch_and_reload_module(module, trans=AssignTransformer)
|
|
17
|
+
except:
|
|
18
|
+
return module
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__builtins__.update(**dict(
|
|
23
|
+
__import__=custom_import
|
|
24
|
+
))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import sys
|
|
3
|
+
from .transformer import AssignTransformer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'patch_node_ast',
|
|
8
|
+
'patch_code_ast',
|
|
9
|
+
'patch_file_ast',
|
|
10
|
+
'patch_module_ast',
|
|
11
|
+
'patch_and_reload_module'
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def patch_node_ast(node, trans=AssignTransformer):
|
|
16
|
+
trans = trans()
|
|
17
|
+
new_node = trans.visit(node)
|
|
18
|
+
ast.fix_missing_locations(new_node)
|
|
19
|
+
return new_node
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def patch_code_ast(code_str, trans=AssignTransformer):
|
|
23
|
+
code_ast = ast.parse(code_str)
|
|
24
|
+
return patch_node_ast(code_ast, trans)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def patch_file_ast(filename, trans=AssignTransformer):
|
|
28
|
+
with open(filename, "r") as f:
|
|
29
|
+
code_str = ''.join(f.readlines())
|
|
30
|
+
return patch_code_ast(code_str, trans)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def patch_module_ast(module, trans=AssignTransformer):
|
|
34
|
+
if not hasattr(module, '__file__'):
|
|
35
|
+
return module
|
|
36
|
+
filename = module.__file__.replace('.pyc', '.py')
|
|
37
|
+
return patch_file_ast(filename, trans)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def patch_and_reload_module(module = None, trans=AssignTransformer):
|
|
41
|
+
if module is None:
|
|
42
|
+
module_name = sys._getframe(1).f_globals["__name__"]
|
|
43
|
+
module = sys.modules[module_name]
|
|
44
|
+
if not hasattr(module, "patched") or not module.patched:
|
|
45
|
+
module.patched = True
|
|
46
|
+
patched_ast = patch_module_ast(module)
|
|
47
|
+
module.modified_source = ast.unparse(patched_ast)
|
|
48
|
+
patched_code = compile(patched_ast, module.__name__, "exec")
|
|
49
|
+
module.executing_patch = True
|
|
50
|
+
exec(patched_code, module.__dict__)
|
|
51
|
+
module.executing_patch = False
|
|
52
|
+
return module.executing_patch
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import copy
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'AssignTransformer'
|
|
6
|
+
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AssignTransformer(ast.NodeTransformer):
|
|
11
|
+
def generic_visit(self, node):
|
|
12
|
+
ast.NodeTransformer.generic_visit(self, node)
|
|
13
|
+
return node
|
|
14
|
+
|
|
15
|
+
def gen_assign_checker_ast(self, node):
|
|
16
|
+
if isinstance(node.targets[0], ast.Tuple):
|
|
17
|
+
raise RuntimeError("unpacking during assignment is not supported")
|
|
18
|
+
load_targets = []
|
|
19
|
+
for target in node.targets:
|
|
20
|
+
new_target = copy.deepcopy(target)
|
|
21
|
+
new_target.ctx = ast.Load()
|
|
22
|
+
load_targets.append(new_target)
|
|
23
|
+
conditions = []
|
|
24
|
+
for target in node.targets:
|
|
25
|
+
load_target = copy.deepcopy(target)
|
|
26
|
+
load_target.ctx = ast.Load()
|
|
27
|
+
if type(target) != ast.Attribute:
|
|
28
|
+
conditions.append(ast.Call(func = ast.Name(id='hasattr', ctx=ast.Load()),
|
|
29
|
+
args = [load_target, ast.Constant('_assign_')]))
|
|
30
|
+
else:
|
|
31
|
+
conditions.append(ast.BoolOp(op = ast.And(),
|
|
32
|
+
values = [ast.Call(func = ast.Name(id='hasattr', ctx=ast.Load()),
|
|
33
|
+
args = [load_target, ast.Constant('_assign_')]),
|
|
34
|
+
ast.Call(func = ast.Name(id='hasattr', ctx=ast.Load()),
|
|
35
|
+
args = [load_target.value, ast.Constant('__dict__')]),
|
|
36
|
+
ast.Compare(left = ast.Constant(load_target.attr),
|
|
37
|
+
ops = [ast.In()],
|
|
38
|
+
comparators = [ast.Attribute(value = load_target.value,
|
|
39
|
+
attr = "__dict__",
|
|
40
|
+
ctx = ast.Load())
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
new_node = ast.If(test = ast.Constant(True),
|
|
48
|
+
body = [ast.Try(body = [ast.Expr(load_target)],
|
|
49
|
+
handlers = [ast.ExceptHandler(None,
|
|
50
|
+
None,
|
|
51
|
+
body = [ast.Assign(targets = [target],
|
|
52
|
+
value = node.value)
|
|
53
|
+
])
|
|
54
|
+
],
|
|
55
|
+
orelse = [ast.If(test = condition,
|
|
56
|
+
body = [ast.Assign(targets = [target],
|
|
57
|
+
value = ast.Call(func = ast.Attribute(value = load_target,
|
|
58
|
+
attr = '_assign_',
|
|
59
|
+
ctx = ast.Load()),
|
|
60
|
+
args = [node.value],
|
|
61
|
+
keywords = [],
|
|
62
|
+
starargs = None,
|
|
63
|
+
kwargs = None)
|
|
64
|
+
)
|
|
65
|
+
],
|
|
66
|
+
orelse = [ast.Assign(targets = [target],
|
|
67
|
+
value = node.value)
|
|
68
|
+
])
|
|
69
|
+
],
|
|
70
|
+
finalbody = []) for target, load_target, condition in zip(node.targets, load_targets, conditions)
|
|
71
|
+
],
|
|
72
|
+
orelse = [])
|
|
73
|
+
return new_node
|
|
74
|
+
|
|
75
|
+
def gen_annassign_checker_ast(self, node):
|
|
76
|
+
if node.value:
|
|
77
|
+
target = node.target
|
|
78
|
+
load_target = copy.deepcopy(node.target)
|
|
79
|
+
load_target.ctx = ast.Load()
|
|
80
|
+
if type(target) != ast.Attribute:
|
|
81
|
+
condition = ast.Call(func = ast.Name(id='hasattr', ctx=ast.Load()),
|
|
82
|
+
args = [load_target, ast.Constant('_assign_')])
|
|
83
|
+
else:
|
|
84
|
+
condition = ast.BoolOp(op = ast.And(),
|
|
85
|
+
values = [ast.Call(func = ast.Name(id='hasattr', ctx=ast.Load()),
|
|
86
|
+
args = [load_target, ast.Constant('_assign_')]),
|
|
87
|
+
ast.Call(func = ast.Name(id='hasattr', ctx=ast.Load()),
|
|
88
|
+
args = [load_target.value, ast.Constant('__dict__')]),
|
|
89
|
+
ast.Compare(left = ast.Constant(load_target.attr),
|
|
90
|
+
ops = [ast.In()],
|
|
91
|
+
comparators = [ast.Attribute(value = load_target.value,
|
|
92
|
+
attr = "__dict__",
|
|
93
|
+
ctx = ast.Load())
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
new_node = ast.If(test = ast.Constant(True),
|
|
101
|
+
body = [ast.Try(body = [ast.Expr(load_target)],
|
|
102
|
+
handlers = [ast.ExceptHandler(None,
|
|
103
|
+
None,
|
|
104
|
+
body = [ast.AnnAssign(target = target,
|
|
105
|
+
annotation = node.annotation,
|
|
106
|
+
value = node.value,
|
|
107
|
+
simple = node.simple)
|
|
108
|
+
])
|
|
109
|
+
],
|
|
110
|
+
orelse = [ast.If(test = condition,
|
|
111
|
+
body = [ast.AnnAssign(target = target,
|
|
112
|
+
annotation = node.annotation,
|
|
113
|
+
value = ast.Call(func = ast.Attribute(value = load_target,
|
|
114
|
+
attr = '_assign_',
|
|
115
|
+
ctx = ast.Load()),
|
|
116
|
+
args = [node.value, node.annotation],
|
|
117
|
+
keywords = [],
|
|
118
|
+
starargs = None,
|
|
119
|
+
kwargs = None),
|
|
120
|
+
simple = node.simple
|
|
121
|
+
)
|
|
122
|
+
],
|
|
123
|
+
orelse = [ast.AnnAssign(target = target,
|
|
124
|
+
annotation = node.annotation,
|
|
125
|
+
value = node.value,
|
|
126
|
+
simple = node.simple)
|
|
127
|
+
])
|
|
128
|
+
],
|
|
129
|
+
finalbody = [])
|
|
130
|
+
],
|
|
131
|
+
orelse = [])
|
|
132
|
+
else:
|
|
133
|
+
new_node = node
|
|
134
|
+
return new_node
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def visit_Assign(self, node):
|
|
138
|
+
new_node = self.gen_assign_checker_ast(node)
|
|
139
|
+
ast.copy_location(new_node, node)
|
|
140
|
+
ast.fix_missing_locations(new_node)
|
|
141
|
+
return new_node
|
|
142
|
+
|
|
143
|
+
def visit_AnnAssign(self, node):
|
|
144
|
+
new_node = self.gen_annassign_checker_ast(node)
|
|
145
|
+
ast.copy_location(new_node, node)
|
|
146
|
+
ast.fix_missing_locations(new_node)
|
|
147
|
+
return new_node
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assign_overload
|
|
2
|
+
import wrapt
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class T(wrapt.ObjectProxy):
|
|
6
|
+
def _assign_(self, value, *annotation):
|
|
7
|
+
print(f"called with {value}")
|
|
8
|
+
self.__wrapped__ = value
|
|
9
|
+
return self
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class A:
|
|
13
|
+
a = T(10)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test1():
|
|
17
|
+
global b
|
|
18
|
+
#a, b = T(), T()
|
|
19
|
+
print("here")
|
|
20
|
+
b = c = d = T(10)
|
|
21
|
+
b = 20
|
|
22
|
+
print(b)
|
|
23
|
+
print(type(b))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test2():
|
|
27
|
+
a = A()
|
|
28
|
+
a.a = T(5)
|
|
29
|
+
a.a = 30
|
|
30
|
+
print(a.a)
|
|
31
|
+
del a.a
|
|
32
|
+
print(a.a)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
test1()
|
|
37
|
+
print()
|
|
38
|
+
test2()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if assign_overload.patch_and_reload_module():
|
|
42
|
+
main()
|