From e636848ddc41fbfd7de84b8088789672e35c0381 Mon Sep 17 00:00:00 2001 From: Georgi Georgiev <310867+chutzimir@users.noreply.github.com> Date: Tue, 23 May 2023 00:59:56 +0900 Subject: [PATCH] config: Look for configuration file in parent directories Inspired be ESLint's search, it looks for configuration files in all parent directories up until it reaches the user's home or root. closes #571 --- docs/configuration.rst | 3 ++- tests/test_cli.py | 61 ++++++++++++++++++++++++++++++++++++++++++ yamllint/cli.py | 22 ++++++++++----- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 2b27e93b..9624b496 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -15,7 +15,8 @@ If ``-c`` is not provided, yamllint will look for a configuration file in the following locations (by order of preference): - a file named ``.yamllint``, ``.yamllint.yaml``, or ``.yamllint.yml`` in the - current working directory + current working directory, or a parent directory (the search for this file is + terminated at the user's home or filesystem root) - a filename referenced by ``$YAMLLINT_CONFIG_FILE``, if set - a file named ``$XDG_CONFIG_HOME/yamllint/config`` or ``~/.config/yamllint/config``, if present diff --git a/tests/test_cli.py b/tests/test_cli.py index 713a4909..419af920 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -734,3 +734,64 @@ def test_config_file(self): self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', '')) + + def test_parent_config_file(self): + workspace = {'a/b/c/d/e/f/g/a.yml': 'hello: world\n'} + conf = ('---\n' + 'extends: relaxed\n') + + for conf_file in ('.yamllint', '.yamllint.yml', '.yamllint.yaml'): + with self.subTest(conf_file): + with temp_workspace(workspace): + with RunContext(self) as ctx: + os.chdir('a/b/c/d/e/f') + cli.run(('-f', 'parsable', '.')) + + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), + (0, './g/a.yml:1:1: [warning] missing ' + 'document start "---" (document-start)\n', + '')) + + with temp_workspace({**workspace, **{conf_file: conf}}): + with RunContext(self) as ctx: + os.chdir('a/b/c/d/e/f') + cli.run(('-f', 'parsable', '.')) + + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), + (0, '', '')) + + def test_multiple_parent_config_file(self): + workspace = {'a/b/c/3spaces.yml': 'array:\n' + ' - item\n', + 'a/b/c/4spaces.yml': 'array:\n' + ' - item\n', + 'a/.yamllint': '---\n' + 'extends: relaxed\n' + 'rules:\n' + ' indentation:\n' + ' spaces: 4\n', + } + + conf3 = ('---\n' + 'extends: relaxed\n' + 'rules:\n' + ' indentation:\n' + ' spaces: 3\n') + + with temp_workspace(workspace): + with RunContext(self) as ctx: + os.chdir('a/b/c') + cli.run(('-f', 'parsable', '.')) + + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), + (0, './3spaces.yml:2:4: [warning] wrong indentation: ' + 'expected 4 but found 3 (indentation)\n', '')) + + with temp_workspace({**workspace, **{'a/b/.yamllint.yml': conf3}}): + with RunContext(self) as ctx: + os.chdir('a/b/c') + cli.run(('-f', 'parsable', '.')) + + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), + (0, './4spaces.yml:2:5: [warning] wrong indentation: ' + 'expected 3 but found 4 (indentation)\n', '')) diff --git a/yamllint/cli.py b/yamllint/cli.py index 5574f1b4..d7fa1561 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -141,6 +141,19 @@ def show_problems(problems, file, args_format, no_warn): return max_level +def find_project_config_filepath(path='.'): + for filename in ('.yamllint', '.yamllint.yaml', '.yamllint.yml'): + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + return filepath + + if os.path.abspath(path) == os.path.abspath(os.path.expanduser('~')): + return None + if os.path.abspath(path) == os.path.abspath(os.path.join(path, '..')): + return None + return find_project_config_filepath(path=os.path.join(path, '..')) + + def run(argv=None): parser = argparse.ArgumentParser(prog=APP_NAME, description=APP_DESCRIPTION) @@ -185,6 +198,7 @@ def run(argv=None): else: user_global_config = os.path.expanduser('~/.config/yamllint/config') + project_config_filepath = find_project_config_filepath() try: if args.config_data is not None: if args.config_data != '' and ':' not in args.config_data: @@ -192,12 +206,8 @@ def run(argv=None): conf = YamlLintConfig(content=args.config_data) elif args.config_file is not None: conf = YamlLintConfig(file=args.config_file) - elif os.path.isfile('.yamllint'): - conf = YamlLintConfig(file='.yamllint') - elif os.path.isfile('.yamllint.yaml'): - conf = YamlLintConfig(file='.yamllint.yaml') - elif os.path.isfile('.yamllint.yml'): - conf = YamlLintConfig(file='.yamllint.yml') + elif project_config_filepath: + conf = YamlLintConfig(file=project_config_filepath) elif os.path.isfile(user_global_config): conf = YamlLintConfig(file=user_global_config) else: