diff --git a/ChangeLog b/ChangeLog index 409dfec8f6..a22e2ce6f9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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? =========================== @@ -22,6 +26,7 @@ Release date: TBA Close #3175 + What's New in Pylint 2.4.2? =========================== diff --git a/doc/user_guide/run.rst b/doc/user_guide/run.rst index 338a070645..d8268c36d4 100644 --- a/doc/user_guide/run.rst +++ b/doc/user_guide/run.rst @@ -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 \ diff --git a/doc/whatsnew/2.5.rst b/doc/whatsnew/2.5.rst new file mode 100644 index 0000000000..a1d97fb6ca --- /dev/null +++ b/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. + diff --git a/doc/whatsnew/index.rst b/doc/whatsnew/index.rst index e7faa7cc76..542b9743b0 100644 --- a/doc/whatsnew/index.rst +++ b/doc/whatsnew/index.rst @@ -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 diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index bfca25d80c..cc3c14a1a2 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -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 diff --git a/pylint/config.py b/pylint/config.py index 0925575b44..219bc6b78e 100644 --- a/pylint/config.py +++ b/pylint/config.py @@ -46,6 +46,8 @@ import time from typing import Any, Dict, Tuple +import toml + from pylint import utils USER_HOME = os.path.expanduser("~") @@ -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() @@ -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 diff --git a/pylint/lint.py b/pylint/lint.py index a98970b782..095eb0e2b5 100644 --- a/pylint/lint.py +++ b/pylint/lint.py @@ -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 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000000..4d79a6a7fb --- /dev/null +++ b/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)