Compare commits

..

29 Commits

Author SHA1 Message Date
Adrien Vergé
483a8d89a5 yamllint version 1.22.1 2020-04-15 07:55:57 +02:00
Adrien Vergé
fa87913566 quoted-strings: Fix only-when-needed on corner cases
Change implementation of `required: only-when-needed`, because
maintaining a list of `START_TOKENS` and just looking at the first
character of string values has proven to be partially broken.

Cf. discussion at
https://github.com/adrienverge/yamllint/pull/246#issuecomment-612354097.

Fixes https://github.com/adrienverge/yamllint/issues/242 and
https://github.com/adrienverge/yamllint/pull/244.
2020-04-15 07:48:59 +02:00
Adrien Vergé
961c496b4f yamllint version 1.22.0 2020-04-13 14:32:08 +02:00
Adrien Vergé
ce7d3fcc7b quoted-strings: Remove test_quotes_required()
It is exactly the same tests as `test_quote_type_any()`.
2020-04-13 14:28:02 +02:00
Adrien Vergé
0bffba1e13 quoted-strings: Remove test_single_quotes_required()
It is exactly the same tests as `test_quote_type_single()`.
2020-04-13 14:28:02 +02:00
Adrien Vergé
2d8639c3a1 quoted-strings: Fix broken rule for list items
The rule worked for values like:

    flow-map: {a: foo, b: "bar"}
    block-map:
      a: foo
      b: "bar"

But not for:

    flow-seq: [foo, "bar"]
    block-seq:
      - foo
      - "bar"

Also add tests to make sure there will be no regression.

Fixes: #208.
2020-04-13 14:15:29 +02:00
Adrien Vergé
e284d74be1 quoted-strings: Rename tests names for clarity
And move only-when-needed tests at the end for readability.
2020-04-13 14:15:29 +02:00
Adrien Vergé
1a13837e84 docs: Sunset Python 2
Keep supporting Python 2.7 for one extra year after upstream dropped it:
https://www.python.org/doc/sunset-python-2/
2020-04-09 16:29:43 +02:00
Adrien Vergé
46ed0c02be truthy: Add missing test removed from PR
See https://github.com/adrienverge/yamllint/pull/247#discussion_r405421376.
2020-04-08 12:31:12 +02:00
ilyam8
6ce11dedb4 truthy: add check-keys option 2020-04-08 12:26:21 +02:00
Adrien Vergé
542ae758f5 yamllint version 1.21.0 2020-03-24 07:53:14 +01:00
Rui Pinge
3a6a09b7b6 Add support for redundant quotes in quoted-strings rule
Co-Authored-By: Adrien Vergé
2020-03-24 07:44:07 +01:00
Rui Pinge
15aea73fbe Fix quoted-strings rules not working for string values matching scalars 2020-03-14 14:22:29 +01:00
Martin Packman
91763f5476 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
2020-02-13 12:02:45 +01:00
Martin Packman
5b049e4229 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.
2020-02-13 12:02:45 +01:00
Adrien Vergé
044c7f0248 cli: Test unicode chars in paths too 2020-01-17 16:01:05 +01:00
Adrien Vergé
734d5d5f73 CI: Run tests on Python 3.8
Python 3.8 was released in October 2019.
2020-01-03 09:33:35 +01:00
dhutty
fd86455076 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.
2020-01-03 09:29:33 +01:00
Adrien Vergé
13a0f11e7c yamllint version 1.20.0 2019-12-26 16:06:29 +01:00
Sylvestre Ledru
43b95e99d1 Use 'syntax' as rule name upon syntax errors 2019-12-17 19:29:49 +01:00
ffapitalle
8fa9eb3ced Add --no-warnings option to suppress warning messages
Use `--no-warnings` option to hide warning messages. It only shows
problems marked as errors.
2019-12-12 09:12:53 +01:00
Adrien Vergé
da3788e95a yamllint version 1.19.0 2019-11-19 11:28:21 +01:00
Joel Baranick
fb400dc64b Allow disabling all checks for a file
Allow disabling of a file, even if it is invalid YAML (syntax error) by
including `# yamllint disable-file` in the first line.
2019-11-19 11:26:31 +01:00
Adrien Vergé
92324ae730 yamllint version 1.18.0 2019-10-15 09:49:20 +02:00
Imran Iqbal
7359785ea0 fix(default.yaml): disable empty-values & octal-values by default
* Close #204
2019-10-15 09:41:32 +02:00
Hossein Zolfi
579a975b70 docs: Fix pre-commit config file
* pre-commit show warning for unsupported key (sha)
* Demonstrate how to use custom yamllint
2019-10-01 11:36:07 +02:00
Imran Iqbal
f3d9196aa0 docs(configuration): improve yaml-files code example
* A straight copy/paste of the existing example into the `.yamllint` file results in a `yamllint` error!
2019-09-10 19:47:25 +02:00
Ibrahim AshShohail
881d301883 feat: Support reading config from .yamllint.yml and .yamllint.yaml
Signed-off-by: Ibrahim AshShohail <me@ibrasho.com>
2019-08-27 09:49:09 +02:00
Adrien Vergé
b62b424dd4 feat: Lint .yamllint by default 2019-08-26 10:01:40 +02:00
21 changed files with 838 additions and 279 deletions

View File

@@ -3,10 +3,10 @@ dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069)
language: python language: python
python: python:
- 2.7 - 2.7
- 3.4
- 3.5 - 3.5
- 3.6 - 3.6
- 3.7 - 3.7
- 3.8
- nightly - nightly
install: install:
- pip install pyyaml coveralls flake8 flake8-import-order doc8 - pip install pyyaml coveralls flake8 flake8-import-order doc8

View File

@@ -1,6 +1,45 @@
Changelog Changelog
========= =========
1.22.1 (2020-04-15)
-------------------
- Fix ``quoted-strings`` rule with ``only-when-needed`` on corner cases
1.22.0 (2020-04-13)
-------------------
- Add ``check-keys`` option to the ``truthy`` rule
- Fix ``quoted-strings`` rule not working on sequences items
- Sunset Python 2
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)
-------------------
- Add --no-warnings option to suppress warning messages
- Use 'syntax' as rule name upon syntax errors
1.19.0 (2019-11-19)
-------------------
- Allow disabling all checks for a file with ``# yamllint disable-file``
1.18.0 (2019-10-15)
-------------------
- Lint ``.yamllint`` config file by default
- Also read config from ``.yamllint.yml`` and ``.yamllint.yaml``
- Improve documentation for ``yaml-files``
- Update documentation for ``pre-commit``
- Explicitly disable ``empty-values`` and ``octal-values`` rules
1.17.0 (2019-08-12) 1.17.0 (2019-08-12)
------------------- -------------------

