Skip to content

Commit

Permalink
pyp: improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
hauntsaninja committed May 12, 2020
1 parent 50ff196 commit 13720e1
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 13 deletions.
49 changes: 37 additions & 12 deletions pyp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import sys
import textwrap
import traceback
from collections import defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple

Expand Down Expand Up @@ -119,8 +120,8 @@ def get_config_contents() -> str:
try:
with open(config_file, "r") as f:
return f.read()
except FileNotFoundError:
raise PypError(f"Config file not found at PYP_CONFIG_PATH={config_file}")
except FileNotFoundError as e:
raise PypError(f"Config file not found at PYP_CONFIG_PATH={config_file}") from e


class PypError(Exception):
Expand All @@ -146,7 +147,7 @@ def __init__(self) -> None:
config_ast = ast.parse(config_contents)
except SyntaxError as e:
error = f": {e.text!r}" if e.text else ""
raise PypError(f"Config has invalid syntax{error}")
raise PypError(f"Config has invalid syntax{error}") from e

# List of config parts
self.parts: List[ast.stmt] = config_ast.body
Expand Down Expand Up @@ -194,7 +195,7 @@ def inner(index: int, part: ast.AST) -> None:
else:
raise PypError(
"Config only supports a subset of Python at module level; "
f"unsupported construct on line {part.lineno}"
f"unsupported construct ({type(part).__name__.lower()}) on line {part.lineno}"
)

for index, part in enumerate(self.parts):
Expand All @@ -220,8 +221,13 @@ def __init__(
define_pypprint: bool,
config: PypConfig,
) -> None:
def parse_input(c: List[str]) -> ast.Module:
return ast.parse(textwrap.dedent("\n".join(c).strip()))
def parse_input(code: List[str]) -> ast.Module:
try:
return ast.parse(textwrap.dedent("\n".join(code).strip()))
except SyntaxError as e:
message = traceback.format_exception_only(type(e), e)
message[0] = "Invalid input\n\n"
raise PypError("".join(message).strip()) from e

self.before_tree = parse_input(before)
self.tree = parse_input(code)
Expand Down Expand Up @@ -459,11 +465,14 @@ def build(self) -> ast.Module:

ret = ast.parse("")
ret.body = self.before_tree.body + self.tree.body + self.after_tree.body
return ast.fix_missing_locations(ret)
ast.fix_missing_locations(ret)
for i, node in enumerate(ret.body):
ast.increment_lineno(node, i)
return ret


def unparse(tree: ast.Module) -> str:
"""Returns a Python script equivalent to executing ``tree``."""
def unparse(tree: ast.AST, no_fallback: bool = False) -> str:
"""Returns Python code equivalent to executing ``tree``."""
if sys.version_info >= (3, 9):
return ast.unparse(tree)
try:
Expand All @@ -472,7 +481,8 @@ def unparse(tree: ast.Module) -> str:
return astunparse.unparse(tree) # type: ignore
except ImportError:
pass

if no_fallback:
raise ImportError
return f"""
from ast import *
tree = fix_missing_locations({ast.dump(tree)})
Expand All @@ -491,7 +501,22 @@ def run_pyp(args: argparse.Namespace) -> None:
print(config.shebang)
print(unparse(tree))
else:
exec(compile(tree, filename="<ast>", mode="exec"), {})
try:
exec(compile(tree, filename="<ast>", mode="exec"), {})
except Exception as e:
message = (
"Code raised the following exception, consider using --explain to investigate:\n\n"
+ "".join(traceback.format_exception_only(type(e), e)).strip()
)
try:
lineno = e.__traceback__.tb_next.tb_lineno # type: ignore
code = unparse(
next(n for n in tree.body if getattr(n, "lineno", -1) == lineno), True
).strip()
message += f"\n\nPossibly at:\n{code}"
except Exception:
pass
raise PypError(message) from e


def parse_options(args: List[str]) -> argparse.Namespace:
Expand Down Expand Up @@ -545,7 +570,7 @@ def main() -> None:
try:
run_pyp(parse_options(sys.argv[1:]))
except PypError as e:
print(f"ERROR: {e}", file=sys.stderr)
print(f"error: {e}", file=sys.stderr)
sys.exit(1)


Expand Down
3 changes: 2 additions & 1 deletion tests/test_pyp.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,9 @@ def test_edge_cases():
assert run_pyp("pyp 'print(1)'") == "1\n"

assert run_pyp("pyp 'output = 0; 1'") == "1\n"
with pytest.raises(ImportError):
with pytest.raises(Exception) as e:
run_pyp("pyp 'output.foo()'")
assert isinstance(e.value.__cause__, ImportError)

assert run_pyp("pyp 'pypprint(1); pypprint(1, 2)'") == "1\n1 2\n"
assert run_pyp("pyp i", input="a\nb") == "0\n1\n"
Expand Down

0 comments on commit 13720e1

Please sign in to comment.