Skip to content

Commit 331fdd8

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 8709a53 commit 331fdd8

File tree

2 files changed

+87
-41
lines changed

2 files changed

+87
-41
lines changed

coverage/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ def config_files_to_try(config_file: bool | str) -> list[tuple[str, bool, bool]]
587587
assert isinstance(config_file, str)
588588
files_to_try = [
589589
(config_file, True, specified_file),
590+
(".coveragerc.toml", True, False),
590591
("setup.cfg", False, False),
591592
("tox.ini", False, False),
592593
("pyproject.toml", False, False),

tests/test_config.py

Lines changed: 86 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def test_default_config(self) -> None:
3333

3434
def test_arguments(self) -> None:
3535
# Arguments to the constructor are applied to the configuration.
36-
cov = coverage.Coverage(timid=True, data_file="fooey.dat", concurrency="multiprocessing")
36+
cov = coverage.Coverage(
37+
timid=True, data_file="fooey.dat", concurrency="multiprocessing")
3738
assert cov.config.timid
3839
assert not cov.config.branch
3940
assert cov.config.data_file == "fooey.dat"
@@ -66,9 +67,10 @@ def test_named_config_file(self, file_class: FilePathType) -> None:
6667
assert not cov.config.branch
6768
assert cov.config.data_file == "delete.me"
6869

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", """\
70+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
71+
def test_toml_config_file(self, filename) -> None:
72+
# A pyproject.toml and coveragerc.toml will be read into the configuration.
73+
self.make_file(filename, """\
7274
# This is just a bogus toml file for testing.
7375
[tool.somethingelse]
7476
authors = ["Joe D'Ávila <[email protected]>"]
@@ -94,11 +96,13 @@ def test_toml_config_file(self) -> None:
9496
assert cov.config.precision == 3
9597
assert cov.config.html_title == "tabblo & «ταБЬℓσ»"
9698
assert cov.config.fail_under == 90.5
97-
assert cov.config.get_plugin_options("plugins.a_plugin") == {"hello": "world"}
99+
assert cov.config.get_plugin_options("plugins.a_plugin") == {
100+
"hello": "world"}
98101

99-
def test_toml_ints_can_be_floats(self) -> None:
102+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
103+
def test_toml_ints_can_be_floats(self, filename) -> None:
100104
# Test that our class doesn't reject integers when loading floats
101-
self.make_file("pyproject.toml", """\
105+
self.make_file(filename, """\
102106
# This is just a bogus toml file for testing.
103107
[tool.coverage.report]
104108
fail_under = 90
@@ -214,6 +218,7 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None:
214218
with pytest.raises(ConfigError, match=msg):
215219
coverage.Coverage()
216220

221+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
217222
@pytest.mark.parametrize("bad_config, msg", [
218223
("[tool.coverage.run]\ntimid = \"maybe?\"\n", r"maybe[?]"),
219224
("[tool.coverage.run\n", None),
@@ -237,9 +242,9 @@ def test_parse_errors(self, bad_config: str, msg: str) -> None:
237242
("[tool.coverage.report]\nprecision=1.23", "not an integer"),
238243
('[tool.coverage.report]\nfail_under="s"', "couldn't convert to a float"),
239244
])
240-
def test_toml_parse_errors(self, bad_config: str, msg: str) -> None:
245+
def test_toml_parse_errors(self, filename, bad_config: str, msg: str) -> None:
241246
# Im-parsable values raise ConfigError, with details.
242-
self.make_file("pyproject.toml", bad_config)
247+
self.make_file(filename, bad_config)
243248
with pytest.raises(ConfigError, match=msg):
244249
coverage.Coverage()
245250

@@ -263,11 +268,13 @@ def test_environment_vars_in_config(self) -> None:
263268
cov = coverage.Coverage()
264269
assert cov.config.data_file == "hello-world.fooey"
265270
assert cov.config.branch is True
266-
assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
271+
assert cov.config.exclude_list == [
272+
"the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
267273

268-
def test_environment_vars_in_toml_config(self) -> None:
274+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
275+
def test_environment_vars_in_toml_config(self, filename) -> None:
269276
# Config files can have $envvars in them.
270-
self.make_file("pyproject.toml", """\
277+
self.make_file(filename, """\
271278
[tool.coverage.run]
272279
data_file = "$DATA_FILE.fooey"
273280
branch = "$BRANCH"
@@ -296,7 +303,8 @@ def test_environment_vars_in_toml_config(self) -> None:
296303
assert cov.config.branch is True
297304
assert cov.config.precision == 3
298305
assert cov.config.data_file == "hello-world.fooey"
299-
assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
306+
assert cov.config.exclude_list == [
307+
"the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"]
300308

301309
def test_tilde_in_config(self) -> None:
302310
# Config entries that are file paths can be tilde-expanded.
@@ -330,9 +338,10 @@ def test_tilde_in_config(self) -> None:
330338

331339
self.assert_tilde_results()
332340

333-
def test_tilde_in_toml_config(self) -> None:
341+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
342+
def test_tilde_in_toml_config(self, filename) -> None:
334343
# Config entries that are file paths can be tilde-expanded.
335-
self.make_file("pyproject.toml", """\
344+
self.make_file(filename, """\
336345
[tool.coverage.run]
337346
data_file = "~/data.file"
338347
@@ -384,7 +393,8 @@ def expanduser(s: str) -> str:
384393
assert cov.config.lcov_output == "/Users/me/lcov/~foo.lcov"
385394
assert cov.config.xml_output == "/Users/me/somewhere/xml.out"
386395
assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"]
387-
assert cov.config.paths == {'mapping': ['/Users/me/src', '/Users/joe/source']}
396+
assert cov.config.paths == {'mapping': [
397+
'/Users/me/src', '/Users/joe/source']}
388398

389399
def test_tweaks_after_constructor(self) -> None:
390400
# set_option can be used after construction to affect the config.
@@ -440,7 +450,8 @@ def test_tweak_error_checking(self) -> None:
440450
def test_tweak_plugin_options(self) -> None:
441451
# Plugin options have a more flexible syntax.
442452
cov = coverage.Coverage()
443-
cov.set_option("run:plugins", ["fooey.plugin", "xyzzy.coverage.plugin"])
453+
cov.set_option("run:plugins", [
454+
"fooey.plugin", "xyzzy.coverage.plugin"])
444455
cov.set_option("fooey.plugin:xyzzy", 17)
445456
cov.set_option("xyzzy.coverage.plugin:plugh", ["a", "b"])
446457
with pytest.raises(ConfigError, match="No such option: 'no_such.plugin:foo'"):
@@ -460,12 +471,13 @@ def test_unknown_option(self) -> None:
460471
with pytest.warns(CoverageWarning, match=msg):
461472
_ = coverage.Coverage()
462473

463-
def test_unknown_option_toml(self) -> None:
464-
self.make_file("pyproject.toml", """\
474+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
475+
def test_unknown_option_toml(self, filename) -> None:
476+
self.make_file(filename, """\
465477
[tool.coverage.run]
466478
xyzzy = 17
467479
""")
468-
msg = r"Unrecognized option '\[tool.coverage.run\] xyzzy=' in config file pyproject.toml"
480+
msg = f"Unrecognized option '\\[tool.coverage.run\\] xyzzy=' in config file {filename}"
469481
with pytest.warns(CoverageWarning, match=msg):
470482
_ = coverage.Coverage()
471483

@@ -512,14 +524,16 @@ def test_exceptions_from_missing_things(self) -> None:
512524
with pytest.raises(ConfigError, match="No option 'foo' in section: 'xyzzy'"):
513525
config.get("xyzzy", "foo")
514526

515-
def test_exclude_also(self) -> None:
516-
self.make_file("pyproject.toml", """\
527+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
528+
def test_exclude_also(self, filename) -> None:
529+
self.make_file(filename, """\
517530
[tool.coverage.report]
518531
exclude_also = ["foobar", "raise .*Error"]
519532
""")
520533
cov = coverage.Coverage()
521534

522-
expected = coverage.config.DEFAULT_EXCLUDE + ["foobar", "raise .*Error"]
535+
expected = coverage.config.DEFAULT_EXCLUDE + \
536+
["foobar", "raise .*Error"]
523537
assert cov.config.exclude_list == expected
524538

525539
def test_partial_also(self) -> None:
@@ -529,7 +543,8 @@ def test_partial_also(self) -> None:
529543
""")
530544
cov = coverage.Coverage()
531545

532-
expected = coverage.config.DEFAULT_PARTIAL + ["foobar", "raise .*Error"]
546+
expected = coverage.config.DEFAULT_PARTIAL + \
547+
["foobar", "raise .*Error"]
533548
assert cov.config.partial_list == expected
534549

535550
def test_core_option(self) -> None:
@@ -672,10 +687,12 @@ def assert_config_settings_are_correct(self, cov: Coverage) -> None:
672687
assert cov.config.source_dirs == ["cooldir"]
673688
assert cov.config.disable_warnings == ["abcd", "efgh"]
674689

675-
assert cov.get_exclude_list() == ["if 0:", r"pragma:?\s+no cover", "another_tab"]
690+
assert cov.get_exclude_list() == [
691+
"if 0:", r"pragma:?\s+no cover", "another_tab"]
676692
assert cov.config.ignore_errors
677693
assert cov.config.run_omit == ["twenty"]
678-
assert cov.config.report_omit == ["one", "another", "some_more", "yet_more"]
694+
assert cov.config.report_omit == [
695+
"one", "another", "some_more", "yet_more"]
679696
assert cov.config.report_include == ["thirty"]
680697
assert cov.config.precision == 3
681698

@@ -719,7 +736,8 @@ def check_config_file_settings_in_other_file(self, fname: str, contents: str) ->
719736
self.assert_config_settings_are_correct(cov)
720737

721738
def test_config_file_settings_in_setupcfg(self) -> None:
722-
self.check_config_file_settings_in_other_file("setup.cfg", self.SETUP_CFG)
739+
self.check_config_file_settings_in_other_file(
740+
"setup.cfg", self.SETUP_CFG)
723741

724742
def test_config_file_settings_in_toxini(self) -> None:
725743
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)
732750
self.assert_config_settings_are_correct(cov)
733751

734752
def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self) -> None:
735-
self.check_other_config_if_coveragerc_specified("setup.cfg", self.SETUP_CFG)
753+
self.check_other_config_if_coveragerc_specified(
754+
"setup.cfg", self.SETUP_CFG)
736755

737756
def test_config_file_settings_in_tox_if_coveragerc_specified(self) -> None:
738-
self.check_other_config_if_coveragerc_specified("tox.ini", self.TOX_INI)
757+
self.check_other_config_if_coveragerc_specified(
758+
"tox.ini", self.TOX_INI)
739759

740760
def check_other_not_read_if_coveragerc(self, fname: str) -> None:
741761
"""Check config `fname` is not read if .coveragerc exists."""
@@ -839,35 +859,38 @@ def test_no_toml_installed_explicit_toml(self) -> None:
839859
coverage.Coverage(config_file="cov.toml")
840860

841861
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
842-
def test_no_toml_installed_pyproject_toml(self) -> None:
843-
# Can't have coverage config in pyproject.toml without toml installed.
844-
self.make_file("pyproject.toml", """\
862+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
863+
def test_no_toml_installed_pyproject_toml(self, filename) -> None:
864+
# Can't have coverage config in pyproject.toml and .coveragerc.toml without toml installed.
865+
self.make_file(filename, """\
845866
# A toml file!
846867
[tool.coverage.run]
847868
xyzzy = 17
848869
""")
849870
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
850-
msg = "Can't read 'pyproject.toml' without TOML support"
871+
msg = "Can't read '{filename}' without TOML support"
851872
with pytest.raises(ConfigError, match=msg):
852873
coverage.Coverage()
853874

854875
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
855-
def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None:
876+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
877+
def test_no_toml_installed_pyproject_toml_shorter_syntax(self, filename) -> None:
856878
# Can't have coverage config in pyproject.toml without toml installed.
857-
self.make_file("pyproject.toml", """\
879+
self.make_file(filename, """\
858880
# A toml file!
859881
[tool.coverage]
860882
run.parallel = true
861883
""")
862884
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
863-
msg = "Can't read 'pyproject.toml' without TOML support"
885+
msg = "Can't read '{filename}' without TOML support"
864886
with pytest.raises(ConfigError, match=msg):
865887
coverage.Coverage()
866888

867889
@pytest.mark.skipif(env.PYVERSION >= (3, 11), reason="Python 3.11 has toml in stdlib")
868-
def test_no_toml_installed_pyproject_no_coverage(self) -> None:
890+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
891+
def test_no_toml_installed_pyproject_no_coverage(self, filename) -> None:
869892
# It's ok to have non-coverage pyproject.toml without toml installed.
870-
self.make_file("pyproject.toml", """\
893+
self.make_file(filename, """\
871894
# A toml file!
872895
[tool.something]
873896
xyzzy = 17
@@ -879,16 +902,38 @@ def test_no_toml_installed_pyproject_no_coverage(self) -> None:
879902
assert not cov.config.branch
880903
assert cov.config.data_file == ".coverage"
881904

882-
def test_exceptions_from_missing_toml_things(self) -> None:
883-
self.make_file("pyproject.toml", """\
905+
@pytest.mark.parametrize("filename", ["pyproject.toml", ".coveragerc.toml"])
906+
def test_exceptions_from_missing_toml_things(self, filename) -> None:
907+
self.make_file(filename, """\
884908
[tool.coverage.run]
885909
branch = true
886910
""")
887911
config = TomlConfigParser(False)
888-
config.read("pyproject.toml")
912+
config.read(filename)
889913
with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
890914
config.options("xyzzy")
891915
with pytest.raises(ConfigError, match="No section: 'xyzzy'"):
892916
config.get("xyzzy", "foo")
893917
with pytest.raises(ConfigError, match="No option 'foo' in section: 'tool.coverage.run'"):
894918
config.get("run", "foo")
919+
920+
def test_coveragerc_toml_priority(self) -> None:
921+
"""Test that .coveragerc.toml has priority over pyproject.toml."""
922+
self.make_file(".coveragerc.toml", """\
923+
[tool.coverage.run]
924+
timid = true
925+
data_file = ".toml-data.dat"
926+
branch = true
927+
""")
928+
929+
self.make_file("pyproject.toml", """\
930+
[tool.coverage.run]
931+
timid = false
932+
data_file = "pyproject-data.dat"
933+
branch = false
934+
""")
935+
cov = coverage.Coverage()
936+
937+
assert cov.config.timid is True
938+
assert cov.config.data_file == ".toml-data.dat"
939+
assert cov.config.branch is True

0 commit comments

Comments
 (0)