View File

@@ -21,6 +21,10 @@ indentation, etc.
Written in Python (compatible with Python 2 & 3). Written in Python (compatible with Python 2 & 3).
⚠ Python 2 upstream support stopped on January 1, 2020. yamllint will keep
best-effort support for Python 2.7 until January 1, 2021. Passed that date,
yamllint will drop all Python 2-related code.
Documentation Documentation
------------- -------------

View File

@@ -14,7 +14,8 @@ To use a custom configuration file, use the ``-c`` option:
If ``-c`` is not provided, yamllint will look for a configuration file in the If ``-c`` is not provided, yamllint will look for a configuration file in the
following locations (by order of preference): following locations (by order of preference):
- ``.yamllint`` in the current working directory - ``.yamllint``, ``.yamllint.yaml`` or ``.yamllint.yml`` in the current working
directory
- ``$XDG_CONFIG_HOME/yamllint/config`` - ``$XDG_CONFIG_HOME/yamllint/config``
- ``~/.config/yamllint/config`` - ``~/.config/yamllint/config``
@@ -115,6 +116,9 @@ return code will be:
* ``1`` if one or more errors occur * ``1`` if one or more errors occur
* ``2`` if no errors occur, but one or more warnings occur * ``2`` if no errors occur, but one or more warnings occur
If the script is invoked with the ``--no-warnings`` option, it won't output
warning level problems, only error level ones.
YAML files extensions YAML files extensions
--------------------- ---------------------
@@ -126,6 +130,7 @@ configuration option. The default is:
yaml-files: yaml-files:
- '*.yaml' - '*.yaml'
- '*.yml' - '*.yml'
- '.yamllint'
The same rules as for ignoring paths apply (``.gitignore``-style path pattern, The same rules as for ignoring paths apply (``.gitignore``-style path pattern,
see below). see below).

View File

@@ -73,3 +73,31 @@ It is possible, although not recommend, to disabled **all** rules:
If you need to disable multiple rules, it is allowed to chain rules like this: If you need to disable multiple rules, it is allowed to chain rules like this:
``# yamllint disable rule:hyphens rule:commas rule:indentation``. ``# yamllint disable rule:hyphens rule:commas rule:indentation``.
Disabling all checks for a file
-------------------------------
To prevent yamllint from reporting problems for a specific file, add the
directive comment ``# yamllint disable-file`` as the first line of the file.
For instance:
.. code-block:: yaml
# yamllint disable-file
# The following mapping contains the same key twice, but I know what I'm doing:
key: value 1
key: value 2
- This line is waaaaaaaaaay too long but yamllint will not report anything about it.
This line will be checked by yamllint.
or:
.. code-block:: jinja
# yamllint disable-file
# This file is not valid YAML because it is a Jinja template
{% if extra_info %}
key1: value1
{% endif %}
key2: value2

View File

@@ -10,8 +10,10 @@ Here is an example, to add in your .pre-commit-config.yaml
.. code:: yaml .. code:: yaml
--- ---
# Update the sha variable with the release version that you want, from the yamllint repo # Update the rev variable with the release version that you want, from the yamllint repo
# You can pass your custom .yamllint with args attribute.
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
sha: v1.8.1 rev: v1.17.0
hooks: hooks:
- id: yamllint - id: yamllint
args: [-c=/path/to/.yamllint]

View File

@@ -28,7 +28,9 @@ class OctalValuesTestCase(RuleTestCase):
self.check('user-city: 0o10', conf) self.check('user-city: 0o10', conf)
def test_implicit_octal_values(self): def test_implicit_octal_values(self):
conf = ('octal-values: {forbid-implicit-octal: true}\n' conf = ('octal-values:\n'
' forbid-implicit-octal: true\n'
' forbid-explicit-octal: false\n'
'new-line-at-end-of-file: disable\n' 'new-line-at-end-of-file: disable\n'
'document-start: disable\n') 'document-start: disable\n')
self.check('user-city: 010', conf, problem=(1, 15)) self.check('user-city: 010', conf, problem=(1, 15))
@@ -50,7 +52,9 @@ class OctalValuesTestCase(RuleTestCase):
' - 0e3\n', conf) ' - 0e3\n', conf)
def test_explicit_octal_values(self): def test_explicit_octal_values(self):
conf = ('octal-values: {forbid-explicit-octal: true}\n' conf = ('octal-values:\n'
' forbid-implicit-octal: false\n'
' forbid-explicit-octal: true\n'
'new-line-at-end-of-file: disable\n' 'new-line-at-end-of-file: disable\n'
'document-start: disable\n') 'document-start: disable\n')
self.check('user-city: 0o10', conf, problem=(1, 16)) self.check('user-city: 0o10', conf, problem=(1, 16))

View File

