Skip to content

Commit aa34295

Browse files
authored
Merge pull request #5 from Hum9183/release/v0.4.0
Release/v0.4.0
2 parents f301feb + 7264259 commit aa34295

18 files changed

Lines changed: 362 additions & 115 deletions

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,12 @@ python -m pytest deep_reloader/tests/ -vv
163163
- 例外クラスをリロード対象から除外する
164164
- アプリケーションを再起動する
165165

166-
- **import文未対応**(将来対応予定)
167-
- 現在は `import module` 形式の依存関係は解析対象外です
168-
- 対応: `from module import something` 形式のみ解析・リロード
169-
- 今後のバージョンで対応予定
166+
- **import文非対応**(仕様)
167+
- `import module` 形式の依存関係は解析対象外です
168+
- 現在対応しているのは `from module import something` 形式のみです
169+
- **理由**:
170+
- `import xxx` は主に標準ライブラリや外部ライブラリで使用され、これらはリロード対象外です
171+
- 自作パッケージ内では `from . import module` を使うのが一般的な慣習です
170172

171173
- **単一パッケージのみリロード**(仕様)
172174
- `deep_reload()`は、指定されたモジュールと同じパッケージに属するモジュールのみをリロードします

__init__.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import logging
22

3-
from ._metadata import __author__, __version__
43
from .deep_reloader import deep_reload
5-
from .imported_symbols import ImportedSymbols
6-
from .module_info import ModuleInfo
7-
from .symbol_extractor import SymbolExtractor
84

95

106
def setup_logging(log_level: int = logging.INFO) -> logging.Logger:
@@ -37,7 +33,4 @@ def setup_logging(log_level: int = logging.INFO) -> logging.Logger:
3733
__all__ = [
3834
'deep_reload',
3935
'setup_logging',
40-
'ModuleInfo',
41-
'ImportedSymbols',
42-
'SymbolExtractor',
4336
]

imported_symbols.py

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Iterator, List, Optional
2+
from typing import List, Optional
33

44
logger = logging.getLogger(__name__)
55

@@ -18,39 +18,6 @@ def __init__(self, names: Optional[List[str]] = None) -> None:
1818
"""
1919
self.names: List[str] = names or []
2020

21-
def __iter__(self) -> Iterator[str]:
22-
return iter(self.names)
23-
24-
def __len__(self) -> int:
25-
return len(self.names)
26-
27-
def __repr__(self) -> str:
28-
return f'ImportedSymbols({self.names})'
29-
30-
def __contains__(self, name: str) -> bool:
31-
"""指定したシンボル名が含まれているかを返す"""
32-
return name in self.names
33-
34-
def __bool__(self) -> bool:
35-
"""空でなければ True を返す(if symbols: が可能)"""
36-
return bool(self.names)
37-
38-
def to_list(self) -> List[str]:
39-
"""シンボル名のリストをコピーして返す"""
40-
return list(self.names)
41-
42-
def add(self, *new_names: str) -> None:
43-
"""新しいシンボル名を追加(重複は自動的に除去)"""
44-
for name in new_names:
45-
if name not in self.names:
46-
self.names.append(name)
47-
48-
def merge(self, other: 'ImportedSymbols') -> None:
49-
"""他の ImportedSymbols と結合(重複除去)"""
50-
for name in other.names:
51-
if name not in self.names:
52-
self.names.append(name)
53-
5421
def copy_to(self, source_module, target_module) -> None:
5522
"""
5623
source_module(例:a)から target_module(例:b)へ

module_info.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,20 @@ def reload(self, visited=None) -> None:
4646
if name in visited:
4747
return
4848

49+
# このモジュールの処理が完了したことをマーク
50+
visited.add(name)
51+
52+
# 子を再帰的にリロード(子が先に完了する必要がある)
53+
for child in self.children:
54+
child.reload(visited)
55+
4956
# 一時的にsys.modulesから削除してキャッシュをクリア
5057
sys.modules.pop(name, None)
5158
importlib.invalidate_caches()
5259

5360
# 新しいモジュールをインポート
5461
new_module = importlib.import_module(name)
5562

