Skip to content

Commit 9cc8fd0

Browse files
committed
feature: add arc_description method for #1850
1 parent 0f3b227 commit 9cc8fd0

File tree

2 files changed

+59
-29
lines changed

2 files changed

+59
-29
lines changed

coverage/parser.py

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,16 @@ def exit_counts(self) -> dict[TLineNo, int]:
346346

347347
return exit_counts
348348

349+
def _finish_action_msg(self, action_msg: str | None, end: TLineNo) -> str:
350+
"""Apply some defaulting and formatting to an arc's description."""
351+
if action_msg is None:
352+
if end < 0:
353+
action_msg = "jump to the function exit"
354+
else:
355+
action_msg = "jump to line {lineno}"
356+
action_msg = action_msg.format(lineno=end)
357+
return action_msg
358+
349359
def missing_arc_description(self, start: TLineNo, end: TLineNo) -> str:
350360
"""Provide an English sentence describing a missing arc."""
351361
if self._missing_arc_fragments is None:
@@ -355,22 +365,26 @@ def missing_arc_description(self, start: TLineNo, end: TLineNo) -> str:
355365
fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)])
356366

357367
msgs = []
358-
for smsg, emsg in fragment_pairs:
359-
if emsg is None:
360-
if end < 0:
361-
emsg = "didn't jump to the function exit"
362-
else:
363-
emsg = "didn't jump to line {lineno}"
364-
emsg = emsg.format(lineno=end)
365-
366-
msg = f"line {start} {emsg}"
367-
if smsg is not None:
368-
msg += f" because {smsg.format(lineno=start)}"
368+
for missing_cause_msg, action_msg in fragment_pairs:
369+
action_msg = self._finish_action_msg(action_msg, end)
370+
msg = f"line {start} didn't {action_msg}"
371+
if missing_cause_msg is not None:
372+
msg += f" because {missing_cause_msg.format(lineno=start)}"
369373

370374
msgs.append(msg)
371375

372376
return " or ".join(msgs)
373377

378+
def arc_description(self, start: TLineNo, end: TLineNo) -> str:
379+
"""Provide an English description of an arc's effect."""
380+
if self._missing_arc_fragments is None:
381+
self._analyze_ast()
382+
assert self._missing_arc_fragments is not None
383+
384+
fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)])
385+
action_msg = self._finish_action_msg(fragment_pairs[0][1], end)
386+
return action_msg
387+
374388

375389
class ByteParser:
376390
"""Parse bytecode to understand the structure of code."""
@@ -451,7 +465,7 @@ class ArcStart:
451465
452466
`lineno` is the line number the arc starts from.
453467
454-
`cause` is an English text fragment used as the `startmsg` for
468+
`cause` is an English text fragment used as the `missing_cause_msg` for
455469
AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an
456470
arc wasn't executed, so should fit well into a sentence of the form,
457471
"Line 17 didn't run because {cause}." The fragment can include "{lineno}"
@@ -485,10 +499,21 @@ def __call__(
485499
self,
486500
start: TLineNo,
487501
end: TLineNo,
488-
smsg: str | None = None,
489-
emsg: str | None = None,
502+
missing_cause_msg: str | None = None,
503+
action_msg: str | None = None,
490504
) -> None:
491-
...
505+
"""
506+
Record an arc from `start` to `end`.
507+
508+
`missing_cause_msg` is a description of the reason the arc wasn't
509+
taken if it wasn't taken. For example, "the condition on line 10 was
510+
never true."
511+
512+
`action_msg` is a description of what the arc does, like "jump to line
513+
10" or "exit from function 'fooey'."
514+
515+
"""
516+
492517

493518
TArcFragments = Dict[TArc, List[Tuple[Optional[str], Optional[str]]]]
494519

@@ -549,15 +574,15 @@ def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
549574
for xit in exits:
550575
add_arc(
551576
xit.lineno, -self.start, xit.cause,
552-
f"didn't except from function {self.name!r}",
577+
f"except from function {self.name!r}",
553578
)
554579
return True
555580

556581
def process_return_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool:
557582
for xit in exits:
558583
add_arc(
559584
xit.lineno, -self.start, xit.cause,
560-
f"didn't return from function {self.name!r}",
585+
f"return from function {self.name!r}",
561586
)
562587
return True
563588

@@ -601,10 +626,10 @@ class AstArcAnalyzer:
601626
`missing_arc_fragments`: a dict mapping (from, to) arcs to lists of
602627
message fragments explaining why the arc is missing from execution::
603628
604-
{ (start, end): [(startmsg, endmsg), ...], }
629+
{ (start, end): [(missing_cause_msg, action_msg), ...], }
605630
606631
For an arc starting from line 17, they should be usable to form complete
607-
sentences like: "Line 17 {endmsg} because {startmsg}".
632+
sentences like: "Line 17 didn't {action_msg} because {missing_cause_msg}".
608633
609634
NOTE: Starting in July 2024, I've been whittling this down to only report
610635
arc that are part of true branches. It's not clear how far this work will
@@ -715,30 +740,27 @@ def _code_object__FunctionDef(self, node: ast.FunctionDef) -> None:
715740

716741
def _code_object__ClassDef(self, node: ast.ClassDef) -> None:
717742
start = self.line_for_node(node)
718-
exits = self.process_body(node.body)#, from_start=ArcStart(start))
743+
exits = self.process_body(node.body)
719744
for xit in exits:
720-
self.add_arc(
721-
xit.lineno, -start, xit.cause,
722-
f"didn't exit the body of class {node.name!r}",
723-
)
745+
self.add_arc(xit.lineno, -start, xit.cause, f"exit class {node.name!r}")
724746

725747
def add_arc(
726748
self,
727749
start: TLineNo,
728750
end: TLineNo,
729-
smsg: str | None = None,
730-
emsg: str | None = None,
751+
missing_cause_msg: str | None = None,
752+
action_msg: str | None = None,
731753
) -> None:
732754
"""Add an arc, including message fragments to use if it is missing."""
733755
if self.debug: # pragma: debugging
734-
print(f"Adding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}")
756+
print(f"Adding possible arc: ({start}, {end}): {missing_cause_msg!r}, {action_msg!r}")
735757
print(short_stack(), end="\n\n")
736758
self.arcs.add((start, end))
737759
if start in self.current_with_starts:
738760
self.with_entries.add((start, end))
739761

740-
if smsg is not None or emsg is not None:
741-
self.missing_arc_fragments[(start, end)].append((smsg, emsg))
762+
if missing_cause_msg is not None or action_msg is not None:
763+
self.missing_arc_fragments[(start, end)].append((missing_cause_msg, action_msg))
742764

743765
def nearest_blocks(self) -> Iterable[Block]:
744766
"""Yield the blocks in nearest-to-farthest order."""

coverage/plugin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,14 @@ def missing_arc_description(
542542
"""
543543
return f"Line {start} didn't jump to line {end}"
544544

545+
def arc_description(
546+
self,
547+
start: TLineNo, # pylint: disable=unused-argument
548+
end: TLineNo
549+
) -> str:
550+
"""Provide an English description of an arc's effect."""
551+
return f"jump to line {end}"
552+
545553
def source_token_lines(self) -> TSourceTokenLines:
546554
"""Generate a series of tokenized lines, one for each line in `source`.
547555

0 commit comments

Comments
 (0)