mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 08:38:32 +02:00
Reformat messages and add new centralized exception handling (#1417)
* 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:
parent
d6ff04cf17
commit
5273f8769d
11 changed files with 204 additions and 96 deletions
29
jrnl/cli.py
29
jrnl/cli.py
|
@ -3,10 +3,15 @@
|
|||
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from .jrnl import run
|
||||
from .args import parse_args
|
||||
from .exception import JrnlError
|
||||
from jrnl.jrnl import run
|
||||
from jrnl.args import parse_args
|
||||
from jrnl.output import print_msg
|
||||
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):
|
||||
|
@ -33,9 +38,23 @@ def cli(manual_args=None):
|
|||
|
||||
return run(args)
|
||||
|
||||
except JrnlError as e:
|
||||
print(e.message, file=sys.stderr)
|
||||
except JrnlException as e:
|
||||
e.print()
|
||||
return 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print_msg(Message(MsgText.KeyboardInterruptMsg, MsgType.WARNING))
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
is_debug = args.debug # type: ignore
|
||||
except NameError:
|
||||
# error happened before args were parsed
|
||||
is_debug = "--debug" in sys.argv[1:]
|
||||
|
||||
if is_debug:
|
||||
traceback.print_tb(sys.exc_info()[2])
|
||||
|
||||
print_msg(Message(MsgText.UncaughtException, MsgType.ERROR, {"exception": e}))
|
||||
return 1
|
||||
|
|
|
@ -13,7 +13,10 @@ avoid any possible overhead for these standalone commands.
|
|||
"""
|
||||
import platform
|
||||
import sys
|
||||
from .exception import JrnlError
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
|
||||
|
||||
def preconfig_diagnostic(_):
|
||||
|
@ -70,10 +73,15 @@ 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",
|
||||
journal_name=args.journal_name,
|
||||
journal_type=journal.__class__.__name__,
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.CannotEncryptJournalType,
|
||||
MsgType.ERROR,
|
||||
{
|
||||
"journal_name": args.journal_name,
|
||||
"journal_type": journal.__class__.__name__,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
journal.config["encrypt"] = True
|
||||
|
|
|
@ -7,7 +7,11 @@ import yaml
|
|||
import xdg.BaseDirectory
|
||||
|
||||
from . import __version__
|
||||
from .exception import JrnlError
|
||||
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
|
||||
|
@ -68,12 +72,18 @@ def get_config_path():
|
|||
try:
|
||||
config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
|
||||
except FileExistsError:
|
||||
raise JrnlError(
|
||||
"ConfigDirectoryIsFile",
|
||||
config_directory_path=os.path.join(
|
||||
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
|
||||
raise JrnlException(
|
||||
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
|
||||
)
|
||||
|
|
|
@ -6,10 +6,16 @@ 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.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=""):
|
||||
|
@ -47,16 +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(
|
||||
f"[Writing Entry; on a blank line, press {_how_to_quit} to finish writing]\n",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.WritingEntryStart,
|
||||
MsgType.TITLE,
|
||||
{
|
||||
"how_to_quit": MsgText.HowToQuitWindows
|
||||
if on_windows()
|
||||
else MsgText.HowToQuitLinux
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
raise JrnlException(
|
||||
Message(MsgText.KeyboardInterruptMsg, MsgType.ERROR),
|
||||
Message(MsgText.JournalNotSaved, MsgType.WARNING),
|
||||
)
|
||||
|
||||
return raw
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
import textwrap
|
||||
from jrnl.messages import Message
|
||||
from jrnl.output import print_msg
|
||||
|
||||
|
||||
class UserAbort(Exception):
|
||||
|
@ -13,40 +14,12 @@ class UpgradeValidationException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class JrnlError(Exception):
|
||||
class JrnlException(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 __init__(self, *messages: Message):
|
||||
self.messages = messages
|
||||
|
||||
def _get_error_message(self, **kwargs):
|
||||
error_messages = {
|
||||
"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.
|
||||
""",
|
||||
}
|
||||
|
||||
msg = error_messages[self.error_type].format(**kwargs)
|
||||
msg = textwrap.dedent(msg)
|
||||
return msg
|
||||
def print(self) -> None:
|
||||
for msg in self.messages:
|
||||
print_msg(msg)
|
||||
|
|
79
jrnl/messages.py
Normal file
79
jrnl/messages.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
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):
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
# --- Exceptions ---#
|
||||
UncaughtException = """
|
||||
ERROR
|
||||
{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"
|
||||
|
||||
# --- 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 = {}
|
|
@ -2,14 +2,16 @@
|
|||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
import sys
|
||||
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):
|
||||
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 +36,16 @@ def list_journals(configuration):
|
|||
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
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]
|
||||
|
||||
# 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)}]"
|
||||
|
||||
print("\n".join(msg_text), file=sys.stderr)
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
# 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.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from textwrap import TextWrapper
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
|
@ -40,7 +43,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,9 +87,14 @@ 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",
|
||||
config_linewrap=linewrap,
|
||||
columns=width_violation,
|
||||
journal=journal,
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.LineWrapTooSmallForDateFormat,
|
||||
MsgType.NORMAL,
|
||||
{
|
||||
"config_linewrap": linewrap,
|
||||
"columns": width_violation,
|
||||
"journal": journal,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
|
@ -96,7 +96,7 @@ Feature: Writing new entries.
|
|||
When we run "jrnl --config-override editor ''" and enter ""
|
||||
Then the stdin prompt should have been called
|
||||
And the output should be empty
|
||||
And the error output should contain "Writing Entry; on a blank line"
|
||||
And the error output should contain "To finish writing, press"
|
||||
And the editor should not have been called
|
||||
|
||||
Examples: configs
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import textwrap
|
||||
|
||||
from jrnl.exception import JrnlError
|
||||
|
||||
|
||||
def test_config_directory_exception_message():
|
||||
ex = JrnlError(
|
||||
"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.
|
||||
"""
|
||||
)
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from jrnl.exception import JrnlError
|
||||
from jrnl.exception import JrnlException
|
||||
|
||||
from jrnl.plugins.fancy_exporter import check_provided_linewrap_viability
|
||||
|
||||
|
||||
|
@ -23,6 +24,5 @@ class TestFancy:
|
|||
|
||||
total_linewrap = 12
|
||||
|
||||
with pytest.raises(JrnlError) as e:
|
||||
with pytest.raises(JrnlException):
|
||||
check_provided_linewrap_viability(total_linewrap, [content], journal)
|
||||
assert e.value.error_type == "LineWrapTooSmallForDateFormat"
|
||||
|
|
Loading…
Add table
Reference in a new issue