Skip to content

Commit

Permalink
Update error message formatting and implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
superatomic committed Sep 4, 2023
1 parent ffdd7d3 commit 0b0f2f8
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 66 deletions.
26 changes: 26 additions & 0 deletions src/tldr_man/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2023 Olivia Kinnear
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Color and styling utilities for output."""

from functools import partial

from click import style


style_command = partial(style, fg='cyan', bold=True)
style_error = partial(style, fg='red', bold=True)
style_input = partial(style, fg='yellow')
style_path = partial(style, fg='blue', italic=True)
style_url = partial(style, underline=True)
74 changes: 74 additions & 0 deletions src/tldr_man/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2023 Olivia Kinnear
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Exceptions raised by the client."""

from urllib.parse import quote as url_escape
from typing import Optional, IO, AnyStr

from click import echo, get_current_context
from click.exceptions import ClickException

from tldr_man.color import style_command, style_error, style_input, style_url


class ColoredClickException(ClickException):
def __init__(self, message: str):
super().__init__(message)
self.ctx = get_current_context(silent=True)

def show(self, file: Optional[IO[AnyStr]] = None) -> None:
if file is None:
# noinspection PyProtectedMember
from click._compat import get_text_stderr
file = get_text_stderr()

color = False if self.ctx is None else self.ctx.color
echo(style_error("Error: ") + self.format_message(), file=file, color=color)


class Fail(ColoredClickException):
"""Represents a generic failure."""


class PageNotFound(ColoredClickException):
_URL_BASE = "https://github.com/tldr-pages/tldr/issues/new?title=page%20request:%20"

def format_message(self) -> str:
return '\n'.join([
f"Page {style_input(self.message)} is not found.",
f" Try running {style_command('tldr --update')} to update the tldr-pages cache.",
"",
"Request this page here:",
f" {style_url(self._URL_BASE + url_escape(self.message))}",
])


class NoPageCache(ColoredClickException):
exit_code = 3


class ExternalCommandNotFound(ColoredClickException):
exit_code = 127

def __init__(self, command_name: str, message: str):
super().__init__(message)
self.command_name = command_name

def format_message(self) -> str:
return f"Couldn't find the {style_command(self.command_name)} command.\n" + self.message


def eprint(message: str) -> None:
echo(style_error(message), err=True)
5 changes: 3 additions & 2 deletions src/tldr_man/languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@

from click import Context

from tldr_man.color import style_input
from tldr_man.errors import Fail
from tldr_man.pages import CACHE_DIR, language_directory_to_code
from tldr_man.util import exit_with


def all_languages() -> Iterator[str]:
Expand Down Expand Up @@ -95,7 +96,7 @@ def get_locales(ctx: Context) -> list[str]:
if language is not None:
page_locale = get_language_directory(language)
if page_locale not in all_languages():
exit_with(f"Unrecognized locale: {language}")
raise Fail(f"Unrecognized locale: {style_input(language)}")
else:
return [page_locale]
else:
Expand Down
21 changes: 4 additions & 17 deletions src/tldr_man/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,11 @@ def wrapper(ctx: Context, _param, value):
if not value or ctx.resilient_parsing:
return

exited_with_error = False
# noinspection PyBroadException
exited_with_error = True
try:
return func(ctx, value) if value is not True else func(ctx)
except Exception:
from traceback import format_exc
from sys import stderr

print(format_exc(), file=stderr)
exited_with_error = True # Don't call the `ctx.exit()` in the `finally` block
ctx.exit(1)
except SystemExit as err:
# If `sys.exit()` was called, exit that context with the given status code.
exited_with_error = True # Don't call the `ctx.exit()` in the `finally` block
ctx.exit(err.code)
func(ctx, value) if value is not True else func(ctx)
exited_with_error = False
except KeyboardInterrupt:
exited_with_error = True
ctx.exit(130)
finally:
if not exited_with_error:
Expand Down Expand Up @@ -175,8 +163,7 @@ def cli(locales, page_sections, page: list[str], **_):

page_path = pages.find_page(page_name, locales, page_sections)

if page_path is not None:
pages.display_page(page_path)
pages.display_page(page_path)


if __name__ == '__main__':
Expand Down
55 changes: 21 additions & 34 deletions src/tldr_man/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
import requests
from click import style, echo, secho, progressbar, format_filename

from tldr_man.util import mkstemp_path, mkdtemp_path, eprint, exit_with
from tldr_man.color import style_command, style_path, style_url
from tldr_man.errors import Fail, NoPageCache, ExternalCommandNotFound, PageNotFound, eprint
from tldr_man.util import mkstemp_path, mkdtemp_path

CACHE_DIR_NAME = 'tldr-man'

Expand All @@ -53,28 +55,14 @@
"""[1:-1]

PANDOC_MISSING_MESSAGE = """
Error: Couldn't find the `pandoc` command.
PANDOC_MISSING_MESSAGE = f"""
Make sure that pandoc is installed and on your PATH.
Installation instructions: https://pandoc.org/installing.html
Installation instructions: {style_url('https://pandoc.org/installing.html')}
"""[1:-1]

MANPAGE_MISSING_MESSAGE = """
Error: Couldn't find the `man` command.
Make sure that `man` is on your PATH.
"""[1:-1]

PAGE_NOT_FOUND_MESSAGE = """
Error: Page `{page_name}` is not found.
Try running `tldr --update` to update the tldr-pages cache.
Request this page here:
https://github.com/tldr-pages/tldr/issues/new?title=page%20request:%20{page_name}
"""[1:-1]

