mirror of
https://github.com/adrienverge/yamllint.git
synced 2025-02-25 18:55:20 -06:00
cli: Cleanly skip broken symlinks that are ignored
Before this commit, yamllint would output "[Errno 2] No such file or directory" when running on a directory which contained a broken symbolic link, even if the file is set to be ignored in yamllint configuration. This commit fixes that, and adds corresponding tests. As a side effect this changes `yamllint.linter.run(stream, config)`, so tools that would use this API need to filter ignored files beforehand. Fixes https://github.com/adrienverge/yamllint/issues/399
This commit is contained in:
@@ -14,8 +14,10 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from io import StringIO
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -54,6 +56,33 @@ class RuleTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(real_problems, expected_problems)
|
self.assertEqual(real_problems, expected_problems)
|
||||||
|
|
||||||
|
|
||||||
|
class RunContext:
|
||||||
|
"""Context manager for ``cli.run()`` to capture exit code and streams."""
|
||||||
|
|
||||||
|
def __init__(self, case):
|
||||||
|
self.stdout = self.stderr = None
|
||||||
|
self._raises_ctx = case.assertRaises(SystemExit)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._raises_ctx.__enter__()
|
||||||
|
self.old_sys_stdout = sys.stdout
|
||||||
|
self.old_sys_stderr = sys.stderr
|
||||||
|
sys.stdout = self.outstream = StringIO()
|
||||||
|
sys.stderr = self.errstream = StringIO()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc_info):
|
||||||
|
self.stdout = self.outstream.getvalue()
|
||||||
|
self.stderr = self.errstream.getvalue()
|
||||||
|
sys.stdout = self.old_sys_stdout
|
||||||
|
sys.stderr = self.old_sys_stderr
|
||||||
|
return self._raises_ctx.__exit__(*exc_info)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def returncode(self):
|
||||||
|
return self._raises_ctx.exception.code
|
||||||
|
|
||||||
|
|
||||||
def build_temp_workspace(files):
|
def build_temp_workspace(files):
|
||||||
tempdir = tempfile.mkdtemp(prefix='yamllint-tests-')
|
tempdir = tempfile.mkdtemp(prefix='yamllint-tests-')
|
||||||
|
|
||||||
@@ -64,6 +93,8 @@ def build_temp_workspace(files):
|
|||||||
|
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
|
elif isinstance(content, str) and content.startswith('symlink://'):
|
||||||
|
os.symlink(content[10:], path)
|
||||||
else:
|
else:
|
||||||
mode = 'wb' if isinstance(content, bytes) else 'w'
|
mode = 'wb' if isinstance(content, bytes) else 'w'
|
||||||
with open(path, mode) as f:
|
with open(path, mode) as f:
|
||||||
|
|||||||
@@ -23,38 +23,11 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from tests.common import build_temp_workspace, temp_workspace
|
from tests.common import build_temp_workspace, RunContext, temp_workspace
|
||||||
|
|
||||||
from yamllint import cli, config
|
from yamllint import cli, config
|
||||||
|
|
||||||
|
|
||||||
class RunContext:
|
|
||||||
"""Context manager for ``cli.run()`` to capture exit code and streams."""
|
|
||||||
|
|
||||||
def __init__(self, case):
|
|
||||||
self.stdout = self.stderr = None
|
|
||||||
self._raises_ctx = case.assertRaises(SystemExit)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self._raises_ctx.__enter__()
|
|
||||||
self.old_sys_stdout = sys.stdout
|
|
||||||
self.old_sys_stderr = sys.stderr
|
|
||||||
sys.stdout = self.outstream = StringIO()
|
|
||||||
sys.stderr = self.errstream = StringIO()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *exc_info):
|
|
||||||
self.stdout = self.outstream.getvalue()
|
|
||||||
self.stderr = self.errstream.getvalue()
|
|
||||||
sys.stdout = self.old_sys_stdout
|
|
||||||
sys.stderr = self.old_sys_stderr
|
|
||||||
return self._raises_ctx.__exit__(*exc_info)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def returncode(self):
|
|
||||||
return self._raises_ctx.exception.code
|
|
||||||
|
|
||||||
|
|
||||||
# Check system's UTF-8 availability
|
# Check system's UTF-8 availability
|
||||||
def utf8_available():
|
def utf8_available():
|
||||||
try:
|
try:
|
||||||
@@ -112,6 +85,9 @@ class CommandLineTestCase(unittest.TestCase):
|
|||||||
'key: other value\n',
|
'key: other value\n',
|
||||||
# empty dir
|
# empty dir
|
||||||
'empty-dir': [],
|
'empty-dir': [],
|
||||||
|
# symbolic link
|
||||||
|
'symlinks/file-without-yaml-extension': '42\n',
|
||||||
|
'symlinks/link.yaml': 'symlink://file-without-yaml-extension',
|
||||||
# non-YAML file
|
# non-YAML file
|
||||||
'no-yaml.json': '---\n'
|
'no-yaml.json': '---\n'
|
||||||
'key: value\n',
|
'key: value\n',
|
||||||
@@ -152,6 +128,7 @@ class CommandLineTestCase(unittest.TestCase):
|
|||||||
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
||||||
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
||||||
os.path.join(self.wd, 'sub/ok.yaml'),
|
os.path.join(self.wd, 'sub/ok.yaml'),
|
||||||
|
os.path.join(self.wd, 'symlinks/link.yaml'),
|
||||||
os.path.join(self.wd, 'warn.yaml')],
|
os.path.join(self.wd, 'warn.yaml')],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -189,6 +166,7 @@ class CommandLineTestCase(unittest.TestCase):
|
|||||||
os.path.join(self.wd, 'en.yaml'),
|
os.path.join(self.wd, 'en.yaml'),
|
||||||
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
||||||
os.path.join(self.wd, 'sub/ok.yaml'),
|
os.path.join(self.wd, 'sub/ok.yaml'),
|
||||||
|
os.path.join(self.wd, 'symlinks/link.yaml'),
|
||||||
os.path.join(self.wd, 'warn.yaml')]
|
os.path.join(self.wd, 'warn.yaml')]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,6 +204,8 @@ class CommandLineTestCase(unittest.TestCase):
|
|||||||
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
||||||
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
|
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
|
||||||
os.path.join(self.wd, 'sub/ok.yaml'),
|
os.path.join(self.wd, 'sub/ok.yaml'),
|
||||||
|
os.path.join(self.wd, 'symlinks/file-without-yaml-extension'),
|
||||||
|
os.path.join(self.wd, 'symlinks/link.yaml'),
|
||||||
os.path.join(self.wd, 'warn.yaml')]
|
os.path.join(self.wd, 'warn.yaml')]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -247,6 +227,8 @@ class CommandLineTestCase(unittest.TestCase):
|
|||||||
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
||||||
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
|
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
|
||||||
os.path.join(self.wd, 'sub/ok.yaml'),
|
os.path.join(self.wd, 'sub/ok.yaml'),
|
||||||
|
os.path.join(self.wd, 'symlinks/file-without-yaml-extension'),
|
||||||
|
os.path.join(self.wd, 'symlinks/link.yaml'),
|
||||||
os.path.join(self.wd, 'warn.yaml')]
|
os.path.join(self.wd, 'warn.yaml')]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -711,6 +693,7 @@ class CommandLineTestCase(unittest.TestCase):
|
|||||||
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
||||||
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
|
||||||
os.path.join(self.wd, 'sub/ok.yaml'),
|
os.path.join(self.wd, 'sub/ok.yaml'),
|
||||||
|
os.path.join(self.wd, 'symlinks/link.yaml'),
|
||||||
os.path.join(self.wd, 'warn.yaml')]
|
os.path.join(self.wd, 'warn.yaml')]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -727,6 +710,7 @@ class CommandLineTestCase(unittest.TestCase):
|
|||||||
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
|
||||||
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
|
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
|
||||||
os.path.join(self.wd, 'sub/ok.yaml'),
|
os.path.join(self.wd, 'sub/ok.yaml'),
|
||||||
|
os.path.join(self.wd, 'symlinks/link.yaml'),
|
||||||
os.path.join(self.wd, 'warn.yaml')]
|
os.path.join(self.wd, 'warn.yaml')]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from tests.common import build_temp_workspace
|
from tests.common import build_temp_workspace, RunContext
|
||||||
|
|
||||||
from yamllint import cli, config
|
from yamllint import cli, config
|
||||||
from yamllint.config import YamlLintConfigError
|
from yamllint.config import YamlLintConfigError
|
||||||
@@ -773,3 +773,33 @@ class IgnoreConfigTestCase(unittest.TestCase):
|
|||||||
'./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:4:17: ' + trailing,
|
||||||
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
|
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
def test_run_with_ignore_with_broken_symlink(self):
|
||||||
|
wd = build_temp_workspace({
|
||||||
|
'file-without-yaml-extension': '42\n',
|
||||||
|
'link.yaml': 'symlink://file-without-yaml-extension',
|
||||||
|
'link-404.yaml': 'symlink://file-that-does-not-exist',
|
||||||
|
})
|
||||||
|
backup_wd = os.getcwd()
|
||||||
|
os.chdir(wd)
|
||||||
|
|
||||||
|
with RunContext(self) as ctx:
|
||||||
|
cli.run(('-f', 'parsable', '.'))
|
||||||
|
self.assertNotEqual(ctx.returncode, 0)
|
||||||
|
|
||||||
|
with open(os.path.join(wd, '.yamllint'), 'w') as f:
|
||||||
|
f.write('extends: default\n'
|
||||||
|
'ignore: |\n'
|
||||||
|
' *404.yaml\n')
|
||||||
|
with RunContext(self) as ctx:
|
||||||
|
cli.run(('-f', 'parsable', '.'))
|
||||||
|
self.assertEqual(ctx.returncode, 0)
|
||||||
|
docstart = '[warning] missing document start "---" (document-start)'
|
||||||
|
out = '\n'.join(sorted(ctx.stdout.splitlines()))
|
||||||
|
self.assertEqual(out, '\n'.join((
|
||||||
|
'./.yamllint:1:1: ' + docstart,
|
||||||
|
'./link.yaml:1:1: ' + docstart,
|
||||||
|
)))
|
||||||
|
|
||||||
|
os.chdir(backup_wd)
|
||||||
|
shutil.rmtree(wd)
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ def find_files_recursively(items, conf):
|
|||||||
for root, _dirnames, filenames in os.walk(item):
|
for root, _dirnames, filenames in os.walk(item):
|
||||||
for f in filenames:
|
for f in filenames:
|
||||||
filepath = os.path.join(root, f)
|
filepath = os.path.join(root, f)
|
||||||
if conf.is_yaml_file(filepath):
|
if (conf.is_yaml_file(filepath) and
|
||||||
|
not conf.is_file_ignored(filepath)):
|
||||||
yield filepath
|
yield filepath
|
||||||
else:
|
else:
|
||||||
yield item
|
yield item
|
||||||
@@ -209,8 +210,7 @@ def run(argv=None):
|
|||||||
|
|
||||||
if args.list_files:
|
if args.list_files:
|
||||||
for file in find_files_recursively(args.files, conf):
|
for file in find_files_recursively(args.files, conf):
|
||||||
if not conf.is_file_ignored(file):
|
print(file)
|
||||||
print(file)
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
max_level = 0
|
max_level = 0
|
||||||
|
|||||||
@@ -222,9 +222,6 @@ def run(input, conf, filepath=None):
|
|||||||
:param input: buffer, string or stream to read from
|
:param input: buffer, string or stream to read from
|
||||||
:param conf: yamllint configuration object
|
:param conf: yamllint configuration object
|
||||||
"""
|
"""
|
||||||
if filepath is not None and conf.is_file_ignored(filepath):
|
|
||||||
return ()
|
|
||||||
|
|
||||||
if isinstance(input, (bytes, str)):
|
if isinstance(input, (bytes, str)):
|
||||||
return _run(input, conf, filepath)
|
return _run(input, conf, filepath)
|
||||||
elif isinstance(input, io.IOBase):
|
elif isinstance(input, io.IOBase):
|
||||||
|
|||||||
Reference in New Issue
Block a user