Update and modularize exception handling

cc #1024 #1141

- Stack traces are no longer shown to users unless the --debug flag is
  being used
- Errors, warnings, and other messages contain color as needed
- Converted error messages to Enum
- Adds print_msg function to centralize output (this should replace all
  other output in other modules)

Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
Jonathan Wren 2022-02-05 16:05:43 -08:00
parent 49930e16f7
commit 09c04aa5de
9 changed files with 135 additions and 63 deletions

View file

@ -3,10 +3,13 @@
import logging import logging
import sys import sys
import traceback
from .jrnl import run from .jrnl import run
from .args import parse_args from .args import parse_args
from .exception import JrnlError from .exception import JrnlException
from jrnl.output import print_msg
from jrnl.output import Message
def configure_logger(debug=False): def configure_logger(debug=False):
@ -33,9 +36,20 @@ def cli(manual_args=None):
return run(args) return run(args)
except JrnlError as e: except JrnlException as e:
print(e.message, file=sys.stderr) print_msg(e.title, e.message, msg=Message.ERROR)
return 1 return 1
except KeyboardInterrupt: except KeyboardInterrupt:
print_msg("KeyboardInterrupt", "Aborted by user", msg=Message.ERROR)
return 1
except Exception as e:
# uncaught exception
if args.debug:
print("\n")
traceback.print_tb(sys.exc_info()[2])
return 1
print_msg(f"{type(e).__name__}\n", str(e), msg=Message.ERROR)
return 1 return 1

View file

@ -13,7 +13,8 @@ avoid any possible overhead for these standalone commands.
""" """
import platform import platform
import sys import sys
from .exception import JrnlError from .exception import JrnlException
from .exception import JrnlExceptionMessage
def preconfig_diagnostic(_): def preconfig_diagnostic(_):
@ -70,8 +71,8 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
journal = open_journal(args.journal_name, config) journal = open_journal(args.journal_name, config)
if hasattr(journal, "can_be_encrypted") and not journal.can_be_encrypted: if hasattr(journal, "can_be_encrypted") and not journal.can_be_encrypted:
raise JrnlError( raise JrnlException(
"CannotEncryptJournalType", JrnlExceptionMessage.CannotEncryptJournalType,
journal_name=args.journal_name, journal_name=args.journal_name,
journal_type=journal.__class__.__name__, journal_type=journal.__class__.__name__,
) )

View file

