3
3
4
4
"""Determining whether files are being measured/reported or not."""
5
5
6
- # For finding the stdlib
7
- import atexit
8
6
import inspect
9
7
import itertools
10
8
import os
11
9
import platform
12
10
import re
13
11
import sys
12
+ import sysconfig
14
13
import traceback
15
14
16
15
from coverage import env
17
- from coverage .backward import code_object
16
+ from coverage .backward import code_object , importlib_util_find_spec
18
17
from coverage .disposition import FileDisposition , disposition_init
19
18
from coverage .files import TreeMatcher , FnmatchMatcher , ModuleMatcher
20
19
from coverage .files import prep_patterns , find_python_files , canonical_filename
@@ -108,14 +107,53 @@ def module_has_file(mod):
108
107
return os .path .exists (mod__file__ )
109
108
110
109
110
+ def file_for_module (modulename ):
111
+ """Find the file for `modulename`, or return None."""
112
+ if importlib_util_find_spec :
113
+ filename = None
114
+ try :
115
+ spec = importlib_util_find_spec (modulename )
116
+ except ImportError :
117
+ pass
118
+ else :
119
+ if spec is not None :
120
+ filename = spec .origin
121
+ return filename
122
+ else :
123
+ import imp
124
+ openfile = None
125
+ glo , loc = globals (), locals ()
126
+ try :
127
+ # Search for the module - inside its parent package, if any - using
128
+ # standard import mechanics.
129
+ if '.' in modulename :
130
+ packagename , name = modulename .rsplit ('.' , 1 )
131
+ package = __import__ (packagename , glo , loc , ['__path__' ])
132
+ searchpath = package .__path__
133
+ else :
134
+ packagename , name = None , modulename
135
+ searchpath = None # "top-level search" in imp.find_module()
136
+ openfile , pathname , _ = imp .find_module (name , searchpath )
137
+ return pathname
138
+ except ImportError :
139
+ return None
140
+ finally :
141
+ if openfile :
142
+ openfile .close ()
143
+
144
+
111
145
def add_stdlib_paths (paths ):
112
146
"""Add paths where the stdlib can be found to the set `paths`."""
113
147
# Look at where some standard modules are located. That's the
114
148
# indication for "installed with the interpreter". In some
115
149
# environments (virtualenv, for example), these modules may be
116
150
# spread across a few locations. Look at all the candidate modules
117
151
# we've imported, and take all the different ones.
118
- for m in (atexit , inspect , os , platform , _pypy_irc_topic , re , _structseq , traceback ):
152
+ modules_we_happen_to_have = [
153
+ inspect , itertools , os , platform , re , sysconfig , traceback ,
154
+ _pypy_irc_topic , _structseq ,
155
+ ]
156
+ for m in modules_we_happen_to_have :
119
157
if m is not None and hasattr (m , "__file__" ):
120
158
paths .add (canonical_path (m , directory = True ))
121
159
@@ -129,6 +167,20 @@ def add_stdlib_paths(paths):
129
167
paths .add (canonical_path (structseq_file ))
130
168
131
169
170
+ def add_third_party_paths (paths ):
171
+ """Add locations for third-party packages to the set `paths`."""
172
+ # Get the paths that sysconfig knows about.
173
+ scheme_names = set (sysconfig .get_scheme_names ())
174
+
175
+ for scheme in scheme_names :
176
+ # https://foss.heptapod.net/pypy/pypy/-/issues/3433
177
+ better_scheme = "pypy_posix" if scheme == "pypy" else scheme
178
+ if os .name in better_scheme .split ("_" ):
179
+ config_paths = sysconfig .get_paths (scheme )
180
+ for path_name in ["platlib" , "purelib" ]:
181
+ paths .add (config_paths [path_name ])
182
+
183
+
132
184
def add_coverage_paths (paths ):
133
185
"""Add paths where coverage.py code can be found to the set `paths`."""
134
186
cover_path = canonical_path (__file__ , directory = True )
@@ -156,8 +208,8 @@ def __init__(self, warn, debug):
156
208
# The matchers for should_trace.
157
209
self .source_match = None
158
210
self .source_pkgs_match = None
159
- self .pylib_paths = self .cover_paths = None
160
- self .pylib_match = self .cover_match = None
211
+ self .pylib_paths = self .cover_paths = self . third_paths = None
212
+ self .pylib_match = self .cover_match = self . third_match = None
161
213
self .include_match = self .omit_match = None
162
214
self .plugins = []
163
215
self .disp_class = FileDisposition
@@ -168,6 +220,9 @@ def __init__(self, warn, debug):
168
220
self .source_pkgs_unmatched = []
169
221
self .omit = self .include = None
170
222
223
+ # Is the source inside a third-party area?
224
+ self .source_in_third = False
225
+
171
226
def configure (self , config ):
172
227
"""Apply the configuration to get ready for decision-time."""
173
228
self .source_pkgs .extend (config .source_pkgs )
@@ -191,6 +246,10 @@ def configure(self, config):
191
246
self .cover_paths = set ()
192
247
add_coverage_paths (self .cover_paths )
193
248
249
+ # Find where third-party packages are installed.
250
+ self .third_paths = set ()
251
+ add_third_party_paths (self .third_paths )
252
+
194
253
def debug (msg ):
195
254
if self .debug :
196
255
self .debug .write (msg )
@@ -218,6 +277,24 @@ def debug(msg):
218
277
if self .omit :
219
278
self .omit_match = FnmatchMatcher (self .omit )
220
279
debug ("Omit matching: {!r}" .format (self .omit_match ))
280
+ if self .third_paths :
281
+ self .third_match = TreeMatcher (self .third_paths )
282
+ debug ("Third-party lib matching: {!r}" .format (self .third_match ))
283
+
284
+ # Check if the source we want to measure has been installed as a
285
+ # third-party package.
286
+ for pkg in self .source_pkgs :
287
+ try :
288
+ modfile = file_for_module (pkg )
289
+ debug ("Imported {} as {}" .format (pkg , modfile ))
290
+ except CoverageException as exc :
291
+ debug ("Couldn't import {}: {}" .format (pkg , exc ))
292
+ continue
293
+ if modfile and self .third_match .match (modfile ):
294
+ self .source_in_third = True
295
+ for src in self .source :
296
+ if self .third_match .match (src ):
297
+ self .source_in_third = True
221
298
222
299
def should_trace (self , filename , frame = None ):
223
300
"""Decide whether to trace execution in `filename`, with a reason.
@@ -352,6 +429,9 @@ def check_include_omit_etc(self, filename, frame):
352
429
ok = True
353
430
if not ok :
354
431
return extra + "falls outside the --source spec"
432
+ if not self .source_in_third :
433
+ if self .third_match .match (filename ):
434
+ return "inside --source, but in third-party"
355
435
elif self .include_match :
356
436
if not self .include_match .match (filename ):
357
437
return "falls outside the --include trees"
@@ -361,6 +441,10 @@ def check_include_omit_etc(self, filename, frame):
361
441
if self .pylib_match and self .pylib_match .match (filename ):
362
442
return "is in the stdlib"
363
443
444
+ # Exclude anything in the third-party installation areas.
445
+ if self .third_match and self .third_match .match (filename ):
446
+ return "is a third-party module"
447
+
364
448
# We exclude the coverage.py code itself, since a little of it
365
449
# will be measured otherwise.
366
450
if self .cover_match and self .cover_match .match (filename ):
@@ -485,14 +569,15 @@ def sys_info(self):
485
569
Returns a list of (key, value) pairs.
486
570
"""
487
571
info = [
488
- ('cover_paths' , self .cover_paths ),
489
- ('pylib_paths' , self .pylib_paths ),
572
+ ("coverage_paths" , self .cover_paths ),
573
+ ("stdlib_paths" , self .pylib_paths ),
574
+ ("third_party_paths" , self .third_paths ),
490
575
]
491
576
492
577
matcher_names = [
493
578
'source_match' , 'source_pkgs_match' ,
494
579
'include_match' , 'omit_match' ,
495
- 'cover_match' , 'pylib_match' ,
580
+ 'cover_match' , 'pylib_match' , 'third_match' ,
496
581
]
497
582
498
583
for matcher_name in matcher_names :
0 commit comments