@@ -30,45 +30,128 @@ def extract(self) -> List[Tuple[ModuleType, ImportedSymbols]]:
3030 return []
3131
3232 results : List [Tuple [ModuleType , ImportedSymbols ]] = []
33- for node in ast .walk (self .tree ): # walkで全ノード探索(動的なfrom-import文も検出)
33+ for node in ast .walk (self .tree ):
3434 if isinstance (node , ast .ImportFrom ):
35- child_module = self ._resolve_imported_module (node )
36- # 無効な依存関係を除外: 存在しないモジュール(None)と自分自身への参照
37- if child_module is None or child_module is self .module :
38- continue
35+ # 1つのimport文から複数の依存関係が生まれる可能性があるためextendを使用
36+ # 例: from . import module1, module2, func → 最大3つのタプルが返る
37+ results .extend (self ._extract_from_node (node ))
38+ return results
39+
40+ def _extract_from_node (self , node : ast .ImportFrom ) -> List [Tuple [ModuleType , ImportedSymbols ]]:
41+ """ImportFromノードから依存関係を抽出する
42+
43+ 戻り値がリストである理由:
44+ 1つのimport文から複数の依存関係が生まれる場合がある
45+ 例: from . import module1, module2, func
46+ → [(module1, ImportedSymbols(['module1'])),
47+ (module2, ImportedSymbols(['module2'])),
48+ (parent_package, ImportedSymbols(['func']))]
49+ """
50+ if node .level > 0 and node .module is None :
51+ # from . import yyy のパターン
52+ return self ._extract_dot_only (node )
53+ else :
54+ # 上記以外(from xxx import yyy 系)のパターン
55+ return self ._extract_module_specified (node )
56+
57+ def _extract_dot_only (self , node : ast .ImportFrom ) -> List [Tuple [ModuleType , ImportedSymbols ]]:
58+ """from . import yyy 形式のインポートから依存関係を抽出する
59+
60+ モジュールインポートとシンボルインポートを自動判別して処理する
61+ """
62+ parent_module = self ._import_parent_package (node .level )
63+ if parent_module is None or parent_module is self .module :
64+ return []
65+
66+ # 各インポート名をモジュールまたはシンボルとして分類
67+ module_imports = []
68+ symbol_names = []
69+
70+ for alias in node .names :
71+ child_module = self ._import_relative_module (node .level , alias .name )
72+ if child_module is not None and child_module is not self .module :
73+ module_imports .append ((child_module , alias .name ))
74+ else :
75+ symbol_names .append (alias .name )
76+
77+ # 結果をまとめる
78+ results = []
79+ for child_module , module_name in module_imports :
80+ results .append ((child_module , ImportedSymbols ([module_name ])))
81+
82+ if symbol_names :
83+ results .append ((parent_module , ImportedSymbols (symbol_names )))
3984
40- symbols = self ._extract_symbols (child_module , node )
41- results .append ((child_module , symbols ))
4285 return results
4386
44- def _parse_ast (self ) -> Optional [ ast . AST ]:
45- """モジュールをASTにパース """
46- try :
47- source = inspect . getsource ( self . module )
48- return ast . parse ( source )
49- except ( OSError , TypeError , SyntaxError ):
50- # 組み込みモジュール(os, sys等)、バイナリ拡張(.pyd/.so)、
51- # Maya内部モジュール(maya.cmds等)はソースコードが取得できないためNoneを返す
52- return None
87+ def _extract_module_specified (self , node : ast . ImportFrom ) -> List [ Tuple [ ModuleType , ImportedSymbols ] ]:
88+ """from xxx import yyy 形式のインポートから依存関係を抽出する (モジュール名指定あり) """
89+ child_module = self . _import_module ( node )
90+ # モジュール解決失敗 or 自己参照の場合は依存関係なし
91+ if child_module is None or child_module is self . module :
92+ return []
93+
94+ symbols = self . _extract_symbols ( child_module , node )
95+ return [( child_module , symbols )]
5396
54- def _resolve_imported_module (self , stmt : ast .ImportFrom ) -> Optional [ModuleType ]:
55- """ImportFromノードからインポート対象のモジュールを取得 """
97+ def _import_module (self , stmt : ast .ImportFrom ) -> Optional [ModuleType ]:
98+ """from xxx import yyy 形式のインポートからモジュールを取得 """
5699 try :
57100 if stmt .level > 0 :
58- # 相対インポートを絶対パスに変換
59- # importlib.import_module()は絶対パス(絶対モジュール名)しか受け付けないため、
60- # 相対インポート(from .module や from ..module)を絶対パスに変換する必要がある
61- base_name = self .module .__name__ .rsplit ('.' , stmt .level )[0 ]
62- # stmt.moduleがある場合: from ..utils import helper → "parent.utils"
63- # stmt.moduleがNoneの場合: from .. import helper → "parent" (パッケージ自体の__init__.py)
64- target_name = f'{ base_name } .{ stmt .module } ' if stmt .module else base_name
65- return importlib .import_module (target_name )
101+ # 相対インポート(from .xxx import yyy)の場合
102+ return self ._import_relative_module (stmt .level , stmt .module )
66103 else :
67- # 絶対インポートの場合は絶対パスのためそのまま使用
104+ # 絶対インポート(from xxx import yyy)の場合
68105 return importlib .import_module (stmt .module )
69106 except (ModuleNotFoundError , ImportError ):
70107 return None
71108
109+ def _import_relative_module (self , level : int , module_name : str ) -> Optional [ModuleType ]:
110+ """相対パスからモジュールをインポートする
111+
112+ Args:
113+ level: 相対インポートのレベル (1 = ".", 2 = "..", ...)
114+ module_name: インポートするモジュール名
115+
116+ Returns:
117+ インポートされたモジュール、失敗時はNone
118+
119+ 例:
120+ self.moduleがmypackage.sub.mainの場合
121+ - from . import utils
122+ → level=1, module_name='utils' → mypackage.sub.utils
123+ - from .utils import func
124+ → level=1, module_name='utils' → mypackage.sub.utils
125+ """
126+ try :
127+ base_name = self .module .__name__ .rsplit ('.' , level )[0 ]
128+ target_name = f'{ base_name } .{ module_name } '
129+ return importlib .import_module (target_name )
130+ except (ModuleNotFoundError , ImportError ):
131+ return None
132+
133+ def _import_parent_package (self , level : int ) -> Optional [ModuleType ]:
134+ """相対インポートの親パッケージをインポートする
135+
136+ Args:
137+ level: 相対インポートのレベル (1 = ".", 2 = "..", ...)
138+
139+ Returns:
140+ インポートされた親パッケージ、失敗時はNone
141+
142+ 例:
143+ self.moduleがmypackage.sub.mainの場合
144+ - from . import func
145+ → level=1 → mypackage.sub
146+ - from .. import func
147+ → level=2 → mypackage
148+ """
149+ try :
150+ base_name = self .module .__name__ .rsplit ('.' , level )[0 ]
151+ return importlib .import_module (base_name )
152+ except (ModuleNotFoundError , ImportError ):
153+ return None
154+
72155 def _extract_symbols (self , child_module : ModuleType , stmt : ast .ImportFrom ) -> ImportedSymbols :
73156 """ImportFromノードからシンボル名を抽出"""
74157 # AST から実際のシンボル名を抽出(エイリアス名ではなく元の名前)
@@ -90,3 +173,13 @@ def _extract_symbols(self, child_module: ModuleType, stmt: ast.ImportFrom) -> Im
90173 if attr_name .startswith ('__' ) is False : # __name__, __file__ 等の特殊属性を除外
91174 public_attrs .append (attr_name )
92175 return ImportedSymbols (public_attrs )
176+
177+ def _parse_ast (self ) -> Optional [ast .AST ]:
178+ """モジュールをASTにパース"""
179+ try :
180+ source = inspect .getsource (self .module )
181+ return ast .parse (source )
182+ except (OSError , TypeError , SyntaxError ):
183+ # 組み込みモジュール(os, sys等)、バイナリ拡張(.pyd/.so)、
184+ # Maya内部モジュール(maya.cmds等)はソースコードが取得できないためNoneを返す
185+ return None
0 commit comments