Skip to content

Commit

Permalink
Can read setup.cfg and pyproject.toml files
Browse files Browse the repository at this point in the history
Closes #617
  • Loading branch information
AWhetter committed Oct 10, 2019
1 parent 1e05190 commit 3fc8c98
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 34 deletions.
5 changes: 5 additions & 0 deletions ChangeLog
Expand Up @@ -7,6 +7,10 @@ What's New in Pylint 2.5.0?

Release date: TBA

* Can read config from a setup.cfg or pyproject.toml file.

Close #617


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

Close #3175


What's New in Pylint 2.4.2?
===========================

Expand Down
4 changes: 4 additions & 0 deletions doc/user_guide/run.rst
Expand Up @@ -89,6 +89,10 @@ configuration file in the following order and uses the first one it finds:

#. ``pylintrc`` in the current working directory
#. ``.pylintrc`` in the current working directory
#. ``pyproject.toml`` in the current working directory,
providing it has at least one ``tool.pylint.`` section.
#. ``setup.cfg`` in the current working directory,
providing it has at least one ``pylint.`` section
#. If the current working directory is in a Python module, Pylint searches \
up the hierarchy of Python modules until it finds a ``pylintrc`` file. \
This allows you to specify coding standards on a module-by-module \
Expand Down
27 changes: 27 additions & 0 deletions doc/whatsnew/2.5.rst
@@ -0,0 +1,27 @@
**************************
What's New in Pylint 2.5
**************************

:Release: 2.5
:Date: TBC


Summary -- Release highlights
=============================


New checkers
============


Other Changes
=============

* Configuration can be read from a setup.cfg or pyproject.toml file
in the current directory.
A setup.cfg must prepend pylintrc section names with ``pylint.``,
for example ``[pylint.MESSAGES CONTROL]``.
A pyproject.toml file must prepend section names with ``tool.pylint.``,
for example ``[tool.pylint.'MESSAGES CONTROL']``.
These files can also be passed in on the command line.

1 change: 1 addition & 0 deletions doc/whatsnew/index.rst
Expand Up @@ -9,6 +9,7 @@ High level descriptions of the most important changes between major Pylint versi
.. toctree::
:maxdepth: 1

2.5.rst
2.4.rst
2.3.rst
2.2.rst
Expand Down
7 changes: 6 additions & 1 deletion pylint/__pkginfo__.py
Expand Up @@ -29,7 +29,12 @@
if dev_version is not None:
version += "-dev" + str(dev_version)

install_requires = ["astroid>=2.3.0,<2.4", "isort>=4.2.5,<5", "mccabe>=0.6,<0.7"]
install_requires = [
"astroid>=2.3.0,<2.4",
"isort>=4.2.5,<5",
"mccabe>=0.6,<0.7",
"toml>=0.7.1",
]

dependency_links = [] # type: ignore

Expand Down
112 changes: 80 additions & 32 deletions pylint/config.py
Expand Up @@ -46,6 +46,8 @@
import time
from typing import Any, Dict, Tuple

import toml

from pylint import utils

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


def find_pylintrc():
"""search the pylint rc file and return its path if it find it, else None
"""
# is there a pylint rc file in the current directory ?
if os.path.exists("pylintrc"):
return os.path.abspath("pylintrc")
if os.path.exists(".pylintrc"):
return os.path.abspath(".pylintrc")
def _toml_has_config(path):
with open(path, "r") as toml_handle:
content = toml.load(toml_handle)
try:
content["tool"]["pylint"]
except KeyError:
return False

return True


def _cfg_has_config(path):
parser = configparser.ConfigParser()
parser.read(path)
return any(section.startswith("pylint.") for section in parser.sections())


def find_default_config_files():
"""Find all possible config files."""
rc_names = ("pylintrc", ".pylintrc")
config_names = rc_names + ("pyproject.toml", "setup.cfg")
for config_name in config_names:
if os.path.isfile(config_name):
if config_name.endswith(".toml") and not _toml_has_config(config_name):
continue
if config_name.endswith(".cfg") and not _cfg_has_config(config_name):
continue

yield os.path.abspath(config_name)

if os.path.isfile("__init__.py"):
curdir = os.path.abspath(os.getcwd())
while os.path.isfile(os.path.join(curdir, "__init__.py")):
curdir = os.path.abspath(os.path.join(curdir, ".."))
if os.path.isfile(os.path.join(curdir, "pylintrc")):
return os.path.join(curdir, "pylintrc")
if os.path.isfile(os.path.join(curdir, ".pylintrc")):
return os.path.join(curdir, ".pylintrc")
for rc_name in rc_names:
rc_path = os.path.join(curdir, rc_name)
if os.path.isfile(rc_path):
yield rc_path

