diff --git a/Pipfile b/Pipfile index e89a3d3..3f52182 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ pyside6 = "~=6.3" requests = "~=2.28" requests-futures = "~=1.0" packaging = "~=21.3" +loguru = "~=0.6" [dev-packages] pyinstaller = "~=5.1" diff --git a/Pipfile.lock b/Pipfile.lock index bee7e65..28a8016 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "633aca7e0b43a295ce3429d3e12976a4c16dcf422cee53e37d266f8000ef5feb" + "sha256": "b04dfa0eda7fd2df5b6d27d5a1f7d71168c984c74ce363c45a888430bca7b7d8" }, "pipfile-spec": 6, "requires": { @@ -40,6 +40,14 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, + "loguru": { + "hashes": [ + "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c", + "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3" + ], + "index": "pypi", + "version": "==0.6.0" + }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", diff --git a/setup.cfg b/setup.cfg index 7696758..847f79f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ install_requires = requests requests-futures packaging + loguru [options.packages.find] where = src diff --git a/src/fime/about.py b/src/fime/about.py index a2049bb..5288d00 100644 --- a/src/fime/about.py +++ b/src/fime/about.py @@ -1,22 +1,22 @@ import sys -import traceback from copy import copy +from pathlib import Path from textwrap import dedent from threading import Lock from typing import Optional +from loguru import logger from packaging.version import Version from requests_futures.sessions import FuturesSession -from fime.progressindicator import ProgressIndicator -from fime.util import get_icon - try: from PySide6 import QtCore, QtGui, QtWidgets except ImportError: from PySide2 import QtCore, QtGui, QtWidgets import fime +from fime.progressindicator import ProgressIndicator +from fime.util import get_icon URL = "https://gitlab.com/faerbit/fime/-/releases/permalink/latest" @@ -51,8 +51,7 @@ class UpdateChecker: else: self.result = f"Newer fime version available: {latest_version}" except Exception: - print("Could not get update info:\n", file=sys.stderr) - print(traceback.format_exc(), file=sys.stderr) + logger.exception("Could not get update info") finally: with self.lock: self._done = True @@ -63,6 +62,8 @@ class About(QtWidgets.QDialog): super().__init__(parent, *args, **kwargs) self.setWindowTitle("About") + log_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation)) / "logs" + text = dedent(f"""\ fime Copyright (c) 2020 - 2022 Faerbit @@ -71,6 +72,8 @@ class About(QtWidgets.QDialog): fime Version {fime.__version__} Qt Version {QtCore.__version__} Python Version {sys.version} + + Log directory: {log_dir_path} """) text = text.replace("\n", "
") version_label = QtWidgets.QLabel(self) diff --git a/src/fime/config.py b/src/fime/config.py index 8a7b6bf..eff46f2 100644 --- a/src/fime/config.py +++ b/src/fime/config.py @@ -1,6 +1,8 @@ from configparser import ConfigParser from pathlib import Path +from loguru import logger + try: from PySide6 import QtCore except ImportError: @@ -22,7 +24,7 @@ class Config: config_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppConfigLocation)) config_path = config_dir_path / "fime.conf" if config_path.exists(): - print(f'Reading config file "{config_path}"') + logger.info(f'Reading config file "{config_path}"') with open(config_path) as f: config_text = f.read() config_text = "[DEFAULT]\n" + config_text @@ -30,7 +32,7 @@ class Config: if (not self._configparser.has_option("DEFAULT", "jira_url") or not self._configparser.has_option("DEFAULT", "jira_token")): raise FimeException(f'Please add config file {config_path} ' - f'with config keys "jira_url" and "jira_token" in INI style') + f'with config keys "jira_url" and "jira_token" in INI style') @property def jira_url(self): diff --git a/src/fime/data.py b/src/fime/data.py index c1e29ca..4101ab0 100644 --- a/src/fime/data.py +++ b/src/fime/data.py @@ -8,6 +8,8 @@ from datetime import datetime, date, timedelta, time from threading import Thread, Event from typing import List, Tuple, Dict, Union +from loguru import logger + try: from PySide6 import QtCore except ImportError: @@ -63,7 +65,7 @@ class Data(MutableMapping): def _save(self): for key in self._hot_keys: - print(f"... saving dict {key} ...") + logger.info(f"saving dict {key}") to_write = self._cache[key] # apparently thread-safe with open(self.data_path.format(key), "w+") as f: f.write(json.dumps(to_write)) diff --git a/src/fime/main.py b/src/fime/main.py index 9e3db24..8b13bd2 100755 --- a/src/fime/main.py +++ b/src/fime/main.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 +import os import signal import sys from functools import partial +from pathlib import Path -from fime.about import About -from fime.config import Config -from fime.worklog import WorklogDialog +from loguru import logger try: from PySide6 import QtCore, QtWidgets @@ -14,6 +14,9 @@ except ImportError: from PySide2 import QtCore, QtWidgets PYSIDE_6 = False +from fime.about import About +from fime.config import Config +from fime.worklog import WorklogDialog from fime.data import Tasks, Log, Data, LogCommentsData, Worklog, Report from fime.exceptions import FimeException from fime.import_task import ImportTask @@ -143,7 +146,7 @@ class App: action.triggered.connect(item[1]) def sigterm_handler(self, signo, _frame): - print(f'handling signal "{signal.strsignal(signo)}"') + logger.debug(f'handling signal "{signal.strsignal(signo)}"') self.app.quit() def run(self): @@ -164,19 +167,27 @@ class App: self.taskEdit.show() -def excepthook(original, e_type, e_value, tb_obj): +def init_logging(): + log_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation)) / "logs" + logger.add(log_dir_path / "fime_{time:YYYY-MM-DD}.log", rotation="1d", retention="30d", compression="zip", level="INFO") + + +def excepthook(e_type, e_value, tb_obj): if e_type is FimeException: QtWidgets.QMessageBox.critical(None, "Error", str(e_value), QtWidgets.QMessageBox.Ok) + elif issubclass(e_type, KeyboardInterrupt): + sys.__excepthook__(e_type, e_value, tb_obj) else: - original(e_type, e_value, tb_obj) + logger.critical("Unhandled exception", exc_info=(e_type, e_value, tb_obj)) + sys.__excepthook__(e_type, e_value, tb_obj) def main(): # important for QStandardPath to be correct QtCore.QCoreApplication.setApplicationName("fime") + init_logging() # also catches exceptions in other threads - original_excepthook = sys.excepthook - sys.excepthook = partial(excepthook, original_excepthook) + sys.excepthook = excepthook app = App() app.run() diff --git a/src/fime/task_completer.py b/src/fime/task_completer.py index 8d4064d..e0d2b8b 100644 --- a/src/fime/task_completer.py +++ b/src/fime/task_completer.py @@ -1,11 +1,11 @@ import os -import sys import threading -import traceback from enum import Enum, auto from functools import reduce, partial from queue import Queue, Empty +from loguru import logger + try: from PySide6 import QtCore, QtWidgets except ImportError: @@ -105,12 +105,11 @@ class TaskCompleter(QtWidgets.QCompleter): }) else: if not self.escalate: - print("No picker results. Escalating") + logger.debug("No picker results. Escalating") self.escalate = True self.update_search() except Exception: - print("Ignoring exception, as it only breaks autocompletion:", file=sys.stderr) - print(traceback.format_exc(), file=sys.stderr) + logger.exception("Ignoring exception, as it only breaks autocompletion") return def update_search(self): @@ -144,6 +143,5 @@ class TaskCompleter(QtWidgets.QCompleter): "result": extracted, }) except Exception: - print("Ignoring exception, as it only breaks autocompletion:", file=sys.stderr) - print(traceback.format_exc(), file=sys.stderr) + logger.exception("Ignoring exception, as it only breaks autocompletion") return diff --git a/src/fime/util.py b/src/fime/util.py index 3ffb33f..fb66ef7 100644 --- a/src/fime/util.py +++ b/src/fime/util.py @@ -1,5 +1,7 @@ import enum +from loguru import logger + try: from PySide6 import QtCore, QtGui, QtWidgets except ImportError: @@ -13,7 +15,7 @@ def get_screen_height(qobject): if hasattr(qobject, "screen"): return qobject.screen().size().height() else: - print("unable to detect screen height falling back to default value of 1080") + logger.info("unable to detect screen height falling back to default value of 1080") return 1080 @@ -21,7 +23,7 @@ def get_screen_width(qobject): if hasattr(qobject, "screen"): return qobject.screen().size().width() else: - print("unable to detect screen width falling back to default value of 1920") + logger.info("unable to detect screen width falling back to default value of 1920") return 1920 diff --git a/src/fime/worklog_rest.py b/src/fime/worklog_rest.py index 22275d3..9a788fc 100644 --- a/src/fime/worklog_rest.py +++ b/src/fime/worklog_rest.py @@ -1,6 +1,4 @@ import os -import sys -import traceback from concurrent.futures import Future from datetime import date, datetime, timedelta, time from functools import partial @@ -9,6 +7,7 @@ from threading import Lock from typing import List, Dict, Tuple, Optional import requests +from loguru import logger from requests_futures.sessions import FuturesSession from fime.config import Config @@ -42,12 +41,9 @@ class WorklogRest: future.add_done_callback(self._resp_user) return future + @logger.catch(message="Could not get user key") def _resp_user(self, future): - try: - self._user = future.result().json()["key"] - except Exception: - print("Could not get user key:\n", file=sys.stderr) - print(traceback.format_exc(), file=sys.stderr) + self._user = future.result().json()["key"] def get_issues_state(self, issue_keys: List[str], pdate: date) -> List[Tuple[Status, str, Optional[str]]]: ret = [] @@ -114,9 +110,9 @@ class WorklogRest: worklog_found = True break if worklog_found: - print(f"Found existing worklog for issue {issue_key}") + logger.debug(f"Found existing worklog for issue {issue_key}") else: - print(f"Did not find existing worklog for issue {issue_key}") + logger.debug(f"Did not find existing worklog for issue {issue_key}") self._issue_state[issue_key] = (Status.OK, issue_title) def _upload_sanity_check(self, issue_keys: List[str]): @@ -183,7 +179,7 @@ class WorklogRest: with self._issues_lock: if resp.status_code in (200, 201): self._issue_state[issue_key] = (Status.OK, "Successfully uploaded") - print(f"Successfully uploaded issue {issue_key}") + logger.info(f"Successfully uploaded issue {issue_key}") else: msg = dedent(f"""\ Worklog upload failed: @@ -193,4 +189,4 @@ class WorklogRest: Response: {resp.text} """) self._issue_state[issue_key] = (Status.ERROR, msg) - print(msg, file=sys.stderr) + logger.error(msg)