Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0c9b5e0

Browse files
committedJan 2, 2023
mypy: check collector.py and plugin_support.py
1 parent 8f4d404 commit 0c9b5e0

15 files changed

+337
-153
lines changed
 

‎.editorconfig

Lines changed: 3 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 80 additions & 56 deletions
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()

‎coverage/control.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525

2626
from coverage import env
2727
from coverage.annotate import AnnotateReporter
28-
from coverage.collector import Collector, CTracer
28+
from coverage.collector import Collector, HAS_CTRACER
2929
from coverage.config import read_coverage_config
3030
from coverage.context import should_start_context_test_function, combine_context_switchers
3131
from coverage.data import CoverageData, combine_parallel_data
3232
from coverage.debug import DebugControl, short_stack, write_formatted_info
33-
from coverage.disposition import FileDisposition, disposition_debug_msg
33+
from coverage.disposition import disposition_debug_msg
3434
from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError
3535
from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
3636
from coverage.html import HtmlReporter
@@ -46,7 +46,9 @@
4646
from coverage.report import render_report
4747
from coverage.results import Analysis
4848
from coverage.summary import SummaryReporter
49-
from coverage.types import TConfigurable, TConfigSection, TConfigValue, TLineNo, TMorf
49+
from coverage.types import (
50+
TConfigurable, TConfigSection, TConfigValue, TFileDisposition, TLineNo, TMorf,
51+
)
5052
from coverage.xmlreport import XmlReporter
5153

5254

@@ -362,7 +364,7 @@ def _write_startup_debug(self) -> None:
362364
if wrote_any:
363365
write_formatted_info(self._debug.write, "end", ())
364366

365-
def _should_trace(self, filename: str, frame: FrameType) -> FileDisposition:
367+
def _should_trace(self, filename: str, frame: FrameType) -> TFileDisposition:
366368
"""Decide whether to trace execution in `filename`.
367369
368370
Calls `_should_trace_internal`, and returns the FileDisposition.
@@ -1253,7 +1255,7 @@ def plugin_info(plugins: List[Any]) -> List[str]:
12531255
('coverage_version', covmod.__version__),
12541256
('coverage_module', covmod.__file__),
12551257
('tracer', self._collector.tracer_name() if hasattr(self, "_collector") else "-none-"),
1256-
('CTracer', 'available' if CTracer else "unavailable"),
1258+
('CTracer', 'available' if HAS_CTRACER else "unavailable"),
12571259
('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
12581260
('plugins.configurers', plugin_info(self._plugins.configurers)),
12591261
('plugins.context_switchers', plugin_info(self._plugins.context_switchers)),

‎coverage/disposition.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from typing import Optional, Type, TYPE_CHECKING
99

10+
from coverage.types import TFileDisposition
11+
1012
if TYPE_CHECKING:
1113
from coverage.plugin import FileTracer
1214

@@ -30,7 +32,7 @@ def __repr__(self) -> str:
3032
# be implemented in either C or Python. Acting on them is done with these
3133
# functions.
3234

33-
def disposition_init(cls: Type[FileDisposition], original_filename: str) -> FileDisposition:
35+
def disposition_init(cls: Type[TFileDisposition], original_filename: str) -> TFileDisposition:
3436
"""Construct and initialize a new FileDisposition object."""
3537
disp = cls()
3638
disp.original_filename = original_filename
@@ -43,7 +45,7 @@ def disposition_init(cls: Type[FileDisposition], original_filename: str) -> File
4345
return disp
4446

4547

46-
def disposition_debug_msg(disp: FileDisposition) -> str:
48+
def disposition_debug_msg(disp: TFileDisposition) -> str:
4749
"""Make a nice debug message of what the FileDisposition is doing."""
4850
if disp.trace:
4951
msg = f"Tracing {disp.original_filename!r}"

‎coverage/inorout.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import traceback
1717

1818
from types import FrameType, ModuleType
19-
from typing import cast, Any, Iterable, List, Optional, Set, Tuple, TYPE_CHECKING
19+
from typing import (
20+
cast, Any, Iterable, List, Optional, Set, Tuple, Type, TYPE_CHECKING,
21+
)
2022

2123
from coverage import env
2224
from coverage.disposition import FileDisposition, disposition_init
@@ -25,7 +27,7 @@
2527
from coverage.files import prep_patterns, find_python_files, canonical_filename
2628
from coverage.misc import sys_modules_saved
2729
from coverage.python import source_for_file, source_for_morf
28-
from coverage.types import TMorf, TWarnFn, TDebugCtl
30+
from coverage.types import TFileDisposition, TMorf, TWarnFn, TDebugCtl
2931

3032
if TYPE_CHECKING:
3133
from coverage.config import CoverageConfig
@@ -290,9 +292,9 @@ def _debug(msg: str) -> None:
290292
self.source_in_third = True
291293

292294
self.plugins: Plugins
293-
self.disp_class = FileDisposition
295+
self.disp_class: Type[TFileDisposition] = FileDisposition
294296

295-
def should_trace(self, filename: str, frame: Optional[FrameType]=None) -> FileDisposition:
297+
def should_trace(self, filename: str, frame: Optional[FrameType]=None) -> TFileDisposition:
296298
"""Decide whether to trace execution in `filename`, with a reason.
297299
298300
This function is called from the trace function. As each new file name
@@ -304,7 +306,7 @@ def should_trace(self, filename: str, frame: Optional[FrameType]=None) -> FileDi
304306
original_filename = filename
305307
disp = disposition_init(self.disp_class, filename)
306308

307-
def nope(disp: FileDisposition, reason: str) -> FileDisposition:
309+
def nope(disp: TFileDisposition, reason: str) -> TFileDisposition:
308310
"""Simple helper to make it easy to return NO."""
309311
disp.trace = False
310312
disp.reason = reason

‎coverage/plugin.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ def coverage_init(reg, options):
127127
class CoveragePlugin:
128128
"""Base class for coverage.py plug-ins."""
129129

130+
_coverage_plugin_name: str
131+
_coverage_enabled: bool
132+
130133
def file_tracer(self, filename: str) -> Optional[FileTracer]: # pylint: disable=unused-argument
131134
"""Get a :class:`FileTracer` object for a file.
132135
@@ -249,7 +252,12 @@ def sys_info(self) -> Iterable[Tuple[str, Any]]:
249252
return []
250253

251254

252-
class FileTracer:
255+
class CoveragePluginBase:
256+
"""Plugins produce specialized objects, which point back to the original plugin."""
257+
_coverage_plugin: CoveragePlugin
258+
259+
260+
class FileTracer(CoveragePluginBase):
253261
"""Support needed for files during the execution phase.
254262
255263
File tracer plug-ins implement subclasses of FileTracer to return from
@@ -337,7 +345,7 @@ def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]:
337345

338346

339347
@functools.total_ordering
340-
class FileReporter:
348+
class FileReporter(CoveragePluginBase):
341349
"""Support needed for files during the analysis and reporting phases.
342350
343351
File tracer plug-ins implement a subclass of `FileReporter`, and return

‎coverage/plugin_support.py

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,44 @@
33

44
"""Support for plugins."""
55

6+
from __future__ import annotations
7+
68
import os
79
import os.path
810
import sys
911

12+
from types import FrameType
13+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union
14+
15+
from coverage.config import CoverageConfig
1016
from coverage.exceptions import PluginError
1117
from coverage.misc import isolate_module
1218
from coverage.plugin import CoveragePlugin, FileTracer, FileReporter
19+
from coverage.types import TArc, TConfigurable, TDebugCtl, TLineNo, TSourceTokenLines
1320

1421
os = isolate_module(os)
1522

1623

1724
class Plugins:
1825
"""The currently loaded collection of coverage.py plugins."""
1926

20-
def __init__(self):
21-
self.order = []
22-
self.names = {}
23-
self.file_tracers = []
24-
self.configurers = []
25-
self.context_switchers = []
27+
def __init__(self) -> None:
28+
self.order: List[CoveragePlugin] = []
29+
self.names: Dict[str, CoveragePlugin] = {}
30+
self.file_tracers: List[CoveragePlugin] = []
31+
self.configurers: List[CoveragePlugin] = []
32+
self.context_switchers: List[CoveragePlugin] = []
2633

27-
self.current_module = None
28-
self.debug = None
34+
self.current_module: Optional[str] = None
35+
self.debug: Optional[TDebugCtl]
2936

3037
@classmethod
31-
def load_plugins(cls, modules, config, debug=None):
38+
def load_plugins(
39+
cls,
40+
modules: Iterable[str],
41+
config: CoverageConfig,
42+
debug: Optional[TDebugCtl]=None,
43+
) -> Plugins:
3244
"""Load plugins from `modules`.
3345
3446
Returns a Plugins object with the loaded and configured plugins.
@@ -54,7 +66,7 @@ def load_plugins(cls, modules, config, debug=None):
5466
plugins.current_module = None
5567
return plugins
5668

57-
def add_file_tracer(self, plugin):
69+
def add_file_tracer(self, plugin: CoveragePlugin) -> None:
5870
"""Add a file tracer plugin.
5971
6072
`plugin` is an instance of a third-party plugin class. It must
@@ -63,7 +75,7 @@ def add_file_tracer(self, plugin):
6375
"""
6476
self._add_plugin(plugin, self.file_tracers)
6577

66-
def add_configurer(self, plugin):
78+
def add_configurer(self, plugin: CoveragePlugin) -> None:
6779
"""Add a configuring plugin.
6880
6981
`plugin` is an instance of a third-party plugin class. It must
@@ -72,7 +84,7 @@ def add_configurer(self, plugin):
7284
"""
7385
self._add_plugin(plugin, self.configurers)
7486

75-
def add_dynamic_context(self, plugin):
87+
def add_dynamic_context(self, plugin: CoveragePlugin) -> None:
7688
"""Add a dynamic context plugin.
7789
7890
`plugin` is an instance of a third-party plugin class. It must
@@ -81,15 +93,19 @@ def add_dynamic_context(self, plugin):
8193
"""
8294
self._add_plugin(plugin, self.context_switchers)
8395

84-
def add_noop(self, plugin):
96+
def add_noop(self, plugin: CoveragePlugin) -> None:
8597
"""Add a plugin that does nothing.
8698
8799
This is only useful for testing the plugin support.
88100
89101
"""
90102
self._add_plugin(plugin, None)
91103

92-
def _add_plugin(self, plugin, specialized):
104+
def _add_plugin(
105+
self,
106+
plugin: CoveragePlugin,
107+
specialized: Optional[List[CoveragePlugin]],
108+
) -> None:
93109
"""Add a plugin object.
94110
95111
`plugin` is a :class:`CoveragePlugin` instance to add. `specialized`
@@ -102,120 +118,120 @@ def _add_plugin(self, plugin, specialized):
102118
labelled = LabelledDebug(f"plugin {self.current_module!r}", self.debug)
103119
plugin = DebugPluginWrapper(plugin, labelled)
104120

105-
# pylint: disable=attribute-defined-outside-init
106121
plugin._coverage_plugin_name = plugin_name
107122
plugin._coverage_enabled = True
108123
self.order.append(plugin)
109124
self.names[plugin_name] = plugin
110125
if specialized is not None:
111126
specialized.append(plugin)
112127

113-
def __bool__(self):
128+
def __bool__(self) -> bool:
114129
return bool(self.order)
115130

116-
def __iter__(self):
131+
def __iter__(self) -> Iterator[CoveragePlugin]:
117132
return iter(self.order)
118133

119-
def get(self, plugin_name):
134+
def get(self, plugin_name: str) -> CoveragePlugin:
120135
"""Return a plugin by name."""
121136
return self.names[plugin_name]
122137

123138

124139
class LabelledDebug:
125140
"""A Debug writer, but with labels for prepending to the messages."""
126141

127-
def __init__(self, label, debug, prev_labels=()):
142+
def __init__(self, label: str, debug: TDebugCtl, prev_labels: Iterable[str]=()):
128143
self.labels = list(prev_labels) + [label]
129144
self.debug = debug
130145

131-
def add_label(self, label):
146+
def add_label(self, label: str) -> LabelledDebug:
132147
"""Add a label to the writer, and return a new `LabelledDebug`."""
133148
return LabelledDebug(label, self.debug, self.labels)
134149

135-
def message_prefix(self):
150+
def message_prefix(self) -> str:
136151
"""The prefix to use on messages, combining the labels."""
137152
prefixes = self.labels + ['']
138153
return ":\n".join(" "*i+label for i, label in enumerate(prefixes))
139154

140-
def write(self, message):
155+
def write(self, message: str) -> None:
141156
"""Write `message`, but with the labels prepended."""
142157
self.debug.write(f"{self.message_prefix()}{message}")
143158

144159

145160
class DebugPluginWrapper(CoveragePlugin):
146161
"""Wrap a plugin, and use debug to report on what it's doing."""
147162

