Skip to content

Commit 6733eb8

Browse files
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.
1 parent 287d0f9 commit 6733eb8

File tree

2 files changed

+67
-41
lines changed

2 files changed

+67
-41
lines changed

coverage/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]
571571
assert isinstance(config_file, str)
572572
files_to_try = [
573573
(config_file, True, specified_file),
574+
(".coveragerc.toml", True, False),
574575
("setup.cfg", False, False),
575576
("tox.ini", False, False),
576577
("pyproject.toml", False, False),

tests/test_config.py

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ def test_named_config_file(self, file_class: FilePathType) -> None:
6464
assert not cov.config.branch
6565
assert cov.config.data_file == "delete.me"
6666

67-
def test_toml_config_file(self) -> None:
68-
# A pyproject.toml file will be read into the configuration.
69-
self.make_file("pyproject.toml", """\
67+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
68+
def test_toml_config_file(self, filename) -> None:
69+
# A pyproject.toml and coveragerc.toml will be read into the configuration.
70+
self.make_file(filename, """\
7071
# This is just a bogus toml file for testing.
7172
[tool.somethingelse]
7273
authors = ["Joe D'Ávila <[email protected]>"]
@@ -94,9 +95,10 @@ def test_toml_config_file(self) -> None:
9495
assert cov.config.fail_under == 90.5
9596
assert cov.config.get_plugin_options("plugins.a_plugin") == {"hello": "world"}
9697

97-
def test_toml_ints_can_be_floats(self) -> None:
98+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
99+
def test_toml_ints_can_be_floats(self, filename) -> None:
98100
# Test that our class doesn't reject integers when loading floats
99-
self.make_file("pyproject.toml", """\
101+
self.make_file(filename, """\
100102
# This is just a bogus toml file for testing.
101103
[tool.coverage.report]
102104
fail_under = 90
@@ -205,7 +207,8 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None:
205207
self.make_file(".coveragerc", bad_config)
206208
with pytest.raises(ConfigError, match=msg):
207209
coverage.Coverage()
208-
210+
211+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
209212
@pytest.mark.parametrize("bad_config, msg", [
210213
("[tool.coverage.run]\ntimid = \"maybe?\"\n", r"maybe[?]"),
211214
("[tool.coverage.run\n", None),
@@ -223,9 +226,9 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None:
223226
("[tool.coverage.report]\nprecision=1.23", "not an integer"),
224227
('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"),
225228
])
226-
def test_toml_parse_errors(self, bad_config: str, msg: str) -> None:
229+
def test_toml_parse_errors(self, filename, bad_config: str, msg: str) -> None:
227230
# Im-parsable values raise ConfigError, with details.
228-
self.make_file("pyproject.toml", bad_config)
231+
self.make_file(filename, bad_config)
229232
with pytest.raises(ConfigError, match=msg):
230233
coverage.Coverage()
231234

@@ -251,9 +254,10 @@ def test_environment_vars_in_config(self) -> None:
251254
assert cov.config.branch is True
252255
assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
253256

254-
def test_environment_vars_in_toml_config(self) -> None:
257+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
258+
def test_environment_vars_in_toml_config(self, filename) -> None:
255259
# Config files can have $envvars in them.
256-
self.make_file("pyproject.toml", """\
260+
self.make_file(filename, """\
257261
[tool.coverage.run]
258262
data_file = "$DATA_FILE.fooey"
259263
branch = "$BRANCH"
@@ -325,9 +329,10 @@ def expanduser(s: str) -> str:
325329
assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"]
326330
assert cov.config.paths == {'mapping': ['/Users/me/src', '/Users/joe/source']}
327331

328-
def test_tilde_in_toml_config(self) -> None:
332+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
333+
def test_tilde_in_toml_config(self, filename) -> None:
329334
# Config entries that are file paths can be tilde-expanded.
330-
self.make_file("pyproject.toml", """\
335+
self.make_file(filename, """\
331336
[tool.coverage.run]
332337
data_file = "~/data.file"
333338
@@ -441,22 +446,14 @@ def test_unknown_option(self) -> None:
441446
msg = r"Unrecognized option '\[run\] xyzzy=' in config file .coveragerc"
442447
with pytest.warns(CoverageWarning, match=msg):
443448
_ = coverage.Coverage()
444-
445-
def test_unknown_option_toml(self) -> None:
446-
self.make_file("pyproject.toml", """\
449+
450+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
451+
def test_unknown_option_toml(self, filename) -> None:
452+
self.make_file(filename, """\
447453
[tool.coverage.run]
448454
xyzzy = 17
449455
""")
450-
msg = r"Unrecognized option '\[tool.coverage.run\] xyzzy=' in config file pyproject.toml"
451-
with pytest.warns(CoverageWarning, match=msg):
452-
_ = coverage.Coverage()
453-
454-
def test_misplaced_option(self) -> None:
455-
self.make_file(".coveragerc", """\
456-
[report]
457-
branch = True
458-
""")
459-
msg = r"Unrecognized option '\[report\] branch=' in config file .coveragerc"
456+
msg = f"Unrecognized option '\\[tool.coverage.run\\] xyzzy=' in config file {filename}"
460457
with pytest.warns(CoverageWarning, match=msg):
461458
_ = coverage.Coverage()
462459

@@ -468,7 +465,7 @@ def test_unknown_option_in_other_ini_file(self) -> None:
468465
msg = r"Unrecognized option '\[coverage:run\] huh=' in config file setup.cfg"
469466
with pytest.warns(CoverageWarning, match=msg):
470467
_ = coverage.Coverage()
471-
468+
472469
def test_exceptions_from_missing_things(self) -> None:
473470
self.make_file("config.ini", """\
474471
[run]
@@ -481,8 +478,10 @@ def test_exceptions_from_missing_things(self) -> None:
481478
with pytest.raises(ConfigError, match="No option 'foo' in section: 'xyzzy'"):
482479
config.get("xyzzy", "foo")
483480

484-
def test_exclude_also(self) -> None:
485-
self.make_file("pyproject.toml", """\
481+
482+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
483+
def test_exclude_also(self, filename) -> None:
484+
self.make_file(filename, """\
486485
[tool.coverage.report]
487486
exclude_also = ["foobar", "raise .*Error"]
488487
""")
@@ -775,35 +774,39 @@ def test_no_toml_installed_explicit_toml(self) -> None:
775774
coverage.Coverage(config_file="cov.toml")
776775

777776
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
778-
def test_no_toml_installed_pyproject_toml(self) -> None:
779-
# Can't have coverage config in pyproject.toml without toml installed.
780-
self.make_file("pyproject.toml", """\
777+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
778+
def test_no_toml_installed_pyproject_toml(self, filename) -> None:
779+
# Can't have coverage config in pyproject.toml and .coveragerc.toml without toml installed.
780+
self.make_file(filename, """\
781781
# A toml file!
782782
[tool.coverage.run]
783783
xyzzy = 17
784784
""")
785785
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
786-
msg = "Can't read 'pyproject.toml' without TOML support"
786+
msg = "Can't read '{filename}' without TOML support"
787787
with pytest.raises(ConfigError, match=msg):
788788
coverage.Coverage()
789789

790790
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
791-
def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None:
791+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
792+
def test_no_toml_installed_pyproject_toml_shorter_syntax(self, filename) -> None:
792793
# Can't have coverage config in pyproject.toml without toml installed.
793-
self.make_file("pyproject.toml", """\
794+
self.make_file(filename, """\
794795
# A toml file!
795796
[tool.coverage]
796797
run.parallel = true
797798
""")
798799
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
799-
msg = "Can't read 'pyproject.toml' without TOML support"
800+
msg = "Can't read '{filename}' without TOML support"
800801
with pytest.raises(ConfigError, match=msg):
801802
coverage.Coverage()
802803

804+
803805
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
804-
def test_no_toml_installed_pyproject_no_coverage(self) -> None:
806+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
807+
def test_no_toml_installed_pyproject_no_coverage(self, filename) -> None:
805808
# It's ok to have non-coverage pyproject.toml without toml installed.
806-
self.make_file("pyproject.toml", """\
809+
self.make_file(filename, """\
807810
# A toml file!
808811
[tool.something]
809812
xyzzy = 17
@@ -814,17 +817,39 @@ def test_no_toml_installed_pyproject_no_coverage(self) -> None:
814817
assert not cov.config.timid
815818
assert not cov.config.branch
816819
assert cov.config.data_file == ".coverage"
817-
818-
def test_exceptions_from_missing_toml_things(self) -> None:
819-
self.make_file("pyproject.toml", """\
820+
821+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
822+
def test_exceptions_from_missing_toml_things(self, filename) -> None:
823+
self.make_file(filename, """\
820824
[tool.coverage.run]
821825
branch = true
822826
""")
823827
config = TomlConfigParser(False)
824-
config.read("pyproject.toml")
828+
config.read(filename)
825829
with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
826830
config.options("xyzzy")
827831
with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
828832
config.get("xyzzy", "foo")
829833
with pytest.raises(ConfigError, match="No option 'foo' in section: 'tool.coverage.run'"):
830834
config.get("run", "foo")
835+
836+
def test_coveragerc_toml_priority(self) -> None:
837+
"""Test that .coveragerc.toml has priority over pyproject.toml."""
838+
self.make_file(".coveragerc.toml", """\
839+
[tool.coverage.run]
840+
timid = true
841+
data_file = ".toml-data.dat"
842+
branch = true
843+
""")
844+
845+
self.make_file("pyproject.toml", """\
846+
[tool.coverage.run]
847+
timid = false
848+
data_file = "pyproject-data.dat"
849+
branch = false
850+
""")
851+
cov = coverage.Coverage()
852+
853+
assert cov.config.timid is True
854+
assert cov.config.data_file == ".toml-data.dat"
855+
assert cov.config.branch is True

0 commit comments

Comments
 (0)