@@ -22,6 +22,7 @@ class QuotedTestCase(RuleTestCase):
def test_disabled(self): def test_disabled(self):
conf = 'quoted-strings: disable' conf = 'quoted-strings: disable'
self.check('---\n' self.check('---\n'
'foo: bar\n', conf) 'foo: bar\n', conf)
self.check('---\n' self.check('---\n'
@@ -30,23 +31,34 @@ class QuotedTestCase(RuleTestCase):
'foo: \'bar\'\n', conf) 'foo: \'bar\'\n', conf)
self.check('---\n' self.check('---\n'
'bar: 123\n', conf) 'bar: 123\n', conf)
self.check('---\n'
'bar: "123"\n', conf)
def test_quote_type_any(self): def test_quote_type_any(self):
conf = 'quoted-strings: {quote-type: any}\n' conf = 'quoted-strings: {quote-type: any}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' # fails 'string1: foo\n' # fails
'string2: "foo"\n' 'string2: "foo"\n'
'string3: \'bar\'\n' 'string3: "true"\n'
'string4: !!str genericstring\n' 'string4: "123"\n'
'string5: !!str 456\n' 'string5: \'bar\'\n'
'string6: !!str "quotedgenericstring"\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem=(4, 10)) 'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(4, 10), problem2=(17, 5),
problem3=(19, 12), problem4=(20, 15))
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -55,7 +67,7 @@ class QuotedTestCase(RuleTestCase):
' word 1\n' ' word 1\n'
' word 2\n' ' word 2\n'
'multiline string 3:\n' 'multiline string 3:\n'
' word 1\n' ' word 1\n' # fails
' word 2\n' ' word 2\n'
'multiline string 4:\n' 'multiline string 4:\n'
' "word 1\\\n' ' "word 1\\\n'
@@ -64,20 +76,31 @@ class QuotedTestCase(RuleTestCase):
def test_quote_type_single(self): def test_quote_type_single(self):
conf = 'quoted-strings: {quote-type: single}\n' conf = 'quoted-strings: {quote-type: single}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' # fails 'string1: foo\n' # fails
'string2: "foo"\n' # fails 'string2: "foo"\n' # fails
'string3: \'bar\'\n' 'string3: "true"\n' # fails
'string4: !!str genericstring\n' 'string4: "123"\n' # fails
'string5: !!str 456\n' 'string5: \'bar\'\n'
'string6: !!str "quotedgenericstring"\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(4, 10), problem2=(5, 10)) 'block-seq:\n'
' - foo\n' # fails
' - "foo"\n' # fails
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(4, 10), problem2=(5, 10), problem3=(6, 10),
problem4=(7, 10), problem5=(17, 5), problem6=(18, 5),
problem7=(19, 12), problem8=(19, 17), problem9=(20, 15),
problem10=(20, 23))
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -86,7 +109,7 @@ class QuotedTestCase(RuleTestCase):
' word 1\n' ' word 1\n'
' word 2\n' ' word 2\n'
'multiline string 3:\n' 'multiline string 3:\n'
' word 1\n' ' word 1\n' # fails
' word 2\n' ' word 2\n'
'multiline string 4:\n' 'multiline string 4:\n'
' "word 1\\\n' ' "word 1\\\n'
@@ -95,20 +118,68 @@ class QuotedTestCase(RuleTestCase):
def test_quote_type_double(self): def test_quote_type_double(self):
conf = 'quoted-strings: {quote-type: double}\n' conf = 'quoted-strings: {quote-type: double}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' # fails 'string1: foo\n' # fails
'string2: "foo"\n' 'string2: "foo"\n'
'string3: \'bar\'\n' # fails 'string3: "true"\n'
'string4: !!str genericstring\n' 'string4: "123"\n'
'string5: !!str 456\n' 'string5: \'bar\'\n' # fails
'string6: !!str "quotedgenericstring"\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(4, 10), problem2=(6, 10)) 'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(4, 10), problem2=(8, 10), problem3=(17, 5),
problem4=(19, 12), problem5=(20, 15))
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_any_quotes_not_required(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'
'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf)
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -122,4 +193,167 @@ class QuotedTestCase(RuleTestCase):
'multiline string 4:\n' 'multiline string 4:\n'
' "word 1\\\n' ' "word 1\\\n'
' word 2"\n', ' word 2"\n',
conf, problem1=(9, 3)) conf)
def test_single_quotes_not_required(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'
'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10),
problem4=(18, 5), problem5=(19, 17), problem6=(20, 23))
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_only_when_needed(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'
'block-seq:\n'
' - foo\n'
' - "foo"\n' # fails
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(5, 10), problem2=(8, 10), problem3=(18, 5),
problem4=(19, 17), problem5=(20, 23))
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_only_when_needed_single_quotes(self):
conf = ('quoted-strings: {quote-type: single,\n'
' 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'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n'
'block-seq:\n'
' - foo\n'
' - "foo"\n' # fails
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10),
problem4=(8, 10), problem5=(18, 5), problem6=(19, 17),
problem7=(20, 23))
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_only_when_needed_corner_cases(self):
conf = 'quoted-strings: {required: only-when-needed}\n'
self.check('---\n'
'- ""\n'
'- "- item"\n'
'- "key: value"\n'
'- "%H:%M:%S"\n'
'- "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
'- \'"quoted"\'\n'
'- "\'foo\' == \'bar\'"\n'
'- "\'Mac\' in ansible_facts.product_name"\n',
conf)
self.check('---\n'
'k1: ""\n'
'k2: "- item"\n'
'k3: "key: value"\n'
'k4: "%H:%M:%S"\n'
'k5: "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
'k6: \'"quoted"\'\n'
'k7: "\'foo\' == \'bar\'"\n'
'k8: "\'Mac\' in ansible_facts.product_name"\n',
conf)
self.check('---\n'
'- ---\n'
'- "---"\n' # fails
'- ----------\n'
'- "----------"\n' # fails
'- :wq\n'
'- ":wq"\n', # fails
conf, problem1=(3, 3), problem2=(5, 3), problem3=(7, 3))
self.check('---\n'
'k1: ---\n'
'k2: "---"\n' # fails
'k3: ----------\n'
'k4: "----------"\n' # fails
'k5: :wq\n'
'k6: ":wq"\n', # fails
conf, problem1=(3, 5), problem2=(5, 5), problem3=(7, 5))

View File

@@ -114,3 +114,33 @@ class TruthyTestCase(RuleTestCase):
'boolean5: !!bool off\n' 'boolean5: !!bool off\n'
'boolean6: !!bool NO\n', 'boolean6: !!bool NO\n',
conf) conf)
def test_check_keys_disabled(self):
conf = ('truthy:\n'
' allowed-values: []\n'
' check-keys: false\n'
'key-duplicates: disable\n')
self.check('---\n'
'YES: 0\n'
'Yes: 0\n'
'yes: 0\n'
'No: 0\n'
'No: 0\n'
'no: 0\n'
'TRUE: 0\n'
'True: 0\n'
'true: 0\n'
'FALSE: 0\n'
'False: 0\n'
'false: 0\n'
'ON: 0\n'
'On: 0\n'
'on: 0\n'
'OFF: 0\n'
'Off: 0\n'
'off: 0\n'
'YES:\n'
' Yes:\n'
' yes:\n'
' on: 0\n',
conf)

View File