@ -7,7 +7,8 @@ import yaml
import xdg.BaseDirectory import xdg.BaseDirectory
from . import __version__ from . import __version__
from .exception import JrnlError from .exception import JrnlException
from .exception import JrnlExceptionMessage
from .color import ERROR_COLOR from .color import ERROR_COLOR
from .color import RESET_COLOR from .color import RESET_COLOR
from .output import list_journals from .output import list_journals
@ -68,8 +69,8 @@ def get_config_path():
try: try:
config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
except FileExistsError: except FileExistsError:
raise JrnlError( raise JrnlException(
"ConfigDirectoryIsFile", JrnlExceptionMessage.ConfigDirectoryIsFile,
config_directory_path=os.path.join( config_directory_path=os.path.join(
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
), ),

View file

@ -6,10 +6,13 @@ import tempfile
import textwrap import textwrap
from pathlib import Path from pathlib import Path
from .color import ERROR_COLOR from jrnl.color import ERROR_COLOR
from .color import RESET_COLOR from jrnl.color import RESET_COLOR
from .os_compat import on_windows from jrnl.os_compat import on_windows
from .os_compat import split_args from jrnl.os_compat import split_args
from jrnl.output import print_msg
from jrnl.output import Message
from jrnl.exception import JrnlException
def get_text_from_editor(config, template=""): def get_text_from_editor(config, template=""):
@ -48,15 +51,16 @@ def get_text_from_editor(config, template=""):
def get_text_from_stdin(): def get_text_from_stdin():
_how_to_quit = "Ctrl+z and then Enter" if on_windows() else "Ctrl+d" _how_to_quit = "Ctrl+z and then Enter" if on_windows() else "Ctrl+d"
print( print_msg(
f"[Writing Entry; on a blank line, press {_how_to_quit} to finish writing]\n", "Writing Entry",
file=sys.stderr, f"To finish writing, press {_how_to_quit} on a blank line.",
msg=Message.NORMAL,
) )
try: try:
raw = sys.stdin.read() raw = sys.stdin.read()
except KeyboardInterrupt: except KeyboardInterrupt:
logging.error("Write mode: keyboard interrupt") logging.error("Write mode: keyboard interrupt")
print("[Entry NOT saved to journal]", file=sys.stderr) print_msg("Entry NOT saved to journal", msg=Message.NORMAL)
sys.exit(0) raise JrnlException("KeyboardInterrupt")
return raw return raw

View file

@ -2,6 +2,8 @@
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
import textwrap import textwrap
from enum import Enum
class UserAbort(Exception): class UserAbort(Exception):
pass pass
@ -13,40 +15,53 @@ class UpgradeValidationException(Exception):
pass pass
class JrnlError(Exception): 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.""" """Common exceptions raised by jrnl."""
def __init__(self, error_type, **kwargs): def __init__(self, exception_msg: JrnlExceptionMessage, **kwargs):
self.error_type = error_type self.exception_msg = exception_msg
self.title = str(exception_msg.name)
self.message = self._get_error_message(**kwargs) self.message = self._get_error_message(**kwargs)
def _get_error_message(self, **kwargs): def _get_error_message(self, **kwargs):
error_messages = { msg = self.exception_msg.value
"ConfigDirectoryIsFile": """ msg = msg.format(**kwargs)
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.
""",
}
msg = error_messages[self.error_type].format(**kwargs)
msg = textwrap.dedent(msg) msg = textwrap.dedent(msg)
return msg return msg

View file

@ -1,15 +1,18 @@
# Copyright (C) 2012-2021 jrnl contributors # Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
from enum import Enum
import logging 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
def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs): def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs):
import sys
import textwrap
from .color import RESET_COLOR
from .color import WARNING_COLOR
warning_msg = f""" warning_msg = f"""
The command {old_cmd} is deprecated and will be removed from jrnl soon. The command {old_cmd} is deprecated and will be removed from jrnl soon.
@ -34,3 +37,31 @@ def list_journals(configuration):
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
) )
return result return result
MessageProps = namedtuple("MessageProps", ["value", "color", "pipe"])
class Message(Enum):
NORMAL = MessageProps(0, "cyan", sys.stderr)
WARNING = MessageProps(1, "yellow", sys.stderr)
ERROR = MessageProps(2, "red", sys.stderr)
@property
def color(self):
return self.value.color
@property
def pipe(self):
return self.value.pipe
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=msg.pipe)

View file

@ -2,7 +2,8 @@
# Copyright (C) 2012-2021 jrnl contributors # Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
from jrnl.exception import JrnlError from jrnl.exception import JrnlException
from jrnl.exception import JrnlExceptionMessage
from textwrap import TextWrapper from textwrap import TextWrapper
from .text_exporter import TextExporter from .text_exporter import TextExporter
@ -40,7 +41,7 @@ class FancyExporter(TextExporter):
card = [ card = [
cls.border_a + cls.border_b * (initial_linewrap) + cls.border_c + date_str cls.border_a + cls.border_b * (initial_linewrap) + cls.border_c + date_str
] ]
check_provided_linewrap_viability(linewrap, card, entry.journal) check_provided_linewrap_viability(linewrap, card, entry.journal.name)
w = TextWrapper( w = TextWrapper(
width=initial_linewrap, width=initial_linewrap,
@ -84,8 +85,8 @@ class FancyExporter(TextExporter):
def check_provided_linewrap_viability(linewrap, card, journal): def check_provided_linewrap_viability(linewrap, card, journal):
if len(card[0]) > linewrap: if len(card[0]) > linewrap:
width_violation = len(card[0]) - linewrap width_violation = len(card[0]) - linewrap
raise JrnlError( raise JrnlException(
"LineWrapTooSmallForDateFormat", JrnlExceptionMessage.LineWrapTooSmallForDateFormat,
config_linewrap=linewrap, config_linewrap=linewrap,
columns=width_violation, columns=width_violation,
journal=journal, journal=journal,

View file

@ -1,11 +1,13 @@
import textwrap import textwrap
from jrnl.exception import JrnlError from jrnl.exception import JrnlException
from jrnl.exception import JrnlExceptionMessage
def test_config_directory_exception_message(): def test_config_directory_exception_message():
ex = JrnlError( ex = JrnlException(
"ConfigDirectoryIsFile", config_directory_path="/config/directory/path" JrnlExceptionMessage.ConfigDirectoryIsFile,
config_directory_path="/config/directory/path",
) )
assert ex.message == textwrap.dedent( assert ex.message == textwrap.dedent(

View file

@ -1,6 +1,7 @@
import pytest import pytest
from jrnl.exception import JrnlError from jrnl.exception import JrnlException
from jrnl.exception import JrnlExceptionMessage
from jrnl.plugins.fancy_exporter import check_provided_linewrap_viability from jrnl.plugins.fancy_exporter import check_provided_linewrap_viability
@ -23,6 +24,8 @@ class TestFancy:
total_linewrap = 12 total_linewrap = 12
with pytest.raises(JrnlError) as e: with pytest.raises(JrnlException) as e:
check_provided_linewrap_viability(total_linewrap, [content], journal) check_provided_linewrap_viability(total_linewrap, [content], journal)
assert e.value.error_type == "LineWrapTooSmallForDateFormat" assert (
e.value.exception_msg == JrnlExceptionMessage.LineWrapTooSmallForDateFormat
)