diff --git a/.travis.yml b/.travis.yml index bd365c1..c40c6bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,12 +9,15 @@ python: - 3.6 - nightly install: - - pip install pyyaml flake8 flake8-import-order coveralls + - pip install pyyaml flake8 flake8-import-order coveralls sphinx - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install unittest2; fi - pip install . script: - if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then flake8 .; fi - yamllint --strict $(git ls-files '*.yaml' '*.yml') - coverage run --source=yamllint setup.py test + - if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then + python setup.py build_sphinx; + fi after_success: coveralls diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 73de95f..bc1fec4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog ========= +1.10.0 (2017-11-05) +------------------- + +- Fix colored output on Windows +- Check documentation compilation on continuous integration +- Add a new `empty-values` rule +- Make sure test files are included in dist bundle +- Tests: Use en_US.UTF-8 locale when C.UTF-8 not available +- Tests: Dynamically detect Python executable path + 1.9.0 (2017-10-16) ------------------ diff --git a/MANIFEST.in b/MANIFEST.in index 8c22eeb..792a672 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include LICENSE include README.rst include docs/* +include tests/*.py tests/rules/*.py tests/yaml-1.2-spec-examples/* diff --git a/docs/rules.rst b/docs/rules.rst index dab310d..24318c7 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -59,6 +59,11 @@ empty-lines .. automodule:: yamllint.rules.empty_lines +empty-values +------------ + +.. automodule:: yamllint.rules.empty_values + hyphens ------- diff --git a/setup.cfg b/setup.cfg index 4d5142d..82ba8ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,9 @@ universal = 1 [flake8] import-order-style = pep8 + +[build_sphinx] +all-files = 1 +source-dir = docs +build-dir = docs/_build +warning-is-error = 1 diff --git a/setup.py b/setup.py index e87980f..0c61d01 100644 --- a/setup.py +++ b/setup.py @@ -44,8 +44,7 @@ setup( packages=find_packages(exclude=['tests', 'tests.*']), entry_points={'console_scripts': ['yamllint=yamllint.cli:run']}, - package_data={'yamllint': ['conf/*.yaml'], - 'tests': ['yaml-1.2-spec-examples/*']}, + package_data={'yamllint': ['conf/*.yaml']}, install_requires=['pathspec >=0.5.3', 'pyyaml'], test_suite='tests', ) diff --git a/tests/common.py b/tests/common.py index 11dd235..ddeb867 100644 --- a/tests/common.py +++ b/tests/common.py @@ -20,7 +20,7 @@ import sys try: assert sys.version_info >= (2, 7) import unittest -except: +except AssertionError: import unittest2 as unittest import yaml diff --git a/tests/rules/test_empty_values.py b/tests/rules/test_empty_values.py new file mode 100644 index 0000000..137ec3b --- /dev/null +++ b/tests/rules/test_empty_values.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Greg Dubicki +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tests.common import RuleTestCase + + +class EmptyValuesTestCase(RuleTestCase): + rule_id = 'empty-values' + + def test_disabled(self): + conf = ('empty-values: disable\n' + 'braces: disable\n' + 'commas: disable\n') + self.check('---\n' + 'foo:\n', conf) + self.check('---\n' + 'foo:\n' + ' bar:\n', conf) + self.check('---\n' + '{a:}\n', conf) + self.check('---\n' + 'foo: {a:}\n', conf) + self.check('---\n' + '- {a:}\n' + '- {a:, b: 2}\n' + '- {a: 1, b:}\n' + '- {a: 1, b: , }\n', conf) + self.check('---\n' + '{a: {b: , c: {d: 4, e:}}, f:}\n', conf) + + def test_in_block_mappings_disabled(self): + conf = ('empty-values: {forbid-in-block-mappings: false,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'foo:\n', conf) + self.check('---\n' + 'foo:\n' + 'bar: aaa\n', conf) + + def test_in_block_mappings_single_line(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'implicitly-null:\n', conf, problem1=(2, 17)) + self.check('---\n' + 'implicitly-null:with-colons:in-key:\n', conf, + problem1=(2, 36)) + self.check('---\n' + 'implicitly-null:with-colons:in-key2:\n', conf, + problem1=(2, 37)) + + def test_in_block_mappings_all_lines(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'foo:\n' + 'bar:\n' + 'foobar:\n', conf, problem1=(2, 5), + problem2=(3, 5), problem3=(4, 8)) + + def test_in_block_mappings_explicit_end_of_document(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'foo:\n' + '...\n', conf, problem1=(2, 5)) + + def test_in_block_mappings_not_end_of_document(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'foo:\n' + 'bar:\n' + ' aaa\n', conf, problem1=(2, 5)) + + def test_in_block_mappings_different_level(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'foo:\n' + ' bar:\n' + 'aaa: bbb\n', conf, problem1=(3, 6)) + + def test_in_block_mappings_empty_flow_mapping(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n' + 'braces: disable\n' + 'commas: disable\n') + self.check('---\n' + 'foo: {a:}\n', conf) + self.check('---\n' + '- {a:, b: 2}\n' + '- {a: 1, b:}\n' + '- {a: 1, b: , }\n', conf) + + def test_in_block_mappings_empty_block_sequence(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'foo:\n' + ' -\n', conf) + + def test_in_block_mappings_not_empty_or_explicit_null(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'foo:\n' + ' bar:\n' + ' aaa\n', conf) + self.check('---\n' + 'explicitly-null: null\n', conf) + self.check('---\n' + 'explicitly-null:with-colons:in-key: null\n', conf) + self.check('---\n' + 'false-null: nulL\n', conf) + self.check('---\n' + 'empty-string: \'\'\n', conf) + self.check('---\n' + 'nullable-boolean: false\n', conf) + self.check('---\n' + 'nullable-int: 0\n', conf) + self.check('---\n' + 'First occurrence: &anchor Foo\n' + 'Second occurrence: *anchor\n', conf) + + def test_in_block_mappings_various_explicit_null(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n') + self.check('---\n' + 'null-alias: ~\n', conf) + self.check('---\n' + 'null-key1: {?: val}\n', conf) + self.check('---\n' + 'null-key2: {? !!null "": val}\n', conf) + + def test_in_block_mappings_comments(self): + conf = ('empty-values: {forbid-in-block-mappings: true,\n' + ' forbid-in-flow-mappings: false}\n' + 'comments: disable\n') + self.check('---\n' + 'empty: # comment\n' + 'foo:\n' + ' bar: # comment\n', conf, + problem1=(2, 7), + problem2=(4, 7)) + + def test_in_flow_mappings_disabled(self): + conf = ('empty-values: {forbid-in-block-mappings: false,\n' + ' forbid-in-flow-mappings: false}\n' + 'braces: disable\n' + 'commas: disable\n') + self.check('---\n' + '{a:}\n', conf) + self.check('---\n' + 'foo: {a:}\n', conf) + self.check('---\n' + '- {a:}\n' + '- {a:, b: 2}\n' + '- {a: 1, b:}\n' + '- {a: 1, b: , }\n', conf) + self.check('---\n' + '{a: {b: , c: {d: 4, e:}}, f:}\n', conf) + + def test_in_flow_mappings_single_line(self): + conf = ('empty-values: {forbid-in-block-mappings: false,\n' + ' forbid-in-flow-mappings: true}\n' + 'braces: disable\n' + 'commas: disable\n') + self.check('---\n' + '{a:}\n', conf, + problem=(2, 4)) + self.check('---\n' + 'foo: {a:}\n', conf, + problem=(2, 9)) + self.check('---\n' + '- {a:}\n' + '- {a:, b: 2}\n' + '- {a: 1, b:}\n' + '- {a: 1, b: , }\n', conf, + problem1=(2, 6), + problem2=(3, 6), + problem3=(4, 12), + problem4=(5, 12)) + self.check('---\n' + '{a: {b: , c: {d: 4, e:}}, f:}\n', conf, + problem1=(2, 8), + problem2=(2, 23), + problem3=(2, 29)) + + def test_in_flow_mappings_multi_line(self): + conf = ('empty-values: {forbid-in-block-mappings: false,\n' + ' forbid-in-flow-mappings: true}\n' + 'braces: disable\n' + 'commas: disable\n') + self.check('---\n' + 'foo: {\n' + ' a:\n' + '}\n', conf, + problem=(3, 5)) + self.check('---\n' + '{\n' + ' a: {\n' + ' b: ,\n' + ' c: {\n' + ' d: 4,\n' + ' e:\n' + ' }\n' + ' },\n' + ' f:\n' + '}\n', conf, + problem1=(4, 7), + problem2=(7, 9), + problem3=(10, 5)) + + def test_in_flow_mappings_various_explicit_null(self): + conf = ('empty-values: {forbid-in-block-mappings: false,\n' + ' forbid-in-flow-mappings: true}\n' + 'braces: disable\n' + 'commas: disable\n') + self.check('---\n' + '{explicit-null: null}\n', conf) + self.check('---\n' + '{null-alias: ~}\n', conf) + self.check('---\n' + 'null-key1: {?: val}\n', conf) + self.check('---\n' + 'null-key2: {? !!null "": val}\n', conf) + + def test_in_flow_mappings_comments(self): + conf = ('empty-values: {forbid-in-block-mappings: false,\n' + ' forbid-in-flow-mappings: true}\n' + 'braces: disable\n' + 'commas: disable\n' + 'comments: disable\n') + self.check('---\n' + '{\n' + ' a: {\n' + ' b: , # comment\n' + ' c: {\n' + ' d: 4, # comment\n' + ' e: # comment\n' + ' }\n' + ' },\n' + ' f: # comment\n' + '}\n', conf, + problem1=(4, 7), + problem2=(7, 9), + problem3=(10, 5)) diff --git a/tests/test_cli.py b/tests/test_cli.py index bec7078..47b5daf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -27,7 +27,7 @@ import sys try: assert sys.version_info >= (2, 7) import unittest -except: +except AssertionError: import unittest2 as unittest from yamllint import cli @@ -299,7 +299,10 @@ class CommandLineTestCase(unittest.TestCase): # Make sure the default localization conditions on this "system" # support UTF-8 encoding. loc = locale.getlocale() - locale.setlocale(locale.LC_ALL, 'C.UTF-8') + try: + locale.setlocale(locale.LC_ALL, 'C.UTF-8') + except locale.Error: + locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') sys.stdout, sys.stderr = StringIO(), StringIO() with self.assertRaises(SystemExit) as ctx: diff --git a/tests/test_config.py b/tests/test_config.py index a23da10..7e719b1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -24,7 +24,7 @@ import sys try: assert sys.version_info >= (2, 7) import unittest -except: +except AssertionError: import unittest2 as unittest from yamllint import cli diff --git a/tests/test_linter.py b/tests/test_linter.py index edd803f..6c7ae62 100644 --- a/tests/test_linter.py +++ b/tests/test_linter.py @@ -19,7 +19,7 @@ import sys try: assert sys.version_info >= (2, 7) import unittest -except: +except AssertionError: import unittest2 as unittest from yamllint.config import YamlLintConfig diff --git a/tests/test_module.py b/tests/test_module.py index 678f40c..8bdffcc 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -22,10 +22,13 @@ import sys try: assert sys.version_info >= (2, 7) import unittest -except: +except AssertionError: import unittest2 as unittest +PYTHON = sys.executable or 'python' + + @unittest.skipIf(sys.version_info < (2, 7), 'Python 2.6 not supported') class ModuleTestCase(unittest.TestCase): def setUp(self): @@ -46,7 +49,7 @@ class ModuleTestCase(unittest.TestCase): def test_run_module_no_args(self): with self.assertRaises(subprocess.CalledProcessError) as ctx: - subprocess.check_output(['python', '-m', 'yamllint'], + subprocess.check_output([PYTHON, '-m', 'yamllint'], stderr=subprocess.STDOUT) self.assertEqual(ctx.exception.returncode, 2) self.assertRegexpMatches(ctx.exception.output.decode(), @@ -54,7 +57,7 @@ class ModuleTestCase(unittest.TestCase): def test_run_module_on_bad_dir(self): with self.assertRaises(subprocess.CalledProcessError) as ctx: - subprocess.check_output(['python', '-m', 'yamllint', + subprocess.check_output([PYTHON, '-m', 'yamllint', '/does/not/exist'], stderr=subprocess.STDOUT) self.assertRegexpMatches(ctx.exception.output.decode(), @@ -62,7 +65,7 @@ class ModuleTestCase(unittest.TestCase): def test_run_module_on_file(self): out = subprocess.check_output( - ['python', '-m', 'yamllint', os.path.join(self.wd, 'warn.yaml')]) + [PYTHON, '-m', 'yamllint', os.path.join(self.wd, 'warn.yaml')]) lines = out.decode().splitlines() self.assertIn('/warn.yaml', lines[0]) self.assertEqual('\n'.join(lines[1:]), @@ -71,7 +74,7 @@ class ModuleTestCase(unittest.TestCase): def test_run_module_on_dir(self): with self.assertRaises(subprocess.CalledProcessError) as ctx: - subprocess.check_output(['python', '-m', 'yamllint', self.wd]) + subprocess.check_output([PYTHON, '-m', 'yamllint', self.wd]) self.assertEqual(ctx.exception.returncode, 1) files = ctx.exception.output.decode().split('\n\n') diff --git a/tests/test_parser.py b/tests/test_parser.py index c5c51d8..7ed1f98 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -18,7 +18,7 @@ import sys try: assert sys.version_info >= (2, 7) import unittest -except: +except AssertionError: import unittest2 as unittest import yaml diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 831da23..8ff52f9 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.9.0' +APP_VERSION = '1.10.0' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé' diff --git a/yamllint/cli.py b/yamllint/cli.py index 41695a3..0faee16 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -16,9 +16,9 @@ from __future__ import print_function -import os.path +import os import sys - +import platform import argparse from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION @@ -38,6 +38,15 @@ def find_files_recursively(items): yield item +def supports_color(): + supported_platform = not (platform.system() == 'Windows' and not + ('ANSICON' in os.environ or + ('TERM' in os.environ and + os.environ['TERM'] == 'ANSI'))) + return (supported_platform and + hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()) + + class Format(object): @staticmethod def parsable(problem, filename): @@ -134,7 +143,7 @@ def run(argv=None): for problem in linter.run(f, conf, filepath): if args.format == 'parsable': print(Format.parsable(problem, file)) - elif sys.stdout.isatty(): + elif supports_color(): if first: print('\033[4m%s\033[0m' % file) first = False diff --git a/yamllint/conf/default.yaml b/yamllint/conf/default.yaml index 57cff64..8c0e89a 100644 --- a/yamllint/conf/default.yaml +++ b/yamllint/conf/default.yaml @@ -32,6 +32,9 @@ rules: max: 2 max-start: 0 max-end: 0 + empty-values: + forbid-in-block-mappings: false + forbid-in-flow-mappings: false hyphens: max-spaces-after: 1 indentation: diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py index 83dca76..189ec69 100644 --- a/yamllint/rules/__init__.py +++ b/yamllint/rules/__init__.py @@ -24,6 +24,7 @@ from yamllint.rules import ( document_end, document_start, empty_lines, + empty_values, hyphens, indentation, key_duplicates, @@ -45,6 +46,7 @@ _RULES = { document_end.ID: document_end, document_start.ID: document_start, empty_lines.ID: empty_lines, + empty_values.ID: empty_values, hyphens.ID: hyphens, indentation.ID: indentation, key_duplicates.ID: key_duplicates, diff --git a/yamllint/rules/empty_values.py b/yamllint/rules/empty_values.py new file mode 100644 index 0000000..daa62b2 --- /dev/null +++ b/yamllint/rules/empty_values.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2017 Greg Dubicki +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Use this rule to prevent nodes with empty content, that implicitly result in +``null`` values. + +.. rubric:: Options + +* Use ``forbid-in-block-mappings`` to prevent empty values in block mappings. +* Use ``forbid-in-flow-mappings`` to prevent empty values in flow mappings. + +.. rubric:: Examples + +#. With ``empty-values: {forbid-in-block-mappings: true}`` + + the following code snippets would **PASS**: + :: + + some-mapping: + sub-element: correctly indented + + :: + + explicitly-null: null + + the following code snippets would **FAIL**: + :: + + some-mapping: + sub-element: incorrectly indented + + :: + + implicitly-null: + +#. With ``empty-values: {forbid-in-flow-mappings: true}`` + + the following code snippet would **PASS**: + :: + + {prop: null} + {a: 1, b: 2, c: 3} + + the following code snippets would **FAIL**: + :: + + {prop: } + + :: + + {a: 1, b:, c: 3} + +""" + +import yaml + +from yamllint.linter import LintProblem + + +ID = 'empty-values' +TYPE = 'token' +CONF = {'forbid-in-block-mappings': bool, + 'forbid-in-flow-mappings': bool} + + +def check(conf, token, prev, next, nextnext, context): + + if conf['forbid-in-block-mappings']: + if isinstance(token, yaml.ValueToken) and isinstance(next, ( + yaml.KeyToken, yaml.BlockEndToken)): + yield LintProblem(token.start_mark.line + 1, + token.end_mark.column + 1, + 'empty value in block mapping') + + if conf['forbid-in-flow-mappings']: + if isinstance(token, yaml.ValueToken) and isinstance(next, ( + yaml.FlowEntryToken, yaml.FlowMappingEndToken)): + yield LintProblem(token.start_mark.line + 1, + token.end_mark.column + 1, + 'empty value in flow mapping')