@@ -32,6 +32,29 @@ from yamllint import cli
from yamllint import config 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): class CommandLineTestCase(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -59,12 +82,15 @@ class CommandLineTestCase(unittest.TestCase):
'no-yaml.json': '---\n' 'no-yaml.json': '---\n'
'key: value\n', 'key: value\n',
# non-ASCII chars # non-ASCII chars
'non-ascii/utf-8': ( 'non-ascii/éçäγλνπ¥/utf-8': (
u'---\n' u'---\n'
u'- hétérogénéité\n' u'- hétérogénéité\n'
u'# 19.99 €\n' u'# 19.99 €\n'
u'- お早う御座います。\n' u'- お早う御座います。\n'
u'# الأَبْجَدِيَّة العَرَبِيَّة\n').encode('utf-8'), u'# الأَبْجَدِيَّة العَرَبِيَّة\n').encode('utf-8'),
# dos line endings yaml
'dos.yml': '---\r\n'
'dos: true',
}) })
@classmethod @classmethod
@@ -78,6 +104,7 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'), [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, '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, '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, 'sub/ok.yaml'),
@@ -123,7 +150,8 @@ class CommandLineTestCase(unittest.TestCase):
' - \'*.yml\'\n') ' - \'*.yml\'\n')
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), 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' conf = config.YamlLintConfig('extends: default\n'
@@ -140,9 +168,10 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'), [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, 'empty.yml'),
os.path.join(self.wd, 'no-yaml.json'), 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, '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, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')] os.path.join(self.wd, 'warn.yaml')]
@@ -156,9 +185,10 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'), [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, 'empty.yml'),
os.path.join(self.wd, 'no-yaml.json'), 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, '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, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')] os.path.join(self.wd, 'warn.yaml')]
@@ -170,205 +200,148 @@ class CommandLineTestCase(unittest.TestCase):
' - \'**/utf-8\'\n') ' - \'**/utf-8\'\n')
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), 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): def test_run_with_bad_arguments(self):
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(()) cli.run(())
self.assertNotEqual(ctx.returncode, 0)
self.assertEqual(ctx.stdout, '')
self.assertRegexpMatches(ctx.stderr, r'^usage')
self.assertNotEqual(ctx.exception.code, 0) with RunContext(self) as ctx:
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:
cli.run(('--unknown-arg', )) 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) with RunContext(self) as ctx:
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:
cli.run(('-c', './conf.yaml', '-d', 'relaxed', 'file')) cli.run(('-c', './conf.yaml', '-d', 'relaxed', 'file'))
self.assertNotEqual(ctx.returncode, 0)
self.assertNotEqual(ctx.exception.code, 0) self.assertEqual(ctx.stdout, '')
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertRegexpMatches( self.assertRegexpMatches(
err.splitlines()[-1], ctx.stderr.splitlines()[-1],
r'^yamllint: error: argument -d\/--config-data: ' r'^yamllint: error: argument -d\/--config-data: '
r'not allowed with argument -c\/--config-file$' r'not allowed with argument -c\/--config-file$'
) )
# checks if reading from stdin and files are mutually exclusive # checks if reading from stdin and files are mutually exclusive
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(('-', 'file')) cli.run(('-', 'file'))
self.assertNotEqual(ctx.returncode, 0)
self.assertNotEqual(ctx.exception.code, 0) self.assertEqual(ctx.stdout, '')
self.assertRegexpMatches(ctx.stderr, r'^usage')
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertRegexpMatches(err, r'^usage')
def test_run_with_bad_config(self): def test_run_with_bad_config(self):
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(('-d', 'rules: {a: b}', 'file')) cli.run(('-d', 'rules: {a: b}', 'file'))
self.assertEqual(ctx.returncode, -1)
self.assertEqual(ctx.exception.code, -1) self.assertEqual(ctx.stdout, '')
self.assertRegexpMatches(ctx.stderr, r'^invalid config: no such rule')
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertRegexpMatches(err, r'^invalid config: no such rule')
def test_run_with_empty_config(self): def test_run_with_empty_config(self):
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(('-d', '', 'file')) cli.run(('-d', '', 'file'))
self.assertEqual(ctx.returncode, -1)
self.assertEqual(ctx.exception.code, -1) self.assertEqual(ctx.stdout, '')
self.assertRegexpMatches(ctx.stderr, r'^invalid config: not a dict')
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertRegexpMatches(err, r'^invalid config: not a dict')
def test_run_with_config_file(self): def test_run_with_config_file(self):
with open(os.path.join(self.wd, 'config'), 'w') as f: with open(os.path.join(self.wd, 'config'), 'w') as f:
f.write('rules: {trailing-spaces: disable}') 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'))) 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: with open(os.path.join(self.wd, 'config'), 'w') as f:
f.write('rules: {trailing-spaces: enable}') 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'))) 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): def test_run_with_user_global_config_file(self):
home = os.path.join(self.wd, 'fake-home') home = os.path.join(self.wd, 'fake-home')
os.mkdir(home) dir = os.path.join(home, '.config', 'yamllint')
dir = os.path.join(home, '.config') os.makedirs(dir)
os.mkdir(dir)
dir = os.path.join(dir, 'yamllint')
os.mkdir(dir)
config = os.path.join(dir, 'config') config = os.path.join(dir, 'config')
temp = os.environ['HOME'] self.addCleanup(os.environ.update, HOME=os.environ['HOME'])
os.environ['HOME'] = home os.environ['HOME'] = home
with open(config, 'w') as f: with open(config, 'w') as f:
f.write('rules: {trailing-spaces: disable}') 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'), )) 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: with open(config, 'w') as f:
f.write('rules: {trailing-spaces: enable}') 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'), )) cli.run((os.path.join(self.wd, 'a.yaml'), ))
self.assertEqual(ctx.exception.code, 1) self.assertEqual(ctx.returncode, 1)
os.environ['HOME'] = temp
def test_run_version(self): def test_run_version(self):
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(('--version', )) cli.run(('--version', ))
self.assertEqual(ctx.returncode, 0)
self.assertEqual(ctx.exception.code, 0) self.assertRegexpMatches(ctx.stdout + ctx.stderr, r'yamllint \d+\.\d+')
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertRegexpMatches(out + err, r'yamllint \d+\.\d+')
def test_run_non_existing_file(self): def test_run_non_existing_file(self):
file = os.path.join(self.wd, 'i-do-not-exist.yaml') path = os.path.join(self.wd, 'i-do-not-exist.yaml')
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run(('-f', 'parsable', path))
cli.run(('-f', 'parsable', file)) self.assertEqual(ctx.returncode, -1)
self.assertEqual(ctx.stdout, '')
self.assertEqual(ctx.exception.code, -1) self.assertRegexpMatches(ctx.stderr, r'No such file or directory')
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertRegexpMatches(err, r'No such file or directory')
def test_run_one_problem_file(self): 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 RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run(('-f', 'parsable', path))
cli.run(('-f', 'parsable', file)) self.assertEqual(ctx.returncode, 1)
self.assertEqual(ctx.stdout, (
self.assertEqual(ctx.exception.code, 1)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'%s:2:4: [error] trailing spaces (trailing-spaces)\n' '%s:2:4: [error] trailing spaces (trailing-spaces)\n'
'%s:3:4: [error] no new line character at the end of file ' '%s:3:4: [error] no new line character at the end of file '
'(new-line-at-end-of-file)\n') % (file, file)) '(new-line-at-end-of-file)\n' % (path, path)))
self.assertEqual(err, '') self.assertEqual(ctx.stderr, '')
def test_run_one_warning(self): def test_run_one_warning(self):
file = os.path.join(self.wd, 'warn.yaml') path = os.path.join(self.wd, 'warn.yaml')
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run(('-f', 'parsable', path))
cli.run(('-f', 'parsable', file)) self.assertEqual(ctx.returncode, 0)
self.assertEqual(ctx.exception.code, 0)
def test_run_warning_in_strict_mode(self): 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 RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run(('-f', 'parsable', '--strict', path))
cli.run(('-f', 'parsable', '--strict', file)) self.assertEqual(ctx.returncode, 2)
self.assertEqual(ctx.exception.code, 2)
def test_run_one_ok_file(self): def test_run_one_ok_file(self):
file = os.path.join(self.wd, 'sub', 'ok.yaml') path = os.path.join(self.wd, 'sub', 'ok.yaml')
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run(('-f', 'parsable', path))
cli.run(('-f', 'parsable', file)) self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
self.assertEqual(ctx.exception.code, 0)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertEqual(err, '')
def test_run_empty_file(self): 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 RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run(('-f', 'parsable', path))
cli.run(('-f', 'parsable', file)) self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
self.assertEqual(ctx.exception.code, 0)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertEqual(err, '')
def test_run_non_ascii_file(self): 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" # Make sure the default localization conditions on this "system"
# support UTF-8 encoding. # support UTF-8 encoding.
@@ -377,63 +350,46 @@ class CommandLineTestCase(unittest.TestCase):
locale.setlocale(locale.LC_ALL, 'C.UTF-8') locale.setlocale(locale.LC_ALL, 'C.UTF-8')
except locale.Error: except locale.Error:
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
self.addCleanup(locale.setlocale, locale.LC_ALL, loc)
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run(('-f', 'parsable', path))
cli.run(('-f', 'parsable', file)) self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
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, '')
def test_run_multiple_files(self): def test_run_multiple_files(self):
items = [os.path.join(self.wd, 'empty.yml'), items = [os.path.join(self.wd, 'empty.yml'),
os.path.join(self.wd, 's')] 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 RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(['-f', 'parsable'] + items) cli.run(['-f', 'parsable'] + items)
self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
self.assertEqual(ctx.exception.code, 1) self.assertEqual(ctx.stdout, (
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'%s:3:1: [error] duplication of key "key" in mapping ' '%s:3:1: [error] duplication of key "key" in mapping '
'(key-duplicates)\n') % file) '(key-duplicates)\n') % path)
self.assertEqual(err, '')
def test_run_piped_output_nocolor(self): def test_run_piped_output_nocolor(self):
file = os.path.join(self.wd, 'a.yaml') path = os.path.join(self.wd, 'a.yaml')
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run((path, ))
cli.run((file, )) self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
self.assertEqual(ctx.stdout, (
self.assertEqual(ctx.exception.code, 1)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'%s\n' '%s\n'
' 2:4 error trailing spaces (trailing-spaces)\n' ' 2:4 error trailing spaces (trailing-spaces)\n'
' 3:4 error no new line character at the end of file ' ' 3:4 error no new line character at the end of file '
'(new-line-at-end-of-file)\n' '(new-line-at-end-of-file)\n'
'\n' % file)) '\n' % path))
self.assertEqual(err, '')
def test_run_default_format_output_in_tty(self): 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 # Create a pseudo-TTY and redirect stdout to it
master, slave = pty.openpty() master, slave = pty.openpty()
sys.stdout = sys.stderr = os.fdopen(slave, 'w') sys.stdout = sys.stderr = os.fdopen(slave, 'w')
with self.assertRaises(SystemExit) as ctx: with self.assertRaises(SystemExit) as ctx:
cli.run((file, )) cli.run((path, ))
sys.stdout.flush() sys.stdout.flush()
self.assertEqual(ctx.exception.code, 1) self.assertEqual(ctx.exception.code, 1)
@@ -456,79 +412,108 @@ class CommandLineTestCase(unittest.TestCase):
' \033[2m3:4\033[0m \033[31merror\033[0m ' ' \033[2m3:4\033[0m \033[31merror\033[0m '
'no new line character at the end of file ' 'no new line character at the end of file '
'\033[2m(new-line-at-end-of-file)\033[0m\n' '\033[2m(new-line-at-end-of-file)\033[0m\n'
'\n' % file)) '\n' % path))
def test_run_default_format_output_without_tty(self): def test_run_default_format_output_without_tty(self):
file = os.path.join(self.wd, 'a.yaml') path = os.path.join(self.wd, 'a.yaml')
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run((path, ))
cli.run((file, )) expected_out = (
self.assertEqual(ctx.exception.code, 1)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'%s\n' '%s\n'
' 2:4 error trailing spaces (trailing-spaces)\n' ' 2:4 error trailing spaces (trailing-spaces)\n'
' 3:4 error no new line character at the end of file ' ' 3:4 error no new line character at the end of file '
'(new-line-at-end-of-file)\n' '(new-line-at-end-of-file)\n'
'\n' % file)) '\n' % path)
self.assertEqual(err, '') self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_run_auto_output_without_tty_output(self): 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 RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run((path, '--format', 'auto'))
cli.run((file, '--format', 'auto')) expected_out = (
self.assertEqual(ctx.exception.code, 1)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'%s\n' '%s\n'
' 2:4 error trailing spaces (trailing-spaces)\n' ' 2:4 error trailing spaces (trailing-spaces)\n'
' 3:4 error no new line character at the end of file ' ' 3:4 error no new line character at the end of file '
'(new-line-at-end-of-file)\n' '(new-line-at-end-of-file)\n'
'\n' % file)) '\n' % path)
self.assertEqual(err, '') self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_run_format_colored(self): def test_run_format_colored(self):
file = os.path.join(self.wd, 'a.yaml') path = os.path.join(self.wd, 'a.yaml')
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run((path, '--format', 'colored'))
cli.run((file, '--format', 'colored')) expected_out = (
self.assertEqual(ctx.exception.code, 1)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'\033[4m%s\033[0m\n' '\033[4m%s\033[0m\n'
' \033[2m2:4\033[0m \033[31merror\033[0m ' ' \033[2m2:4\033[0m \033[31merror\033[0m '
'trailing spaces \033[2m(trailing-spaces)\033[0m\n' 'trailing spaces \033[2m(trailing-spaces)\033[0m\n'
' \033[2m3:4\033[0m \033[31merror\033[0m ' ' \033[2m3:4\033[0m \033[31merror\033[0m '
'no new line character at the end of file ' 'no new line character at the end of file '
'\033[2m(new-line-at-end-of-file)\033[0m\n' '\033[2m(new-line-at-end-of-file)\033[0m\n'
'\n' % file)) '\n' % path)
self.assertEqual(err, '') self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_run_read_from_stdin(self): def test_run_read_from_stdin(self):
# prepares stdin with an invalid yaml string so that we can check # prepares stdin with an invalid yaml string so that we can check
# for its specific error, and be assured that stdin was read # 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( sys.stdin = StringIO(
'I am a string\n' 'I am a string\n'
'therefore: I am an error\n') 'therefore: I am an error\n')
with self.assertRaises(SystemExit) as ctx: with RunContext(self) as ctx:
cli.run(('-', '-f', 'parsable')) cli.run(('-', '-f', 'parsable'))
expected_out = (
self.assertNotEqual(ctx.exception.code, 0)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'stdin:2:10: [error] syntax error: ' 'stdin:2:10: [error] syntax error: '
'mapping values are not allowed here\n')) 'mapping values are not allowed here (syntax)\n')
self.assertEqual(err, '') self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_run_no_warnings(self):
path = os.path.join(self.wd, 'a.yaml')
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' % path)
self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
path = os.path.join(self.wd, 'warn.yaml')
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):
path = os.path.join(self.wd, 'warn.yaml')
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, ''))

