Skip to content

Commit 89095bb

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 614dfbf commit 89095bb

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
@@ -573,6 +573,7 @@ def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]
573573
assert isinstance(config_file, str)
574574
files_to_try = [
575575
(config_file, True, specified_file),
576+
(".coveragerc.toml", True, False),
576577
("setup.cfg", False, False),
577578
("tox.ini", False, False),
578579
("pyproject.toml", False, False),

tests/test_config.py

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

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

99-
def test_toml_ints_can_be_floats(self) -> None:
100+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
101+
def test_toml_ints_can_be_floats(self, filename) -> None:
100102
# Test that our class doesn't reject integers when loading floats
101-
self.make_file("pyproject.toml", """\
103+
self.make_file(filename, """\
102104
# This is just a bogus toml file for testing.
103105
[tool.coverage.report]
104106
fail_under = 90
@@ -207,7 +209,8 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None:
207209
self.make_file(".coveragerc", bad_config)
208210
with pytest.raises(ConfigError, match=msg):
209211
coverage.Coverage()
210-
212+
213+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
211214
@pytest.mark.parametrize("bad_config, msg", [
212215
("[tool.coverage.run]\ntimid = \"maybe?\"\n", r"maybe[?]"),
213216
("[tool.coverage.run\n", None),
@@ -225,9 +228,9 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None:
225228
("[tool.coverage.report]\nprecision=1.23", "not an integer"),
226229
('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"),
227230
])
228-
def test_toml_parse_errors(self, bad_config: str, msg: str) -> None:
231+
def test_toml_parse_errors(self, filename, bad_config: str, msg: str) -> None:
229232
# Im-parsable values raise ConfigError, with details.
230-
self.make_file("pyproject.toml", bad_config)
233+
self.make_file(filename, bad_config)
231234
with pytest.raises(ConfigError, match=msg):
232235
coverage.Coverage()
233236

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

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

330-
def test_tilde_in_toml_config(self) -> None:
334+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
335+
def test_tilde_in_toml_config(self, filename) -> None:
331336
# Config entries that are file paths can be tilde-expanded.
332-
self.make_file("pyproject.toml", """\
337+
self.make_file(filename, """\
333338
[tool.coverage.run]
334339
data_file = "~/data.file"
335340
@@ -443,22 +448,14 @@ def test_unknown_option(self) -> None:
443448
msg = r"Unrecognized option '\[run\] xyzzy=' in config file .coveragerc"
444449
with pytest.warns(CoverageWarning, match=msg):
445450
_ = coverage.Coverage()
446-
447-
def test_unknown_option_toml(self) -> None:
448-
self.make_file("pyproject.toml", """\
451+
452+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
453+
def test_unknown_option_toml(self, filename) -> None:
454+
self.make_file(filename, """\
449455
[tool.coverage.run]
450456
xyzzy = 17
451457
""")
452-
msg = r"Unrecognized option '\[tool.coverage.run\] xyzzy=' in config file pyproject.toml"
453-
with pytest.warns(CoverageWarning, match=msg):
454-
_ = coverage.Coverage()
455-
456-
def test_misplaced_option(self) -> None:
457-
self.make_file(".coveragerc", """\
458-
[report]
459-
branch = True
460-
""")
461-
msg = r"Unrecognized option '\[report\] branch=' in config file .coveragerc"
458+
msg = f"Unrecognized option '\\[tool.coverage.run\\] xyzzy=' in config file {filename}"
462459
with pytest.warns(CoverageWarning, match=msg):
463460
_ = coverage.Coverage()
464461

@@ -470,7 +467,7 @@ def test_unknown_option_in_other_ini_file(self) -> None:
470467
msg = r"Unrecognized option '\[coverage:run\] huh=' in config file setup.cfg"
471468
with pytest.warns(CoverageWarning, match=msg):
472469
_ = coverage.Coverage()
473-
470+
474471
def test_exceptions_from_missing_things(self) -> None:
475472
self.make_file("config.ini", """\
476473
[run]
@@ -483,8 +480,10 @@ def test_exceptions_from_missing_things(self) -> None:
483480
with pytest.raises(ConfigError, match="No option 'foo' in section: 'xyzzy'"):
484481
config.get("xyzzy", "foo")
485482

486-
def test_exclude_also(self) -> None:
487-
self.make_file("pyproject.toml", """\
483+
484+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
485+
def test_exclude_also(self, filename) -> None:
486+
self.make_file(filename, """\
488487
[tool.coverage.report]
489488
exclude_also = ["foobar", "raise .*Error"]
490489
""")
@@ -800,35 +799,39 @@ def test_no_toml_installed_explicit_toml(self) -> None:
800799
coverage.Coverage(config_file="cov.toml")
801800

802801
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
803-
def test_no_toml_installed_pyproject_toml(self) -> None:
804-
# Can't have coverage config in pyproject.toml without toml installed.
805-
self.make_file("pyproject.toml", """\
802+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
803+
def test_no_toml_installed_pyproject_toml(self, filename) -> None:
804+
# Can't have coverage config in pyproject.toml and .coveragerc.toml without toml installed.
805+
self.make_file(filename, """\
806806
# A toml file!
807807
[tool.coverage.run]
808808
xyzzy = 17
809809
""")
810810
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
811-
msg = "Can't read 'pyproject.toml' without TOML support"
811+
msg = "Can't read '{filename}' without TOML support"
812812
with pytest.raises(ConfigError, match=msg):
813813
coverage.Coverage()
814814

815815
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
816-
def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None:
816+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
817+
def test_no_toml_installed_pyproject_toml_shorter_syntax(self, filename) -> None:
817818
# Can't have coverage config in pyproject.toml without toml installed.
818-
self.make_file("pyproject.toml", """\
819+
self.make_file(filename, """\
819820
# A toml file!
820821
[tool.coverage]
821822
run.parallel = true
822823
""")
823824
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
824-
msg = "Can't read 'pyproject.toml' without TOML support"
825+
msg = "Can't read '{filename}' without TOML support"
825826
with pytest.raises(ConfigError, match=msg):
826827
coverage.Coverage()
827828

829+
828830
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
829-
def test_no_toml_installed_pyproject_no_coverage(self) -> None:
831+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
832+
def test_no_toml_installed_pyproject_no_coverage(self, filename) -> None:
830833
# It's ok to have non-coverage pyproject.toml without toml installed.
831-
self.make_file("pyproject.toml", """\
834+
self.make_file(filename, """\
832835
# A toml file!
833836
[tool.something]
834837
xyzzy = 17
@@ -839,17 +842,39 @@ def test_no_toml_installed_pyproject_no_coverage(self) -> None:
839842
assert not cov.config.timid
840843
assert not cov.config.branch
841844
assert cov.config.data_file == ".coverage"
842-
843-
def test_exceptions_from_missing_toml_things(self) -> None:
844-
self.make_file("pyproject.toml", """\
845+
846+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
847+
def test_exceptions_from_missing_toml_things(self, filename) -> None:
848+
self.make_file(filename, """\
845849
[tool.coverage.run]
846850
branch = true
847851
""")
848852
config = TomlConfigParser(False)
849-
config.read("pyproject.toml")
853+
config.read(filename)
850854
with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
851855
config.options("xyzzy")
852856
with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
853857
config.get("xyzzy", "foo")
854858
with pytest.raises(ConfigError, match="No option 'foo' in section: 'tool.coverage.run'"):
855859
config.get("run", "foo")
860+
861+
def test_coveragerc_toml_priority(self) -> None:
862+
"""Test that .coveragerc.toml has priority over pyproject.toml."""
863+
self.make_file(".coveragerc.toml", """\
864+
[tool.coverage.run]
865+
timid = true
866+
data_file = ".toml-data.dat"
867+
branch = true
868+
""")
869+
870+
self.make_file("pyproject.toml", """\
871+
[tool.coverage.run]
872+
timid = false
873+
data_file = "pyproject-data.dat"
874+
branch = false
875+
""")
876+
cov = coverage.Coverage()
877+
878+
assert cov.config.timid is True
879+
assert cov.config.data_file == ".toml-data.dat"
880+
assert cov.config.branch is True

0 commit comments

Comments
 (0)