3
3
4
4
"""Raw data collector for coverage.py."""
5
5
6
+ from __future__ import annotations
7
+
8
+ import functools
6
9
import os
7
10
import sys
8
11
12
+ from types import FrameType
13
+ from typing import (
14
+ cast , Any , Callable , Dict , List , Mapping , Optional , Set , Tuple , Type , TypeVar ,
15
+ )
16
+
9
17
from coverage import env
10
18
from coverage .config import CoverageConfig
19
+ from coverage .data import CoverageData
11
20
from coverage .debug import short_stack
12
21
from coverage .disposition import FileDisposition
13
22
from coverage .exceptions import ConfigError
14
23
from coverage .misc import human_sorted_items , isolate_module
24
+ from coverage .plugin import CoveragePlugin
15
25
from coverage .pytracer import PyTracer
26
+ from coverage .types import (
27
+ TArc , TFileDisposition , TLineNo , TTraceData , TTraceFn , TTracer , TWarnFn ,
28
+ )
16
29
17
30
os = isolate_module (os )
18
31
19
32
20
33
try :
21
34
# Use the C extension code when we can, for speed.
22
35
from coverage .tracer import CTracer , CFileDisposition
36
+ HAS_CTRACER = True
23
37
except ImportError :
24
38
# Couldn't import the C extension, maybe it isn't built.
25
39
if os .getenv ('COVERAGE_TEST_TRACER' ) == 'c' : # pragma: part covered
31
45
# exception here causes all sorts of other noise in unittest.
32
46
sys .stderr .write ("*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n " )
33
47
sys .exit (1 )
34
- CTracer = None
48
+ HAS_CTRACER = False
35
49
50
+ T = TypeVar ("T" )
36
51
37
52
class Collector :
38
53
"""Collects trace data.
@@ -53,15 +68,22 @@ class Collector:
53
68
# The stack of active Collectors. Collectors are added here when started,
54
69
# and popped when stopped. Collectors on the stack are paused when not
55
70
# the top, and resumed when they become the top again.
56
- _collectors = []
71
+ _collectors : List [ Collector ] = []
57
72
58
73
# The concurrency settings we support here.
59
74
LIGHT_THREADS = {"greenlet" , "eventlet" , "gevent" }
60
75
61
76
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 :
65
87
"""Create a collector.
66
88
67
89
`should_trace` is a function, taking a file name and a frame, and
@@ -107,28 +129,29 @@ def __init__(
107
129
self .concurrency = concurrency
108
130
assert isinstance (self .concurrency , list ), f"Expected a list: { self .concurrency !r} "
109
131
132
+ self .covdata : CoverageData
110
133
self .threading = None
111
- self .covdata = None
112
- self .static_context = None
134
+ self .static_context : Optional [str ] = None
113
135
114
136
self .origin = short_stack ()
115
137
116
138
self .concur_id_func = None
117
- self .mapped_file_cache = {}
118
139
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
126
146
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
128
150
self .file_disposition_class = CFileDisposition
129
151
self .supports_plugins = True
130
152
self .packed_arcs = True
131
153
else :
154
+ self ._trace_class = PyTracer
132
155
self .file_disposition_class = FileDisposition
133
156
self .supports_plugins = False
134
157
self .packed_arcs = False
@@ -182,22 +205,22 @@ def __init__(
182
205
183
206
self .reset ()
184
207
185
- def __repr__ (self ):
208
+ def __repr__ (self ) -> str :
186
209
return f"<Collector at 0x{ id (self ):x} : { self .tracer_name ()} >"
187
210
188
- def use_data (self , covdata , context ) :
211
+ def use_data (self , covdata : CoverageData , context : Optional [ str ]) -> None :
189
212
"""Use `covdata` for recording data."""
190
213
self .covdata = covdata
191
214
self .static_context = context
192
215
self .covdata .set_context (self .static_context )
193
216
194
- def tracer_name (self ):
217
+ def tracer_name (self ) -> str :
195
218
"""Return the class name of the tracer we're using."""
196
219
return self ._trace_class .__name__
197
220
198
- def _clear_data (self ):
221
+ def _clear_data (self ) -> None :
199
222
"""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
201
224
# keys and data values that were still in use higher up the stack
202
225
# when we are called as part of switch_context.
203
226
for d in self .data .values ():
@@ -206,18 +229,16 @@ def _clear_data(self):
206
229
for tracer in self .tracers :
207
230
tracer .reset_activity ()
208
231
209
- def reset (self ):
232
+ def reset (self ) -> None :
210
233
"""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]
215
236
216
237
# A dictionary mapping file names to file tracer plugin names that will
217
238
# handle them.
218
- self .file_tracers = {}
239
+ self .file_tracers : Dict [ str , str ] = {}
219
240
220
- self .disabled_plugins = set ()
241
+ self .disabled_plugins : Set [ str ] = set ()
221
242
222
243
# The .should_trace_cache attribute is a cache from file names to
223
244
# coverage.FileDisposition objects, or None. When a file is first
@@ -248,11 +269,11 @@ def reset(self):
248
269
self .should_trace_cache = {}
249
270
250
271
# Our active Tracers.
251
- self .tracers = []
272
+ self .tracers : List [ TTracer ] = []
252
273
253
274
self ._clear_data ()
254
275
255
- def _start_tracer (self ):
276
+ def _start_tracer (self ) -> TTraceFn :
256
277
"""Start a new Tracer object, and store it in self.tracers."""
257
278
tracer = self ._trace_class ()
258
279
tracer .data = self .data
@@ -271,6 +292,7 @@ def _start_tracer(self):
271
292
tracer .check_include = self .check_include
272
293
if hasattr (tracer , 'should_start_context' ):
273
294
tracer .should_start_context = self .should_start_context
295
+ if hasattr (tracer , 'switch_context' ):
274
296
tracer .switch_context = self .switch_context
275
297
if hasattr (tracer , 'disable_plugin' ):
276
298
tracer .disable_plugin = self .disable_plugin
@@ -288,7 +310,7 @@ def _start_tracer(self):
288
310
#
289
311
# New in 3.12: threading.settrace_all_threads: https://github.com/python/cpython/pull/96681
290
312
291
- def _installation_trace (self , frame , event , arg ) :
313
+ def _installation_trace (self , frame : FrameType , event : str , arg : Any ) -> TTraceFn :
292
314
"""Called on new threads, installs the real tracer."""
293
315
# Remove ourselves as the trace function.
294
316
sys .settrace (None )
@@ -301,7 +323,7 @@ def _installation_trace(self, frame, event, arg):
301
323
# Return the new trace function to continue tracing in this scope.
302
324
return fn
303
325
304
- def start (self ):
326
+ def start (self ) -> None :
305
327
"""Start collecting trace information."""
306
328
if self ._collectors :
307
329
self ._collectors [- 1 ].pause ()
@@ -310,7 +332,7 @@ def start(self):
310
332
311
333
# Check to see whether we had a fullcoverage tracer installed. If so,
312
334
# get the stack frames it stashed away for us.
313
- traces0 = []
335
+ traces0 : List [ Tuple [ Tuple [ FrameType , str , Any ], TLineNo ]] = []
314
336
fn0 = sys .gettrace ()
315
337
if fn0 :
316
338
tracer0 = getattr (fn0 , '__self__' , None )
@@ -341,7 +363,7 @@ def start(self):
341
363
if self .threading :
342
364
self .threading .settrace (self ._installation_trace )
343
365
344
- def stop (self ):
366
+ def stop (self ) -> None :
345
367
"""Stop collecting trace information."""
346
368
assert self ._collectors
347
369
if self ._collectors [- 1 ] is not self :
@@ -360,7 +382,7 @@ def stop(self):
360
382
if self ._collectors :
361
383
self ._collectors [- 1 ].resume ()
362
384
363
- def pause (self ):
385
+ def pause (self ) -> None :
364
386
"""Pause tracing, but be prepared to `resume`."""
365
387
for tracer in self .tracers :
366
388
tracer .stop ()
@@ -372,7 +394,7 @@ def pause(self):
372
394
if self .threading :
373
395
self .threading .settrace (None )
374
396
375
- def resume (self ):
397
+ def resume (self ) -> None :
376
398
"""Resume tracing after a `pause`."""
377
399
for tracer in self .tracers :
378
400
tracer .start ()
@@ -381,16 +403,17 @@ def resume(self):
381
403
else :
382
404
self ._start_tracer ()
383
405
384
- def _activity (self ):
406
+ def _activity (self ) -> bool :
385
407
"""Has any activity been traced?
386
408
387
409
Returns a boolean, True if any trace function was invoked.
388
410
389
411
"""
390
412
return any (tracer .activity () for tracer in self .tracers )
391
413
392
- def switch_context (self , new_context ) :
414
+ def switch_context (self , new_context : Optional [ str ]) -> None :
393
415
"""Switch to a new dynamic context."""
416
+ context : Optional [str ]
394
417
self .flush_data ()
395
418
if self .static_context :
396
419
context = self .static_context
@@ -400,24 +423,22 @@ def switch_context(self, new_context):
400
423
context = new_context
401
424
self .covdata .set_context (context )
402
425
403
- def disable_plugin (self , disposition ) :
426
+ def disable_plugin (self , disposition : TFileDisposition ) -> None :
404
427
"""Disable the plugin mentioned in `disposition`."""
405
428
file_tracer = disposition .file_tracer
429
+ assert file_tracer is not None
406
430
plugin = file_tracer ._coverage_plugin
407
431
plugin_name = plugin ._coverage_plugin_name
408
432
self .warn (f"Disabling plug-in { plugin_name !r} due to previous exception" )
409
433
plugin ._coverage_enabled = False
410
434
disposition .trace = False
411
435
412
- def cached_mapped_file (self , filename ):
436
+ @functools .lru_cache (maxsize = 0 )
437
+ def cached_mapped_file (self , filename : str ) -> str :
413
438
"""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 )
419
440
420
- def mapped_file_dict (self , d ) :
441
+ def mapped_file_dict (self , d : Mapping [ str , T ]) -> Dict [ str , T ] :
421
442
"""Return a dict like d, but with keys modified by file_mapper."""
422
443
# The call to list(items()) ensures that the GIL protects the dictionary
423
444
# iterator against concurrent modifications by tracers running
@@ -431,16 +452,17 @@ def mapped_file_dict(self, d):
431
452
runtime_err = ex
432
453
else :
433
454
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
436
458
437
459
return {self .cached_mapped_file (k ): v for k , v in items }
438
460
439
- def plugin_was_disabled (self , plugin ) :
461
+ def plugin_was_disabled (self , plugin : CoveragePlugin ) -> None :
440
462
"""Record that `plugin` was disabled during the run."""
441
463
self .disabled_plugins .add (plugin ._coverage_plugin_name )
442
464
443
- def flush_data (self ):
465
+ def flush_data (self ) -> bool :
444
466
"""Save the collected data to our associated `CoverageData`.
445
467
446
468
Data may have also been saved along the way. This forces the
@@ -456,8 +478,9 @@ def flush_data(self):
456
478
# Unpack the line number pairs packed into integers. See
457
479
# tracer.c:CTracer_record_pair for the C code that creates
458
480
# 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 ():
461
484
tuples = []
462
485
for packed in packeds :
463
486
l1 = packed & 0xFFFFF
@@ -467,12 +490,13 @@ def flush_data(self):
467
490
if packed & (1 << 41 ):
468
491
l2 *= - 1
469
492
tuples .append ((l1 , l2 ))
470
- data [fname ] = tuples
493
+ arc_data [fname ] = tuples
471
494
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 ))
474
497
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 ))
476
500
477
501
file_tracers = {
478
502
k : v for k , v in self .file_tracers .items ()
0 commit comments