diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index de0e50c..105fccd 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,14 @@
Changelog
=========
+1.16.0 (2019-06-07)
+-------------------
+
+- Add FreeBSD installation instructions
+- Fix the ``line`` rule to correctly handle DOS new lines
+- Add the ``allowed-values`` option to the ``truthy`` rule
+- Allow configuration options to be a list of enums
+
1.15.0 (2019-02-11)
-------------------
diff --git a/README.rst b/README.rst
index 4314f09..7f59956 100644
--- a/README.rst
+++ b/README.rst
@@ -51,6 +51,13 @@ On Debian 8+ / Ubuntu 16.04+:
sudo apt-get install yamllint
+On FreeBSD:
+
+.. code:: sh
+
+ pkg install py27-yamllint
+ pkg install py36-yamllint
+
On Mac OS 10.11+:
.. code:: bash
diff --git a/setup.py b/setup.py
index 1b93008..ffa2ee2 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from setuptools import setup, find_packages
+from setuptools import find_packages, setup
from yamllint import (__author__, __license__,
APP_NAME, APP_VERSION, APP_DESCRIPTION)
diff --git a/tests/rules/test_empty_lines.py b/tests/rules/test_empty_lines.py
index b44a8a5..ab17d71 100644
--- a/tests/rules/test_empty_lines.py
+++ b/tests/rules/test_empty_lines.py
@@ -78,3 +78,22 @@ class EmptyLinesTestCase(RuleTestCase):
'document-start: disable\n')
self.check('non empty\n', conf)
self.check('non empty\n\n', conf, problem=(2, 1))
+
+ def test_with_dos_newlines(self):
+ conf = ('empty-lines: {max: 2, max-start: 0, max-end: 0}\n'
+ 'new-lines: {type: dos}\n'
+ 'document-start: disable\n')
+ self.check('---\r\n', conf)
+ self.check('---\r\ntext\r\n\r\ntext\r\n', conf)
+ self.check('\r\n---\r\ntext\r\n\r\ntext\r\n', conf,
+ problem=(1, 1))
+ self.check('\r\n\r\n\r\n---\r\ntext\r\n\r\ntext\r\n', conf,
+ problem=(3, 1))
+ self.check('---\r\ntext\r\n\r\n\r\n\r\ntext\r\n', conf,
+ problem=(5, 1))
+ self.check('---\r\ntext\r\n\r\n\r\n\r\n\r\n\r\n\r\ntext\r\n', conf,
+ problem=(8, 1))
+ self.check('---\r\ntext\r\n\r\ntext\r\n\r\n', conf,
+ problem=(5, 1))
+ self.check('---\r\ntext\r\n\r\ntext\r\n\r\n\r\n\r\n', conf,
+ problem=(7, 1))
diff --git a/tests/rules/test_line_length.py b/tests/rules/test_line_length.py
index 216317c..c1865f1 100644
--- a/tests/rules/test_line_length.py
+++ b/tests/rules/test_line_length.py
@@ -171,3 +171,12 @@ class LineLengthTestCase(RuleTestCase):
'# This is a test to check if “line-length” works nice\n'
'with: “unicode characters” that span accross bytes! ↺\n',
conf, problem1=(2, 53), problem2=(3, 53))
+
+ def test_with_dos_newlines(self):
+ conf = ('line-length: {max: 10}\n'
+ 'new-lines: {type: dos}\n'
+ 'new-line-at-end-of-file: disable\n')
+ self.check('---\r\nABCD EFGHI', conf)
+ self.check('---\r\nABCD EFGHI\r\n', conf)
+ self.check('---\r\nABCD EFGHIJ', conf, problem=(2, 11))
+ self.check('---\r\nABCD EFGHIJ\r\n', conf, problem=(2, 11))
diff --git a/tests/rules/test_new_lines.py b/tests/rules/test_new_lines.py
index c77a5d7..a76d608 100644
--- a/tests/rules/test_new_lines.py
+++ b/tests/rules/test_new_lines.py
@@ -31,16 +31,20 @@ class NewLinesTestCase(RuleTestCase):
self.check('---\r\ntext\r\n', conf)
def test_unix_type(self):
- conf = 'new-lines: {type: unix}'
+ conf = ('new-line-at-end-of-file: disable\n'
+ 'new-lines: {type: unix}\n')
self.check('', conf)
+ self.check('\r', conf)
self.check('\n', conf)
self.check('\r\n', conf, problem=(1, 1))
self.check('---\ntext\n', conf)
self.check('---\r\ntext\r\n', conf, problem=(1, 4))
def test_dos_type(self):
- conf = 'new-lines: {type: dos}\n'
+ conf = ('new-line-at-end-of-file: disable\n'
+ 'new-lines: {type: dos}\n')
self.check('', conf)
+ self.check('\r', conf)
self.check('\n', conf, problem=(1, 1))
self.check('\r\n', conf)
self.check('---\ntext\n', conf, problem=(1, 4))
diff --git a/tests/rules/test_truthy.py b/tests/rules/test_truthy.py
index b0402ed..82e4f6c 100644
--- a/tests/rules/test_truthy.py
+++ b/tests/rules/test_truthy.py
@@ -49,6 +49,54 @@ class TruthyTestCase(RuleTestCase):
problem3=(7, 3), problem4=(7, 7),
problem5=(8, 3), problem6=(8, 7))
+ def test_different_allowed_values(self):
+ conf = ('truthy:\n'
+ ' allowed-values: ["yes", "no"]\n')
+ self.check('---\n'
+ 'key1: foo\n'
+ 'key2: yes\n'
+ 'key3: bar\n'
+ 'key4: no\n', conf)
+ self.check('---\n'
+ 'key1: true\n'
+ 'key2: Yes\n'
+ 'key3: false\n'
+ 'key4: no\n'
+ 'key5: yes\n',
+ conf,
+ problem1=(2, 7), problem2=(3, 7),
+ problem3=(4, 7))
+
+ def test_combined_allowed_values(self):
+ conf = ('truthy:\n'
+ ' allowed-values: ["yes", "no", "true", "false"]\n')
+ self.check('---\n'
+ 'key1: foo\n'
+ 'key2: yes\n'
+ 'key3: bar\n'
+ 'key4: no\n', conf)
+ self.check('---\n'
+ 'key1: true\n'
+ 'key2: Yes\n'
+ 'key3: false\n'
+ 'key4: no\n'
+ 'key5: yes\n',
+ conf, problem1=(3, 7))
+
+ def test_no_allowed_values(self):
+ conf = ('truthy:\n'
+ ' allowed-values: []\n')
+ self.check('---\n'
+ 'key1: foo\n'
+ 'key2: bar\n', conf)
+ self.check('---\n'
+ 'key1: true\n'
+ 'key2: yes\n'
+ 'key3: false\n'
+ 'key4: no\n', conf,
+ problem1=(2, 7), problem2=(3, 7),
+ problem3=(4, 7), problem4=(5, 7))
+
def test_explicit_types(self):
conf = 'truthy: enable\n'
self.check('---\n'
diff --git a/tests/test_config.py b/tests/test_config.py
index f5d9402..d676382 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -174,6 +174,25 @@ class SimpleConfigTestCase(unittest.TestCase):
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': 'abc'})
+ Rule.CONF = {'multiple': ['item1', 'item2', 'item3']}
+ Rule.DEFAULT = {'multiple': ['item1']}
+ config.validate_rule_conf(Rule, {'multiple': []})
+ config.validate_rule_conf(Rule, {'multiple': ['item2']})
+ config.validate_rule_conf(Rule, {'multiple': ['item2', 'item3']})
+ config.validate_rule_conf(Rule, {})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule,
+ {'multiple': 'item1'})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule,
+ {'multiple': ['']})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule,
+ {'multiple': ['item1', 4]})
+ self.assertRaises(config.YamlLintConfigError,
+ config.validate_rule_conf, Rule,
+ {'multiple': ['item4']})
+
class ExtendedConfigTestCase(unittest.TestCase):
def test_extend_on_object(self):
diff --git a/yamllint/__init__.py b/yamllint/__init__.py
index 91c19eb..932054e 100644
--- a/yamllint/__init__.py
+++ b/yamllint/__init__.py
@@ -22,7 +22,7 @@ indentation, etc."""
APP_NAME = 'yamllint'
-APP_VERSION = '1.15.0'
+APP_VERSION = '1.16.0'
APP_DESCRIPTION = __doc__
__author__ = u'Adrien Vergé'
diff --git a/yamllint/cli.py b/yamllint/cli.py
index c5916e5..9975b4e 100644
--- a/yamllint/cli.py
+++ b/yamllint/cli.py
@@ -16,15 +16,15 @@
from __future__ import print_function
+import argparse
import os
-import sys
import platform
-import argparse
+import sys
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
+from yamllint import linter
from yamllint.config import YamlLintConfig, YamlLintConfigError
from yamllint.linter import PROBLEM_LEVELS
-from yamllint import linter
def find_files_recursively(items):
diff --git a/yamllint/config.py b/yamllint/config.py
index b4a7c1d..2cafe8a 100644
--- a/yamllint/config.py
+++ b/yamllint/config.py
@@ -134,12 +134,26 @@ def validate_rule_conf(rule, conf):
raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' %
(optkey, rule.ID))
+ # Example: CONF = {option: (bool, 'mixed')}
+ # → {option: true} → {option: mixed}
if isinstance(options[optkey], tuple):
if (conf[optkey] not in options[optkey] and
type(conf[optkey]) not in options[optkey]):
raise YamlLintConfigError(
'invalid config: option "%s" of "%s" should be in %s'
% (optkey, rule.ID, options[optkey]))
+ # Example: CONF = {option: ['flag1', 'flag2']}
+ # → {option: [flag1]} → {option: [flag1, flag2]}
+ elif isinstance(options[optkey], list):
+ if (type(conf[optkey]) is not list or
+ any(flag not in options[optkey]
+ for flag in conf[optkey])):
+ raise YamlLintConfigError(
+ ('invalid config: option "%s" of "%s" should only '
+ 'contain values in %s')
+ % (optkey, rule.ID, str(options[optkey])))
+ # Example: CONF = {option: int}
+ # → {option: 42}
else:
if not isinstance(conf[optkey], options[optkey]):
raise YamlLintConfigError(
diff --git a/yamllint/parser.py b/yamllint/parser.py
index ae1ed5f..de331f4 100644
--- a/yamllint/parser.py
+++ b/yamllint/parser.py
@@ -77,7 +77,10 @@ def line_generator(buffer):
cur = 0
next = buffer.find('\n')
while next != -1:
- yield Line(line_no, buffer, start=cur, end=next)
+ if next > 0 and buffer[next - 1] == '\r':
+ yield Line(line_no, buffer, start=cur, end=next - 1)
+ else:
+ yield Line(line_no, buffer, start=cur, end=next)
cur = next + 1
next = buffer.find('\n', cur)
line_no += 1
diff --git a/yamllint/rules/colons.py b/yamllint/rules/colons.py
index fd46bef..1a63cad 100644
--- a/yamllint/rules/colons.py
+++ b/yamllint/rules/colons.py
@@ -72,7 +72,7 @@ Use this rule to control the number of spaces before and after colons (``:``).
import yaml
-from yamllint.rules.common import spaces_after, spaces_before, is_explicit_key
+from yamllint.rules.common import is_explicit_key, spaces_after, spaces_before
ID = 'colons'
diff --git a/yamllint/rules/empty_lines.py b/yamllint/rules/empty_lines.py
index 335b125..d9a8c4d 100644
--- a/yamllint/rules/empty_lines.py
+++ b/yamllint/rules/empty_lines.py
@@ -66,27 +66,37 @@ DEFAULT = {'max': 2,
def check(conf, line):
if line.start == line.end and line.end < len(line.buffer):
# Only alert on the last blank line of a series
- if (line.end < len(line.buffer) - 1 and
- line.buffer[line.end + 1] == '\n'):
+ if (line.end + 2 <= len(line.buffer) and
+ line.buffer[line.end:line.end + 2] == '\n\n'):
+ return
+ elif (line.end + 4 <= len(line.buffer) and
+ line.buffer[line.end:line.end + 4] == '\r\n\r\n'):
return
blank_lines = 0
- while (line.start > blank_lines and
- line.buffer[line.start - blank_lines - 1] == '\n'):
+ start = line.start
+ while start >= 2 and line.buffer[start - 2:start] == '\r\n':
+ blank_lines += 1
+ start -= 2
+ while start >= 1 and line.buffer[start - 1] == '\n':
blank_lines += 1
+ start -= 1
max = conf['max']
# Special case: start of document
- if line.start - blank_lines == 0:
+ if start == 0:
blank_lines += 1 # first line doesn't have a preceding \n
max = conf['max-start']
# Special case: end of document
# NOTE: The last line of a file is always supposed to end with a new
# line. See POSIX definition of a line at:
- if line.end == len(line.buffer) - 1 and line.buffer[line.end] == '\n':
+ if ((line.end == len(line.buffer) - 1 and
+ line.buffer[line.end] == '\n') or
+ (line.end == len(line.buffer) - 2 and
+ line.buffer[line.end:line.end + 2] == '\r\n')):
# Allow the exception of the one-byte file containing '\n'
if line.end == 0:
return
diff --git a/yamllint/rules/indentation.py b/yamllint/rules/indentation.py
index 8bb7175..d83eb65 100644
--- a/yamllint/rules/indentation.py
+++ b/yamllint/rules/indentation.py
@@ -193,7 +193,7 @@ Use this rule to control the indentation.
import yaml
from yamllint.linter import LintProblem
-from yamllint.rules.common import is_explicit_key, get_real_end_line
+from yamllint.rules.common import get_real_end_line, is_explicit_key
ID = 'indentation'
diff --git a/yamllint/rules/new_lines.py b/yamllint/rules/new_lines.py
index 3aae90f..686bac2 100644
--- a/yamllint/rules/new_lines.py
+++ b/yamllint/rules/new_lines.py
@@ -36,10 +36,11 @@ DEFAULT = {'type': 'unix'}
def check(conf, line):
if line.start == 0 and len(line.buffer) > line.end:
if conf['type'] == 'dos':
- if line.buffer[line.end - 1:line.end + 1] != '\r\n':
+ if (line.end + 2 > len(line.buffer) or
+ line.buffer[line.end:line.end + 2] != '\r\n'):
yield LintProblem(1, line.end - line.start + 1,
'wrong new line character: expected \\r\\n')
else:
- if line.end > 0 and line.buffer[line.end - 1] == '\r':
- yield LintProblem(1, line.end - line.start,
+ if line.buffer[line.end] == '\r':
+ yield LintProblem(1, line.end - line.start + 1,
'wrong new line character: expected \\n')
diff --git a/yamllint/rules/truthy.py b/yamllint/rules/truthy.py
index b738dc2..3cf058b 100644
--- a/yamllint/rules/truthy.py
+++ b/yamllint/rules/truthy.py
@@ -15,13 +15,22 @@
# along with this program. If not, see .
"""
-Use this rule to forbid non-explictly typed truthy values other than ``true``
-and ``false``, for example ``YES``, ``False`` and ``off``.
+Use this rule to forbid non-explictly typed truthy values other than allowed
+ones (by default: ``true`` and ``false``), for example ``YES`` or ``off``.
This can be useful to prevent surprises from YAML parsers transforming
``[yes, FALSE, Off]`` into ``[true, false, false]`` or
``{y: 1, yes: 2, on: 3, true: 4, True: 5}`` into ``{y: 1, true: 5}``.
+.. rubric:: Options
+
+* ``allowed-values`` defines the list of truthy values which will be ignored
+ during linting. The default is ``['true', 'false']``, but can be changed to
+ any list containing: ``'TRUE'``, ``'True'``, ``'true'``, ``'FALSE'``,
+ ``'False'``, ``'false'``, ``'YES'``, ``'Yes'``, ``'yes'``, ``'NO'``,
+ ``'No'``, ``'no'``, ``'ON'``, ``'On'``, ``'on'``, ``'OFF'``, ``'Off'``,
+ ``'off'``.
+
.. rubric:: Examples
#. With ``truthy: {}``
@@ -63,29 +72,55 @@ This can be useful to prevent surprises from YAML parsers transforming
yes: 1
on: 2
True: 3
+
+#. With ``truthy: {allowed-values: ["yes", "no"]}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ - yes
+ - no
+ - "true"
+ - 'false'
+ - foo
+ - bar
+
+ the following code snippet would **FAIL**:
+ ::
+
+ - true
+ - false
+ - on
+ - off
"""
import yaml
from yamllint.linter import LintProblem
-ID = 'truthy'
-TYPE = 'token'
TRUTHY = ['YES', 'Yes', 'yes',
'NO', 'No', 'no',
- 'TRUE', 'True', # 'true' is a boolean
- 'FALSE', 'False', # 'false' is a boolean
+ 'TRUE', 'True', 'true',
+ 'FALSE', 'False', 'false',
'ON', 'On', 'on',
'OFF', 'Off', 'off']
+ID = 'truthy'
+TYPE = 'token'
+CONF = {'allowed-values': list(TRUTHY)}
+DEFAULT = {'allowed-values': ['true', 'false']}
+
+
def check(conf, token, prev, next, nextnext, context):
if prev and isinstance(prev, yaml.tokens.TagToken):
return
if isinstance(token, yaml.tokens.ScalarToken):
- if token.value in TRUTHY and token.style is None:
+ if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and
+ token.style is None):
yield LintProblem(token.start_mark.line + 1,
token.start_mark.column + 1,
- "truthy value should be true or false")
+ "truthy value should be one of [" +
+ ", ".join(sorted(conf['allowed-values'])) + "]")