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 sys
import traceback
from .jrnl import run
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):
@ -33,9 +36,20 @@ def cli(manual_args=None):
return run(args)
except JrnlError as e:
print(e.message, file=sys.stderr)
except JrnlException as e:
print_msg(e.title, e.message, msg=Message.ERROR)
return 1
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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,18 @@
# 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
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"""
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
)
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
# 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 .text_exporter import TextExporter
@ -40,7 +41,7 @@ class FancyExporter(TextExporter):
card = [
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(
width=initial_linewrap,
@ -84,8 +85,8 @@ class FancyExporter(TextExporter):
def check_provided_linewrap_viability(linewrap, card, journal):
if len(card[0]) > linewrap:
width_violation = len(card[0]) - linewrap
raise JrnlError(
"LineWrapTooSmallForDateFormat",
raise JrnlException(
JrnlExceptionMessage.LineWrapTooSmallForDateFormat,
config_linewrap=linewrap,
columns=width_violation,
journal=journal,

View file

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

View file

@ -1,6 +1,7 @@
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
@ -23,6 +24,8 @@ class TestFancy:
total_linewrap = 12
with pytest.raises(JrnlError) as e:
with pytest.raises(JrnlException) as e:
check_provided_linewrap_viability(total_linewrap, [content], journal)
assert e.value.error_type == "LineWrapTooSmallForDateFormat"
assert (
e.value.exception_msg == JrnlExceptionMessage.LineWrapTooSmallForDateFormat
)