diff --git a/jrnl/cli.py b/jrnl/cli.py index e90c8b66..03b4f2f0 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -5,11 +5,13 @@ import logging import sys import traceback -from .jrnl import run -from .args import parse_args -from .exception import JrnlException +from jrnl.jrnl import run +from jrnl.args import parse_args from jrnl.output import print_msg -from jrnl.output import Message +from jrnl.exception import JrnlException +from jrnl.messages import Message +from jrnl.messages import MsgText +from jrnl.messages import MsgType def configure_logger(debug=False): @@ -37,23 +39,22 @@ def cli(manual_args=None): return run(args) except JrnlException as e: - print_msg(e.title, e.message, msg=Message.ERROR) + e.print() return 1 except KeyboardInterrupt: - print_msg("KeyboardInterrupt", "Aborted by user", msg=Message.ERROR) + print_msg(Message(MsgText.KeyboardInterruptMsg, MsgType.WARNING)) return 1 except Exception as e: - # uncaught exception - if args.debug: - print("\n") - traceback.print_tb(sys.exc_info()[2]) - return 1 + try: + is_debug = args.debug # type: ignore + except NameError: + # error happened before args were parsed + is_debug = "--debug" in sys.argv[1:] - file_issue = ( - "\n\nThis is probably a bug. Please file an issue at:" - + "\nhttps://github.com/jrnl-org/jrnl/issues/new/choose" - ) - print_msg(f"{type(e).__name__}\n", f"{e}{file_issue}", msg=Message.ERROR) + if is_debug: + traceback.print_tb(sys.exc_info()[2]) + + print_msg(Message(MsgText.UncaughtException, MsgType.ERROR, {"exception": e})) return 1 diff --git a/jrnl/commands.py b/jrnl/commands.py index 8d203c7e..c795402a 100644 --- a/jrnl/commands.py +++ b/jrnl/commands.py @@ -13,8 +13,10 @@ avoid any possible overhead for these standalone commands. """ import platform import sys -from .exception import JrnlException -from .exception import JrnlExceptionMessage +from jrnl.exception import JrnlException +from jrnl.messages import Message +from jrnl.messages import MsgText +from jrnl.messages import MsgType def preconfig_diagnostic(_): @@ -72,9 +74,14 @@ def postconfig_encrypt(args, config, original_config, **kwargs): if hasattr(journal, "can_be_encrypted") and not journal.can_be_encrypted: raise JrnlException( - JrnlExceptionMessage.CannotEncryptJournalType, - journal_name=args.journal_name, - journal_type=journal.__class__.__name__, + Message( + MsgText.CannotEncryptJournalType, + MsgType.ERROR, + { + "journal_name": args.journal_name, + "journal_type": journal.__class__.__name__, + }, + ) ) journal.config["encrypt"] = True diff --git a/jrnl/config.py b/jrnl/config.py index 80080123..e1929a1d 100644 --- a/jrnl/config.py +++ b/jrnl/config.py @@ -7,8 +7,11 @@ import yaml import xdg.BaseDirectory from . import __version__ -from .exception import JrnlException -from .exception import JrnlExceptionMessage +from jrnl.exception import JrnlException +from jrnl.messages import Message +from jrnl.messages import MsgText +from jrnl.messages import MsgType + from .color import ERROR_COLOR from .color import RESET_COLOR from .output import list_journals @@ -70,11 +73,17 @@ def get_config_path(): config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) except FileExistsError: raise JrnlException( - JrnlExceptionMessage.ConfigDirectoryIsFile, - config_directory_path=os.path.join( - xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE + Message( + MsgText.ConfigDirectoryIsFile, + MsgType.ERROR, + { + "config_directory_path": os.path.join( + xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE + ) + }, ), ) + return os.path.join( config_directory_path or os.path.expanduser("~"), DEFAULT_CONFIG_NAME ) diff --git a/jrnl/editor.py b/jrnl/editor.py index 90ecb199..ff304826 100644 --- a/jrnl/editor.py +++ b/jrnl/editor.py @@ -11,8 +11,11 @@ from jrnl.color import RESET_COLOR from jrnl.os_compat import on_windows from jrnl.os_compat import split_args from jrnl.output import print_msg -from jrnl.output import Message + from jrnl.exception import JrnlException +from jrnl.messages import Message +from jrnl.messages import MsgText +from jrnl.messages import MsgType def get_text_from_editor(config, template=""): @@ -50,17 +53,25 @@ def get_text_from_editor(config, template=""): def get_text_from_stdin(): - _how_to_quit = "Ctrl+z and then Enter" if on_windows() else "Ctrl+d" print_msg( - "Writing Entry", - f"To finish writing, press {_how_to_quit} on a blank line.", - msg=Message.NORMAL, + Message( + MsgText.WritingEntryStart, + MsgType.TITLE, + { + "how_to_quit": MsgText.HowToQuitWindows.value + if on_windows() + else MsgText.HowToQuitLinux.value + }, + ) ) + try: raw = sys.stdin.read() except KeyboardInterrupt: logging.error("Write mode: keyboard interrupt") - print_msg("Entry NOT saved to journal", msg=Message.NORMAL) - raise JrnlException("KeyboardInterrupt") + raise JrnlException( + Message(MsgText.KeyboardInterruptMsg, MsgType.ERROR), + Message(MsgText.JournalNotSaved, MsgType.WARNING), + ) return raw diff --git a/jrnl/exception.py b/jrnl/exception.py index 3ec5666b..76da211d 100644 --- a/jrnl/exception.py +++ b/jrnl/exception.py @@ -1,8 +1,7 @@ # Copyright (C) 2012-2021 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html -import textwrap - -from enum import Enum +from jrnl.messages import Message +from jrnl.output import print_msg class UserAbort(Exception): @@ -15,53 +14,12 @@ class UpgradeValidationException(Exception): pass -class JrnlExceptionMessage(Enum): - ConfigDirectoryIsFile = """ - The path to your jrnl configuration directory is a file, not a directory: - - {config_directory_path} - - Removing this file will allow jrnl to save its configuration. - """ - - LineWrapTooSmallForDateFormat = """ - The provided linewrap value of {config_linewrap} is too small by - {columns} columns to display the timestamps in the configured time - format for journal {journal}. - - You can avoid this error by specifying a linewrap value that is larger - by at least {columns} in the configuration file or by using - --config-override at the command line - """ - - CannotEncryptJournalType = """ - The journal {journal_name} can't be encrypted because it is a - {journal_type} journal. - - To encrypt it, create a new journal referencing a file, export - this journal to the new journal, then encrypt the new journal. - """ - - KeyboardInterrupt = "Aborted by user" - - SomeTest = """ - Some error or something - - This is a thing to test with this message or whatever and maybe it just - keeps going forever because it's super long for no apparent reason - """ - - class JrnlException(Exception): """Common exceptions raised by jrnl.""" - def __init__(self, exception_msg: JrnlExceptionMessage, **kwargs): - self.exception_msg = exception_msg - self.title = str(exception_msg.name) - self.message = self._get_error_message(**kwargs) + def __init__(self, *messages: Message): + self.messages = messages - def _get_error_message(self, **kwargs): - msg = self.exception_msg.value - msg = msg.format(**kwargs) - msg = textwrap.dedent(msg) - return msg + def print(self) -> None: + for msg in self.messages: + print_msg(msg) diff --git a/jrnl/messages.py b/jrnl/messages.py new file mode 100644 index 00000000..41b76e3d --- /dev/null +++ b/jrnl/messages.py @@ -0,0 +1,84 @@ +from enum import Enum +from typing import NamedTuple +from typing import Mapping + + +class _MsgColor(NamedTuple): + # This is a colorama color, and colorama doesn't support enums or type hints + # see: https://github.com/tartley/colorama/issues/91 + color: str + + +class MsgType(Enum): + TITLE = _MsgColor("cyan") + NORMAL = _MsgColor("white") + WARNING = _MsgColor("yellow") + ERROR = _MsgColor("red") + + @property + def color(self) -> _MsgColor: + return self.value.color + + +class MsgText(Enum): + + # --- Exceptions ---# + UncaughtException = """ + ERROR OF SOME SORT + {exception} + + This is probably a bug. Please file an issue at: + https://github.com/jrnl-org/jrnl/issues/new/choose + """ + + ConfigDirectoryIsFile = """ + The path to your jrnl configuration directory is a file, not a directory: + + {config_directory_path} + + Removing this file will allow jrnl to save its configuration. + """ + + LineWrapTooSmallForDateFormat = """ + The provided linewrap value of {config_linewrap} is too small by + {columns} columns to display the timestamps in the configured time + format for journal {journal}. + + You can avoid this error by specifying a linewrap value that is larger + by at least {columns} in the configuration file or by using + --config-override at the command line + """ + + CannotEncryptJournalType = """ + The journal {journal_name} can't be encrypted because it is a + {journal_type} journal. + + To encrypt it, create a new journal referencing a file, export + this journal to the new journal, then encrypt the new journal. + """ + + KeyboardInterruptMsg = "Aborted by user" + + SomeTest = """ + Some error or something + + This is a thing to test with this message or whatever and maybe it just + keeps going forever because it's super long for no apparent reason + """ + + # --- Journal status ---# + JournalNotSaved = "Entry NOT saved to journal" + + # --- Editor ---# + WritingEntryStart = """ + Writing Entry + To finish writing, press {how_to_quit} on a blank line. + """ + HowToQuitWindows = "Ctrl+z and then Enter" + HowToQuitLinux = "Ctrl+d" + + +class Message(NamedTuple): + text: MsgText + type: MsgType = MsgType.NORMAL + params: Mapping = {} diff --git a/jrnl/output.py b/jrnl/output.py index b98a661f..4f28b96a 100644 --- a/jrnl/output.py +++ b/jrnl/output.py @@ -1,15 +1,14 @@ # Copyright (C) 2012-2021 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html -from enum import Enum import logging import sys -from collections import namedtuple import textwrap from jrnl.color import colorize from jrnl.color import RESET_COLOR from jrnl.color import WARNING_COLOR +from jrnl.messages import Message def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs): @@ -39,25 +38,14 @@ def list_journals(configuration): return result -MessageProps = namedtuple("MessageProps", ["value", "color"]) +def print_msg(msg: Message): + msg_text = textwrap.dedent(msg.text.value.format(**msg.params)).strip().split("\n") + longest_string = len(max(msg_text, key=len)) + msg_text = [f"[ {line:<{longest_string}} ]" for line in msg_text] -class Message(Enum): - NORMAL = MessageProps(0, "cyan") - WARNING = MessageProps(1, "yellow") - ERROR = MessageProps(2, "red") + # colorize can't be called until after the lines are padded, + # because python gets confused by the ansi color codes + msg_text[0] = f"[{colorize(msg_text[0][1:-1], msg.type.color)}]" - @property - def color(self): - return self.value.color - - -def print_msg(title: str, body: str = "", msg: Message = Message.NORMAL): - - # @todo Add some polish around colorization of multi-line messages - result = colorize(title, msg.color) - - if body: - result += f"\n{body}" - - print(result, file=sys.stderr) + print("\n".join(msg_text), file=sys.stderr) diff --git a/jrnl/plugins/fancy_exporter.py b/jrnl/plugins/fancy_exporter.py index 4dc7897e..c3dbc467 100644 --- a/jrnl/plugins/fancy_exporter.py +++ b/jrnl/plugins/fancy_exporter.py @@ -3,7 +3,9 @@ # License: https://www.gnu.org/licenses/gpl-3.0.html from jrnl.exception import JrnlException -from jrnl.exception import JrnlExceptionMessage +from jrnl.messages import Message +from jrnl.messages import MsgText +from jrnl.messages import MsgType from textwrap import TextWrapper from .text_exporter import TextExporter @@ -86,8 +88,13 @@ def check_provided_linewrap_viability(linewrap, card, journal): if len(card[0]) > linewrap: width_violation = len(card[0]) - linewrap raise JrnlException( - JrnlExceptionMessage.LineWrapTooSmallForDateFormat, - config_linewrap=linewrap, - columns=width_violation, - journal=journal, + Message( + MsgText.LineWrapTooSmallForDateFormat, + MsgType.NORMAL, + { + "config_linewrap": linewrap, + "columns": width_violation, + "journal": journal, + }, + ) ) diff --git a/tests/unit/test_exception.py b/tests/unit/test_exception.py deleted file mode 100644 index c723a5be..00000000 --- a/tests/unit/test_exception.py +++ /dev/null @@ -1,21 +0,0 @@ -import textwrap - -from jrnl.exception import JrnlException -from jrnl.exception import JrnlExceptionMessage - - -def test_config_directory_exception_message(): - ex = JrnlException( - JrnlExceptionMessage.ConfigDirectoryIsFile, - config_directory_path="/config/directory/path", - ) - - assert ex.message == textwrap.dedent( - """ - The path to your jrnl configuration directory is a file, not a directory: - - /config/directory/path - - Removing this file will allow jrnl to save its configuration. - """ - ) diff --git a/tests/unit/test_export.py b/tests/unit/test_export.py index 0f9a5b7d..05c29a1f 100644 --- a/tests/unit/test_export.py +++ b/tests/unit/test_export.py @@ -1,7 +1,7 @@ import pytest from jrnl.exception import JrnlException -from jrnl.exception import JrnlExceptionMessage + from jrnl.plugins.fancy_exporter import check_provided_linewrap_viability @@ -24,8 +24,5 @@ class TestFancy: total_linewrap = 12 - with pytest.raises(JrnlException) as e: + with pytest.raises(JrnlException): check_provided_linewrap_viability(total_linewrap, [content], journal) - assert ( - e.value.exception_msg == JrnlExceptionMessage.LineWrapTooSmallForDateFormat - )