Skip to content

Commit 9e798d1

Browse files
Fix crashes during toml configuration parsing
Add test for current pyproject.toml issues Add a 'bad-configuration-option-value' message for bad toml configuration We do not catch all the problem in toml because we don't know what is expected so we can't recommend. See #5259 We can detect bad top level option when reading the toml
1 parent dcf2d93 commit 9e798d1

15 files changed

+154
-57
lines changed

ChangeLog

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ Release date: TBA
146146

147147
Closes #5261
148148

149+
* Crashes when a list is encountered in a toml configuration do not happen anymore. A new
150+
``bad-configuration-option-value`` checker was added that will emit for these problems.
151+
Some bad configurations might still silently not be taken into account which is tracked in
152+
a follow-up issue.
153+
154+
Closes #4580
155+
Follow-up in #5259
156+
149157
* Added new checker ``useless-with-lock`` to find incorrect usage of with statement and threading module locks.
150158
Emitted when ``with threading.Lock():`` is used instead of ``with lock_instance:``.
151159

pylint/config/option_manager_mixin.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,14 @@ def read_config_file(self, config_file=None, verbose=None):
271271

272272
use_config_file = config_file and os.path.exists(config_file)
273273
if use_config_file:
274+
mod_name = config_file
275+
self.current_name = mod_name
276+
try:
277+
self.set_current_module(mod_name)
278+
except AttributeError:
279+
# 'OptionsManagerMixIn' object has no attribute 'set_current_module'
280+
# Ie: the class is not used as a mixin
281+
pass
274282
parser = self.cfgfile_parser
275283
if config_file.endswith(".toml"):
276284
self._parse_toml(config_file, parser)
@@ -292,9 +300,8 @@ def read_config_file(self, config_file=None, verbose=None):
292300
msg = "No config file found, using default configuration"
293301
print(msg, file=sys.stderr)
294302

295-
@staticmethod
296303
def _parse_toml(
297-
config_file: Union[Path, str], parser: configparser.ConfigParser
304+
self, config_file: Union[Path, str], parser: configparser.ConfigParser
298305
) -> None:
299306
"""Parse and handle errors of a toml configuration file."""
300307
with open(config_file, encoding="utf-8") as fp:
@@ -304,23 +311,42 @@ def _parse_toml(
304311
except KeyError:
305312
return
306313
for section, values in sections_values.items():
314+
section_name = section.upper()
307315
# TOML has rich types, convert values to
308316
# strings as ConfigParser expects.
317+
if not isinstance(values, dict):
318+
# This class is a mixin the add_message come from the pylinter
319+
self.add_message( # type: ignore
320+
"bad-configuration-option-value", line=0, args=(section, values)
321+
)
322+
continue
309323
for option, value in values.items():
310324
if isinstance(value, bool):
311325
values[option] = "yes" if value else "no"
312-
elif isinstance(value, (int, float)):
313-
values[option] = str(value)
314326
elif isinstance(value, list):
315327
values[option] = ",".join(value)
316-
parser._sections[section.upper()] = values # type: ignore
328+
else:
329+
values[option] = str(value)
330+
for option, value in values.items():
331+
try:
332+
parser.set(section_name, option, value=value)
333+
except configparser.NoSectionError:
334+
parser.add_section(section_name)
335+
parser.set(section_name, option, value=value)
317336

318337
def load_config_file(self):
319338
"""Dispatch values previously read from a configuration file to each
320339
options provider)"""
321340
parser = self.cfgfile_parser
322341
for section in parser.sections():
323-
for option, value in parser.items(section):
342+
try:
343+
items = parser.items(section)
344+
except ValueError:
345+
self.add_message(
346+
"bad-configuration-option-value", line=0, args=(section, "?")
347+
)
348+
continue
349+
for option, value in items:
324350
try:
325351
self.global_set_option(option, value)
326352
except (KeyError, optparse.OptionError):

pylint/lint/pylinter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ def _load_reporter_by_class(reporter_class: str) -> type:
158158
"bad-plugin-value",
159159
"Used when a bad value is used in 'load-plugins'.",
160160
),
161+
"E0014": (
162+
"Bad top level configuration section name encountered '%s' : '%s'",
163+
"bad-configuration-option-value",
164+
"Used when a top section value can't be parsed probably because it does not exists.",
165+
),
161166
}
162167

163168

