def get_project_modules(self, exclude_dirs=None) -> list: """ 获得项目所有可导入的模块路径. 第三方模块分为项目模块和外部模块. 本程序只负责分析项目模块的依赖关系, 因此通过本方法过滤 掉外部模块的路径. 例如: import sys # builtin module import src.downloader # project module 那么本方法只收录 ['src.downloader'], 不收录 ['sys']. IN: prjdir: str. an absolute project directory. e.g. 'D:/myprj/' OT: prj_modules: list. e.g. ['testflight.test_app_launcher', 'testflight .downloader', ...] """ all_files = file_sniffer.findall_files(self.prjdir) all_pyfiles = [x for x in all_files if x.endswith('.py')] # -> ['D:/myprj/src/app.py', 'D:/myprj/src/downloader.py', ...] if exclude_dirs: # DEL (2019-07-31): 效益较低. 未来将会移除. lk.loga(exclude_dirs) for adir in exclude_dirs: all_files = file_sniffer.findall_files( file_sniffer.prettify_dir(abspath(adir))) # lk.loga(all_files) pyfiles = (x for x in all_files if x.endswith('.py')) for f in pyfiles: all_pyfiles.remove(f) prj_modules = [self.get_module_by_filepath(x) for x in all_pyfiles] # -> ['src.app', 'src.downloader', ...] lk.loga(len(all_pyfiles), prj_modules) return prj_modules
def main(self): call_stream = [self.pyfile] for pyfile in call_stream: lk.logdx(pyfile, style='◆') module_calls, prj_modules = self.pyfile_analyser.main(pyfile) """ module_calls: {module1: [call1, call2, ...], ...} prj_modules: [prj_module1, prj_module2, ...] """ # ------------------------------------------------ for module, calls in module_calls.items(): lk.loga(module, len(calls), calls) self.writer.record(module, calls) # ------------------------------------------------ new_pyfiles = self.get_new_pyfiles(prj_modules) for i in new_pyfiles: if i not in call_stream: call_stream.append(i) # calc elapsed time lk.total_count = lk.counter # TEST self.writer.show( self.module_helper.get_module_by_filepath( self.pyfile ) + '.module' )
def run_line(self, lino: int): ast_line = ast_tree.get(lino) lk.logd(ast_line, length=8) # -> [(obj_type, obj_val), ...] for i in ast_line: obj_type, obj_val = i lk.loga(obj_type, obj_val) # obj_type: str. e.g. "<class '_ast.Call'>" # obj_val: str/dict. e.g. '__name__', {'os': 'os'}, ... method = self.registered_methods.get(obj_type, self.do_nothing) method(obj_val)
def run_block(self, current_module: str): """ IN: module: str OT: self.calls: list """ lk.logd('run block', current_module, style='■') if current_module not in self.module_linos: # 说明此 module 是从外部导入的模块, 如 module = 'testflight.downloader'. assert self.module_analyser.is_prj_module(current_module) self.outer_calls.append(current_module) # return module_path # update hooks # self.module_hooks 需要在每次更新 self.run_block() 时同步更新. 这是因为, 不同 # 的 block 定义的区间范围不同, 而不同的区间范围包含的变量指配 (assigns) 也可能是不同 # 的. # 例如在 module = testflight.test_app_launcher.module 层级, self.module # _hooks = {'main': 'testflight.test_app_launcher.main'}. 到了 module = # testflight.test_app_launcher.Init 来运行 run_block 的时候, self.module # _hooks 就变成了 {'main': 'testflight.test_app_launcher.Init.main'}. 也就 # 是说在不同的 block 区间, 'main' 指配的 module 对象发生了变化, 因此必须更新 self # .module_hooks 才能适应最新变化. self.module_hooks = self.assign_analyser.indexing_assign_reachables( current_module, self.module_linos) lk.logt('[I4252]', 'update module hooks', self.module_hooks) # reset hooks self.var_hooks.clear() self.calls.clear() linos = self.module_linos[current_module] # the linos is in ordered. lk.loga(current_module, linos) for lino in linos: self.run_line(lino) # | for index, lino in enumerate(linos): # | # noinspection PyBroadException # | try: # | self.run_line(lino) # | except Exception: # | if index == 0: # | continue # | else: # | raise Exception return self.calls
def __init__(self, top_module, prj_modules): self.top_module = top_module self.prj_modules = prj_modules self.max_lino = max(ast_indents.keys()) self.top_linos = [ lino for lino, indent in ast_indents.items() if indent == 0 ] self.top_assigns = self.update_assigns(self.top_linos) # 注意: self.top_assigns 是包含非项目模块的. # -> {'os': 'os', 'downloader': 'testflight.downloader', 'Parser': 'test # flight.parser.Parser', 'main': 'testflight.app.main', 'Init': 'testfli # ght.app.Init'} self.top_assigns_prj_only = self.get_only_prj_modules(self.top_assigns) lk.loga(self.top_assigns) lk.loga(self.top_assigns_prj_only)
def __init__(self, module_helper, ast_tree, ast_indents): self.module_helper = module_helper self.ast_tree = ast_tree self.ast_indents = ast_indents self.max_lino = max(ast_indents.keys()) lk.loga(self.max_lino) self.top_linos = [ lino for lino, indent in ast_indents.items() if indent == 0 ] self.top_module = module_helper.get_top_module() self.top_assigns = self.find_global_vars() # type: dict # -> {'os': 'os', 'downloader': 'testflight.downloader', 'Parser': # 'testflight.parser.Parser', 'main': 'testflight.app.main', 'Init': # 'testflight.app.Init'} lk.loga(self.top_assigns)
def indexing_module_linos(self, master_module='', linos=None): """ 获取 pyfile 内每个 module 对应的行号范围. 根据 {lino:indent} 和 ast_tree 创建 {module:linos} 的字典. 注: 1. 每个 module (无论是父子关系还是兄弟关系) 之间的范围互不重叠. 2. AClass.__init__ 被认作 AClass IN: master_module: str. 当为空时, 将使用默认值 self.top_module (e.g. 'src.app') 不为空时, 请注意传入的是当前要观察的 module 的上一级 module. 例如我们要编译 src.app.main.child_method 所在的层级, 则 top_module 应传入 src .app.main. 用例参考: src.analyser.AssignAnalyser#update _assigns linos: None/list. 您可以自定义要读取的 module 范围, 本方法会仅针对这个区间进行 编译. 例如: 1 | def aaa(): 2 | def bbb(): # <- start 3 | def ccc(): 4 | pass 5 | # <- end 6 | def ddd(): 7 | pass 则本方法只编译 range(2, 5) 范围内的数据, 并返回 {'src.app.aaa.bbb': [ 2, 5], 'src.app.aaa.bbb.ccc': [3, 4]} 作为编译结果. 注意: 指定的范围的开始位置的缩进必须小于等于结束位置的缩进 (空行除外). 如果该参数为 None, 则默认使用所有行号 (`range(0, len(code_lines))`). self.ast_tree: dict. {lino: [(obj_type, obj_val), ...], ...} lino: int. 行号, 从 1 开始数. obj_type: str. 对象类型, 例如 "<class 'FunctionDef'>" 等. 完整的支持 列表参考 src.utils.ast_helper.AstHelper#eval_node(). obj_val: str/dict. 对象的值, 目前仅存在 str 或 dict 类型的数据. 示例: (str) 'print' (dict) {'src.downloader.Downloader': 'src.downloader.Downloader'} (多用于描述 Import) self.ast_indents: dict. {lino: indent, ...}. 由 AstHelper#create _lino_indent_dict() 提供. lino: int. 行号, 从 1 开始数. indent: int. 该行的列缩进位置, 为 4 的整数倍数, 如 0, 4, 8, 12 等. self.top_module: str. e.g. 'src.app' OT: module_linos: dict. {module: [lino, ...]} module: str. 模块的路径名. lino_list: list. 模块所涉及的行号列表, 已经过排序, 行号从 1 开始数, 最大 不超过当前 pyfile 的总代码行数. e.g. { 'src.app.module': [1, 2, 3, 9, 10], 'src.app.main': [4, 5, 8], 'src.app.main.child_method': [6, 7], ... } 有了 module_linos 以后, 我们就可以在已知 module 的调用关系的情况下, 专注 于读取该 module 对应的区间范围, 逐行分析每条语句, 并进一步发现新的调用关系, 以此产生裂变效应. 详见 src.analyser.VirtualRunner#main(). """ if master_module == '': master_module = self.top_module assert linos is None linos = self.prj_linos # the linos are already sorted. else: assert linos is not None lk.logd('indexing module linos', master_module) # ------------------------------------------------ # ast_defs: ast definitions ast_defs = ("<class '_ast.FunctionDef'>", "<class '_ast.ClassDef'>") ast_args = ("<class '_ast.arg'>",) indent_module_holder = {-4: self.top_module} # format: {indent: module} module_linos = {} # format: {module: linos} last_module = '' last_indent = 0 # last_indent 初始化值不影响方法的正常执行. 因为我们事先能保证 linos 参数的第一个 # lino 的 indent 一定是正常的, 而伴随着第一个 lino 的循环结尾, last_indent 就能 # 得到安全的更新, 因此 last_indent 的初始值无论是几都是安全的. for lino in linos: obj_type, obj_val = self.eval_ast_line(lino) # -> "<class '_ast.FunctionDef'>", 'main' indent = self.ast_indents.get(lino, -1) parent_indent = indent - 4 # lk.loga(lino, indent, last_module, obj_type, obj_val) if parent_indent in indent_module_holder: parent_module = indent_module_holder[parent_indent] if obj_type in ast_defs: # obj_type = "<class 'FunctionDef'>", obj_val = 'main' if obj_val == '__init__': # source_code = `def __init__(self):` current_module = parent_module else: current_module = parent_module + '.' + obj_val # -> 'src.app.main' elif obj_type in ast_args: current_module = last_module elif indent == 0 \ or last_module == self.runtime_module: current_module = self.runtime_module # | current_module = parent_module + '.module' else: # indent > 0 and last_parent_module not in ( # master_module, self # .runtime_module current_module = parent_module # update indent_module_holder indent_module_holder.update({indent: current_module}) # -> {0: 'src.app.main'}, {4: 'src.app.main.child_method'}, ... else: lk.loga(lino, indent, last_module) indent = last_indent current_module = last_module """ case 1: source_code = ``` 1 | def main(): # <- cursor 2 | pass ``` indent = 0, obj_val = 'main' -> indent - 4 = -4 -> parent_module = 'src.app' -> current_module = 'src.app.main' case 2: source_code = ``` 1 | def main(): 2 | def child_method(): # <- cursor 3 | pass ``` indent = 4, obj_val = 'child_method' -> indent - 4 = 0 -> parent_module = 'src.app.main' -> current_module = 'src.app.main.child_method' case 3: source_code = ``` 1 | def main(): 2 | def child_method(): 3 | print('aaa', 'bbb' 4 | 'ccc') # <- cursor ``` indent = 14, obj_val = "'ccc'" -> indent - 4 = 10 -> 10 not in indent_module_holder.keys(), so return the fallback value: 'src.app.main.child_method' -> current_module = 'src.app.main.child_method' """ # update module_linos node = module_linos.setdefault(current_module, []) node.append(lino) # NOTE: the lino is in ordered # update last vars last_module = current_module last_indent = indent # TEST show # lk.logt('[D3421]', self.top_module, indent_module_holder) show_modules_lightly = tuple( self.module_helper.get_module_seg(x, 'r0') for x in module_linos ) lk.logt('[I4204]', self.top_module, show_modules_lightly) """ -> module_linos = { 'testflight.test_app_launcher.module': [1, 3, 4, 38, 39], 'testflight.test_app_launcher.main' : [8, 11, 12, 21, 22, 24, 25, 27, 28], 'testflight.test_app_launcher.main.child_method' : [14, 15], 'testflight.test_app_launcher.main.child_method2': [17, 18, 19], 'testflight.test_app_launcher.Init' : [31], 'testflight.test_app_launcher.Init.main' : [33, 35] } """ # raise Exception # TEST return module_linos
def indexing_module_linos(self, top_module='', linos=None): """ 根据 {lino:indent} 和 ast_tree 创建 {module:linos} 的字典. ARGS: top_module linos: list. 您可以自定义要读取的 module 范围, 本方法会仅针对这个区间进行编译. 例如: 1 | def aaa(): 2 | def bbb(): # <- start 3 | def ccc(): 4 | pass 5 | # <- end 6 | def ddd(): 7 | pass 则本方法只编译 start=2 - end=5 范围内的数据, 并返回 {'src.app.aaa.bbb' : [2, 5], 'src.app.aaa.bbb.ccc': [3, 4]} 作为编译结果. 注意: 指定的范围的开始位置的缩进必须小于等于结束位置的缩进 (空行除外). IN: ast_tree: dict. {lino: [(obj_type, obj_val), ...], ...} lino: int. 行号, 从 1 开始数 obj_type: str. 对象类型, 例如 "<class 'FunctionDef'>" 等. 完整的支持 列表参考 src.utils.ast_helper.AstHelper#eval_node() obj_val: str/dict. 对象的值, 目前仅存在 str 或 dict 类型的数据. 示例: (str) 'print' (dict) (多用于描述 Import) {'src.downloader.Downloader': 'src.downloader.Downloader'} lino_indent: dict. {lino: indent, ...}. 由 AstHelper#create _lino_indent_dict() 提供 lino: int. 行号, 从 1 开始数 indent: int. 该行的列缩进位置, 为 4 的整数倍数, 如 0, 4, 8, 12 等 self.top_module: str. e.g. 'src.app' OT: module_linos: dict. {module: [lino, ...]} module: str. 模块的路径名. lino_list: list. 模块所涉及的行号列表, 已经过排序, 行号从 1 开始数, 最 大不超过当前 pyfile 的总代码行数. e.g. {'src.app.module': [1, 2, 3, 9, 10], 'src.app.main': [4, 5, 8], 'src.app.main.child_method': [6, 7], ...} 有了 module_linos 以后, 我们就可以在已知 module 的调用关系的情况下, 专注于 读取该 module 对应的区间范围, 逐行分析每条语句, 并进一步发现新的调用关系, 以 此产生裂变效应. 详见 src.analyser.VirtualRunner#main(). """ lk.logd('indexing module linos') if top_module == '': top_module = self.top_module if linos is None: linos = list(ast_tree.keys()) linos.sort() # ------------------------------------------------ def eval_ast_line(): ast_line = ast_tree[lino] # type: list # ast_line is type of list, assert it not empty. assert ast_line # here we only take its first element. return ast_line[0] # ast_defs: ast definitions ast_defs = ("<class '_ast.FunctionDef'>", "<class '_ast.ClassDef'>") indent_module_dict = {} # format: {indent: module} out = {} # format: {module: lino_list} last_indent = 0 for lino in linos: # lk.loga(lino) obj_type, obj_val = eval_ast_line() indent = ast_indents.get(lino, -1) """ special: indent 有一个特殊值 -1, 表示下面这种情况: def abc(): # -> indent = 0 print('aaa', # -> indent = 4 'bbb', # -> indent = 10 -> -1 'ccc') # -> indent = 10 -> -1 当 indent 为 -1 时, 则认为本行的 indent 保持与上次 indent 一致. """ if indent == -1: indent = last_indent assert obj_type not in ast_defs assert indent % 4 == 0, (lino, indent) """ 当 indent >= last_indent 时: 在 indent_holder 中开辟新键. 当 indent < last_indent 时: 从 indent_holder 更新并移除高缩进的键. """ lk.loga(lino, indent, obj_type, obj_val) # noinspection PyUnresolvedReferences parent_module = indent_module_dict.get(indent - 4, top_module) """ case 1: indent = 0, obj_val = 'main' -> indent - 4 = -4 -> -4 not in indent_module_dict. so assign default: parent_module = top_module = 'src.app' -> current_module = 'src.app.main' case 2: indent = 4, obj_val = 'child_method' -> indent - 4 = 0 -> parent_module = 'src.app.main' -> current_module = 'src.app.main.child_method' """ if obj_type in ast_defs: # obj_type = "<class 'FunctionDef'>", obj_val = 'main' current_module = parent_module + '.' + obj_val # lk.logt('[TEMPRINT]', current_module) # -> 'src.app.main' elif indent > 0: current_module = parent_module else: current_module = parent_module + '.' + 'module' # -> 'src.app.module' # lk.loga(parent_module, current_module) node = out.setdefault(current_module, []) node.append(lino) # NOTE: the lino is in ordered # update indent_module_dict indent_module_dict.update({indent: current_module}) # -> {0: 'src.app.main'}, {4: 'src.app.main.child_method'}, ... # update last_indent last_indent = indent # sort for lino_list in out.values(): lino_list.sort() # TEST print lk.loga(indent_module_dict) lk.logt('[I4204]', out) return out