From f8288842e58a8b6007ff15ab380c84b00eb9b7a3 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Sun, 30 Apr 2023 22:48:34 +0200 Subject: Search for pyproject.toml config file in parent dirs (#7163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pierre Sassoulas Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> --- .pyenchant_pylint_custom_dict.txt | 3 ++ doc/user_guide/usage/run.rst | 3 ++ doc/whatsnew/fragments/7163.other | 3 ++ pylint/config/find_default_config_files.py | 30 ++++++++++++++++++- tests/config/test_find_default_config_files.py | 41 ++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 doc/whatsnew/fragments/7163.other diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt index 507babde2..6cd91e1ba 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/.pyenchant_pylint_custom_dict.txt @@ -261,6 +261,7 @@ pylint pylintdict pylintrc pylint's +pyproject pypy pyreverse pytest @@ -318,6 +319,8 @@ subtree supcls superclass symilar +symlink +symlinks sys tbump tempfile diff --git a/doc/user_guide/usage/run.rst b/doc/user_guide/usage/run.rst index b9dfedc88..7e6e1a830 100644 --- a/doc/user_guide/usage/run.rst +++ b/doc/user_guide/usage/run.rst @@ -105,6 +105,9 @@ configuration file in the following order and uses the first one it finds: providing it has at least one ``pylint.`` section #. ``tox.ini`` in the current working directory, providing it has at least one ``pylint.`` section +#. Pylint will search for the ``pyproject.toml`` file up the directories hierarchy + unless it's found, or a ``.git``/``.hg`` directory is found, or the file system root + is approached. #. If the current working directory is in a Python package, Pylint searches \ up the hierarchy of Python packages until it finds a ``pylintrc`` file. \ This allows you to specify coding standards on a module-by-module \ diff --git a/doc/whatsnew/fragments/7163.other b/doc/whatsnew/fragments/7163.other new file mode 100644 index 000000000..93f731aae --- /dev/null +++ b/doc/whatsnew/fragments/7163.other @@ -0,0 +1,3 @@ +Search for ``pyproject.toml`` recursively in parent directories up to a project or file system root. + +Refs #7163, Closes #3289 diff --git a/pylint/config/find_default_config_files.py b/pylint/config/find_default_config_files.py index a121b32a3..3b03f6357 100644 --- a/pylint/config/find_default_config_files.py +++ b/pylint/config/find_default_config_files.py @@ -16,7 +16,28 @@ else: import tomli as tomllib RC_NAMES = (Path("pylintrc"), Path(".pylintrc")) -CONFIG_NAMES = (*RC_NAMES, Path("pyproject.toml"), Path("setup.cfg")) +PYPROJECT_NAME = Path("pyproject.toml") +CONFIG_NAMES = (*RC_NAMES, PYPROJECT_NAME, Path("setup.cfg")) + + +def _find_pyproject() -> Path: + """Search for file pyproject.toml in the parent directories recursively. + + It resolves symlinks, so if there is any symlink up in the tree, it does not respect them + """ + current_dir = Path.cwd().resolve() + is_root = False + while not is_root: + if (current_dir / PYPROJECT_NAME).is_file(): + return current_dir / PYPROJECT_NAME + is_root = ( + current_dir == current_dir.parent + or (current_dir / ".git").is_dir() + or (current_dir / ".hg").is_dir() + ) + current_dir = current_dir.parent + + return current_dir def _toml_has_config(path: Path | str) -> bool: @@ -99,6 +120,13 @@ def find_default_config_files() -> Iterator[Path]: except OSError: pass + try: + parent_pyproject = _find_pyproject() + if parent_pyproject.is_file() and _toml_has_config(parent_pyproject): + yield parent_pyproject.resolve() + except OSError: + pass + try: yield from _find_config_in_home_or_environment() except OSError: diff --git a/tests/config/test_find_default_config_files.py b/tests/config/test_find_default_config_files.py index f78f640b2..0b513a3d5 100644 --- a/tests/config/test_find_default_config_files.py +++ b/tests/config/test_find_default_config_files.py @@ -129,6 +129,47 @@ def test_pylintrc_parentdir() -> None: assert next(config.find_default_config_files()) == expected +@pytest.mark.usefixtures("pop_pylintrc") +def test_pyproject_toml_parentdir() -> None: + """Test the search of pyproject.toml file in parent directories""" + with tempdir() as chroot: + with fake_home(): + chroot_path = Path(chroot) + files = [ + "pyproject.toml", + "git/pyproject.toml", + "git/a/pyproject.toml", + "git/a/.git", + "git/a/b/c/__init__.py", + "hg/pyproject.toml", + "hg/a/pyproject.toml", + "hg/a/.hg", + "hg/a/b/c/__init__.py", + "none/sub/__init__.py", + ] + testutils.create_files(files) + for config_file in files: + if config_file.endswith("pyproject.toml"): + with open(config_file, "w", encoding="utf-8") as fd: + fd.write('[tool.pylint."messages control"]\n') + results = { + "": chroot_path / "pyproject.toml", + "git": chroot_path / "git" / "pyproject.toml", + "git/a": chroot_path / "git" / "a" / "pyproject.toml", + "git/a/b": chroot_path / "git" / "a" / "pyproject.toml", + "git/a/b/c": chroot_path / "git" / "a" / "pyproject.toml", + "hg": chroot_path / "hg" / "pyproject.toml", + "hg/a": chroot_path / "hg" / "a" / "pyproject.toml", + "hg/a/b": chroot_path / "hg" / "a" / "pyproject.toml", + "hg/a/b/c": chroot_path / "hg" / "a" / "pyproject.toml", + "none": chroot_path / "pyproject.toml", + "none/sub": chroot_path / "pyproject.toml", + } + for basedir, expected in results.items(): + os.chdir(chroot_path / basedir) + assert next(config.find_default_config_files(), None) == expected + + @pytest.mark.usefixtures("pop_pylintrc") def test_pylintrc_parentdir_no_package() -> None: """Test that we don't find a pylintrc in sub-packages.""" -- cgit v1.2.1