Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reformat messages and add new centralized exception handling #1417

Merged
merged 31 commits into from Mar 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
09c04aa
Update and modularize exception handling
wren Feb 6, 2022
5726bc6
format with black
wren Feb 12, 2022
4157547
add message to catch-all exception block
wren Feb 12, 2022
9cb5630
Unskip some tests (#1399)
wren Jan 15, 2022
0f496ca
Bump ipython from 7.28.0 to 7.31.1 (#1401)
dependabot[bot] Jan 22, 2022
896027e
Update changelog [ci skip]
jrnl-bot Jan 22, 2022
dfaab70
Bump asteval from 0.9.25 to 0.9.26 (#1400)
dependabot[bot] Feb 5, 2022
054df35
Update changelog [ci skip]
jrnl-bot Feb 5, 2022
371171c
Bump black from 21.12b0 to 22.1.0 (#1404)
dependabot[bot] Feb 5, 2022
275ce8b
Update changelog [ci skip]
jrnl-bot Feb 5, 2022
dd6aa2f
Add reference documentation to docs site and separate out "Tips and T…
micahellison Feb 5, 2022
8be1c2e
Update changelog [ci skip]
jrnl-bot Feb 5, 2022
7fd1a99
Add --co alias for --config-override (#1397)
micahellison Feb 5, 2022
bb748db
Add hash as a default tag symbol (#1398)
micahellison Feb 5, 2022
7532f79
Update changelog [ci skip]
jrnl-bot Feb 5, 2022
a1c4ca2
Increment version to v2.8.4-beta2
jrnl-bot Feb 5, 2022
4761fee
Update changelog [ci skip]
jrnl-bot Feb 5, 2022
299ddb2
Increment version to v2.8.4
jrnl-bot Feb 12, 2022
b274fa4
Update changelog [ci skip]
jrnl-bot Feb 12, 2022
97149f1
Bump pytest from 6.2.5 to 7.0.0 (#1407)
dependabot[bot] Feb 12, 2022
44e56fb
Update changelog [ci skip]
jrnl-bot Feb 12, 2022
c6fa22b
Drop support for Python 3.7 and 3.8 (#1412)
micahellison Feb 12, 2022
76d9bd4
Update changelog [ci skip]
jrnl-bot Feb 12, 2022
f5e6705
Tidy up git ignore (#1414)
nelnog Feb 17, 2022
c41f4fd
fix behavior that was confusing pytest
wren Feb 19, 2022
ce84a7e
update test to match new message
wren Feb 19, 2022
6c97792
whitespace change
wren Feb 19, 2022
f8b4460
Merge branch 'develop' into sys-exit-1141
wren Feb 19, 2022
85a45ab
add new message handling
wren Feb 27, 2022
92065f6
clean up MsgText so value doesn't have to be used as much
wren Feb 27, 2022
8f8be0b
clean up placeholder messages
wren Feb 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 24 additions & 5 deletions jrnl/cli.py
Expand Up @@ -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):
Expand All @@ -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
18 changes: 13 additions & 5 deletions jrnl/commands.py
Expand Up @@ -13,7 +13,10 @@
"""
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(_):
Expand Down Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions jrnl/config.py
Expand Up @@ -7,7 +7,11 @@
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
Expand Down Expand Up @@ -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
)
Expand Down
35 changes: 25 additions & 10 deletions jrnl/editor.py
Expand Up @@ -6,10 +6,16 @@
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=""):
Expand Down Expand Up @@ -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(
wren marked this conversation as resolved.
Show resolved Hide resolved
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
45 changes: 9 additions & 36 deletions jrnl/exception.py
@@ -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):
Expand All @@ -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 _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 __init__(self, *messages: Message):
self.messages = messages

def print(self) -> None:
for msg in self.messages:
print_msg(msg)
79 changes: 79 additions & 0 deletions jrnl/messages.py
@@ -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 = {}
25 changes: 20 additions & 5 deletions jrnl/output.py
Expand Up @@ -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
def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs):

warning_msg = f"""
The command {old_cmd} is deprecated and will be removed from jrnl soon.
Expand All @@ -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)
22 changes: 15 additions & 7 deletions jrnl/plugins/fancy_exporter.py
Expand Up @@ -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
Expand Down Expand Up @@ -40,7 +43,7 @@ def export_entry(cls, entry):
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,
Expand Down Expand Up @@ -84,9 +87,14 @@ def export_journal(cls, journal):
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,
},
)
)
4 changes: 2 additions & 2 deletions tests/bdd/features/write.feature
@@ -1,6 +1,6 @@
Feature: Writing new entries.

Scenario Outline: Multiline entry with punctuation should keep title punctuation
Scenario Outline: Multiline entry with punctuation should keep title punctuation
Given we use the config "<config_file>"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl This is. the title\\n This is the second line"
Expand Down Expand Up @@ -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
Expand Down