From 331fdd8f725bee0bc28412ecef2a6b35d149705d Mon Sep 17 00:00:00 2001 From: Olena Date: Wed, 16 Apr 2025 02:04:00 +0300 Subject: [PATCH 1/5] Support `.coveragerc.toml` for configuration This patch provides a new configuration option for coverage.py. Considering that many projects have switched to toml configurations, this change offers a more flexible approach to manage coverage settings. --- coverage/config.py | 1 + tests/test_config.py | 127 +++++++++++++++++++++++++++++-------------- 2 files changed, 87 insertions(+), 41 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index 967f0817b..d63c30cc9 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -587,6 +587,7 @@ def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]] assert isinstance(config_file, str) files_to_try = [ (config_file, True, specified_file), + (".coveragerc.toml", True, False), ("setup.cfg", False, False), ("tox.ini", False, False), ("pyproject.toml", False, False), diff --git a/tests/test_config.py b/tests/test_config.py index c4d011373..3889ad2f7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -33,7 +33,8 @@ def test_default_config(self) -> None: def test_arguments(self) -> None: # Arguments to the constructor are applied to the configuration. - cov = coverage.Coverage(timid=True, data_file="fooey.dat", concurrency="multiprocessing") + cov = coverage.Coverage( + timid=True, data_file="fooey.dat", concurrency="multiprocessing") assert cov.config.timid assert not cov.config.branch assert cov.config.data_file == "fooey.dat" @@ -66,9 +67,10 @@ def test_named_config_file(self, file_class: FilePathType) -> None: assert not cov.config.branch assert cov.config.data_file == "delete.me" - def test_toml_config_file(self) -> None: - # A pyproject.toml file will be read into the configuration. - self.make_file("pyproject.toml", """\ + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_toml_config_file(self, filename) -> None: + # A pyproject.toml and coveragerc.toml will be read into the configuration. + self.make_file(filename, """\ # This is just a bogus toml file for testing. [tool.somethingelse] authors = ["Joe D'Ávila "] @@ -94,11 +96,13 @@ def test_toml_config_file(self) -> None: assert cov.config.precision == 3 assert cov.config.html_title == "tabblo & «ταБЬℓσ»" assert cov.config.fail_under == 90.5 - assert cov.config.get_plugin_options("plugins.a_plugin") == {"hello": "world"} + assert cov.config.get_plugin_options("plugins.a_plugin") == { + "hello": "world"} - def test_toml_ints_can_be_floats(self) -> None: + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_toml_ints_can_be_floats(self, filename) -> None: # Test that our class doesn't reject integers when loading floats - self.make_file("pyproject.toml", """\ + self.make_file(filename, """\ # This is just a bogus toml file for testing. [tool.coverage.report] fail_under = 90 @@ -214,6 +218,7 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None: with pytest.raises(ConfigError, match=msg): coverage.Coverage() + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) @pytest.mark.parametrize("bad_config, msg", [ ("[tool.coverage.run]\ntimid = \"maybe?\"\n", r"maybe[?]"), ("[tool.coverage.run\n", None), @@ -237,9 +242,9 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None: ("[tool.coverage.report]\nprecision=1.23", "not an integer"), ('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"), ]) - def test_toml_parse_errors(self, bad_config: str, msg: str) -> None: + def test_toml_parse_errors(self, filename, bad_config: str, msg: str) -> None: # Im-parsable values raise ConfigError, with details. - self.make_file("pyproject.toml", bad_config) + self.make_file(filename, bad_config) with pytest.raises(ConfigError, match=msg): coverage.Coverage() @@ -263,11 +268,13 @@ def test_environment_vars_in_config(self) -> None: cov = coverage.Coverage() assert cov.config.data_file == "hello-world.fooey" assert cov.config.branch is True - assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] + assert cov.config.exclude_list == [ + "the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] - def test_environment_vars_in_toml_config(self) -> None: + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_environment_vars_in_toml_config(self, filename) -> None: # Config files can have $envvars in them. - self.make_file("pyproject.toml", """\ + self.make_file(filename, """\ [tool.coverage.run] data_file = "$DATA_FILE.fooey" branch = "$BRANCH" @@ -296,7 +303,8 @@ def test_environment_vars_in_toml_config(self) -> None: assert cov.config.branch is True assert cov.config.precision == 3 assert cov.config.data_file == "hello-world.fooey" - assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] + assert cov.config.exclude_list == [ + "the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] def test_tilde_in_config(self) -> None: # Config entries that are file paths can be tilde-expanded. @@ -330,9 +338,10 @@ def test_tilde_in_config(self) -> None: self.assert_tilde_results() - def test_tilde_in_toml_config(self) -> None: + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_tilde_in_toml_config(self, filename) -> None: # Config entries that are file paths can be tilde-expanded. - self.make_file("pyproject.toml", """\ + self.make_file(filename, """\ [tool.coverage.run] data_file = "~/data.file" @@ -384,7 +393,8 @@ def expanduser(s: str) -> str: assert cov.config.lcov_output == "/Users/me/lcov/~foo.lcov" assert cov.config.xml_output == "/Users/me/somewhere/xml.out" assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"] - assert cov.config.paths == {'mapping': ['/Users/me/src', '/Users/joe/source']} + assert cov.config.paths == {'mapping': [ + '/Users/me/src', '/Users/joe/source']} def test_tweaks_after_constructor(self) -> None: # set_option can be used after construction to affect the config. @@ -440,7 +450,8 @@ def test_tweak_error_checking(self) -> None: def test_tweak_plugin_options(self) -> None: # Plugin options have a more flexible syntax. cov = coverage.Coverage() - cov.set_option("run:plugins", ["fooey.plugin", "xyzzy.coverage.plugin"]) + cov.set_option("run:plugins", [ + "fooey.plugin", "xyzzy.coverage.plugin"]) cov.set_option("fooey.plugin:xyzzy", 17) cov.set_option("xyzzy.coverage.plugin:plugh", ["a", "b"]) with pytest.raises(ConfigError, match="No such option: 'no_such.plugin:foo'"): @@ -460,12 +471,13 @@ def test_unknown_option(self) -> None: with pytest.warns(CoverageWarning, match=msg): _ = coverage.Coverage() - def test_unknown_option_toml(self) -> None: - self.make_file("pyproject.toml", """\ + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_unknown_option_toml(self, filename) -> None: + self.make_file(filename, """\ [tool.coverage.run] xyzzy = 17 """) - msg = r"Unrecognized option '\[tool.coverage.run\] xyzzy=' in config file pyproject.toml" + msg = f"Unrecognized option '\\[tool.coverage.run\\] xyzzy=' in config file {filename}" with pytest.warns(CoverageWarning, match=msg): _ = coverage.Coverage() @@ -512,14 +524,16 @@ def test_exceptions_from_missing_things(self) -> None: with pytest.raises(ConfigError, match="No option 'foo' in section: 'xyzzy'"): config.get("xyzzy", "foo") - def test_exclude_also(self) -> None: - self.make_file("pyproject.toml", """\ + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_exclude_also(self, filename) -> None: + self.make_file(filename, """\ [tool.coverage.report] exclude_also = ["foobar", "raise .*Error"] """) cov = coverage.Coverage() - expected = coverage.config.DEFAULT_EXCLUDE + ["foobar", "raise .*Error"] + expected = coverage.config.DEFAULT_EXCLUDE + \ + ["foobar", "raise .*Error"] assert cov.config.exclude_list == expected def test_partial_also(self) -> None: @@ -529,7 +543,8 @@ def test_partial_also(self) -> None: """) cov = coverage.Coverage() - expected = coverage.config.DEFAULT_PARTIAL + ["foobar", "raise .*Error"] + expected = coverage.config.DEFAULT_PARTIAL + \ + ["foobar", "raise .*Error"] assert cov.config.partial_list == expected def test_core_option(self) -> None: @@ -672,10 +687,12 @@ def assert_config_settings_are_correct(self, cov: Coverage) -> None: assert cov.config.source_dirs == ["cooldir"] assert cov.config.disable_warnings == ["abcd", "efgh"] - assert cov.get_exclude_list() == ["if 0:", r"pragma:?\s+no cover", "another_tab"] + assert cov.get_exclude_list() == [ + "if 0:", r"pragma:?\s+no cover", "another_tab"] assert cov.config.ignore_errors assert cov.config.run_omit == ["twenty"] - assert cov.config.report_omit == ["one", "another", "some_more", "yet_more"] + assert cov.config.report_omit == [ + "one", "another", "some_more", "yet_more"] assert cov.config.report_include == ["thirty"] assert cov.config.precision == 3 @@ -719,7 +736,8 @@ def check_config_file_settings_in_other_file(self, fname: str, contents: str) -> self.assert_config_settings_are_correct(cov) def test_config_file_settings_in_setupcfg(self) -> None: - self.check_config_file_settings_in_other_file("setup.cfg", self.SETUP_CFG) + self.check_config_file_settings_in_other_file( + "setup.cfg", self.SETUP_CFG) def test_config_file_settings_in_toxini(self) -> None: self.check_config_file_settings_in_other_file("tox.ini", self.TOX_INI) @@ -732,10 +750,12 @@ def check_other_config_if_coveragerc_specified(self, fname: str, contents: str) self.assert_config_settings_are_correct(cov) def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self) -> None: - self.check_other_config_if_coveragerc_specified("setup.cfg", self.SETUP_CFG) + self.check_other_config_if_coveragerc_specified( + "setup.cfg", self.SETUP_CFG) def test_config_file_settings_in_tox_if_coveragerc_specified(self) -> None: - self.check_other_config_if_coveragerc_specified("tox.ini", self.TOX_INI) + self.check_other_config_if_coveragerc_specified( + "tox.ini", self.TOX_INI) def check_other_not_read_if_coveragerc(self, fname: str) -> None: """Check config `fname` is not read if .coveragerc exists.""" @@ -839,35 +859,38 @@ def test_no_toml_installed_explicit_toml(self) -> None: coverage.Coverage(config_file="cov.toml") @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") - def test_no_toml_installed_pyproject_toml(self) -> None: - # Can't have coverage config in pyproject.toml without toml installed. - self.make_file("pyproject.toml", """\ + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_no_toml_installed_pyproject_toml(self, filename) -> None: + # Can't have coverage config in pyproject.toml and .coveragerc.toml without toml installed. + self.make_file(filename, """\ # A toml file! [tool.coverage.run] xyzzy = 17 """) with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): - msg = "Can't read 'pyproject.toml' without TOML support" + msg = "Can't read '{filename}' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage() @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") - def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None: + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_no_toml_installed_pyproject_toml_shorter_syntax(self, filename) -> None: # Can't have coverage config in pyproject.toml without toml installed. - self.make_file("pyproject.toml", """\ + self.make_file(filename, """\ # A toml file! [tool.coverage] run.parallel = true """) with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): - msg = "Can't read 'pyproject.toml' without TOML support" + msg = "Can't read '{filename}' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage() @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") - def test_no_toml_installed_pyproject_no_coverage(self) -> None: + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_no_toml_installed_pyproject_no_coverage(self, filename) -> None: # It's ok to have non-coverage pyproject.toml without toml installed. - self.make_file("pyproject.toml", """\ + self.make_file(filename, """\ # A toml file! [tool.something] xyzzy = 17 @@ -879,16 +902,38 @@ def test_no_toml_installed_pyproject_no_coverage(self) -> None: assert not cov.config.branch assert cov.config.data_file == ".coverage" - def test_exceptions_from_missing_toml_things(self) -> None: - self.make_file("pyproject.toml", """\ + @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) + def test_exceptions_from_missing_toml_things(self, filename) -> None: + self.make_file(filename, """\ [tool.coverage.run] branch = true """) config = TomlConfigParser(False) - config.read("pyproject.toml") + config.read(filename) with pytest.raises(ConfigError, match="No section: 'xyzzy'"): config.options("xyzzy") with pytest.raises(ConfigError, match="No section: 'xyzzy'"): config.get("xyzzy", "foo") with pytest.raises(ConfigError, match="No option 'foo' in section: 'tool.coverage.run'"): config.get("run", "foo") + + def test_coveragerc_toml_priority(self) -> None: + """Test that .coveragerc.toml has priority over pyproject.toml.""" + self.make_file(".coveragerc.toml", """\ + [tool.coverage.run] + timid = true + data_file = ".toml-data.dat" + branch = true + """) + + self.make_file("pyproject.toml", """\ + [tool.coverage.run] + timid = false + data_file = "pyproject-data.dat" + branch = false + """) + cov = coverage.Coverage() + + assert cov.config.timid is True + assert cov.config.data_file == ".toml-data.dat" + assert cov.config.branch is True From b41d75bd77349c51d73ae2f161f1d82b8387b11a Mon Sep 17 00:00:00 2001 From: Olena <107187316+OlenaYefymenko@users.noreply.github.com> Date: Fri, 18 Apr 2025 23:59:10 +0300 Subject: [PATCH 2/5] Add f-string for error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- tests/test_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 3889ad2f7..d047ab8bf 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -868,7 +868,7 @@ def test_no_toml_installed_pyproject_toml(self, filename) -> None: xyzzy = 17 """) with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): - msg = "Can't read '{filename}' without TOML support" + msg = f"Can't read '{filename}' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage() @@ -882,7 +882,7 @@ def test_no_toml_installed_pyproject_toml_shorter_syntax(self, filename) -> None run.parallel = true """) with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): - msg = "Can't read '{filename}' without TOML support" + msg = f"Can't read '{filename}' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage() From c3965d547207c546e7b0ba5e395f1808b13e1fae Mon Sep 17 00:00:00 2001 From: Olena Date: Fri, 6 Jun 2025 01:56:25 +0300 Subject: [PATCH 3/5] This patch parametrizes tests for configuration files. Adds a new test to verify behavior when TOML support is unavailable. --- tests/test_config.py | 45 ++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index d047ab8bf..a5cb57a34 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -859,38 +859,35 @@ def test_no_toml_installed_explicit_toml(self) -> None: coverage.Coverage(config_file="cov.toml") @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") - @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_no_toml_installed_pyproject_toml(self, filename) -> None: - # Can't have coverage config in pyproject.toml and .coveragerc.toml without toml installed. - self.make_file(filename, """\ + def test_no_toml_installed_pyproject_toml(self) -> None: + # Can't have coverage config in pyproject.toml without toml installed. + self.make_file("pyproject.toml", """\ # A toml file! [tool.coverage.run] xyzzy = 17 """) with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): - msg = f"Can't read '{filename}' without TOML support" + msg = "Can't read 'pyproject.toml' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage() @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") - @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_no_toml_installed_pyproject_toml_shorter_syntax(self, filename) -> None: + def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None: # Can't have coverage config in pyproject.toml without toml installed. - self.make_file(filename, """\ + self.make_file("pyproject.toml", """\ # A toml file! [tool.coverage] run.parallel = true """) with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): - msg = f"Can't read '{filename}' without TOML support" + msg = "Can't read 'pyproject.toml' without TOML support" with pytest.raises(ConfigError, match=msg): coverage.Coverage() @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") - @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_no_toml_installed_pyproject_no_coverage(self, filename) -> None: + def test_no_toml_installed_pyproject_no_coverage(self) -> None: # It's ok to have non-coverage pyproject.toml without toml installed. - self.make_file(filename, """\ + self.make_file("pyproject.toml", """\ # A toml file! [tool.something] xyzzy = 17 @@ -937,3 +934,27 @@ def test_coveragerc_toml_priority(self) -> None: assert cov.config.timid is True assert cov.config.data_file == ".toml-data.dat" assert cov.config.branch is True + + + @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") + def test_toml_file_exists_but_no_toml_support(self) -> None: + """Test behavior when .coveragerc.toml exists but TOML support is missing.""" + self.make_file(".coveragerc.toml", """\ + [tool.coverage.run] + timid = true + data_file = ".toml-data.dat" + """) + + with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): + msg = "Can't read '.coveragerc.toml' without TOML support" + with pytest.raises(ConfigError, match=msg): + coverage.Coverage(config_file=".coveragerc.toml") + self.make_file(".coveragerc", """\ + [run] + timid = false + data_file = .ini-data.dat + """) + + cov = coverage.Coverage() + assert not cov.config.timid + assert cov.config.data_file == ".ini-data.dat" From 6b2b08c7c49466c6b430a864cefe574efbe73dfc Mon Sep 17 00:00:00 2001 From: Olena Date: Fri, 6 Jun 2025 02:44:43 +0300 Subject: [PATCH 4/5] Fix linter issues --- tests/test_config.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index a5cb57a34..f32371c6d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -68,7 +68,7 @@ def test_named_config_file(self, file_class: FilePathType) -> None: assert cov.config.data_file == "delete.me" @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_toml_config_file(self, filename) -> None: + def test_toml_config_file(self, filename: str) -> None: # A pyproject.toml and coveragerc.toml will be read into the configuration. self.make_file(filename, """\ # This is just a bogus toml file for testing. @@ -100,7 +100,7 @@ def test_toml_config_file(self, filename) -> None: "hello": "world"} @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_toml_ints_can_be_floats(self, filename) -> None: + def test_toml_ints_can_be_floats(self, filename: str) -> None: # Test that our class doesn't reject integers when loading floats self.make_file(filename, """\ # This is just a bogus toml file for testing. @@ -242,7 +242,7 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None: ("[tool.coverage.report]\nprecision=1.23", "not an integer"), ('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"), ]) - def test_toml_parse_errors(self, filename, bad_config: str, msg: str) -> None: + def test_toml_parse_errors(self, filename: str, bad_config: str, msg: str) -> None: # Im-parsable values raise ConfigError, with details. self.make_file(filename, bad_config) with pytest.raises(ConfigError, match=msg): @@ -272,7 +272,7 @@ def test_environment_vars_in_config(self) -> None: "the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_environment_vars_in_toml_config(self, filename) -> None: + def test_environment_vars_in_toml_config(self, filename: str) -> None: # Config files can have $envvars in them. self.make_file(filename, """\ [tool.coverage.run] @@ -339,7 +339,7 @@ def test_tilde_in_config(self) -> None: self.assert_tilde_results() @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_tilde_in_toml_config(self, filename) -> None: + def test_tilde_in_toml_config(self, filename: str) -> None: # Config entries that are file paths can be tilde-expanded. self.make_file(filename, """\ [tool.coverage.run] @@ -472,7 +472,7 @@ def test_unknown_option(self) -> None: _ = coverage.Coverage() @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_unknown_option_toml(self, filename) -> None: + def test_unknown_option_toml(self, filename: str) -> None: self.make_file(filename, """\ [tool.coverage.run] xyzzy = 17 @@ -525,7 +525,7 @@ def test_exceptions_from_missing_things(self) -> None: config.get("xyzzy", "foo") @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_exclude_also(self, filename) -> None: + def test_exclude_also(self, filename: str) -> None: self.make_file(filename, """\ [tool.coverage.report] exclude_also = ["foobar", "raise .*Error"] @@ -900,7 +900,7 @@ def test_no_toml_installed_pyproject_no_coverage(self) -> None: assert cov.config.data_file == ".coverage" @pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"]) - def test_exceptions_from_missing_toml_things(self, filename) -> None: + def test_exceptions_from_missing_toml_things(self, filename: str) -> None: self.make_file(filename, """\ [tool.coverage.run] branch = true @@ -934,8 +934,7 @@ def test_coveragerc_toml_priority(self) -> None: assert cov.config.timid is True assert cov.config.data_file == ".toml-data.dat" assert cov.config.branch is True - - + @pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib") def test_toml_file_exists_but_no_toml_support(self) -> None: """Test behavior when .coveragerc.toml exists but TOML support is missing.""" @@ -944,7 +943,7 @@ def test_toml_file_exists_but_no_toml_support(self) -> None: timid = true data_file = ".toml-data.dat" """) - + with mock.patch.object(coverage.tomlconfig, "has_tomllib", False): msg = "Can't read '.coveragerc.toml' without TOML support" with pytest.raises(ConfigError, match=msg): @@ -954,7 +953,7 @@ def test_toml_file_exists_but_no_toml_support(self) -> None: timid = false data_file = .ini-data.dat """) - + cov = coverage.Coverage() assert not cov.config.timid assert cov.config.data_file == ".ini-data.dat" From c584e0da70d6621cda3cced622021d79a9abdcc2 Mon Sep 17 00:00:00 2001 From: Olena Date: Mon, 16 Jun 2025 02:10:54 +0300 Subject: [PATCH 5/5] Update documentation, changelog, and contributors list for new `.coveragerc.toml` configuration file support. --- CHANGES.rst | 7 +++++ CONTRIBUTORS.txt | 1 + doc/config.rst | 66 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e2b4430a1..ed3c58740 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -64,6 +64,13 @@ Unreleased .. _pull 1998: https://github.com/nedbat/coveragepy/pull/1998 .. _issue 2001: https://github.com/nedbat/coveragepy/issues/2001 +- Feature: Added support for ``.coveragerc.toml`` configuration files. This + provides a more flexible approach to manage coverage settings as many + projects have switched to TOML configurations. Closes `issue 1643`_. + +.. _issue 1643: https://github.com/nedbat/coveragepy/issues/1643 +.. _pull 1952: https://github.com/nedbat/coveragepy/pull/1952 + .. start-releases diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 227c3122c..b65af8ecc 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -186,6 +186,7 @@ Nils Kattenbeck Noel O'Boyle Oleg Höfling Oleh Krehel +Olena Yefymenko Olivier Grisel Ori Avtalion Pablo Carballo diff --git a/doc/config.rst b/doc/config.rst index 5aea2e251..79f0adb35 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -39,12 +39,13 @@ environment variable. If ``.coveragerc`` doesn't exist and another file hasn't been specified, then coverage.py will look for settings in other common configuration files, in this -order: setup.cfg, tox.ini, or pyproject.toml. The first file found with +order: .coveragerc.toml, setup.cfg, tox.ini, or pyproject.toml. The first file found with coverage.py settings will be used and other files won't be consulted. -Coverage.py will read from "pyproject.toml" if TOML support is available, +Coverage.py will read from ".coveragerc.toml" and "pyproject.toml" if TOML support is available, either because you are running on Python 3.11 or later, or because you -installed with the ``toml`` extra (``pip install coverage[toml]``). +installed with the ``toml`` extra (``pip install coverage[toml]``). Both files +use the same ``[tool.coverage]`` section structure. Syntax @@ -136,6 +137,35 @@ Here's a sample configuration file, in each syntax: [html] directory = coverage_html_report """, + + coveragerc_toml=r""" + [tool.coverage.run] + branch = true + + [tool.coverage.report] + # Regexes for lines to exclude from consideration + exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", + ] + + ignore_errors = true + + [tool.coverage.html] + directory = "coverage_html_report" + """, toml=r""" [tool.coverage.run] branch = true @@ -198,6 +228,36 @@ Here's a sample configuration file, in each syntax: [html] directory = coverage_html_report + .. code-tab:: toml + :caption: .coveragerc.toml + + [tool.coverage.run] + branch = true + + [tool.coverage.report] + # Regexes for lines to exclude from consideration + exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", + ] + + ignore_errors = true + + [tool.coverage.html] + directory = "coverage_html_report" + .. code-tab:: toml :caption: pyproject.toml