From fd86455076d97210cf2c8e5837f8731d3c9c721c Mon Sep 17 00:00:00 2001 From: dhutty Date: Fri, 3 Jan 2020 03:29:33 -0500 Subject: [PATCH 1/8] CI: Disable building on Python 3.4 As can be seen in https://travis-ci.org/adrienverge/yamllint/builds/631325436?utm_source=github_status&utm_medium=notification The dependency, pathspec, requires Python '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*' but the running Python is 3.4.8 This commit stops Travis building yamllint against 3.4 so that CI can pass again. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 58dd54a..9c0882a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069) language: python python: - 2.7 - - 3.4 - 3.5 - 3.6 - 3.7 From 734d5d5f736eb9d31c111ae8a0ca834d96c51bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 3 Jan 2020 09:26:09 +0100 Subject: [PATCH 2/8] CI: Run tests on Python 3.8 Python 3.8 was released in October 2019. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9c0882a..44ea648 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - 3.5 - 3.6 - 3.7 + - 3.8 - nightly install: - pip install pyyaml coveralls flake8 flake8-import-order doc8 From 044c7f02482d1d45149349da0dba9c2b4b62347b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 17 Jan 2020 15:56:26 +0100 Subject: [PATCH 3/8] cli: Test unicode chars in paths too --- tests/test_cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 4244dc8..25c8a14 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -59,7 +59,7 @@ class CommandLineTestCase(unittest.TestCase): 'no-yaml.json': '---\n' 'key: value\n', # non-ASCII chars - 'non-ascii/utf-8': ( + 'non-ascii/éçäγλνπ¥/utf-8': ( u'---\n' u'- hétérogénéité\n' u'# 19.99 €\n' @@ -142,7 +142,7 @@ class CommandLineTestCase(unittest.TestCase): [os.path.join(self.wd, 'a.yaml'), os.path.join(self.wd, 'empty.yml'), os.path.join(self.wd, 'no-yaml.json'), - os.path.join(self.wd, 'non-ascii/utf-8'), + os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'), 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, 'warn.yaml')] @@ -158,7 +158,7 @@ class CommandLineTestCase(unittest.TestCase): [os.path.join(self.wd, 'a.yaml'), os.path.join(self.wd, 'empty.yml'), os.path.join(self.wd, 'no-yaml.json'), - os.path.join(self.wd, 'non-ascii/utf-8'), + os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'), 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, 'warn.yaml')] @@ -170,7 +170,7 @@ class CommandLineTestCase(unittest.TestCase): ' - \'**/utf-8\'\n') self.assertEqual( sorted(cli.find_files_recursively([self.wd], conf)), - [os.path.join(self.wd, 'non-ascii/utf-8')] + [os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8')] ) def test_run_with_bad_arguments(self): @@ -368,7 +368,7 @@ class CommandLineTestCase(unittest.TestCase): self.assertEqual(err, '') def test_run_non_ascii_file(self): - file = os.path.join(self.wd, 'non-ascii', 'utf-8') + file = os.path.join(self.wd, 'non-ascii', 'éçäγλνπ¥', 'utf-8') # Make sure the default localization conditions on this "system" # support UTF-8 encoding. From 5b049e42297942dc80853e2b2e5c298a40ea324c Mon Sep 17 00:00:00 2001 From: Martin Packman Date: Mon, 10 Feb 2020 20:43:10 +0000 Subject: [PATCH 4/8] Add RunContext helper for cli tests Single context manager that includes exit code and output streams. Use new RunContext throughout test_cli. Largely non-functional change, saving some repetition of setup. Also improve some failures by bundling multiple assertions into one. --- tests/test_cli.py | 376 +++++++++++++++++++--------------------------- 1 file changed, 151 insertions(+), 225 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 25c8a14..b8c0cf9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -32,6 +32,29 @@ from yamllint import cli from yamllint import config +class RunContext(object): + """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__() + sys.stdout = self.outstream = StringIO() + sys.stderr = self.errstream = StringIO() + return self + + def __exit__(self, *exc_info): + self.stdout, sys.stdout = self.outstream.getvalue(), sys.__stdout__ + self.stderr, sys.stderr = self.errstream.getvalue(), sys.__stderr__ + return self._raises_ctx.__exit__(*exc_info) + + @property + def returncode(self): + return self._raises_ctx.exception.code + + class CommandLineTestCase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -174,201 +197,144 @@ class CommandLineTestCase(unittest.TestCase): ) def test_run_with_bad_arguments(self): - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(()) + self.assertNotEqual(ctx.returncode, 0) + self.assertEqual(ctx.stdout, '') + self.assertRegexpMatches(ctx.stderr, r'^usage') - self.assertNotEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertRegexpMatches(err, r'^usage') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('--unknown-arg', )) + self.assertNotEqual(ctx.returncode, 0) + self.assertEqual(ctx.stdout, '') + self.assertRegexpMatches(ctx.stderr, r'^usage') - self.assertNotEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertRegexpMatches(err, r'^usage') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('-c', './conf.yaml', '-d', 'relaxed', 'file')) - - self.assertNotEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') + self.assertNotEqual(ctx.returncode, 0) + self.assertEqual(ctx.stdout, '') self.assertRegexpMatches( - err.splitlines()[-1], + ctx.stderr.splitlines()[-1], r'^yamllint: error: argument -d\/--config-data: ' r'not allowed with argument -c\/--config-file$' ) # checks if reading from stdin and files are mutually exclusive - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('-', 'file')) - - self.assertNotEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertRegexpMatches(err, r'^usage') + self.assertNotEqual(ctx.returncode, 0) + self.assertEqual(ctx.stdout, '') + self.assertRegexpMatches(ctx.stderr, r'^usage') def test_run_with_bad_config(self): - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('-d', 'rules: {a: b}', 'file')) - - self.assertEqual(ctx.exception.code, -1) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertRegexpMatches(err, r'^invalid config: no such rule') + self.assertEqual(ctx.returncode, -1) + self.assertEqual(ctx.stdout, '') + self.assertRegexpMatches(ctx.stderr, r'^invalid config: no such rule') def test_run_with_empty_config(self): - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('-d', '', 'file')) - - self.assertEqual(ctx.exception.code, -1) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertRegexpMatches(err, r'^invalid config: not a dict') + self.assertEqual(ctx.returncode, -1) + self.assertEqual(ctx.stdout, '') + self.assertRegexpMatches(ctx.stderr, r'^invalid config: not a dict') def test_run_with_config_file(self): with open(os.path.join(self.wd, 'config'), 'w') as f: f.write('rules: {trailing-spaces: disable}') - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('-c', f.name, os.path.join(self.wd, 'a.yaml'))) - self.assertEqual(ctx.exception.code, 0) + self.assertEqual(ctx.returncode, 0) with open(os.path.join(self.wd, 'config'), 'w') as f: f.write('rules: {trailing-spaces: enable}') - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('-c', f.name, os.path.join(self.wd, 'a.yaml'))) - self.assertEqual(ctx.exception.code, 1) + self.assertEqual(ctx.returncode, 1) def test_run_with_user_global_config_file(self): home = os.path.join(self.wd, 'fake-home') - os.mkdir(home) - dir = os.path.join(home, '.config') - os.mkdir(dir) - dir = os.path.join(dir, 'yamllint') - os.mkdir(dir) + dir = os.path.join(home, '.config', 'yamllint') + os.makedirs(dir) config = os.path.join(dir, 'config') - temp = os.environ['HOME'] + self.addCleanup(os.environ.update, HOME=os.environ['HOME']) os.environ['HOME'] = home with open(config, 'w') as f: f.write('rules: {trailing-spaces: disable}') - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run((os.path.join(self.wd, 'a.yaml'), )) - self.assertEqual(ctx.exception.code, 0) + self.assertEqual(ctx.returncode, 0) with open(config, 'w') as f: f.write('rules: {trailing-spaces: enable}') - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run((os.path.join(self.wd, 'a.yaml'), )) - self.assertEqual(ctx.exception.code, 1) - - os.environ['HOME'] = temp + self.assertEqual(ctx.returncode, 1) def test_run_version(self): - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('--version', )) - - self.assertEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertRegexpMatches(out + err, r'yamllint \d+\.\d+') + self.assertEqual(ctx.returncode, 0) + self.assertRegexpMatches(ctx.stdout + ctx.stderr, r'yamllint \d+\.\d+') def test_run_non_existing_file(self): - file = os.path.join(self.wd, 'i-do-not-exist.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run(('-f', 'parsable', file)) - - self.assertEqual(ctx.exception.code, -1) + path = os.path.join(self.wd, 'i-do-not-exist.yaml') - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertRegexpMatches(err, r'No such file or directory') + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', path)) + self.assertEqual(ctx.returncode, -1) + self.assertEqual(ctx.stdout, '') + self.assertRegexpMatches(ctx.stderr, r'No such file or directory') def test_run_one_problem_file(self): - file = os.path.join(self.wd, 'a.yaml') + path = os.path.join(self.wd, 'a.yaml') - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run(('-f', 'parsable', file)) - - self.assertEqual(ctx.exception.code, 1) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', path)) + self.assertEqual(ctx.returncode, 1) + self.assertEqual(ctx.stdout, ( '%s:2:4: [error] trailing spaces (trailing-spaces)\n' '%s:3:4: [error] no new line character at the end of file ' - '(new-line-at-end-of-file)\n') % (file, file)) - self.assertEqual(err, '') + '(new-line-at-end-of-file)\n' % (path, path))) + self.assertEqual(ctx.stderr, '') def test_run_one_warning(self): - file = os.path.join(self.wd, 'warn.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run(('-f', 'parsable', file)) + path = os.path.join(self.wd, 'warn.yaml') - self.assertEqual(ctx.exception.code, 0) + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', path)) + self.assertEqual(ctx.returncode, 0) def test_run_warning_in_strict_mode(self): - file = os.path.join(self.wd, 'warn.yaml') + path = os.path.join(self.wd, 'warn.yaml') - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run(('-f', 'parsable', '--strict', file)) - - self.assertEqual(ctx.exception.code, 2) + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', '--strict', path)) + self.assertEqual(ctx.returncode, 2) def test_run_one_ok_file(self): - file = os.path.join(self.wd, 'sub', 'ok.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run(('-f', 'parsable', file)) - - self.assertEqual(ctx.exception.code, 0) + path = os.path.join(self.wd, 'sub', 'ok.yaml') - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertEqual(err, '') + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', path)) + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', '')) def test_run_empty_file(self): - file = os.path.join(self.wd, 'empty.yml') + path = os.path.join(self.wd, 'empty.yml') - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run(('-f', 'parsable', file)) - - self.assertEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertEqual(err, '') + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', path)) + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', '')) def test_run_non_ascii_file(self): - file = os.path.join(self.wd, 'non-ascii', 'éçäγλνπ¥', 'utf-8') + path = os.path.join(self.wd, 'non-ascii', 'éçäγλνπ¥', 'utf-8') # Make sure the default localization conditions on this "system" # support UTF-8 encoding. @@ -377,63 +343,46 @@ class CommandLineTestCase(unittest.TestCase): locale.setlocale(locale.LC_ALL, 'C.UTF-8') except locale.Error: locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') + self.addCleanup(locale.setlocale, locale.LC_ALL, loc) - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run(('-f', 'parsable', file)) - - locale.setlocale(locale.LC_ALL, loc) - - self.assertEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, '') - self.assertEqual(err, '') + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', path)) + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', '')) def test_run_multiple_files(self): items = [os.path.join(self.wd, 'empty.yml'), os.path.join(self.wd, 's')] - file = items[1] + '/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml' + path = items[1] + '/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml' - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(['-f', 'parsable'] + items) - - self.assertEqual(ctx.exception.code, 1) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + self.assertEqual((ctx.returncode, ctx.stderr), (1, '')) + self.assertEqual(ctx.stdout, ( '%s:3:1: [error] duplication of key "key" in mapping ' - '(key-duplicates)\n') % file) - self.assertEqual(err, '') + '(key-duplicates)\n') % path) def test_run_piped_output_nocolor(self): - file = os.path.join(self.wd, 'a.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run((file, )) + path = os.path.join(self.wd, 'a.yaml') - self.assertEqual(ctx.exception.code, 1) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + with RunContext(self) as ctx: + cli.run((path, )) + self.assertEqual((ctx.returncode, ctx.stderr), (1, '')) + self.assertEqual(ctx.stdout, ( '%s\n' ' 2:4 error trailing spaces (trailing-spaces)\n' ' 3:4 error no new line character at the end of file ' '(new-line-at-end-of-file)\n' - '\n' % file)) - self.assertEqual(err, '') + '\n' % path)) def test_run_default_format_output_in_tty(self): - file = os.path.join(self.wd, 'a.yaml') + path = os.path.join(self.wd, 'a.yaml') # Create a pseudo-TTY and redirect stdout to it master, slave = pty.openpty() sys.stdout = sys.stderr = os.fdopen(slave, 'w') with self.assertRaises(SystemExit) as ctx: - cli.run((file, )) + cli.run((path, )) sys.stdout.flush() self.assertEqual(ctx.exception.code, 1) @@ -456,114 +405,91 @@ class CommandLineTestCase(unittest.TestCase): ' \033[2m3:4\033[0m \033[31merror\033[0m ' 'no new line character at the end of file ' '\033[2m(new-line-at-end-of-file)\033[0m\n' - '\n' % file)) + '\n' % path)) def test_run_default_format_output_without_tty(self): - file = os.path.join(self.wd, 'a.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run((file, )) - - self.assertEqual(ctx.exception.code, 1) + path = os.path.join(self.wd, 'a.yaml') - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + with RunContext(self) as ctx: + cli.run((path, )) + expected_out = ( '%s\n' ' 2:4 error trailing spaces (trailing-spaces)\n' ' 3:4 error no new line character at the end of file ' '(new-line-at-end-of-file)\n' - '\n' % file)) - self.assertEqual(err, '') + '\n' % path) + self.assertEqual( + (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) def test_run_auto_output_without_tty_output(self): - file = os.path.join(self.wd, 'a.yaml') + path = os.path.join(self.wd, 'a.yaml') - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run((file, '--format', 'auto')) - - self.assertEqual(ctx.exception.code, 1) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + with RunContext(self) as ctx: + cli.run((path, '--format', 'auto')) + expected_out = ( '%s\n' ' 2:4 error trailing spaces (trailing-spaces)\n' ' 3:4 error no new line character at the end of file ' '(new-line-at-end-of-file)\n' - '\n' % file)) - self.assertEqual(err, '') + '\n' % path) + self.assertEqual( + (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) def test_run_format_colored(self): - file = os.path.join(self.wd, 'a.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run((file, '--format', 'colored')) - - self.assertEqual(ctx.exception.code, 1) + path = os.path.join(self.wd, 'a.yaml') - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + with RunContext(self) as ctx: + cli.run((path, '--format', 'colored')) + expected_out = ( '\033[4m%s\033[0m\n' ' \033[2m2:4\033[0m \033[31merror\033[0m ' 'trailing spaces \033[2m(trailing-spaces)\033[0m\n' ' \033[2m3:4\033[0m \033[31merror\033[0m ' 'no new line character at the end of file ' '\033[2m(new-line-at-end-of-file)\033[0m\n' - '\n' % file)) - self.assertEqual(err, '') + '\n' % path) + self.assertEqual( + (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) def test_run_read_from_stdin(self): # prepares stdin with an invalid yaml string so that we can check # for its specific error, and be assured that stdin was read - sys.stdout, sys.stderr = StringIO(), StringIO() + self.addCleanup(setattr, sys, 'stdin', sys.__stdin__) sys.stdin = StringIO( 'I am a string\n' 'therefore: I am an error\n') - with self.assertRaises(SystemExit) as ctx: + with RunContext(self) as ctx: cli.run(('-', '-f', 'parsable')) - - self.assertNotEqual(ctx.exception.code, 0) - - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + expected_out = ( 'stdin:2:10: [error] syntax error: ' - 'mapping values are not allowed here (syntax)\n')) - self.assertEqual(err, '') + 'mapping values are not allowed here (syntax)\n') + self.assertEqual( + (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) def test_run_no_warnings(self): - file = os.path.join(self.wd, 'a.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run((file, '--no-warnings', '-f', 'auto')) - - self.assertEqual(ctx.exception.code, 1) + path = os.path.join(self.wd, 'a.yaml') - out, err = sys.stdout.getvalue(), sys.stderr.getvalue() - self.assertEqual(out, ( + with RunContext(self) as ctx: + cli.run((path, '--no-warnings', '-f', 'auto')) + expected_out = ( '%s\n' ' 2:4 error trailing spaces (trailing-spaces)\n' ' 3:4 error no new line character at the end of file ' '(new-line-at-end-of-file)\n' - '\n' % file)) - self.assertEqual(err, '') - - file = os.path.join(self.wd, 'warn.yaml') + '\n' % path) + self.assertEqual( + (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run((file, '--no-warnings', '-f', 'auto')) + path = os.path.join(self.wd, 'warn.yaml') - self.assertEqual(ctx.exception.code, 0) + with RunContext(self) as ctx: + cli.run((path, '--no-warnings', '-f', 'auto')) + self.assertEqual(ctx.returncode, 0) def test_run_no_warnings_and_strict(self): - file = os.path.join(self.wd, 'warn.yaml') - - sys.stdout, sys.stderr = StringIO(), StringIO() - with self.assertRaises(SystemExit) as ctx: - cli.run((file, '--no-warnings', '-s')) + path = os.path.join(self.wd, 'warn.yaml') - self.assertEqual(ctx.exception.code, 2) + with RunContext(self) as ctx: + cli.run((path, '--no-warnings', '-s')) + self.assertEqual(ctx.returncode, 2) From 91763f5476983e8df2f0ea24c6d01ff03ef0a3d7 Mon Sep 17 00:00:00 2001 From: Martin Packman Date: Mon, 10 Feb 2020 20:35:16 +0000 Subject: [PATCH 5/8] Fix new-lines rule on Python 3 Use io.open() when reading files in cli which has the same behaviour in Python 2 and Python 3, and supply the newline='' parameter which handles but does not translate line endings. Add dos.yml test file with windows newlines. Also add to file finding test expected output. Add test for new-lines rule through the cli. Validates files are read with the correct universal newlines setting. Fixes adrienverge/yamllint#228 --- tests/test_cli.py | 26 +++++++++++++++++++++++++- yamllint/cli.py | 3 ++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index b8c0cf9..517bc62 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -88,6 +88,9 @@ class CommandLineTestCase(unittest.TestCase): u'# 19.99 €\n' u'- お早う御座います。\n' u'# الأَبْجَدِيَّة العَرَبِيَّة\n').encode('utf-8'), + # dos line endings yaml + 'dos.yml': '---\r\n' + 'dos: true', }) @classmethod @@ -101,6 +104,7 @@ class CommandLineTestCase(unittest.TestCase): self.assertEqual( sorted(cli.find_files_recursively([self.wd], conf)), [os.path.join(self.wd, 'a.yaml'), + os.path.join(self.wd, 'dos.yml'), os.path.join(self.wd, 'empty.yml'), 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'), @@ -146,7 +150,8 @@ class CommandLineTestCase(unittest.TestCase): ' - \'*.yml\'\n') self.assertEqual( sorted(cli.find_files_recursively([self.wd], conf)), - [os.path.join(self.wd, 'empty.yml')] + [os.path.join(self.wd, 'dos.yml'), + os.path.join(self.wd, 'empty.yml')] ) conf = config.YamlLintConfig('extends: default\n' @@ -163,6 +168,7 @@ class CommandLineTestCase(unittest.TestCase): self.assertEqual( sorted(cli.find_files_recursively([self.wd], conf)), [os.path.join(self.wd, 'a.yaml'), + os.path.join(self.wd, 'dos.yml'), os.path.join(self.wd, 'empty.yml'), os.path.join(self.wd, 'no-yaml.json'), os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'), @@ -179,6 +185,7 @@ class CommandLineTestCase(unittest.TestCase): self.assertEqual( sorted(cli.find_files_recursively([self.wd], conf)), [os.path.join(self.wd, 'a.yaml'), + os.path.join(self.wd, 'dos.yml'), os.path.join(self.wd, 'empty.yml'), os.path.join(self.wd, 'no-yaml.json'), os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'), @@ -493,3 +500,20 @@ class CommandLineTestCase(unittest.TestCase): with RunContext(self) as ctx: cli.run((path, '--no-warnings', '-s')) self.assertEqual(ctx.returncode, 2) + + def test_run_non_universal_newline(self): + path = os.path.join(self.wd, 'dos.yml') + + with RunContext(self) as ctx: + cli.run(('-d', 'rules:\n new-lines:\n type: dos', path)) + self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', '')) + + with RunContext(self) as ctx: + cli.run(('-d', 'rules:\n new-lines:\n type: unix', path)) + expected_out = ( + '%s\n' + ' 1:4 error wrong new line character: expected \\n' + ' (new-lines)\n' + '\n' % path) + self.assertEqual( + (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) diff --git a/yamllint/cli.py b/yamllint/cli.py index 26bdb1f..e99fd2c 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -17,6 +17,7 @@ from __future__ import print_function import argparse +import io import os import platform import sys @@ -176,7 +177,7 @@ def run(argv=None): for file in find_files_recursively(args.files, conf): filepath = file[2:] if file.startswith('./') else file try: - with open(file) as f: + with io.open(file, newline='') as f: problems = linter.run(f, conf, filepath) except EnvironmentError as e: print(e, file=sys.stderr) From 15aea73fbe7d020fe4a4fff7e36713c67442266e Mon Sep 17 00:00:00 2001 From: Rui Pinge Date: Sat, 14 Mar 2020 13:22:29 +0000 Subject: [PATCH 6/8] Fix quoted-strings rules not working for string values matching scalars --- tests/rules/test_quoted_strings.py | 41 +++++++++++++++++++----------- yamllint/rules/quoted_strings.py | 37 ++++++++++++++------------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 04ee492..973660c 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -30,6 +30,8 @@ class QuotedTestCase(RuleTestCase): 'foo: \'bar\'\n', conf) self.check('---\n' 'bar: 123\n', conf) + self.check('---\n' + 'bar: "123"\n', conf) def test_quote_type_any(self): conf = 'quoted-strings: {quote-type: any}\n' @@ -37,11 +39,14 @@ class QuotedTestCase(RuleTestCase): 'boolean1: true\n' 'number1: 123\n' 'string1: foo\n' # fails - 'string2: "foo"\n' - 'string3: \'bar\'\n' - 'string4: !!str genericstring\n' - 'string5: !!str 456\n' - 'string6: !!str "quotedgenericstring"\n' + 'string2: "true"\n' + 'string3: "123"\n' + 'string4: \'true\'\n' + 'string5: "foo"\n' + 'string6: \'bar\'\n' + 'string7: !!str genericstring\n' + 'string8: !!str 456\n' + 'string9: !!str "quotedgenericstring"\n' 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' @@ -69,15 +74,18 @@ class QuotedTestCase(RuleTestCase): 'number1: 123\n' 'string1: foo\n' # fails 'string2: "foo"\n' # fails - 'string3: \'bar\'\n' - 'string4: !!str genericstring\n' - 'string5: !!str 456\n' - 'string6: !!str "quotedgenericstring"\n' + 'string3: "true"\n' # fails + 'string4: "123"\n' # fails + 'string5: \'bar\'\n' + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(5, 10)) + conf, problem1=(4, 10), problem2=(5, 10), + problem3=(6, 10), problem4=(7, 10)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -100,15 +108,18 @@ class QuotedTestCase(RuleTestCase): 'number1: 123\n' 'string1: foo\n' # fails 'string2: "foo"\n' - 'string3: \'bar\'\n' # fails - 'string4: !!str genericstring\n' - 'string5: !!str 456\n' - 'string6: !!str "quotedgenericstring"\n' + 'string3: \'true\'\n' # fails + 'string4: \'123\'\n' # fails + 'string5: \'bar\'\n' # fails + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(6, 10)) + conf, problem1=(4, 10), problem2=(6, 10), + problem3=(7, 10), problem4=(8, 10)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index b019261..4fc5aa2 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -52,27 +52,30 @@ DEFAULT = {'quote-type': 'any'} def check(conf, token, prev, next, nextnext, context): quote_type = conf['quote-type'] - if (isinstance(token, yaml.tokens.ScalarToken) and + if not (isinstance(token, yaml.tokens.ScalarToken) and isinstance(prev, (yaml.ValueToken, yaml.TagToken))): - # Ignore explicit types, e.g. !!str testtest or !!int 42 - if (prev and isinstance(prev, yaml.tokens.TagToken) and - prev.value[0] == '!!'): - return + return + + # Ignore explicit types, e.g. !!str testtest or !!int 42 + if (prev and isinstance(prev, yaml.tokens.TagToken) and + prev.value[0] == '!!'): + return - # Ignore numbers, booleans, etc. + # Ignore numbers, booleans, etc. + if token.plain: resolver = yaml.resolver.Resolver() if resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False)) != 'tag:yaml.org,2002:str': return - # Ignore multi-line strings - if (not token.plain) and (token.style == "|" or token.style == ">"): - return - - if ((quote_type == 'single' and token.style != "'") or - (quote_type == 'double' and token.style != '"') or - (quote_type == 'any' and token.style is None)): - yield LintProblem( - token.start_mark.line + 1, - token.start_mark.column + 1, - "string value is not quoted with %s quotes" % (quote_type)) + # Ignore multi-line strings + if (not token.plain) and (token.style == "|" or token.style == ">"): + return + + if ((quote_type == 'single' and token.style != "'") or + (quote_type == 'double' and token.style != '"') or + (quote_type == 'any' and token.style is None)): + yield LintProblem( + token.start_mark.line + 1, + token.start_mark.column + 1, + "string value is not quoted with %s quotes" % (quote_type)) From 3a6a09b7b627cb88c1ea1651047a8c540b651ef1 Mon Sep 17 00:00:00 2001 From: Rui Pinge Date: Mon, 9 Mar 2020 10:19:48 +0000 Subject: [PATCH 7/8] Add support for redundant quotes in quoted-strings rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Adrien Vergé --- tests/rules/test_quoted_strings.py | 236 +++++++++++++++++++++++++++-- yamllint/rules/quoted_strings.py | 99 +++++++++--- 2 files changed, 305 insertions(+), 30 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 973660c..0fdba71 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -22,6 +22,7 @@ class QuotedTestCase(RuleTestCase): def test_disabled(self): conf = 'quoted-strings: disable' + self.check('---\n' 'foo: bar\n', conf) self.check('---\n' @@ -35,18 +36,18 @@ class QuotedTestCase(RuleTestCase): def test_quote_type_any(self): conf = 'quoted-strings: {quote-type: any}\n' + self.check('---\n' 'boolean1: true\n' 'number1: 123\n' 'string1: foo\n' # fails - 'string2: "true"\n' - 'string3: "123"\n' - 'string4: \'true\'\n' - 'string5: "foo"\n' - 'string6: \'bar\'\n' - 'string7: !!str genericstring\n' - 'string8: !!str 456\n' - 'string9: !!str "quotedgenericstring"\n' + 'string2: "foo"\n' + 'string3: "true"\n' + 'string4: "123"\n' + 'string5: \'bar\'\n' + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' @@ -60,7 +61,7 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 3:\n' - ' word 1\n' + ' word 1\n' # fails ' word 2\n' 'multiline string 4:\n' ' "word 1\\\n' @@ -69,6 +70,7 @@ class QuotedTestCase(RuleTestCase): def test_quote_type_single(self): conf = 'quoted-strings: {quote-type: single}\n' + self.check('---\n' 'boolean1: true\n' 'number1: 123\n' @@ -94,7 +96,7 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 3:\n' - ' word 1\n' + ' word 1\n' # fails ' word 2\n' 'multiline string 4:\n' ' "word 1\\\n' @@ -103,13 +105,83 @@ class QuotedTestCase(RuleTestCase): def test_quote_type_double(self): conf = 'quoted-strings: {quote-type: double}\n' + self.check('---\n' 'boolean1: true\n' 'number1: 123\n' 'string1: foo\n' # fails 'string2: "foo"\n' - 'string3: \'true\'\n' # fails - 'string4: \'123\'\n' # fails + 'string3: "true"\n' + 'string4: "123"\n' + 'string5: \'bar\'\n' # fails + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' + 'binary: !!binary binstring\n' + 'integer: !!int intstring\n' + 'boolean2: !!bool boolstring\n' + 'boolean3: !!bool "quotedboolstring"\n', + conf, problem1=(4, 10), problem2=(8, 10)) + self.check('---\n' + 'multiline string 1: |\n' + ' line 1\n' + ' line 2\n' + 'multiline string 2: >\n' + ' word 1\n' + ' word 2\n' + 'multiline string 3:\n' + ' word 1\n' # fails + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' + ' word 2"\n', + conf, problem1=(9, 3)) + + def test_disallow_redundant_quotes(self): + conf = 'quoted-strings: {required: only-when-needed}\n' + + self.check('---\n' + 'boolean1: true\n' + 'number1: 123\n' + 'string1: foo\n' + 'string2: "foo"\n' # fails + 'string3: "true"\n' + 'string4: "123"\n' + 'string5: \'bar\'\n' # fails + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' + 'binary: !!binary binstring\n' + 'integer: !!int intstring\n' + 'boolean2: !!bool boolstring\n' + 'boolean3: !!bool "quotedboolstring"\n', + conf, problem1=(5, 10), problem2=(8, 10)) + self.check('---\n' + 'multiline string 1: |\n' + ' line 1\n' + ' line 2\n' + 'multiline string 2: >\n' + ' word 1\n' + ' word 2\n' + 'multiline string 3:\n' + ' word 1\n' + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' # fails + ' word 2"\n', + conf, problem1=(12, 3)) + + def test_disallow_redundant_single_quotes(self): + conf = 'quoted-strings: {quote-type: single, ' + \ + 'required: only-when-needed}\n' + + self.check('---\n' + 'boolean1: true\n' + 'number1: 123\n' + 'string1: foo\n' + 'string2: "foo"\n' # fails + 'string3: "true"\n' # fails + 'string4: "123"\n' # fails 'string5: \'bar\'\n' # fails 'string6: !!str genericstring\n' 'string7: !!str 456\n' @@ -118,7 +190,7 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(6, 10), + conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), problem4=(8, 10)) self.check('---\n' 'multiline string 1: |\n' @@ -131,6 +203,144 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 4:\n' + ' "word 1\\\n' # fails + ' word 2"\n', + conf, problem1=(12, 3)) + + def test_single_quotes_required(self): + conf = 'quoted-strings: {quote-type: single, required: true}\n' + + self.check('---\n' + 'boolean1: true\n' + 'number1: 123\n' + 'string1: foo\n' # fails + 'string2: "foo"\n' # fails + 'string3: "true"\n' # fails + 'string4: "123"\n' # fails + 'string5: \'bar\'\n' + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' + 'binary: !!binary binstring\n' + 'integer: !!int intstring\n' + 'boolean2: !!bool boolstring\n' + 'boolean3: !!bool "quotedboolstring"\n', + conf, problem1=(4, 10), problem2=(5, 10), + problem3=(6, 10), problem4=(7, 10)) + self.check('---\n' + 'multiline string 1: |\n' + ' line 1\n' + ' line 2\n' + 'multiline string 2: >\n' + ' word 1\n' + ' word 2\n' + 'multiline string 3:\n' + ' word 1\n' # fails + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' # fails + ' word 2"\n', + conf, problem1=(9, 3), problem2=(12, 3)) + + def test_any_quotes_relaxed(self): + conf = 'quoted-strings: {quote-type: any, required: false}\n' + + self.check('---\n' + 'boolean1: true\n' + 'number1: 123\n' + 'string1: foo\n' + 'string2: "foo"\n' + 'string3: "true"\n' + 'string4: "123"\n' + 'string5: \'bar\'\n' + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' + 'binary: !!binary binstring\n' + 'integer: !!int intstring\n' + 'boolean2: !!bool boolstring\n' + 'boolean3: !!bool "quotedboolstring"\n', + conf) + self.check('---\n' + 'multiline string 1: |\n' + ' line 1\n' + ' line 2\n' + 'multiline string 2: >\n' + ' word 1\n' + ' word 2\n' + 'multiline string 3:\n' + ' word 1\n' + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' + ' word 2"\n', + conf) + + def test_single_quotes_relaxed(self): + conf = 'quoted-strings: {quote-type: single, required: false}\n' + + self.check('---\n' + 'boolean1: true\n' + 'number1: 123\n' + 'string1: foo\n' + 'string2: "foo"\n' # fails + 'string3: "true"\n' # fails + 'string4: "123"\n' # fails + 'string5: \'bar\'\n' + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' + 'binary: !!binary binstring\n' + 'integer: !!int intstring\n' + 'boolean2: !!bool boolstring\n' + 'boolean3: !!bool "quotedboolstring"\n', + conf, problem2=(5, 10), + problem3=(6, 10), problem4=(7, 10)) + self.check('---\n' + 'multiline string 1: |\n' + ' line 1\n' + ' line 2\n' + 'multiline string 2: >\n' + ' word 1\n' + ' word 2\n' + 'multiline string 3:\n' + ' word 1\n' + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' # fails + ' word 2"\n', + conf, problem1=(12, 3)) + + def test_quotes_required(self): + conf = 'quoted-strings: {quote-type: any, required: true}\n' + + self.check('---\n' + 'boolean1: true\n' + 'number1: 123\n' + 'string1: foo\n' # fails + 'string2: "foo"\n' + 'string3: "true"\n' + 'string4: "123"\n' + 'string5: \'bar\'\n' + 'string6: !!str genericstring\n' + 'string7: !!str 456\n' + 'string8: !!str "quotedgenericstring"\n' + 'binary: !!binary binstring\n' + 'integer: !!int intstring\n' + 'boolean2: !!bool boolstring\n' + 'boolean3: !!bool "quotedboolstring"\n', + conf, problem2=(4, 10)) + self.check('---\n' + 'multiline string 1: |\n' + ' line 1\n' + ' line 2\n' + 'multiline string 2: >\n' + ' word 1\n' + ' word 2\n' + 'multiline string 3:\n' + ' word 1\n' # fails + ' word 2\n' + 'multiline string 4:\n' ' "word 1\\\n' ' word 2"\n', conf, problem1=(9, 3)) diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index 4fc5aa2..aaa635d 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -15,15 +15,23 @@ # along with this program. If not, see . """ -Use this rule to forbid any string values that are not quoted. -You can also enforce the type of the quote used using the ``quote-type`` option -(``single``, ``double`` or ``any``). +Use this rule to forbid any string values that are not quoted, or to prevent +quoted strings without needing it. You can also enforce the type of the quote +used. + +.. rubric:: Options + +* ``quote-type`` defines allowed quotes: ``single``, ``double`` or ``any`` + (default). +* ``required`` defines whether using quotes in string values is required + (``true``, default) or not (``false``), or only allowed when really needed + (``only-when-needed``). **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked. .. rubric:: Examples -#. With ``quoted-strings: {quote-type: any}`` +#. With ``quoted-strings: {quote-type: any, required: true}`` the following code snippet would **PASS**: :: @@ -37,6 +45,24 @@ You can also enforce the type of the quote used using the ``quote-type`` option :: foo: bar + +#. With ``quoted-strings: {quote-type: single, required: only-when-needed}`` + + the following code snippet would **PASS**: + :: + + foo: bar + bar: foo + not_number: '123' + not_boolean: 'true' + not_comment: '# comment' + not_list: '[1, 2, 3]' + not_map: '{a: 1, b: 2}' + + the following code snippet would **FAIL**: + :: + + foo: 'bar' """ import yaml @@ -45,13 +71,23 @@ from yamllint.linter import LintProblem ID = 'quoted-strings' TYPE = 'token' -CONF = {'quote-type': ('any', 'single', 'double')} -DEFAULT = {'quote-type': 'any'} +CONF = {'quote-type': ('any', 'single', 'double'), + 'required': (True, False, 'only-when-needed')} +DEFAULT = {'quote-type': 'any', + 'required': True} +DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str' +START_TOKENS = {'#', '*', '!', '?', '@', '`', '&', + ',', '-', '{', '}', '[', ']', ':'} -def check(conf, token, prev, next, nextnext, context): - quote_type = conf['quote-type'] +def quote_match(quote_type, token_style): + return ((quote_type == 'any') or + (quote_type == 'single' and token_style == "'") or + (quote_type == 'double' and token_style == '"')) + + +def check(conf, token, prev, next, nextnext, context): if not (isinstance(token, yaml.tokens.ScalarToken) and isinstance(prev, (yaml.ValueToken, yaml.TagToken))): return @@ -62,20 +98,49 @@ def check(conf, token, prev, next, nextnext, context): return # Ignore numbers, booleans, etc. - if token.plain: - resolver = yaml.resolver.Resolver() - if resolver.resolve(yaml.nodes.ScalarNode, token.value, - (True, False)) != 'tag:yaml.org,2002:str': - return + resolver = yaml.resolver.Resolver() + tag = resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False)) + if token.plain and tag != DEFAULT_SCALAR_TAG: + return # Ignore multi-line strings if (not token.plain) and (token.style == "|" or token.style == ">"): return - if ((quote_type == 'single' and token.style != "'") or - (quote_type == 'double' and token.style != '"') or - (quote_type == 'any' and token.style is None)): + quote_type = conf['quote-type'] + required = conf['required'] + + # Completely relaxed about quotes (same as the rule being disabled) + if required is False and quote_type == 'any': + return + + msg = None + if required is True: + + # Quotes are mandatory and need to match config + if token.style is None or not quote_match(quote_type, token.style): + msg = "string value is not quoted with %s quotes" % (quote_type) + + elif required is False: + + # Quotes are not mandatory but when used need to match config + if token.style and not quote_match(quote_type, token.style): + msg = "string value is not quoted with %s quotes" % (quote_type) + + elif not token.plain: + + # Quotes are disallowed when not needed + if (tag == DEFAULT_SCALAR_TAG and token.value + and token.value[0] not in START_TOKENS): + msg = "string value is redundantly quoted with %s quotes" % ( + quote_type) + + # But when used need to match config + elif token.style and not quote_match(quote_type, token.style): + msg = "string value is not quoted with %s quotes" % (quote_type) + + if msg is not None: yield LintProblem( token.start_mark.line + 1, token.start_mark.column + 1, - "string value is not quoted with %s quotes" % (quote_type)) + msg) From 542ae758f5de70a37c6c322c16b83b92fdc008bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Tue, 24 Mar 2020 07:53:14 +0100 Subject: [PATCH 8/8] yamllint version 1.21.0 --- CHANGELOG.rst | 7 +++++++ yamllint/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 119917f..4e56ab6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +1.21.0 (2020-03-24) +------------------- + +- Fix ``new-lines`` rule on Python 3 with DOS line endings +- Fix ``quoted-strings`` rule not working for string values matching scalars +- Add ``required: only-when-needed`` option to the ``quoted-strings`` rule + 1.20.0 (2019-12-26) ------------------- diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 676c80f..5b04ef6 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.20.0' +APP_VERSION = '1.21.0' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé'