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')