mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 17:27:53 +03:00
57e6e896ad
Summary: The current error message is a bit noisy. Let's just get to the point about the filename and line number that is interesting without tracebacks. This only affects functions using the `autofix.eq` API, other kinds of exceptions will have tracebacks as usual. Before, run-tests.py (19 lines): --- test-empty-t.py.out +++ test-empty-t.py.err @@ -0,0 +1,14 @@ +Traceback (most recent call last): + File "hg/tests/test-empty-t.py", line 71, in <module> + """ + File "hg/tests/testutil/dott/shobj.py", line 89, in __eq__ + autofix.eq(out, rhs, nested=1, eqfunc=eqglob) + File "hg/tests/testutil/autofix.py", line 93, in eq + raise AssertionError("actual != expected\n%s" % diff) +AssertionError: actual != expected +--- expected ++++ actual +@@ -1 +1 @@ +-someheads ++allheads + ERROR: test-empty-t.py output changed Before, run directly via python (13 lines): Traceback (most recent call last): File "test-empty-t.py", line 71, in <module> """ File "hg/tests/testutil/dott/shobj.py", line 89, in __eq__ autofix.eq(out, rhs, nested=1, eqfunc=eqglob) File "hg/tests/testutil/autofix.py", line 93, in eq raise AssertionError("actual != expected\n%s" % diff) AssertionError: actual != expected --- expected +++ actual @@ -1 +1 @@ -someheads +allheads After, run-tests.py (8 lines): --- test-empty-t.py:71 (expected) +++ test-empty-t.py:71 (actual) @@ -1 +1 @@ -someheads +allheads ERROR: test-empty-t.py output changed After, run directly (5 lines): % python test-empty-t.py --- test-empty-t.py:71 (expected) +++ test-empty-t.py:71 (actual) @@ -1 +1 @@ -someheads +allheads Reviewed By: xavierd Differential Revision: D17277286 fbshipit-source-id: a48d4d1e817f67e221a901977e0c0f8bdc1a62ab
118 lines
3.5 KiB
Python
118 lines
3.5 KiB
Python
# Copyright 2019 Facebook, Inc.
|
|
#
|
|
# This software may be used and distributed according to the terms of the
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import inspect
|
|
import os
|
|
|
|
|
|
class memoize(dict):
|
|
def __init__(self, func):
|
|
self.func = func
|
|
|
|
def __call__(self, *args):
|
|
return self[args]
|
|
|
|
def __missing__(self, key):
|
|
result = self[key] = self.func(*key)
|
|
return result
|
|
|
|
|
|
@memoize
|
|
def parse(path):
|
|
# Import parso lazily. So importing this module won't error out if parso is
|
|
# not available.
|
|
import parso
|
|
|
|
return parso.parse(open(path).read())
|
|
|
|
|
|
def argspans(nested=0):
|
|
"""Return argument positions of the function being called.
|
|
|
|
The return value is in this form:
|
|
|
|
filepath, lineno, indent, spans
|
|
|
|
filepath is the absolute file path.
|
|
|
|
lineno is the line number as seen by Python (not necessarily the start or
|
|
the end line).
|
|
|
|
indent is the number of spaces of the indentation of the function call.
|
|
|
|
spans is in this form:
|
|
|
|
[((start line, start col), (end line, end col))]
|
|
|
|
Line numbers start from 1. Column numbers start from 0.
|
|
|
|
`spans, indent` can be `None, 0` if the parsing library (parso) is not
|
|
available, or the callsite location cannot be found.
|
|
|
|
If nested is 0, check the function calling `argspans`. If nested is 1, check
|
|
function calling the function calling `argspans`, and so on.
|
|
"""
|
|
path, lineno, funcname = sourcelocation(nested + 1)
|
|
|
|
def locate(node):
|
|
"""Find the node that is the callsite invocation"""
|
|
children = getattr(node, "children", ())
|
|
if len(children) == 2:
|
|
name = children[0]
|
|
if (
|
|
name.type == "name"
|
|
and name.value == funcname
|
|
and node.end_pos[0] >= lineno
|
|
and node.start_pos[0] <= lineno
|
|
):
|
|
yield node, "args"
|
|
return
|
|
if len(children) == 3 and funcname == "__eq__":
|
|
# Special case: Treat the RHS as "__eq__" args.
|
|
op = children[1]
|
|
if (
|
|
op.type == "operator"
|
|
and op.value == "=="
|
|
and node.end_pos[0] >= lineno
|
|
and node.start_pos[0] <= lineno
|
|
):
|
|
yield node, "=="
|
|
return
|
|
for c in children:
|
|
for subnode in locate(c):
|
|
yield subnode
|
|
|
|
try:
|
|
node, nodetype = next(locate(parse(path)))
|
|
except StopIteration:
|
|
spans = None
|
|
indent = 0
|
|
else:
|
|
if nodetype == "args":
|
|
arglist = node.children[1].children[1]
|
|
assert arglist.type == "arglist"
|
|
# "::2" removes argument separators like ",".
|
|
spans = [(a.start_pos, a.end_pos) for a in arglist.children[::2]]
|
|
elif nodetype == "==":
|
|
rhs = node.children[2]
|
|
spans = [(rhs.start_pos, rhs.end_pos)]
|
|
indent = node.start_pos[1]
|
|
|
|
return path, lineno, indent, spans
|
|
|
|
|
|
def sourcelocation(nested=0, _cwd=os.getcwd()):
|
|
"""Return (path, lineno, funcname) from Python frames"""
|
|
frame = inspect.currentframe().f_back # the function calling argspans()
|
|
for _i in range(nested):
|
|
frame = frame.f_back
|
|
funcname = frame.f_code.co_name
|
|
frame = frame.f_back # the callsite calling "the function" (funcname)
|
|
lineno = frame.f_lineno
|
|
path = os.path.realpath(os.path.join(_cwd, frame.f_code.co_filename))
|
|
return path, lineno, funcname
|