Skip to content

Commit a447abe

Browse files
committed
feat: Add support for -x/--exclude-lines.
1 parent a9f5869 commit a447abe

File tree

2 files changed

+59
-22
lines changed

2 files changed

+59
-22
lines changed

src/slipcover/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def main():
132132
ap.add_argument('--pretty-print', action='store_true', help="pretty-print JSON output")
133133
ap.add_argument('--out', type=Path, help="specify output file name")
134134
ap.add_argument('--source', help="specify directories to cover")
135+
ap.add_argument('-x', '--exclude-lines', action='append', type=str, help="Regex line patterns to ignore coverage for")
135136
ap.add_argument('--omit', help="specify file(s) to omit")
136137
ap.add_argument('--immediate', action='store_true',
137138
help=(argparse.SUPPRESS if platform.python_implementation() == "PyPy" else "request immediate de-instrumentation"))
@@ -185,10 +186,11 @@ def main():
185186
for o in args.omit.split(','):
186187
file_matcher.addOmit(o)
187188

189+
exclude_lines = set(args.exclude_lines) if args.exclude_lines else None
188190

189191
sci = sc.Slipcover(immediate=args.immediate,
190192
d_miss_threshold=args.threshold, branch=args.branch,
191-
disassemble=args.dis, source=args.source)
193+
disassemble=args.dis, source=args.source, exclude_lines=exclude_lines)
192194

193195

194196
if not args.dont_wrap_pytest:

src/slipcover/slipcover.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
2+
import functools
23
import sys
34
import dis
45
import types
5-
from typing import Dict, Set, List, Tuple, Optional, Iterator, cast
6+
from typing import Dict, Set, List, Tuple, TYPE_CHECKING, Iterator, Optional
67
from collections import defaultdict, Counter
78
import threading
89

@@ -14,6 +15,9 @@
1415
from . import branch as br
1516
from .version import __version__
1617

18+
if TYPE_CHECKING:
19+
from re import Pattern
20+
1721
# FIXME provide __all__
1822

1923
# Counter.total() is new in 3.10
@@ -231,13 +235,19 @@ def both(f, field):
231235
class Slipcover:
232236
def __init__(self, immediate: bool = False,
233237
d_miss_threshold: int = 50, branch: bool = False,
234-
disassemble: bool = False, source: Optional[List[str]] = None):
238+
disassemble: bool = False, source: Optional[List[str]] = None,
239+
exclude_lines: Optional[Set[str]] = None):
235240
self.immediate = immediate
236241
self.d_miss_threshold = d_miss_threshold
237242
self.branch = branch
238243
self.disassemble = disassemble
239244
self.source = source
240245

246+
self.exclude_lines = None
247+
if exclude_lines:
248+
import re
249+
self.exclude_lines = {re.compile(exclude_line) for exclude_line in exclude_lines}
250+
241251
# mutex protecting this state
242252
self.lock = threading.RLock()
243253

@@ -291,46 +301,46 @@ def _get_newly_seen(self):
291301

292302
if sys.version_info >= (3,12):
293303
@staticmethod
294-
def lines_from_code(co: types.CodeType) -> Iterator[int]:
304+
def lines_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[int]:
295305
for c in co.co_consts:
296306
if isinstance(c, types.CodeType):
297-
yield from Slipcover.lines_from_code(c)
307+
yield from Slipcover.lines_from_code(c, exclude_lines)
298308

299-
yield from (line for _, line in findlinestarts(co) if not br.is_branch(line))
309+
yield from (line for _, line in findlinestarts(co) if not br.is_branch(line) and Slipcover.consider_line(co, exclude_lines))
300310

301311

302312
@staticmethod
303-
def branches_from_code(co: types.CodeType) -> Iterator[Tuple[int, int]]:
313+
def branches_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[Tuple[int, int]]:
304314
for c in co.co_consts:
305315
if isinstance(c, types.CodeType):
306-
yield from Slipcover.branches_from_code(c)
316+
yield from Slipcover.branches_from_code(c, exclude_lines)
307317

308-
yield from (br.decode_branch(line) for _, line in findlinestarts(co) if br.is_branch(line))
318+
yield from (br.decode_branch(line) for _, line in findlinestarts(co) if br.is_branch(line) and Slipcover.consider_line(co, exclude_lines))
309319