56-
# 子を再帰的にリロード(子が先に完了する必要がある)
57-
for child in self.children:
58-
child.reload(visited)
59-
6063
# 子のリロード後、from-importシンボルを新しいモジュールにコピー
6164
# (new_moduleの関数の__globals__に正しい値を設定するため)
6265
for child in self.children:
@@ -75,6 +78,7 @@ def reload(self, visited=None) -> None:
7578
# sys.modulesをself.moduleで上書き
7679
sys.modules[name] = self.module
7780

81+
# 訪問済みとしてマーク
7882
visited.add(name)
7983

8084
logger.debug(f'RELOADED {name}')

symbol_extractor.py

Lines changed: 120 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests/test_absolute_import_basic.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import importlib
21
import textwrap
32

43
try:
@@ -43,8 +42,7 @@ def test_simple_from_import_reload(tmp_path):
4342
deep_reload(test_package.b)
4443

4544
# 更新された値を確認
46-
new_b = importlib.import_module('test_package.b')
47-
assert new_b.x == 999
45+
assert test_package.b.x == 999
4846

4947

5048
if __name__ == '__main__':

tests/test_absolute_import_chained.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import importlib
21
import textwrap
32

43
try:
@@ -52,13 +51,9 @@ def test_chained_from_import_reload(tmp_path):
5251
deep_reload(test_package.c)
5352

5453
# 更新された値を確認
55-
new_a = importlib.import_module('test_package.a')
56-
new_b = importlib.import_module('test_package.b')
57-
new_c = importlib.import_module('test_package.c')
58-
59-
assert new_a.value == 777
60-
assert new_b.value == 777
61-
assert new_c.value == 777
54+
assert test_package.a.value == 777
55+
assert test_package.b.value == 777
56+
assert test_package.c.value == 777
6257

6358

6459
if __name__ == "__main__":

tests/test_absolute_import_wildcard.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import importlib
21
import textwrap
32

43
try:
@@ -52,9 +51,8 @@ def test_wildcard_from_import_reload(tmp_path):
5251
deep_reload(test_package.b)
5352

5453
# 更新された値を確認
55-
new_b = importlib.import_module('test_package.b')
56-
assert new_b.x == 100
57-
assert new_b.y == 200
54+
assert test_package.b.x == 100
55+
assert test_package.b.y == 200
5856

5957

6058
if __name__ == '__main__':

tests/test_architecture_demo.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,14 @@ def show_info():
166166

167167
# リロード後の値を確認
168168
# config.pyの変更がutils.py、main.pyまで伝播していることを確認
169-
import importlib
170-
171-
new_main = importlib.import_module('test_package.main')
172-
assert new_main.show_info() == 'Running: UpdatedApp v2.5.0'
169+
assert test_package.main.show_info() == 'Running: UpdatedApp v2.5.0'
173170

174171
# 実行方式の検出と表示
175172
# この情報により、どちらの方式で実行されているかが分かります
176173
execution_method = _detect_execution_method()
177174
print(f"実行方式: {execution_method}")
178175
print(f"更新前: Running: DemoApp v1.0.0")
179-
print(f"更新後: {new_main.show_info()}")
176+
print(f"更新後: {test_package.main.show_info()}")
180177
print("※ deep_reload()により依存チェーン(config -> utils -> main)がすべて更新されました")
181178

182179
# 成功メッセージ

tests/test_circular_import.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
A → B → A のような循環インポート構造が正しくリロードされることを確認
44
"""
55

6-
import importlib
76
import textwrap
87

98
try:
@@ -82,9 +81,8 @@ def call_a():
8281
deep_reload(module_a)
8382

8483
# 更新確認
85-
new_module_a = importlib.import_module('circular_pkg.module_a')
86-
assert new_module_a.func_a() == 'A-v2'
87-
assert new_module_a.call_b() == 'B-v2'
84+
assert module_a.func_a() == 'A-v2'
85+
assert module_a.call_b() == 'B-v2'
8886

8987

9088
if __name__ == "__main__":

0 commit comments

Comments
 (0)