Skip to content

Commit

Permalink
Rework error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankSpruce committed Aug 22, 2020
1 parent 349bd69 commit 8c73beb
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 83 deletions.
2 changes: 1 addition & 1 deletion gersemi/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ParsingError(SyntaxError):

def __str__(self):
context, line, column = self.args # pylint: disable=unbalanced-tuple-unpacking
return f":{line}:{column}: {self.description}\n{context}"
return f"{line}:{column}: {self.description}\n{context}"


class GenericParsingError(ParsingError):
Expand Down
42 changes: 42 additions & 0 deletions gersemi/result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from dataclasses import astuple, dataclass
from pathlib import Path
from typing import Callable, TypeVar, Union
from gersemi.exceptions import ASTMismatch, ParsingError
from gersemi.utils import fromfile


T = TypeVar("T", covariant=True)


@dataclass
class Error:
exception: Exception
path: Path


Result = Union[T, Error]


def apply(function: Callable[..., T], path: Path, *args, **kwargs) -> Result[T]:
try:
return function(path, *args, **kwargs)
except Exception as exception: # pylint: disable=broad-except
return Error(exception, path)


ERROR_MESSAGE_TEMPLATES = {
ASTMismatch: "{path}: AST mismatch after formatting",
ParsingError: "{path}:{exception}",
UnicodeDecodeError: "{path}: file can't be read: {exception}",
}
FALLBACK_ERROR_MESSAGE_TEMPLATE = "{path}: runtime error, {exception}"


def get_error_message(error: Error) -> str:
exception, path = astuple(error)
message = FALLBACK_ERROR_MESSAGE_TEMPLATE
for exception_type, template in ERROR_MESSAGE_TEMPLATES.items():
if isinstance(exception, exception_type):
message = template
break
return message.format(path=fromfile(path), exception=exception)
93 changes: 31 additions & 62 deletions gersemi/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
import multiprocessing.dummy as mp_dummy
from pathlib import Path
import sys
from typing import Callable, Iterable
from lark.exceptions import VisitError as LarkVisitError
from typing import Callable, Dict, Iterable
from gersemi.configuration import Configuration
from gersemi.custom_command_definition_finder import find_custom_command_definitions
from gersemi.exceptions import ASTMismatch, ParsingError
from gersemi.formatter import create_formatter, Formatter
from gersemi.mode import Mode
from gersemi.parser import PARSER as parser
from gersemi.result import Result, Error, apply, get_error_message
from gersemi.return_codes import SUCCESS, INTERNAL_ERROR
from gersemi.task_result import TaskResult
from gersemi.tasks.check_formatting import check_formatting, quiet_check_formatting
from gersemi.tasks.forward_to_stdout import forward_to_stdout
from gersemi.tasks.format_file import format_file, Error, FormattedFile, Result
from gersemi.tasks.format_file import format_file, FormattedFile
from gersemi.tasks.rewrite_in_place import rewrite_in_place
from gersemi.tasks.show_diff import show_colorized_diff, show_diff
from gersemi.utils import fromfile, smart_open
from gersemi.utils import smart_open
from gersemi.keywords import Keywords


CHUNKSIZE = 16
Expand All @@ -30,7 +30,7 @@
print_to_stderr = partial(print, file=sys.stderr)


def get_files(paths):
def get_files(paths: Iterable[Path]) -> Iterable[Path]:
def get_files_from_single_path(path):
if path.is_dir():
return chain(path.rglob("CMakeLists.txt"), path.rglob("*.cmake"),)
Expand All @@ -43,47 +43,41 @@ def get_files_from_single_path(path):
)


def has_custom_command_definition(code):
def has_custom_command_definition(code: str) -> bool:
lowercased = code.lower()
has_function_definition = "function" in lowercased and "endfunction" in lowercased
has_macro_definition = "macro" in lowercased and "endmacro" in lowercased
return has_function_definition or has_macro_definition


def safe_read(filepath, *args, **kwargs):
try:
with smart_open(filepath, "r", *args, **kwargs) as f:
return f.read()
except UnicodeDecodeError as exception:
print_to_stderr(f"File {fromfile(filepath)} can't be read: ", exception)
return None


def find_custom_command_definitions_in_file(filepath):
code = safe_read(filepath)
if code is None or not has_custom_command_definition(code):
return None

