Skip to content

Commit 212f652

Browse files
committed
test: run mypy on config.py
1 parent cceadff commit 212f652

File tree

4 files changed

+100
-64
lines changed

4 files changed

+100
-64
lines changed

coverage/config.py

Lines changed: 87 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,36 @@
33

44
"""Config file for coverage.py"""
55

6+
from __future__ import annotations
67
import collections
78
import configparser
89
import copy
910
import os
1011
import os.path
1112
import re
1213

14+
from typing import (
15+
Any, Callable, Dict, Iterable, List, Optional, Tuple, Union,
16+
)
17+
1318
from coverage.exceptions import ConfigError
14-
from coverage.misc import contract, isolate_module, human_sorted_items, substitute_variables
19+
from coverage.misc import isolate_module, human_sorted_items, substitute_variables
1520

1621
from coverage.tomlconfig import TomlConfigParser, TomlDecodeError
1722

1823
os = isolate_module(os)
1924

2025

26+
# One value read from a config file.
27+
TConfigValue = Union[str, List[str]]
28+
# An entire config section, mapping option names to values.
29+
TConfigSection = Dict[str, TConfigValue]
30+
31+
2132
class HandyConfigParser(configparser.ConfigParser):
2233
"""Our specialization of ConfigParser."""
2334

24-
def __init__(self, our_file):
35+
def __init__(self, our_file: bool) -> None:
2536
"""Create the HandyConfigParser.
2637
2738
`our_file` is True if this config file is specifically for coverage,
@@ -34,41 +45,46 @@ def __init__(self, our_file):
3445
if our_file:
3546
self.section_prefixes.append("")
3647

37-
def read(self, filenames, encoding_unused=None):
48+
def read( # type: ignore[override]
49+
self,
50+
filenames: Iterable[str],
51+
encoding_unused: Optional[str]=None,
52+
) -> List[str]:
3853
"""Read a file name as UTF-8 configuration data."""
3954
return super().read(filenames, encoding="utf-8")
4055

41-
def has_option(self, section, option):
42-
for section_prefix in self.section_prefixes:
43-
real_section = section_prefix + section
44-
has = super().has_option(real_section, option)
45-
if has:
46-
return has
47-
return False
48-
49-
def has_section(self, section):
56+
def real_section(self, section: str) -> Optional[str]:
57+
"""Get the actual name of a section."""
5058
for section_prefix in self.section_prefixes:
5159
real_section = section_prefix + section
5260
has = super().has_section(real_section)
5361
if has:
5462
return real_section
63+
return None
64+
65+
def has_option(self, section: str, option: str) -> bool:
66+
real_section = self.real_section(section)
67+
if real_section is not None:
68+
return super().has_option(real_section, option)
5569
return False
5670

57-
def options(self, section):
58-
for section_prefix in self.section_prefixes:
59-
real_section = section_prefix + section
60-
if super().has_section(real_section):
61-
return super().options(real_section)
71+
def has_section(self, section: str) -> bool:
72+
return bool(self.real_section(section))
73+
74+
def options(self, section: str) -> List[str]:
75+
real_section = self.real_section(section)
76+
if real_section is not None:
77+
return super().options(real_section)
6278
raise ConfigError(f"No section: {section!r}")
6379

64-
def get_section(self, section):
80+
def get_section(self, section: str) -> TConfigSection:
6581
"""Get the contents of a section, as a dictionary."""
66-
d = {}
82+
d: TConfigSection = {}
6783
for opt in self.options(section):
6884
d[opt] = self.get(section, opt)
6985
return d
7086

71-
def get(self, section, option, *args, **kwargs):
87+
def get(self, section: str, option: str, *args: Any, **kwargs: Any) -> str: # type: ignore
7288
"""Get a value, replacing environment variables also.
7389
7490
The arguments are the same as `ConfigParser.get`, but in the found
@@ -85,11 +101,11 @@ def get(self, section, option, *args, **kwargs):
85101
else:
86102
raise ConfigError(f"No option {option!r} in section: {section!r}")
87103

88-
v = super().get(real_section, option, *args, **kwargs)
104+
v: str = super().get(real_section, option, *args, **kwargs)
89105
v = substitute_variables(v, os.environ)
90106
return v
91107

92-
def getlist(self, section, option):
108+
def getlist(self, section: str, option: str) -> List[str]:
93109
"""Read a list of strings.
94110
95111
The value of `section` and `option` is treated as a comma- and newline-
@@ -107,7 +123,7 @@ def getlist(self, section, option):
107123
values.append(value)
108124
return values
109125

110-
def getregexlist(self, section, option):
126+
def getregexlist(self, section: str, option: str) -> List[str]:
111127
"""Read a list of full-line regexes.
112128
113129
The value of `section` and `option` is treated as a newline-separated
@@ -131,6 +147,9 @@ def getregexlist(self, section, option):
131147
return value_list
132148

133149

