thonny-codemate 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thonny_codemate-0.1.0.dist-info/METADATA +307 -0
- thonny_codemate-0.1.0.dist-info/RECORD +27 -0
- thonny_codemate-0.1.0.dist-info/WHEEL +5 -0
- thonny_codemate-0.1.0.dist-info/licenses/LICENSE +21 -0
- thonny_codemate-0.1.0.dist-info/top_level.txt +1 -0
- thonnycontrib/__init__.py +1 -0
- thonnycontrib/thonny_codemate/__init__.py +397 -0
- thonnycontrib/thonny_codemate/api.py +154 -0
- thonnycontrib/thonny_codemate/context_manager.py +296 -0
- thonnycontrib/thonny_codemate/external_providers.py +714 -0
- thonnycontrib/thonny_codemate/i18n.py +506 -0
- thonnycontrib/thonny_codemate/llm_client.py +841 -0
- thonnycontrib/thonny_codemate/message_virtualization.py +136 -0
- thonnycontrib/thonny_codemate/model_manager.py +515 -0
- thonnycontrib/thonny_codemate/performance_monitor.py +141 -0
- thonnycontrib/thonny_codemate/prompts.py +102 -0
- thonnycontrib/thonny_codemate/ui/__init__.py +1 -0
- thonnycontrib/thonny_codemate/ui/chat_view.py +687 -0
- thonnycontrib/thonny_codemate/ui/chat_view_html.py +1299 -0
- thonnycontrib/thonny_codemate/ui/custom_prompt_dialog.py +175 -0
- thonnycontrib/thonny_codemate/ui/markdown_renderer.py +484 -0
- thonnycontrib/thonny_codemate/ui/model_download_dialog.py +355 -0
- thonnycontrib/thonny_codemate/ui/settings_dialog.py +1218 -0
- thonnycontrib/thonny_codemate/utils/__init__.py +25 -0
- thonnycontrib/thonny_codemate/utils/constants.py +138 -0
- thonnycontrib/thonny_codemate/utils/error_messages.py +92 -0
- thonnycontrib/thonny_codemate/utils/unified_error_handler.py +310 -0
@@ -0,0 +1,296 @@
|
|
1
|
+
"""
|
2
|
+
コンテキストマネージャー
|
3
|
+
プロジェクト内の複数ファイルのコンテキストを管理し、LLMに提供
|
4
|
+
"""
|
5
|
+
import os
|
6
|
+
import logging
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import List, Dict, Optional, Set
|
9
|
+
from dataclasses import dataclass
|
10
|
+
import ast
|
11
|
+
|
12
|
+
from thonny import get_workbench
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class FileContext:
|
19
|
+
"""ファイルのコンテキスト情報"""
|
20
|
+
path: str
|
21
|
+
content: str
|
22
|
+
imports: List[str]
|
23
|
+
functions: List[str]
|
24
|
+
classes: List[str]
|
25
|
+
is_current: bool = False
|
26
|
+
|
27
|
+
|
28
|
+
class ContextManager:
|
29
|
+
"""
|
30
|
+
プロジェクトのコンテキストを管理するクラス
|
31
|
+
関連ファイルを特定し、LLMに適切なコンテキストを提供
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(self, max_files: int = 5, max_file_size: int = 10000):
|
35
|
+
self.max_files = max_files
|
36
|
+
self.max_file_size = max_file_size
|
37
|
+
self._file_cache: Dict[str, FileContext] = {}
|
38
|
+
|
39
|
+
def get_project_context(self, current_file: Optional[str] = None) -> List[FileContext]:
|
40
|
+
"""
|
41
|
+
現在のプロジェクトのコンテキストを取得(現在のファイルのみ)
|
42
|
+
|
43
|
+
Args:
|
44
|
+
current_file: 現在編集中のファイルパス
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
現在のファイルのコンテキストリスト
|
48
|
+
"""
|
49
|
+
contexts = []
|
50
|
+
|
51
|
+
# 現在のファイルを取得
|
52
|
+
if not current_file:
|
53
|
+
editor = get_workbench().get_editor_notebook().get_current_editor()
|
54
|
+
if editor:
|
55
|
+
current_file = editor.get_filename()
|
56
|
+
|
57
|
+
if not current_file:
|
58
|
+
return contexts
|
59
|
+
|
60
|
+
current_path = Path(current_file)
|
61
|
+
|
62
|
+
# 現在のファイルのコンテキストのみを追加
|
63
|
+
current_context = self._analyze_file(current_path)
|
64
|
+
if current_context:
|
65
|
+
current_context.is_current = True
|
66
|
+
contexts.append(current_context)
|
67
|
+
|
68
|
+
return contexts
|
69
|
+
|
70
|
+
def _find_project_root(self, current_path: Path) -> Path:
|
71
|
+
"""プロジェクトのルートディレクトリを検索"""
|
72
|
+
# 一般的なプロジェクトマーカーを探す
|
73
|
+
markers = ['.git', 'pyproject.toml', 'setup.py', 'requirements.txt', '.venv', 'venv']
|
74
|
+
|
75
|
+
path = current_path.parent
|
76
|
+
while path != path.parent:
|
77
|
+
for marker in markers:
|
78
|
+
if (path / marker).exists():
|
79
|
+
return path
|
80
|
+
path = path.parent
|
81
|
+
|
82
|
+
# マーカーが見つからない場合は現在のファイルの親ディレクトリ
|
83
|
+
return current_path.parent
|
84
|
+
|
85
|
+
def _find_related_files(self, current_file: Path, project_root: Path) -> List[Path]:
|
86
|
+
"""関連ファイルを検索"""
|
87
|
+
related_files = []
|
88
|
+
current_imports = set()
|
89
|
+
|
90
|
+
# 現在のファイルのインポートを解析
|
91
|
+
try:
|
92
|
+
if current_file.suffix == '.py':
|
93
|
+
with open(current_file, 'r', encoding='utf-8') as f:
|
94
|
+
tree = ast.parse(f.read())
|
95
|
+
current_imports = self._extract_imports(tree)
|
96
|
+
except Exception as e:
|
97
|
+
logger.warning(f"Failed to parse imports from {current_file}: {e}")
|
98
|
+
|
99
|
+
# プロジェクト内のPythonファイルを検索(深さ制限付き)
|
100
|
+
max_depth = 3 # 最大3階層まで
|
101
|
+
processed_count = 0
|
102
|
+
max_files_to_check = 100 # 最大100ファイルまでチェック
|
103
|
+
|
104
|
+
def find_python_files(path: Path, depth: int = 0):
|
105
|
+
nonlocal processed_count
|
106
|
+
if depth > max_depth or processed_count > max_files_to_check:
|
107
|
+
return
|
108
|
+
|
109
|
+
try:
|
110
|
+
for item in path.iterdir():
|
111
|
+
if processed_count > max_files_to_check:
|
112
|
+
break
|
113
|
+
|
114
|
+
# 除外するディレクトリ
|
115
|
+
if item.is_dir():
|
116
|
+
if item.name in ['__pycache__', '.venv', 'venv', '.git', 'node_modules', '.pytest_cache']:
|
117
|
+
continue
|
118
|
+
find_python_files(item, depth + 1)
|
119
|
+
|
120
|
+
# Pythonファイルの処理
|
121
|
+
elif item.suffix == '.py' and item != current_file:
|
122
|
+
processed_count += 1
|
123
|
+
|
124
|
+
# ファイルサイズチェック
|
125
|
+
if item.stat().st_size > self.max_file_size:
|
126
|
+
continue
|
127
|
+
|
128
|
+
yield item
|
129
|
+
except PermissionError:
|
130
|
+
pass
|
131
|
+
|
132
|
+
for py_file in find_python_files(project_root):
|
133
|
+
|
134
|
+
# インポート関係をチェック
|
135
|
+
try:
|
136
|
+
module_name = self._path_to_module(py_file, project_root)
|
137
|
+
if module_name in current_imports:
|
138
|
+
related_files.append(py_file)
|
139
|
+
continue
|
140
|
+
|
141
|
+
# 逆方向のインポートもチェック
|
142
|
+
with open(py_file, 'r', encoding='utf-8') as f:
|
143
|
+
tree = ast.parse(f.read())
|
144
|
+
file_imports = self._extract_imports(tree)
|
145
|
+
|
146
|
+
current_module = self._path_to_module(current_file, project_root)
|
147
|
+
if current_module in file_imports:
|
148
|
+
related_files.append(py_file)
|
149
|
+
except Exception as e:
|
150
|
+
logger.debug(f"Failed to analyze {py_file}: {e}")
|
151
|
+
|
152
|
+
# 同じディレクトリのファイルを優先
|
153
|
+
same_dir_files = [f for f in current_file.parent.glob("*.py")
|
154
|
+
if f != current_file and f.stat().st_size <= self.max_file_size]
|
155
|
+
|
156
|
+
# 関連度順にソート(同じディレクトリ → インポート関係)
|
157
|
+
sorted_files = []
|
158
|
+
for f in same_dir_files:
|
159
|
+
if f not in related_files:
|
160
|
+
sorted_files.append(f)
|
161
|
+
sorted_files.extend(related_files)
|
162
|
+
|
163
|
+
return sorted_files
|
164
|
+
|
165
|
+
def _analyze_file(self, file_path: Path) -> Optional[FileContext]:
|
166
|
+
"""ファイルを解析してコンテキストを作成"""
|
167
|
+
# キャッシュチェック
|
168
|
+
cache_key = str(file_path)
|
169
|
+
if cache_key in self._file_cache:
|
170
|
+
cached = self._file_cache[cache_key]
|
171
|
+
# ファイルが変更されていない場合はキャッシュを使用
|
172
|
+
if file_path.stat().st_mtime <= os.path.getmtime(cache_key):
|
173
|
+
return cached
|
174
|
+
|
175
|
+
try:
|
176
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
177
|
+
content = f.read()
|
178
|
+
|
179
|
+
# ASTを解析
|
180
|
+
tree = ast.parse(content)
|
181
|
+
|
182
|
+
imports = list(self._extract_imports(tree))
|
183
|
+
functions = []
|
184
|
+
classes = []
|
185
|
+
|
186
|
+
for node in ast.walk(tree):
|
187
|
+
if isinstance(node, ast.FunctionDef):
|
188
|
+
# トップレベル関数のみ
|
189
|
+
if isinstance(node, ast.FunctionDef) and node.col_offset == 0:
|
190
|
+
functions.append(node.name)
|
191
|
+
elif isinstance(node, ast.ClassDef):
|
192
|
+
classes.append(node.name)
|
193
|
+
|
194
|
+
context = FileContext(
|
195
|
+
path=str(file_path),
|
196
|
+
content=content,
|
197
|
+
imports=imports,
|
198
|
+
functions=functions,
|
199
|
+
classes=classes
|
200
|
+
)
|
201
|
+
|
202
|
+
# キャッシュに保存
|
203
|
+
self._file_cache[cache_key] = context
|
204
|
+
|
205
|
+
return context
|
206
|
+
|
207
|
+
except Exception as e:
|
208
|
+
logger.warning(f"Failed to analyze file {file_path}: {e}")
|
209
|
+
return None
|
210
|
+
|
211
|
+
def _extract_imports(self, tree: ast.AST) -> Set[str]:
|
212
|
+
"""ASTからインポートを抽出"""
|
213
|
+
imports = set()
|
214
|
+
|
215
|
+
for node in ast.walk(tree):
|
216
|
+
if isinstance(node, ast.Import):
|
217
|
+
for alias in node.names:
|
218
|
+
imports.add(alias.name.split('.')[0])
|
219
|
+
elif isinstance(node, ast.ImportFrom):
|
220
|
+
if node.module:
|
221
|
+
imports.add(node.module.split('.')[0])
|
222
|
+
|
223
|
+
return imports
|
224
|
+
|
225
|
+
def _path_to_module(self, file_path: Path, project_root: Path) -> str:
|
226
|
+
"""ファイルパスをモジュール名に変換"""
|
227
|
+
try:
|
228
|
+
relative = file_path.relative_to(project_root)
|
229
|
+
parts = relative.with_suffix('').parts
|
230
|
+
return '.'.join(parts)
|
231
|
+
except ValueError:
|
232
|
+
return file_path.stem
|
233
|
+
|
234
|
+
def format_context_for_llm(self, contexts: List[FileContext]) -> str:
|
235
|
+
"""LLM用にコンテキストをフォーマット"""
|
236
|
+
if not contexts:
|
237
|
+
return ""
|
238
|
+
|
239
|
+
formatted = []
|
240
|
+
|
241
|
+
# 現在のファイル
|
242
|
+
current = next((c for c in contexts if c.is_current), None)
|
243
|
+
if current:
|
244
|
+
formatted.append(f"=== Current File: {current.path} ===")
|
245
|
+
formatted.append(self._format_file_summary(current))
|
246
|
+
formatted.append("")
|
247
|
+
|
248
|
+
# 関連ファイル
|
249
|
+
related = [c for c in contexts if not c.is_current]
|
250
|
+
if related:
|
251
|
+
formatted.append("=== Related Files ===")
|
252
|
+
for context in related:
|
253
|
+
formatted.append(f"\n--- {context.path} ---")
|
254
|
+
formatted.append(self._format_file_summary(context))
|
255
|
+
|
256
|
+
return '\n'.join(formatted)
|
257
|
+
|
258
|
+
def _format_file_summary(self, context: FileContext) -> str:
|
259
|
+
"""ファイルのサマリーをフォーマット"""
|
260
|
+
lines = []
|
261
|
+
|
262
|
+
if context.imports:
|
263
|
+
lines.append(f"Imports: {', '.join(context.imports[:10])}")
|
264
|
+
|
265
|
+
if context.classes:
|
266
|
+
lines.append(f"Classes: {', '.join(context.classes)}")
|
267
|
+
|
268
|
+
if context.functions:
|
269
|
+
lines.append(f"Functions: {', '.join(context.functions[:10])}")
|
270
|
+
|
271
|
+
# コードの最初の部分を含める(コメントやdocstringを含む可能性)
|
272
|
+
code_lines = context.content.split('\n')
|
273
|
+
preview_lines = []
|
274
|
+
for i, line in enumerate(code_lines[:20]):
|
275
|
+
if line.strip() and not line.strip().startswith('#'):
|
276
|
+
preview_lines.append(line)
|
277
|
+
if len(preview_lines) >= 5:
|
278
|
+
break
|
279
|
+
|
280
|
+
if preview_lines:
|
281
|
+
lines.append("\nCode preview:")
|
282
|
+
lines.extend(preview_lines)
|
283
|
+
|
284
|
+
return '\n'.join(lines)
|
285
|
+
|
286
|
+
def get_context_summary(self) -> Dict[str, any]:
|
287
|
+
"""現在のコンテキストのサマリーを取得"""
|
288
|
+
contexts = self.get_project_context()
|
289
|
+
|
290
|
+
return {
|
291
|
+
"total_files": len(contexts),
|
292
|
+
"current_file": next((c.path for c in contexts if c.is_current), None),
|
293
|
+
"related_files": [c.path for c in contexts if not c.is_current],
|
294
|
+
"total_classes": sum(len(c.classes) for c in contexts),
|
295
|
+
"total_functions": sum(len(c.functions) for c in contexts),
|
296
|
+
}
|