Skip to content

Commit 2344380

Browse files
committed
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 #399
1 parent f66855b commit 2344380

File tree

5 files changed

+77
-35
lines changed

5 files changed

+77
-35
lines changed

tests/common.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1515

1616
import contextlib
17+
from io import StringIO
1718
import os
1819
import shutil
20+
import sys
1921
import tempfile
2022
import unittest
2123

@@ -54,6 +56,33 @@ def check(self, source, conf, **kwargs):
5456
self.assertEqual(real_problems, expected_problems)
5557

5658

59+
class RunContext:
60+
"""Context manager for ``cli.run()`` to capture exit code and streams."""
61+
62+
def __init__(self, case):
63+
self.stdout = self.stderr = None
64+
self._raises_ctx = case.assertRaises(SystemExit)
65+
66+
def __enter__(self):
67+
self._raises_ctx.__enter__()
68+
self.old_sys_stdout = sys.stdout
69+
self.old_sys_stderr = sys.stderr
70+
sys.stdout = self.outstream = StringIO()
71+
sys.stderr = self.errstream = StringIO()
72+
return self
73+
74+
def __exit__(self, *exc_info):
75+
self.stdout = self.outstream.getvalue()
76+
self.stderr = self.errstream.getvalue()
77+
sys.stdout = self.old_sys_stdout
78+
sys.stderr = self.old_sys_stderr
79+
return self._raises_ctx.__exit__(*exc_info)
80+
81+
@property
82+
def returncode(self):
83+
return self._raises_ctx.exception.code
84+
85+
5786
def build_temp_workspace(files):
5887
tempdir = tempfile.mkdtemp(prefix='yamllint-tests-')
5988

@@ -64,6 +93,8 @@ def build_temp_workspace(files):
6493

6594
if isinstance(content, list):
6695
os.mkdir(path)
96+
elif isinstance(content, str) and content.startswith('symlink://'):
97+
os.symlink(content[10:], path)
6798
else:
6899
mode = 'wb' if isinstance(content, bytes) else 'w'
69100
with open(path, mode) as f:

tests/test_cli.py

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,11 @@
2323
import unittest
2424
from io import StringIO
2525

26-
from tests.common import build_temp_workspace, temp_workspace
26+
from tests.common import build_temp_workspace, RunContext, temp_workspace
2727

2828
from yamllint import cli, config
2929

3030

31-
class RunContext:
32-
"""Context manager for ``cli.run()`` to capture exit code and streams."""
33-
34-
def __init__(self, case):
35-
self.stdout = self.stderr = None
36-
self._raises_ctx = case.assertRaises(SystemExit)
37-
38-
def __enter__(self):
39-
self._raises_ctx.__enter__()
40-
self.old_sys_stdout = sys.stdout
41-
self.old_sys_stderr = sys.stderr
42-
sys.stdout = self.outstream = StringIO()
43-
sys.stderr = self.errstream = StringIO()
44-
return self
45-
46-
def __exit__(self, *exc_info):
47-
self.stdout = self.outstream.getvalue()
48-
self.stderr = self.errstream.getvalue()
49-
sys.stdout = self.old_sys_stdout
50-
sys.stderr = self.old_sys_stderr
51-
return self._raises_ctx.__exit__(*exc_info)
52-
53-
@property
54-
def returncode(self):
55-
return self._raises_ctx.exception.code
56-
57-
5831
# Check system's UTF-8 availability
5932
def utf8_available():
6033
try:
@@ -112,6 +85,9 @@ def setUpClass(cls):
11285
'key: other value\n',
11386
# empty dir
11487
'empty-dir': [],
88+
# symbolic link
89+
'symlinks/file-without-yaml-extension': '42\n',
90+
'symlinks/link.yaml': 'symlink://file-without-yaml-extension',
11591
# non-YAML file
11692
'no-yaml.json': '---\n'
11793
'key: value\n',
@@ -152,6 +128,7 @@ def test_find_files_recursively(self):
152128
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
153129
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
154130
os.path.join(self.wd, 'sub/ok.yaml'),
131+
os.path.join(self.wd, 'symlinks/link.yaml'),
155132
os.path.join(self.wd, 'warn.yaml')],
156133
)
157134

@@ -189,6 +166,7 @@ def test_find_files_recursively(self):
189166
os.path.join(self.wd, 'en.yaml'),
190167
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
191168
os.path.join(self.wd, 'sub/ok.yaml'),
169+
os.path.join(self.wd, 'symlinks/link.yaml'),
192170
os.path.join(self.wd, 'warn.yaml')]
193171
)
194172

