From 174a64d3aa817e860edaf72642b37773c88dc638 Mon Sep 17 00:00:00 2001 From: Jelle Helsen Date: Tue, 17 Dec 2024 20:35:32 +0100 Subject: [PATCH] fix: ignore-from-file relative to config file --- docs/configuration.rst | 1 + tests/test_config.py | 91 ++++++++++++++++++++++++++++++++++++++++++ yamllint/config.py | 20 +++++++++- 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 9624b496..d78fd995 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -227,6 +227,7 @@ or: ignore-from-file: [.gitignore, .yamlignore] .. note:: However, this is mutually exclusive with the ``ignore`` key. + Also, this filepath is considered relative to the config file. If you need to know the exact list of files that yamllint would process, without really linting them, you can use ``--list-files``: diff --git a/tests/test_config.py b/tests/test_config.py index fb570c66..258d64a3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -498,6 +498,29 @@ def test_ignore_from_file_not_exist(self): config.YamlLintConfig, 'extends: default\n' 'ignore-from-file: not_found_file\n') + def test_ignore_from_file_exist(self): + with open(os.path.join(self.wd, '.gitignore'), 'w') as f: + f.write('*.dont-lint-me.yaml\n' + '/bin/\n' + '!/bin/*.lint-me-anyway.yaml\n') + parsed = config.YamlLintConfig('extends: default\n' + 'ignore-from-file: .gitignore\n') + assert parsed.is_file_ignored("/bin/ignored") is True + + def test_ignore_from_file_from_subdir_fails_without_config_file(self): + with open(os.path.join(self.wd, '.gitignore'), 'w') as f: + f.write('*.dont-lint-me.yaml\n' + '/bin/\n' + '!/bin/*.lint-me-anyway.yaml\n') + try: + os.chdir(os.path.join(self.wd, 'bin')) + self.assertRaises( + FileNotFoundError, + config.YamlLintConfig, 'extends: default\n' + 'ignore-from-file: .gitignore\n') + finally: + os.chdir(self.wd) + def test_ignore_from_file_incorrect_type(self): self.assertRaises( YamlLintConfigError, @@ -721,6 +744,74 @@ def test_run_with_ignore_from_file(self): './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen, ))) + def test_run_in_subdir_with_ignore_from_file(self): + with open(os.path.join(self.wd, '.yamllint'), 'w') as f: + f.write('extends: default\n' + 'ignore-from-file: .gitignore\n' + 'rules:\n' + ' key-duplicates:\n' + ' ignore-from-file: .ignore-key-duplicates\n') + + with open(os.path.join(self.wd, '.gitignore'), 'w') as f: + f.write('*.dont-lint-me.yaml\n' + '/bin/\n' + '!/bin/*.lint-me-anyway.yaml\n') + + with open(os.path.join(self.wd, '.ignore-key-duplicates'), 'w') as f: + f.write('/ign-dup\n') + + sys.stdout = StringIO() + try: + os.chdir(os.path.join(self.wd, 'bin')) + with self.assertRaises(SystemExit): + cli.run(('-f', 'parsable', '.')) + finally: + os.chdir(self.wd) + + out = sys.stdout.getvalue() + out = '\n'.join(sorted(out.splitlines())) + + 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(( + './file.lint-me-anyway.yaml:3:3: ' + keydup, + './file.lint-me-anyway.yaml:4:17: ' + trailing, + './file.lint-me-anyway.yaml:5:5: ' + hyphen, + ))) + + def test_run_in_subdir_with_ignore_from_file_in_subdir(self): + if os.path.exists(os.path.join(self.wd, '.gitignore')): + os.remove(os.path.join(self.wd, '.gitignore')) + with open(os.path.join(self.wd, '.yamllint'), 'w') as f: + f.write('extends: default\n' + 'ignore-from-file: .gitignore\n' + 'rules:\n' + ' key-duplicates:\n' + ' ignore-from-file: .ignore-key-duplicates\n') + + with open(os.path.join(self.wd, 'bin', '.gitignore'), 'w') as f: + f.write('*.dont-lint-me.yaml\n' + '/bin/\n' + '!/bin/*.lint-me-anyway.yaml\n') + + with open(os.path.join(self.wd, '.ignore-key-duplicates'), 'w') as f: + f.write('/ign-dup\n') + + sys.stdout = StringIO() + sys.stderr = StringIO() + try: + os.chdir(os.path.join(self.wd, 'bin')) + with self.assertRaises(FileNotFoundError): + cli.run(('-f', 'parsable', '.')) + finally: + os.chdir(self.wd) + + out = sys.stdout.getvalue() + + self.assertEqual(out, '') + def test_run_with_ignored_from_file(self): with open(os.path.join(self.wd, '.yamllint'), 'w') as f: f.write('ignore-from-file: [.gitignore, .yamlignore]\n' diff --git a/yamllint/config.py b/yamllint/config.py index 9ce62549..03b921e7 100644 --- a/yamllint/config.py +++ b/yamllint/config.py @@ -31,6 +31,7 @@ def __init__(self, content=None, file=None): assert (content is None) ^ (file is None) self.ignore = None + self.config_dir = None self.yaml_files = pathspec.PathSpec.from_lines( 'gitwildmatch', ['*.yaml', '*.yml', '.yamllint']) @@ -38,6 +39,7 @@ def __init__(self, content=None, file=None): self.locale = None if file is not None: + self.config_dir = os.path.dirname(file) with open(file) as f: content = f.read() @@ -45,6 +47,8 @@ def __init__(self, content=None, file=None): self.validate() def is_file_ignored(self, filepath): + if self.config_dir: + filepath = os.path.relpath(filepath, start=self.config_dir) return self.ignore and self.ignore.match_file(filepath) def is_yaml_file(self, filepath): @@ -109,6 +113,11 @@ def parse(self, raw_content): raise YamlLintConfigError( 'invalid config: ignore-from-file should contain ' 'filename(s), either as a list or string') + if self.config_dir is not None: + conf['ignore-from-file'] = [ + f if os.path.basename(f) != f + else os.path.join(self.config_dir, f) + for f in conf['ignore-from-file']] with fileinput.input(conf['ignore-from-file']) as f: self.ignore = pathspec.PathSpec.from_lines('gitwildmatch', f) elif 'ignore' in conf: @@ -145,10 +154,12 @@ def validate(self): except Exception as e: raise YamlLintConfigError(f'invalid config: {e}') from e - self.rules[id] = validate_rule_conf(rule, self.rules[id]) + self.rules[id] = validate_rule_conf(rule, + self.rules[id], + self.config_dir) -def validate_rule_conf(rule, conf): +def validate_rule_conf(rule, conf, config_dir=None): if conf is False: # disable return False @@ -163,6 +174,11 @@ def validate_rule_conf(rule, conf): raise YamlLintConfigError( 'invalid config: ignore-from-file should contain ' 'valid filename(s), either as a list or string') + if config_dir is not None: + conf['ignore-from-file'] = [ + f if os.path.basename(f) != f + else os.path.join(config_dir, f) + for f in conf['ignore-from-file']] with fileinput.input(conf['ignore-from-file']) as f: conf['ignore'] = pathspec.PathSpec.from_lines( 'gitwildmatch', f)