python-path-support.md•7.29 kB
# Python Interpreter Selection (pythonPath Support)
## 概要
Debug-MCPは `pythonPath` パラメータを使用して、デバッグセッションで使用するPythonインタープリタを指定できます。これにより、異なるバージョンのPythonや仮想環境のインタープリタを使ってスクリプトをデバッグできます。
## 問題の背景
### 発生していた問題
別のリポジトリでデバッグを試みた際、以下のエラーが発生:
```
AttributeError: 'PosixPath' object has no attribute '_str'
```
### 原因
1. **Python バージョンの不一致**
- デバッグサーバーと対象リポジトリが異なるPythonバージョンを使用
- `multiprocessing.Process` は親プロセスと同じインタープリタを強制使用
- 異なるバージョン間で `pathlib.Path` オブジェクトをpickleで転送すると内部構造の不一致により破損
2. **内部実装の詳細**
- `PosixPath._str` はPython 3.11で追加されたスロット
- 古いバージョンで作成された Path を新しいバージョンで読むと属性不足でエラー
## 解決方法
### アーキテクチャの変更
**以前**: `multiprocessing.Process`
- 親と同じPythonインタープリタを使用(変更不可)
- IPC: `multiprocessing.Pipe`(pickle ベース)
**現在**: `subprocess.Popen`
- 任意のPythonインタープリタを指定可能
- IPC: stdin/stdout による JSON Lines(バージョン非依存)
### 実装の詳細
#### 1. Runner のスタンドアロン化
`runner_main.py` を作成し、独立したスクリプトとして実行可能に:
```python
# src/mcp_debug_tool/runner_main.py
python_executable = session.python_path or sys.executable
runner_script = Path(__file__).parent / "runner_main.py"
process = subprocess.Popen(
[python_executable, str(runner_script), str(workspace_root)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
```
#### 2. JSON Lines による IPC
pickle の代わりに JSON Lines を使用:
```python
# 送信
command_json = json.dumps(command) + '\n'
process.stdin.write(command_json)
process.stdin.flush()
# 受信
response_line = process.stdout.readline()
response_data = json.loads(response_line)
```
#### 3. Path オブジェクトの安全な変換
サブプロセス内で Path を文字列に変換:
```python
def _convert_paths_to_str(obj):
"""Recursively convert Path objects to strings."""
if isinstance(obj, Path):
import os
try:
return os.fspath(obj)
except (TypeError, AttributeError):
# Fallback for broken Path objects
if hasattr(obj, 'parts'):
return '/'.join(obj.parts) if obj.parts else ''
return '<invalid path object>'
# ... 再帰的に dict/list を処理
```
## 使用方法
### 1. デフォルト(現在のPython)
```json
{
"entry": "main.py"
}
```
現在のプロセスと同じPythonインタープリタを使用。
### 2. 明示的なパス指定
```json
{
"entry": "main.py",
"pythonPath": "/usr/local/bin/python3.11"
}
```
指定したインタープリタでデバッグセッションを実行。
### 3. 仮想環境のPython
```json
{
"entry": "main.py",
"pythonPath": "/path/to/project/.venv/bin/python"
}
```
プロジェクト固有の仮想環境を使用。
### 4. pyenv で管理されたPython
```bash
# pyenvで使用するバージョンを確認
pyenv which python # 例: /Users/user/.pyenv/versions/3.11.9/bin/python
# MCP クライアントから指定
{
"entry": "app.py",
"pythonPath": "/Users/user/.pyenv/versions/3.11.9/bin/python"
}
```
## ベストプラクティス
### 1. プロジェクトの想定バージョンを確認
```bash
# .python-version を確認
cat .python-version
# pyproject.toml を確認
grep "requires-python" pyproject.toml
```
### 2. 仮想環境のPythonを使用
対象プロジェクトが仮想環境を持つ場合は、そのPythonを指定:
```bash
# 仮想環境の有無を確認
ls -la .venv/bin/python
ls -la venv/bin/python
# 絶対パスを取得
realpath .venv/bin/python
```
### 3. バージョン互換性の確認
サーバーとターゲットのPythonバージョンを確認:
```bash
# サーバー側
python --version
# ターゲット側
/path/to/target/python --version
```
## トラブルシューティング
### エラー: "Python interpreter not found"
```json
{
"error": {
"type": "FileNotFoundError",
"message": "Python interpreter not found: /path/to/python"
}
}
```
**解決策**:
- パスが正しいか確認
- Python が実行可能か確認: `ls -l /path/to/python`
- 絶対パスを使用
### エラー: "Runner process terminated unexpectedly"
**原因**:
- 指定したPythonに必要なパッケージがインストールされていない
- バージョンが古すぎる(< 3.11)
**解決策**:
```bash
# 依存関係をインストール
/path/to/python -m pip install pydantic
# バージョン確認
/path/to/python --version
```
### Path オブジェクトのエラーが継続する場合
**応急処置**:
1. サーバーとターゲットを同じPythonバージョンに統一
2. 仮想環境を作り直す
```bash
# 古い環境を削除
rm -rf .venv
# 新しい環境を作成(同じバージョンで)
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## テスト
統合テストで動作確認:
```bash
# pythonPath サポートのテスト
uv run pytest tests/integration/test_python_path.py -v
# 全体のテスト
uv run pytest tests/integration/ -v
```
## 技術的詳細
### selectによるタイムアウト処理
```python
import select
ready, _, _ = select.select([process.stdout], [], [], timeout)
if ready:
response = process.stdout.readline()
else:
# Timeout handling
```
### プロセス終了の確認
```python
# subprocess.Popen の場合
if process.poll() is not None:
# プロセスは終了している
# multiprocessing.Process の場合(旧実装)
if not process.is_alive():
# プロセスは終了している
```
### グレースフル終了
```python
# 1. 終了コマンドを送信
terminate_cmd = json.dumps({"command": "terminate"}) + '\n'
process.stdin.write(terminate_cmd)
process.wait(timeout=5)
# 2. SIGTERM
process.terminate()
process.wait(timeout=5)
# 3. SIGKILL(最終手段)
process.kill()
process.wait()
```
## 制限事項
1. **Python 3.11 以上が必須**
- サーバー自体は 3.11+ が必要
- ターゲットも 3.11+ 推奨(古いバージョンは動作保証外)
2. **クロスプラットフォーム**
- `select.select()` は Windows で制限あり
- Windows では将来的に `asyncio` への移行が必要
3. **依存関係**
- ターゲットの Python に `pydantic` が必要
- デバッグ対象のコードが使用するパッケージも必要
## 今後の改善
- [ ] Windows サポート(asyncio subprocess)
- [ ] 自動的な仮想環境検出
- [ ] Python バージョン互換性チェック
- [ ] 依存関係の自動インストール