11from __future__ import annotations
2+ import functools
23import sys
34import dis
45import types
5- from typing import Dict , Set , List , Tuple , Optional , Iterator , cast
6+ from typing import Dict , Set , List , Tuple , TYPE_CHECKING , Iterator , Optional
67from collections import defaultdict , Counter
78import threading
89
1415from . import branch as br
1516from .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):
231235class 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