if "PYLINTRC" in os.environ and os.path.exists(os.environ["PYLINTRC"]):
pylintrc = os.environ["PYLINTRC"]
if os.path.isfile(os.environ["PYLINTRC"]):
yield os.environ["PYLINTRC"]
else:
user_home = os.path.expanduser("~")
if user_home in ("~", "/root"):
pylintrc = ".pylintrc"
else:
pylintrc = os.path.join(user_home, ".pylintrc")
if not os.path.isfile(pylintrc):
pylintrc = os.path.join(user_home, ".config", "pylintrc")
if not os.path.isfile(pylintrc):
if os.path.isfile("/etc/pylintrc"):
pylintrc = "/etc/pylintrc"
else:
pylintrc = None
return pylintrc
if user_home not in ("~", "/root"):
home_rc = os.path.join(user_home, ".pylintrc")
if os.path.isfile(home_rc):
yield home_rc
home_rc = os.path.join(user_home, ".config", "pylintrc")
if os.path.isfile(home_rc):
yield home_rc

if os.path.isfile("/etc/pylintrc"):
yield "/etc/pylintrc"


def find_pylintrc():
"""search the pylint rc file and return its path if it find it, else None
"""
for config_file in find_default_config_files():
if config_file.endswith("pylintrc"):
return config_file

return None


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

# Use this encoding in order to strip the BOM marker, if any.
with io.open(config_file, "r", encoding="utf_8_sig") as fp:
parser.read_file(fp)
if config_file.endswith(".toml"):
with open(config_file, "r") as fp:
content = toml.load(fp)

# normalize sections'title
for sect, values in list(parser._sections.items()):
if not sect.isupper() and values:
parser._sections[sect.upper()] = values
try:
sections_values = content["tool"]["pylint"]
except KeyError:
pass
else:
for section, values in sections_values.items():
parser._sections[section.upper()] = values
else:
# Use this encoding in order to strip the BOM marker, if any.
with io.open(config_file, "r", encoding="utf_8_sig") as fp:
parser.read_file(fp)

# normalize sections'title
for sect, values in list(parser._sections.items()):
if sect.startswith("pylint."):
sect = sect[len("pylint.") :]
if not sect.isupper() and values:
parser._sections[sect.upper()] = values

if not verbose:
return
Expand Down
4 changes: 3 additions & 1 deletion pylint/lint.py
Expand Up @@ -620,7 +620,9 @@ def __init__(self, options=(), reporter=None, option_groups=(), pylintrc=None):
MessagesHandlerMixIn.__init__(self)
reporters.ReportsHandlerMixIn.__init__(self)
super(PyLinter, self).__init__(
usage=__doc__, version=full_version, config_file=pylintrc or config.PYLINTRC
usage=__doc__,
version=full_version,
config_file=pylintrc or next(config.find_default_config_files(), None),
)
checkers.BaseTokenChecker.__init__(self)
# provided reports
Expand Down
40 changes: 40 additions & 0 deletions tests/test_config.py
@@ -0,0 +1,40 @@
import unittest.mock

import pylint.lint
import pytest


def test_can_read_toml(tmp_path):
config_file = tmp_path / "pyproject.toml"
config_file.write_text(
"[tool.pylint.'messages control']\n"
"disable='all'\n"
"enable='missing-module-docstring'\n"
"jobs=10\n"
)

linter = pylint.lint.PyLinter()
linter.global_set_option = unittest.mock.MagicMock()
linter.read_config_file(str(config_file))

assert linter.global_set_option.called_with("disable", "all")
assert linter.global_set_option.called_with("enable", "missing-module-docstring")
assert linter.global_set_option.called_with("jobs", 10)


def test_can_read_setup_cfg(tmp_path):
config_file = tmp_path / "setup.cfg"
config_file.write_text(
"[pylint.messages control]\n"
"disable=all\n"
"enable=missing-module-docstring\n"
"jobs=10\n"
)

linter = pylint.lint.PyLinter()
linter.global_set_option = unittest.mock.MagicMock()
linter.read_config_file(str(config_file))

assert linter.global_set_option.called_with("disable", "all")
assert linter.global_set_option.called_with("enable", "missing-module-docstring")
assert linter.global_set_option.called_with("jobs", 10)

0 comments on commit 3fc8c98

Please sign in to comment.