From 98aa352da01f5544686ab41974ea52bb589bae78 Mon Sep 17 00:00:00 2001 From: blissful Date: Sat, 4 Nov 2023 00:52:24 -0400 Subject: [PATCH] polish how expected errors are printed: rm traceback, add some color --- rose/__main__.py | 17 ++++++++++++++++- rose/audiotags.py | 6 +++--- rose/cli.py | 28 ++++++++++++++++------------ rose/collages.py | 8 ++++---- rose/common.py | 4 +++- rose/config.py | 10 +++++----- rose/config_test.py | 3 ++- rose/playlists.py | 14 +++++++++----- rose/releases.py | 14 +++++++++----- rose/rule_parser.py | 10 ++++++---- rose/rule_parser_test.py | 5 +++-- rose/rules.py | 12 +++++------- rose/tracks.py | 4 ++-- rose/virtualfs.py | 4 ---- setup.py | 2 +- 15 files changed, 84 insertions(+), 57 deletions(-) diff --git a/rose/__main__.py b/rose/__main__.py index 2cbfc8c..1d0f13c 100644 --- a/rose/__main__.py +++ b/rose/__main__.py @@ -1,4 +1,19 @@ +import sys + +import click + from rose.cli import cli +from rose.common import RoseExpectedError + + +def main() -> None: + try: + cli() + except RoseExpectedError as e: + click.secho(f"{e.__class__.__module__}.{e.__class__.__name__}: ", fg="red", nl=False) + click.secho(str(e)) + sys.exit(1) + if __name__ == "__main__": - cli() + main() diff --git a/rose/audiotags.py b/rose/audiotags.py index 80f8a32..7a9992d 100644 --- a/rose/audiotags.py +++ b/rose/audiotags.py @@ -24,7 +24,7 @@ import mutagen.oggvorbis from rose.artiststr import ArtistMapping, format_artist_string, parse_artist_string -from rose.common import RoseError +from rose.common import RoseError, RoseExpectedError TAG_SPLITTER_REGEX = re.compile(r" \\\\ | / |; ?| vs\. ") YEAR_REGEX = re.compile(r"\d{4}$") @@ -66,11 +66,11 @@ def _normalize_rtype(x: str | None) -> str: return "unknown" -class UnsupportedFiletypeError(RoseError): +class UnsupportedFiletypeError(RoseExpectedError): pass -class UnsupportedTagValueTypeError(RoseError): +class UnsupportedTagValueTypeError(RoseExpectedError): pass diff --git a/rose/cli.py b/rose/cli.py index c4e5928..c43382a 100644 --- a/rose/cli.py +++ b/rose/cli.py @@ -29,7 +29,7 @@ remove_release_from_collage, rename_collage, ) -from rose.common import RoseError, valid_uuid +from rose.common import RoseExpectedError, valid_uuid from rose.config import Config from rose.playlists import ( add_track_to_playlist, @@ -61,15 +61,15 @@ logger = logging.getLogger(__name__) -class InvalidReleaseArgError(RoseError): +class InvalidReleaseArgError(RoseExpectedError): pass -class InvalidTrackArgError(RoseError): +class InvalidTrackArgError(RoseExpectedError): pass -class DaemonAlreadyRunningError(RoseError): +class DaemonAlreadyRunningError(RoseExpectedError): pass @@ -506,11 +506,13 @@ def parse_release_argument(r: str) -> str: return m[1] raise InvalidReleaseArgError( f"""\ -{r} is not a valid release argument. Release arguments must be: +{r} is not a valid release argument. -1. The release UUID -2. The path of the source directory of a release -3. The path of the release in the virtual filesystem (from any view) +Release arguments must be: + + 1. The release UUID + 2. The path of the source directory of a release + 3. The path of the release in the virtual filesystem (from any view) {r} is not recognized as any of the above. """ @@ -530,11 +532,13 @@ def parse_track_argument(t: str) -> str: return af.id raise InvalidTrackArgError( f"""\ -{t} is not a valid track argument. Track arguments must be: +{t} is not a valid track argument. + +Track arguments must be: -1. The track UUID -2. The path of the track in the source directory -3. The path of the track in the virtual filesystem (from any view) + 1. The track UUID + 2. The path of the track in the source directory + 3. The path of the track in the virtual filesystem (from any view) {t} is not recognized as any of the above. """ diff --git a/rose/collages.py b/rose/collages.py index e492418..fc8fce2 100644 --- a/rose/collages.py +++ b/rose/collages.py @@ -20,22 +20,22 @@ update_cache_evict_nonexistent_collages, update_cache_for_collages, ) -from rose.common import RoseError +from rose.common import RoseExpectedError from rose.config import Config from rose.releases import resolve_release_ids logger = logging.getLogger(__name__) -class DescriptionMismatchError(RoseError): +class DescriptionMismatchError(RoseExpectedError): pass -class CollageDoesNotExistError(RoseError): +class CollageDoesNotExistError(RoseExpectedError): pass -class CollageAlreadyExistsError(RoseError): +class CollageAlreadyExistsError(RoseExpectedError): pass diff --git a/rose/common.py b/rose/common.py index bcb7dcb..3827a4f 100644 --- a/rose/common.py +++ b/rose/common.py @@ -18,7 +18,9 @@ class RoseError(Exception): pass -class InvalidCoverArtFileError(RoseError): +class RoseExpectedError(RoseError): + """These errors are printed without traceback.""" + pass diff --git a/rose/config.py b/rose/config.py index 715f200..d3a7abf 100644 --- a/rose/config.py +++ b/rose/config.py @@ -14,7 +14,7 @@ import appdirs import tomllib -from rose.common import RoseError, sanitize_filename +from rose.common import RoseExpectedError, sanitize_filename from rose.rule_parser import MetadataRule, RuleSyntaxError XDG_CONFIG_ROSE = Path(appdirs.user_config_dir("rose")) @@ -25,19 +25,19 @@ XDG_CACHE_ROSE.mkdir(parents=True, exist_ok=True) -class ConfigNotFoundError(RoseError): +class ConfigNotFoundError(RoseExpectedError): pass -class ConfigDecodeError(RoseError): +class ConfigDecodeError(RoseExpectedError): pass -class MissingConfigKeyError(RoseError): +class MissingConfigKeyError(RoseExpectedError): pass -class InvalidConfigValueError(RoseError, ValueError): +class InvalidConfigValueError(RoseExpectedError, ValueError): pass diff --git a/rose/config_test.py b/rose/config_test.py index 08433d4..f4dd6d9 100644 --- a/rose/config_test.py +++ b/rose/config_test.py @@ -1,6 +1,7 @@ import tempfile from pathlib import Path +import click import pytest from rose.config import Config, ConfigNotFoundError, InvalidConfigValueError, MissingConfigKeyError @@ -422,7 +423,7 @@ def write(x: str) -> None: with pytest.raises(InvalidConfigValueError) as excinfo: Config.parse(config_path_override=path) assert ( - str(excinfo.value) + click.unstyle(str(excinfo.value)) == f"""\ Failed to parse stored_metadata_rules in configuration file ({path}): rule {{'matcher': 'tracktitle:hi', 'actions': ['delete:hi']}}: Failed to parse action 1, invalid syntax: diff --git a/rose/playlists.py b/rose/playlists.py index 9f5b027..2d23a60 100644 --- a/rose/playlists.py +++ b/rose/playlists.py @@ -23,25 +23,29 @@ update_cache_evict_nonexistent_playlists, update_cache_for_playlists, ) -from rose.common import InvalidCoverArtFileError, RoseError +from rose.common import RoseExpectedError from rose.config import Config logger = logging.getLogger(__name__) -class DescriptionMismatchError(RoseError): +class InvalidCoverArtFileError(RoseExpectedError): pass -class PlaylistDoesNotExistError(RoseError): +class DescriptionMismatchError(RoseExpectedError): pass -class TrackDoesNotExistError(RoseError): +class PlaylistDoesNotExistError(RoseExpectedError): pass -class PlaylistAlreadyExistsError(RoseError): +class TrackDoesNotExistError(RoseExpectedError): + pass + + +class PlaylistAlreadyExistsError(RoseExpectedError): pass diff --git a/rose/releases.py b/rose/releases.py index 0094bcc..22a29c3 100644 --- a/rose/releases.py +++ b/rose/releases.py @@ -34,7 +34,7 @@ update_cache_for_collages, update_cache_for_releases, ) -from rose.common import InvalidCoverArtFileError, RoseError, valid_uuid +from rose.common import RoseError, RoseExpectedError, valid_uuid from rose.config import Config from rose.rule_parser import MetadataAction from rose.rules import execute_metadata_actions @@ -42,19 +42,23 @@ logger = logging.getLogger(__name__) -class ReleaseDoesNotExistError(RoseError): +class InvalidCoverArtFileError(RoseExpectedError): pass -class ReleaseEditFailedError(RoseError): +class ReleaseDoesNotExistError(RoseExpectedError): pass -class InvalidReleaseEditResumeFileError(RoseError): +class ReleaseEditFailedError(RoseExpectedError): pass -class UnknownArtistRoleError(RoseError): +class InvalidReleaseEditResumeFileError(RoseExpectedError): + pass + + +class UnknownArtistRoleError(RoseExpectedError): pass diff --git a/rose/rule_parser.py b/rose/rule_parser.py index 27a3846..d4126f4 100644 --- a/rose/rule_parser.py +++ b/rose/rule_parser.py @@ -13,12 +13,14 @@ from dataclasses import dataclass from typing import Literal -from rose.common import RoseError +import click + +from rose.common import RoseError, RoseExpectedError logger = logging.getLogger(__name__) -class InvalidRuleError(RoseError): +class InvalidRuleError(RoseExpectedError): pass @@ -35,8 +37,8 @@ def __str__(self) -> str: Failed to parse {self.rule_name}, invalid syntax: {self.rule} - {" " * self.index}^ - {" " * self.index}{self.feedback} + {" " * self.index}{click.style("^", fg="red")} + {" " * self.index}{click.style(self.feedback, bold=True)} """ diff --git a/rose/rule_parser_test.py b/rose/rule_parser_test.py index ab80191..0d93104 100644 --- a/rose/rule_parser_test.py +++ b/rose/rule_parser_test.py @@ -1,5 +1,6 @@ import re +import click import pytest from rose.rule_parser import ( @@ -93,7 +94,7 @@ def test_rule_parse_matcher() -> None: def test_err(rule: str, err: str) -> None: with pytest.raises(RuleSyntaxError) as exc: MetadataMatcher.parse(rule) - assert str(exc.value) == err + assert click.unstyle(str(exc.value)) == err test_err( "tracknumber^Track$", @@ -234,7 +235,7 @@ def test_rule_parse_action() -> None: def test_err(rule: str, err: str, matcher: MetadataMatcher | None = None) -> None: with pytest.raises(RuleSyntaxError) as exc: MetadataAction.parse(rule, 1, matcher) - assert str(exc.value) == err + assert click.unstyle(str(exc.value)) == err test_err( "haha::delete", diff --git a/rose/rules.py b/rose/rules.py index c90166f..7f2691b 100644 --- a/rose/rules.py +++ b/rose/rules.py @@ -18,7 +18,7 @@ get_release_source_paths_from_ids, update_cache_for_releases, ) -from rose.common import RoseError, uniq +from rose.common import RoseError, RoseExpectedError, uniq from rose.config import Config from rose.rule_parser import ( AddAction, @@ -34,11 +34,7 @@ logger = logging.getLogger(__name__) -class InvalidRuleActionError(RoseError): - pass - - -class InvalidReplacementValueError(RoseError): +class InvalidReplacementValueError(RoseExpectedError): pass @@ -389,7 +385,9 @@ def execute_single_action(action: MetadataAction, value: str | int | None) -> st return bhv.src.sub(bhv.dst, strvalue) elif isinstance(bhv, DeleteAction): return None - raise InvalidRuleActionError(f"Invalid action {type(bhv)} for single-value tag") + raise RoseError( + f"Invalid action {type(bhv)} for single-value tag: Should have been caught in parsing" + ) def execute_multi_value_action( diff --git a/rose/tracks.py b/rose/tracks.py index 04ade28..4741133 100644 --- a/rose/tracks.py +++ b/rose/tracks.py @@ -10,7 +10,7 @@ from rose.cache import ( get_track, ) -from rose.common import RoseError +from rose.common import RoseExpectedError from rose.config import Config from rose.rule_parser import MetadataAction from rose.rules import execute_metadata_actions @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -class TrackDoesNotExistError(RoseError): +class TrackDoesNotExistError(RoseExpectedError): pass diff --git a/rose/virtualfs.py b/rose/virtualfs.py index 5aca844..790d36f 100644 --- a/rose/virtualfs.py +++ b/rose/virtualfs.py @@ -327,10 +327,6 @@ def label(self, label: str) -> bool: return True -class UnknownFileHandleError(RoseError): - pass - - class FileHandleManager: """ FileDescriptorGenerator generates file descriptors and handles wrapping so that we do not go diff --git a/setup.py b/setup.py index 58c1802..0dc3651 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ author="blissful", author_email="blissful@sunsetglow.net", license="Apache-2.0", - entry_points={"console_scripts": ["rose = rose.__main__:cli"]}, + entry_points={"console_scripts": ["rose = rose.__main__:main"]}, packages=setuptools.find_namespace_packages(where="."), package_data={"rose": ["*.sql", ".version"]}, install_requires=[