148-
def __init__(self, plugin, debug):
163+
def __init__(self, plugin: CoveragePlugin, debug: LabelledDebug) -> None:
149164
super().__init__()
150165
self.plugin = plugin
151166
self.debug = debug
152167

153-
def file_tracer(self, filename):
168+
def file_tracer(self, filename: str) -> Optional[FileTracer]:
154169
tracer = self.plugin.file_tracer(filename)
155170
self.debug.write(f"file_tracer({filename!r}) --> {tracer!r}")
156171
if tracer:
157172
debug = self.debug.add_label(f"file {filename!r}")
158173
tracer = DebugFileTracerWrapper(tracer, debug)
159174
return tracer
160175

161-
def file_reporter(self, filename):
176+
def file_reporter(self, filename: str) -> Union[FileReporter, str]:
162177
reporter = self.plugin.file_reporter(filename)
178+
assert isinstance(reporter, FileReporter)
163179
self.debug.write(f"file_reporter({filename!r}) --> {reporter!r}")
164180
if reporter:
165181
debug = self.debug.add_label(f"file {filename!r}")
166182
reporter = DebugFileReporterWrapper(filename, reporter, debug)
167183
return reporter
168184

169-
def dynamic_context(self, frame):
185+
def dynamic_context(self, frame: FrameType) -> Optional[str]:
170186
context = self.plugin.dynamic_context(frame)
171187
self.debug.write(f"dynamic_context({frame!r}) --> {context!r}")
172188
return context
173189

174-
def find_executable_files(self, src_dir):
190+
def find_executable_files(self, src_dir: str) -> Iterable[str]:
175191
executable_files = self.plugin.find_executable_files(src_dir)
176192
self.debug.write(f"find_executable_files({src_dir!r}) --> {executable_files!r}")
177193
return executable_files
178194

179-
def configure(self, config):
195+
def configure(self, config: TConfigurable) -> None:
180196
self.debug.write(f"configure({config!r})")
181197
self.plugin.configure(config)
182198

183-
def sys_info(self):
199+
def sys_info(self) -> Iterable[Tuple[str, Any]]:
184200
return self.plugin.sys_info()
185201

186202

187203
class DebugFileTracerWrapper(FileTracer):
188204
"""A debugging `FileTracer`."""
189205

190-
def __init__(self, tracer, debug):
206+
def __init__(self, tracer: FileTracer, debug: LabelledDebug) -> None:
191207
self.tracer = tracer
192208
self.debug = debug
193209

194-
def _show_frame(self, frame):
210+
def _show_frame(self, frame: FrameType) -> str:
195211
"""A short string identifying a frame, for debug messages."""
196212
return "%s@%d" % (
197213
os.path.basename(frame.f_code.co_filename),
198214
frame.f_lineno,
199215
)
200216

201-
def source_filename(self):
217+
def source_filename(self) -> str:
202218
sfilename = self.tracer.source_filename()
203219
self.debug.write(f"source_filename() --> {sfilename!r}")
204220
return sfilename
205221

206-
def has_dynamic_source_filename(self):
222+
def has_dynamic_source_filename(self) -> bool:
207223
has = self.tracer.has_dynamic_source_filename()
208224
self.debug.write(f"has_dynamic_source_filename() --> {has!r}")
209225
return has
210226

211-
def dynamic_source_filename(self, filename, frame):
227+
def dynamic_source_filename(self, filename: str, frame: FrameType) -> Optional[str]:
212228
dyn = self.tracer.dynamic_source_filename(filename, frame)
213229
self.debug.write("dynamic_source_filename({!r}, {}) --> {!r}".format(
214230
filename, self._show_frame(frame), dyn,
215231
))
216232
return dyn
217233

