From d73b4f07a072e20f8d385c74e6e79ca9e2737274 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 17 Apr 2026 23:26:18 +0800 Subject: [PATCH 1/3] Enforce CLAUDE.md compliance: lint cleanup and type annotations Fix 31 ruff violations (unused imports, semicolons, inline statements, f-string noise, module-level import order) and add return/parameter type annotations across 63 files covering utils, code_scan, git_client, and all pyside_ui layers. Public APIs, private helpers, __init__ and parameters now carry explicit types (Qt event classes, QModelIndex, Callable, specific widget/model types rather than bare None). --- je_editor/code_scan/ruff_thread.py | 4 +- je_editor/code_scan/watchdog_implement.py | 8 +- je_editor/code_scan/watchdog_thread.py | 8 +- je_editor/git_client/git_action.py | 31 +++---- je_editor/git_client/git_cli.py | 2 +- .../browser/browser_download_window.py | 8 +- .../browser/browser_serach_lineedit.py | 5 +- je_editor/pyside_ui/browser/browser_view.py | 8 +- je_editor/pyside_ui/browser/browser_widget.py | 6 +- .../pyside_ui/browser/main_browser_widget.py | 10 +-- .../code/auto_save/auto_save_manager.py | 2 +- .../code/auto_save/auto_save_thread.py | 12 +-- .../pyside_ui/code/base_process_manager.py | 10 +-- .../pyside_ui/code/code_format/pep8_format.py | 7 +- .../pyside_ui/code/code_process/code_exec.py | 8 +- .../code_edit_plaintext.py | 62 +++++++------- .../pyside_ui/code/running_process_manager.py | 6 +- .../code/shell_process/shell_exec.py | 7 +- .../pyside_ui/code/syntax/python_syntax.py | 6 +- .../code/textedit_code_result/code_record.py | 2 +- .../code/variable_inspector/inspector_gui.py | 26 +++--- .../dialog/ai_dialog/set_ai_dialog.py | 4 +- .../dialog/file_dialog/create_file_dialog.py | 4 +- .../dialog/search_ui/search_error_box.py | 2 +- .../dialog/search_ui/search_replace_widget.py | 57 +++++++------ .../dialog/search_ui/search_text_box.py | 2 +- .../code_diff_viewer_widget.py | 6 +- .../line_number_code_viewer.py | 24 +++--- .../multi_file_diff_viewer.py | 10 +-- .../side_by_side_diff_widget.py | 52 ++++++------ .../git_ui/git_client/commit_table.py | 6 +- .../git_client/git_branch_tree_widget.py | 14 ++-- .../git_ui/git_client/git_client_gui.py | 81 ++++++++++--------- .../pyside_ui/git_ui/git_client/graph_view.py | 19 ++--- .../pyside_ui/main_ui/ai_widget/ai_config.py | 2 +- .../pyside_ui/main_ui/ai_widget/ask_thread.py | 4 +- .../pyside_ui/main_ui/ai_widget/chat_ui.py | 12 +-- .../main_ui/ai_widget/langchain_interface.py | 2 +- .../main_ui/console_widget/console_gui.py | 30 ++++--- .../console_widget/qprocess_adapter.py | 18 ++--- .../pyside_ui/main_ui/dock/destroy_dock.py | 5 +- .../pyside_ui/main_ui/editor/editor_widget.py | 14 ++-- .../main_ui/editor/editor_widget_dock.py | 5 +- .../pyside_ui/main_ui/editor/process_input.py | 12 +-- .../main_ui/ipython_widget/ipython_console.py | 4 +- je_editor/pyside_ui/main_ui/main_editor.py | 18 ++--- .../main_ui/menu/dock_menu/build_dock_menu.py | 2 +- .../main_ui/menu/file_menu/build_file_menu.py | 2 +- .../main_ui/menu/help_menu/build_help_menu.py | 4 +- .../menu/plugin_menu/build_plugin_menu.py | 10 +-- .../menu/python_env_menu/build_venv_menu.py | 4 +- .../menu/run_menu/under_run_menu/utils.py | 2 +- .../menu/tab_menu/build_tab_git_menu.py | 6 +- .../main_ui/menu/tab_menu/build_tab_menu.py | 6 +- .../menu/tab_menu/build_tab_tools_menu.py | 8 +- .../main_ui/menu/text_menu/build_text_menu.py | 4 +- .../plugin_browser/plugin_browser_widget.py | 26 +++--- .../save_settings/user_color_setting_file.py | 2 +- .../main_ui/system_tray/extend_system_tray.py | 6 +- .../main_ui/toolbar/toolbar_builder.py | 35 ++++---- je_editor/utils/json_format/json_process.py | 5 +- je_editor/utils/logging/loggin_instance.py | 4 +- .../multi_language/multi_language_wrapper.py | 4 +- .../redirect_manager_class.py | 10 +-- 64 files changed, 396 insertions(+), 389 deletions(-) diff --git a/je_editor/code_scan/ruff_thread.py b/je_editor/code_scan/ruff_thread.py index 1055fe6..acfaef0 100644 --- a/je_editor/code_scan/ruff_thread.py +++ b/je_editor/code_scan/ruff_thread.py @@ -10,7 +10,7 @@ class RuffThread(threading.Thread): 使用子執行緒執行 Ruff (Python 程式碼檢查/格式化工具)。 """ - def __init__(self, ruff_commands: list, std_queue: Queue, stderr_queue: Queue): + def __init__(self, ruff_commands: list, std_queue: Queue, stderr_queue: Queue) -> None: """ Initialize the RuffThread. 初始化 RuffThread。 @@ -32,7 +32,7 @@ def __init__(self, ruff_commands: list, std_queue: Queue, stderr_queue: Queue): self.std_queue = std_queue self.stderr_queue = stderr_queue - def run(self): + def run(self) -> None: """ Run the Ruff process in a separate thread. 在子執行緒中執行 Ruff 程式。 diff --git a/je_editor/code_scan/watchdog_implement.py b/je_editor/code_scan/watchdog_implement.py index 048b215..fd3a481 100644 --- a/je_editor/code_scan/watchdog_implement.py +++ b/je_editor/code_scan/watchdog_implement.py @@ -2,7 +2,7 @@ from queue import Queue from typing import Dict -from watchdog.events import FileSystemEventHandler +from watchdog.events import FileSystemEvent, FileSystemEventHandler from je_editor.code_scan.ruff_thread import RuffThread @@ -13,7 +13,7 @@ class RuffPythonFileChangeHandler(FileSystemEventHandler): 當 Python 檔案被修改時,自動觸發 Ruff 檢查。 """ - def __init__(self, ruff_commands: list = None, debounce_interval: float = 1.0): + def __init__(self, ruff_commands: list = None, debounce_interval: float = 1.0) -> None: """ :param ruff_commands: Ruff command list, e.g. ["ruff", "check"] :param debounce_interval: Minimum interval (seconds) between re-runs for the same file @@ -27,14 +27,14 @@ def __init__(self, ruff_commands: list = None, debounce_interval: float = 1.0): self.last_run_time: Dict[str, float] = {} self.debounce_interval = debounce_interval - def _start_new_thread(self, file_path: str): + def _start_new_thread(self, file_path: str) -> None: """Helper to start a new Ruff thread for a given file.""" ruff_thread = RuffThread(self.ruff_commands, self.stdout_queue, self.stderr_queue) self.ruff_threads_dict[file_path] = ruff_thread self.last_run_time[file_path] = time.time() ruff_thread.start() - def on_modified(self, event): + def on_modified(self, event: FileSystemEvent) -> None: """Triggered when a file is modified.""" if event.is_directory: return diff --git a/je_editor/code_scan/watchdog_thread.py b/je_editor/code_scan/watchdog_thread.py index 94ee98a..cc733c7 100644 --- a/je_editor/code_scan/watchdog_thread.py +++ b/je_editor/code_scan/watchdog_thread.py @@ -14,7 +14,7 @@ class WatchdogThread(threading.Thread): 使用 watchdog 監控檔案變化的執行緒。 """ - def __init__(self, check_path: str): + def __init__(self, check_path: str) -> None: """ :param check_path: Path to monitor (directory or file) 要監控的路徑(資料夾或檔案) @@ -25,7 +25,7 @@ def __init__(self, check_path: str): self.running = True self.observer = Observer() - def run(self): + def run(self) -> None: """Start the watchdog observer loop.""" if not self.check_path.exists(): print(f"[Error] Path does not exist: {self.check_path}", file=sys.stderr) @@ -48,11 +48,11 @@ def run(self): self.observer.join() print("[Watchdog] Monitoring stopped") - def stop(self): + def stop(self) -> None: """Stop the watchdog thread safely.""" self.running = False - def _process_ruff_output(self): + def _process_ruff_output(self) -> None: """Process stdout/stderr queues from Ruff threads.""" while not self.ruff_handler.stdout_queue.empty(): line = self.ruff_handler.stdout_queue.get() diff --git a/je_editor/git_client/git_action.py b/je_editor/git_client/git_action.py index 50a9e2e..8fccdb8 100644 --- a/je_editor/git_client/git_action.py +++ b/je_editor/git_client/git_action.py @@ -1,12 +1,13 @@ import os from datetime import datetime +from typing import Any, Callable from PySide6.QtCore import QThread, Signal from git import Repo, GitCommandError, InvalidGitRepositoryError, NoSuchPathError # Simple audit logger -def audit_log(repo_path: str, action: str, detail: str, ok: bool, err: str = ""): +def audit_log(repo_path: str, action: str, detail: str, ok: bool, err: str = "") -> None: """ Append an audit log entry to 'audit.log' in the repo directory. This is useful for compliance and traceability. @@ -27,11 +28,11 @@ class GitService: Keeps UI logic separate from Git logic. """ - def __init__(self): + def __init__(self) -> None: self.repo: Repo | None = None self.repo_path: str | None = None - def open_repo(self, path: str): + def open_repo(self, path: str) -> None: try: self.repo = Repo(path) self.repo_path = path @@ -40,20 +41,20 @@ def open_repo(self, path: str): audit_log(path, "open_repo", path, False, str(e)) raise - def list_branches(self): + def list_branches(self) -> list[str]: self._ensure_repo() branches = [head.name for head in self.repo.heads] audit_log(self.repo_path, "list_branches", ",".join(branches), True) return branches - def current_branch(self): + def current_branch(self) -> str: self._ensure_repo() try: return self.repo.active_branch.name except TypeError: return "(detached HEAD)" - def checkout(self, branch: str): + def checkout(self, branch: str) -> None: self._ensure_repo() try: self.repo.git.checkout(branch) @@ -62,7 +63,7 @@ def checkout(self, branch: str): audit_log(self.repo_path, "checkout", branch, False, str(e)) raise - def list_commits(self, branch: str, max_count: int = 100): + def list_commits(self, branch: str, max_count: int = 100) -> list[dict]: self._ensure_repo() commits = list(self.repo.iter_commits(branch, max_count=max_count)) data = [ @@ -96,7 +97,7 @@ def show_diff_of_commit(self, commit_sha: str) -> str: audit_log(self.repo_path, "show_diff", commit_sha, True) return out - def stage_all(self): + def stage_all(self) -> None: self._ensure_repo() try: self.repo.git.add(all=True) @@ -105,7 +106,7 @@ def stage_all(self): audit_log(self.repo_path, "stage_all", "git_client add -A", False, str(e)) raise - def commit(self, message: str): + def commit(self, message: str) -> None: self._ensure_repo() if not message.strip(): raise ValueError("Commit message is empty.") @@ -116,7 +117,7 @@ def commit(self, message: str): audit_log(self.repo_path, "commit", message, False, str(e)) raise - def pull(self, remote: str = "origin", branch: str | None = None): + def pull(self, remote: str = "origin", branch: str | None = None) -> str: self._ensure_repo() if branch is None: branch = self.current_branch() @@ -128,7 +129,7 @@ def pull(self, remote: str = "origin", branch: str | None = None): audit_log(self.repo_path, "pull", f"{remote}/{branch}", False, str(e)) raise - def push(self, remote: str = "origin", branch: str | None = None): + def push(self, remote: str = "origin", branch: str | None = None) -> str: self._ensure_repo() if branch is None: branch = self.current_branch() @@ -140,11 +141,11 @@ def push(self, remote: str = "origin", branch: str | None = None): audit_log(self.repo_path, "push", f"{remote}/{branch}", False, str(e)) raise - def remotes(self): + def remotes(self) -> list[str]: self._ensure_repo() return [r.name for r in self.repo.remotes] - def _ensure_repo(self): + def _ensure_repo(self) -> None: if self.repo is None: raise RuntimeError("Repository not opened.") @@ -161,13 +162,13 @@ class GitWorker(QThread): """ done = Signal(object, object) - def __init__(self, fn, *args, **kwargs): + def __init__(self, fn: Callable, *args: Any, **kwargs: Any) -> None: super().__init__() self.fn = fn self.args = args self.kwargs = kwargs - def run(self): + def run(self) -> None: try: res = self.fn(*self.args, **self.kwargs) self.done.emit(res, None) diff --git a/je_editor/git_client/git_cli.py b/je_editor/git_client/git_cli.py index 8979078..331c9ce 100644 --- a/je_editor/git_client/git_cli.py +++ b/je_editor/git_client/git_cli.py @@ -7,7 +7,7 @@ class GitCLI: - def __init__(self, repo_path: Path): + def __init__(self, repo_path: Path) -> None: self.repo_path = Path(repo_path) def is_git_repo(self) -> bool: diff --git a/je_editor/pyside_ui/browser/browser_download_window.py b/je_editor/pyside_ui/browser/browser_download_window.py index 79cfdc0..387d7fd 100644 --- a/je_editor/pyside_ui/browser/browser_download_window.py +++ b/je_editor/pyside_ui/browser/browser_download_window.py @@ -12,7 +12,7 @@ class BrowserDownloadWindow(QWidget): 瀏覽器下載視窗,用來顯示下載的詳細資訊。 """ - def __init__(self, download_instance: QWebEngineDownloadRequest): + def __init__(self, download_instance: QWebEngineDownloadRequest) -> None: """ Initialize the download window with a given QWebEngineDownloadRequest. 使用指定的 QWebEngineDownloadRequest 初始化下載視窗。 @@ -50,7 +50,7 @@ def __init__(self, download_instance: QWebEngineDownloadRequest): self.box_layout.addWidget(self.show_download_detail_plaintext) self.setLayout(self.box_layout) - def print_finish(self): + def print_finish(self) -> None: """ Slot function triggered when download finishes. 當下載完成時觸發,將完成狀態輸出到 logger 與文字框。 @@ -58,7 +58,7 @@ def print_finish(self): jeditor_logger.info("BrowserDownloadWindow Print Download is Finished") self.show_download_detail_plaintext.appendPlainText(str(self.download_instance.isFinished())) - def print_interrupt(self): + def print_interrupt(self) -> None: """ Slot function triggered when download is interrupted. 當下載被中斷時觸發,將中斷原因輸出到 logger 與文字框。 @@ -66,7 +66,7 @@ def print_interrupt(self): jeditor_logger.info("BrowserDownloadWindow Print interruptReason") self.show_download_detail_plaintext.appendPlainText(str(self.download_instance.interruptReason())) - def print_state(self): + def print_state(self) -> None: """ Slot function triggered when download state changes. 當下載狀態改變時觸發,將狀態輸出到 logger 與文字框。 diff --git a/je_editor/pyside_ui/browser/browser_serach_lineedit.py b/je_editor/pyside_ui/browser/browser_serach_lineedit.py index 676e784..8d472a4 100644 --- a/je_editor/pyside_ui/browser/browser_serach_lineedit.py +++ b/je_editor/pyside_ui/browser/browser_serach_lineedit.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from PySide6.QtCore import Qt +from PySide6.QtGui import QKeyEvent from PySide6.QtWidgets import QLineEdit from je_editor.utils.logging.loggin_instance import jeditor_logger @@ -19,7 +20,7 @@ class BrowserLineSearch(QLineEdit): 自訂的 QLineEdit,用於瀏覽器搜尋輸入。 """ - def __init__(self, browser_widget: BrowserWidget): + def __init__(self, browser_widget: BrowserWidget) -> None: """ Initialize the search line with a reference to the browser widget. 初始化搜尋輸入框,並保存瀏覽器元件的參考。 @@ -35,7 +36,7 @@ def __init__(self, browser_widget: BrowserWidget): # 保存瀏覽器元件的參考,用於觸發搜尋 self.browser = browser_widget - def keyPressEvent(self, event) -> None: + def keyPressEvent(self, event: QKeyEvent) -> None: """ Handle key press events. 當使用者按下按鍵時觸發: diff --git a/je_editor/pyside_ui/browser/browser_view.py b/je_editor/pyside_ui/browser/browser_view.py index 52c014a..5d68b7d 100644 --- a/je_editor/pyside_ui/browser/browser_view.py +++ b/je_editor/pyside_ui/browser/browser_view.py @@ -7,6 +7,8 @@ if TYPE_CHECKING: from je_editor.pyside_ui.browser.browser_widget import BrowserWidget +from PySide6.QtCore import QObject +from PySide6.QtGui import QCloseEvent from PySide6.QtWebEngineCore import QWebEngineDownloadRequest from PySide6.QtWebEngineWidgets import QWebEngineView @@ -22,7 +24,7 @@ class BrowserView(QWebEngineView): """ def __init__(self, start_url: str = "https://www.google.com/", - main_widget: BrowserWidget = None, parent=None): + main_widget: BrowserWidget = None, parent: QObject | None = None) -> None: """ Initialize the browser view with a start URL. 使用指定的起始網址初始化瀏覽器視圖。 @@ -46,7 +48,7 @@ def __init__(self, start_url: str = "https://www.google.com/", self.main_widget = main_widget - def download_file(self, download_instance: QWebEngineDownloadRequest): + def download_file(self, download_instance: QWebEngineDownloadRequest) -> None: """ Handle a new download request. 當有新的下載請求時觸發: @@ -73,7 +75,7 @@ def download_file(self, download_instance: QWebEngineDownloadRequest): self.download_window_list.append(download_detail_window) download_detail_window.show() - def closeEvent(self, event) -> None: + def closeEvent(self, event: QCloseEvent) -> None: """ Handle the close event of the browser view. 當瀏覽器視窗關閉時: diff --git a/je_editor/pyside_ui/browser/browser_widget.py b/je_editor/pyside_ui/browser/browser_widget.py index c608ca6..d926979 100644 --- a/je_editor/pyside_ui/browser/browser_widget.py +++ b/je_editor/pyside_ui/browser/browser_widget.py @@ -19,7 +19,7 @@ class BrowserWidget(QWidget): def __init__(self, start_url: str = "https://www.google.com/", search_prefix: str = "https://www.google.com.tw/search?q=", - main_widget: MainBrowserWidget = None, browser_view: BrowserView = None): + main_widget: MainBrowserWidget = None, browser_view: BrowserView = None) -> None: # --- Browser setting / 瀏覽器設定 --- super().__init__() self.main_widget = main_widget @@ -71,7 +71,7 @@ def __init__(self, start_url: str = "https://www.google.com/", self.grid_layout.addWidget(self.browser, 1, 0, -1, -1) self.setLayout(self.grid_layout) - def search(self): + def search(self) -> None: """ Perform a search using the text in the input line. 使用輸入框的文字進行搜尋,將字串附加到 search_prefix 後送出。 @@ -81,7 +81,7 @@ def search(self): # Append input text to search prefix and set as browser URL self.browser.setUrl(f"{self.search_prefix}{self.url_input.text()}") - def find_text(self): + def find_text(self) -> None: """ Open a dialog to find text in the current page. 開啟輸入對話框,在當前頁面中搜尋文字。 diff --git a/je_editor/pyside_ui/browser/main_browser_widget.py b/je_editor/pyside_ui/browser/main_browser_widget.py index 9384861..6c01ea8 100644 --- a/je_editor/pyside_ui/browser/main_browser_widget.py +++ b/je_editor/pyside_ui/browser/main_browser_widget.py @@ -11,7 +11,7 @@ class MainBrowserWidget(QWidget): """ def __init__(self, start_url: str = "https://www.google.com/", - search_prefix: str = "https://www.google.com.tw/search?q="): + search_prefix: str = "https://www.google.com.tw/search?q=") -> None: super().__init__() # 初始化時記錄訊息 (方便除錯) # Log initialization info (for debugging) @@ -40,7 +40,7 @@ def __init__(self, start_url: str = "https://www.google.com/", grid_layout.addWidget(self.browser_tab) # 把分頁加入佈局 / Add tab widget to layout self.setLayout(grid_layout) # 設定主視窗佈局 / Set main layout - def add_browser_tab(self, browser_widget: BrowserWidget, title: str = "New Tab"): + def add_browser_tab(self, browser_widget: BrowserWidget, title: str = "New Tab") -> None: # 在「+」分頁前插入新分頁 # Insert new tab before the "+" tab plus_index = self.browser_tab.count() - 1 @@ -54,7 +54,7 @@ def add_browser_tab(self, browser_widget: BrowserWidget, title: str = "New Tab") self.browser_tab.indexOf(bw), t) ) - def add_plus_tab(self): + def add_plus_tab(self) -> None: """新增一個固定的「+」分頁 / Add a fixed "+" tab""" add_new_page_widget = QWidget() self.browser_tab.addTab(add_new_page_widget, "+") @@ -64,7 +64,7 @@ def add_plus_tab(self): # 點擊分頁時觸發事件 / Connect tab click event self.browser_tab.tabBar().tabBarClicked.connect(self.handle_tab_changed) - def handle_tab_changed(self, index: int): + def handle_tab_changed(self, index: int) -> None: """如果點到「+」分頁,就開新分頁 If "+" tab is clicked, open a new tab """ @@ -77,7 +77,7 @@ def handle_tab_changed(self, index: int): "Google" ) - def close_tab(self, index: int): + def close_tab(self, index: int) -> None: """關閉指定分頁,但保留「+」 Close the specified tab, but keep the "+" """ diff --git a/je_editor/pyside_ui/code/auto_save/auto_save_manager.py b/je_editor/pyside_ui/code/auto_save/auto_save_manager.py index b881c39..8336e25 100644 --- a/je_editor/pyside_ui/code/auto_save/auto_save_manager.py +++ b/je_editor/pyside_ui/code/auto_save/auto_save_manager.py @@ -22,7 +22,7 @@ file_is_open_manager_dict: dict = dict() -def init_new_auto_save_thread(file_path: str, widget: EditorWidget): +def init_new_auto_save_thread(file_path: str, widget: EditorWidget) -> None: """ 初始化新的自動儲存執行緒 Initialize a new auto-save thread for the given file and editor widget. diff --git a/je_editor/pyside_ui/code/auto_save/auto_save_thread.py b/je_editor/pyside_ui/code/auto_save/auto_save_thread.py index e7b7259..dd62820 100644 --- a/je_editor/pyside_ui/code/auto_save/auto_save_thread.py +++ b/je_editor/pyside_ui/code/auto_save/auto_save_thread.py @@ -1,9 +1,9 @@ import time from pathlib import Path from threading import Thread, Event -from typing import Union +from typing import Callable, Union -from PySide6.QtCore import QObject, Signal, Slot, QTimer +from PySide6.QtCore import QObject, Qt, Signal, Slot from je_editor.pyside_ui.code.plaintext_code_edit.code_edit_plaintext import CodeEditor from je_editor.utils.file.save.save_file import write_file @@ -18,7 +18,7 @@ class _TextFetcher(QObject): text_fetched = Signal(str) fetch_requested = Signal() - def __init__(self, editor: CodeEditor, parent=None): + def __init__(self, editor: CodeEditor, parent: QObject | None = None) -> None: super().__init__(parent) self._editor = editor self._pending_text: Union[str, None] = None @@ -47,10 +47,6 @@ def fetch(self) -> Union[str, None]: return self._pending_text -# Need Qt import for ConnectionType -from PySide6.QtCore import Qt - - class CodeEditSaveThread(Thread): """ This thread is used to auto save current file. @@ -59,7 +55,7 @@ class CodeEditSaveThread(Thread): def __init__( self, file_to_save: Union[str, None] = None, editor: Union[None, CodeEditor] = None, - before_write_callback=None): + before_write_callback: Callable | None = None) -> None: jeditor_logger.info(f"Init CodeEditSaveThread " f"file_to_save: {file_to_save} " f"editor: {editor}") diff --git a/je_editor/pyside_ui/code/base_process_manager.py b/je_editor/pyside_ui/code/base_process_manager.py index 004cf07..02686ef 100644 --- a/je_editor/pyside_ui/code/base_process_manager.py +++ b/je_editor/pyside_ui/code/base_process_manager.py @@ -7,7 +7,7 @@ from PySide6.QtCore import QTimer from PySide6.QtGui import QTextCharFormat -from PySide6.QtWidgets import QTextEdit +from PySide6.QtWidgets import QTextEdit, QWidget from je_editor.pyside_ui.code.running_process_manager import run_instance_manager from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict @@ -22,10 +22,10 @@ class BaseProcessManager: def __init__( self, - main_window=None, + main_window: Union[QWidget, None] = None, encoding: str = "utf-8", buffer_size: int = 1024, - ): + ) -> None: self.read_program_error_output_from_thread: Union[Thread, None] = None self.read_program_output_from_thread: Union[Thread, None] = None self.main_window = main_window @@ -44,7 +44,7 @@ def still_running(self) -> bool: return self._still_running @still_running.setter - def still_running(self, value: bool): + def still_running(self, value: bool) -> None: self._still_running = value def _start_reader_threads(self) -> None: @@ -154,7 +154,7 @@ def exit_program(self) -> None: cleanup_thread.start() @staticmethod - def _cleanup_in_background(threads: list, process) -> None: + def _cleanup_in_background(threads: list, process: subprocess.Popen | None) -> None: """在背景執行清理工作 / Perform cleanup work in background""" for t in threads: try: diff --git a/je_editor/pyside_ui/code/code_format/pep8_format.py b/je_editor/pyside_ui/code/code_format/pep8_format.py index 9a479f7..8828dd4 100644 --- a/je_editor/pyside_ui/code/code_format/pep8_format.py +++ b/je_editor/pyside_ui/code/code_format/pep8_format.py @@ -1,4 +1,5 @@ import tokenize +from typing import Any import pycodestyle @@ -6,7 +7,7 @@ class PEP8FormatChecker(pycodestyle.Checker): - def __init__(self, filename: str, **kwargs): + def __init__(self, filename: str, **kwargs: Any) -> None: """ 自訂的 PEP8 格式檢查器,繼承自 pycodestyle.Checker。 Custom PEP8 format checker, inherits from pycodestyle.Checker. @@ -41,7 +42,7 @@ def __init__(self, filename: str, **kwargs): # 儲存錯誤訊息的清單 / List to store error messages self.error_list: list = list() - def replace_report_error(self, line_number, offset, text, check): + def replace_report_error(self, line_number: int, offset: int, text: str, check: Any) -> None: """ 自訂錯誤回報方法,過濾掉特定錯誤 (例如 W191)。 Custom error reporting method, filters out specific errors (e.g., W191). @@ -55,7 +56,7 @@ def replace_report_error(self, line_number, offset, text, check): if not text.startswith("W191"): self.error_list.append(f"{text} on line: {line_number}, offset: {offset}") - def check_all_format(self, expected=None, line_offset=0) -> int: + def check_all_format(self, expected: Any = None, line_offset: int = 0) -> int: """ 執行所有格式檢查。 Run all checks on the input file. diff --git a/je_editor/pyside_ui/code/code_process/code_exec.py b/je_editor/pyside_ui/code/code_process/code_exec.py index 2af1117..900a363 100644 --- a/je_editor/pyside_ui/code/code_process/code_exec.py +++ b/je_editor/pyside_ui/code/code_process/code_exec.py @@ -31,7 +31,7 @@ def __init__( program_language: str = "python", program_encoding: str = "utf-8", program_buffer: int = 1024, - ): + ) -> None: jeditor_logger.info(f"Init ExecManager " f"main_window: {main_window} " f"program_language: {program_language} " @@ -52,7 +52,7 @@ def still_run_program(self) -> bool: return self.still_running @still_run_program.setter - def still_run_program(self, value: bool): + def still_run_program(self, value: bool) -> None: self.still_running = value def renew_path(self) -> None: @@ -72,7 +72,7 @@ def later_init(self) -> None: else: raise JEditorException(je_editor_init_error) - def exec_code(self, exec_file_name, exec_prefix: Union[str, list] = None) -> None: + def exec_code(self, exec_file_name: str, exec_prefix: Union[str, list] = None) -> None: """ 執行指定檔案 Execute given file @@ -214,7 +214,7 @@ def exec_with_plugin_config(self, exec_file_name: str, run_config: dict) -> None if self.process is not None: self.process.terminate() - def full_exit_program(self): + def full_exit_program(self) -> None: """完全結束程式 / Fully exit program""" jeditor_logger.info("ExecManager full_exit_program") if self.timer is not None: diff --git a/je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py b/je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py index 35e8fa8..99bc05d 100644 --- a/je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py +++ b/je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py @@ -1,17 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING - -from je_editor.utils.logging.loggin_instance import jeditor_logger - -# 僅在型別檢查時匯入,避免循環引用 -# Only imported for type checking, avoids circular imports -if TYPE_CHECKING: - from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget - from je_editor.pyside_ui.main_ui.editor.editor_widget_dock import FullEditorWidget - -from typing import Union, List +from typing import TYPE_CHECKING, Union, List import jedi # Python 自動補全與靜態分析工具 from PySide6 import QtGui @@ -21,21 +11,32 @@ QTextDocument, QTextCursor, QTextOption, QColor, QWheelEvent ) from PySide6.QtWidgets import QPlainTextEdit, QWidget, QTextEdit, QCompleter, QInputDialog -from jedi.api.classes import Completion + +from je_editor.pyside_ui.code.syntax.python_syntax import PythonHighlighter +from je_editor.pyside_ui.dialog.search_ui.search_text_box import SearchBox +from je_editor.pyside_ui.dialog.search_ui.search_replace_widget import SearchReplaceDialog +from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict +from je_editor.utils.logging.loggin_instance import jeditor_logger + +# 僅在型別檢查時匯入,避免循環引用 +# Only imported for type checking, avoids circular imports +if TYPE_CHECKING: + from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget + from je_editor.pyside_ui.main_ui.editor.editor_widget_dock import FullEditorWidget class _JediCompleteWorker(QObject): """背景執行 Jedi 自動補全 / Run Jedi autocomplete in background thread""" finished = Signal(list) # list of completion names - def __init__(self, code: str, line: int, column: int, env=None): + def __init__(self, code: str, line: int, column: int, env: jedi.api.environment.Environment | None = None) -> None: super().__init__() self._code = code self._line = line self._column = column self._env = env - def run(self): + def run(self) -> None: try: if self._env is not None: script = jedi.Script(code=self._code, environment=self._env) @@ -47,13 +48,8 @@ def run(self): except Exception: self.finished.emit([]) -from je_editor.pyside_ui.code.syntax.python_syntax import PythonHighlighter -from je_editor.pyside_ui.dialog.search_ui.search_text_box import SearchBox -from je_editor.pyside_ui.dialog.search_ui.search_replace_widget import SearchReplaceDialog -from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict - -def venv_check(): +def venv_check() -> Path: """檢查當前工作目錄下是否有 venv 資料夾 / Check if venv exists in current working directory""" jeditor_logger.info("code_edit_plaintext.py venv check") venv_path = Path.cwd() / "venv" @@ -73,7 +69,7 @@ class CodeEditor(QPlainTextEdit): - 自動補全 (Autocomplete with Jedi) """ - def __init__(self, main_window: Union[EditorWidget, FullEditorWidget]): + def __init__(self, main_window: Union[EditorWidget, FullEditorWidget]) -> None: jeditor_logger.info(f"Init CodeEditor main_window: {main_window}") super().__init__() @@ -162,13 +158,13 @@ def __init__(self, main_window: Union[EditorWidget, FullEditorWidget]): self._bracket_open = set('([{') self.cursorPositionChanged.connect(self._highlight_matching_bracket) - def reset_highlighter(self): + def reset_highlighter(self) -> None: """重設語法高亮 / Reset syntax highlighter""" jeditor_logger.info("CodeEditor reset_highlighter") self.highlighter = PythonHighlighter(self.document(), main_window=self) self.highlight_current_line() - def check_env(self): + def check_env(self) -> None: """檢查虛擬環境並建立 Jedi 環境 / Check venv and create Jedi environment""" jeditor_logger.info("CodeEditor check_env") path = venv_check() @@ -189,7 +185,7 @@ def set_complete(self, list_to_complete: list) -> None: completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self.completer = completer - def insert_completion(self, completion) -> None: + def insert_completion(self, completion: str) -> None: """ 插入補全文字 Insert completion text into editor @@ -205,13 +201,13 @@ def insert_completion(self, completion) -> None: self.setTextCursor(text_cursor) @property - def text_under_cursor(self): + def text_under_cursor(self) -> str: """取得游標下的文字 / Get text under cursor""" text_cursor = self.textCursor() text_cursor.select(QTextCursor.SelectionType.WordUnderCursor) return text_cursor.selectedText() - def focusInEvent(self, e) -> None: + def focusInEvent(self, e: QtGui.QFocusEvent) -> None: """當編輯器獲得焦點時,確保 completer 綁定正確""" if self.completer: self.completer.setWidget(self) @@ -314,7 +310,7 @@ def find_back_text(self) -> None: text = self.search_box.search_input.text() self.find(text, QTextDocument.FindFlag.FindBackward) - def line_number_paint(self, event) -> None: + def line_number_paint(self, event: QtGui.QPaintEvent) -> None: """ 繪製行號區域 Paint line number area @@ -356,14 +352,14 @@ def line_number_width(self) -> int: space = 12 * digits return space - def update_line_number_area_width(self, value) -> None: + def update_line_number_area_width(self, value: int) -> None: """ 更新行號區域寬度 Update line number area width """ self.setViewportMargins(self.line_number_width(), 0, 0, 0) - def resizeEvent(self, event) -> None: + def resizeEvent(self, event: QtGui.QResizeEvent) -> None: """ 視窗大小改變時,調整行號區域 Resize line number paint area @@ -374,7 +370,7 @@ def resizeEvent(self, event) -> None: QRect(cr.left(), cr.top(), self.line_number_width(), cr.height()), ) - def update_line_number_area(self, rect, dy) -> None: + def update_line_number_area(self, rect: QRect, dy: int) -> None: """ 更新行號顯示 Update line number area @@ -811,7 +807,7 @@ def keyPressEvent(self, event: QKeyEvent) -> None: self.completer.popup().close() self._complete_timer.start() - def mousePressEvent(self, event) -> None: + def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: """ 滑鼠點擊事件 Mouse press event @@ -826,12 +822,12 @@ class LineNumber(QWidget): Widget used to paint line numbers """ - def __init__(self, editor): + def __init__(self, editor: CodeEditor) -> None: jeditor_logger.info("Init LineNumber") QWidget.__init__(self, parent=editor) self.editor = editor - def paintEvent(self, event) -> None: + def paintEvent(self, event: QtGui.QPaintEvent) -> None: """ 呼叫編輯器的 line_number_paint 來繪製行號 Delegate painting to CodeEditor.line_number_paint diff --git a/je_editor/pyside_ui/code/running_process_manager.py b/je_editor/pyside_ui/code/running_process_manager.py index c42d221..2dfdbc4 100644 --- a/je_editor/pyside_ui/code/running_process_manager.py +++ b/je_editor/pyside_ui/code/running_process_manager.py @@ -19,13 +19,13 @@ class RunInstanceManager(object): Manager for ExecManager and ShellManager instances """ - def __init__(self): + def __init__(self) -> None: # 初始化,建立一個空的實例清單 # Initialize with an empty instance list jeditor_logger.info("Init RunInstanceManager") self.instance_list: List[Union[ExecManager, ShellManager]] = list() - def remove_instance(self, instance) -> None: + def remove_instance(self, instance: Union[ExecManager, ShellManager]) -> None: """ 從清單中移除已結束的實例 Remove a finished instance from the list @@ -35,7 +35,7 @@ def remove_instance(self, instance) -> None: except ValueError: pass - def close_all_instance(self): + def close_all_instance(self) -> None: """ 關閉所有執行中的實例,透過各自的 exit_program 正確清理 timer、thread 與 process Close all running instances via their own exit_program for proper cleanup diff --git a/je_editor/pyside_ui/code/shell_process/shell_exec.py b/je_editor/pyside_ui/code/shell_process/shell_exec.py index 7c770c5..7f56e3a 100644 --- a/je_editor/pyside_ui/code/shell_process/shell_exec.py +++ b/je_editor/pyside_ui/code/shell_process/shell_exec.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Union, Callable -from PySide6.QtCore import QTimer from PySide6.QtGui import QTextCharFormat from PySide6.QtWidgets import QTextEdit @@ -27,7 +26,7 @@ def __init__( shell_encoding: str = "utf-8", program_buffer: int = 1024, after_done_function: Union[None, Callable] = None - ): + ) -> None: jeditor_logger.info(f"Init ShellManager " f"main_window: {main_window} " f"shell_encoding: {shell_encoding} " @@ -47,7 +46,7 @@ def still_run_shell(self) -> bool: return self.still_running @still_run_shell.setter - def still_run_shell(self, value: bool): + def still_run_shell(self, value: bool) -> None: self.still_running = value def renew_path(self) -> None: @@ -109,7 +108,7 @@ def exec_shell(self, shell_command: Union[str, list]) -> None: if self.process is not None: self.process.terminate() - def process_run_over(self): + def process_run_over(self) -> None: """當子程序結束時呼叫 / Called when subprocess finishes""" jeditor_logger.info("ShellManager process_run_over") if self.timer is not None: diff --git a/je_editor/pyside_ui/code/syntax/python_syntax.py b/je_editor/pyside_ui/code/syntax/python_syntax.py index 3031ab2..af6735b 100644 --- a/je_editor/pyside_ui/code/syntax/python_syntax.py +++ b/je_editor/pyside_ui/code/syntax/python_syntax.py @@ -10,7 +10,7 @@ from PySide6.QtCore import QRegularExpression from PySide6.QtGui import QSyntaxHighlighter -from PySide6.QtGui import QTextCharFormat +from PySide6.QtGui import QTextCharFormat, QTextDocument # 匯入語法設定,包括關鍵字、規則、擴展設定 # Import syntax settings: keywords, rules, and extended settings @@ -28,7 +28,7 @@ class PythonHighlighter(QSyntaxHighlighter): Python syntax highlighter class, inherits from QSyntaxHighlighter """ - def __init__(self, parent=None, main_window: CodeEditor = None): + def __init__(self, parent: QTextDocument | None = None, main_window: CodeEditor = None) -> None: jeditor_logger.info(f"Init PythonHighlighter parent: {parent}") super().__init__(parent) @@ -105,7 +105,7 @@ def __init__(self, parent=None, main_window: CodeEditor = None): pattern = QRegularExpression(rf"\b{word}\b") self.highlight_rules.append((pattern, text_char_format)) - def highlightBlock(self, text) -> None: + def highlightBlock(self, text: str) -> None: """ 對每一行文字進行語法高亮 Apply syntax highlighting to each block of text diff --git a/je_editor/pyside_ui/code/textedit_code_result/code_record.py b/je_editor/pyside_ui/code/textedit_code_result/code_record.py index 023cf22..126c24a 100644 --- a/je_editor/pyside_ui/code/textedit_code_result/code_record.py +++ b/je_editor/pyside_ui/code/textedit_code_result/code_record.py @@ -14,7 +14,7 @@ class CodeRecord(QTextEdit): # 繼承自 QTextEdit,作為程式碼輸出紀錄區 # Extend QTextEdit, used as a code output record area - def __init__(self): + def __init__(self) -> None: jeditor_logger.info("Init CodeRecord") super().__init__() self.setLineWrapMode(self.LineWrapMode.NoWrap) # 禁止自動換行 / disable line wrapping diff --git a/je_editor/pyside_ui/code/variable_inspector/inspector_gui.py b/je_editor/pyside_ui/code/variable_inspector/inspector_gui.py index 43e9a42..289500f 100644 --- a/je_editor/pyside_ui/code/variable_inspector/inspector_gui.py +++ b/je_editor/pyside_ui/code/variable_inspector/inspector_gui.py @@ -1,6 +1,8 @@ import ast -from PySide6.QtCore import QAbstractTableModel, Qt, QTimer, QSortFilterProxyModel +from typing import Any + +from PySide6.QtCore import QAbstractTableModel, QModelIndex, QObject, Qt, QTimer, QSortFilterProxyModel from PySide6.QtWidgets import QTableView, QVBoxLayout, QWidget, QLineEdit, QLabel from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper @@ -12,7 +14,7 @@ class VariableModel(QAbstractTableModel): Variable model: manages and displays variables from a given namespace """ - def __init__(self, parent=None, namespace: dict | None = None): + def __init__(self, parent: QObject | None = None, namespace: dict | None = None) -> None: super().__init__(parent) self.variables = [] # 儲存變數資訊 [名稱, 型別, 值字串, 真實值] # Store variable info [name, type, repr(value), actual value] @@ -23,11 +25,11 @@ def namespace(self) -> dict: return self._namespace @namespace.setter - def namespace(self, ns: dict): + def namespace(self, ns: dict) -> None: self._namespace = ns self.update_data() - def update_data(self): + def update_data(self) -> None: """ 更新變數清單,從指定命名空間中擷取 Update variable list from the provided namespace @@ -48,15 +50,15 @@ def update_data(self): ] self.endResetModel() - def rowCount(self, parent=None): + def rowCount(self, parent: QModelIndex | None = None) -> int: # 回傳變數數量 / return number of variables return len(self.variables) - def columnCount(self, parent=None): + def columnCount(self, parent: QModelIndex | None = None) -> int: # 固定三欄:名稱、型別、值 / fixed 3 columns: name, type, value return 3 - def data(self, index, role=Qt.ItemDataRole.DisplayRole): + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: # 提供表格顯示的資料 # Provide data for table display if not index.isValid() or not (0 <= index.row() < len(self.variables)): @@ -65,7 +67,7 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): return self.variables[index.row()][index.column()] return None - def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any: # 設定表頭文字 (支援多語系) # Set header labels (multi-language support) if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: @@ -76,7 +78,7 @@ def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): ][section] return None - def flags(self, index): + def flags(self, index: QModelIndex) -> Qt.ItemFlag: # 設定欄位屬性,僅允許「值」欄可編輯 # Set column flags, only "value" column is editable if not index.isValid() or not (0 <= index.row() < len(self.variables)): @@ -86,7 +88,7 @@ def flags(self, index): base |= Qt.ItemFlag.ItemIsEditable return base - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + def setData(self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole) -> bool: """ 更新變數值,並同步到全域變數 Update variable value and sync to globals() @@ -129,7 +131,7 @@ class VariableProxy(QSortFilterProxyModel): Proxy model: supports filtering and forwards editing """ - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + def setData(self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole) -> bool: # 將編輯操作轉發到原始模型 # Forward editing to source model return self.sourceModel().setData( @@ -143,7 +145,7 @@ class VariableInspector(QWidget): Variable inspector: GUI interface to display and search global variables """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle(language_wrapper.language_word_dict.get("variable_inspector_title")) layout = QVBoxLayout(self) diff --git a/je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py b/je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py index e05df6e..0fe0c7e 100644 --- a/je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py +++ b/je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py @@ -11,7 +11,7 @@ class SetAIDialog(QWidget): Dialog for configuring AI model settings """ - def __init__(self): + def __init__(self) -> None: jeditor_logger.info("Init SetAIDialog") super().__init__() @@ -46,7 +46,7 @@ def __init__(self): self.setWindowTitle(language_wrapper.language_word_dict.get("add_ai_model_title")) self.setLayout(self.grid_layout) - def update_ai_config(self): + def update_ai_config(self) -> None: """ 更新 AI 設定,將使用者輸入的 base_url、api_key、chat_model 儲存到 ai_config.choosable_ai 中 diff --git a/je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py b/je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py index 33ba0c3..03c6b56 100644 --- a/je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py +++ b/je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py @@ -12,7 +12,7 @@ class CreateFileDialog(QWidget): Dialog for creating a new file """ - def __init__(self): + def __init__(self) -> None: jeditor_logger.info("Init CreateFileDialog") super().__init__() @@ -40,7 +40,7 @@ def __init__(self): self.setWindowTitle(language_wrapper.language_word_dict.get("create_file_dialog_pushbutton")) self.setLayout(self.box_layout) - def create_file(self): + def create_file(self) -> None: """ 建立檔案的邏輯: 1. 檢查輸入是否為空 diff --git a/je_editor/pyside_ui/dialog/search_ui/search_error_box.py b/je_editor/pyside_ui/dialog/search_ui/search_error_box.py index 57141fa..d837e9a 100644 --- a/je_editor/pyside_ui/dialog/search_ui/search_error_box.py +++ b/je_editor/pyside_ui/dialog/search_ui/search_error_box.py @@ -10,7 +10,7 @@ class SearchResultBox(QWidget): Search result dialog box """ - def __init__(self): + def __init__(self) -> None: jeditor_logger.info("Init SearchResultBox") super().__init__() diff --git a/je_editor/pyside_ui/dialog/search_ui/search_replace_widget.py b/je_editor/pyside_ui/dialog/search_ui/search_replace_widget.py index f2e2814..2dd7dc1 100644 --- a/je_editor/pyside_ui/dialog/search_ui/search_replace_widget.py +++ b/je_editor/pyside_ui/dialog/search_ui/search_replace_widget.py @@ -5,15 +5,15 @@ from pathlib import Path from typing import TYPE_CHECKING, Optional -from PySide6.QtCore import Qt, QThread, Signal +from PySide6.QtCore import Qt, QThread, QRegularExpression, Signal from PySide6.QtGui import QTextCursor, QTextDocument +from PySide6.QtGui import QCloseEvent from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QComboBox, QCheckBox, QLabel, QTreeWidget, QTreeWidgetItem, - QFileDialog, QPlainTextEdit, QMessageBox + QFileDialog, QPlainTextEdit, QMessageBox, QWidget ) -from je_editor.utils.logging.loggin_instance import jeditor_logger from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper if TYPE_CHECKING: @@ -42,7 +42,7 @@ class _SearchWorker(QThread): match_found = Signal(str, int, str) # (file_path, line_number, line_text) finished_signal = Signal(int) # total_matches - def __init__(self, root: str, pattern: str, case_sensitive: bool, use_regex: bool): + def __init__(self, root: str, pattern: str, case_sensitive: bool, use_regex: bool) -> None: super().__init__() self.root = root self.pattern = pattern @@ -50,10 +50,10 @@ def __init__(self, root: str, pattern: str, case_sensitive: bool, use_regex: boo self.use_regex = use_regex self._stop = False - def stop(self): + def stop(self) -> None: self._stop = True - def run(self): + def run(self) -> None: total = 0 flags = 0 if self.case_sensitive else re.IGNORECASE try: @@ -92,7 +92,7 @@ class SearchReplaceDialog(QDialog): Search & Replace dialog supporting three scopes: Current File, Folder, Project """ - def __init__(self, editor_widget: EditorWidget, parent=None): + def __init__(self, editor_widget: EditorWidget, parent: QWidget | None = None) -> None: super().__init__(parent) self.editor_widget = editor_widget self._worker: Optional[_SearchWorker] = None @@ -107,7 +107,7 @@ def __init__(self, editor_widget: EditorWidget, parent=None): self._init_ui() - def _init_ui(self): + def _init_ui(self) -> None: layout = QVBoxLayout(self) layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(6) @@ -200,14 +200,14 @@ def _init_ui(self): # ---------- Scope UI ---------- - def _on_scope_changed(self, index: int): + def _on_scope_changed(self, index: int) -> None: """根據範圍切換顯示/隱藏資料夾選擇 / Toggle folder picker visibility""" scope = self.scope_combo.currentData() is_folder = scope == "folder" self.folder_input.setVisible(is_folder) self.btn_pick_folder.setVisible(is_folder) - def _pick_folder(self): + def _pick_folder(self) -> None: """選擇搜尋資料夾 / Pick search folder""" d = QFileDialog.getExistingDirectory(self, self._lang("search_replace_pick_folder"), os.getcwd()) if d: @@ -215,7 +215,7 @@ def _pick_folder(self): # ---------- Search ---------- - def do_search(self): + def do_search(self) -> None: """根據範圍執行搜尋 / Execute search based on scope""" pattern = self.search_input.text() if not pattern: @@ -234,7 +234,7 @@ def do_search(self): project_root = self._get_project_root() self._search_in_directory(pattern, project_root) - def _search_in_current_file(self, pattern: str): + def _search_in_current_file(self, pattern: str) -> None: """在目前檔案中搜尋 / Search in current file""" self.result_tree.clear() editor = self.editor_widget.code_edit @@ -264,7 +264,7 @@ def _search_in_current_file(self, pattern: str): # 同時高亮第一個匹配 / Also highlight first match self.find_next() - def _search_in_directory(self, pattern: str, root: str): + def _search_in_directory(self, pattern: str, root: str) -> None: """在資料夾中搜尋 / Search in directory""" self.result_tree.clear() self.status_label.setText(self._lang("search_replace_searching")) @@ -282,7 +282,7 @@ def _search_in_directory(self, pattern: str, root: str): self._worker.finished_signal.connect(self._on_search_finished) self._worker.start() - def _on_match_found(self, file_path: str, line_no: int, line_text: str): + def _on_match_found(self, file_path: str, line_no: int, line_text: str) -> None: """收到搜尋結果 / Receive search result""" if file_path not in self._file_groups: parent = QTreeWidgetItem([file_path, "", ""]) @@ -295,12 +295,12 @@ def _on_match_found(self, file_path: str, line_no: int, line_text: str): child.setData(0, Qt.ItemDataRole.UserRole, {"file": file_path, "line": line_no}) parent.addChild(child) - def _on_search_finished(self, total: int): + def _on_search_finished(self, total: int) -> None: """搜尋完成 / Search finished""" self.status_label.setText( self._lang("search_replace_found_count").format(count=total)) - def _on_result_double_click(self, item: QTreeWidgetItem, column: int): + def _on_result_double_click(self, item: QTreeWidgetItem, column: int) -> None: """雙擊結果項目:跳轉到對應位置 / Double-click result: navigate to location""" data = item.data(0, Qt.ItemDataRole.UserRole) if not data: @@ -321,7 +321,7 @@ def _on_result_double_click(self, item: QTreeWidgetItem, column: int): # 目前檔案 / Current file self._goto_line(self.editor_widget.code_edit, line_no) - def _goto_line(self, editor: QPlainTextEdit, line_no: int): + def _goto_line(self, editor: QPlainTextEdit, line_no: int) -> None: """跳轉到指定行 / Navigate to specific line""" block = editor.document().findBlockByNumber(line_no - 1) if block.isValid(): @@ -333,15 +333,15 @@ def _goto_line(self, editor: QPlainTextEdit, line_no: int): # ---------- Find prev/next in current file ---------- - def find_next(self): + def find_next(self) -> None: """在目前檔案中尋找下一個 / Find next in current file""" self._find_in_editor(forward=True) - def find_prev(self): + def find_prev(self) -> None: """在目前檔案中尋找上一個 / Find previous in current file""" self._find_in_editor(forward=False) - def _find_in_editor(self, forward: bool): + def _find_in_editor(self, forward: bool) -> None: """在編輯器中搜尋 / Search in editor""" pattern = self.search_input.text() if not pattern: @@ -377,9 +377,8 @@ def _find_in_editor(self, forward: bool): else: editor.find(pattern, flags) - def _build_qregex(self, pattern: str): + def _build_qregex(self, pattern: str) -> Optional[QRegularExpression]: """建立 QRegularExpression / Build QRegularExpression""" - from PySide6.QtCore import QRegularExpression options = QRegularExpression.PatternOption.NoPatternOption if not self.chk_case.isChecked(): options |= QRegularExpression.PatternOption.CaseInsensitiveOption @@ -390,7 +389,7 @@ def _build_qregex(self, pattern: str): # ---------- Replace ---------- - def do_replace(self): + def do_replace(self) -> None: """取代目前選取的匹配項 / Replace current selected match""" scope = self.scope_combo.currentData() if scope == "file": @@ -398,7 +397,7 @@ def do_replace(self): else: self._replace_selected_result() - def do_replace_all(self): + def do_replace_all(self) -> None: """全部取代 / Replace all""" pattern = self.search_input.text() replacement = self.replace_input.text() @@ -418,7 +417,7 @@ def do_replace_all(self): project_root = self._get_project_root() self._replace_all_in_directory(pattern, replacement, project_root) - def _replace_in_current_editor(self): + def _replace_in_current_editor(self) -> None: """在編輯器中取代目前選取的匹配 / Replace currently selected match in editor""" editor = self.editor_widget.code_edit replacement = self.replace_input.text() @@ -429,7 +428,7 @@ def _replace_in_current_editor(self): # 跳到下一個匹配 / Move to next match self.find_next() - def _replace_all_in_current_file(self, pattern: str, replacement: str): + def _replace_all_in_current_file(self, pattern: str, replacement: str) -> None: """在目前檔案中全部取代 / Replace all in current file""" editor = self.editor_widget.code_edit text = editor.toPlainText() @@ -455,7 +454,7 @@ def _replace_all_in_current_file(self, pattern: str, replacement: str): self._lang("search_replace_replaced_count").format(count=count)) self.do_search() - def _replace_selected_result(self): + def _replace_selected_result(self) -> None: """取代結果列表中目前選取的項目 / Replace selected result item""" items = self.result_tree.selectedItems() if not items: @@ -502,7 +501,7 @@ def _replace_selected_result(self): except Exception as e: self.status_label.setText(f"Error: {e}") - def _replace_all_in_directory(self, pattern: str, replacement: str, root: str): + def _replace_all_in_directory(self, pattern: str, replacement: str, root: str) -> None: """在整個目錄中全部取代 / Replace all in directory""" case = self.chk_case.isChecked() use_regex = self.chk_regex.isChecked() @@ -568,7 +567,7 @@ def _get_project_root(self) -> str: return root return os.getcwd() - def closeEvent(self, event): + def closeEvent(self, event: QCloseEvent) -> None: """關閉對話框時停止搜尋執行緒 / Stop search worker on close""" if self._worker and self._worker.isRunning(): self._worker.stop() diff --git a/je_editor/pyside_ui/dialog/search_ui/search_text_box.py b/je_editor/pyside_ui/dialog/search_ui/search_text_box.py index 3d7c109..98caeb5 100644 --- a/je_editor/pyside_ui/dialog/search_ui/search_text_box.py +++ b/je_editor/pyside_ui/dialog/search_ui/search_text_box.py @@ -10,7 +10,7 @@ class SearchBox(QWidget): Search box widget """ - def __init__(self): + def __init__(self) -> None: jeditor_logger.info("Init SearchBox") super().__init__() diff --git a/je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py b/je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py index ecfd83a..e272f30 100644 --- a/je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py +++ b/je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py @@ -13,7 +13,7 @@ class DiffViewerWidget(QWidget): Git 差異檢視器應用程式 """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Git Diff Viewer") @@ -58,7 +58,7 @@ def __init__(self): layout.setMenuBar(self.menubar) # 把選單列放在上方 / put menu bar at the top layout.addWidget(self.viewer) # 把差異檢視器放在主要區域 / add diff viewer in main area - def open_repo(self): + def open_repo(self) -> None: """ Open a Git repository and display its diff. 開啟一個 Git 專案並顯示差異。 @@ -79,7 +79,7 @@ def open_repo(self): # 如果開啟失敗,顯示錯誤訊息 / show error if failed QMessageBox.critical(self, "Error", f"Failed to open repo:\n{e}") - def set_theme(self, mode: str): + def set_theme(self, mode: str) -> None: """ Switch between dark and light themes. 切換深色與淺色主題。 diff --git a/je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py b/je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py index c97aaa9..306aac9 100644 --- a/je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py +++ b/je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py @@ -1,5 +1,5 @@ from PySide6.QtCore import QRect, QSize, Qt -from PySide6.QtGui import QPainter, QColor +from PySide6.QtGui import QPainter, QColor, QPaintEvent, QResizeEvent from PySide6.QtWidgets import QPlainTextEdit, QWidget, QTextEdit @@ -9,18 +9,18 @@ class LineNumberArea(QWidget): 用來顯示行號的側邊元件。 """ - def __init__(self, editor): + def __init__(self, editor: QPlainTextEdit) -> None: super().__init__(editor) self.code_editor = editor # 綁定主要的編輯器 / Bind to main editor - def sizeHint(self): + def sizeHint(self) -> QSize: """ Suggest width for line number area. 建議行號區域的寬度。 """ return QSize(self.code_editor.line_number_area_width(), 0) - def paintEvent(self, event): + def paintEvent(self, event: QPaintEvent) -> None: """ Delegate paint event to the main editor. 將繪製事件交給主要編輯器處理。 @@ -34,7 +34,7 @@ class LineNumberedCodeViewer(QPlainTextEdit): 帶有行號顯示的文字編輯器。 """ - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.lineNumberArea = LineNumberArea(self) @@ -47,7 +47,7 @@ def __init__(self, parent=None): self.update_line_number_area_width(0) self.highlight_current_line() - def line_number_area_width(self): + def line_number_area_width(self) -> int: """ Calculate width of line number area based on digit count. 根據行數位數計算行號區寬度。 @@ -56,14 +56,14 @@ def line_number_area_width(self): space = 3 + self.fontMetrics().horizontalAdvance('9') * digits return space - def update_line_number_area_width(self, _): + def update_line_number_area_width(self, _: int) -> None: """ Adjust viewport margins to fit line number area. 調整視口邊界以容納行號區。 """ self.setViewportMargins(self.line_number_area_width(), 0, 0, 0) - def update_line_number_area(self, rect, dy): + def update_line_number_area(self, rect: QRect, dy: int) -> None: """ Update/redraw line number area when scrolling or editing. 當滾動或編輯時更新行號區。 @@ -75,7 +75,7 @@ def update_line_number_area(self, rect, dy): if rect.contains(self.viewport().rect()): self.update_line_number_area_width(0) - def resizeEvent(self, event): + def resizeEvent(self, event: QResizeEvent) -> None: """ Resize line number area when editor resizes. 編輯器大小改變時調整行號區。 @@ -84,7 +84,7 @@ def resizeEvent(self, event): cr = self.contentsRect() self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) - def line_number_area_paint_event(self, event): + def line_number_area_paint_event(self, event: QPaintEvent) -> None: """ Paint line numbers. 繪製行號。 @@ -112,7 +112,7 @@ def line_number_area_paint_event(self, event): bottom = top + int(self.blockBoundingRect(block).height()) blockNumber += 1 - def highlight_current_line(self): + def highlight_current_line(self) -> None: """ Highlight the current line where the cursor is. 高亮顯示游標所在的行。 @@ -134,7 +134,7 @@ def highlight_current_line(self): self.setExtraSelections(merged) - def apply_theme_to_editor(self, dark: bool): + def apply_theme_to_editor(self, dark: bool) -> None: self.setStyleSheet("QPlainTextEdit { background-color: #1e1e1e; color: #d4d4d4; }" if dark else "QPlainTextEdit { background-color: white; color: black; }") # Re-trigger current line highlight with the new color diff --git a/je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py b/je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py index 278daef..7015b4e 100644 --- a/je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py +++ b/je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py @@ -9,7 +9,7 @@ class MultiFileDiffViewer(QWidget): 多檔案差異檢視器,使用分頁 (tab) 來顯示每個檔案的差異。 """ - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: """ Initialize the multi-file diff viewer. 初始化多檔案差異檢視器。 @@ -24,7 +24,7 @@ def __init__(self, parent=None): layout = QVBoxLayout(self) layout.addWidget(self.tabs) - def set_diff_text(self, diff_text: str): + def set_diff_text(self, diff_text: str) -> None: """ Set the diff text and split it into per-file tabs. 設定差異文字,並依檔案拆分成多個分頁。 @@ -36,7 +36,7 @@ def set_diff_text(self, diff_text: str): viewer.set_diff_text(ftext) self.tabs.addTab(viewer, file_name) # 新增分頁,標題為檔名 - def _split_by_file(self, diff_text: str): + def _split_by_file(self, diff_text: str) -> list[tuple[str, str]]: """ Split a unified diff text into chunks per file. 將 unified diff 文字依檔案切分。 @@ -67,7 +67,7 @@ def _split_by_file(self, diff_text: str): return chunks - def set_dark_theme(self): + def set_dark_theme(self) -> None: """ Apply dark theme to all tabs. 對所有分頁套用深色主題。 @@ -77,7 +77,7 @@ def set_dark_theme(self): if isinstance(w, SideBySideDiffWidget): w.set_dark_theme() - def set_light_theme(self): + def set_light_theme(self) -> None: """ Apply light theme to all tabs. 對所有分頁套用淺色主題。 diff --git a/je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py b/je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py index ed5c08c..036dab2 100644 --- a/je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py +++ b/je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py @@ -10,7 +10,7 @@ class SideBySideDiffWidget(QWidget): 左右對照的差異檢視元件。 """ - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) # === 顏色設定 / Color configuration === @@ -65,7 +65,7 @@ def __init__(self, parent=None): # 預設深色模式 / Default to dark theme self.set_dark_theme() - def _sync_scrollbars(self): + def _sync_scrollbars(self) -> None: """ Synchronize scrollbars between left and right editors. 同步左右編輯器的捲軸。 @@ -83,7 +83,7 @@ def _sync_scrollbars(self): self.leftEdit.horizontalScrollBar().setValue ) - def set_diff_text(self, diff_text: str): + def set_diff_text(self, diff_text: str) -> None: """ Parse unified diff text and display it in side-by-side editors. 解析 unified diff 文字並顯示在左右編輯器。 @@ -101,7 +101,7 @@ def set_diff_text(self, diff_text: str): self.leftEdit.moveCursor(QTextCursor.MoveOperation.Start) self.rightEdit.moveCursor(QTextCursor.MoveOperation.Start) - def _set_text_with_highlights(self, edit: QPlainTextEdit, lines, marks): + def _set_text_with_highlights(self, edit: QPlainTextEdit, lines: list[str], marks: list[str]) -> None: """ Set text and apply syntax highlighting based on diff marks. 設定文字並依 diff 標記加上背景色。 @@ -140,7 +140,7 @@ def _set_text_with_highlights(self, edit: QPlainTextEdit, lines, marks): edit.setExtraSelections(merged) - def _line_selection(self, edit: QPlainTextEdit, line_index: int, fmt: QTextCharFormat): + def _line_selection(self, edit: QPlainTextEdit, line_index: int, fmt: QTextCharFormat) -> QTextEdit.ExtraSelection: """ Create a selection for a specific line with given format. 建立某一行的選取區並套用格式。 @@ -155,7 +155,7 @@ def _line_selection(self, edit: QPlainTextEdit, line_index: int, fmt: QTextCharF sel.cursor = cursor return sel - def _parse_unified_diff(self, diff_text: str): + def _parse_unified_diff(self, diff_text: str) -> tuple: """ Parse unified diff into left/right lines and marks. 將 unified diff 解析成左右行與標記。 @@ -163,15 +163,15 @@ def _parse_unified_diff(self, diff_text: str): left_lines, right_lines, left_marks, right_marks = [], [], [], [] left_name, right_name = None, None - def add_left(line, mark=None): + def add_left(line: str, mark: str | None = None) -> None: left_lines.append(line) left_marks.append(mark or "CTX") - def add_right(line, mark=None): + def add_right(line: str, mark: str | None = None) -> None: right_lines.append(line) right_marks.append(mark or "CTX") - def align(): + def align() -> None: # 對齊左右行數 / Align left and right line counts if len(left_lines) < len(right_lines): for _ in range(len(right_lines) - len(left_lines)): @@ -182,39 +182,39 @@ def align(): for raw in diff_text.splitlines(): if raw.startswith("diff "): - add_left(raw, "HDR"); - add_right(raw, "HDR"); + add_left(raw, "HDR") + add_right(raw, "HDR") align() elif raw.startswith("--- "): left_name = raw[4:].strip() - add_left(raw, "HDR"); - add_right("", "HDR"); + add_left(raw, "HDR") + add_right("", "HDR") align() elif raw.startswith("+++ "): right_name = raw[4:].strip() - add_left("", "HDR"); - add_right(raw, "HDR"); + add_left("", "HDR") + add_right(raw, "HDR") align() elif raw.startswith("@@"): - add_left(raw, "HUNK"); - add_right(raw, "HUNK"); + add_left(raw, "HUNK") + add_right(raw, "HUNK") align() elif raw.startswith("-"): - add_left(raw, "DEL"); - add_right("", None); + add_left(raw, "DEL") + add_right("", None) align() elif raw.startswith("+"): - add_left("", None); - add_right(raw, "ADD"); + add_left("", None) + add_right(raw, "ADD") align() else: - add_left(raw, None); - add_right(raw, None); + add_left(raw, None) + add_right(raw, None) align() return left_lines, right_lines, left_marks, right_marks, left_name, right_name - def _reapply_highlights_for_theme(self): + def _reapply_highlights_for_theme(self) -> None: """ Reapply highlights when theme changes. 主題切換時重新套用高亮。 @@ -253,7 +253,7 @@ def _reapply_highlights_for_theme(self): merged = updated edit.setExtraSelections(merged) - def set_dark_theme(self): + def set_dark_theme(self) -> None: """ Apply dark theme colors. 套用深色主題配色。 @@ -268,7 +268,7 @@ def set_dark_theme(self): self.leftEdit.apply_theme_to_editor(dark=self.is_dark) self.rightEdit.apply_theme_to_editor(dark=self.is_dark) - def set_light_theme(self): + def set_light_theme(self) -> None: """ Apply light theme colors. 套用淺色主題配色。 diff --git a/je_editor/pyside_ui/git_ui/git_client/commit_table.py b/je_editor/pyside_ui/git_ui/git_client/commit_table.py index 6cd0dab..62ca12b 100644 --- a/je_editor/pyside_ui/git_ui/git_client/commit_table.py +++ b/je_editor/pyside_ui/git_ui/git_client/commit_table.py @@ -1,5 +1,5 @@ from PySide6.QtGui import QStandardItemModel, QStandardItem -from PySide6.QtWidgets import QTableView +from PySide6.QtWidgets import QTableView, QWidget class CommitTable(QTableView): @@ -8,7 +8,7 @@ class CommitTable(QTableView): CommitTable class: A table view to display Git commit history """ - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) # 建立一個 QStandardItemModel,初始為 0 行 5 欄 @@ -35,7 +35,7 @@ def __init__(self, parent=None): # Stretch the last column to fill available space self.horizontalHeader().setStretchLastSection(True) - def set_commits(self, commits): + def set_commits(self, commits: list[dict]) -> None: """ 將 commit 資料填入表格 Populate the table with commit data diff --git a/je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py b/je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py index 97cf3b6..271e955 100644 --- a/je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py +++ b/je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py @@ -1,7 +1,7 @@ import logging from pathlib import Path -from PySide6.QtCore import QTimer, QFileSystemWatcher, Qt +from PySide6.QtCore import QItemSelection, QTimer, QFileSystemWatcher, Qt from PySide6.QtGui import QAction from PySide6.QtWidgets import ( QFileDialog, QToolBar, QMessageBox, QStatusBar, @@ -24,7 +24,7 @@ class GitTreeViewGUI(QWidget): GitTreeViewGUI: A graphical viewer for Git commit history """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.language_wrapper_get = language_wrapper.language_word_dict.get self.setWindowTitle(self.language_wrapper_get("git_graph_title")) @@ -79,7 +79,7 @@ def __init__(self): # === 表格選擇事件 / Table selection event === self.commit_table.selectionModel().selectionChanged.connect(self._on_table_selection) - def _on_table_selection(self, selected, _): + def _on_table_selection(self, selected: QItemSelection, _: QItemSelection) -> None: """ 當使用者在提交表格中選擇某一列時,讓圖形檢視器聚焦到該行 When user selects a row in commit table, focus graph view on that row @@ -89,7 +89,7 @@ def _on_table_selection(self, selected, _): row = selected.indexes()[0].row() self.graph_view.focus_row(row) - def open_repo(self): + def open_repo(self) -> None: """ 開啟 Git 專案並初始化圖形檢視 Open a Git repository and set up the graph view @@ -110,7 +110,7 @@ def open_repo(self): self._setup_watcher() self.refresh_graph() - def _setup_watcher(self): + def _setup_watcher(self) -> None: """ 設定檔案監控,監控 .git 目錄與相關檔案 Setup file watcher to monitor .git directory and related files @@ -130,14 +130,14 @@ def _setup_watcher(self): if refs_dir.exists(): self.watcher.addPath(str(refs_dir)) - def _on_git_changed(self): + def _on_git_changed(self) -> None: """ 當 Git 目錄變更時,啟動延遲刷新 When Git directory changes, start delayed refresh """ self.refresh_timer.start(500) - def refresh_graph(self): + def refresh_graph(self) -> None: """ 刷新 Git 提交圖與提交表格 Refresh Git commit graph and commit table diff --git a/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py b/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py index 9c53966..ef3e305 100644 --- a/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +++ b/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Any, Callable from PySide6.QtCore import Qt, QTimer, QThread, Signal, QObject from PySide6.QtGui import QTextOption, QTextCharFormat, QColor, QFont, QSyntaxHighlighter, QAction @@ -15,12 +16,12 @@ class _GitWorker(QObject): finished = Signal(object) # result error = Signal(str) # error message - def __init__(self, func, *args): + def __init__(self, func: Callable, *args: Any) -> None: super().__init__() self._func = func self._args = args - def run(self): + def run(self) -> None: try: result = self._func(*self._args) self.finished.emit(result) @@ -33,20 +34,20 @@ class GitChangeItem: 簡單的資料結構,用來存放檔案變更資訊。 """ - def __init__(self, path: str, status: str): + def __init__(self, path: str, status: str) -> None: self.path = path # repo 相對路徑 / repo-relative path self.status = status # 狀態,例如 'untracked', 'modified', 'deleted', 'renamed', 'staged' class GitGui(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.current_repo: Repo | None = None self.last_opened_repo_path = None self._init_ui() # 初始化 UI self._restore_last_opened_repository() # 嘗試還原上次開啟的 repo - def _run_git_in_background(self, func, on_done=None, on_error=None): + def _run_git_in_background(self, func: Callable, on_done: Callable | None = None, on_error: Callable | None = None) -> None: """ 在背景執行緒中執行 Git 操作,避免阻塞主執行緒 Run Git operation in background thread to avoid blocking UI @@ -70,7 +71,7 @@ def _run_git_in_background(self, func, on_done=None, on_error=None): self._bg_threads = [(t, w) for t, w in self._bg_threads if t.isRunning()] self._bg_threads.append((thread, worker)) - def _init_ui(self): + def _init_ui(self) -> None: # === Top controls / 上方控制區 === self.repo_path_label = QLabel("Repository: (none)") self.open_repo_button = QPushButton("Open Repo") @@ -189,7 +190,7 @@ def _init_ui(self): # ---------- Repo operations ---------- # ---------- 儲存庫操作 ---------- - def _restore_last_opened_repository(self): + def _restore_last_opened_repository(self) -> None: """ Restore the last opened repository if available. 如果有記錄上次開啟的儲存庫,則嘗試重新載入。 @@ -197,7 +198,7 @@ def _restore_last_opened_repository(self): if self.last_opened_repo_path: self._load_repository_from_path(Path(self.last_opened_repo_path)) - def on_open_repository_requested(self): + def on_open_repository_requested(self) -> None: """ Open a Git repository via file dialog. 透過檔案選擇對話框開啟 Git 儲存庫。 @@ -208,7 +209,7 @@ def on_open_repository_requested(self): self._load_repository_from_path(Path(repo_directory)) self.last_opened_repo_path = str(repo_directory) - def _load_repository_from_path(self, selected_directory_path: Path): + def _load_repository_from_path(self, selected_directory_path: Path) -> None: """ Load a Git repository from a given folder. 從指定資料夾載入 Git 儲存庫。 @@ -236,7 +237,7 @@ def _load_repository_from_path(self, selected_directory_path: Path): self._refresh_change_list() self._update_ui_controls(True) - def _refresh_branch_list(self): + def _refresh_branch_list(self) -> None: """ Refresh branch list in the combo box. 更新分支清單。 @@ -260,14 +261,14 @@ def _refresh_branch_list(self): self.branch_selector.setEditable(True) self.branch_selector.setEditText(self.current_repo.head.commit.hexsha[:8]) - def on_branch_selection_changed(self): + def on_branch_selection_changed(self) -> None: """ Triggered when branch selection changes. 分支選擇改變時觸發,目前不做動作,需按下 Checkout 才會生效。 """ pass - def on_checkout_branch_requested(self): + def on_checkout_branch_requested(self) -> None: """ Checkout the selected branch. 切換到選取的分支。 @@ -297,13 +298,13 @@ def _is_binary_path(self, abs_path: Path, sniff_bytes: int = 2048) -> bool: except Exception: return False - def _safe_set_diff_text(self, text: str): + def _safe_set_diff_text(self, text: str) -> None: # 套用高亮顏色 self.diff_viewer.setPlainText(text if text else "(no content)") if hasattr(self, "highlighter"): self.highlighter.rehighlight() - def _show_diff_for_change(self, change: GitChangeItem): + def _show_diff_for_change(self, change: GitChangeItem) -> None: current_repo = self.current_repo if not current_repo: self._safe_set_diff_text("Error: repository not loaded.") @@ -406,7 +407,7 @@ def _show_diff_for_change(self, change: GitChangeItem): except Exception as e: self._safe_set_diff_text(f"Unexpected error while generating diff:\n{e}") - def _refresh_change_list(self): + def _refresh_change_list(self) -> None: """ Collect changes from working tree and index, then render list. 收集工作目錄與索引的變更,並更新清單。 @@ -446,7 +447,7 @@ def _refresh_change_list(self): # === Render list / 渲染清單 === self.changes_list_widget.clear() - def add_item(txt: str, bold=False, disabled=False): + def add_item(txt: str, bold: bool = False, disabled: bool = False) -> None: """ Add a section header item to the list. 新增一個區段標題項目到清單。 @@ -485,7 +486,7 @@ def add_item(txt: str, bold=False, disabled=False): self.repo_status_label.setText(f"Status: {summary}") self.diff_viewer.setPlainText("Select files to stage/unstage.") - def _add_change_item(self, change: GitChangeItem): + def _add_change_item(self, change: GitChangeItem) -> None: """ Add a file change entry to the list. 將檔案變更項目加入清單。 @@ -496,7 +497,7 @@ def _add_change_item(self, change: GitChangeItem): list_item.setCheckState(Qt.CheckState.Unchecked) # 預設未勾選 self.changes_list_widget.addItem(list_item) - def _get_selected_changes(self): + def _get_selected_changes(self) -> list[GitChangeItem]: """ Collect all checked selected_items from the list. 收集所有被勾選的檔案項目。 @@ -510,7 +511,7 @@ def _get_selected_changes(self): selected_items.append(change) return selected_items - def on_change_selection_changed(self): + def on_change_selection_changed(self) -> None: """ Show diff for the selected file. 顯示選取檔案的差異。 @@ -528,7 +529,7 @@ def on_change_selection_changed(self): if isinstance(change, GitChangeItem): self._show_diff_for_change(change) - def on_stage_selected_changes(self): + def on_stage_selected_changes(self) -> None: """ Stage selected_changes files. 將選取的檔案加入暫存區。 @@ -556,7 +557,7 @@ def on_stage_selected_changes(self): except GitCommandError as e: QMessageBox.critical(self, "Stage Error", str(e)) - def on_unstage_selected_changes(self): + def on_unstage_selected_changes(self) -> None: """ Unstage selected_changes files. 將選取的檔案從暫存區移除。 @@ -592,7 +593,7 @@ def on_unstage_selected_changes(self): except GitCommandError as e: QMessageBox.critical(self, "Unstage Error", str(e)) - def on_stage_all_changes(self): + def on_stage_all_changes(self) -> None: """ Stage all changes (equivalent to git add -A). 將所有變更加入暫存區(等同於 git add -A)。 @@ -605,7 +606,7 @@ def on_stage_all_changes(self): except GitCommandError as e: QMessageBox.critical(self, "Stage All Error", str(e)) - def on_commit_staged_changes(self): + def on_commit_staged_changes(self) -> None: """ Commit staged changes with a message. 提交已暫存的變更。 @@ -630,7 +631,7 @@ def on_commit_staged_changes(self): except GitCommandError as e: QMessageBox.critical(self, "Commit Error", str(e)) - def _update_ui_controls(self, enabled: bool): + def _update_ui_controls(self, enabled: bool) -> None: """ Enable or disable UI controls depending on repo state. 根據是否有開啟 repo 來啟用或停用 UI 控制項。 @@ -643,7 +644,7 @@ def _update_ui_controls(self, enabled: bool): ): widget.setEnabled(enabled) - def on_unstage_all_changes(self): + def on_unstage_all_changes(self) -> None: """ Unstage all changes. 將所有檔案從暫存區移除。 @@ -662,7 +663,7 @@ def on_unstage_all_changes(self): except Exception as e: QMessageBox.critical(self, "Unstage All Error", str(e)) - def on_track_all_untracked_files(self): + def on_track_all_untracked_files(self) -> None: """ Track all untracked files. 將所有未追蹤檔案加入暫存區。 @@ -681,7 +682,7 @@ def on_track_all_untracked_files(self): except Exception as e: QMessageBox.critical(self, "Track Untracked Error", str(e)) - def on_clone_repository_requested(self): + def on_clone_repository_requested(self) -> None: """ Clone a remote repository into a local folder. 複製遠端 Git 儲存庫到本地資料夾。 @@ -723,7 +724,7 @@ def on_clone_repository_requested(self): # ===== GitHub ===== - def on_push_to_github(self): + def on_push_to_github(self) -> None: if not self.current_repo: QMessageBox.warning(self, "Warning", "No repository opened.") return @@ -731,17 +732,17 @@ def on_push_to_github(self): self.git_push_button.setEnabled(False) self.git_push_button.setText("Pushing...") - def do_push(): + def do_push() -> str: origin = repo.remote(name="origin") result = origin.push() return "\n".join(str(r) for r in result) - def on_done(msg): + def on_done(msg: str) -> None: self.git_push_button.setEnabled(True) self.git_push_button.setText("Push") QMessageBox.information(self, "Push Result", f"Pushed to origin:\n{msg}") - def on_error(err): + def on_error(err: str) -> None: self.git_push_button.setEnabled(True) self.git_push_button.setText("Push") QMessageBox.critical(self, "Push Error", err) @@ -776,15 +777,15 @@ def get_unpushed_commit_count(self, remote_name: str = "origin") -> dict: except Exception as e: return {"ahead": 0, "behind": 0, "error": str(e)} - def update_commit_status(self): + def update_commit_status(self) -> None: """在背景執行緒中更新 commit 狀態 / Update commit status in background""" if not self.current_repo: return - def do_check(): + def do_check() -> dict: return self.get_unpushed_commit_count() - def on_done(result): + def on_done(result: dict) -> None: if result["error"]: self.commit_status_label.setText(f"Error: {result['error']}") else: @@ -792,14 +793,14 @@ def on_done(result): f"Ahead (push): {result['ahead']} | Behind (pull): {result['behind']}" ) - def on_error(err): + def on_error(err: str) -> None: self.commit_status_label.setText(f"Error: {err}") self._run_git_in_background(do_check, on_done, on_error) # ===== Theme ===== - def apply_light_theme(self): + def apply_light_theme(self) -> None: """ Switch to light theme highlighting. 切換到淺色主題的高亮顯示。 @@ -807,7 +808,7 @@ def apply_light_theme(self): self.highlighter.configure_theme_colors(use_light_mode=True) self.highlighter.rehighlight() - def apply_dark_theme(self): + def apply_dark_theme(self) -> None: """ Switch to dark theme highlighting. 切換到深色主題的高亮顯示。 @@ -822,11 +823,11 @@ class GitDiffHighlighter(QSyntaxHighlighter): Git diff 文字的語法高亮器。 """ - def __init__(self, parent): + def __init__(self, parent: QPlainTextEdit) -> None: super().__init__(parent) self.configure_theme_colors() - def configure_theme_colors(self, use_light_mode: bool = False): + def configure_theme_colors(self, use_light_mode: bool = False) -> None: """ Update colors depending on theme. 根據主題更新顏色。 @@ -858,7 +859,7 @@ def configure_theme_colors(self, use_light_mode: bool = False): self.meta_format = QTextCharFormat() self.meta_format.setForeground(self.color_meta) - def highlightBlock(self, line_text: str): + def highlightBlock(self, line_text: str) -> None: """ Apply highlighting rules to each line of diff text. 對 diff 文字的每一行套用高亮規則。 diff --git a/je_editor/pyside_ui/git_ui/git_client/graph_view.py b/je_editor/pyside_ui/git_ui/git_client/graph_view.py index f26e49f..f2640a5 100644 --- a/je_editor/pyside_ui/git_ui/git_client/graph_view.py +++ b/je_editor/pyside_ui/git_ui/git_client/graph_view.py @@ -3,12 +3,13 @@ from typing import Optional from PySide6.QtCore import Qt, QRectF, QPointF -from PySide6.QtGui import QPainter, QPainterPath, QPen, QColor, QBrush, QTransform +from PySide6.QtGui import QPainter, QPainterPath, QPen, QColor, QBrush, QResizeEvent, QTransform, QWheelEvent from PySide6.QtWidgets import ( QGraphicsView, QGraphicsScene, QGraphicsEllipseItem, QGraphicsPathItem, QGraphicsSimpleTextItem, + QWidget, ) from je_editor.git_client.commit_graph import CommitGraph @@ -56,7 +57,7 @@ class CommitGraphView(QGraphicsView): Git commit graph visualization view """ - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.language_wrapper_get = language_wrapper.language_word_dict.get @@ -80,7 +81,7 @@ def __init__(self, parent=None): self._padding = 40 self._zoom = 1.0 - def set_graph(self, graph: CommitGraph): + def set_graph(self, graph: CommitGraph) -> None: """ 設定 commit graph 並重新繪製 Set commit graph and redraw @@ -96,7 +97,7 @@ def _row_y(self, row: int) -> float: # 計算 row 的 Y 座標 / calculate Y position for row return row * ROW_HEIGHT + NODE_RADIUS * 2 - def _redraw(self): + def _redraw(self) -> None: """ 重新繪製整個 commit graph Redraw the entire commit graph @@ -161,7 +162,7 @@ def _redraw(self): self._row_y(len(self.graph.nodes)) + self._padding) ) - def _apply_initial_view(self): + def _apply_initial_view(self) -> None: """ 初始縮放設定 Apply initial zoom setting @@ -170,7 +171,7 @@ def _apply_initial_view(self): self._apply_zoom_transform() # === 滑鼠滾輪縮放 / Mouse wheel zoom === - def wheelEvent(self, event): + def wheelEvent(self, event: QWheelEvent) -> None: if not self._scene.items(): return @@ -185,7 +186,7 @@ def wheelEvent(self, event): else: super().wheelEvent(event) - def _apply_zoom_transform(self): + def _apply_zoom_transform(self) -> None: """ 套用縮放轉換 Apply zoom transform @@ -194,7 +195,7 @@ def _apply_zoom_transform(self): t.scale(self._zoom, self._zoom) self.setTransform(t) - def resizeEvent(self, event): + def resizeEvent(self, event: QResizeEvent) -> None: """ 視窗大小改變時的事件 Handle resize event (keep scene rect, no auto-fit) @@ -204,7 +205,7 @@ def resizeEvent(self, event): # Keep scene rect; do not auto-fit # === 外部控制輔助方法 / Helper for external controllers === - def focus_row(self, row: int): + def focus_row(self, row: int) -> None: """ 將檢視器聚焦到指定的 row Center the view around a specific row diff --git a/je_editor/pyside_ui/main_ui/ai_widget/ai_config.py b/je_editor/pyside_ui/main_ui/ai_widget/ai_config.py index 94b9ef7..ebd125c 100644 --- a/je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +++ b/je_editor/pyside_ui/main_ui/ai_widget/ai_config.py @@ -7,7 +7,7 @@ class AIConfig(object): AIConfig class: manages AI model configuration and message queue """ - def __init__(self): + def __init__(self) -> None: # 當前 AI 模型的系統提示詞 (system prompt) # Current AI model system prompt self.current_ai_model_system_prompt: str = "" diff --git a/je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py b/je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py index 1cfe3a3..ed0b766 100644 --- a/je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +++ b/je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py @@ -10,7 +10,7 @@ class AskThread(Thread): AskThread class: runs AI model calls in a background thread to avoid blocking the main thread """ - def __init__(self, lang_chain_interface: LangChainInterface, prompt): + def __init__(self, lang_chain_interface: LangChainInterface, prompt: str) -> None: """ 初始化 AskThread Initialize AskThread @@ -23,7 +23,7 @@ def __init__(self, lang_chain_interface: LangChainInterface, prompt): self.lang_chain_interface = lang_chain_interface self.prompt = prompt - def run(self): + def run(self) -> None: """ 執行緒的主要邏輯: 1. 呼叫 AI 模型並取得回應 diff --git a/je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py b/je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py index 2f3ec26..322f398 100644 --- a/je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +++ b/je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py @@ -21,7 +21,7 @@ class ChatUI(QWidget): - def __init__(self, main_window: EditorMain): + def __init__(self, main_window: EditorMain) -> None: super().__init__() self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) # 關閉視窗時自動釋放資源 / Auto delete on close self.main_window = main_window @@ -87,12 +87,12 @@ def __init__(self, main_window: EditorMain): self.load_ai_config() # 更新聊天面板字體大小 / Update chat panel font size - def update_panel_text_size(self): + def update_panel_text_size(self) -> None: self.chat_panel.setFont( QFontDatabase.font(self.font().family(), "", int(self.font_size_combobox.currentText()))) # 載入 AI 設定檔 / Load AI configuration file - def load_ai_config(self, show_load_complete: bool = False): + def load_ai_config(self, show_load_complete: bool = False) -> None: ai_config_file = Path.cwd() / ".jeditor" / "ai_config.json" if ai_config_file.exists(): json_data: dict = read_json(str(ai_config_file)) @@ -117,7 +117,7 @@ def load_ai_config(self, show_load_complete: bool = False): load_complete.exec() # 呼叫 AI 模型 / Call AI model - def call_ai_model(self): + def call_ai_model(self) -> None: if isinstance(self.lang_chain_interface, LangChainInterface): # 建立新執行緒處理 AI 請求 / Start a new thread for AI request # Store reference to prevent garbage collection before thread completes @@ -137,13 +137,13 @@ def call_ai_model(self): f"prompt_template: {ai_info.get('prompt_template')}") # 從訊息佇列中取出 AI 回覆並顯示 / Pull AI response from queue - def pull_message(self): + def pull_message(self) -> None: if not ai_config.message_queue.empty(): ai_response = ai_config.message_queue.get_nowait() self.chat_panel.appendPlainText(ai_response) # 顯示回覆 / Display response self.chat_panel.appendPlainText("\n") # 開啟 AI 設定對話框 / Open AI config dialog - def set_ai_config(self): + def set_ai_config(self) -> None: self.set_ai_config_dialog = SetAIDialog() self.set_ai_config_dialog.show() diff --git a/je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py b/je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py index 0c2b02b..4488fbc 100644 --- a/je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +++ b/je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py @@ -22,7 +22,7 @@ class LangChainInterface(object): """ def __init__(self, main_window: ChatUI, prompt_template: str, base_url: str, - api_key: Union[SecretStr, str], chat_model: str): + api_key: Union[SecretStr, str], chat_model: str) -> None: """ 初始化 LangChainInterface Initialize LangChainInterface diff --git a/je_editor/pyside_ui/main_ui/console_widget/console_gui.py b/je_editor/pyside_ui/main_ui/console_widget/console_gui.py index 834d238..ca9e7c2 100644 --- a/je_editor/pyside_ui/main_ui/console_widget/console_gui.py +++ b/je_editor/pyside_ui/main_ui/console_widget/console_gui.py @@ -1,6 +1,6 @@ import os -from PySide6.QtCore import Qt, QEvent +from PySide6.QtCore import QObject, Qt, QEvent from PySide6.QtGui import QTextCursor, QColor, QKeyEvent from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit, QLineEdit, @@ -17,7 +17,7 @@ class ConsoleWidget(QWidget): ConsoleWidget provides an interactive console interface for running commands and viewing output. """ - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.language_word_dict_get = language_wrapper.language_word_dict.get @@ -97,7 +97,7 @@ def __init__(self, parent=None): # 事件過濾器:支援上下鍵瀏覽歷史指令 # Event filter: Support navigating command history with Up/Down keys - def eventFilter(self, obj, event): + def eventFilter(self, obj: QObject, event: QEvent) -> bool: if obj is self.input and isinstance(event, QKeyEvent) and event.type() == QEvent.Type.KeyPress: if event.key() == Qt.Key.Key_Up: self.history_prev() @@ -108,16 +108,19 @@ def eventFilter(self, obj, event): return super().eventFilter(obj, event) # 瀏覽上一個歷史指令 / Navigate to previous command in history - def history_prev(self): - if not self.history: return + def history_prev(self) -> None: + if not self.history: + return self.history_index = len(self.history) - 1 if self.history_index < 0 else max(0, self.history_index - 1) self.input.setText(self.history[self.history_index]) self.input.setCursorPosition(len(self.input.text())) # 瀏覽下一個歷史指令 / Navigate to next command in history - def history_next(self): - if not self.history: return - if self.history_index < 0: return + def history_next(self) -> None: + if not self.history: + return + if self.history_index < 0: + return self.history_index += 1 if self.history_index >= len(self.history): self.history_index = -1 @@ -127,7 +130,7 @@ def history_next(self): self.input.setCursorPosition(len(self.input.text())) # 選擇新的工作目錄 / Pick a new working directory - def pick_cwd(self): + def pick_cwd(self) -> None: d = QFileDialog.getExistingDirectory(self, "Select working directory", os.getcwd()) if d: self.proc.set_cwd(d) @@ -135,7 +138,7 @@ def pick_cwd(self): self.proc.system.emit(f'cd "{d}"') # 在輸出區域新增文字 (支援顏色) / Append text to output area (with optional color) - def append_text(self, text, color=None): + def append_text(self, text: str, color: str | None = None) -> None: cursor = self.output.textCursor() cursor.movePosition(QTextCursor.MoveOperation.End) fmt = self.output.currentCharFormat() @@ -147,9 +150,10 @@ def append_text(self, text, color=None): self.output.ensureCursorVisible() # 執行輸入的指令 / Run the entered command - def run_command(self): + def run_command(self) -> None: cmd = self.input.text().strip() - if not cmd: return + if not cmd: + return if not self.history or self.history[-1] != cmd: self.history.append(cmd) self.history_index = -1 @@ -158,7 +162,7 @@ def run_command(self): self.input.clear() # 子程序結束時的處理 / Handle process finished event - def on_finished(self, code, status): + def on_finished(self, code: int, status: int) -> None: self.append_text( f"\n{self.language_word_dict_get('dynamic_console_done').format(code=code, status=status)}\n", "#888" diff --git a/je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py b/je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py index ed80788..fd097c9 100644 --- a/je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py +++ b/je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py @@ -16,7 +16,7 @@ class ConsoleProcessAdapter(QObject): stderr = Signal(str) # 錯誤輸出訊號 / Standard error signal system = Signal(str) # 系統訊息訊號 / System message signal - def __init__(self, parent=None): + def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) # 建立 QProcess 物件 / Create QProcess object self.proc = QProcess(self) @@ -30,11 +30,11 @@ def __init__(self, parent=None): self.proc.finished.connect(self.finished) # 設定工作目錄 / Set working directory - def set_cwd(self, path: str): + def set_cwd(self, path: str) -> None: self.proc.setWorkingDirectory(path) # 啟動 shell / Start shell - def start_shell(self, shell: str = "auto"): + def start_shell(self, shell: str = "auto") -> None: if self.is_running(): self.system.emit("Shell already running") # 如果已經在執行,發送提示 / Emit message if already running return @@ -46,14 +46,14 @@ def start_shell(self, shell: str = "auto"): QTimer.singleShot(500, lambda: self.send_command("chcp 65001")) # 傳送指令到 shell / Send command to shell - def send_command(self, cmd: str): + def send_command(self, cmd: str) -> None: if not self.is_running(): self.system.emit("Shell not running") # 如果 shell 未啟動,發送提示 / Emit message if not running return self.proc.write((cmd + "\n").encode("utf-8")) # 傳送指令並換行 / Send command with newline # 停止 shell / Stop shell - def stop(self): + def stop(self) -> None: if not self.is_running(): return self.proc.terminate() # 嘗試正常結束 / Try graceful termination @@ -61,19 +61,19 @@ def stop(self): QTimer.singleShot(1000, lambda: self.is_running() and self.proc.kill()) # 判斷是否正在執行 / Check if process is running - def is_running(self): + def is_running(self) -> bool: return self.proc.state() != QProcess.ProcessState.NotRunning # 處理標準輸出 / Handle standard output - def _on_stdout(self): + def _on_stdout(self) -> None: self.stdout.emit(bytes(self.proc.readAllStandardOutput()).decode("utf-8", errors="replace")) # 處理錯誤輸出 / Handle standard error - def _on_stderr(self): + def _on_stderr(self) -> None: self.stderr.emit(bytes(self.proc.readAllStandardError()).decode("utf-8", errors="replace")) # 建立 shell 指令 / Build shell command - def _build_shell_command(self, shell: str): + def _build_shell_command(self, shell: str) -> tuple[str, list[str]]: if shell == "auto": shell = "cmd" if os.name == "nt" else "bash" # Windows 預設 cmd,Linux/macOS 預設 bash if os.name == "nt": diff --git a/je_editor/pyside_ui/main_ui/dock/destroy_dock.py b/je_editor/pyside_ui/main_ui/dock/destroy_dock.py index 82e83d1..ceacf3d 100644 --- a/je_editor/pyside_ui/main_ui/dock/destroy_dock.py +++ b/je_editor/pyside_ui/main_ui/dock/destroy_dock.py @@ -1,4 +1,5 @@ from PySide6.QtCore import Qt +from PySide6.QtGui import QCloseEvent from PySide6.QtWidgets import QDockWidget from je_editor.utils.logging.loggin_instance import jeditor_logger @@ -13,7 +14,7 @@ class DestroyDock(QDockWidget): and automatically release resources and log events when closed. """ - def __init__(self): + def __init__(self) -> None: # 初始化時記錄日誌 / Log initialization jeditor_logger.info("Init DestroyDock") super().__init__() @@ -26,7 +27,7 @@ def __init__(self): # Set attribute: delete object on close to free memory self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) - def closeEvent(self, event) -> None: + def closeEvent(self, event: QCloseEvent) -> None: """ 覆寫 closeEvent,在關閉時額外處理: - 記錄關閉事件 diff --git a/je_editor/pyside_ui/main_ui/editor/editor_widget.py b/je_editor/pyside_ui/main_ui/editor/editor_widget.py index f06fd68..fa5db56 100644 --- a/je_editor/pyside_ui/main_ui/editor/editor_widget.py +++ b/je_editor/pyside_ui/main_ui/editor/editor_widget.py @@ -16,7 +16,7 @@ from pathlib import Path from typing import Union -from PySide6.QtCore import Qt, QFileInfo, QDir, QMimeData, QFileSystemWatcher +from PySide6.QtCore import Qt, QFileInfo, QDir, QFileSystemWatcher from PySide6.QtGui import QDragEnterEvent, QDropEvent from PySide6.QtWidgets import QWidget, QGridLayout, QSplitter, QScrollArea, QFileSystemModel, QTreeView, QTabWidget, \ QMessageBox @@ -50,7 +50,7 @@ class EditorWidget(QWidget): - Auto-save and file management """ - def __init__(self, main_window: EditorMain): + def __init__(self, main_window: EditorMain) -> None: jeditor_logger.info(f"Init EditorWidget main_window: {main_window}") super().__init__() # 啟用拖放功能 / Enable drag and drop @@ -201,7 +201,7 @@ def set_project_treeview(self) -> None: # 點擊檔案時觸發 / Connect click event self.project_treeview.clicked.connect(self.treeview_click) - def check_is_open(self, path: Path): + def check_is_open(self, path: Path) -> bool: """ 檢查檔案是否已經開啟,如果已開啟則切換到該分頁。 Check if the file is already open, if yes then switch to that tab. @@ -290,7 +290,7 @@ def treeview_click(self) -> None: if path.is_file(): self.open_an_file(path) - def _on_text_changed(self): + def _on_text_changed(self) -> None: """ 文字變更時標記為未儲存,並在 tab 標題加上 * Mark as modified when text changes, add * to tab title @@ -303,7 +303,7 @@ def _on_text_changed(self): if not title.endswith(" *"): self.tab_manager.setTabText(idx, title + " *") - def mark_saved(self): + def mark_saved(self) -> None: """ 儲存後清除未儲存標記 Clear the unsaved marker after saving @@ -315,7 +315,7 @@ def mark_saved(self): if title.endswith(" *"): self.tab_manager.setTabText(idx, title[:-2]) - def rename_self_tab(self): + def rename_self_tab(self) -> None: """ 將分頁的標籤名稱改為目前檔案名稱 (不限當前分頁)。 Rename this tab to the current file name (works for any tab, not just current). @@ -327,7 +327,7 @@ def rename_self_tab(self): self.setObjectName(str(Path(self.current_file))) self._is_modified = False - def check_file_format(self): + def check_file_format(self) -> None: """ 檢查目前檔案的程式碼格式 (僅支援 Python)。 Check the code format of the current file (only supports Python). diff --git a/je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py b/je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py index a959fbe..5acd9bb 100644 --- a/je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py +++ b/je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py @@ -1,6 +1,7 @@ from pathlib import Path from PySide6.QtCore import Qt +from PySide6.QtGui import QCloseEvent from PySide6.QtWidgets import QWidget, QGridLayout, QScrollArea from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper @@ -19,7 +20,7 @@ class FullEditorWidget(QWidget): including code editing area, scroll support, and auto-save on close. """ - def __init__(self, current_file: str): + def __init__(self, current_file: str) -> None: # 初始化時記錄日誌 / Log initialization jeditor_logger.info(f"Init FullEditorWidget current_file: {current_file}") super().__init__() @@ -55,7 +56,7 @@ def __init__(self, current_file: str): f"font-family: {user_setting_dict.get('font', 'Lato')};" ) - def closeEvent(self, event) -> None: + def closeEvent(self, event: QCloseEvent) -> None: """ 覆寫 closeEvent,在關閉視窗時自動儲存檔案內容。 Override closeEvent to auto-save file content when closing window. diff --git a/je_editor/pyside_ui/main_ui/editor/process_input.py b/je_editor/pyside_ui/main_ui/editor/process_input.py index 117c00e..da039a9 100644 --- a/je_editor/pyside_ui/main_ui/editor/process_input.py +++ b/je_editor/pyside_ui/main_ui/editor/process_input.py @@ -1,7 +1,7 @@ from __future__ import annotations # 允許未來版本的型別註解功能 / Enable postponed evaluation of type annotations from typing import \ - TYPE_CHECKING # 用於避免循環匯入,僅在型別檢查時載入 / Used to avoid circular imports, only loaded during type checking + IO, TYPE_CHECKING # 用於避免循環匯入,僅在型別檢查時載入 / Used to avoid circular imports, only loaded during type checking from PySide6.QtCore import Qt # Qt 核心模組 / Qt core module from PySide6.QtWidgets import QWidget, QLineEdit, QBoxLayout, QPushButton, QHBoxLayout @@ -28,7 +28,7 @@ class ProcessInput(QWidget): ProcessInput is an input widget that allows users to send commands to different subprocesses. """ - def __init__(self, main_window: EditorWidget, process_type: str = "debugger"): + def __init__(self, main_window: EditorWidget, process_type: str = "debugger") -> None: # 初始化時記錄日誌 / Log initialization jeditor_logger.info("Init ProcessInput " f"main_window: {main_window} " @@ -72,7 +72,7 @@ def __init__(self, main_window: EditorWidget, process_type: str = "debugger"): # 設定主佈局 / Apply layout self.setLayout(self.box_layout) - def _safe_write_stdin(self, process_stdin): + def _safe_write_stdin(self, process_stdin: IO[bytes] | None) -> None: """ 安全地將輸入文字寫入子程序 stdin,處理管線斷開的情況 Safely write input text to subprocess stdin, handling broken pipe @@ -86,19 +86,19 @@ def _safe_write_stdin(self, process_stdin): jeditor_logger.warning(f"ProcessInput stdin write failed: {e}") # === Debugger 指令傳送 / Send command to debugger === - def debugger_send_command(self): + def debugger_send_command(self) -> None: jeditor_logger.info("EditorWidget debugger_send_command") if self.main_window.exec_python_debugger is not None and self.main_window.exec_python_debugger.process is not None: self._safe_write_stdin(self.main_window.exec_python_debugger.process.stdin) # === Shell 指令傳送 / Send command to shell === - def shell_send_command(self): + def shell_send_command(self) -> None: jeditor_logger.info("EditorWidget shell_send_command") if self.main_window.exec_shell is not None and self.main_window.exec_shell.process is not None: self._safe_write_stdin(self.main_window.exec_shell.process.stdin) # === Program 指令傳送 / Send command to program === - def program_send_command(self): + def program_send_command(self) -> None: jeditor_logger.info("EditorWidget program_send_command") if self.main_window.exec_program is not None and self.main_window.exec_program.process is not None: self._safe_write_stdin(self.main_window.exec_program.process.stdin) diff --git a/je_editor/pyside_ui/main_ui/ipython_widget/ipython_console.py b/je_editor/pyside_ui/main_ui/ipython_widget/ipython_console.py index a5aaadf..e8942a4 100644 --- a/je_editor/pyside_ui/main_ui/ipython_widget/ipython_console.py +++ b/je_editor/pyside_ui/main_ui/ipython_widget/ipython_console.py @@ -24,7 +24,7 @@ class IpythonWidget(QWidget): - Provides an interactive Python environment within the application """ - def __init__(self, main_window: EditorMain): + def __init__(self, main_window: EditorMain) -> None: # 初始化時記錄日誌 / Log initialization jeditor_logger.info(f"Init IpythonWidget main_window: {main_window}") super().__init__() @@ -58,7 +58,7 @@ def __init__(self, main_window: EditorMain): # 設定主佈局 / Apply layout self.setLayout(self.grid_layout) - def close(self): + def close(self) -> None: """ 覆寫 close 方法,確保關閉時正確釋放資源 Override close method to properly release resources diff --git a/je_editor/pyside_ui/main_ui/main_editor.py b/je_editor/pyside_ui/main_ui/main_editor.py index 7533bdb..23e12b8 100644 --- a/je_editor/pyside_ui/main_ui/main_editor.py +++ b/je_editor/pyside_ui/main_ui/main_editor.py @@ -11,7 +11,7 @@ # 匯入 PySide6 (Qt for Python) 的核心模組 # Import PySide6 core modules from PySide6.QtCore import QTimer, QEvent -from PySide6.QtGui import QFontDatabase, QIcon, Qt, QTextCharFormat +from PySide6.QtGui import QCloseEvent, QFontDatabase, QIcon, Qt, QTextCharFormat from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QTabWidget, QLabel, QMessageBox # 匯入 Qt Material 主題工具 # Import Qt Material style tools @@ -54,7 +54,7 @@ class EditorMain(QMainWindow, QtStyleTools): 繼承 QMainWindow 與 QtStyleTools """ - def __init__(self, debug_mode: bool = False, show_system_tray_ray: bool = False, extend: bool = False): + def __init__(self, debug_mode: bool = False, show_system_tray_ray: bool = False, extend: bool = False) -> None: # 初始化時記錄 log # Log initialization jeditor_logger.info(f"Init EditorMain " @@ -235,12 +235,12 @@ def __init__(self, debug_mode: bool = False, show_system_tray_ray: bool = False, close_timer.timeout.connect(self.debug_close) close_timer.start() - def clear_code_result(self): + def clear_code_result(self) -> None: """ 清除目前編輯器的輸出結果 Clear the current editor's output result """ - jeditor_logger.info(f"EditorMain clear_code_result") + jeditor_logger.info("EditorMain clear_code_result") widget = self.tab_widget.currentWidget() if isinstance(widget, EditorWidget): widget.code_result.clear() @@ -307,7 +307,7 @@ def startup_setting(self) -> None: 啟動時套用使用者設定 (字型、樣式、上次開啟的檔案) Apply user settings on startup (fonts, styles, last opened file) """ - jeditor_logger.info(f"EditorMain startup_setting") + jeditor_logger.info("EditorMain startup_setting") # 設定 UI 字型與大小 # Set UI font and size self.setStyleSheet( @@ -363,7 +363,7 @@ def startup_setting(self) -> None: # Update color settings update_actually_color_dict() - def go_to_new_tab(self, file_path: Path): + def go_to_new_tab(self, file_path: Path) -> None: """ 開啟新分頁並載入檔案 Open a new tab and load a file @@ -459,7 +459,7 @@ def _periodic_save_settings(self) -> None: except Exception as e: jeditor_logger.warning(f"Periodic settings save failed: {e}") - def closeEvent(self, event) -> None: + def closeEvent(self, event: QCloseEvent) -> None: """ 視窗關閉事件:關閉所有分頁並儲存使用者設定 Window close event: close all tabs and save user settings @@ -488,7 +488,7 @@ def event(self, event: QEvent) -> bool: else: return super().event(event) - def close_tab(self, index: int): + def close_tab(self, index: int) -> None: """ 關閉指定索引的分頁,若有未儲存的修改則提示使用者 Close tab at given index, prompt if unsaved changes @@ -514,7 +514,7 @@ def close_tab(self, index: int): self.tab_widget.removeTab(index) @staticmethod - def debug_close(): + def debug_close() -> None: """ 除錯模式下自動關閉程式 Auto-close the program in debug mode diff --git a/je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py b/je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py index 273189d..668a497 100644 --- a/je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py @@ -129,7 +129,7 @@ def set_dock_menu(ui_we_want_to_set: EditorMain) -> None: ui_we_want_to_set.dock_git_menu.addAction(ui_we_want_to_set.dock_menu.new_code_diff_viewer) -def add_dock_widget(ui_we_want_to_set: EditorMain, widget_type: str = None): +def add_dock_widget(ui_we_want_to_set: EditorMain, widget_type: str = None) -> None: """ 根據 widget_type 新增對應的 Dock 視窗,並加到主視窗右側。 Add a dock widget based on widget_type and attach it to the right side of the main window. diff --git a/je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py b/je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py index 5f486e1..1107966 100644 --- a/je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py @@ -178,7 +178,7 @@ def set_encoding(ui_we_want_to_set: EditorMain, action: QAction) -> None: # 顯示新建檔案對話框 # Show Create File dialog -def show_create_file_dialog(ui_we_want_to_set: EditorMain): +def show_create_file_dialog(ui_we_want_to_set: EditorMain) -> None: jeditor_logger.info("build_file_menu.py show_create_file_dialog " f"ui_we_want_to_set: {ui_we_want_to_set}") ui_we_want_to_set.create_file_dialog = CreateFileDialog() diff --git a/je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py b/je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py index 66393b8..4ae4d0c 100644 --- a/je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py @@ -75,7 +75,7 @@ def set_help_menu(ui_we_want_to_set: EditorMain) -> None: # 開啟內建瀏覽器分頁 # Open a new tab in the embedded browser -def open_web_browser(ui_we_want_to_set: EditorMain, url: str, tab_name: str): +def open_web_browser(ui_we_want_to_set: EditorMain, url: str, tab_name: str) -> None: jeditor_logger.info("build_help_menu.py open_web_browser " f"ui_we_want_to_set: {ui_we_want_to_set} " f"url: {url} " @@ -88,7 +88,7 @@ def open_web_browser(ui_we_want_to_set: EditorMain, url: str, tab_name: str): # 顯示「關於」訊息框 # Show "About" message box -def show_about(): +def show_about() -> None: jeditor_logger.info("build_help_menu.py show_about") message_box = QMessageBox() message_box.setText( diff --git a/je_editor/pyside_ui/main_ui/menu/plugin_menu/build_plugin_menu.py b/je_editor/pyside_ui/main_ui/menu/plugin_menu/build_plugin_menu.py index 6e4c575..d02880e 100644 --- a/je_editor/pyside_ui/main_ui/menu/plugin_menu/build_plugin_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/plugin_menu/build_plugin_menu.py @@ -4,7 +4,7 @@ # 僅用於型別檢查,避免循環匯入 # For type checking only (avoids circular imports) -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable # 匯入 Qt 動作與訊息框 # Import QAction and QMessageBox from PySide6 @@ -142,12 +142,12 @@ def _open_plugin_browser(ui_we_want_to_set: EditorMain) -> None: ) -def _make_about_callback(name: str, version: str, author: str): +def _make_about_callback(name: str, version: str, author: str) -> Callable[[], None]: """ 建立顯示插件資訊的回呼函式。 Create a callback to show plugin info dialog. """ - def callback(): + def callback() -> None: message_box = QMessageBox() message_box.setWindowTitle(name) message_box.setText( @@ -159,12 +159,12 @@ def callback(): return callback -def _make_run_callback(ui_we_want_to_set: EditorMain, run_config: dict, suffix: str): +def _make_run_callback(ui_we_want_to_set: EditorMain, run_config: dict, suffix: str) -> Callable[[], None]: """ 建立使用插件執行設定來執行程式的回呼函式。 Create a callback to run a program using plugin run config. """ - def callback(): + def callback() -> None: from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget from je_editor.pyside_ui.code.code_process.code_exec import ExecManager from je_editor.pyside_ui.dialog.file_dialog.save_file_dialog import choose_file_get_save_file_path diff --git a/je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py b/je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py index c4e43d1..deed3b4 100644 --- a/je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py @@ -118,7 +118,7 @@ def create_venv(ui_we_want_to_set: EditorMain) -> None: # 使用 pip 安裝套件 (通用函式) # Run pip install command (general function) -def shell_pip_install(ui_we_want_to_set: EditorMain, pip_install_command_list: list): +def shell_pip_install(ui_we_want_to_set: EditorMain, pip_install_command_list: list) -> None: """ 使用 pip 安裝套件 (通用函式)。詢問使用者套件名稱後附加到指令清單中。 Run pip install command (general function). Ask user for package name and append to command list. @@ -227,7 +227,7 @@ def pip_install_package(ui_we_want_to_set: EditorMain) -> None: # 選擇 Python 解譯器 # Choose Python interpreter -def chose_python_interpreter(ui_we_want_to_set: EditorMain): +def chose_python_interpreter(ui_we_want_to_set: EditorMain) -> None: jeditor_logger.info(f"build_venv_menu.py chose_python_interpreter ui_we_want_to_set: {ui_we_want_to_set}") file_path = QFileDialog().getOpenFileName( parent=ui_we_want_to_set, diff --git a/je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py b/je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py index e9e66ee..fba2e6b 100644 --- a/je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py +++ b/je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py @@ -21,7 +21,7 @@ # 顯示「請先關閉目前執行中的程式」訊息框 # Show a message box: "Please stop the currently running program first" -def please_close_current_running_messagebox(ui_we_want_to_set: EditorMain): +def please_close_current_running_messagebox(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌,方便除錯 # Log info for debugging jeditor_logger.info(f"utils.py please_close_current_running_messagebox ui_we_want_to_set: {ui_we_want_to_set}") diff --git a/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_git_menu.py b/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_git_menu.py index 955e817..8650b22 100644 --- a/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_git_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_git_menu.py @@ -48,7 +48,7 @@ def set_tab_git_menu(ui_we_want_to_set: EditorMain) -> None: ) ui_we_want_to_set.tab_menu.git_menu.addAction(ui_we_want_to_set.tab_menu.git_menu.add_code_diff_viewer_ui_action) -def add_git_client_tab(ui_we_want_to_set: EditorMain): +def add_git_client_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 Git Client 分頁 # Log: add a Git Client tab jeditor_logger.info(f"build_tab_menu.py add git client tab ui_we_want_to_set: {ui_we_want_to_set}") @@ -61,7 +61,7 @@ def add_git_client_tab(ui_we_want_to_set: EditorMain): ) -def add_git_tree_view_tab(ui_we_want_to_set: EditorMain): +def add_git_tree_view_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 Git Branch Tree 分頁 # Log: add a Git Branch Tree tab jeditor_logger.info(f"build_tab_menu.py add git tree view tab ui_we_want_to_set: {ui_we_want_to_set}") @@ -73,7 +73,7 @@ def add_git_tree_view_tab(ui_we_want_to_set: EditorMain): f"{ui_we_want_to_set.tab_widget.count()}" ) -def add_code_diff_compare_tab(ui_we_want_to_set: EditorMain): +def add_code_diff_compare_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 Code Diff Compare 分頁 # Log: add a Code Diff Compare tab jeditor_logger.info(f"build_tab_menu.py add code diff compare tab ui_we_want_to_set: {ui_we_want_to_set}") diff --git a/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py b/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py index f8a07d0..bfac083 100644 --- a/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py @@ -66,7 +66,7 @@ def set_tab_menu(ui_we_want_to_set: EditorMain) -> None: # === 以下為各分頁新增函式 === # === Functions to add each tab === -def add_editor_tab(ui_we_want_to_set: EditorMain): +def add_editor_tab(ui_we_want_to_set: EditorMain) -> EditorWidget: # 新增 Editor 分頁 # Add Editor tab jeditor_logger.info(f"build_tab_menu.py add editor tab ui_we_want_to_set: {ui_we_want_to_set}") @@ -78,7 +78,7 @@ def add_editor_tab(ui_we_want_to_set: EditorMain): return widget -def add_web_tab(ui_we_want_to_set: EditorMain): +def add_web_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 Web 分頁 # Log: add a Web tab jeditor_logger.info(f"build_tab_menu.py add web tab ui_we_want_to_set: {ui_we_want_to_set}") @@ -91,7 +91,7 @@ def add_web_tab(ui_we_want_to_set: EditorMain): ) -def add_console_widget_tab(ui_we_want_to_set: EditorMain): +def add_console_widget_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 Console 分頁 # Log: add a Console tab jeditor_logger.info(f"build_tab_menu.py add console widget tab ui_we_want_to_set: {ui_we_want_to_set}") diff --git a/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_tools_menu.py b/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_tools_menu.py index 0175710..0f16210 100644 --- a/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_tools_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_tools_menu.py @@ -58,7 +58,7 @@ def set_tab_tools_menu(ui_we_want_to_set: EditorMain) -> None: ui_we_want_to_set.tab_menu.tools_menu.addAction(ui_we_want_to_set.tab_menu.tools_menu.add_chat_ui_action) -def add_ipython_tab(ui_we_want_to_set: EditorMain): +def add_ipython_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 IPython 分頁 # Log: add an IPython tab jeditor_logger.info(f"build_tab_menu.py add ipython tab ui_we_want_to_set: {ui_we_want_to_set}") @@ -71,7 +71,7 @@ def add_ipython_tab(ui_we_want_to_set: EditorMain): ) -def add_variable_inspector_tab(ui_we_want_to_set: EditorMain): +def add_variable_inspector_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 Variable Inspector 分頁 # Log: add a Variable Inspector tab jeditor_logger.info(f"build_tab_menu.py add variable inspector tab ui_we_want_to_set: {ui_we_want_to_set}") @@ -84,7 +84,7 @@ def add_variable_inspector_tab(ui_we_want_to_set: EditorMain): ) -def add_frontengine_tab(ui_we_want_to_set: EditorMain): +def add_frontengine_tab(ui_we_want_to_set: EditorMain) -> None: # 新增 FrontEngine 分頁 # Add FrontEngine tab jeditor_logger.info(f"build_tab_menu.py add frontengine tab ui_we_want_to_set: {ui_we_want_to_set}") @@ -94,7 +94,7 @@ def add_frontengine_tab(ui_we_want_to_set: EditorMain): f"{ui_we_want_to_set.tab_widget.count()}") -def add_chat_ui_tab(ui_we_want_to_set: EditorMain): +def add_chat_ui_tab(ui_we_want_to_set: EditorMain) -> None: # 紀錄日誌:新增 Chat UI 分頁 # Log: add a Chat UI tab jeditor_logger.info(f"build_tab_menu.py add chat_ui tab ui_we_want_to_set: {ui_we_want_to_set}") diff --git a/je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py b/je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py index 3276ad6..f8fb99b 100644 --- a/je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py +++ b/je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from PySide6.QtGui import QAction, QTextOption +from PySide6.QtGui import QAction from PySide6.QtWidgets import QPlainTextEdit from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget @@ -35,7 +35,7 @@ # Import language wrapper for multilingual UI -def set_text_menu(ui_we_want_to_set: EditorMain): +def set_text_menu(ui_we_want_to_set: EditorMain) -> None: """ 建立文字選單,包含字型與字體大小的子選單 Create the text menu, including font and font size submenus diff --git a/je_editor/pyside_ui/main_ui/plugin_browser/plugin_browser_widget.py b/je_editor/pyside_ui/main_ui/plugin_browser/plugin_browser_widget.py index a37208d..30a9687 100644 --- a/je_editor/pyside_ui/main_ui/plugin_browser/plugin_browser_widget.py +++ b/je_editor/pyside_ui/main_ui/plugin_browser/plugin_browser_widget.py @@ -41,7 +41,7 @@ class PluginBrowserWidget(QWidget): Plugin browser main widget. """ - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) jeditor_logger.info("Init PluginBrowserWidget") @@ -56,7 +56,7 @@ def __init__(self, parent=None): self._init_ui() - def _init_ui(self): + def _init_ui(self) -> None: main_layout = QVBoxLayout(self) # === 頂部:Repo URL 輸入 === @@ -146,7 +146,7 @@ def _init_ui(self): # ----- Actions / 動作 ----- - def _on_fetch_clicked(self): + def _on_fetch_clicked(self) -> None: """使用者按下 Fetch 按鈕 / User clicks Fetch button.""" repo_url = self._repo_input.text().strip().rstrip("/") # 解析 owner/repo / Parse owner/repo @@ -172,7 +172,7 @@ def _on_fetch_clicked(self): Thread(target=self._fetch_worker, args=(owner, repo), daemon=True).start() - def _fetch_worker(self, owner: str, repo: str): + def _fetch_worker(self, owner: str, repo: str) -> None: """背景取得插件列表 / Background fetch plugin list.""" try: plugins = fetch_repo_tree(owner, repo) @@ -186,7 +186,7 @@ def _fetch_worker(self, owner: str, repo: str): except Exception as e: self._signals.error.emit(str(e)) - def _on_plugins_loaded(self, plugins: list[dict]): + def _on_plugins_loaded(self, plugins: list[dict]) -> None: """插件列表載入完成 / Plugin list loaded.""" self._set_loading(False) self._plugins = plugins @@ -224,7 +224,7 @@ def _on_plugins_loaded(self, plugins: list[dict]): language_wrapper.language_word_dict.get( "plugin_browser_status_loaded", "Loaded {count} plugins").format(count=count)) - def _on_item_selected(self, current: QTreeWidgetItem, _previous): + def _on_item_selected(self, current: QTreeWidgetItem, _previous: QTreeWidgetItem | None) -> None: """使用者選取插件 / User selects a plugin.""" if current is None: return @@ -265,7 +265,7 @@ def _on_item_selected(self, current: QTreeWidgetItem, _previous): daemon=True, ).start() - def _fetch_source_worker(self, url: str): + def _fetch_source_worker(self, url: str) -> None: """背景下載原始碼預覽 / Background fetch source preview.""" try: from je_editor.pyside_ui.main_ui.plugin_browser.github_api import _download_text @@ -274,12 +274,12 @@ def _fetch_source_worker(self, url: str): except Exception as e: self._signals.metadata.emit({"_source": f"Error: {e}"}) - def _on_metadata_loaded(self, data: dict): + def _on_metadata_loaded(self, data: dict) -> None: """原始碼預覽載入完成 / Source preview loaded.""" source = data.get("_source", "") self._detail_text.setPlainText(source) - def _on_download_clicked(self): + def _on_download_clicked(self) -> None: """使用者按下下載按鈕 / User clicks download button.""" if self._current_plugin is None: return @@ -323,7 +323,7 @@ def _on_download_clicked(self): daemon=True, ).start() - def _download_worker(self, url: str, dest_dir: str, file_name: str): + def _download_worker(self, url: str, dest_dir: str, file_name: str) -> None: """背景下載插件 / Background download plugin.""" try: saved = download_plugin_file(url, dest_dir, file_name) @@ -331,7 +331,7 @@ def _download_worker(self, url: str, dest_dir: str, file_name: str): except Exception as e: self._signals.error.emit(str(e)) - def _on_download_done(self, saved_path: str): + def _on_download_done(self, saved_path: str) -> None: """下載完成 / Download complete.""" self._set_loading(False) self._download_btn.setEnabled(True) @@ -349,7 +349,7 @@ def _on_download_done(self, saved_path: str): ).format(path=saved_path)) msg.exec() - def _on_error(self, error_msg: str): + def _on_error(self, error_msg: str) -> None: """顯示錯誤 / Show error.""" jeditor_logger.error(f"PluginBrowserWidget error: {error_msg}") self._set_loading(False) @@ -357,7 +357,7 @@ def _on_error(self, error_msg: str): # ----- Helpers ----- - def _set_loading(self, loading: bool): + def _set_loading(self, loading: bool) -> None: """切換載入狀態 / Toggle loading state.""" self._progress.setVisible(loading) self._fetch_btn.setEnabled(not loading) diff --git a/je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py b/je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py index 5e892f1..c8acb1b 100644 --- a/je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py +++ b/je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py @@ -21,7 +21,7 @@ def _to_qcolor(key: str, fallback: list) -> QColor: ) -def update_actually_color_dict(): +def update_actually_color_dict() -> None: """ 更新實際使用的顏色字典 (actually_color_dict), 將 user_setting_color_dict 中的 RGB 值轉換成 QColor 物件。 diff --git a/je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py b/je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py index f2d46dd..5c71c12 100644 --- a/je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py +++ b/je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py @@ -23,7 +23,7 @@ class ExtendSystemTray(QSystemTrayIcon): Extend system tray functionality with hide, maximize, restore, and close actions """ - def __init__(self, main_window: EditorMain): + def __init__(self, main_window: EditorMain) -> None: # 初始化並記錄日誌 # Initialize and log jeditor_logger.info(f"Init ExtendSystemTray main_window: {main_window}") @@ -66,7 +66,7 @@ def __init__(self, main_window: EditorMain): # Connect click events (e.g., double-click) self.activated.connect(self.clicked) - def close_all(self): + def close_all(self) -> None: """ 關閉應用程式:隱藏圖示、關閉主視窗並結束程式 Close the application: hide tray icon, close main window, and exit program @@ -76,7 +76,7 @@ def close_all(self): self.main_window.close() QApplication.quit() - def clicked(self, reason): + def clicked(self, reason: QSystemTrayIcon.ActivationReason) -> None: """ 處理系統匣點擊事件 Handle system tray click events diff --git a/je_editor/pyside_ui/main_ui/toolbar/toolbar_builder.py b/je_editor/pyside_ui/main_ui/toolbar/toolbar_builder.py index 28e87f0..ecf9b0f 100644 --- a/je_editor/pyside_ui/main_ui/toolbar/toolbar_builder.py +++ b/je_editor/pyside_ui/main_ui/toolbar/toolbar_builder.py @@ -12,6 +12,7 @@ from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper if TYPE_CHECKING: + from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget from je_editor.pyside_ui.main_ui.main_editor import EditorMain @@ -119,7 +120,7 @@ def build_toolbar(main_window: EditorMain) -> None: # ── Callbacks ───────────────────────────────────────────────── -def _get_editor_widget(main_window: EditorMain): +def _get_editor_widget(main_window: EditorMain) -> EditorWidget | None: """取得當前的 EditorWidget / Get current EditorWidget""" from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget widget = main_window.tab_widget.currentWidget() @@ -128,7 +129,7 @@ def _get_editor_widget(main_window: EditorMain): return None -def _new_file(main_window: EditorMain): +def _new_file(main_window: EditorMain) -> None: """新增檔案 / New file""" from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget editor_widget = EditorWidget(main_window) @@ -140,37 +141,37 @@ def _new_file(main_window: EditorMain): main_window.tab_widget.setCurrentWidget(editor_widget) -def _open_file(main_window: EditorMain): +def _open_file(main_window: EditorMain) -> None: """開啟檔案 / Open file""" from je_editor.pyside_ui.dialog.file_dialog.open_file_dialog import choose_file_get_open_file_path choose_file_get_open_file_path(main_window) -def _save_file(main_window: EditorMain): +def _save_file(main_window: EditorMain) -> None: """儲存檔案 / Save file""" from je_editor.pyside_ui.dialog.file_dialog.save_file_dialog import choose_file_get_save_file_path choose_file_get_save_file_path(main_window) -def _run_program(main_window: EditorMain): +def _run_program(main_window: EditorMain) -> None: """執行程式 / Run program""" from je_editor.pyside_ui.main_ui.menu.run_menu.under_run_menu.build_program_menu import run_program run_program(main_window) -def _run_debugger(main_window: EditorMain): +def _run_debugger(main_window: EditorMain) -> None: """執行除錯器 / Run debugger""" from je_editor.pyside_ui.main_ui.menu.run_menu.under_run_menu.build_debug_menu import run_debugger run_debugger(main_window) -def _stop_program(main_window: EditorMain): +def _stop_program(main_window: EditorMain) -> None: """停止程式 / Stop program""" from je_editor.pyside_ui.main_ui.menu.run_menu.build_run_menu import stop_program stop_program(main_window) -def _open_search(main_window: EditorMain): +def _open_search(main_window: EditorMain) -> None: """開啟搜尋與取代 / Open search & replace""" widget = _get_editor_widget(main_window) if widget: @@ -182,7 +183,7 @@ class _GitBranchWorker(QObject): finished = Signal(list, str) # (branch_names, current_branch_or_sha) error = Signal(str) - def run(self): + def run(self) -> None: try: from git import Repo import os @@ -205,11 +206,11 @@ class _GitCheckoutWorker(QObject): finished = Signal() error = Signal(str) - def __init__(self, target: str): + def __init__(self, target: str) -> None: super().__init__() self._target = target - def run(self): + def run(self) -> None: try: from git import Repo import os @@ -224,7 +225,7 @@ def run(self): _bg_threads = [] -def _run_in_background(worker, thread_parent): +def _run_in_background(worker: QObject, thread_parent: QObject) -> QThread: """通用的背景執行緒啟動器 / Generic background thread runner""" global _bg_threads thread = QThread(thread_parent) @@ -240,13 +241,13 @@ def _run_in_background(worker, thread_parent): return thread -def _git_refresh_branches(main_window: EditorMain): +def _git_refresh_branches(main_window: EditorMain) -> None: """重新載入 Git 分支清單 (背景執行) / Refresh git branch list in background""" combo: QComboBox = main_window.toolbar_branch_combo worker = _GitBranchWorker() - def on_done(heads, current): + def on_done(heads: list[str], current: str) -> None: combo.clear() if heads: combo.addItems(heads) @@ -261,7 +262,7 @@ def on_done(heads, current): _run_in_background(worker, main_window) -def _git_checkout(main_window: EditorMain): +def _git_checkout(main_window: EditorMain) -> None: """切換 Git 分支 (背景執行) / Checkout git branch in background""" combo: QComboBox = main_window.toolbar_branch_combo target = combo.currentText().strip() @@ -270,7 +271,7 @@ def _git_checkout(main_window: EditorMain): worker = _GitCheckoutWorker(target) - def on_done(): + def on_done() -> None: _git_refresh_branches(main_window) # 同步更新底部面板的 GitGui (如果有的話) # Sync bottom panel GitGui if available @@ -279,7 +280,7 @@ def on_done(): widget.git_gui._refresh_branch_list() widget.git_gui._refresh_change_list() - def on_error(err): + def on_error(err: str) -> None: QMessageBox.critical(main_window, "Checkout Error", err) worker.finished.connect(on_done) diff --git a/je_editor/utils/json_format/json_process.py b/je_editor/utils/json_format/json_process.py index 4292201..44e1eec 100644 --- a/je_editor/utils/json_format/json_process.py +++ b/je_editor/utils/json_format/json_process.py @@ -2,6 +2,7 @@ import sys from json import dumps from json import loads +from typing import Any # 匯入自訂錯誤訊息與例外類別 # Import custom error messages and exception class @@ -11,7 +12,7 @@ from je_editor.utils.logging.loggin_instance import jeditor_logger -def __process_json(json_string: str, **kwargs) -> str: +def __process_json(json_string: str, **kwargs: Any) -> str: """ 功能說明 (Function Description): 嘗試將輸入的 JSON 字串重新格式化 (pretty print)。 @@ -41,7 +42,7 @@ def __process_json(json_string: str, **kwargs) -> str: raise JEditorJsonException(wrong_json_data_error) -def reformat_json(json_string: str, **kwargs) -> str: +def reformat_json(json_string: str, **kwargs: Any) -> str: """ 功能說明 (Function Description): 對外提供的 JSON 格式化函式,會呼叫內部的 __process_json。 diff --git a/je_editor/utils/logging/loggin_instance.py b/je_editor/utils/logging/loggin_instance.py index 318a4b5..e801421 100644 --- a/je_editor/utils/logging/loggin_instance.py +++ b/je_editor/utils/logging/loggin_instance.py @@ -31,8 +31,8 @@ class JEditorLoggingHandler(RotatingFileHandler): # redirect logging stderr output to queue (註解說明,但目前未實作) # 註解提到要將 stderr 輸出導向 queue,但目前程式碼僅繼承 RotatingFileHandler - def __init__(self, filename: str = "JEditor.log", mode="w", - max_bytes: int = 1073741824, backup_count: int = 0): + def __init__(self, filename: str = "JEditor.log", mode: str = "w", + max_bytes: int = 1073741824, backup_count: int = 0) -> None: """ :param filename: 日誌檔案名稱 / log file name :param mode: 檔案開啟模式 (預設 w 覆寫) / file open mode (default "w" overwrite) diff --git a/je_editor/utils/multi_language/multi_language_wrapper.py b/je_editor/utils/multi_language/multi_language_wrapper.py index 4715535..c1ce352 100644 --- a/je_editor/utils/multi_language/multi_language_wrapper.py +++ b/je_editor/utils/multi_language/multi_language_wrapper.py @@ -10,7 +10,7 @@ class LanguageWrapper(object): - A language wrapper to manage the current language and its corresponding dictionary. """ - def __init__(self): + def __init__(self) -> None: # 初始化時記錄日誌 # Log initialization jeditor_logger.info("Init LanguageWrapper") @@ -30,7 +30,7 @@ def __init__(self): # Select the dictionary based on current language self.language_word_dict: dict = self.choose_language_dict.get(self.language, english_word_dict) - def reset_language(self, language) -> None: + def reset_language(self, language: str) -> None: """ 重設語言 (Reset the language) :param language: 任何已註冊的語言鍵 / Any registered language key diff --git a/je_editor/utils/redirect_manager/redirect_manager_class.py b/je_editor/utils/redirect_manager/redirect_manager_class.py index cf89f98..cea6841 100644 --- a/je_editor/utils/redirect_manager/redirect_manager_class.py +++ b/je_editor/utils/redirect_manager/redirect_manager_class.py @@ -12,11 +12,11 @@ class RedirectStdOut(logging.Handler): - Redirect standard output (stdout) to a queue """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.encoding = sys.__stdout__.encoding if sys.__stdout__ else "utf-8" - def write(self, content_to_write) -> None: + def write(self, content_to_write: str) -> None: # 將輸出內容放入 RedirectManager 的 stdout queue # Put output content into RedirectManager's stdout queue redirect_manager_instance.std_out_queue.put(content_to_write) @@ -40,11 +40,11 @@ class RedirectStdErr(logging.Handler): - Redirect standard error (stderr) to a queue """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.encoding = sys.__stderr__.encoding if sys.__stderr__ else "utf-8" - def write(self, content_to_write) -> None: + def write(self, content_to_write: str) -> None: # 將錯誤輸出內容放入 RedirectManager 的 stderr queue # Put error output content into RedirectManager's stderr queue redirect_manager_instance.std_err_queue.put(content_to_write) @@ -70,7 +70,7 @@ class RedirectManager(object): - Provides set_redirect and restore_std methods """ - def __init__(self): + def __init__(self) -> None: jeditor_logger.info("Init RedirectManager") # 建立 stdout 與 stderr 的 queue # Create queues for stdout and stderr From 0df39e3720c9eb0470ff2aeda4f4fbac5af40d8e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 18 Apr 2026 00:40:57 +0800 Subject: [PATCH 2/3] Create CLAUDE.md --- CLAUDE.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..67b2225 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# CLAUDE.md - JEditor Project Guidelines + +## Project Overview + +JEditor is a Python-based code editor built with PySide6 (Qt), featuring syntax highlighting, code formatting, plugin system, Git integration, and LangChain-powered AI assistance. + +- **Language**: Python 3.10+ +- **UI Framework**: PySide6 6.11.0 +- **Package Manager**: pip / setuptools +- **Testing**: pytest + pytest-qt +- **Linting**: ruff, pycodestyle +- **Formatting**: yapf + +## Project Structure + +``` +je_editor/ # Main package + pyside_ui/ # UI layer (Qt widgets) + browser/ # Built-in browser + code/ # Code editor components (syntax, formatting, process management) + dialog/ # Dialog windows + git_ui/ # Git UI components + main_ui/ # Main window and layout + code_scan/ # Ruff linting & watchdog file monitoring + git_client/ # Git operations (GitPython) + plugins/ # Plugin loader system + utils/ # Shared utilities +test/ # Unit tests (pytest) +docs/ # Sphinx documentation +``` + +## Build & Test Commands + +```bash +# Install dependencies +pip install -r requirements.txt + +# Install dev dependencies +pip install -r dev_requirements.txt + +# Run tests (excludes Qt UI tests by default) +pytest + +# Build package (stable) +python -m build + +# Build dev package (rename pyproject.toml <-> dev.toml first) +python -m build +``` + +## Design Principles + +### Design Patterns + +- **MVC separation**: UI widgets (View) in `pyside_ui/`, business logic (Model/Controller) in `code_scan/`, `git_client/`, `utils/` +- **Plugin architecture**: Extend functionality via `plugins/plugin_loader.py` without modifying core code +- **Observer pattern**: Use Qt signals/slots for decoupled event communication between components +- **Strategy pattern**: Code formatting and syntax highlighting are interchangeable strategies +- **Single Responsibility**: Each module handles one concern; avoid god classes + +### Software Engineering + +- **DRY**: Extract shared logic into `utils/`; never duplicate code across modules +- **SOLID principles**: Favor composition over inheritance; depend on abstractions +- **Fail fast**: Validate inputs at boundaries; raise clear exceptions early +- **Minimal public API**: Keep internal methods private (`_prefix`); expose only what consumers need +- **Type hints**: All function signatures must include type annotations + +### Performance + +- **Lazy loading**: Defer heavy imports and widget initialization until needed +- **Thread safety**: Run file I/O, Git operations, linting, and process execution in `QThread` or background threads — never block the UI thread +- **Efficient string handling**: Use `QTextCursor` batch operations for bulk text edits; avoid repeated `setText()` calls +- **Resource cleanup**: Always release file handles, threads, and subprocesses in `closeEvent` or `deleteLater` +- **Debounce**: Throttle expensive operations triggered by rapid user input (typing, resizing) + +### Security (Mandatory) + +- **No shell injection**: Never pass unsanitized user input to `subprocess.Popen(shell=True)` or `os.system()`. Use `subprocess.run()` with argument lists +- **Path traversal prevention**: Validate and sanitize all file paths from user input; reject paths containing `..` when operating within a project directory +- **No eval/exec on user data**: Never use `eval()`, `exec()`, or `compile()` on untrusted input +- **Sensitive data**: Never hardcode API keys, tokens, or credentials. Load from environment variables or config files excluded via `.gitignore` +- **Dependency awareness**: Pin dependency versions; review before upgrading +- **Input validation**: Sanitize all external input (file content, plugin data, user dialog input) at system boundaries + +## Code Style + +- Follow PEP 8; enforced by ruff +- Use `snake_case` for functions/variables, `PascalCase` for classes +- Keep functions short and focused (< 50 lines preferred) +- Remove dead code — do not comment out unused blocks or leave `# TODO` stubs without tracking + +## Git & Commit Rules + +- **Commit messages**: Write in English, concise, imperative mood (e.g., "Add plugin hot-reload support") +- **No AI attribution**: Do not mention any AI tool, assistant, or model name in commit messages or code comments +- **Branch strategy**: `main` = stable release, `dev` = active development +- **Clean commits**: Each commit should be a single logical change; no unrelated changes bundled together From 2da1acbca2f1d4c755f15312017523b28263abaf Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 18 Apr 2026 14:26:21 +0800 Subject: [PATCH 3/3] Add SonarQube/Codacy compliance rules and fix violations - Document static analysis rules in CLAUDE.md (complexity, exception handling, security, resource management) - Replace broad except Exception: pass with specific exception types and debug-level logging in git_action, base_process_manager, git_client_gui - Replace print() with jeditor_logger in watchdog_thread and json_process - Annotate intentional shell=True in shell_exec with nosec/noqa and rationale --- CLAUDE.md | 58 +++++++++++++++++++ je_editor/code_scan/watchdog_thread.py | 16 ++--- je_editor/git_client/git_action.py | 11 ++-- .../pyside_ui/code/base_process_manager.py | 12 ++-- .../code/shell_process/shell_exec.py | 5 +- .../git_ui/git_client/git_client_gui.py | 6 +- je_editor/utils/json_format/json_process.py | 7 +-- 7 files changed, 90 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 67b2225..8ac431f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,64 @@ python -m build - Keep functions short and focused (< 50 lines preferred) - Remove dead code — do not comment out unused blocks or leave `# TODO` stubs without tracking +## Static Analysis Compliance (SonarQube / Codacy) + +All code must pass SonarQube and Codacy quality gates. Adhere to the following rules: + +### Complexity & Maintainability + +- **Cognitive complexity**: Keep functions below 15 (SonarQube S3776). Break deeply nested logic into helper functions +- **Cyclomatic complexity**: Functions should stay under 10 branches; extract conditionals into smaller functions +- **Function length**: Soft limit 50 lines, hard limit 80 lines of executable code +- **Parameter count**: Max 7 parameters per function (SonarQube S107); use dataclasses or `**kwargs` for larger sets +- **Nesting depth**: Max 4 levels of nested control flow (SonarQube S134) +- **No duplicate code**: Extract 3+ line repeated blocks into shared utilities (SonarQube copy-paste detector) +- **String literal duplication**: Extract any string literal used 3+ times into a module-level constant (SonarQube S1192) +- **No magic numbers**: Replace unnamed numeric literals with named constants (SonarQube S109); exceptions: `0`, `1`, `-1`, `2` + +### Exception Handling + +- **No bare `except:`**: Always specify exception types (SonarQube S5754, Codacy PyLint W0702) +- **No silent swallowing**: Never `except: pass` without logging or re-raising (SonarQube S2737) +- **No overly broad `except Exception`** unless logged and re-raised at boundaries +- **Chain exceptions**: Use `raise NewError(...) from original_error` to preserve context +- **Use `logging` over `print`** for diagnostics in library/production code (SonarQube S4792) + +### Code Quality + +- **No commented-out code**: Delete it — rely on git history (SonarQube S125) +- **No unused imports/variables/parameters**: Remove them (SonarQube S1128, S1854) +- **Explicit `None` checks**: Use `is None` / `is not None`, never `== None` (SonarQube S2197) +- **No redundant boolean**: `if x:` not `if x == True:`; `if not x:` not `if x == False:` +- **Consistent return types**: A function should always return the same type (or always `None`); avoid `return None` in numeric functions +- **No assignment in conditions**: Avoid `if (x := func()):` in complex expressions (SonarQube S1121) +- **Identifier naming**: Min 3 characters except loop counters (`i`, `j`, `k`); no single-letter names for non-trivial scope +- **String formatting**: Prefer f-strings over `%` or `.format()` unless logging (logging uses `%` lazy formatting) + +### Security (SonarQube / Codacy Bandit rules) + +- **No hardcoded credentials** (SonarQube S2068, Bandit B105/B106): passwords, tokens, keys +- **No weak hashing** for security contexts (SonarQube S4790, Bandit B303): MD5/SHA1 only allowed for non-security uses (e.g., cache keys) with explicit comment +- **No `random` for security** (Bandit B311): use `secrets` module for tokens, IDs, crypto +- **No `pickle`/`marshal` on untrusted data** (Bandit B301/B302) +- **No `yaml.load` without `SafeLoader`** (Bandit B506) +- **No `tempfile.mktemp`** (Bandit B306): use `NamedTemporaryFile` / `mkstemp` +- **No `assert` in production logic** (Bandit B101): asserts are stripped with `-O`; use explicit `raise` +- **No XML parsers vulnerable to XXE** (Bandit B313-B320): use `defusedxml` +- **TLS verification**: Never `verify=False` in `requests` or urllib calls + +### Resource Management + +- **Always use context managers**: `with open(...)`, `with lock`, `with QMutexLocker(...)` — never manual `.close()` without `try/finally` +- **Close Qt resources**: call `deleteLater()` or use `setAttribute(Qt.WA_DeleteOnClose)` for modal dialogs +- **Encoding explicit**: always pass `encoding='utf-8'` to `open()` (SonarQube S5122 / Ruff PLW1514) + +### Testing & Documentation + +- **No empty test functions** (SonarQube S1186) +- **No identical test cases** (SonarQube S4144) +- **Public API docstrings**: all public classes/functions should have docstrings describing purpose, args, returns, raises + ## Git & Commit Rules - **Commit messages**: Write in English, concise, imperative mood (e.g., "Add plugin hot-reload support") diff --git a/je_editor/code_scan/watchdog_thread.py b/je_editor/code_scan/watchdog_thread.py index cc733c7..277b981 100644 --- a/je_editor/code_scan/watchdog_thread.py +++ b/je_editor/code_scan/watchdog_thread.py @@ -1,4 +1,3 @@ -import sys import threading import time from pathlib import Path @@ -6,6 +5,7 @@ from watchdog.observers import Observer from je_editor.code_scan.watchdog_implement import RuffPythonFileChangeHandler +from je_editor.utils.logging.loggin_instance import jeditor_logger class WatchdogThread(threading.Thread): @@ -28,13 +28,13 @@ def __init__(self, check_path: str) -> None: def run(self) -> None: """Start the watchdog observer loop.""" if not self.check_path.exists(): - print(f"[Error] Path does not exist: {self.check_path}", file=sys.stderr) + jeditor_logger.error(f"[Watchdog] Path does not exist: {self.check_path}") return # 設定監控 self.observer.schedule(self.ruff_handler, str(self.check_path), recursive=True) self.observer.start() - print(f"[Watchdog] Monitoring started on {self.check_path}") + jeditor_logger.info(f"[Watchdog] Monitoring started on {self.check_path}") try: while self.running: @@ -42,11 +42,11 @@ def run(self) -> None: # 這裡可以加上 queue 輸出處理 self._process_ruff_output() except KeyboardInterrupt: - print("[Watchdog] Interrupted by user") + jeditor_logger.info("[Watchdog] Interrupted by user") finally: self.observer.stop() self.observer.join() - print("[Watchdog] Monitoring stopped") + jeditor_logger.info("[Watchdog] Monitoring stopped") def stop(self) -> None: """Stop the watchdog thread safely.""" @@ -56,11 +56,11 @@ def _process_ruff_output(self) -> None: """Process stdout/stderr queues from Ruff threads.""" while not self.ruff_handler.stdout_queue.empty(): line = self.ruff_handler.stdout_queue.get() - print(f"[Ruff STDOUT] {line}") + jeditor_logger.info(f"[Ruff STDOUT] {line}") while not self.ruff_handler.stderr_queue.empty(): line = self.ruff_handler.stderr_queue.get() - print(f"[Ruff STDERR] {line}", file=sys.stderr) + jeditor_logger.error(f"[Ruff STDERR] {line}") if __name__ == '__main__': @@ -73,6 +73,6 @@ def _process_ruff_output(self) -> None: while True: time.sleep(1) except KeyboardInterrupt: - print("[Main] Stopping watchdog...") + jeditor_logger.info("[Main] Stopping watchdog...") watchdog_thread.stop() watchdog_thread.join() diff --git a/je_editor/git_client/git_action.py b/je_editor/git_client/git_action.py index 8fccdb8..4c85788 100644 --- a/je_editor/git_client/git_action.py +++ b/je_editor/git_client/git_action.py @@ -5,6 +5,8 @@ from PySide6.QtCore import QThread, Signal from git import Repo, GitCommandError, InvalidGitRepositoryError, NoSuchPathError +from je_editor.utils.logging.loggin_instance import jeditor_logger + # Simple audit logger def audit_log(repo_path: str, action: str, detail: str, ok: bool, err: str = "") -> None: @@ -17,8 +19,9 @@ def audit_log(repo_path: str, action: str, detail: str, ok: bool, err: str = "") with open(path, "a", encoding="utf-8") as f: ts = datetime.now().isoformat(timespec="seconds") f.write(f"{ts}\taction={action}\tok={ok}\tdetail={detail}\terr={err}\n") - except Exception: - pass # Never let audit logging failure break the UI + except OSError as audit_err: + # Never let audit logging failure break the UI; record at debug level + jeditor_logger.debug(f"audit_log write failed: {audit_err}") # Git service layer @@ -91,8 +94,8 @@ def show_diff_of_commit(self, commit_sha: str) -> str: for d in diffs: try: text.append(d.diff.decode("utf-8", errors="replace")) - except Exception: - pass + except (UnicodeDecodeError, AttributeError) as decode_err: + jeditor_logger.debug(f"diff decode skipped: {decode_err}") out = "".join(text) if text else "(No patch content)" audit_log(self.repo_path, "show_diff", commit_sha, True) return out diff --git a/je_editor/pyside_ui/code/base_process_manager.py b/je_editor/pyside_ui/code/base_process_manager.py index 02686ef..cbcc33c 100644 --- a/je_editor/pyside_ui/code/base_process_manager.py +++ b/je_editor/pyside_ui/code/base_process_manager.py @@ -159,8 +159,8 @@ def _cleanup_in_background(threads: list, process: subprocess.Popen | None) -> N for t in threads: try: t.join(timeout=2) - except Exception: - pass + except RuntimeError as join_err: + jeditor_logger.debug(f"thread join failed during cleanup: {join_err}") if process is not None: try: process.terminate() @@ -169,10 +169,10 @@ def _cleanup_in_background(threads: list, process: subprocess.Popen | None) -> N try: process.kill() process.wait(timeout=2) - except Exception: - pass - except OSError: - pass + except (OSError, subprocess.TimeoutExpired) as kill_err: + jeditor_logger.debug(f"process kill failed during cleanup: {kill_err}") + except OSError as term_err: + jeditor_logger.debug(f"process terminate failed during cleanup: {term_err}") def _exit_message_prefix(self) -> str: """退出訊息前綴 (子類別覆寫) / Exit message prefix (override in subclass)""" diff --git a/je_editor/pyside_ui/code/shell_process/shell_exec.py b/je_editor/pyside_ui/code/shell_process/shell_exec.py index 7f56e3a..ee2577f 100644 --- a/je_editor/pyside_ui/code/shell_process/shell_exec.py +++ b/je_editor/pyside_ui/code/shell_process/shell_exec.py @@ -87,7 +87,10 @@ def exec_shell(self, shell_command: Union[str, list]) -> None: text_format.setForeground(actually_color_dict.get("normal_output_color")) text_cursor.insertText(str(args), text_format) text_cursor.insertBlock() - self.process = subprocess.Popen( + # shell=True is required: this is the user-facing shell execution feature + # of the editor, invoked only with commands the user explicitly types. + # Not a user-input-driven pipeline from untrusted data. + self.process = subprocess.Popen( # noqa: S602 # nosec B602 args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py b/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py index ef3e305..1b4f2ab 100644 --- a/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +++ b/je_editor/pyside_ui/git_ui/git_client/git_client_gui.py @@ -10,6 +10,8 @@ ) from git import Repo, InvalidGitRepositoryError, NoSuchPathError, GitCommandError +from je_editor.utils.logging.loggin_instance import jeditor_logger + class _GitWorker(QObject): """背景執行 Git 操作的 Worker / Background worker for Git operations""" @@ -587,8 +589,8 @@ def on_unstage_selected_changes(self) -> None: for file_path in file_paths: try: self.current_repo.index.remove([file_path], working_tree=True) - except Exception: - pass + except (GitCommandError, OSError) as remove_err: + jeditor_logger.debug(f"index.remove skipped for {file_path}: {remove_err}") self._refresh_change_list() except GitCommandError as e: QMessageBox.critical(self, "Unstage Error", str(e)) diff --git a/je_editor/utils/json_format/json_process.py b/je_editor/utils/json_format/json_process.py index 44e1eec..1f992ff 100644 --- a/je_editor/utils/json_format/json_process.py +++ b/je_editor/utils/json_format/json_process.py @@ -1,5 +1,4 @@ import json.decoder -import sys from json import dumps from json import loads from typing import Any @@ -27,9 +26,9 @@ def __process_json(json_string: str, **kwargs: Any) -> str: # Try to parse string into JSON, then dump with indentation return dumps(loads(json_string), indent=4, sort_keys=True, **kwargs) except json.JSONDecodeError as error: - # 如果 JSON 格式錯誤,輸出錯誤訊息到 stderr 並拋出例外 - # If JSON format is invalid, print error to stderr and raise exception - print(wrong_json_data_error, file=sys.stderr) + # 如果 JSON 格式錯誤,記錄錯誤訊息並拋出例外 + # If JSON format is invalid, log error and raise exception + jeditor_logger.error(wrong_json_data_error) raise error except TypeError: # 如果輸入不是合法 JSON 字串,嘗試直接將物件轉為 JSON