3
3
4
4
"""Config file for coverage.py"""
5
5
6
+ from __future__ import annotations
6
7
import collections
7
8
import configparser
8
9
import copy
9
10
import os
10
11
import os .path
11
12
import re
12
13
14
+ from typing import (
15
+ Any , Callable , Dict , Iterable , List , Optional , Tuple , Union ,
16
+ )
17
+
13
18
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
15
20
16
21
from coverage .tomlconfig import TomlConfigParser , TomlDecodeError
17
22
18
23
os = isolate_module (os )
19
24
20
25
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
+
21
32
class HandyConfigParser (configparser .ConfigParser ):
22
33
"""Our specialization of ConfigParser."""
23
34
24
- def __init__ (self , our_file ) :
35
+ def __init__ (self , our_file : bool ) -> None :
25
36
"""Create the HandyConfigParser.
26
37
27
38
`our_file` is True if this config file is specifically for coverage,
@@ -34,41 +45,46 @@ def __init__(self, our_file):
34
45
if our_file :
35
46
self .section_prefixes .append ("" )
36
47
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 ]:
38
53
"""Read a file name as UTF-8 configuration data."""
39
54
return super ().read (filenames , encoding = "utf-8" )
40
55
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."""
50
58
for section_prefix in self .section_prefixes :
51
59
real_section = section_prefix + section
52
60
has = super ().has_section (real_section )
53
61
if has :
54
62
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 )
55
69
return False
56
70
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 )
62
78
raise ConfigError (f"No section: { section !r} " )
63
79
64
- def get_section (self , section ) :
80
+ def get_section (self , section : str ) -> TConfigSection :
65
81
"""Get the contents of a section, as a dictionary."""
66
- d = {}
82
+ d : TConfigSection = {}
67
83
for opt in self .options (section ):
68
84
d [opt ] = self .get (section , opt )
69
85
return d
70
86
71
- def get (self , section , option , * args , ** kwargs ):
87
+ def get (self , section : str , option : str , * args : Any , ** kwargs : Any ) -> str : # type: ignore
72
88
"""Get a value, replacing environment variables also.
73
89
74
90
The arguments are the same as `ConfigParser.get`, but in the found
@@ -85,11 +101,11 @@ def get(self, section, option, *args, **kwargs):
85
101
else :
86
102
raise ConfigError (f"No option { option !r} in section: { section !r} " )
87
103
88
- v = super ().get (real_section , option , * args , ** kwargs )
104
+ v : str = super ().get (real_section , option , * args , ** kwargs )
89
105
v = substitute_variables (v , os .environ )
90
106
return v
91
107
92
- def getlist (self , section , option ) :
108
+ def getlist (self , section : str , option : str ) -> List [ str ] :
93
109
"""Read a list of strings.
94
110
95
111
The value of `section` and `option` is treated as a comma- and newline-
@@ -107,7 +123,7 @@ def getlist(self, section, option):
107
123
values .append (value )
108
124
return values
109
125
110
- def getregexlist (self , section , option ) :
126
+ def getregexlist (self , section : str , option : str ) -> List [ str ] :
111
127
"""Read a list of full-line regexes.
112
128
113
129
The value of `section` and `option` is treated as a newline-separated
@@ -131,6 +147,9 @@ def getregexlist(self, section, option):
131
147
return value_list
132
148
133
149
150
+ TConfigParser = Union [HandyConfigParser , TomlConfigParser ]
151
+
152
+
134
153
# The default line exclusion regexes.
135
154
DEFAULT_EXCLUDE = [
136
155
r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)' ,
@@ -159,16 +178,16 @@ class CoverageConfig:
159
178
"""
160
179
# pylint: disable=too-many-instance-attributes
161
180
162
- def __init__ (self ):
181
+ def __init__ (self ) -> None :
163
182
"""Initialize the configuration attributes to their defaults."""
164
183
# Metadata about the config.
165
184
# We tried to read these config files.
166
- self .attempted_config_files = []
185
+ self .attempted_config_files : List [ str ] = []
167
186
# 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 ] = []
169
188
# 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
172
191
173
192
# Defaults for [run] and [report]
174
193
self ._include = None
@@ -181,17 +200,17 @@ def __init__(self):
181
200
self .context = None
182
201
self .cover_pylib = False
183
202
self .data_file = ".coverage"
184
- self .debug = []
185
- self .disable_warnings = []
203
+ self .debug : List [ str ] = []
204
+ self .disable_warnings : List [ str ] = []
186
205
self .dynamic_context = None
187
206
self .parallel = False
188
- self .plugins = []
207
+ self .plugins : List [ str ] = []
189
208
self .relative_files = False
190
209
self .run_include = None
191
210
self .run_omit = None
192
211
self .sigterm = False
193
212
self .source = None
194
- self .source_pkgs = []
213
+ self .source_pkgs : List [ str ] = []
195
214
self .timid = False
196
215
self ._crash = None
197
216
@@ -233,27 +252,26 @@ def __init__(self):
233
252
self .lcov_output = "coverage.lcov"
234
253
235
254
# Defaults for [paths]
236
- self .paths = collections . OrderedDict ()
255
+ self .paths : Dict [ str , List [ str ]] = {}
237
256
238
257
# Options for plugins
239
- self .plugin_options = {}
258
+ self .plugin_options : Dict [ str , TConfigSection ] = {}
240
259
241
260
MUST_BE_LIST = {
242
261
"debug" , "concurrency" , "plugins" ,
243
262
"report_omit" , "report_include" ,
244
263
"run_omit" , "run_include" ,
245
264
}
246
265
247
- def from_args (self , ** kwargs ) :
266
+ def from_args (self , ** kwargs : TConfigValue ) -> None :
248
267
"""Read config values from `kwargs`."""
249
268
for k , v in kwargs .items ():
250
269
if v is not None :
251
270
if k in self .MUST_BE_LIST and isinstance (v , str ):
252
271
v = [v ]
253
272
setattr (self , k , v )
254
273
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 :
257
275
"""Read configuration from a .rc file.
258
276
259
277
`filename` is a file name to read.
@@ -267,6 +285,7 @@ def from_file(self, filename, warn, our_file):
267
285
268
286
"""
269
287
_ , ext = os .path .splitext (filename )
288
+ cp : TConfigParser
270
289
if ext == '.toml' :
271
290
cp = TomlConfigParser (our_file )
272
291
else :
@@ -299,7 +318,7 @@ def from_file(self, filename, warn, our_file):
299
318
all_options [section ].add (option )
300
319
301
320
for section , options in all_options .items ():
302
- real_section = cp .has_section (section )
321
+ real_section = cp .real_section (section )
303
322
if real_section :
304
323
for unknown in set (cp .options (section )) - options :
305
324
warn (
@@ -335,7 +354,7 @@ def from_file(self, filename, warn, our_file):
335
354
336
355
return used
337
356
338
- def copy (self ):
357
+ def copy (self ) -> CoverageConfig :
339
358
"""Return a copy of the configuration."""
340
359
return copy .deepcopy (self )
341
360
@@ -409,7 +428,13 @@ def copy(self):
409
428
('lcov_output' , 'lcov:output' ),
410
429
]
411
430
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 :
413
438
"""Set an attribute on self if it exists in the ConfigParser.
414
439
415
440
Returns True if the attribute was set.
@@ -422,11 +447,11 @@ def _set_attr_from_config_option(self, cp, attr, where, type_=''):
422
447
return True
423
448
return False
424
449
425
- def get_plugin_options (self , plugin ) :
450
+ def get_plugin_options (self , plugin : str ) -> TConfigSection :
426
451
"""Get a dictionary of options for the plugin named `plugin`."""
427
452
return self .plugin_options .get (plugin , {})
428
453
429
- def set_option (self , option_name , value ) :
454
+ def set_option (self , option_name : str , value : Union [ TConfigValue , TConfigSection ]) -> None :
430
455
"""Set an option in the configuration.
431
456
432
457
`option_name` is a colon-separated string indicating the section and
@@ -438,7 +463,7 @@ def set_option(self, option_name, value):
438
463
"""
439
464
# Special-cased options.
440
465
if option_name == "paths" :
441
- self .paths = value
466
+ self .paths = value # type: ignore
442
467
return
443
468
444
469
# Check all the hard-coded options.
@@ -451,13 +476,13 @@ def set_option(self, option_name, value):
451
476
# See if it's a plugin option.
452
477
plugin_name , _ , key = option_name .partition (":" )
453
478
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
455
480
return
456
481
457
482
# If we get here, we didn't find the option.
458
483
raise ConfigError (f"No such option: { option_name !r} " )
459
484
460
- def get_option (self , option_name ) :
485
+ def get_option (self , option_name : str ) -> Optional [ TConfigValue ] :
461
486
"""Get an option from the configuration.
462
487
463
488
`option_name` is a colon-separated string indicating the section and
@@ -469,13 +494,13 @@ def get_option(self, option_name):
469
494
"""
470
495
# Special-cased options.
471
496
if option_name == "paths" :
472
- return self .paths
497
+ return self .paths # type: ignore
473
498
474
499
# Check all the hard-coded options.
475
500
for option_spec in self .CONFIG_FILE_OPTIONS :
476
501
attr , where = option_spec [:2 ]
477
502
if where == option_name :
478
- return getattr (self , attr )
503
+ return getattr (self , attr ) # type: ignore
479
504
480
505
# See if it's a plugin option.
481
506
plugin_name , _ , key = option_name .partition (":" )
@@ -485,28 +510,28 @@ def get_option(self, option_name):
485
510
# If we get here, we didn't find the option.
486
511
raise ConfigError (f"No such option: { option_name !r} " )
487
512
488
- def post_process_file (self , path ) :
513
+ def post_process_file (self , path : str ) -> str :
489
514
"""Make final adjustments to a file path to make it usable."""
490
515
return os .path .expanduser (path )
491
516
492
- def post_process (self ):
517
+ def post_process (self ) -> None :
493
518
"""Make final adjustments to settings to make them usable."""
494
519
self .data_file = self .post_process_file (self .data_file )
495
520
self .html_dir = self .post_process_file (self .html_dir )
496
521
self .xml_output = self .post_process_file (self .xml_output )
497
- self .paths = collections . OrderedDict (
522
+ self .paths = dict (
498
523
(k , [self .post_process_file (f ) for f in v ])
499
524
for k , v in self .paths .items ()
500
525
)
501
526
502
- def debug_info (self ):
527
+ def debug_info (self ) -> List [ Tuple [ str , str ]] :
503
528
"""Make a list of (name, value) pairs for writing debug info."""
504
- return human_sorted_items (
529
+ return human_sorted_items ( # type: ignore
505
530
(k , v ) for k , v in self .__dict__ .items () if not k .startswith ("_" )
506
531
)
507
532
508
533
509
- def config_files_to_try (config_file ) :
534
+ def config_files_to_try (config_file : Union [ bool , str ]) -> List [ Tuple [ str , bool , bool ]] :
510
535
"""What config files should we try to read?
511
536
512
537
Returns a list of tuples:
@@ -520,12 +545,14 @@ def config_files_to_try(config_file):
520
545
specified_file = (config_file is not True )
521
546
if not specified_file :
522
547
# 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
525
551
specified_file = True
526
552
if not specified_file :
527
553
# Still no file specified. Default to .coveragerc
528
554
config_file = ".coveragerc"
555
+ assert isinstance (config_file , str )
529
556
files_to_try = [
530
557
(config_file , True , specified_file ),
531
558
("setup.cfg" , False , False ),
@@ -535,7 +562,11 @@ def config_files_to_try(config_file):
535
562
return files_to_try
536
563
537
564
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 :
539
570
"""Read the coverage.py configuration.
540
571
541
572
Arguments:
0 commit comments