Skip to content

Commit 9b54389

Browse files
committed
fix: make third-party detection work with namespace packages. #1231
1 parent 27db7b4 commit 9b54389

File tree

3 files changed

+113
-12
lines changed

3 files changed

+113
-12
lines changed

CHANGES.rst

+7
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ This list is detailed and covers changes in each pre-release version.
2222
Unreleased
2323
----------
2424

25+
- Namespace packages being measured weren't properly handled by the new code
26+
that ignores third-party packages. If the namespace package was installed, it
27+
was ignored as a third-party package. That problem (`issue 1231`_) is now
28+
fixed.
29+
2530
- The :meth:`.CoverageData.contexts_by_lineno` method was documented to return
2631
a dict, but was returning a defaultdict. Now it returns a plain dict. It
2732
also no longer returns negative numbered keys.
2833

34+
.. _issue 1231: https://github.com/nedbat/coveragepy/issues/1231
35+
2936

3037
.. _changes_601:
3138

coverage/inorout.py

+32-9
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,26 @@ def module_has_file(mod):
107107
return os.path.exists(mod__file__)
108108

109109

110-
def file_for_module(modulename):
111-
"""Find the file for `modulename`, or return None."""
110+
def file_and_path_for_module(modulename):
111+
"""Find the file and search path for `modulename`.
112+
113+
Returns:
114+
filename: The filename of the module, or None.
115+
path: A list (possibly empty) of directories to find submodules in.
116+
117+
"""
112118
filename = None
119+
path = []
113120
try:
114121
spec = importlib.util.find_spec(modulename)
115122
except ImportError:
116123
pass
117124
else:
118125
if spec is not None:
119-
filename = spec.origin
120-
return filename
126+
if spec.origin != "namespace":
127+
filename = spec.origin
128+
path = list(spec.submodule_search_locations or ())
129+
return filename, path
121130

122131

123132
def add_stdlib_paths(paths):
@@ -263,15 +272,29 @@ def debug(msg):
263272
# third-party package.
264273
for pkg in self.source_pkgs:
265274
try:
266-
modfile = file_for_module(pkg)
267-
debug(f"Imported {pkg} as {modfile}")
275+
modfile, path = file_and_path_for_module(pkg)
276+
debug(f"Imported source package {pkg!r} as {modfile!r}")
268277
except CoverageException as exc:
269-
debug(f"Couldn't import {pkg}: {exc}")
278+
debug(f"Couldn't import source package {pkg!r}: {exc}")
270279
continue
271-
if modfile and self.third_match.match(modfile):
272-
self.source_in_third = True
280+
if modfile:
281+
if self.third_match.match(modfile):
282+
debug(
283+
f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}"
284+
)
285+
self.source_in_third = True
286+
else:
287+
for pathdir in path:
288+
if self.third_match.match(pathdir):
289+
debug(
290+
f"Source is in third-party because of {pkg!r} path directory " +
291+
f"at {pathdir!r}"
292+
)
293+
self.source_in_third = True
294+
273295
for src in self.source:
274296
if self.third_match.match(src):
297+
debug(f"Source is in third-party because of source directory {src!r}")
275298
self.source_in_third = True
276299

277300
def should_trace(self, filename, frame=None):

tests/test_process.py

