-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeep_reloader.py
More file actions
189 lines (145 loc) · 7.73 KB
/
deep_reloader.py
File metadata and controls
189 lines (145 loc) · 7.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import importlib
import logging
import shutil
import sys
from pathlib import Path
from types import ModuleType
from .dependency_extractor import DependencyExtractor
from .domain import DependencyNode
logger = logging.getLogger(__name__)
def deep_reload(module: ModuleType) -> None:
"""モジュールを再帰的にリロードする。
Maya開発でのモジュール変更を即座に反映させるために設計されています。
リロード後、引数で渡されたモジュールオブジェクトの内容が自動的に更新されるため、
戻り値を受け取る必要はありません。
Args:
module: リロード対象のモジュール
Note:
ログレベルの設定には setup_logging() 関数を使用してください。
例: setup_logging(logging.DEBUG)
Example:
```python
from mypackage import main
from deep_reloader import deep_reload
deep_reload(main) # mainの中身が自動的に更新される
main.restart() # 新しいコードが実行される
```
"""
# キャッシュを無効化して .py の変更を認識させる
importlib.invalidate_caches()
# ターゲットパッケージ名を自動推定
module_name = module.__name__
if '.' in module_name:
target_package = module_name.split('.')[0]
else:
target_package = module_name
# TODO: パフォーマンス最適化 - ファイル変更検出による差分リロード
# - ファイルのタイムスタンプキャッシュで変更検出
# - 変更されたモジュールのみのリロード(現在は全モジュール対象)
# - AST解析結果のキャッシュ(頻繁にアクセスされるモジュール用)
# - 依存関係ツリーのキャッシュ(構造変更時のみ再構築)
# ツリー構築(まずツリーを構築して全モジュールを把握)
# TODO: ツリー構造のデバッグ出力機能を追加
# - 依存関係ツリーの視覚的表示(階層構造、インデント付き)
# - 各モジュールの詳細情報(パス、サイズ、最終更新時刻)
# - スキップされるモジュールの理由と一覧
visited_modules = set() # 循環インポート検出用
root = _build_tree(module, visited_modules, target_package)
# ツリー全体の __pycache__ を削除
_clear_pycache_recursive(root)
# リロード
reload_tree(root)
def _build_tree(module: ModuleType, visited_modules: set, target_package: str) -> DependencyNode:
"""
AST 解析して DependencyNode ツリーを構築
Args:
module: 解析対象のモジュール
visited_modules: 循環インポート検出用の訪問済みモジュールセット
target_package: リロード対象のパッケージ名(例: 'routinerecipe')
このパッケージに属するモジュールのみをリロード対象とする
Note:
target_packageに一致しないモジュール(組み込みモジュールやサードパーティライブラリ、その他の自作パッケージ)は
スキップされ、リロード対象から除外されます。
"""
node = DependencyNode(module)
# 循環インポート検出: すでに訪問済みなら子の展開はスキップ(無限ループ防止)
# ノード自体は作成して返す(将来のデバッグ出力で循環参照を可視化するため)
if module.__name__ in visited_modules:
return node
visited_modules.add(module.__name__)
extractor = DependencyExtractor(module)
for dependency in extractor.extract():
# ターゲットパッケージに属するモジュールのみをツリーに追加
if not dependency.module.__name__.startswith(target_package):
logger.debug(f'Skipped module (not in target package): {dependency.module.__name__}')
continue
child_node = _build_tree(dependency.module, visited_modules, target_package)
child_node.symbols = dependency.symbols
node.children.append(child_node)
return node
def _clear_pycache_recursive(node: DependencyNode) -> None:
"""
DependencyNode ツリー全体を再帰的にたどって __pycache__ を削除
"""
_clear_single_pycache(node.module)
for child in node.children:
_clear_pycache_recursive(child)
def _clear_single_pycache(module: ModuleType) -> None:
"""
1つのモジュールに対応する __pycache__ を削除
"""
module_file = getattr(module, '__file__', None)
if module_file is None:
return
module_dir = Path(module_file).parent
pycache_dir = module_dir / '__pycache__'
if pycache_dir.exists():
try:
shutil.rmtree(pycache_dir)
logger.debug(f'Cleared pycache {pycache_dir}')
except Exception as e:
logger.warning(f'Failed to clear pycache {pycache_dir}: {e!r}')
def reload_tree(node: DependencyNode, visited_modules: set = None) -> None:
"""依存関係ツリーを再帰的にリロード
DependencyNodeで構成された依存ツリーを深さ優先探索でリロードします。
処理の流れ:
1. 子モジュールを再帰的にリロード(子が先に完了する必要がある)
2. importlib.reload()で新しいモジュールオブジェクト(reloaded_module)を作成
3. node.module.__dict__を更新(削除された属性を除去、新しい属性を追加・上書き)
4. sys.modules[name]にnode.moduleを登録(reloaded_moduleではなく)
重要な設計思想:
- node.moduleのオブジェクトIDを保持することで、既存の参照を有効に保つ
- reloaded_moduleは一時的な作業用オブジェクトとして使用
- __dict__を更新することで、オブジェクトを置き換えずに中身だけを更新
- importlib.reload()が自動的にimport文を再実行するため、from-importしたシンボルも最新になる
Args:
node: リロード対象のノード
visited_modules: 訪問済みモジュールのセット(循環参照防止)
"""
# 再帰処理で訪問済みモジュールを記録するセット
if visited_modules is None:
visited_modules = set()
name = node.module.__name__
# 既に訪問済みのモジュールはスキップ(重複処理防止・処理時間短縮)
if name in visited_modules:
return
# このモジュールの処理が完了したことをマーク
visited_modules.add(name)
# 子を再帰的にリロード(子が先に完了する必要がある)
for child in node.children:
reload_tree(child, visited_modules)
# importlib.reload()を使用してリロード
# これにより、sys.modulesから削除せずに安全にリロードできる
# また、from .child import xxx などのimport文が再実行され、最新の値が自動的に設定される
reloaded_module = importlib.reload(node.module)
# リロード前のモジュール(node.module)にあって、リロード後のモジュール(reloaded_module)に存在しなくなった属性を削除する
old_attrs = set(node.module.__dict__.keys())
new_attrs = set(reloaded_module.__dict__.keys())
for key in old_attrs - new_attrs:
if not key.startswith('__'): # __name__, __file__等の特殊属性は保持
del node.module.__dict__[key]
# node.module.__dict__をreloaded_module.__dict__で更新(属性を追加・上書き)
node.module.__dict__.update(reloaded_module.__dict__)
# sys.modulesをnode.moduleで上書き
sys.modules[name] = node.module
logger.debug(f'RELOADED {name}')