try:
parse_tree = parser.parse(code)
return find_custom_command_definitions(parse_tree)
except ParsingError as exception:
print_to_stderr(f"{fromfile(filepath)}{exception}")
except LarkVisitError as exception:
print_to_stderr(
f"Runtime error when interpretting {fromfile(filepath)}: ", exception,
)
return None
def find_custom_command_definitions_in_file_impl(filepath: Path) -> Dict[str, Keywords]:
with smart_open(filepath, "r") as f:
code = f.read()
if not has_custom_command_definition(code):
return dict()

parse_tree = parser.parse(code)
return find_custom_command_definitions(parse_tree)


def find_custom_command_definitions_in_file(
filepath: Path,
) -> Result[Dict[str, Keywords]]:
return apply(find_custom_command_definitions_in_file_impl, filepath)


def find_all_custom_command_definitions(paths, pool):
def find_all_custom_command_definitions(
paths: Iterable[Path], pool
) -> Dict[str, Keywords]:
result = dict()

files = get_files(paths)
find = find_custom_command_definitions_in_file

for defs in pool.imap_unordered(find, files, chunksize=CHUNKSIZE):
if defs is not None:
if isinstance(defs, Error):
print_to_stderr(get_error_message(defs))
else:
result.update(defs)
return result

Expand All @@ -101,40 +95,15 @@ def select_task(mode: Mode, configuration: Configuration):
}[mode](configuration)


ERROR_MESSAGE_TEMPLATES = {
UnicodeDecodeError: "File {path} can't be read: {exception}",
ParsingError: "{path}{exception}",
ASTMismatch: "Failed to format {path}: AST mismatch after formatting",
}
FALLBACK_ERROR_MESSAGE_TEMPLATE = "Runtime error when formatting {path}: {exception}"


def get_error_message(error: Error) -> str:
exception, path = astuple(error)
message = FALLBACK_ERROR_MESSAGE_TEMPLATE
for exception_type, template in ERROR_MESSAGE_TEMPLATES.items():
if isinstance(exception, exception_type):
message = template
break
return message.format(path=fromfile(path), exception=exception)


def apply(
executor: Callable[[FormattedFile], TaskResult], formatted_file: Result
def run_task(
path: Path, formatter: Formatter, task: Callable[[FormattedFile], TaskResult]
) -> TaskResult:
formatted_file: Result[FormattedFile] = apply(format_file, path, formatter)
if isinstance(formatted_file, Error):
return TaskResult(
return_code=INTERNAL_ERROR, to_stderr=get_error_message(formatted_file)
)

return executor(formatted_file)


def run_task(
path: Path, formatter: Formatter, task: Callable[[FormattedFile], TaskResult]
) -> TaskResult:
formatted_file = format_file(path, formatter)
return apply(task, formatted_file)
return task(formatted_file)


def consume_task_result(task_result: TaskResult) -> int:
Expand Down
21 changes: 1 addition & 20 deletions gersemi/tasks/format_file.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Union
from lark.exceptions import VisitError as LarkVisitError
from gersemi.exceptions import ASTMismatch, ParsingError
from gersemi.formatter import Formatter
from gersemi.utils import smart_open

Expand All @@ -15,15 +12,6 @@ class FormattedFile:
path: Path


@dataclass
class Error:
exception: Exception
path: Path


Result = Union[FormattedFile, Error]


def get_newlines_style(code: str) -> str:
crlf = "\r\n"
if crlf in code:
Expand All @@ -36,7 +24,7 @@ def translate_newlines_to_line_feed(code: str) -> str:
return code.replace("\r\n", "\n").replace("\r", "\n")


def format_file_impl(path: Path, formatter: Formatter) -> FormattedFile:
def format_file(path: Path, formatter: Formatter) -> FormattedFile:
with smart_open(path, "r", newline="") as f:
code = f.read()

Expand All @@ -48,10 +36,3 @@ def format_file_impl(path: Path, formatter: Formatter) -> FormattedFile:
newlines_style=newlines_style,
path=path,
)


def format_file(path: Path, formatter: Formatter) -> Result:
try:
return format_file_impl(path, formatter)
except (UnicodeError, ParsingError, LarkVisitError, ASTMismatch) as exception:
return Error(exception, path)

0 comments on commit 8c73beb

Please sign in to comment.