├── .gitignore ├── MANIFEST.in ├── Makefile ├── assign ├── __init__.py ├── magic.py ├── patch.py └── transformer.py ├── readme.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.pyc 3 | *.egg-info 4 | build/ 5 | dist/ 6 | 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include readme.md 2 | prune tests 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | upload: 2 | python setup.py sdist --formats=gztar register upload 3 | -------------------------------------------------------------------------------- /assign/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.1' 2 | -------------------------------------------------------------------------------- /assign/magic.py: -------------------------------------------------------------------------------- 1 | from assign.transformer import AssignTransformer 2 | from assign.patch import patch_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 | try: 14 | patch_module(module, trans=AssignTransformer) 15 | except: 16 | return module 17 | return module 18 | 19 | 20 | __builtins__.update(**dict( 21 | __import__=custom_import 22 | )) 23 | -------------------------------------------------------------------------------- /assign/patch.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from assign.transformer import AssignTransformer 3 | 4 | __all__ = [ 5 | 'patch_node_ast', 6 | 'patch_code_ast', 7 | 'patch_file_ast', 8 | 'patch_module_ast', 9 | 'patch_module' 10 | ] 11 | 12 | 13 | def patch_node_ast(node, trans=AssignTransformer): 14 | trans = trans() 15 | new_node = trans.visit(node) 16 | ast.fix_missing_locations(new_node) 17 | return new_node 18 | 19 | 20 | def patch_code_ast(code_str, trans=AssignTransformer): 21 | code_ast = ast.parse(code_str) 22 | return patch_node_ast(code_ast, trans) 23 | 24 | 25 | def patch_file_ast(filename, trans=AssignTransformer): 26 | with open(filename, "r") as f: 27 | code_str = ''.join(f.readlines()) 28 | return patch_code_ast(code_str, trans) 29 | 30 | 31 | def patch_module_ast(module, trans=AssignTransformer): 32 | if not hasattr(module, '__file__'): 33 | return module 34 | filename = module.__file__.replace('.pyc', '.py') 35 | return patch_file_ast(filename, trans) 36 | 37 | 38 | def patch_module(module, trans=AssignTransformer): 39 | patched_ast = patch_module_ast(module) 40 | patched_code = compile(patched_ast, module.__name__, "exec") 41 | exec(patched_code, module.__dict__) 42 | -------------------------------------------------------------------------------- /assign/transformer.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | __all__ = [ 4 | 'gen_assign_checker_ast', 5 | 'AssignTransformer' 6 | 7 | ] 8 | 9 | 10 | def gen_assign_checker_ast(node): 11 | targets = [t.id for t in node.targets] 12 | obj_name = node.value.id 13 | 14 | return ast.If( 15 | test=ast.Call( 16 | func=ast.Name(id='hasattr', ctx=ast.Load()), 17 | args=[ 18 | ast.Name(id=obj_name, ctx=ast.Load()), 19 | ast.Str(s='__assign__'), 20 | ], 21 | keywords=[], 22 | starargs=None, 23 | kwargs=None 24 | ), 25 | body=[ 26 | ast.Assign( 27 | targets=[ast.Name(id=target, ctx=ast.Store())], 28 | value=ast.Call( 29 | func=ast.Attribute( 30 | value=ast.Name(id=obj_name, ctx=ast.Load()), 31 | attr='__assign__', 32 | ctx=ast.Load() 33 | ), 34 | args=[ast.Str(s=target)], 35 | keywords=[], 36 | starargs=None, 37 | kwargs=None 38 | ) 39 | ) 40 | for target in targets], 41 | orelse=[] 42 | ) 43 | 44 | 45 | class AssignTransformer(ast.NodeTransformer): 46 | def generic_visit(self, node): 47 | ast.NodeTransformer.generic_visit(self, node) 48 | return node 49 | 50 | def visit_Assign(self, node): 51 | if not isinstance(node.value, ast.Name): 52 | return node 53 | new_node = gen_assign_checker_ast(node) 54 | ast.copy_location(new_node, node) 55 | ast.fix_missing_locations(new_node) 56 | return new_node 57 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Assign 2 | 3 | --------- 4 | 5 | `assign` is a black magic module for supporting `obj.__assign__`: 6 | 7 | ### How to use it: 8 | 9 | #### 1. magic patch 10 | 11 | Suppose that there is a `test.py` 12 | 13 | ```python 14 | 15 | a = 1 16 | 17 | 18 | class T(): 19 | def __assign__(self, v): 20 | print('called with %s' % v) 21 | 22 | 23 | b = T() 24 | c = b 25 | 26 | ``` 27 | It just works as: 28 | 29 | ```python 30 | Python 3.6.0 (default, Mar 6 2017, 15:44:48) 31 | [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)] on darwin 32 | Type "help", "copyright", "credits" or "license" for more information. 33 | >>> import magic 34 | >>> import test 35 | called with c 36 | 37 | ``` 38 | 39 | #### 2. manually patch 40 | 41 | ``` 42 | from assign.patch import patch_module 43 | import test 44 | 45 | patch_module(test) 46 | 47 | ``` 48 | 49 | ### Install 50 | 51 | just: 52 | 53 | `pip install assign` 54 | 55 | ### Notes 56 | 57 | * Tested with `Py2.7` and `Py3.6` 58 | 59 | ### Known Issues 60 | 61 | * 1. Won't work under `REPL` 62 | * 2. May slow import operation. 63 | * 3. May failed when patch some modules like `os` and `sys` -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding:utf8 2 | 3 | import os 4 | import codecs 5 | import re 6 | from setuptools import setup, find_packages 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | _CWD = os.path.dirname(__file__) 10 | 11 | NAME = 'assign' 12 | DESCRIPTION = 'A Black Magic for support obj.__assign__ method' 13 | AUTHOR = 'ryankung' 14 | EMAIL = 'ryankung@ieee.org' 15 | 16 | 17 | def read(*parts): 18 | # intentionally *not* adding an encoding option to open, See: 19 | # https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 20 | return codecs.open(os.path.join(here, *parts), 'r').read() 21 | 22 | 23 | def find_version(*file_paths): 24 | version_file = read(*file_paths) 25 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 26 | version_file, re.M) 27 | if version_match: 28 | return version_match.group(1) 29 | raise RuntimeError("Unable to find version string.") 30 | 31 | 32 | setup( 33 | name=NAME, 34 | description=DESCRIPTION, 35 | long_description=open(os.path.join(here, 'readme.md')).read(), 36 | version=find_version('assign', '__init__.py'), 37 | packages=find_packages(exclude=['examples', 'tests', 'docs']), 38 | install_requires=[], 39 | package_dir={'': '.'}, 40 | author=AUTHOR, 41 | author_email=EMAIL, 42 | license="MIT", 43 | platforms=['any'], 44 | url="", 45 | classifiers=["Intended Audience :: Developers", 46 | "Programming Language :: Python", 47 | "Topic :: Software Development :: Libraries :: Python Modules"], 48 | include_package_data=True 49 | ) 50 | --------------------------------------------------------------------------------