218-
def line_number_range(self, frame):
234+
def line_number_range(self, frame: FrameType) -> Tuple[TLineNo, TLineNo]:
219235
pair = self.tracer.line_number_range(frame)
220236
self.debug.write(f"line_number_range({self._show_frame(frame)}) --> {pair!r}")
221237
return pair
@@ -224,57 +240,57 @@ def line_number_range(self, frame):
224240
class DebugFileReporterWrapper(FileReporter):
225241
"""A debugging `FileReporter`."""
226242

227-
def __init__(self, filename, reporter, debug):
243+
def __init__(self, filename: str, reporter: FileReporter, debug: LabelledDebug) -> None:
228244
super().__init__(filename)
229245
self.reporter = reporter
230246
self.debug = debug
231247

232-
def relative_filename(self):
248+
def relative_filename(self) -> str:
233249
ret = self.reporter.relative_filename()
234250
self.debug.write(f"relative_filename() --> {ret!r}")
235251
return ret
236252

237-
def lines(self):
253+
def lines(self) -> Set[TLineNo]:
238254
ret = self.reporter.lines()
239255
self.debug.write(f"lines() --> {ret!r}")
240256
return ret
241257

242-
def excluded_lines(self):
258+
def excluded_lines(self) -> Set[TLineNo]:
243259
ret = self.reporter.excluded_lines()
244260
self.debug.write(f"excluded_lines() --> {ret!r}")
245261
return ret
246262

247-
def translate_lines(self, lines):
263+
def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]:
248264
ret = self.reporter.translate_lines(lines)
249265
self.debug.write(f"translate_lines({lines!r}) --> {ret!r}")
250266
return ret
251267

252-
def translate_arcs(self, arcs):
268+
def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]:
253269
ret = self.reporter.translate_arcs(arcs)
254270
self.debug.write(f"translate_arcs({arcs!r}) --> {ret!r}")
255271
return ret
256272

257-
def no_branch_lines(self):
273+
def no_branch_lines(self) -> Set[TLineNo]:
258274
ret = self.reporter.no_branch_lines()
259275
self.debug.write(f"no_branch_lines() --> {ret!r}")
260276
return ret
261277

262-
def exit_counts(self):
278+
def exit_counts(self) -> Dict[TLineNo, int]:
263279
ret = self.reporter.exit_counts()
264280
self.debug.write(f"exit_counts() --> {ret!r}")
265281
return ret
266282

267-
def arcs(self):
283+
def arcs(self) -> Set[TArc]:
268284
ret = self.reporter.arcs()
269285
self.debug.write(f"arcs() --> {ret!r}")
270286
return ret
271287

272-
def source(self):
288+
def source(self) -> str:
273289
ret = self.reporter.source()
274290
self.debug.write("source() --> %d chars" % (len(ret),))
275291
return ret
276292

277-
def source_token_lines(self):
293+
def source_token_lines(self) -> TSourceTokenLines:
278294
ret = list(self.reporter.source_token_lines())
279295
self.debug.write("source_token_lines() --> %d tokens" % (len(ret),))
280296
return ret

‎coverage/pytracer.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
import dis
88
import sys
99

10+
from types import FrameType
11+
from typing import Any, Callable, Dict, Mapping, Optional
12+
1013
from coverage import env
14+
from coverage.types import TFileDisposition, TTraceData, TTraceFn, TTracer, TWarnFn
1115

1216
# We need the YIELD_VALUE opcode below, in a comparison-friendly form.
1317
RESUME = dis.opmap.get('RESUME')
@@ -22,7 +26,7 @@
2226

2327
THIS_FILE = __file__.rstrip("co")
2428

25-
class PyTracer:
29+
class PyTracer(TTracer):
2630
"""Python implementation of the raw data tracer."""
2731

2832
# Because of poor implementations of trace-function-manipulating tools,
@@ -41,14 +45,17 @@ class PyTracer:
4145
# PyTracer to get accurate results. The command-line --timid argument is
4246
# used to force the use of this tracer.
4347