CACHE_DOES_NOT_EXIST_MESSAGE = """
The tldr-pages cache needs to be generated before `tldr` can be used.
Run `tldr --update` to generate the cache.
CACHE_DOES_NOT_EXIST_MESSAGE = f"""
The tldr-pages cache needs to be generated before {style_command('tldr')} can be used.
Run {style_command('tldr --update')} to generate the cache.
"""[1:-1]


Expand All @@ -99,11 +87,11 @@ def download_archive(location: Path, url: str = ZIP_ARCHIVE_URL) -> None:
try:
r = requests.get(url, timeout=10)
except requests.ConnectionError:
exit_with(f"Error: Could not make connection to {url}")
raise Fail(f"Could not make connection to {style_url(url)}")
except requests.Timeout:
exit_with(f"Error: Request to {url} timed out")
raise Fail(f"Request to {style_url(url)} timed out")
except requests.RequestException:
eprint(f"The following error occurred when trying to access {url}:")
eprint(f"The following error occurred when trying to access {style_url(url)}:")
raise
else:
location.write_bytes(r.content)
Expand All @@ -113,7 +101,7 @@ def update_cache() -> None:
"""Updates the tldr-pages manpage cache."""

if not pandoc_exists():
exit_with(PANDOC_MISSING_MESSAGE, exitcode=127)
raise ExternalCommandNotFound('pandoc', PANDOC_MISSING_MESSAGE)

ensure_cache_dir_update_safety()

Expand All @@ -133,8 +121,7 @@ def update_cache() -> None:
try:
zip_path = zipfile.Path(zip_archive_location)
except zipfile.BadZipFile:
eprint(f"Error: Got a bad zipfile from {ZIP_ARCHIVE_URL}")
raise
raise Fail(f"Got a bad zipfile from {style_url(ZIP_ARCHIVE_URL)}")

# Iterate through each language and section in the zip file.
for language_dir in zip_path.iterdir():
Expand Down Expand Up @@ -242,15 +229,15 @@ def ensure_cache_dir_update_safety():
if not (path.is_dir() and EXPECTED_CACHE_CONTENT_PATTERN.match(path.name))]

if problematic_files:
exit_with('\n'.join([
f"Error: Cache directory at {format_filename(CACHE_DIR)} contains non-cache files.",
raise Fail('\n'.join([
f"Cache directory at {style_path(format_filename(CACHE_DIR))} contains non-cache files.",
"Updating could cause data loss and is a potentially destructive action.",
"",
"The following files would be removed:",
*problematic_files,
"",
"To force an update, run the following command to delete the cache:",
f" rm -r {shlex.quote(str(CACHE_DIR))}",
style_command(f" rm -r {shlex.quote(str(CACHE_DIR))}"),
]))


Expand Down Expand Up @@ -291,7 +278,7 @@ def render_manpage(tldr_page: str) -> str:
return run(['pandoc', '-', '-s', '-t', 'man', '-f', 'markdown-tex_math_dollars-smart'],
input=res, stdout=PIPE, encoding="utf-8").stdout
except FileNotFoundError:
exit_with(PANDOC_MISSING_MESSAGE, exitcode=127)
raise ExternalCommandNotFound('pandoc', PANDOC_MISSING_MESSAGE)


def pandoc_exists() -> bool:
Expand All @@ -305,27 +292,27 @@ def pandoc_exists() -> bool:
def verify_tldr_cache_exists():
"""Display a specific message if the tldr manpage cache doesn't exist yet, and then exit."""
if not CACHE_DIR.exists():
exit_with(CACHE_DOES_NOT_EXIST_MESSAGE, exitcode=3)
raise NoPageCache(CACHE_DOES_NOT_EXIST_MESSAGE)


def display_page(page: Path) -> None:
try:
run(['man', page])
except FileNotFoundError as err:
if err.filename == 'man':
exit_with(MANPAGE_MISSING_MESSAGE, exitcode=127)
raise ExternalCommandNotFound('man', 'Make sure that man is on your PATH.')
else:
raise


def find_page(page_name: str, /, locales: Iterable[str], page_sections: Iterable[str]) -> Optional[Path]:
def find_page(page_name: str, /, locales: Iterable[str], page_sections: Iterable[str]) -> Path:
for search_dir in get_dir_search_order(locales, page_sections):
page = search_dir / (page_name + '.' + MANPAGE_SECTION)

if page.exists():
return page
else:
exit_with(PAGE_NOT_FOUND_MESSAGE.format(page_name=page_name))
raise PageNotFound(page_name)


def get_dir_search_order(locales: Iterable[str], page_sections: Iterable[str]) -> Iterable[Path]:
Expand Down
14 changes: 1 addition & 13 deletions src/tldr_man/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,11 @@

"""Various utility functions."""

from sys import exit
from pathlib import Path
from tempfile import mkstemp, mkdtemp
from typing import TypeVar, NoReturn
from typing import TypeVar
from collections.abc import Iterable, Iterator, Hashable

from click import secho


T = TypeVar('T', bound=Hashable)

Expand All @@ -39,12 +36,3 @@ def mkstemp_path(*args, **kwargs) -> Path:

def mkdtemp_path(*args, **kwargs) -> Path:
return Path(mkdtemp(*args, **kwargs))


def eprint(message: str):
secho(message, fg='red', err=True)


def exit_with(message: str, exitcode: int = 1) -> NoReturn:
eprint(message)
exit(exitcode)

0 comments on commit 0b0f2f8

Please sign in to comment.