310320
else:
311321
@staticmethod
312-
def lines_from_code(co: types.CodeType) -> Iterator[int]:
322+
def lines_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[int]:
313323
for c in co.co_consts:
314324
if isinstance(c, types.CodeType):
315-
yield from Slipcover.lines_from_code(c)
325+
yield from Slipcover.lines_from_code(c, exclude_lines)
316326

317327
# Python 3.11 generates a 0th line; 3.11+ generates a line just for RESUME
318-
yield from (line for _, line in findlinestarts(co))
328+
yield from (line for _, line in findlinestarts(co) if Slipcover.consider_line(co, exclude_lines))
319329

320330

321331
@staticmethod
322-
def branches_from_code(co: types.CodeType) -> Iterator[Tuple[int, int]]:
332+
def branches_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[Tuple[int, int]]:
323333
for c in co.co_consts:
324334
if isinstance(c, types.CodeType):
325-
yield from Slipcover.branches_from_code(c)
335+
yield from Slipcover.branches_from_code(c, exclude_lines)
326336

327337
ed = bc.Editor(co)
328338
for _, _, br_index in ed.find_const_assignments(br.BRANCH_NAME):
329339
yield co.co_consts[br_index]
330340

331341

332342
if sys.version_info >= (3,12):
333-
def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None) -> types.CodeType:
343+
def instrument(self, co: types.CodeType, parent: types.CodeType = 0) -> types.CodeType:
334344
"""Instruments a code object for coverage detection.
335345
336346
If invoked on a function, instruments its code.
@@ -351,13 +361,13 @@ def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None
351361

352362
if not parent:
353363
with self.lock:
354-
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co))
355-
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co))
364+
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co, self.exclude_lines))
365+
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co, self.exclude_lines))
356366

357367
return co
358368

359369
else:
360-
def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None) -> types.CodeType:
370+
def instrument(self, co: types.CodeType, parent: types.CodeType = 0) -> types.CodeType:
361371
"""Instruments a code object for coverage detection.
362372
363373
If invoked on a function, instruments its code.
@@ -439,8 +449,8 @@ def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None
439449

440450
with self.lock:
441451
if not parent:
442-
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co))
443-
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co))
452+
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co, self.exclude_lines))
453+
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co, self.exclude_lines))
444454

445455
self.instrumented[co.co_filename].add(new_code)
446456

@@ -500,6 +510,21 @@ def deinstrument(self, co, lines: set) -> types.CodeType:
500510

501511
return new_code
502512

513+
@staticmethod
514+
def consider_line(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None):
515+
if not exclude_lines:
516+
return True
517+
518+
line = get_source(co)
519+
if not line:
520+
return True
521+
522+
for exclusion in exclude_lines:
523+
if exclusion.search(line):
524+
return False
525+
526+
return True
527+
503528

504529
def _add_unseen_source_files(self, source: List[str]):
505530
import ast
@@ -521,9 +546,9 @@ def _add_unseen_source_files(self, source: List[str]):
521546
if self.branch:
522547
t = br.preinstrument(t)
523548
code = compile(t, filename, "exec")
524-
self.code_lines[filename] = set(Slipcover.lines_from_code(code))
549+
self.code_lines[filename] = set(Slipcover.lines_from_code(code, self.exclude_lines))
525550
if self.branch:
526-
self.code_branches[filename] = set(Slipcover.branches_from_code(code))
551+
self.code_branches[filename] = set(Slipcover.branches_from_code(code, self.exclude_lines))
527552

528553
except Exception as e: # for SyntaxError and such... FIXME curate list and catch only those
529554
print(f"Warning: unable to include {filename}: {e}")
@@ -684,3 +709,13 @@ def deinstrument_seen(self) -> None:
684709

685710
# all references should have been replaced now... right?
686711
self.replace_map.clear()
712+
713+
714+
@functools.lru_cache(None)
715+
def get_source(co) -> Optional[str]:
716+
import inspect
717+
718+
try:
719+
return '\n'.join(inspect.getsourcelines(co)[0])
720+
except Exception:
721+
return None

0 commit comments

Comments
 (0)