tests/config/file_to_lint.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Perfect module with only documentation for test_config_pyproject_toml.py"""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[tool.pylint."messages control"]
2+
disable = "logging-not-lazy,logging-format-interpolation"
3+
jobs = "10"
4+
reports = "yes"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.pylint.basic]
2+
name-group = { "a"="b" }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.pylint.imports]
2+
preferred-modules = { "a"="b" }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.pylint]
2+
load-plugins = []
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[tool.pylint]
2+
# Both disable and load-plugins are not top level section
3+
load-plugins = []
4+
disable = "logging-not-lazy,logging-format-interpolation"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Both disable and load-plugins are not top level section
2+
[tool.pylint."imports"]
3+
disable = [
4+
"logging-not-lazy",
5+
"logging-format-interpolation",
6+
]
7+
preferred-modules = { "a"="b" }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[tool.pylint."messages control"]
2+
disable = [
3+
"logging-not-lazy",
4+
"logging-format-interpolation",
5+
]
6+
jobs = 10
7+
reports = true
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.pylint]
2+
disable = "logging-not-lazy,logging-format-interpolation"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# The pylint_websockets plugin does not exist and therefore this toml is invalid
2+
[tool.poe.tasks]
3+
docs = {cmd = "sphinx-build docs build", help = "Build documentation"}
4+
5+
[tool.pylint.MASTER]
6+
load-plugins = 'pylint_websockets'
7+
8+
format = ["black", "isort"]

tests/config/test_config.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -124,54 +124,3 @@ def test_read_config_file() -> None:
124124
jobs, jobs_nr = options_manager_mix_in.cfgfile_parser.items(section)[1]
125125
assert jobs == "jobs"
126126
assert jobs_nr == "10"
127-
128-
129-
def test_toml_with_empty_list_for_plugins(tmp_path):
130-
# This would check that there is no crash for when empty lists
131-
# passed as plugins , refer #4580
132-
config_file = tmp_path / "pyproject.toml"
133-
config_file.write_text(
134-
"""
135-
[tool.pylint]
136-
disable = "logging-not-lazy,logging-format-interpolation"
137-
load-plugins = []
138-
"""
139-
)
140-
with pytest.raises(AttributeError):
141-
check_configuration_file_reader(config_file)
142-
143-
144-
def test_toml_with_invalid_data_for_imports(tmp_path):
145-
# This would test config with invalid data for imports section
146-
# refer #4580
147-
config_file = tmp_path / "pyproject.toml"
148-
config_file.write_text(
149-
"""
150-
[tool.pylint."imports"]
151-
disable = [
152-
"logging-not-lazy",
153-
"logging-format-interpolation",
154-
]
155-
preferred-modules = { "a"="b" }
156-
"""
157-
)
158-
with pytest.raises(AttributeError):
159-
check_configuration_file_reader(config_file)
160-
161-
162-
def test_toml_with_invalid_data_for_basic(tmp_path):
163-
# This would test config with invalid data for basic section
164-
# refer #4580
165-
config_file = tmp_path / "pyproject.toml"
166-
config_file.write_text(
167-
"""
168-
[tool.pylint."basic"]
169-
disable = [
170-
"logging-not-lazy",
171-
"logging-format-interpolation",
172-
]
173-
name-group = { "a"="b" }
174-
"""
175-
)
176-
with pytest.raises(AttributeError):
177-
check_configuration_file_reader(config_file)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from pathlib import Path
2+
from typing import List
3+
from unittest.mock import patch
4+
5+
import pytest
6+
7+
from pylint import run_pylint
8+
9+
HERE = Path(__file__).parent
10+
11+
12+
@pytest.mark.parametrize(
13+
"pyproject_toml_path,expected_results",
14+
[
15+
[
16+
"issue_4580",
17+
{
18+
"basic_example.toml": {"code": 0},
19+
"correct_basic_name_group.toml": {"code": 0},
20+
"correct_import_preferred_module.toml": {"code": 0},
21+
"empty_list.toml": {
22+
"code": 2,
23+
"out": "encountered 'load-plugins' : '[]'",
24+
},
25+
"invalid_data_for_basic.toml": {
26+
"code": 2,
27+
"out": "encountered 'load-plugins' : '[]'",
28+
},
29+
"invalid_data_for_import.toml": {"code": 0},
30+
"rich_types.toml": {"code": 0},
31+
"top_level_disable.toml": {
32+
"code": 2,
33+
"out": "encountered 'disable' : 'logging-not-lazy,",
34+
},
35+
},
36+
],
37+
[
38+
"issue_4746",
39+
{
40+
"loaded_plugin_does_not_exists.toml": {
41+
"code": 2,
42+
"out": "'pylint_websockets' is impossible to load",
43+
}
44+
},
45+
],
46+
],
47+
)
48+
def test_config_pyproject_toml(pyproject_toml_path, expected_results, capsys):
49+
pyproject_toml_paths: List[Path] = (HERE / pyproject_toml_path).iterdir()
50+
for toml_path in pyproject_toml_paths:
51+
expected_result = expected_results[toml_path.name]
52+
file_to_lint = str(HERE / "file_to_lint.py")
53+
with patch(
54+
"sys.argv",
55+
["pylint", "--rcfile", str(toml_path), file_to_lint, "-r", "n"],
56+
):
57+
try:
58+
run_pylint()
59+
except SystemExit as ex:
60+
out, err = capsys.readouterr()
61+
msg = f"Wrong result with configuration {toml_path}"
62+
if "out" not in expected_result:
63+
assert not out, msg
64+
else:
65+
assert expected_result["out"] in out, msg
66+
if "err" not in expected_result:
67+
assert not err, msg
68+
else:
69+
assert expected_result["err"] in err, msg
70+
assert ex.code == expected_result.get("code"), msg

0 commit comments

Comments
 (0)