Skip to content

Commit cfa1a98

Browse files
authored
Merge pull request #6 from Hum9183/release/v0.5.0
Release/v0.5.0
2 parents aa34295 + 58f3224 commit cfa1a98

35 files changed

Lines changed: 1608 additions & 1175 deletions

README.md

Lines changed: 49 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# deep_reloader
22

33
> [!WARNING]
4-
> このソフトウェアは現在プレリリース版(v0.3.0)です。APIが変更される可能性があります。
4+
> このソフトウェアは現在プレリリース版です。APIが変更される可能性があります。
55
66
Pythonモジュールの依存関係を解析して、再帰的にリロードを行うライブラリです。特にMayaでのスクリプト開発時に、モジュール変更を即座に反映させるために設計されています。
77

@@ -11,7 +11,27 @@ Pythonモジュールの依存関係を解析して、再帰的にリロード
1111
- **AST解析**: 静的解析により from-import文 を正確に検出
1212
- **ワイルドカード対応**: `from module import *` もサポート
1313
- **相対インポート対応**: パッケージ内の相対インポートを正しく処理
14-
- **循環参照対応**: Pythonで動作する循環インポート(関数内での遅延インポート)を正しくリロード
14+
- **循環参照対応**: Pythonで動作する循環インポートを正しくリロード
15+
16+
## インストール
17+
18+
Pythonパスが通っている場所であればどこでも配置可能です。
19+
本READMEでは一般的なMayaのscriptsフォルダーを例として説明します。
20+
21+
```
22+
~/Documents/maya/scripts/ (例)
23+
└── deep_reloader/
24+
├── __init__.py
25+
├── _metadata.py
26+
├── deep_reloader.py
27+
├── from_clause.py
28+
├── import_clause.py
29+
├── module_info.py
30+
├── symbol_extractor.py
31+
├── LICENSE
32+
├── README.md
33+
└── tests/
34+
```
1535

1636
## 使用方法
1737

@@ -46,53 +66,14 @@ deep_reload(your_module)
4666
- `logging.INFO`: モジュールリロードの状況を表示(デフォルト)
4767
- `logging.WARNING`: エラーと警告のみ表示
4868

49-
## インストール
50-
51-
Pythonパスが通っている場所であればどこでも配置可能です。
52-
本READMEでは一般的なMayaのscriptsフォルダーを例として説明します。
53-
54-
```
55-
~/Documents/maya/scripts/ (例)
56-
└── deep_reloader/
57-
├── __init__.py
58-
├── _metadata.py
59-
├── deep_reloader.py
60-
├── imported_symbols.py
61-
├── module_info.py
62-
├── symbol_extractor.py
63-
├── LICENSE
64-
├── README.md
65-
└── tests/ # テストファイル(開発・デバッグ用)
66-
```
67-
6869
## テスト実行
6970

70-
**注意: テストはVSCodeやコマンドラインで実行してください。Maya内部での実行はサポートしていません。**
71-
72-
このプロジェクトでは、スクリプト実行とpytest実行の両方をサポートしています。Maya開発環境での利便性を考慮し、pytestが利用できない環境でも直接スクリプトとしてテストを実行できます。
73-
74-
### スクリプト実行
75-
76-
各テストファイルを直接Python スクリプトとして実行できます:
71+
**注意: テストはpytestで実行してください。Maya内部での実行はサポートしていません。**
7772

78-
#### コマンドライン実行
79-
80-
```shell
81-
# 全テストを一括実行(フルパス指定)
82-
python ~/Documents/maya/scripts/deep_reloader/tests/test_runner.py
83-
84-
# 個別テスト実行(フルパス指定)
85-
python ~/Documents/maya/scripts/deep_reloader/tests/test_absolute_import_basic.py
86-
```
87-
88-
#### VSCode実行
89-
90-
VSCodeでテストファイルを開いて「▶️ Run Python File」ボタンで実行できます。
73+
このプロジェクトのテストはpytest専用です。開発環境でpytestを使用してテストを実行してください。
9174

