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.
Files changed (27) hide show
  1. thonny_codemate-0.1.0.dist-info/METADATA +307 -0
  2. thonny_codemate-0.1.0.dist-info/RECORD +27 -0
  3. thonny_codemate-0.1.0.dist-info/WHEEL +5 -0
  4. thonny_codemate-0.1.0.dist-info/licenses/LICENSE +21 -0
  5. thonny_codemate-0.1.0.dist-info/top_level.txt +1 -0
  6. thonnycontrib/__init__.py +1 -0
  7. thonnycontrib/thonny_codemate/__init__.py +397 -0
  8. thonnycontrib/thonny_codemate/api.py +154 -0
  9. thonnycontrib/thonny_codemate/context_manager.py +296 -0
  10. thonnycontrib/thonny_codemate/external_providers.py +714 -0
  11. thonnycontrib/thonny_codemate/i18n.py +506 -0
  12. thonnycontrib/thonny_codemate/llm_client.py +841 -0
  13. thonnycontrib/thonny_codemate/message_virtualization.py +136 -0
  14. thonnycontrib/thonny_codemate/model_manager.py +515 -0
  15. thonnycontrib/thonny_codemate/performance_monitor.py +141 -0
  16. thonnycontrib/thonny_codemate/prompts.py +102 -0
  17. thonnycontrib/thonny_codemate/ui/__init__.py +1 -0
  18. thonnycontrib/thonny_codemate/ui/chat_view.py +687 -0
  19. thonnycontrib/thonny_codemate/ui/chat_view_html.py +1299 -0
  20. thonnycontrib/thonny_codemate/ui/custom_prompt_dialog.py +175 -0
  21. thonnycontrib/thonny_codemate/ui/markdown_renderer.py +484 -0
  22. thonnycontrib/thonny_codemate/ui/model_download_dialog.py +355 -0
  23. thonnycontrib/thonny_codemate/ui/settings_dialog.py +1218 -0
  24. thonnycontrib/thonny_codemate/utils/__init__.py +25 -0
  25. thonnycontrib/thonny_codemate/utils/constants.py +138 -0
  26. thonnycontrib/thonny_codemate/utils/error_messages.py +92 -0
  27. 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
+ }