44-
def __init__(self):
48+
def __init__(self) -> None:
49+
# pylint: disable=super-init-not-called
4550
# Attributes set from the collector:
46-
self.data = None
51+
self.data: TTraceData
4752
self.trace_arcs = False
48-
self.should_trace = None
49-
self.should_trace_cache = None
50-
self.should_start_context = None
51-
self.warn = None
53+
self.should_trace: Callable[[str, FrameType], TFileDisposition]
54+
self.should_trace_cache: Mapping[str, Optional[TFileDisposition]]
55+
self.should_start_context: Optional[Callable[[FrameType], Optional[str]]] = None
56+
self.switch_context: Optional[Callable[[Optional[str]], None]] = None
57+
self.warn: TWarnFn
58+
5259
# The threading module to use, if any.
5360
self.threading = None
5461

@@ -71,14 +78,13 @@ def __init__(self):
7178
# re-create a bound method object all the time.
7279
self._cached_bound_method_trace = self._trace
7380

74-
def __repr__(self):
75-
return "<PyTracer at 0x{:x}: {} lines in {} files>".format(
76-
id(self),
77-
sum(len(v) for v in self.data.values()),
78-
len(self.data),
79-
)
81+
def __repr__(self) -> str:
82+
me = id(self)
83+
points = sum(len(v) for v in self.data.values())
84+
files = len(self.data)
85+
return f"<PyTracer at 0x{me:x}: {points} data points in {files} files>"
8086

81-
def log(self, marker, *args):
87+
def log(self, marker, *args) -> None:
8288
"""For hard-core logging of what this tracer is doing."""
8389
with open("/tmp/debug_trace.txt", "a") as f:
8490
f.write("{} {}[{}]".format(
@@ -101,7 +107,7 @@ def log(self, marker, *args):
101107
f.write(stack)
102108
f.write("\n")
103109

104-
def _trace(self, frame, event, arg_unused):
110+
def _trace(self, frame: FrameType, event: str, arg_unused: Any) -> TTraceFn:
105111
"""The trace function passed to sys.settrace."""
106112

107113
if THIS_FILE in frame.f_code.co_filename:
@@ -242,7 +248,7 @@ def _trace(self, frame, event, arg_unused):
242248
self.switch_context(None)
243249
return self._cached_bound_method_trace
244250

245-
def start(self):
251+
def start(self) -> TTraceFn:
246252
"""Start this Tracer.
247253
248254
Return a Python function suitable for use with sys.settrace().
@@ -263,7 +269,7 @@ def start(self):
263269
sys.settrace(self._cached_bound_method_trace)
264270
return self._cached_bound_method_trace
265271

266-
def stop(self):
272+
def stop(self) -> None:
267273
"""Stop this Tracer."""
268274
# Get the active tracer callback before setting the stop flag to be
269275
# able to detect if the tracer was changed prior to stopping it.
@@ -293,14 +299,14 @@ def stop(self):
293299
slug="trace-changed",
294300
)
295301

296-
def activity(self):
302+
def activity(self) -> bool:
297303
"""Has there been any activity?"""
298304
return self._activity
299305

300-
def reset_activity(self):
306+
def reset_activity(self) -> None:
301307
"""Reset the activity() flag."""
302308
self._activity = False
303309

304-
def get_stats(self):
310+
def get_stats(self) -> Optional[Dict[str, int]]:
305311
"""Return a dictionary of statistics, or None."""
306312
return None

‎coverage/sqldata.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import zlib
2323

2424
from typing import (
25-
cast, Any, Callable, Dict, Generator, Iterable, List, Optional,
25+
cast, Any, Callable, Collection, Dict, Generator, Iterable, List, Mapping, Optional,
2626
Sequence, Set, Tuple, TypeVar, Union,
2727
)
2828

@@ -430,7 +430,7 @@ def _context_id(self, context: str) -> Optional[int]:
430430
return None
431431

432432
@_locked
433-
def set_context(self, context: str) -> None:
433+
def set_context(self, context: Optional[str]) -> None:
434434
"""Set the current context for future :meth:`add_lines` etc.
435435
436436
`context` is a str, the name of the context to use for the next data
@@ -474,7 +474,7 @@ def data_filename(self) -> str:
474474
return self._filename
475475

476476
@_locked
477-
def add_lines(self, line_data: Dict[str, Sequence[TLineNo]]) -> None:
477+
def add_lines(self, line_data: Mapping[str, Collection[TLineNo]]) -> None:
478478
"""Add measured line data.
479479
480480
`line_data` is a dictionary mapping file names to iterables of ints::
@@ -508,7 +508,7 @@ def add_lines(self, line_data: Dict[str, Sequence[TLineNo]]) -> None:
508508
)
509509

510510
@_locked
511-
def add_arcs(self, arc_data: Dict[str, Set[TArc]]) -> None:
511+
def add_arcs(self, arc_data: Mapping[str, Collection[TArc]]) -> None:
512512
"""Add measured arc data.
513513
514514
`arc_data` is a dictionary mapping file names to iterables of pairs of
@@ -558,7 +558,7 @@ def _choose_lines_or_arcs(self, lines: bool=False, arcs: bool=False) -> None:
558558
)
559559

560560
@_locked
561-
def add_file_tracers(self, file_tracers: Dict[str, str]) -> None:
561+
def add_file_tracers(self, file_tracers: Mapping[str, str]) -> None:
562562
"""Add per-file plugin information.
563563
564564
`file_tracers` is { filename: plugin_name, ... }

‎coverage/tracer.pyi

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2+
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3+
4+
from typing import Any, Dict
5+
6+
from coverage.types import TFileDisposition, TTraceData, TTraceFn, TTracer
7+
8+
class CFileDisposition(TFileDisposition):
9+
canonical_filename: Any
10+
file_tracer: Any
11+
has_dynamic_filename: Any
12+
original_filename: Any
13+
reason: Any
14+
source_filename: Any
15+
trace: Any
16+
def __init__(self) -> None: ...
17+
18+
class CTracer(TTracer):
19+
check_include: Any
20+
concur_id_func: Any
21+
data: TTraceData
22+
disable_plugin: Any
23+
file_tracers: Any
24+
should_start_context: Any
25+
should_trace: Any
26+
should_trace_cache: Any
27+
switch_context: Any
28+
trace_arcs: Any
29+
warn: Any
30+
def __init__(self) -> None: ...
31+
def activity(self) -> bool: ...
32+
def get_stats(self) -> Dict[str, int]: ...
33+
def reset_activity(self) -> Any: ...
34+
def start(self) -> TTraceFn: ...
35+
def stop(self) -> None: ...

‎coverage/types.py

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,101 @@
55
Types for use throughout coverage.py.
66
"""
77

8-
from types import ModuleType
8+
from __future__ import annotations
9+
10+
from types import FrameType, ModuleType
911
from typing import (
10-
Any, Dict, Iterable, List, Optional, Tuple, Union,
12+
Any, Callable, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Union,
1113
TYPE_CHECKING,
1214
)
1315

1416
if TYPE_CHECKING:
1517
# Protocol is new in 3.8. PYVERSIONS
1618
from typing import Protocol
19+
20+
from coverage.plugin import FileTracer
21+
1722
else:
1823
class Protocol: # pylint: disable=missing-class-docstring
1924
pass
2025

26+
## Python tracing
27+
28+
class TTraceFn(Protocol):
29+
"""A Python trace function."""
30+
def __call__(
31+
self,
32+
frame: FrameType,
33+
event: str,
34+
arg: Any,
35+
lineno: Optional[int]=None # Our own twist, see collector.py
36+
) -> TTraceFn:
37+
...
38+
39+
## Coverage.py tracing
40+
41+
# Line numbers are pervasive enough that they deserve their own type.
42+
TLineNo = int
43+
44+
TArc = Tuple[TLineNo, TLineNo]
45+
46+
class TFileDisposition(Protocol):
47+
"""A simple value type for recording what to do with a file."""
48+
49+
original_filename: str
50+
canonical_filename: str
51+
source_filename: Optional[str]
52+
trace: bool
53+
reason: str
54+
file_tracer: Optional[FileTracer]
55+
has_dynamic_filename: bool
56+
57+
58+
# When collecting data, we use a dictionary with a few possible shapes. The
59+
# keys are always file names.
60+
# - If measuring line coverage, the values are sets of line numbers.
61+
# - If measuring arcs in the Python tracer, the values are sets of arcs (pairs
62+
# of line numbers).
63+
# - If measuring arcs in the C tracer, the values are sets of packed arcs (two
64+
# line numbers combined into one integer).
65+
66+
TTraceData = Union[
67+
Dict[str, Set[TLineNo]],
68+
Dict[str, Set[TArc]],
69+
Dict[str, Set[int]],
70+
]
71+
72+
class TTracer(Protocol):
73+
"""Either CTracer or PyTracer."""
74+
75+
data: TTraceData
76+
trace_arcs: bool
77+
should_trace: Callable[[str, FrameType], TFileDisposition]
78+
should_trace_cache: Mapping[str, Optional[TFileDisposition]]
79+
should_start_context: Optional[Callable[[FrameType], Optional[str]]]
80+
switch_context: Optional[Callable[[Optional[str]], None]]
81+
warn: TWarnFn
82+
83+
def __init__(self) -> None:
84+
...
85+
86+
def start(self) -> TTraceFn:
87+
"""Start this tracer, returning a trace function."""
88+
89+
def stop(self) -> None:
90+
"""Stop this tracer."""
91+
92+
def activity(self) -> bool:
93+
"""Has there been any activity?"""
94+
95+
def reset_activity(self) -> None:
96+
"""Reset the activity() flag."""
97+
98+
def get_stats(self) -> Optional[Dict[str, int]]:
99+
"""Return a dictionary of statistics, or None."""
100+
101+
## Coverage
102+
21103
# Many places use kwargs as Coverage kwargs.
22104
TCovKwargs = Any
23105

@@ -56,15 +138,18 @@ def set_option(self, option_name: str, value: Union[TConfigValue, TConfigSection
56138

57139
## Parsing
58140

59-
# Line numbers are pervasive enough that they deserve their own type.
60-
TLineNo = int
61-
62-
TArc = Tuple[TLineNo, TLineNo]
63-
64141
TMorf = Union[ModuleType, str]
65142

66143
TSourceTokenLines = Iterable[List[Tuple[str, str]]]
67144

145+
## Plugins
146+
147+
class TPlugin(Protocol):
148+
"""What all plugins have in common."""
149+
_coverage_plugin_name: str
150+
_coverage_enabled: bool
151+
152+
68153
## Debugging
69154

70155
class TWarnFn(Protocol):

‎tests/test_oddball.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def recur(n):
139139
assert re.fullmatch(
140140
r"Trace function changed, data is likely wrong: None != " +
141141
r"<bound method PyTracer._trace of " +
142-
"<PyTracer at 0x[0-9a-fA-F]+: 5 lines in 1 files>>",
142+
"<PyTracer at 0x[0-9a-fA-F]+: 5 data points in 1 files>>",
143143
cov._warnings[0],
144144
)
145145
else:

‎tox.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ deps =
9696
setenv =
9797
{[testenv]setenv}
9898
C__B=coverage/__init__.py coverage/__main__.py coverage/annotate.py coverage/bytecode.py
99-
C_CC=coverage/cmdline.py coverage/config.py coverage/context.py coverage/control.py
99+
C_CC=coverage/cmdline.py coverage/collector.py coverage/config.py coverage/context.py coverage/control.py
100100
C_DE=coverage/data.py coverage/disposition.py coverage/env.py coverage/exceptions.py
101101
C_FN=coverage/files.py coverage/inorout.py coverage/jsonreport.py coverage/lcovreport.py coverage/multiproc.py coverage/numbits.py
102-
C_OP=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/python.py
102+
C_OP=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/plugin_support.py coverage/python.py
103103
C_QZ=coverage/report.py coverage/results.py coverage/sqldata.py coverage/tomlconfig.py coverage/types.py coverage/version.py
104104
T_AN=tests/test_api.py tests/test_cmdline.py tests/goldtest.py tests/helpers.py tests/test_html.py
105105
TYPEABLE={env:C__B} {env:C_CC} {env:C_DE} {env:C_FN} {env:C_OP} {env:C_QZ} {env:T_AN}

0 commit comments

Comments
 (0)
Please sign in to comment.