From 795c8bdbeb75d687a8044836418e035d418146ab Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Thu, 29 Feb 2024 15:01:29 +0100 Subject: [PATCH 1/3] Add a function to search for pyproject.toml in a project root --- mypy/config_parser.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index a0f93f663522..695508b0ff23 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -217,6 +217,28 @@ def split_commas(value: str) -> list[str]: ) +def _find_pyproject() -> list[str]: + """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 + """ + # We start from the parent dir, since 'pyproject.toml' is already parsed + current_dir = os.path.abspath(os.path.join(os.path.curdir, os.path.pardir)) + is_root = False + while not is_root: + for pyproject_name in defaults.PYPROJECT_CONFIG_FILES: + config_file = os.path.join(current_dir, pyproject_name) + if os.path.isfile(config_file): + return [os.path.abspath(config_file)] + parent = os.path.abspath(os.path.join(current_dir, os.path.pardir)) + is_root = current_dir == parent or any( + os.path.isdir(os.path.join(current_dir, cvs_root)) for cvs_root in (".git", ".hg") + ) + current_dir = parent + + return [] + + def parse_config_file( options: Options, set_strict_flags: Callable[[], None], @@ -236,7 +258,9 @@ def parse_config_file( if filename is not None: config_files: tuple[str, ...] = (filename,) else: - config_files_iter: Iterable[str] = map(os.path.expanduser, defaults.CONFIG_FILES) + config_files_iter: Iterable[str] = map( + os.path.expanduser, defaults.CONFIG_FILES + _find_pyproject() + ) config_files = tuple(config_files_iter) config_parser = configparser.RawConfigParser() From 55a2145f33baf31ee8c5dfb9903780eb58d09466 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Thu, 29 Feb 2024 21:15:42 +0100 Subject: [PATCH 2/3] Find pyproject.toml only once, and use it in CONFIG_FILES --- mypy/config_parser.py | 26 +------------------------- mypy/defaults.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 695508b0ff23..a0f93f663522 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -217,28 +217,6 @@ def split_commas(value: str) -> list[str]: ) -def _find_pyproject() -> list[str]: - """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 - """ - # We start from the parent dir, since 'pyproject.toml' is already parsed - current_dir = os.path.abspath(os.path.join(os.path.curdir, os.path.pardir)) - is_root = False - while not is_root: - for pyproject_name in defaults.PYPROJECT_CONFIG_FILES: - config_file = os.path.join(current_dir, pyproject_name) - if os.path.isfile(config_file): - return [os.path.abspath(config_file)] - parent = os.path.abspath(os.path.join(current_dir, os.path.pardir)) - is_root = current_dir == parent or any( - os.path.isdir(os.path.join(current_dir, cvs_root)) for cvs_root in (".git", ".hg") - ) - current_dir = parent - - return [] - - def parse_config_file( options: Options, set_strict_flags: Callable[[], None], @@ -258,9 +236,7 @@ def parse_config_file( if filename is not None: config_files: tuple[str, ...] = (filename,) else: - config_files_iter: Iterable[str] = map( - os.path.expanduser, defaults.CONFIG_FILES + _find_pyproject() - ) + config_files_iter: Iterable[str] = map(os.path.expanduser, defaults.CONFIG_FILES) config_files = tuple(config_files_iter) config_parser = configparser.RawConfigParser() diff --git a/mypy/defaults.py b/mypy/defaults.py index 6f309668d224..ed0b8d0dc6d9 100644 --- a/mypy/defaults.py +++ b/mypy/defaults.py @@ -12,9 +12,41 @@ # mypy, at least version PYTHON3_VERSION is needed. PYTHON3_VERSION_MIN: Final = (3, 8) # Keep in sync with typeshed's python support + +def find_pyproject() -> str: + """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 + + If the file is not found until the root of FS or repository, PYPROJECT_FILE is used + """ + + def is_root(current_dir: str) -> bool: + parent = os.path.join(current_dir, os.path.pardir) + return os.path.samefile(current_dir, parent) or any( + os.path.isdir(os.path.join(current_dir, cvs_root)) for cvs_root in (".git", ".hg") + ) + + # Preserve the original behavior, returning PYPROJECT_FILE if exists + if os.path.isfile(PYPROJECT_FILE) or is_root(os.path.curdir): + return PYPROJECT_FILE + + # And iterate over the tree + current_dir = os.path.pardir + while not is_root(current_dir): + config_file = os.path.join(current_dir, PYPROJECT_FILE) + if os.path.isfile(config_file): + return config_file + parent = os.path.join(current_dir, os.path.pardir) + current_dir = parent + + return PYPROJECT_FILE + + CACHE_DIR: Final = ".mypy_cache" CONFIG_FILE: Final = ["mypy.ini", ".mypy.ini"] -PYPROJECT_CONFIG_FILES: Final = ["pyproject.toml"] +PYPROJECT_FILE: Final = "pyproject.toml" +PYPROJECT_CONFIG_FILES: Final = [find_pyproject()] SHARED_CONFIG_FILES: Final = ["setup.cfg"] USER_CONFIG_FILES: Final = ["~/.config/mypy/config", "~/.mypy.ini"] if os.environ.get("XDG_CONFIG_HOME"): From c861c5703e1d0b9a6124ae6b2831ac32ec4faa99 Mon Sep 17 00:00:00 2001 From: "Mikhail f. Shiryaev" Date: Fri, 1 Mar 2024 01:52:05 +0100 Subject: [PATCH 3/3] Add a test for reading pyproject.toml recursively --- test-data/unit/cmdline.pyproject.test | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test-data/unit/cmdline.pyproject.test b/test-data/unit/cmdline.pyproject.test index 57e6facad032..e6e5f113a844 100644 --- a/test-data/unit/cmdline.pyproject.test +++ b/test-data/unit/cmdline.pyproject.test @@ -133,3 +133,38 @@ Neither is this! description = "Factory ⸻ A code generator 🏭" \[tool.mypy] [file x.py] + +[case testSearchRecursively] +# cmd: mypy x.py +[file ../pyproject.toml] +\[tool.mypy] +\[tool.mypy.overrides] +module = "x" +disallow_untyped_defs = false +[file x.py] +pass +[out] +../pyproject.toml: tool.mypy.overrides sections must be an array. Please make sure you are using double brackets like so: [[tool.mypy.overrides]] +== Return code: 0 + +[case testSearchRecursivelyStopsGit] +# cmd: mypy x.py +[file .git/test] +[file ../pyproject.toml] +\[tool.mypy] +\[tool.mypy.overrides] +module = "x" +disallow_untyped_defs = false +[file x.py] +i: int = 0 + +[case testSearchRecursivelyStopsHg] +# cmd: mypy x.py +[file .hg/test] +[file ../pyproject.toml] +\[tool.mypy] +\[tool.mypy.overrides] +module = "x" +disallow_untyped_defs = false +[file x.py] +i: int = 0