150+
TConfigParser = Union[HandyConfigParser, TomlConfigParser]
151+
152+
134153
# The default line exclusion regexes.
135154
DEFAULT_EXCLUDE = [
136155
r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)',
@@ -159,16 +178,16 @@ class CoverageConfig:
159178
"""
160179
# pylint: disable=too-many-instance-attributes
161180

162-
def __init__(self):
181+
def __init__(self) -> None:
163182
"""Initialize the configuration attributes to their defaults."""
164183
# Metadata about the config.
165184
# We tried to read these config files.
166-
self.attempted_config_files = []
185+
self.attempted_config_files: List[str] = []
167186
# We did read these config files, but maybe didn't find any content for us.
168-
self.config_files_read = []
187+
self.config_files_read: List[str] = []
169188
# The file that gave us our configuration.
170-
self.config_file = None
171-
self._config_contents = None
189+
self.config_file: Optional[str] = None
190+
self._config_contents: Optional[bytes] = None
172191

173192
# Defaults for [run] and [report]
174193
self._include = None
@@ -181,17 +200,17 @@ def __init__(self):
181200
self.context = None
182201
self.cover_pylib = False
183202
self.data_file = ".coverage"
184-
self.debug = []
185-
self.disable_warnings = []
203+
self.debug: List[str] = []
204+
self.disable_warnings: List[str] = []
186205
self.dynamic_context = None
187206
self.parallel = False
188-
self.plugins = []
207+
self.plugins: List[str] = []
189208
self.relative_files = False
190209
self.run_include = None
191210
self.run_omit = None
192211
self.sigterm = False
193212
self.source = None
194-
self.source_pkgs = []
213+
self.source_pkgs: List[str] = []
195214
self.timid = False
196215
self._crash = None
197216

@@ -233,27 +252,26 @@ def __init__(self):
233252
self.lcov_output = "coverage.lcov"
234253

235254
# Defaults for [paths]
236-
self.paths = collections.OrderedDict()
255+
self.paths: Dict[str, List[str]] = {}
237256

238257
# Options for plugins
239-
self.plugin_options = {}
258+
self.plugin_options: Dict[str, TConfigSection] = {}
240259

241260
MUST_BE_LIST = {
242261
"debug", "concurrency", "plugins",
243262
"report_omit", "report_include",
244263
"run_omit", "run_include",
245264
}
246265

247-
def from_args(self, **kwargs):
266+
def from_args(self, **kwargs: TConfigValue) -> None:
248267
"""Read config values from `kwargs`."""
249268
for k, v in kwargs.items():
250269
if v is not None:
251270
if k in self.MUST_BE_LIST and isinstance(v, str):
252271
v = [v]
253272
setattr(self, k, v)
254273

255-
@contract(filename=str)
256-
def from_file(self, filename, warn, our_file):
274+
def from_file(self, filename: str, warn: Callable[[str], None], our_file: bool) -> bool:
257275
"""Read configuration from a .rc file.
258276
259277
`filename` is a file name to read.
@@ -267,6 +285,7 @@ def from_file(self, filename, warn, our_file):
267285
268286
"""
269287
_, ext = os.path.splitext(filename)
288+
cp: TConfigParser
270289
if ext == '.toml':
271290
cp = TomlConfigParser(our_file)
272291
else:
@@ -299,7 +318,7 @@ def from_file(self, filename, warn, our_file):
299318
all_options[section].add(option)
300319

301320
for section, options in all_options.items():
302-
real_section = cp.has_section(section)
321+
real_section = cp.real_section(section)
303322
if real_section:
304323
for unknown in set(cp.options(section)) - options:
305324
warn(
@@ -335,7 +354,7 @@ def from_file(self, filename, warn, our_file):
335354

336355
return used
337356

338-
def copy(self):
357+
def copy(self) -> CoverageConfig:
339358
"""Return a copy of the configuration."""
340359
return copy.deepcopy(self)
341360

@@ -409,7 +428,13 @@ def copy(self):
409428
('lcov_output', 'lcov:output'),
410429
]
411430

412-
def _set_attr_from_config_option(self, cp, attr, where, type_=''):
431+
def _set_attr_from_config_option(
432+
self,
433+
cp: TConfigParser,
434+
attr: str,
435+
where: str,
436+
type_: str='',
437+
) -> bool:
413438
"""Set an attribute on self if it exists in the ConfigParser.
414439
415440
Returns True if the attribute was set.
@@ -422,11 +447,11 @@ def _set_attr_from_config_option(self, cp, attr, where, type_=''):
422447
return True
423448
return False
424449

425-
def get_plugin_options(self, plugin):
450+
def get_plugin_options(self, plugin: str) -> TConfigSection:
426451
"""Get a dictionary of options for the plugin named `plugin`."""
427452
return self.plugin_options.get(plugin, {})
428453

