From 26445508a2eb1c7ef459a33ec058eb3f3c5b41dd Mon Sep 17 00:00:00 2001 From: Felix Horvat Date: Thu, 17 Nov 2022 12:34:22 +0100 Subject: [PATCH] feat: added support for finding unexecuted namespace packages (#1387) * add support for namespace packages * fixed typo * update documentation * fixed lint issues * changed versionadded * convert to config setting * removed pure formatting changes * code review changes Co-authored-by: Ned Batchelder --- coverage/config.py | 2 ++ coverage/control.py | 1 + coverage/files.py | 5 +++-- coverage/inorout.py | 8 ++++++-- doc/config.rst | 8 ++++++++ tests/test_config.py | 3 +++ tests/test_files.py | 19 ++++++++++++++++++- 7 files changed, 41 insertions(+), 5 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index 7b765edfe..994154f6d 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -200,6 +200,7 @@ def __init__(self): self.fail_under = 0.0 self.format = None self.ignore_errors = False + self.include_namespace_packages = False self.report_include = None self.report_omit = None self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] @@ -375,6 +376,7 @@ def copy(self): ('fail_under', 'report:fail_under', 'float'), ('format', 'report:format', 'boolean'), ('ignore_errors', 'report:ignore_errors', 'boolean'), + ('include_namespace_packages', 'report:include_namespace_packages', 'boolean'), ('partial_always_list', 'report:partial_branches_always', 'regexlist'), ('partial_list', 'report:partial_branches', 'regexlist'), ('precision', 'report:precision', 'int'), diff --git a/coverage/control.py b/coverage/control.py index 91ad5a78a..a955c283e 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -530,6 +530,7 @@ def _init_for_start(self): self._inorout = InOrOut( warn=self._warn, debug=(self._debug if self._debug.should('trace') else None), + include_namespace_packages=self.config.include_namespace_packages ) self._inorout.configure(self.config) self._inorout.plugins = self._plugins diff --git a/coverage/files.py b/coverage/files.py index bfd808ffd..8be292f3d 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -461,7 +461,7 @@ def map(self, path): return path -def find_python_files(dirname): +def find_python_files(dirname, include_namespace_packages): """Yield all of the importable Python files in `dirname`, recursively. To be importable, the files have to be in a directory with a __init__.py, @@ -472,7 +472,8 @@ def find_python_files(dirname): """ for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): - if i > 0 and '__init__.py' not in filenames: + if (i > 0 and '__init__.py' not in filenames + and not include_namespace_packages): # If a directory doesn't have __init__.py, then it isn't # importable and neither are its files del dirnames[:] diff --git a/coverage/inorout.py b/coverage/inorout.py index 2e534c853..0d3f6d670 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -189,9 +189,10 @@ def add_coverage_paths(paths): class InOrOut: """Machinery for determining what files to measure.""" - def __init__(self, warn, debug): + def __init__(self, warn, debug, include_namespace_packages): self.warn = warn self.debug = debug + self.include_namespace_packages = include_namespace_packages # The matchers for should_trace. self.source_match = None @@ -565,7 +566,10 @@ def _find_executable_files(self, src_dir): Yield the file path, and the plugin name that handles the file. """ - py_files = ((py_file, None) for py_file in find_python_files(src_dir)) + py_files = ( + (py_file, None) for py_file in + find_python_files(src_dir, self.include_namespace_packages) + ) plugin_files = self._find_plugin_files(src_dir) for file_path, plugin_name in itertools.chain(py_files, plugin_files): diff --git a/doc/config.rst b/doc/config.rst index 83f64a4dc..b51129826 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -410,6 +410,14 @@ warning instead of an exception. See :ref:`source` for details. +.. _config_include_namespace_packages: + +[report] include_namespace_packages +................................... + +(boolean, default False) Include folders without an ``__init__.py`` in the +coverage. + .. _config_report_omit: [report] omit diff --git a/tests/test_config.py b/tests/test_config.py index 444719fdc..5f8a05476 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -494,6 +494,8 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): skip_covered = TruE skip_empty =TruE + include_namespace_packages = TRUE + [{section}html] directory = c:\\tricky\\dir.somewhere @@ -589,6 +591,7 @@ def assert_config_settings_are_correct(self, cov): assert cov.config.get_plugin_options("plugins.another") == {} assert cov.config.json_show_contexts is True assert cov.config.json_pretty_print is True + assert cov.config.include_namespace_packages is True def test_config_file_settings(self): self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section="")) diff --git a/tests/test_files.py b/tests/test_files.py index 561b961d3..a69d1a4b0 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -585,13 +585,30 @@ def test_find_python_files(self): self.make_file("sub/ssub/~s.py") # nope: editor effluvia self.make_file("sub/lab/exp.py") # nope: no __init__.py self.make_file("sub/windows.pyw") - py_files = set(find_python_files("sub")) + py_files = set(find_python_files("sub", include_namespace_packages=False)) self.assert_same_files(py_files, [ "sub/a.py", "sub/b.py", "sub/ssub/__init__.py", "sub/ssub/s.py", "sub/windows.pyw", ]) + def test_find_python_files_include_namespace_packages(self): + self.make_file("sub/a.py") + self.make_file("sub/b.py") + self.make_file("sub/x.c") # nope: not .py + self.make_file("sub/ssub/__init__.py") + self.make_file("sub/ssub/s.py") + self.make_file("sub/ssub/~s.py") # nope: editor effluvia + self.make_file("sub/lab/exp.py") + self.make_file("sub/windows.pyw") + py_files = set(find_python_files("sub", include_namespace_packages=True)) + self.assert_same_files(py_files, [ + "sub/a.py", "sub/b.py", + "sub/ssub/__init__.py", "sub/ssub/s.py", + "sub/lab/exp.py", + "sub/windows.pyw", + ]) + @pytest.mark.skipif(not env.WINDOWS, reason="Only need to run Windows tests on Windows.") class WindowsFileTest(CoverageTest):