Improve error reporting by include object and lineno
@@ -11,8 +11,8 @@ def pytest_generate_tests(metafunc):
def collect_doc_tests(root):
for obj, doc in collect_docs(root):
- for source in parse_doc_string(doc):
- yield DocTest(obj, source)
+ for start_line, source in parse_doc_string(doc):
+ yield DocTest(obj, source, start_line)
def collect_docs(root):
@@ -29,17 +29,19 @@ def collect_docs(root):
def parse_doc_string(doc):
state = ParserState.WAIT_SOURCE
+ start_line_idx = None
current = []
- for line in doc.splitlines():
+ for line_idx, line in enumerate(doc.splitlines()):
if state is ParserState.WAIT_SOURCE and line.strip() == "```py":
state = ParserState.IN_SOURCE
+ start_line_idx = line_idx
elif state is ParserState.IN_SOURCE and line.strip() != "```":
current.append(line)
elif state is ParserState.IN_SOURCE and line.strip() == "```":
- yield "\n".join(current)
+ yield start_line_idx, "\n".join(current)
current = []
state = ParserState.WAIT_SOURCE
@@ -51,10 +53,27 @@ class ParserState(int, enum.Enum):
class DocTest:
- def __init__(self, obj, source):
+ def __init__(self, obj, source, start_line):
self.obj = obj
self.source = source
+ self.start_line = start_line
def exec(self):
- co = compile(self.source, filename="foo", mode="exec")
+ # make sure the line numbers between exception and source agree
+ source = "\n" * self.start_line + self.source
+
+ try:
+ co = compile(source, filename="<doc>", mode="exec")
exec(co)
+
+ except Exception as cause:
+ raise DocTestError(self.obj, cause) from cause
+
+
+class DocTestError(Exception):
+ def __str__(self):
+ obj, cause = self.args
+
+ lineno = cause.__traceback__.tb_lineno
+
+ return "Error in docstring of {!r} in line {}: {}".format(obj, lineno, cause)
