Skip to content

Commit 3fc8c98

Browse files
committed
Can read setup.cfg and pyproject.toml files
Closes #617
1 parent 1e05190 commit 3fc8c98

File tree

8 files changed

+166
-34
lines changed

8 files changed

+166
-34
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ What's New in Pylint 2.5.0?
77

88
Release date: TBA
99

10+
* Can read config from a setup.cfg or pyproject.toml file.
11+
12+
Close #617
13+
1014

1115
What's New in Pylint 2.4.3?
1216
===========================
@@ -22,6 +26,7 @@ Release date: TBA
2226

2327
Close #3175
2428

29+
2530
What's New in Pylint 2.4.2?
2631
===========================
2732

doc/user_guide/run.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ configuration file in the following order and uses the first one it finds:
8989

9090
#. ``pylintrc`` in the current working directory
9191
#. ``.pylintrc`` in the current working directory
92+
#. ``pyproject.toml`` in the current working directory,
93+
providing it has at least one ``tool.pylint.`` section.
94+
#. ``setup.cfg`` in the current working directory,
95+
providing it has at least one ``pylint.`` section
9296
#. If the current working directory is in a Python module, Pylint searches \
9397
up the hierarchy of Python modules until it finds a ``pylintrc`` file. \
9498
This allows you to specify coding standards on a module-by-module \

doc/whatsnew/2.5.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
**************************
2+
What's New in Pylint 2.5
3+
**************************
4+
5+
:Release: 2.5
6+
:Date: TBC
7+
8+
9+
Summary -- Release highlights
10+
=============================
11+
12+
13+
New checkers
14+
============
15+
16+
17+
Other Changes
18+
=============
19+
20+
* Configuration can be read from a setup.cfg or pyproject.toml file
21+
in the current directory.
22+
A setup.cfg must prepend pylintrc section names with ``pylint.``,
23+
for example ``[pylint.MESSAGES CONTROL]``.
24+
A pyproject.toml file must prepend section names with ``tool.pylint.``,
25+
for example ``[tool.pylint.'MESSAGES CONTROL']``.
26+
These files can also be passed in on the command line.
27+

doc/whatsnew/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ High level descriptions of the most important changes between major Pylint versi
99
.. toctree::
1010
:maxdepth: 1
1111

12+
2.5.rst
1213
2.4.rst
1314
2.3.rst
1415
2.2.rst

pylint/__pkginfo__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929
if dev_version is not None:
3030
version += "-dev" + str(dev_version)
3131

32-
install_requires = ["astroid>=2.3.0,<2.4", "isort>=4.2.5,<5", "mccabe>=0.6,<0.7"]
32+
install_requires = [
33+
"astroid>=2.3.0,<2.4",
34+
"isort>=4.2.5,<5",
35+
"mccabe>=0.6,<0.7",
36+
"toml>=0.7.1",
37+
]
3338

3439
dependency_links = [] # type: ignore
3540

pylint/config.py

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import time
4747
from typing import Any, Dict, Tuple
4848

49+
import toml
50+
4951
from pylint import utils
5052

5153
USER_HOME = os.path.expanduser("~")
@@ -87,38 +89,70 @@ def save_results(results, base):
8789
print("Unable to create file %s: %s" % (data_file, ex), file=sys.stderr)
8890

8991

90-
def find_pylintrc():
91-
"""search the pylint rc file and return its path if it find it, else None
92-
"""
93-
# is there a pylint rc file in the current directory ?
94-
if os.path.exists("pylintrc"):
95-
return os.path.abspath("pylintrc")
96-
if os.path.exists(".pylintrc"):
97-
return os.path.abspath(".pylintrc")
92+
def _toml_has_config(path):
93+
with open(path, "r") as toml_handle:
94+
content = toml.load(toml_handle)
95+
try:
96+
content["tool"]["pylint"]
97+
except KeyError:
98+
return False
99+
100+
return True
101+
102+
103+
def _cfg_has_config(path):
104+
parser = configparser.ConfigParser()
105+
parser.read(path)
106+
return any(section.startswith("pylint.") for section in parser.sections())
107+
108+
109+
def find_default_config_files():
110+
"""Find all possible config files."""
111+
rc_names = ("pylintrc", ".pylintrc")
112+
config_names = rc_names + ("pyproject.toml", "setup.cfg")
113+
for config_name in config_names:
114+
if os.path.isfile(config_name):
115+
if config_name.endswith(".toml") and not _toml_has_config(config_name):
116+
continue
117+
if config_name.endswith(".cfg") and not _cfg_has_config(config_name):
118+
continue
119+
120+
yield os.path.abspath(config_name)
121+
98122
if os.path.isfile("__init__.py"):
99123
curdir = os.path.abspath(os.getcwd())
100124
while os.path.isfile(os.path.join(curdir, "__init__.py")):
101125
curdir = os.path.abspath(os.path.join(curdir, ".."))
102-
if os.path.isfile(os.path.join(curdir, "pylintrc")):
103-
return os.path.join(curdir, "pylintrc")
104-
if os.path.isfile(os.path.join(curdir, ".pylintrc")):
105-
return os.path.join(curdir, ".pylintrc")
126+
for rc_name in rc_names:
127+
rc_path = os.path.join(curdir, rc_name)
128+
if os.path.isfile(rc_path):
129+
yield rc_path
130+
106131
if "PYLINTRC" in os.environ and os.path.exists(os.environ["PYLINTRC"]):
107-
pylintrc = os.environ["PYLINTRC"]
132+
if os.path.isfile(os.environ["PYLINTRC"]):
133+
yield os.environ["PYLINTRC"]
108134
else:
109135
user_home = os.path.expanduser("~")
110-
if user_home in ("~", "/root"):
111-
pylintrc = ".pylintrc"
112-
else:
113-
pylintrc = os.path.join(user_home, ".pylintrc")
114-
if not os.path.isfile(pylintrc):
115-
pylintrc = os.path.join(user_home, ".config", "pylintrc")
116-
if not os.path.isfile(pylintrc):
117-
if os.path.isfile("/etc/pylintrc"):
118-
pylintrc = "/etc/pylintrc"
119-
else:
120-
pylintrc = None
121-
return pylintrc
136+
if user_home not in ("~", "/root"):
137+
home_rc = os.path.join(user_home, ".pylintrc")
138+
if os.path.isfile(home_rc):
139+
yield home_rc
140+
home_rc = os.path.join(user_home, ".config", "pylintrc")
141+
if os.path.isfile(home_rc):
142+
yield home_rc
143+
144+
if os.path.isfile("/etc/pylintrc"):
145+
yield "/etc/pylintrc"
146+
147+
148+
def find_pylintrc():
149+
"""search the pylint rc file and return its path if it find it, else None
150+
"""
151+
for config_file in find_default_config_files():
152+
if config_file.endswith("pylintrc"):
153+
return config_file
154+
155+
return None
122156

