Skip to content

Commit

Permalink
closes adrienverge#360, add support for ignore-from-file key option
Browse files Browse the repository at this point in the history
  • Loading branch information
ndrwnaguib committed Mar 1, 2021
1 parent 8f68248 commit 4220254
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 8 deletions.
8 changes: 8 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ Here is a more complex example:
*.ignore-trailing-spaces.yaml
ascii-art/*
You can also use the ``.gitignore`` (or any file or files) directly through:

.. code-block:: yaml
# For all rules
ignore-from-file: [.gitignore, .yamlignore] # or: ignore-from-file: .gitignore
.. note:: However, is mutually exclusive with the ``ignore`` key.

Setting the locale
------------------

Expand Down
190 changes: 189 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import unittest

from tests.common import build_temp_workspace

from yamllint.config import YamlLintConfigError
from yamllint import cli
from yamllint import config

Expand Down Expand Up @@ -385,6 +385,17 @@ def test_extend_config_override_rule_partly(self):
self.assertEqual(new.rules['empty-lines']['max-end'], 0)


class IgnoreConfig(unittest.TestCase):
def test_mutually_exclusive_ignore_keys(self):
self.assertRaises(
YamlLintConfigError,
config.YamlLintConfig, 'extends: default\n'
'ignore-from-file: .gitignore\n'
'ignore: |\n'
' *.dont-lint-me.yaml\n'
' /bin/\n')


class IgnorePathConfigTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -475,3 +486,180 @@ def test_run_with_ignored_path(self):
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
)))


class IgnoreFromFileConfigTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(IgnoreFromFileConfigTestCase, cls).setUpClass()

bad_yaml = ('---\n'
'- key: val1\n'
' key: val2\n'
'- trailing space \n'
'- lonely hyphen\n')

ignored_files = ('*.dont-lint-me.yaml\n'
'/bin/\n'
'!/bin/*.lint-me-anyway.yaml\n')
cls.wd = build_temp_workspace({
'bin/file.lint-me-anyway.yaml': bad_yaml,
'bin/file.yaml': bad_yaml,
'file-at-root.yaml': bad_yaml,
'file.dont-lint-me.yaml': bad_yaml,
'ign-dup/file.yaml': bad_yaml,
'ign-dup/sub/dir/file.yaml': bad_yaml,
'ign-trail/file.yaml': bad_yaml,
'include/ign-dup/sub/dir/file.yaml': bad_yaml,
's/s/ign-trail/file.yaml': bad_yaml,
's/s/ign-trail/s/s/file.yaml': bad_yaml,
's/s/ign-trail/s/s/file2.lint-me-anyway.yaml': bad_yaml,
'.gitignore': ignored_files,

'.yamllint': 'ignore-from-file: .gitignore\n'
'extends: default\n'
})

cls.backup_wd = os.getcwd()
os.chdir(cls.wd)

@classmethod
def tearDownClass(cls):
super(IgnoreFromFileConfigTestCase, cls).tearDownClass()

os.chdir(cls.backup_wd)

shutil.rmtree(cls.wd)

def test_run_with_ignored_from_file(self):
sys.stdout = StringIO()
with self.assertRaises(SystemExit):
cli.run(('-f', 'parsable', '.'))

out = sys.stdout.getvalue()
out = '\n'.join(sorted(out.splitlines()))

docstart = '[warning] missing document start "---" (document-start)'
keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
trailing = '[error] trailing spaces (trailing-spaces)'
hyphen = '[error] too many spaces after hyphen (hyphens)'

self.assertEqual(out, '\n'.join((
'./.yamllint:1:1: ' + docstart,
'./bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
'./bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
'./bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
'./file-at-root.yaml:3:3: ' + keydup,
'./file-at-root.yaml:4:17: ' + trailing,
'./file-at-root.yaml:5:5: ' + hyphen,
'./ign-dup/file.yaml:3:3: ' + keydup,
'./ign-dup/file.yaml:4:17: ' + trailing,
'./ign-dup/file.yaml:5:5: ' + hyphen,
'./ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
'./ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
'./ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
'./ign-trail/file.yaml:3:3: ' + keydup,
'./ign-trail/file.yaml:4:17: ' + trailing,
'./ign-trail/file.yaml:5:5: ' + hyphen,
'./include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
'./include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
'./include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
'./s/s/ign-trail/file.yaml:3:3: ' + keydup,
'./s/s/ign-trail/file.yaml:4:17: ' + trailing,
'./s/s/ign-trail/file.yaml:5:5: ' + hyphen,
'./s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
'./s/s/ign-trail/s/s/file.yaml:4:17: ' + trailing,
'./s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
)))


class IgnoreFromMultiFileConfigTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(IgnoreFromMultiFileConfigTestCase, cls).setUpClass()

bad_yaml = ('---\n'
'- key: val1\n'
' key: val2\n'
'- trailing space \n'
'- lonely hyphen\n')

ignored_files_file_1 = ('*.dont-lint-me.yaml\n'
'/bin/\n')
ignored_files_file_2 = ('!/bin/*.lint-me-anyway.yaml\n')
cls.wd = build_temp_workspace({
'bin/file.lint-me-anyway.yaml': bad_yaml,
'bin/file.yaml': bad_yaml,
'file-at-root.yaml': bad_yaml,
'file.dont-lint-me.yaml': bad_yaml,
'ign-dup/file.yaml': bad_yaml,
'ign-dup/sub/dir/file.yaml': bad_yaml,
'ign-trail/file.yaml': bad_yaml,
'include/ign-dup/sub/dir/file.yaml': bad_yaml,
's/s/ign-trail/file.yaml': bad_yaml,
's/s/ign-trail/s/s/file.yaml': bad_yaml,
's/s/ign-trail/s/s/file2.lint-me-anyway.yaml': bad_yaml,
'.gitignore': ignored_files_file_1,
'.yamlignore': ignored_files_file_2,

'.yamllint': 'ignore-from-file: [.gitignore, .yamlignore]\n'
'extends: default\n'
})

cls.backup_wd = os.getcwd()
os.chdir(cls.wd)

@classmethod
def tearDownClass(cls):
super(IgnoreFromMultiFileConfigTestCase, cls).tearDownClass()

os.chdir(cls.backup_wd)

shutil.rmtree(cls.wd)

def test_run_with_ignored_from_file(self):
sys.stdout = StringIO()
with self.assertRaises(SystemExit):
cli.run(('-f', 'parsable', '.'))

out = sys.stdout.getvalue()
out = '\n'.join(sorted(out.splitlines()))

docstart = '[warning] missing document start "---" (document-start)'
keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
trailing = '[error] trailing spaces (trailing-spaces)'
hyphen = '[error] too many spaces after hyphen (hyphens)'

self.assertEqual(out, '\n'.join((
'./.yamllint:1:1: ' + docstart,
'./bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
'./bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
'./bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
'./file-at-root.yaml:3:3: ' + keydup,
'./file-at-root.yaml:4:17: ' + trailing,
'./file-at-root.yaml:5:5: ' + hyphen,
'./ign-dup/file.yaml:3:3: ' + keydup,
'./ign-dup/file.yaml:4:17: ' + trailing,
'./ign-dup/file.yaml:5:5: ' + hyphen,
'./ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
'./ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
'./ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
'./ign-trail/file.yaml:3:3: ' + keydup,
'./ign-trail/file.yaml:4:17: ' + trailing,
'./ign-trail/file.yaml:5:5: ' + hyphen,
'./include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
'./include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
'./include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
'./s/s/ign-trail/file.yaml:3:3: ' + keydup,
'./s/s/ign-trail/file.yaml:4:17: ' + trailing,
'./s/s/ign-trail/file.yaml:5:5: ' + hyphen,
'./s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
'./s/s/ign-trail/s/s/file.yaml:4:17: ' + trailing,
'./s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
)))
49 changes: 42 additions & 7 deletions yamllint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,25 @@ def parse(self, raw_content):
except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e)

if 'ignore' in conf:
if not isinstance(conf['ignore'], str):
raise YamlLintConfigError(
'invalid config: ignore should contain file patterns')
self.ignore = pathspec.PathSpec.from_lines(
'gitwildmatch', conf['ignore'].splitlines())
if ('ignore' in conf) ^ ('ignore-from-file' in conf):

if 'ignore-from-file' in conf:
if isinstance(conf['ignore-from-file'], str):
# if str enclose in list
conf['ignore-from-file'] = [conf['ignore-from-file']]
self.ignore = pathspec.PathSpec.from_lines('gitwildmatch',
_concat_files_content(conf['ignore-from-file']).splitlines())

elif 'ignore' in conf:
if not isinstance(conf['ignore'], str):
raise YamlLintConfigError(
'invalid config: ignore should contain file patterns')
self.ignore = pathspec.PathSpec.from_lines(
'gitwildmatch', conf['ignore'].splitlines())
elif 'ignore' in conf and 'ignore-from-file' in conf:
raise YamlLintConfigError(
'invalid config: ignore and ignore-from-file keys cannot be used together'
)

if 'yaml-files' in conf:
if not (isinstance(conf['yaml-files'], list)
Expand Down Expand Up @@ -142,6 +155,11 @@ def validate_rule_conf(rule, conf):
conf['ignore'] = pathspec.PathSpec.from_lines(
'gitwildmatch', conf['ignore'].splitlines())

elif ('ignore-from-file' in conf and not (
isinstance(conf['ignore-from-file'], str) ^ isinstance(conf['ignore-from-file'], list))):
raise YamlLintConfigError(
'invalid config: ignore-from-file must filename(s), either as a list or string')

if 'level' not in conf:
conf['level'] = 'error'
elif conf['level'] not in ('error', 'warning'):
Expand All @@ -151,7 +169,7 @@ def validate_rule_conf(rule, conf):
options = getattr(rule, 'CONF', {})
options_default = getattr(rule, 'DEFAULT', {})
for optkey in conf:
if optkey in ('ignore', 'level'):
if optkey in ('ignore', 'ignore-from-file', 'level'):
continue
if optkey not in options:
raise YamlLintConfigError(
Expand Down Expand Up @@ -211,3 +229,20 @@ def get_extended_config_file(name):

# or a custom conf on filesystem?
return name


def _concat_files_content(filenames: list) -> str:
"""Concatenates file contents together
Args:
filenames (list): the filenames you would like to merge
Returns:
string represents the merged contents of all files
"""
import fileinput
content = ""
with fileinput.input(filenames) as fin:
for line in fin:
content += f"{line}\n"
return content

0 comments on commit 4220254

Please sign in to comment.