429-
def set_option(self, option_name, value):
454+
def set_option(self, option_name: str, value: Union[TConfigValue, TConfigSection]) -> None:
430455
"""Set an option in the configuration.
431456
432457
`option_name` is a colon-separated string indicating the section and
@@ -438,7 +463,7 @@ def set_option(self, option_name, value):
438463
"""
439464
# Special-cased options.
440465
if option_name == "paths":
441-
self.paths = value
466+
self.paths = value # type: ignore
442467
return
443468

444469
# Check all the hard-coded options.
@@ -451,13 +476,13 @@ def set_option(self, option_name, value):
451476
# See if it's a plugin option.
452477
plugin_name, _, key = option_name.partition(":")
453478
if key and plugin_name in self.plugins:
454-
self.plugin_options.setdefault(plugin_name, {})[key] = value
479+
self.plugin_options.setdefault(plugin_name, {})[key] = value # type: ignore
455480
return
456481

457482
# If we get here, we didn't find the option.
458483
raise ConfigError(f"No such option: {option_name!r}")
459484

460-
def get_option(self, option_name):
485+
def get_option(self, option_name: str) -> Optional[TConfigValue]:
461486
"""Get an option from the configuration.
462487
463488
`option_name` is a colon-separated string indicating the section and
@@ -469,13 +494,13 @@ def get_option(self, option_name):
469494
"""
470495
# Special-cased options.
471496
if option_name == "paths":
472-
return self.paths
497+
return self.paths # type: ignore
473498

474499
# Check all the hard-coded options.
475500
for option_spec in self.CONFIG_FILE_OPTIONS:
476501
attr, where = option_spec[:2]
477502
if where == option_name:
478-
return getattr(self, attr)
503+
return getattr(self, attr) # type: ignore
479504

480505
# See if it's a plugin option.
481506
plugin_name, _, key = option_name.partition(":")
@@ -485,28 +510,28 @@ def get_option(self, option_name):
485510
# If we get here, we didn't find the option.
486511
raise ConfigError(f"No such option: {option_name!r}")
487512

488-
def post_process_file(self, path):
513+
def post_process_file(self, path: str) -> str:
489514
"""Make final adjustments to a file path to make it usable."""
490515
return os.path.expanduser(path)
491516

492-
def post_process(self):
517+
def post_process(self) -> None:
493518
"""Make final adjustments to settings to make them usable."""
494519
self.data_file = self.post_process_file(self.data_file)
495520
self.html_dir = self.post_process_file(self.html_dir)
496521
self.xml_output = self.post_process_file(self.xml_output)
497-
self.paths = collections.OrderedDict(
522+
self.paths = dict(
498523
(k, [self.post_process_file(f) for f in v])
499524
for k, v in self.paths.items()
500525
)
501526

502-
def debug_info(self):
527+
def debug_info(self) -> List[Tuple[str, str]]:
503528
"""Make a list of (name, value) pairs for writing debug info."""
504-
return human_sorted_items(
529+
return human_sorted_items( # type: ignore
505530
(k, v) for k, v in self.__dict__.items() if not k.startswith("_")
506531
)
507532

508533

509-
def config_files_to_try(config_file):
534+
def config_files_to_try(config_file: Union[bool, str]) -> List[Tuple[str, bool, bool]]:
510535
"""What config files should we try to read?
511536
512537
Returns a list of tuples:
@@ -520,12 +545,14 @@ def config_files_to_try(config_file):
520545
specified_file = (config_file is not True)
521546
if not specified_file:
522547
# No file was specified. Check COVERAGE_RCFILE.
523-
config_file = os.environ.get('COVERAGE_RCFILE')
524-
if config_file:
548+
rcfile = os.environ.get('COVERAGE_RCFILE')
549+
if rcfile:
550+
config_file = rcfile
525551
specified_file = True
526552
if not specified_file:
527553
# Still no file specified. Default to .coveragerc
528554
config_file = ".coveragerc"
555+
assert isinstance(config_file, str)
529556
files_to_try = [
530557
(config_file, True, specified_file),
531558
("setup.cfg", False, False),
@@ -535,7 +562,11 @@ def config_files_to_try(config_file):
535562
return files_to_try
536563

537564

538-
def read_coverage_config(config_file, warn, **kwargs):
565+
def read_coverage_config(
566+
config_file: Union[bool, str],
567+
warn: Callable[[str], None],
568+
**kwargs: TConfigValue,
569+
) -> CoverageConfig:
539570
"""Read the coverage.py configuration.
540571
541572
Arguments:

coverage/tomlconfig.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,14 @@ def has_option(self, section, option):
119119
return False
120120
return option in data
121121

122-
def has_section(self, section):
122+
def real_section(self, section):
123123
name, _ = self._get_section(section)
124124
return name
125125

126+
def has_section(self, section):
127+
name, _ = self._get_section(section)
128+
return bool(name)
129+
126130
def options(self, section):
127131
_, data = self._get_section(section)
128132
if data is None:

0 commit comments

Comments
 (0)