@@ -226,6 +204,8 @@ def test_find_files_recursively(self):
226204
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
227205
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
228206
os.path.join(self.wd, 'sub/ok.yaml'),
207+
os.path.join(self.wd, 'symlinks/file-without-yaml-extension'),
208+
os.path.join(self.wd, 'symlinks/link.yaml'),
229209
os.path.join(self.wd, 'warn.yaml')]
230210
)
231211

@@ -247,6 +227,8 @@ def test_find_files_recursively(self):
247227
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
248228
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
249229
os.path.join(self.wd, 'sub/ok.yaml'),
230+
os.path.join(self.wd, 'symlinks/file-without-yaml-extension'),
231+
os.path.join(self.wd, 'symlinks/link.yaml'),
250232
os.path.join(self.wd, 'warn.yaml')]
251233
)
252234

@@ -711,6 +693,7 @@ def test_run_list_files(self):
711693
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
712694
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
713695
os.path.join(self.wd, 'sub/ok.yaml'),
696+
os.path.join(self.wd, 'symlinks/link.yaml'),
714697
os.path.join(self.wd, 'warn.yaml')]
715698
)
716699

@@ -727,6 +710,7 @@ def test_run_list_files(self):
727710
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
728711
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
729712
os.path.join(self.wd, 'sub/ok.yaml'),
713+
os.path.join(self.wd, 'symlinks/link.yaml'),
730714
os.path.join(self.wd, 'warn.yaml')]
731715
)
732716

tests/test_config.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import unittest
2121
from io import StringIO
2222

23-
from tests.common import build_temp_workspace
23+
from tests.common import build_temp_workspace, RunContext
2424

2525
from yamllint import cli, config
2626
from yamllint.config import YamlLintConfigError
@@ -773,3 +773,33 @@ def test_run_with_ignored_from_file(self):
773773
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
774774
'./s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
775775
)))
776+
777+
def test_run_with_ignore_with_broken_symlink(self):
778+
wd = build_temp_workspace({
779+
'file-without-yaml-extension': '42\n',
780+
'link.yaml': 'symlink://file-without-yaml-extension',
781+
'link-404.yaml': 'symlink://file-that-does-not-exist',
782+
})
783+
backup_wd = os.getcwd()
784+
os.chdir(wd)
785+
786+
with RunContext(self) as ctx:
787+
cli.run(('-f', 'parsable', '.'))
788+
self.assertNotEqual(ctx.returncode, 0)
789+
790+
with open(os.path.join(wd, '.yamllint'), 'w') as f:
791+
f.write('extends: default\n'
792+
'ignore: |\n'
793+
' *404.yaml\n')
794+
with RunContext(self) as ctx:
795+
cli.run(('-f', 'parsable', '.'))
796+
self.assertEqual(ctx.returncode, 0)
797+
docstart = '[warning] missing document start "---" (document-start)'
798+
out = '\n'.join(sorted(ctx.stdout.splitlines()))
799+
self.assertEqual(out, '\n'.join((
800+
'./.yamllint:1:1: ' + docstart,
801+
'./link.yaml:1:1: ' + docstart,
802+
)))
803+
804+
os.chdir(backup_wd)
805+
shutil.rmtree(wd)

yamllint/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ def find_files_recursively(items, conf):
3030
for root, _dirnames, filenames in os.walk(item):
3131
for f in filenames:
3232
filepath = os.path.join(root, f)
33-
if conf.is_yaml_file(filepath):
33+
if (conf.is_yaml_file(filepath) and
34+
not conf.is_file_ignored(filepath)):
3435
yield filepath
3536
else:
3637
yield item
@@ -209,8 +210,7 @@ def run(argv=None):
209210

210211
if args.list_files:
211212
for file in find_files_recursively(args.files, conf):
212-
if not conf.is_file_ignored(file):
213-
print(file)
213+
print(file)
214214
sys.exit(0)
215215

216216
max_level = 0

yamllint/linter.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,6 @@ def run(input, conf, filepath=None):
222222
:param input: buffer, string or stream to read from
223223
:param conf: yamllint configuration object
224224
"""
225-
if filepath is not None and conf.is_file_ignored(filepath):
226-
return ()
227-
228225
if isinstance(input, (bytes, str)):
229226
return _run(input, conf, filepath)
230227
elif isinstance(input, io.IOBase):

0 commit comments

Comments
 (0)