Skip to content

Commit

Permalink
Look for per-project configuration file in parent directories
Browse files Browse the repository at this point in the history
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
  • Loading branch information
chutzimir committed May 22, 2023
1 parent 019c87d commit 80c1daa
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 7 deletions.
3 changes: 2 additions & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', ''))
22 changes: 16 additions & 6 deletions yamllint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -185,19 +198,16 @@ 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:
args.config_data = 'extends: ' + args.config_data
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:
Expand Down

0 comments on commit 80c1daa

Please sign in to comment.