From c4ccfea15a8edc7287ad4a56ad0e57d502ca8a12 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 13 Oct 2024 00:58:05 -0500 Subject: [PATCH] Update utilities module [add event handler logging] --- .pre-commit-config.yaml | 8 +++---- pyproject.toml | 7 ++---- src/idlealign/__init__.py | 1 + src/idlealign/extension.py | 19 +++++++++++---- src/idlealign/utils.py | 47 ++++++++++++++++++++++++++++++++++++-- tests/test_utils.py | 1 + 6 files changed, 67 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 571088d..4bdcbc5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -28,17 +28,17 @@ repos: types: [file] types_or: [python, pyi] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: v0.6.9 hooks: - id: ruff types: [file] types_or: [python, pyi, toml] - repo: https://github.com/CoolCat467/badgie - rev: v0.9.5 + rev: v0.9.6 hooks: - id: badgie - repo: https://github.com/codespell-project/codespell diff --git a/pyproject.toml b/pyproject.toml index f7438ad..8390ae0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,13 +11,12 @@ authors = [ description = "Emacs Align by Regular Expression for IDLE" readme = {file = "README.md", content-type = "text/markdown"} license = {file = "LICENSE"} -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -110,7 +109,6 @@ select = [ "SIM", # flake8-simplify "SLOT", # flake8-slots "TCH", # flake8-type-checking - "TRIO", # flake8-trio "UP", # pyupgrade "W", # Warning "YTT", # flake8-2020 @@ -170,12 +168,11 @@ partial_branches = [ [tool.tox] legacy_tox_ini = """ [tox] - envlist = py38, py39, py310, py311, py312, mypy, pytest + envlist = py39, py310, py311, py312, mypy, pytest isolated_build = false [gh-actions] python = - 3.8: py38, mypy, pytest 3.9: py39, mypy, pytest 3.10: py310, mypy, pytest 3.11: py311, mypy, pytest diff --git a/src/idlealign/__init__.py b/src/idlealign/__init__.py index 609aa25..d424c9c 100644 --- a/src/idlealign/__init__.py +++ b/src/idlealign/__init__.py @@ -34,6 +34,7 @@ def check_installed() -> bool: return utils.check_installed(__title__, __version__, idlealign) +utils.set_title(__title__) idlealign.reload() diff --git a/src/idlealign/extension.py b/src/idlealign/extension.py index 9aa92f0..b5f8170 100644 --- a/src/idlealign/extension.py +++ b/src/idlealign/extension.py @@ -35,7 +35,7 @@ from re import Pattern -class AlignDialog(SearchDialogBase): # type: ignore[misc] +class AlignDialog(SearchDialogBase): """Dialog for aligning by a pattern in text.""" __slots__ = ( @@ -100,7 +100,7 @@ def store_prefs(self) -> None: self.search_params = utils.get_search_engine_params(self.engine) utils.set_search_engine_params(self.engine, self.global_search_params) - def open( + def open( # type: ignore # "override" self, searchphrase: str | None = None, insert_tags: str | list[str] | tuple[str, ...] = (), @@ -173,7 +173,7 @@ def default_command(self, _event: Event[Any] | None = None) -> bool: pattern = self.engine.getprog() if not pattern: - return False + return False # type: ignore # "unreachable" space_wrap: bool = self.space_wrap_var.get() align_side: bool = self.align_side_var.get() @@ -220,6 +220,7 @@ def window(self) -> AlignDialog: """Window for current text widget.""" return self.create_window() + @utils.log_exceptions def create_window(self) -> AlignDialog: """Create align dialog window.""" root: Tk @@ -228,9 +229,17 @@ def create_window(self) -> AlignDialog: engine: searchengine.SearchEngine = searchengine.get(root) if not hasattr(engine, "_aligndialog"): - engine._aligndialog = AlignDialog(root, engine, self) - return cast(AlignDialog, engine._aligndialog) + engine._aligndialog = AlignDialog( # type: ignore[attr-defined] + root, + engine, + self, + ) + return cast( + AlignDialog, + engine._aligndialog, # type: ignore[attr-defined] + ) + @utils.log_exceptions def align_selection( self, selection: tuple[str, str], diff --git a/src/idlealign/utils.py b/src/idlealign/utils.py index b3b744c..3dc94b6 100644 --- a/src/idlealign/utils.py +++ b/src/idlealign/utils.py @@ -25,21 +25,40 @@ __license__ = "GNU General Public License Version 3" import sys +import time +import traceback from contextlib import contextmanager +from functools import wraps from idlelib import search, searchengine from idlelib.config import idleConf from os.path import abspath +from pathlib import Path from tkinter import TclError, Text, Tk, messagebox -from typing import TYPE_CHECKING, ClassVar, NamedTuple +from typing import TYPE_CHECKING, ClassVar, NamedTuple, TypeVar if TYPE_CHECKING: - from collections.abc import Generator, Sequence + from collections.abc import Callable, Generator, Sequence from idlelib.editor import EditorWindow from idlelib.format import FormatRegion from idlelib.iomenu import IOBinding from idlelib.pyshell import PyShellEditorWindow, PyShellFileList from idlelib.undo import UndoDelegator + from typing_extensions import ParamSpec + + PS = ParamSpec("PS") + +T = TypeVar("T") + +LOGS_PATH = Path(idleConf.userdir) / "logs" +TITLE: str = __title__ + + +def set_title(title: str) -> None: + """Set program title.""" + global TITLE + TITLE = title + def get_required_config( values: dict[str, str], @@ -47,6 +66,8 @@ def get_required_config( extension_title: str, ) -> str: """Get required configuration file data.""" + if __title__ == TITLE: + set_title(extension_title) config = "" # Get configuration defaults settings = "\n".join( @@ -309,6 +330,28 @@ def undo_block(undo: UndoDelegator) -> Generator[None, None, None]: undo.undo_block_stop() +def log_exceptions(function: Callable[PS, T]) -> Callable[PS, T]: + """Log any exceptions raised.""" + + @wraps(function) + def wrapper(*args: PS.args, **kwargs: PS.kwargs) -> T: + """Catch Exceptions, log them to log file, and re-raise.""" + try: + return function(*args, **kwargs) + except Exception as exc: + if not LOGS_PATH.exists(): + LOGS_PATH.mkdir(exist_ok=True) + log_file = LOGS_PATH / f"{TITLE}.log" + with log_file.open("a", encoding="utf-8") as fp: + format_time = time.strftime("[%Y-%m-%d %H:%M:%S] ") + exception_text = "".join(traceback.format_exception(exc)) + for line in exception_text.splitlines(keepends=True): + fp.write(f"{format_time}{line}") + raise + + return wrapper + + class Comment(NamedTuple): """Represents one comment.""" diff --git a/tests/test_utils.py b/tests/test_utils.py index f64b8d6..a2e06e3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import pytest + from idlealign import utils