9275
### pytest実行
9376

94-
pytestが利用可能な環境では、より高機能なテスト実行が可能です:
95-
9677
```shell
9778
# パッケージの親ディレクトリに移動 (例)
9879
cd ~/Documents/maya/scripts/
@@ -101,17 +82,20 @@ cd ~/Documents/maya/scripts/
10182
python -m pytest deep_reloader/tests/ -v
10283

10384
# 特定のテストファイル実行
104-
python -m pytest deep_reloader/tests/test_absolute_import_basic.py -v
85+
python -m pytest deep_reloader/tests/integration/test_absolute_import.py -v
10586

10687
# より詳細な出力
10788
python -m pytest deep_reloader/tests/ -vv
89+
90+
# 簡潔な出力
91+
python -m pytest deep_reloader/tests/ -q
10892
```
10993

11094
### 動作確認済み環境
11195

11296
**テスト開発環境(Maya以外):**
11397
- Python 3.11.9+(現在の開発環境で検証済み)
114-
- pytest 8.4.2+(テスト実行時のみ、現在の開発環境で検証済み
98+
- pytest 8.4.2+(テスト実行に必須
11599

116100
**注意**: 上記はライブラリのテスト・開発で使用している環境です。Maya内での実行環境とは異なります。Mayaのサポートバージョンはまだ確定していません。
117101

@@ -163,36 +147,39 @@ python -m pytest deep_reloader/tests/ -vv
163147
- 例外クラスをリロード対象から除外する
164148
- アプリケーションを再起動する
165149

166-
- **import文非対応**(仕様)
167-
- `import module` 形式の依存関係は解析対象外です
168-
- 現在対応しているのは `from module import something` 形式のみです
169-
- **理由**:
170-
- `import xxx` は主に標準ライブラリや外部ライブラリで使用され、これらはリロード対象外です
171-
- 自作パッケージ内では `from . import module` を使うのが一般的な慣習です
150+
- **import文非対応**(将来的に対応予定)
151+
- `import module` 形式の依存関係は現在は解析対象外です
152+
- 現在対応しているのはfrom-import形式のみです:
153+
- `from xxx import yyy` 形式
154+
- `from .xxx import yyy` 形式
155+
- `from . import yyy` 形式
156+
157+
- **現状の推奨**:
158+
- from-import を使用してください(例: `from deep_reloader import deep_reload`
159+
- `import xxx` 形式は将来のバージョンで対応予定です
160+
161+
- **将来の対応予定**:
162+
- `import mypackage` のような同一パッケージ内のモジュールインポートを検出し、依存関係として追跡
163+
- 標準ライブラリや外部ライブラリは引き続き除外
172164

173165
- **単一パッケージのみリロード**(仕様)
174166
- `deep_reload()`は、指定されたモジュールと同じパッケージに属するモジュールのみをリロードします
175-
- **理由**: 組み込みモジュール(`collections`等)やサードパーティライブラリ(`maya.cmds`, `PySide2`等)のリロードを防ぎ、システムの安定性を保つため
176-
- ****: `deep_reload(routinerecipe.main)` を実行すると、`routinerecipe`パッケージ内のモジュールのみがリロードされます
167+
- **理由**: 組み込みモジュール(`sys`等)やサードパーティライブラリ(`maya.cmds`, `PySide2`等)のリロードを防ぎ、システムの安定性を保つため
168+
- ****: `deep_reload(myutils)` を実行すると、`myutils`パッケージ内のモジュールのみがリロードされます
177169
- **複数の自作パッケージを開発している場合**:
178170
```python
179-
# routinerecipemyutils の両方を開発中の場合
180-
deep_reload(myutils.helper) # myutilsパッケージをリロード
181-
deep_reload(routinerecipe.main) # routinerecipeパッケージをリロード
171+
# myutilsmyfunctions の両方を開発中の場合
172+
deep_reload(myutils.helper) # myutilsパッケージをリロード
173+
deep_reload(myfunctions.main) # myfunctionsパッケージをリロード
182174
```
183175

184-
## バージョン情報
185-
186-
**現在のバージョン**: v0.3.0 (Pre-release)
187-
188176
### リリース状況
189177
- ✅ コア機能実装完了(from-import対応)
190-
- ✅ テストスイート12テスト)
178+
- ✅ テストスイート
191179
- ✅ ドキュメント整備
192180
- ✅ Maya環境での動作検証
193181
- ✅ 循環インポート対応
194182
- 🔄 APIの安定化作業中
195-
- 📋 import文対応の追加
196183
- 📋 デバッグログの強化
197184
- 📋 パフォーマンス最適化とキャッシュ機能
198185

_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = '0.3.0'
1+
__version__ = '0.5.0'
22
__author__ = 'Miyakawa Takeshi'

deep_reloader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _build_tree(module: ModuleType, visited: set, target_package: str) -> Module
8383
node = ModuleInfo(module)
8484

8585
# 循環インポート検出: すでに訪問済みなら子の展開はスキップ(無限ループ防止)
86-
# ただし、ノード自体は作成してリロード対象には含める
86+
# ノード自体は作成して返す(将来のデバッグ出力で循環参照を可視化するため)
8787
if module.__name__ in visited:
8888
return node
8989

from_clause.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import importlib
2+
from types import ModuleType
3+
from typing import Optional, Tuple
4+
5+
6+
class FromClause:
7+
"""from句のモジュールを保持し、from句関連の処理を担当するクラス
8+
9+
from xxx import yyy の xxx 部分(from句)のモジュールを保持し、
10+
サブモジュールのインポートやモジュール/アトリビュート判定を行います。
11+
12+
Attributes:
13+
_module: from句で指定されたモジュール
14+
_base_module: 基準となるモジュール(import文が記述されているモジュール)
15+
"""
16+
17+
def __init__(self, module: ModuleType, base_module: ModuleType) -> None:
18+
self._module = module
19+
self._base_module = base_module
20+
21+
@property
22+
def module(self) -> ModuleType:
23+
"""from句のモジュールを取得"""
24+
return self._module
25+
26+
@classmethod
27+
def resolve(cls, base_module: ModuleType, level: int, module_name: Optional[str]) -> Optional['FromClause']:
28+
"""from句のモジュールを解決してFromClauseインスタンスを生成
29+
30+
Args:
31+
base_module: 基準となるモジュール(import文が記述されているモジュール)
32+
level: 相対インポートのレベル (0=絶対, 1=".", 2="..", ...)
33+
module_name: モジュール名(from . import yyy の場合はNone)
34+
35+
Returns:
36+
FromClauseインスタンス、失敗時はNone
37+
38+
例:
39+
- from math import sin
40+
→ level=0, module_name='math' → mathモジュール
41+
- from . import helper
42+
→ level=1, module_name=None → 親パッケージ
43+
- from .utils import func
44+
→ level=1, module_name='utils' → utilsモジュール
45+
- from ..config import VALUE
46+
→ level=2, module_name='config' → configモジュール
47+
"""
48+
if level > 0 and module_name is None:
49+
# from . import yyy パターン
50+
module = cls._import_relative_parent_package(base_module, level)
51+
else:
52+
# from xxx import yyy パターン
53+
module = cls._import_from_clause(base_module, level, module_name)
54+
55+
if module is None:
56+
return None
57+
58+
return cls(module, base_module)
59+
60+
def try_import_as_module(self, name: str, is_relative_dot_only: bool) -> Tuple[bool, Optional[ModuleType]]:
61+
"""nameをモジュールとしてインポート試行(モジュール/アトリビュート判定のため)
62+
63+
モジュール/アトリビュートの分類判定に使用。
64+
成功すればモジュール、失敗すればアトリビュート(関数/クラス/変数)と判断される。
65+
66+
Args:
67+
name: インポートする名前
68+
is_relative_dot_only: from . import yyy パターンかどうか
69+
70+
Returns:
71+
(is_module, module): is_moduleがTrueならモジュール、Falseならアトリビュート
72+
"""
73+
# どちらのパターンでもサブモジュールとしてインポートを試行
74+
module_candidate = self._try_import_submodule(name)
75+
76+
is_module = module_candidate is not None and module_candidate is not self._base_module
77+
return (is_module, module_candidate if is_module else None)
78+
79+
def _try_import_submodule(self, name: str) -> Optional[ModuleType]:
80+
"""from句のモジュールから指定された名前をサブモジュールとしてインポートを試行
81+
82+
Args:
83+
name: インポートする名前
84+
85+
Returns:
86+
インポートされたサブモジュール、失敗時はNone
87+
"""
88+
try:
89+
full_name = f'{self._module.__name__}.{name}'
90+
return importlib.import_module(full_name)
91+
except (ModuleNotFoundError, ImportError):
92+
return None
93+
94+
@staticmethod
95+
def _import_from_clause(base_module: ModuleType, level: int, module_name: Optional[str]) -> Optional[ModuleType]:
96+
"""from句で指定されたモジュールをインポート
97+
98+
Args:
99+
base_module: 基準となるモジュール
100+
level: 相対インポートのレベル (0=絶対, 1=".", 2="..", ...)
101+
module_name: モジュール名
102+
"""
103+
try:
104+
if level > 0:
105+
# 相対インポート(from .xxx import yyy)の場合
106+
return FromClause._import_from_clause_relative(base_module, level, module_name)
107+
else:
108+
# 絶対インポート(from xxx import yyy)の場合
109+
return importlib.import_module(module_name)
110+
except (ModuleNotFoundError, ImportError):
111+
return None
112+
113+
@staticmethod
114+
def _import_from_clause_relative(base_module: ModuleType, level: int, module_name: str) -> Optional[ModuleType]:
115+
"""from句の相対インポートでモジュールをインポートする
116+
117+
Args:
118+
base_module: 基準となるモジュール
119+
level: 相対インポートのレベル (1 = ".", 2 = "..", ...)
120+
module_name: インポートするモジュール名
121+
122+
Returns:
123+
インポートされたモジュール、失敗時はNone
124+
"""
125+
try:
126+
# パッケージ(__path__を持つ)の場合、level - 1 を使用
127+
if hasattr(base_module, '__path__'):
128+
actual_level = level - 1
129+
else:
130+
actual_level = level
131+
132+
if actual_level == 0:
133+
base_name = base_module.__name__
134+
else:
135+
base_name = base_module.__name__.rsplit('.', actual_level)[0]
136+
137+
target_name = f'{base_name}.{module_name}'
138+
return importlib.import_module(target_name)
139+
except (ModuleNotFoundError, ImportError):
140+
return None
141+
142+
@staticmethod
143+
def _import_relative_parent_package(base_module: ModuleType, level: int) -> Optional[ModuleType]:
144+
"""相対インポートの親パッケージをインポートする
145+
146+
Args:
147+
base_module: 基準となるモジュール
148+
level: 相対インポートのレベル (1 = ".", 2 = "..", ...)
149+
150+
Returns:
151+
インポートされた親パッケージ、失敗時はNone
152+
"""
153+
try:
154+
# パッケージ(__path__を持つ)の場合、level - 1 を使用
155+
if hasattr(base_module, '__path__'):
156+
actual_level = level - 1
157+
else:
158+
actual_level = level
159+
160+
if actual_level == 0:
161+
# 自分自身のパッケージ
162+
return base_module
163+
else:
164+
parent_name = base_module.__name__.rsplit('.', actual_level)[0]
165+
return importlib.import_module(parent_name)
166+
except (ModuleNotFoundError, ImportError, ValueError):
167+
return None

0 commit comments

Comments
 (0)