123157

124158
PYLINTRC = find_pylintrc()
@@ -707,14 +741,28 @@ def helpfunc(option, opt, val, p, level=helplevel):
707741
if use_config_file:
708742
parser = self.cfgfile_parser
709743

710-
# Use this encoding in order to strip the BOM marker, if any.
711-
with io.open(config_file, "r", encoding="utf_8_sig") as fp:
712-
parser.read_file(fp)
744+
if config_file.endswith(".toml"):
745+
with open(config_file, "r") as fp:
746+
content = toml.load(fp)
713747

714-
# normalize sections'title
715-
for sect, values in list(parser._sections.items()):
716-
if not sect.isupper() and values:
717-
parser._sections[sect.upper()] = values
748+
try:
749+
sections_values = content["tool"]["pylint"]
750+
except KeyError:
751+
pass
752+
else:
753+
for section, values in sections_values.items():
754+
parser._sections[section.upper()] = values
755+
else:
756+
# Use this encoding in order to strip the BOM marker, if any.
757+
with io.open(config_file, "r", encoding="utf_8_sig") as fp:
758+
parser.read_file(fp)
759+
760+
# normalize sections'title
761+
for sect, values in list(parser._sections.items()):
762+
if sect.startswith("pylint."):
763+
sect = sect[len("pylint.") :]
764+
if not sect.isupper() and values:
765+
parser._sections[sect.upper()] = values
718766

719767
if not verbose:
720768
return

pylint/lint.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,9 @@ def __init__(self, options=(), reporter=None, option_groups=(), pylintrc=None):
620620
MessagesHandlerMixIn.__init__(self)
621621
reporters.ReportsHandlerMixIn.__init__(self)
622622
super(PyLinter, self).__init__(
623-
usage=__doc__, version=full_version, config_file=pylintrc or config.PYLINTRC
623+
usage=__doc__,
624+
version=full_version,
625+
config_file=pylintrc or next(config.find_default_config_files(), None),
624626
)
625627
checkers.BaseTokenChecker.__init__(self)
626628
# provided reports

tests/test_config.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import unittest.mock
2+
3+
import pylint.lint
4+
import pytest
5+
6+
7+
def test_can_read_toml(tmp_path):
8+
config_file = tmp_path / "pyproject.toml"
9+
config_file.write_text(
10+
"[tool.pylint.'messages control']\n"
11+
"disable='all'\n"
12+
"enable='missing-module-docstring'\n"
13+
"jobs=10\n"
14+
)
15+
16+
linter = pylint.lint.PyLinter()
17+
linter.global_set_option = unittest.mock.MagicMock()
18+
linter.read_config_file(str(config_file))
19+
20+
assert linter.global_set_option.called_with("disable", "all")
21+
assert linter.global_set_option.called_with("enable", "missing-module-docstring")
22+
assert linter.global_set_option.called_with("jobs", 10)
23+
24+
25+
def test_can_read_setup_cfg(tmp_path):
26+
config_file = tmp_path / "setup.cfg"
27+
config_file.write_text(
28+
"[pylint.messages control]\n"
29+
"disable=all\n"
30+
"enable=missing-module-docstring\n"
31+
"jobs=10\n"
32+
)
33+
34+
linter = pylint.lint.PyLinter()
35+
linter.global_set_option = unittest.mock.MagicMock()
36+
linter.read_config_file(str(config_file))
37+
38+
assert linter.global_set_option.called_with("disable", "all")
39+
assert linter.global_set_option.called_with("enable", "missing-module-docstring")
40+
assert linter.global_set_option.called_with("jobs", 10)

0 commit comments

Comments
 (0)