33
44"""Raw data collector for coverage.py."""
55
6+ from __future__ import annotations
7+
8+ import functools
69import os
710import sys
811
12+ from types import FrameType
13+ from typing import (
14+ cast , Any , Callable , Dict , List , Mapping , Optional , Set , Tuple , Type , TypeVar ,
15+ )
16+
917from coverage import env
1018from coverage .config import CoverageConfig
19+ from coverage .data import CoverageData
1120from coverage .debug import short_stack
1221from coverage .disposition import FileDisposition
1322from coverage .exceptions import ConfigError
1423from coverage .misc import human_sorted_items , isolate_module
24+ from coverage .plugin import CoveragePlugin
1525from coverage .pytracer import PyTracer
26+ from coverage .types import (
27+ TArc , TFileDisposition , TLineNo , TTraceData , TTraceFn , TTracer , TWarnFn ,
28+ )
1629
1730os = isolate_module (os )
1831
1932
2033try :
2134 # Use the C extension code when we can, for speed.
2235 from coverage .tracer import CTracer , CFileDisposition
36+ HAS_CTRACER = True
2337except ImportError :
2438 # Couldn't import the C extension, maybe it isn't built.
2539 if os .getenv ('COVERAGE_TEST_TRACER' ) == 'c' : # pragma: part covered
3145 # exception here causes all sorts of other noise in unittest.
3246 sys .stderr .write ("*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n " )
3347 sys .exit (1 )
34- CTracer = None
48+ HAS_CTRACER = False
3549
50+ T = TypeVar ("T" )
3651
3752class Collector :
3853 """Collects trace data.
@@ -53,15 +68,22 @@ class Collector:
5368 # The stack of active Collectors. Collectors are added here when started,
5469 # and popped when stopped. Collectors on the stack are paused when not
5570 # the top, and resumed when they become the top again.
56- _collectors = []
71+ _collectors : List [ Collector ] = []
5772
5873 # The concurrency settings we support here.
5974 LIGHT_THREADS = {"greenlet" , "eventlet" , "gevent" }
6075
6176 def __init__ (
62- self , should_trace , check_include , should_start_context , file_mapper ,
63- timid , branch , warn , concurrency ,
64- ):
77+ self ,
78+ should_trace : Callable [[str , FrameType ], TFileDisposition ],
79+ check_include : Callable [[str , FrameType ], bool ],
80+ should_start_context : Optional [Callable [[FrameType ], Optional [str ]]],
81+ file_mapper : Callable [[str ], str ],
82+ timid : bool ,
83+ branch : bool ,
84+ warn : TWarnFn ,
85+ concurrency : List [str ],
86+ ) -> None :
6587 """Create a collector.
6688
6789 `should_trace` is a function, taking a file name and a frame, and
@@ -107,28 +129,29 @@ def __init__(
107129 self .concurrency = concurrency
108130 assert isinstance (self .concurrency , list ), f"Expected a list: { self .concurrency !r} "
109131
132+ self .covdata : CoverageData
110133 self .threading = None
111- self .covdata = None
112- self .static_context = None
134+ self .static_context : Optional [str ] = None
113135
114136 self .origin = short_stack ()
115137
116138 self .concur_id_func = None
117- self .mapped_file_cache = {}
118139
119- if timid :
120- # Being timid: use the simple Python trace function.
121- self ._trace_class = PyTracer
122- else :
123- # Being fast: use the C Tracer if it is available, else the Python
124- # trace function.
125- self ._trace_class = CTracer or PyTracer
140+ self ._trace_class : Type [TTracer ]
141+ self .file_disposition_class : Type [TFileDisposition ]
142+
143+ use_ctracer = False
144+ if HAS_CTRACER and not timid :
145+ use_ctracer = True
126146
127- if self ._trace_class is CTracer :
147+ #if HAS_CTRACER and self._trace_class is CTracer:
148+ if use_ctracer :
149+ self ._trace_class = CTracer
128150 self .file_disposition_class = CFileDisposition
129151 self .supports_plugins = True
130152 self .packed_arcs = True
131153 else :
154+ self ._trace_class = PyTracer
132155 self .file_disposition_class = FileDisposition
133156 self .supports_plugins = False
134157 self .packed_arcs = False
@@ -182,22 +205,22 @@ def __init__(
182205
183206 self .reset ()
184207
185- def __repr__ (self ):
208+ def __repr__ (self ) -> str :
186209 return f"<Collector at 0x{ id (self ):x} : { self .tracer_name ()} >"
187210
188- def use_data (self , covdata , context ) :
211+ def use_data (self , covdata : CoverageData , context : Optional [ str ]) -> None :
189212 """Use `covdata` for recording data."""
190213 self .covdata = covdata
191214 self .static_context = context
192215 self .covdata .set_context (self .static_context )
193216
194- def tracer_name (self ):
217+ def tracer_name (self ) -> str :
195218 """Return the class name of the tracer we're using."""
196219 return self ._trace_class .__name__
197220
198- def _clear_data (self ):
221+ def _clear_data (self ) -> None :
199222 """Clear out existing data, but stay ready for more collection."""
200- # We used to used self.data.clear(), but that would remove filename
223+ # We used to use self.data.clear(), but that would remove filename
201224 # keys and data values that were still in use higher up the stack
202225 # when we are called as part of switch_context.
203226 for d in self .data .values ():
@@ -206,18 +229,16 @@ def _clear_data(self):
206229 for tracer in self .tracers :
207230 tracer .reset_activity ()
208231
209- def reset (self ):
232+ def reset (self ) -> None :
210233 """Clear collected data, and prepare to collect more."""
211- # A dictionary mapping file names to dicts with line number keys (if not
212- # branch coverage), or mapping file names to dicts with line number
213- # pairs as keys (if branch coverage).
214- self .data = {}
234+ # The trace data we are collecting.
235+ self .data : TTraceData = {} # type: ignore[assignment]
215236
216237 # A dictionary mapping file names to file tracer plugin names that will
217238 # handle them.
218- self .file_tracers = {}
239+ self .file_tracers : Dict [ str , str ] = {}
219240
220- self .disabled_plugins = set ()
241+ self .disabled_plugins : Set [ str ] = set ()
221242
222243 # The .should_trace_cache attribute is a cache from file names to
223244 # coverage.FileDisposition objects, or None. When a file is first
@@ -248,11 +269,11 @@ def reset(self):
248269 self .should_trace_cache = {}
249270
250271 # Our active Tracers.
251- self .tracers = []
272+ self .tracers : List [ TTracer ] = []
252273
253274 self ._clear_data ()
254275
255- def _start_tracer (self ):
276+ def _start_tracer (self ) -> TTraceFn :
256277 """Start a new Tracer object, and store it in self.tracers."""
257278 tracer = self ._trace_class ()
258279 tracer .data = self .data
@@ -271,6 +292,7 @@ def _start_tracer(self):
271292 tracer .check_include = self .check_include
272293 if hasattr (tracer , 'should_start_context' ):
273294 tracer .should_start_context = self .should_start_context
295+ if hasattr (tracer , 'switch_context' ):
274296 tracer .switch_context = self .switch_context
275297 if hasattr (tracer , 'disable_plugin' ):
276298 tracer .disable_plugin = self .disable_plugin
@@ -288,7 +310,7 @@ def _start_tracer(self):
288310 #
289311 # New in 3.12: threading.settrace_all_threads: https://github.com/python/cpython/pull/96681
290312
291- def _installation_trace (self , frame , event , arg ) :
313+ def _installation_trace (self , frame : FrameType , event : str , arg : Any ) -> TTraceFn :
292314 """Called on new threads, installs the real tracer."""
293315 # Remove ourselves as the trace function.
294316 sys .settrace (None )
@@ -301,7 +323,7 @@ def _installation_trace(self, frame, event, arg):
301323 # Return the new trace function to continue tracing in this scope.
302324 return fn
303325
304- def start (self ):
326+ def start (self ) -> None :
305327 """Start collecting trace information."""
306328 if self ._collectors :
307329 self ._collectors [- 1 ].pause ()
@@ -310,7 +332,7 @@ def start(self):
310332
311333 # Check to see whether we had a fullcoverage tracer installed. If so,
312334 # get the stack frames it stashed away for us.
313- traces0 = []
335+ traces0 : List [ Tuple [ Tuple [ FrameType , str , Any ], TLineNo ]] = []
314336 fn0 = sys .gettrace ()
315337 if fn0 :
316338 tracer0 = getattr (fn0 , '__self__' , None )
@@ -341,7 +363,7 @@ def start(self):
341363 if self .threading :
342364 self .threading .settrace (self ._installation_trace )
343365
344- def stop (self ):
366+ def stop (self ) -> None :
345367 """Stop collecting trace information."""
346368 assert self ._collectors
347369 if self ._collectors [- 1 ] is not self :
@@ -360,7 +382,7 @@ def stop(self):
360382 if self ._collectors :
361383 self ._collectors [- 1 ].resume ()
362384
363- def pause (self ):
385+ def pause (self ) -> None :
364386 """Pause tracing, but be prepared to `resume`."""
365387 for tracer in self .tracers :
366388 tracer .stop ()
@@ -372,7 +394,7 @@ def pause(self):
372394 if self .threading :
373395 self .threading .settrace (None )
374396
375- def resume (self ):
397+ def resume (self ) -> None :
376398 """Resume tracing after a `pause`."""
377399 for tracer in self .tracers :
378400 tracer .start ()
@@ -381,16 +403,17 @@ def resume(self):
381403 else :
382404 self ._start_tracer ()
383405
384- def _activity (self ):
406+ def _activity (self ) -> bool :
385407 """Has any activity been traced?
386408
387409 Returns a boolean, True if any trace function was invoked.
388410
389411 """
390412 return any (tracer .activity () for tracer in self .tracers )
391413
392- def switch_context (self , new_context ) :
414+ def switch_context (self , new_context : Optional [ str ]) -> None :
393415 """Switch to a new dynamic context."""
416+ context : Optional [str ]
394417 self .flush_data ()
395418 if self .static_context :
396419 context = self .static_context
@@ -400,24 +423,22 @@ def switch_context(self, new_context):
400423 context = new_context
401424 self .covdata .set_context (context )
402425
403- def disable_plugin (self , disposition ) :
426+ def disable_plugin (self , disposition : TFileDisposition ) -> None :
404427 """Disable the plugin mentioned in `disposition`."""
405428 file_tracer = disposition .file_tracer
429+ assert file_tracer is not None
406430 plugin = file_tracer ._coverage_plugin
407431 plugin_name = plugin ._coverage_plugin_name
408432 self .warn (f"Disabling plug-in { plugin_name !r} due to previous exception" )
409433 plugin ._coverage_enabled = False
410434 disposition .trace = False
411435
412- def cached_mapped_file (self , filename ):
436+ @functools .lru_cache (maxsize = 0 )
437+ def cached_mapped_file (self , filename : str ) -> str :
413438 """A locally cached version of file names mapped through file_mapper."""
414- key = (type (filename ), filename )
415- try :
416- return self .mapped_file_cache [key ]
417- except KeyError :
418- return self .mapped_file_cache .setdefault (key , self .file_mapper (filename ))
439+ return self .file_mapper (filename )
419440
420- def mapped_file_dict (self , d ) :
441+ def mapped_file_dict (self , d : Mapping [ str , T ]) -> Dict [ str , T ] :
421442 """Return a dict like d, but with keys modified by file_mapper."""
422443 # The call to list(items()) ensures that the GIL protects the dictionary
423444 # iterator against concurrent modifications by tracers running
@@ -431,16 +452,17 @@ def mapped_file_dict(self, d):
431452 runtime_err = ex
432453 else :
433454 break
434- else :
435- raise runtime_err # pragma: cant happen
455+ else : # pragma: cant happen
456+ assert isinstance (runtime_err , Exception )
457+ raise runtime_err
436458
437459 return {self .cached_mapped_file (k ): v for k , v in items }
438460
439- def plugin_was_disabled (self , plugin ) :
461+ def plugin_was_disabled (self , plugin : CoveragePlugin ) -> None :
440462 """Record that `plugin` was disabled during the run."""
441463 self .disabled_plugins .add (plugin ._coverage_plugin_name )
442464
443- def flush_data (self ):
465+ def flush_data (self ) -> bool :
444466 """Save the collected data to our associated `CoverageData`.
445467
446468 Data may have also been saved along the way. This forces the
@@ -456,8 +478,9 @@ def flush_data(self):
456478 # Unpack the line number pairs packed into integers. See
457479 # tracer.c:CTracer_record_pair for the C code that creates
458480 # these packed ints.
459- data = {}
460- for fname , packeds in self .data .items ():
481+ arc_data : Dict [str , List [TArc ]] = {}
482+ packed_data = cast (Dict [str , Set [int ]], self .data )
483+ for fname , packeds in packed_data .items ():
461484 tuples = []
462485 for packed in packeds :
463486 l1 = packed & 0xFFFFF
@@ -467,12 +490,13 @@ def flush_data(self):
467490 if packed & (1 << 41 ):
468491 l2 *= - 1
469492 tuples .append ((l1 , l2 ))
470- data [fname ] = tuples
493+ arc_data [fname ] = tuples
471494 else :
472- data = self .data
473- self .covdata .add_arcs (self .mapped_file_dict (data ))
495+ arc_data = cast ( Dict [ str , List [ TArc ]], self .data )
496+ self .covdata .add_arcs (self .mapped_file_dict (arc_data ))
474497 else :
475- self .covdata .add_lines (self .mapped_file_dict (self .data ))
498+ line_data = cast (Dict [str , Set [int ]], self .data )
499+ self .covdata .add_lines (self .mapped_file_dict (line_data ))
476500
477501 file_tracers = {
478502 k : v for k , v in self .file_tracers .items ()
0 commit comments