+74-3
Original file line numberDiff line numberDiff line change
@@ -1691,13 +1691,37 @@ def render(filename, linenum):
16911691
def fourth(x):
16921692
return 4 * x
16931693
""")
1694+
# Some namespace packages.
1695+
make_file("third_pkg/nspkg/fifth/__init__.py", """\
1696+
def fifth(x):
1697+
return 5 * x
1698+
""")
1699+
# The setup.py to install everything.
16941700
make_file("third_pkg/setup.py", """\
16951701
import setuptools
1696-
setuptools.setup(name="third", packages=["third", "fourth"])
1702+
setuptools.setup(
1703+
name="third",
1704+
packages=["third", "fourth", "nspkg.fifth"],
1705+
)
1706+
""")
1707+
1708+
# Some namespace packages.
1709+
make_file("another_pkg/nspkg/sixth/__init__.py", """\
1710+
def sixth(x):
1711+
return 6 * x
1712+
""")
1713+
# The setup.py to install everything.
1714+
make_file("another_pkg/setup.py", """\
1715+
import setuptools
1716+
setuptools.setup(
1717+
name="another",
1718+
packages=["nspkg.sixth"],
1719+
)
16971720
""")
16981721

16991722
# Install the third-party packages.
17001723
run_in_venv("python -m pip install --no-index ./third_pkg")
1724+
run_in_venv("python -m pip install --no-index -e ./another_pkg")
17011725
shutil.rmtree("third_pkg")
17021726

17031727
# Install coverage.
@@ -1719,17 +1743,22 @@ def coverage_command_fixture(request):
17191743
class VirtualenvTest(CoverageTest):
17201744
"""Tests of virtualenv considerations."""
17211745

1746+
expected_stdout = "33\n110\n198\n1.5\n"
1747+
17221748
@pytest.fixture(autouse=True)
17231749
def in_venv_world_fixture(self, venv_world):
17241750
"""For running tests inside venv_world, and cleaning up made files."""
17251751
with change_dir(venv_world):
17261752
self.make_file("myproduct.py", """\
17271753
import colorsys
17281754
import third
1755+
import nspkg.fifth
1756+
import nspkg.sixth
17291757
print(third.third(11))
1758+
print(nspkg.fifth.fifth(22))
1759+
print(nspkg.sixth.sixth(33))
17301760
print(sum(colorsys.rgb_to_hls(1, 0, 0)))
17311761
""")
1732-
self.expected_stdout = "33\n1.5\n" # pylint: disable=attribute-defined-outside-init
17331762

17341763
self.del_environ("COVERAGE_TESTING") # To avoid needing contracts installed.
17351764
self.set_environ("COVERAGE_DEBUG_FILE", "debug_out.txt")
@@ -1738,7 +1767,7 @@ def in_venv_world_fixture(self, venv_world):
17381767
yield
17391768

17401769
for fname in os.listdir("."):
1741-
if fname != "venv":
1770+
if fname not in {"venv", "another_pkg"}:
17421771
os.remove(fname)
17431772

17441773
def get_trace_output(self):
@@ -1829,3 +1858,45 @@ def test_venv_with_dynamic_plugin(self, coverage_command):
18291858
# The output should not have this warning:
18301859
# Already imported a file that will be measured: ...third/render.py (already-imported)
18311860
assert out == "HTML: hello.html@1723\n"
1861+
1862+
def test_installed_namespace_packages(self, coverage_command):
1863+
# https://github.com/nedbat/coveragepy/issues/1231
1864+
# When namespace packages were installed, they were considered
1865+
# third-party packages. Test that isn't still happening.
1866+
out = run_in_venv(coverage_command + " run --source=nspkg myproduct.py")
1867+
# In particular, this warning doesn't appear:
1868+
# Already imported a file that will be measured: .../coverage/__main__.py
1869+
assert out == self.expected_stdout
1870+
1871+
# Check that our tracing was accurate. Files are mentioned because
1872+
# --source refers to a file.
1873+
debug_out = self.get_trace_output()
1874+
assert re_lines(
1875+
debug_out,
1876+
r"^Not tracing .*\bexecfile.py': " +
1877+
"module 'coverage.execfile' falls outside the --source spec"
1878+
)
1879+
assert re_lines(
1880+
debug_out,
1881+
r"^Not tracing .*\bmyproduct.py': module 'myproduct' falls outside the --source spec"
1882+
)
1883+
assert re_lines(
1884+
debug_out,
1885+
r"^Not tracing .*\bcolorsys.py': module 'colorsys' falls outside the --source spec"
1886+
)
1887+
1888+
out = run_in_venv("python -m coverage report")
1889+
1890+
# Name Stmts Miss Cover
1891+
# ------------------------------------------------------------------------------
1892+
# another_pkg/nspkg/sixth/__init__.py 2 0 100%
1893+
# venv/lib/python3.9/site-packages/nspkg/fifth/__init__.py 2 0 100%
1894+
# ------------------------------------------------------------------------------
1895+
# TOTAL 4 0 100%
1896+
1897+
assert "myproduct.py" not in out
1898+
assert "third" not in out
1899+
assert "coverage" not in out
1900+
assert "colorsys" not in out
1901+
assert "fifth" in out
1902+
assert "sixth" in out

0 commit comments

Comments
 (0)