Reformat additional messages and finish centralizing exception handling (#1424)

* 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>

* format with black

* add message to catch-all exception block

* Unskip some tests (#1399)

* remove skip_editor test and tag

* remove useless test

* unskip blank input test

* formatting

* rename test so it doesn't overwrite other test

* unskip some dayone tests that now work

* Bump ipython from 7.28.0 to 7.31.1 (#1401)

Bumps [ipython](https://github.com/ipython/ipython) from 7.28.0 to 7.31.1.
- [Release notes](https://github.com/ipython/ipython/releases)
- [Commits](https://github.com/ipython/ipython/compare/7.28.0...7.31.1)

---
updated-dependencies:
- dependency-name: ipython
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update changelog [ci skip]

* Bump asteval from 0.9.25 to 0.9.26 (#1400)

Bumps [asteval](https://github.com/newville/asteval) from 0.9.25 to 0.9.26.
- [Release notes](https://github.com/newville/asteval/releases)
- [Commits](https://github.com/newville/asteval/compare/0.9.25...0.9.26)

---
updated-dependencies:
- dependency-name: asteval
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update changelog [ci skip]

* Bump black from 21.12b0 to 22.1.0 (#1404)

* Bump black from 21.12b0 to 22.1.0

Bumps [black](https://github.com/psf/black) from 21.12b0 to 22.1.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/commits/22.1.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* Run make format

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>

* Update changelog [ci skip]

* Add reference documentation to docs site and separate out "Tips and Tricks" and "External Editors" from "Recipes" (#1332)

* First draft of command line reference, mostly pulled from help screen

* Add first draft of config file reference, mostly pulled from advanced.md

* Clean up config file doc for readability

* Add --config-file and remove examples from CLI reference

* Add warning about time zone in timeformat

* More small changes, and adding template config keyword

* Cleaning up and re-ordering config file reference

* Clean up reference and anything else from advanced documentation that can live elsewhere and linking to config file reference wherever config file is mentioned

* Fix syntax highlighting in command line reference, clean up content a bit, include --diagnostic

* Mention version config key

* Apply minor changes suggested in PR review

* Rename "recipes" to "Tips and Tricks", pull "External Editors" out of it into its own page, and redirect old recipes link to tips-and-tricks

* Revert broken mkdocs-redirects usage from last commit

* Update changelog [ci skip]

* Add --co alias for --config-override (#1397)

* Add hash as a default tag symbol (#1398)

* Update changelog [ci skip]

* Increment version to v2.8.4-beta2

* Update changelog [ci skip]

* Increment version to v2.8.4

* Update changelog [ci skip]

* Bump pytest from 6.2.5 to 7.0.0 (#1407)

Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.0.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update changelog [ci skip]

* Drop support for Python 3.7 and 3.8 (#1412)

* Remove Python 3.7 and 3.8 from github actions workflows

* Update lockfile after running poetry update a couple times

* Update poetry lock

* Remove Python 3.7 and 3.8 from pyproject.toml and run poetry lock

* Update changelog [ci skip]

* Tidy up git ignore (#1414)

* cleaned gitignore and add comments

* removed colon for readbility

* alphabetize files in sections

Co-authored-by: nelnog <nel.nogales@gmail.com>

* fix behavior that was confusing pytest

* update test to match new message

* whitespace change

* clean up error for manually stopping the inline editor

* udpate error to use new exception handling

* move some exceptions and errors to the new exception handling

* add line breaks to keyboard interrupt so it looks more like other exceptions

* add handling for exceptions that happen earlier in the flow

* add new 'NothingToDelete' error to replace old behavior

* get rid of old exception

* add new exception handling to 'nothing saved to file' errors

* move exception for no editor configured into new handling

* move exception for no alt config to new handling

* get rid of old exception handling for encrypted journal

* Move error for too many wrong passwords into new handling

* fix merge errors

* replace sys.exit call with new exception handling

* replace sys.exit call with new exception handling

* replace sys.exit call with new exception handling

* reformat with black

* clean up old code

* clean up old code

* clean up linting issue

* update uncaught exception for new handling

* update test

* fix mangled lock file

Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jrnl Bot <jrnl.bot@gmail.com>
Co-authored-by: Nelson <35701520+nelnog@users.noreply.github.com>
Co-authored-by: nelnog <nel.nogales@gmail.com>
This commit is contained in:
Jonathan Wren 2022-03-19 12:30:23 -07:00 committed by GitHub
parent a1117918dd
commit bc42f74b2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 204 additions and 153 deletions

View file

@ -21,6 +21,11 @@ from .Journal import Journal
from .Journal import LegacyJournal
from .prompt import create_password
from jrnl.exception import JrnlException
from jrnl.messages import Message
from jrnl.messages import MsgText
from jrnl.messages import MsgType
def make_key(password):
password = password.encode("utf-8")
@ -53,11 +58,11 @@ def decrypt_content(
password = getpass.getpass()
result = decrypt_func(password)
attempt += 1
if result is not None:
return result
else:
print("Extremely wrong password.", file=sys.stderr)
sys.exit(1)
if result is None:
raise JrnlException(Message(MsgText.PasswordMaxTriesExceeded, MsgType.ERROR))
return result
class EncryptedJournal(Journal):
@ -121,15 +126,11 @@ class EncryptedJournal(Journal):
@classmethod
def from_journal(cls, other: Journal):
new_journal = super().from_journal(other)
try:
new_journal.password = (
other.password
if hasattr(other, "password")
else create_password(other.name)
)
except KeyboardInterrupt:
print("[Interrupted while creating new journal]", file=sys.stderr)
sys.exit(1)
new_journal.password = (
other.password
if hasattr(other, "password")
else create_password(other.name)
)
return new_journal

View file

@ -431,13 +431,6 @@ def open_journal(journal_name, config, legacy=False):
from . import EncryptedJournal
try:
if legacy:
return EncryptedJournal.LegacyEncryptedJournal(
journal_name, **config
).open()
return EncryptedJournal.EncryptedJournal(journal_name, **config).open()
except KeyboardInterrupt:
# Since encrypted journals prompt for a password, it's easy for a user to ctrl+c out
print("[Interrupted while opening journal]", file=sys.stderr)
sys.exit(1)
if legacy:
return EncryptedJournal.LegacyEncryptedJournal(journal_name, **config).open()
return EncryptedJournal.EncryptedJournal(journal_name, **config).open()

View file

@ -5,9 +5,10 @@ import logging
import sys
import traceback
from jrnl.jrnl import run
from jrnl.args import parse_args
from .jrnl import run
from .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
@ -36,25 +37,40 @@ def cli(manual_args=None):
configure_logger(args.debug)
logging.debug("Parsed args: %s", args)
return run(args)
status_code = run(args)
except JrnlException as e:
status_code = 1
e.print()
return 1
except KeyboardInterrupt:
print_msg(Message(MsgText.KeyboardInterruptMsg, MsgType.WARNING))
return 1
status_code = 1
print_msg("\nKeyboardInterrupt", "\nAborted by user", msg=Message.ERROR)
except Exception as e:
# uncaught exception
status_code = 1
debug = False
try:
is_debug = args.debug # type: ignore
if args.debug: # type: ignore
debug = True
except NameError:
# error happened before args were parsed
is_debug = "--debug" in sys.argv[1:]
# This should only happen when the exception
# happened before the args were parsed
if "--debug" in sys.argv:
debug = True
if is_debug:
if debug:
print("\n")
traceback.print_tb(sys.exc_info()[2])
print_msg(Message(MsgText.UncaughtException, MsgType.ERROR, {"exception": e}))
return 1
print_msg(
Message(
MsgText.UncaughtException,
MsgType.ERROR,
{"name": type(e).__name__, "exception": e},
)
)
# This should be the only exit point
return status_code

View file

@ -197,9 +197,13 @@ def get_journal_name(args, config):
args.text = args.text[1:]
if args.journal_name not in config["journals"]:
print("No default journal configured.", file=sys.stderr)
print(list_journals(config), file=sys.stderr)
sys.exit(1)
raise JrnlException(
Message(
MsgText.NoDefaultJournal,
MsgType.ERROR,
{"journals": list_journals(config)},
),
)
logging.debug("Using journal name: %s", args.journal_name)
return args

View file

@ -3,11 +3,8 @@ import os
import subprocess
import sys
import tempfile
import textwrap
from pathlib import Path
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
@ -32,22 +29,21 @@ def get_text_from_editor(config, template=""):
try:
subprocess.call(split_args(config["editor"]) + [tmpfile])
except FileNotFoundError as e:
error_msg = f"""
{ERROR_COLOR}{str(e)}{RESET_COLOR}
Please check the 'editor' key in your config file for errors:
{repr(config['editor'])}
"""
print(textwrap.dedent(error_msg).strip(), file=sys.stderr)
exit(1)
except FileNotFoundError:
raise JrnlException(
Message(
MsgText.EditorMisconfigured,
MsgType.ERROR,
{"editor_key": config["editor"]},
)
)
with open(tmpfile, "r", encoding="utf-8") as f:
raw = f.read()
os.remove(tmpfile)
if not raw:
print("[Nothing saved to file]", file=sys.stderr)
raise JrnlException(Message(MsgText.NoTextReceived, MsgType.ERROR))
return raw

View file

@ -4,16 +4,6 @@ from jrnl.messages import Message
from jrnl.output import print_msg
class UserAbort(Exception):
pass
class UpgradeValidationException(Exception):
"""Raised when the contents of an upgraded journal do not match the old journal"""
pass
class JrnlException(Exception):
"""Common exceptions raised by jrnl."""

View file

@ -14,10 +14,14 @@ from .config import get_default_journal_path
from .config import load_config
from .config import save_config
from .config import verify_config_colors
from .exception import UserAbort
from .prompt import yesno
from .upgrade import is_old_version
from jrnl.exception import JrnlException
from jrnl.messages import Message
from jrnl.messages import MsgText
from jrnl.messages import MsgType
def upgrade_config(config_data, alt_config_path=None):
"""Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly.
@ -47,14 +51,14 @@ def find_default_config():
def find_alt_config(alt_config):
if os.path.exists(alt_config):
return alt_config
else:
print(
"Alternate configuration file not found at path specified.", file=sys.stderr
if not os.path.exists(alt_config):
raise JrnlException(
Message(
MsgText.AltConfigNotFound, MsgType.ERROR, {"config_file": alt_config}
)
)
print("Exiting.", file=sys.stderr)
sys.exit(1)
return alt_config
def load_or_install_jrnl(alt_config_path):
@ -72,32 +76,16 @@ def load_or_install_jrnl(alt_config_path):
config = load_config(config_path)
if is_old_version(config_path):
from . import upgrade
from jrnl import upgrade
try:
upgrade.upgrade_jrnl(config_path)
except upgrade.UpgradeValidationException:
print("Aborting upgrade.", file=sys.stderr)
print(
"Please tell us about this problem at the following URL:",
file=sys.stderr,
)
print(
"https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException",
file=sys.stderr,
)
print("Exiting.", file=sys.stderr)
sys.exit(1)
upgrade.upgrade_jrnl(config_path)
upgrade_config(config, alt_config_path)
verify_config_colors(config)
else:
logging.debug("Configuration file not found, installing jrnl...")
try:
config = install()
except KeyboardInterrupt:
raise UserAbort("Installation aborted")
config = install()
logging.debug('Using configuration "%s"', config)
return config

View file

@ -7,17 +7,19 @@ import sys
from . import install
from . import plugins
from .Journal import open_journal
from .color import ERROR_COLOR
from .color import RESET_COLOR
from .config import get_journal_name
from .config import scope_config
from .config import get_config_path
from .editor import get_text_from_editor
from .editor import get_text_from_stdin
from .exception import UserAbort
from . import time
from .override import apply_overrides
from jrnl.exception import JrnlException
from jrnl.messages import Message
from jrnl.messages import MsgText
from jrnl.messages import MsgType
def run(args):
"""
@ -35,18 +37,14 @@ def run(args):
return args.preconfig_cmd(args)
# Load the config, and extract journal name
try:
config = install.load_or_install_jrnl(args.config_file_path)
original_config = config.copy()
config = install.load_or_install_jrnl(args.config_file_path)
original_config = config.copy()
# Apply config overrides
config = apply_overrides(args, config)
# Apply config overrides
config = apply_overrides(args, config)
args = get_journal_name(args, config)
config = scope_config(config, args.journal_name)
except UserAbort as err:
print(f"\n{err}", file=sys.stderr)
sys.exit(1)
args = get_journal_name(args, config)
config = scope_config(config, args.journal_name)
# Run post-config command now that config is ready
if callable(args.postconfig_cmd):
@ -138,7 +136,9 @@ def write_mode(args, config, journal, **kwargs):
if not raw:
logging.error("Write mode: couldn't get raw text")
sys.exit()
raise JrnlException(
Message(MsgText.JrnlExceptionMessage.NoTextReceived, MsgType.ERROR)
)
logging.debug(
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw
@ -202,11 +202,13 @@ def _get_editor_template(config, **kwargs):
logging.debug("Write mode: template loaded: %s", template)
except OSError:
logging.error("Write mode: template not loaded")
print(
f"[Could not read template at '{config['template']}']",
file=sys.stderr,
raise JrnlException(
Message(
MsgText.CantReadTemplate,
MsgType.ERROR,
{"template": config["template"]},
)
)
sys.exit(1)
return template
@ -243,16 +245,13 @@ def _edit_search_results(config, journal, old_entries, **kwargs):
3. Write modifications to journal
"""
if not config["editor"]:
print(
f"""
[{ERROR_COLOR}ERROR{RESET_COLOR}: There is no editor configured.]
Please specify an editor in config file ({get_config_path()})
to use the --edit option.
""",
file=sys.stderr,
raise JrnlException(
Message(
MsgText.EditorNotConfigured,
MsgType.ERROR,
{"config_file": get_config_path()},
)
)
sys.exit(1)
# separate entries we are not editing
other_entries = [e for e in old_entries if e not in journal.entries]
@ -310,11 +309,7 @@ def _pluralize_entry(num):
def _delete_search_results(journal, old_entries, **kwargs):
if not journal.entries:
print(
"[No entries deleted, because the search returned no results.]",
file=sys.stderr,
)
sys.exit(1)
raise JrnlException(Message(MsgText.NothingToDelete, MsgType.ERROR))
entries_to_delete = journal.prompt_delete_entries()

View file

@ -26,7 +26,7 @@ class MsgText(Enum):
# --- Exceptions ---#
UncaughtException = """
ERROR
{name}
{exception}
This is probably a bug. Please file an issue at:
@ -61,6 +61,14 @@ class MsgText(Enum):
KeyboardInterruptMsg = "Aborted by user"
CantReadTemplate = """
Unreadable template
Could not read template file at:
{template}
"""
NoDefaultJournal = "No default journal configured\n{journals}"
# --- Journal status ---#
JournalNotSaved = "Entry NOT saved to journal"
@ -72,6 +80,56 @@ class MsgText(Enum):
HowToQuitWindows = "Ctrl+z and then Enter"
HowToQuitLinux = "Ctrl+d"
EditorMisconfigured = """
No such file or directory: '{editor_key}'
Please check the 'editor' key in your config file for errors:
editor: '{editor_key}'
"""
EditorNotConfigured = """
There is no editor configured
To use the --edit option, please specify an editor your config file:
{config_file}
For examples of how to configure an external editor, see:
https://jrnl.sh/en/stable/external-editors/
"""
NoTextReceived = """
Nothing saved to file
"""
# --- Upgrade --- #
JournalFailedUpgrade = """
The following journal{s} failed to upgrade:
{failed_journals}
Please tell us about this problem at the following URL:
https://github.com/jrnl-org/jrnl/issues/new?title=JournalFailedUpgrade
"""
UpgradeAborted = "jrnl was NOT upgraded"
ImportAborted = "Entries were NOT imported"
# -- Config --- #
AltConfigNotFound = """
Alternate configuration file not found at the given path:
{config_file}
"""
# --- Password --- #
PasswordMaxTriesExceeded = """
Too many attempts with wrong password
"""
# --- Search --- #
NothingToDelete = """
No entries to delete, because the search returned no results
"""
class Message(NamedTuple):
text: MsgText

View file

@ -29,7 +29,7 @@ def list_journals(configuration):
from . import config
"""List the journals specified in the configuration file"""
result = f"Journals defined in {config.get_config_path()}\n"
result = f"Journals defined in config ({config.get_config_path()})\n"
ml = min(max(len(k) for k in configuration["journals"]), 20)
for journal, cfg in configuration["journals"].items():
result += " * {:{}} -> {}\n".format(

View file

@ -4,6 +4,11 @@
import sys
from jrnl.exception import JrnlException
from jrnl.messages import Message
from jrnl.messages import MsgText
from jrnl.messages import MsgType
class JRNLImporter:
"""This plugin imports entries from other jrnl files."""
@ -22,8 +27,11 @@ class JRNLImporter:
try:
other_journal_txt = sys.stdin.read()
except KeyboardInterrupt:
print("[Entries NOT imported into journal.]", file=sys.stderr)
sys.exit(0)
raise JrnlException(
Message(MsgText.KeyboardInterruptMsg, MsgType.ERROR),
Message(MsgText.ImportAborted, MsgType.WARNING),
)
journal.import_(other_journal_txt)
new_cnt = len(journal.entries)
print(

View file

@ -10,10 +10,15 @@ from .EncryptedJournal import EncryptedJournal
from .config import is_config_json
from .config import load_config
from .config import scope_config
from .exception import UpgradeValidationException
from .exception import UserAbort
from .prompt import yesno
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 backup(filename, binary=False):
print(f" Created a backup at {filename}.backup", file=sys.stderr)
@ -27,13 +32,9 @@ def backup(filename, binary=False):
backup.write(contents)
except FileNotFoundError:
print(f"\nError: {filename} does not exist.")
try:
cont = yesno(f"\nCreate {filename}?", default=False)
if not cont:
raise KeyboardInterrupt
except KeyboardInterrupt:
raise UserAbort("jrnl NOT upgraded, exiting.")
cont = yesno(f"\nCreate {filename}?", default=False)
if not cont:
raise JrnlException(Message(MsgText.UpgradeAborted), MsgType.WARNING)
def check_exists(path):
@ -121,12 +122,9 @@ older versions of jrnl anymore.
file=sys.stderr,
)
try:
cont = yesno("\nContinue upgrading jrnl?", default=False)
if not cont:
raise KeyboardInterrupt
except KeyboardInterrupt:
raise UserAbort("jrnl NOT upgraded, exiting.")
cont = yesno("\nContinue upgrading jrnl?", default=False)
if not cont:
raise JrnlException(Message(MsgText.UpgradeAborted), MsgType.WARNING)
for journal_name, path in encrypted_journals.items():
print(
@ -154,15 +152,18 @@ older versions of jrnl anymore.
failed_journals = [j for j in all_journals if not j.validate_parsing()]
if len(failed_journals) > 0:
print(
"\nThe following journal{} failed to upgrade:\n{}".format(
"s" if len(failed_journals) > 1 else "",
"\n".join(j.name for j in failed_journals),
),
file=sys.stderr,
)
print_msg("Aborting upgrade.", msg=Message.NORMAL)
raise UpgradeValidationException
raise JrnlException(
Message(
MsgText.JournalFailedUpgrade,
MsgType.ERROR,
{
"s": "s" if len(failed_journals) > 1 else "",
"failed_journals": "\n".join(j.name for j in failed_journals),
},
)
)
# write all journals - or - don't
for j in all_journals:

View file

@ -41,6 +41,7 @@ Feature: Delete entries from journal
Scenario Outline: Delete flag with nonsense input deletes nothing (issue #932)
Given we use the config "<config_file>"
When we run "jrnl --delete asdfasdf"
Then the output should contain "No entries to delete"
When we run "jrnl -99 --short"
Then the output should be
2020-08-29 11:11 Entry the first.

View file

@ -78,7 +78,7 @@ Feature: Writing new entries.
And we write nothing to the editor if opened
And we use the password "test" if prompted
When we run "jrnl --edit"
Then the error output should contain "[Nothing saved to file]"
Then the error output should contain "Nothing saved to file"
And the editor should have been called
Examples: configs

View file

@ -2,6 +2,7 @@ import pytest
import os
from jrnl.install import find_alt_config
from jrnl.exception import JrnlException
def test_find_alt_config(request):
@ -14,9 +15,9 @@ def test_find_alt_config(request):
def test_find_alt_config_not_exist(request):
bad_config_path = os.path.join(
request.fspath.dirname, "..", "data", "configs", "not-existing-config.yaml"
request.fspath.dirname, "..", "data", "configs", "does-not-exist.yaml"
)
with pytest.raises(SystemExit) as ex:
with pytest.raises(JrnlException) as ex:
found_alt_config = find_alt_config(bad_config_path)
assert found_alt_config is not None
assert isinstance(ex.value, SystemExit)
assert isinstance(ex.value, JrnlException)

View file

@ -1,7 +1,6 @@
import pytest
from jrnl.exception import JrnlException
from jrnl.plugins.fancy_exporter import check_provided_linewrap_viability