View File

@@ -448,11 +448,13 @@ class IgnorePathConfigTestCase(unittest.TestCase):
out = sys.stdout.getvalue() out = sys.stdout.getvalue()
out = '\n'.join(sorted(out.splitlines())) out = '\n'.join(sorted(out.splitlines()))
docstart = '[warning] missing document start "---" (document-start)'
keydup = '[error] duplication of key "key" in mapping (key-duplicates)' keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
trailing = '[error] trailing spaces (trailing-spaces)' trailing = '[error] trailing spaces (trailing-spaces)'
hyphen = '[error] too many spaces after hyphen (hyphens)' hyphen = '[error] too many spaces after hyphen (hyphens)'
self.assertEqual(out, '\n'.join(( self.assertEqual(out, '\n'.join((
'./.yamllint:1:1: ' + docstart,
'./bin/file.lint-me-anyway.yaml:3:3: ' + keydup, './bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
'./bin/file.lint-me-anyway.yaml:4:17: ' + trailing, './bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
'./bin/file.lint-me-anyway.yaml:5:5: ' + hyphen, './bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,

View File

@@ -302,3 +302,104 @@ class YamllintDirectivesTestCase(RuleTestCase):
' c: [x]\n', ' c: [x]\n',
conf, conf,
problem=(6, 2, 'comments-indentation')) problem=(6, 2, 'comments-indentation'))
def test_disable_file_directive(self):
conf = ('comments: {min-spaces-from-content: 2}\n'
'comments-indentation: {}\n')
self.check('# yamllint disable-file\n'
'---\n'
'- a: 1 # comment too close\n'
' b:\n'
' # wrong indentation\n'
' c: [x]\n',
conf)
self.check('# yamllint disable-file\n'
'---\n'
'- a: 1 # comment too close\n'
' b:\n'
' # wrong indentation\n'
' c: [x]\n',
conf)
self.check('#yamllint disable-file\n'
'---\n'
'- a: 1 # comment too close\n'
' b:\n'
' # wrong indentation\n'
' c: [x]\n',
conf)
self.check('#yamllint disable-file \n'
'---\n'
'- a: 1 # comment too close\n'
' b:\n'
' # wrong indentation\n'
' c: [x]\n',
conf)
self.check('---\n'
'# yamllint disable-file\n'
'- a: 1 # comment too close\n'
' b:\n'
' # wrong indentation\n'
' c: [x]\n',
conf,
problem1=(3, 8, 'comments'),
problem2=(5, 2, 'comments-indentation'))
self.check('# yamllint disable-file: rules cannot be specified\n'
'---\n'
'- a: 1 # comment too close\n'
' b:\n'
' # wrong indentation\n'
' c: [x]\n',
conf,
problem1=(3, 8, 'comments'),
problem2=(5, 2, 'comments-indentation'))
self.check('AAAA yamllint disable-file\n'
'---\n'
'- a: 1 # comment too close\n'
' b:\n'
' # wrong indentation\n'
' c: [x]\n',
conf,
problem1=(1, 1, 'document-start'),
problem2=(3, 8, 'comments'),
problem3=(5, 2, 'comments-indentation'))
def test_disable_file_directive_not_at_first_position(self):
self.check('# yamllint disable-file\n'
'---\n'
'- bad : colon and spaces \n',
self.conf)
self.check('---\n'
'# yamllint disable-file\n'
'- bad : colon and spaces \n',
self.conf,
problem1=(3, 7, 'colons'),
problem2=(3, 26, 'trailing-spaces'))
def test_disable_file_directive_with_syntax_error(self):
self.check('# This file is not valid YAML (it is a Jinja template)\n'
'{% if extra_info %}\n'
'key1: value1\n'
'{% endif %}\n'
'key2: value2\n',
self.conf,
problem=(2, 2, 'syntax'))
self.check('# yamllint disable-file\n'
'# This file is not valid YAML (it is a Jinja template)\n'
'{% if extra_info %}\n'
'key1: value1\n'
'{% endif %}\n'
'key2: value2\n',
self.conf)
def test_disable_file_directive_with_dos_lines(self):
self.check('# yamllint disable-file\r\n'
'---\r\n'
'- bad : colon and spaces \r\n',
self.conf)
self.check('# yamllint disable-file\r\n'
'# This file is not valid YAML (it is a Jinja template)\r\n'
'{% if extra_info %}\r\n'
'key1: value1\r\n'
'{% endif %}\r\n'
'key2: value2\r\n',
self.conf)

View File

@@ -22,7 +22,7 @@ indentation, etc."""
APP_NAME = 'yamllint' APP_NAME = 'yamllint'
APP_VERSION = '1.17.0' APP_VERSION = '1.22.1'
APP_DESCRIPTION = __doc__ APP_DESCRIPTION = __doc__
__author__ = u'Adrien Vergé' __author__ = u'Adrien Vergé'

View File

@@ -17,6 +17,7 @@
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import io
import os import os
import platform import platform
import sys import sys
@@ -84,11 +85,14 @@ class Format(object):
return line return line
def show_problems(problems, file, args_format): def show_problems(problems, file, args_format, no_warn):
max_level = 0 max_level = 0
first = True first = True
for problem in problems: for problem in problems:
max_level = max(max_level, PROBLEM_LEVELS[problem.level])
if no_warn and (problem.level != 'error'):
continue
if args_format == 'parsable': if args_format == 'parsable':
print(Format.parsable(problem, file)) print(Format.parsable(problem, file))
elif args_format == 'colored' or \ elif args_format == 'colored' or \
@@ -102,7 +106,6 @@ def show_problems(problems, file, args_format):
print(file) print(file)
first = False first = False
print(Format.standard(problem, file)) print(Format.standard(problem, file))
max_level = max(max_level, PROBLEM_LEVELS[problem.level])
if not first and args_format != 'parsable': if not first and args_format != 'parsable':
print('') print('')
@@ -133,6 +136,9 @@ def run(argv=None):
action='store_true', action='store_true',
help='return non-zero exit code on warnings ' help='return non-zero exit code on warnings '
'as well as errors') 'as well as errors')
parser.add_argument('--no-warnings',
action='store_true',
help='output only error level problems')
parser.add_argument('-v', '--version', action='version', parser.add_argument('-v', '--version', action='version',
version='{} {}'.format(APP_NAME, APP_VERSION)) version='{} {}'.format(APP_NAME, APP_VERSION))
@@ -154,6 +160,10 @@ def run(argv=None):
conf = YamlLintConfig(file=args.config_file) conf = YamlLintConfig(file=args.config_file)
elif os.path.isfile('.yamllint'): elif os.path.isfile('.yamllint'):
conf = YamlLintConfig(file='.yamllint') conf = YamlLintConfig(file='.yamllint')
elif os.path.isfile('.yamllint.yaml'):
conf = YamlLintConfig(file='.yamllint.yaml')
elif os.path.isfile('.yamllint.yml'):
conf = YamlLintConfig(file='.yamllint.yml')
elif os.path.isfile(user_global_config): elif os.path.isfile(user_global_config):
conf = YamlLintConfig(file=user_global_config) conf = YamlLintConfig(file=user_global_config)
else: else:
@@ -167,12 +177,13 @@ def run(argv=None):
for file in find_files_recursively(args.files, conf): for file in find_files_recursively(args.files, conf):
filepath = file[2:] if file.startswith('./') else file filepath = file[2:] if file.startswith('./') else file
try: try:
with open(file) as f: with io.open(file, newline='') as f:
problems = linter.run(f, conf, filepath) problems = linter.run(f, conf, filepath)
except EnvironmentError as e: except EnvironmentError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
prob_level = show_problems(problems, file, args_format=args.format) prob_level = show_problems(problems, file, args_format=args.format,
no_warn=args.no_warnings)
max_level = max(max_level, prob_level) max_level = max(max_level, prob_level)
# read yaml from stdin # read yaml from stdin
@@ -182,7 +193,8 @@ def run(argv=None):
except EnvironmentError as e: except EnvironmentError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
prob_level = show_problems(problems, 'stdin', args_format=args.format) prob_level = show_problems(problems, 'stdin', args_format=args.format,
no_warn=args.no_warnings)
max_level = max(max_level, prob_level) max_level = max(max_level, prob_level)
if max_level == PROBLEM_LEVELS['error']: if max_level == PROBLEM_LEVELS['error']:

View File

@@ -3,6 +3,7 @@
yaml-files: yaml-files:
- '*.yaml' - '*.yaml'
- '*.yml' - '*.yml'
- '.yamllint'
rules: rules:
braces: enable braces: enable
@@ -17,7 +18,7 @@ rules:
document-start: document-start:
level: warning level: warning
empty-lines: enable empty-lines: enable
empty-values: enable empty-values: disable
hyphens: enable hyphens: enable
indentation: enable indentation: enable
key-duplicates: enable key-duplicates: enable
@@ -25,7 +26,7 @@ rules:
line-length: enable line-length: enable
new-line-at-end-of-file: enable new-line-at-end-of-file: enable
new-lines: enable new-lines: enable
octal-values: enable octal-values: disable
quoted-strings: disable quoted-strings: disable
trailing-spaces: enable trailing-spaces: enable
truthy: truthy:

View File

@@ -33,7 +33,7 @@ class YamlLintConfig(object):
self.ignore = None self.ignore = None
self.yaml_files = pathspec.PathSpec.from_lines( self.yaml_files = pathspec.PathSpec.from_lines(
'gitwildmatch', ['*.yaml', '*.yml']) 'gitwildmatch', ['*.yaml', '*.yml', '.yamllint'])
if file is not None: if file is not None:
with open(file) as f: with open(file) as f:

View File

@@ -180,7 +180,7 @@ def get_syntax_error(buffer):
except yaml.error.MarkedYAMLError as e: except yaml.error.MarkedYAMLError as e:
problem = LintProblem(e.problem_mark.line + 1, problem = LintProblem(e.problem_mark.line + 1,
e.problem_mark.column + 1, e.problem_mark.column + 1,
'syntax error: ' + e.problem) 'syntax error: ' + e.problem + ' (syntax)')
problem.level = 'error' problem.level = 'error'
return problem return problem
@@ -189,6 +189,10 @@ def _run(buffer, conf, filepath):
assert hasattr(buffer, '__getitem__'), \ assert hasattr(buffer, '__getitem__'), \
'_run() argument must be a buffer, not a stream' '_run() argument must be a buffer, not a stream'
first_line = next(parser.line_generator(buffer)).content
if re.match(r'^#\s*yamllint disable-file\s*$', first_line):
return
# If the document contains a syntax error, save it and yield it at the # If the document contains a syntax error, save it and yield it at the
# right line # right line
syntax_error = get_syntax_error(buffer) syntax_error = get_syntax_error(buffer)

View File

@@ -75,8 +75,8 @@ ID = 'empty-values'
TYPE = 'token' TYPE = 'token'
CONF = {'forbid-in-block-mappings': bool, CONF = {'forbid-in-block-mappings': bool,
'forbid-in-flow-mappings': bool} 'forbid-in-flow-mappings': bool}
DEFAULT = {'forbid-in-block-mappings': False, DEFAULT = {'forbid-in-block-mappings': True,
'forbid-in-flow-mappings': False} 'forbid-in-flow-mappings': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -66,8 +66,8 @@ ID = 'octal-values'
TYPE = 'token' TYPE = 'token'
CONF = {'forbid-implicit-octal': bool, CONF = {'forbid-implicit-octal': bool,
'forbid-explicit-octal': bool} 'forbid-explicit-octal': bool}
DEFAULT = {'forbid-implicit-octal': False, DEFAULT = {'forbid-implicit-octal': True,
'forbid-explicit-octal': False} 'forbid-explicit-octal': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -15,15 +15,23 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
Use this rule to forbid any string values that are not quoted. Use this rule to forbid any string values that are not quoted, or to prevent
You can also enforce the type of the quote used using the ``quote-type`` option quoted strings without needing it. You can also enforce the type of the quote
(``single``, ``double`` or ``any``). 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. **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked.
.. rubric:: Examples .. rubric:: Examples
#. With ``quoted-strings: {quote-type: any}`` #. With ``quoted-strings: {quote-type: any, required: true}``
the following code snippet would **PASS**: 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 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 import yaml
@@ -45,15 +71,44 @@ from yamllint.linter import LintProblem
ID = 'quoted-strings' ID = 'quoted-strings'
TYPE = 'token' TYPE = 'token'
CONF = {'quote-type': ('any', 'single', 'double')} CONF = {'quote-type': ('any', 'single', 'double'),
DEFAULT = {'quote-type': 'any'} 'required': (True, False, 'only-when-needed')}
DEFAULT = {'quote-type': 'any',
'required': True}
DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str'
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 _quotes_are_needed(string):
loader = yaml.BaseLoader('key: ' + string)
# Remove the 5 first tokens corresponding to 'key: ' (StreamStartToken,
# BlockMappingStartToken, KeyToken, ScalarToken(value=key), ValueToken)
for _ in range(5):
loader.get_token()
try:
a, b = loader.get_token(), loader.get_token()
if (isinstance(a, yaml.ScalarToken) and a.style is None and
isinstance(b, yaml.BlockEndToken)):
return False
return True
except yaml.scanner.ScannerError:
return True
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):
quote_type = conf['quote-type'] if not (isinstance(token, yaml.tokens.ScalarToken) and
isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken,
yaml.FlowSequenceStartToken, yaml.TagToken,
yaml.ValueToken))):
return
if (isinstance(token, yaml.tokens.ScalarToken) and
isinstance(prev, (yaml.ValueToken, yaml.TagToken))):
# Ignore explicit types, e.g. !!str testtest or !!int 42 # Ignore explicit types, e.g. !!str testtest or !!int 42
if (prev and isinstance(prev, yaml.tokens.TagToken) and if (prev and isinstance(prev, yaml.tokens.TagToken) and
prev.value[0] == '!!'): prev.value[0] == '!!'):
@@ -61,18 +116,48 @@ def check(conf, token, prev, next, nextnext, context):
# Ignore numbers, booleans, etc. # Ignore numbers, booleans, etc.
resolver = yaml.resolver.Resolver() resolver = yaml.resolver.Resolver()
if resolver.resolve(yaml.nodes.ScalarNode, token.value, tag = resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False))
(True, False)) != 'tag:yaml.org,2002:str': if token.plain and tag != DEFAULT_SCALAR_TAG:
return return
# Ignore multi-line strings # Ignore multi-line strings
if (not token.plain) and (token.style == "|" or token.style == ">"): if (not token.plain) and (token.style == "|" or token.style == ">"):
return return
if ((quote_type == 'single' and token.style != "'") or quote_type = conf['quote-type']
(quote_type == 'double' and token.style != '"') or required = conf['required']
(quote_type == 'any' and token.style is None)):
# 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
not _quotes_are_needed(token.value)):
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( yield LintProblem(
token.start_mark.line + 1, token.start_mark.line + 1,
token.start_mark.column + 1, token.start_mark.column + 1,
"string value is not quoted with %s quotes" % (quote_type)) msg)

View File

@@ -30,6 +30,9 @@ This can be useful to prevent surprises from YAML parsers transforming
``'False'``, ``'false'``, ``'YES'``, ``'Yes'``, ``'yes'``, ``'NO'``, ``'False'``, ``'false'``, ``'YES'``, ``'Yes'``, ``'yes'``, ``'NO'``,
``'No'``, ``'no'``, ``'ON'``, ``'On'``, ``'on'``, ``'OFF'``, ``'Off'``, ``'No'``, ``'no'``, ``'ON'``, ``'On'``, ``'on'``, ``'OFF'``, ``'Off'``,
``'off'``. ``'off'``.
* ``check-keys`` disables verification for keys in mappings. By default,
``truthy`` rule applies to both keys and values. Set this option to ``false``
to prevent this.
.. rubric:: Examples .. rubric:: Examples
@@ -92,6 +95,22 @@ This can be useful to prevent surprises from YAML parsers transforming
- false - false
- on - on
- off - off
#. With ``truthy: {check-keys: false}``
the following code snippet would **PASS**:
::
yes: 1
on: 2
true: 3
the following code snippet would **FAIL**:
::
yes: Yes
on: On
true: True
""" """
import yaml import yaml
@@ -109,14 +128,18 @@ TRUTHY = ['YES', 'Yes', 'yes',
ID = 'truthy' ID = 'truthy'
TYPE = 'token' TYPE = 'token'
CONF = {'allowed-values': list(TRUTHY)} CONF = {'allowed-values': list(TRUTHY), 'check-keys': bool}
DEFAULT = {'allowed-values': ['true', 'false']} DEFAULT = {'allowed-values': ['true', 'false'], 'check-keys': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):
if prev and isinstance(prev, yaml.tokens.TagToken): if prev and isinstance(prev, yaml.tokens.TagToken):
return return
if (not conf['check-keys'] and isinstance(prev, yaml.tokens.KeyToken) and
isinstance(token, yaml.tokens.ScalarToken)):
return
if isinstance(token, yaml.tokens.ScalarToken): if isinstance(token, yaml.tokens.ScalarToken):
if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and
token.style is None): token.style is None):