Skip to content

Commit 0c9b5e0

Browse files
committed
mypy: check collector.py and plugin_support.py
1 parent 8f4d404 commit 0c9b5e0

15 files changed

+337
-153
lines changed

.editorconfig

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ trim_trailing_whitespace = true
1818
[*.py]
1919
max_line_length = 100
2020

21+
[*.pyi]
22+
max_line_length = 100
23+
2124
[*.c]
2225
max_line_length = 100
2326

MANIFEST.in

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ recursive-include ci *
2929
recursive-include lab *
3030
recursive-include .github *
3131

32+
recursive-include coverage *.pyi
3233
recursive-include coverage/fullcoverage *.py
3334
recursive-include coverage/ctracer *.c *.h
3435

coverage/cmdline.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import coverage
1818
from coverage import Coverage
1919
from coverage import env
20-
from coverage.collector import CTracer
20+
from coverage.collector import HAS_CTRACER
2121
from coverage.config import CoverageConfig
2222
from coverage.control import DEFAULT_DATAFILE
2323
from coverage.data import combinable_files, debug_data_file
@@ -573,7 +573,7 @@ def show_help(
573573

574574
help_params = dict(coverage.__dict__)
575575
help_params['program_name'] = program_name
576-
if CTracer is not None:
576+
if HAS_CTRACER:
577577
help_params['extension_modifier'] = 'with C extension'
578578
else:
579579
help_params['extension_modifier'] = 'without C extension'

coverage/collector.py

+80-56
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,37 @@
33

44
"""Raw data collector for coverage.py."""
55

6+
from __future__ import annotations
7+
8+
import functools
69
import os
710
import 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+
917
from coverage import env
1018
from coverage.config import CoverageConfig
19+
from coverage.data import CoverageData
1120
from coverage.debug import short_stack
1221
from coverage.disposition import FileDisposition
1322
from coverage.exceptions import ConfigError
1423
from coverage.misc import human_sorted_items, isolate_module
24+
from coverage.plugin import CoveragePlugin
1525
from coverage.pytracer import PyTracer
26+
from coverage.types import (
27+
TArc, TFileDisposition, TLineNo, TTraceData, TTraceFn, TTracer, TWarnFn,
28+
)
1629

1730
os = isolate_module(os)
1831

1932

2033
try:
2134
# Use the C extension code when we can, for speed.
2235
from coverage.tracer import CTracer, CFileDisposition
36+
HAS_CTRACER = True
2337
except ImportError:
2438
# Couldn't import the C extension, maybe it isn't built.
2539
if os.getenv('COVERAGE_TEST_TRACER') == 'c': # pragma: part covered
@@ -31,8 +45,9 @@
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

3752
class 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

Comments
 (0)