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
Exemple #2
0
    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)
Exemple #7
0
 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