Compare commits

...

36 Commits

Author SHA1 Message Date
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
Adrien Vergé
ce0336e430 yamllint version 1.17.0 2019-08-12 16:54:51 +02:00
grzesuav
063c854658 feat: Make YAML file extensions configurable 2019-08-12 16:53:30 +02:00
xatier
673bdbd324 fix(truthy): Fix extra whitespace 2019-08-11 14:50:11 +02:00
Remi Pointel
cb5fe2c050 add OpenBSD installation instructions. 2019-07-09 10:04:48 +02:00
Adrien Vergé
930c8eea94 docs: Simplify installation instruction in the README 2019-07-07 18:13:43 +02:00
Adrien Vergé
f6a24552d9 yamllint version 1.16.0 2019-06-07 10:04:55 +02:00
Adrien Vergé
0ba193331b truthy: Validate options passed to 'allowed-values'
Make sure values passed in allowed values are correct ones. This is
possible thanks to previous commit, and should prevent users from
writing incorrect configurations.
2019-06-07 09:59:26 +02:00
Adrien Vergé
f65553c4f7 config: Validate config options with list of enums
Allow rules to declare a list of valid values for an option.

For example, a rule like:

    CONF = {'allowed-values': list}

... allowed any value to be passed in the list (including bad ones).

It is now possible to declare:

    CONF = {'allowed-values': ['value1', 'value2', 'value3']}

... so that the list passed to the options must contain only values in
`['value1', 'value2', 'value3']`.
2019-06-07 09:59:26 +02:00
Adrien Vergé
0fef4c14e7 truthy: Try to make docs on allowed-values more explicit
Edit documentation for the `truthy` rule, in order to:
- add quotes to examples (`'yes'` instead of `yes`) to avoid
  misconfigurations,
- group truthy values in the `allowed-values` option paragraph, for
  easier reading.
2019-06-07 09:59:10 +02:00
Ondrej Vaško
4ef7e05f3a truthy: Add allowed-values configuration option
Allows using key `allowed-values` for `truthy` section in configuration file (#150).

This allows to use configuration `truthy: allowed-values: ["yes", "no",
"..."]`, to set custom allowed truthy values.

This is especially useful for people using ansible, where values like
`yes` or `no` are valid and officially supported, but yamllint reports
them as illegal.

Implemented by difference of set of TRUTHY constants and configured
allowed values.

Signed-off-by: Ondrej Vasko <ondrej.vaskoo@gmail.com>
2019-06-06 09:59:00 +02:00
xatier
43c50379e0 Sort import orders 2019-05-27 11:10:30 +02:00
Adrien Vergé
fec2c2fba7 fix(parser): Correctly handle DOS new lines in 'line' rules
Do not consider the trailing `\r` of a line a part of it.
2019-04-09 16:48:00 +02:00
Mateusz Piotrowski
2a66ec2e5e Add FreeBSD installation instructions 2019-03-21 18:40:28 +01:00
Adrien Vergé
37700ab3e6 yamllint version 1.15.0 2019-02-11 14:18:11 +01:00
Adrien Vergé
f66661e36d docs(cli): Add a paragraph about standard input
See commit 05dfcbc "cli: Add command line option - to read from standard
input", cc @miguelbarao.
2019-02-11 14:16:33 +01:00
Adrien Vergé
d6b89e94e4 chore(docs): Fix conf.py styling 2019-02-11 14:09:20 +01:00
Miguel Barao
05dfcbc109 cli: Add command line option - to read from standard input
If YAML files are given as arguments, parses these files.
If yamllint is run with - option, stdin.
If no arguments are given, just fail.
2019-02-11 14:04:48 +01:00
32 changed files with 1127 additions and 329 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,53 @@
Changelog Changelog
========= =========
1.21.0 (2020-03-24)
-------------------
- Fix ``new-lines`` rule on Python 3 with DOS line endings
- Fix ``quoted-strings`` rule not working for string values matching scalars
- Add ``required: only-when-needed`` option to the ``quoted-strings`` rule
1.20.0 (2019-12-26)
-------------------
- 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)
-------------------
- Simplify installation instructions in the README
- Add OpenBSD installation instructions
- Make YAML file extensions configurable
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)
-------------------
- Allow linting from standard input with ``yamllint -``
1.14.0 (2019-01-14) 1.14.0 (2019-01-14)
------------------- -------------------

View File

@@ -38,31 +38,16 @@ Screenshot
Installation Installation
^^^^^^^^^^^^ ^^^^^^^^^^^^
On Fedora / CentOS (note: `EPEL <https://fedoraproject.org/wiki/EPEL>`_ is Using pip, the Python package manager:
required on CentOS):
.. code:: bash
sudo dnf install yamllint
On Debian 8+ / Ubuntu 16.04+:
.. code:: bash
sudo apt-get install yamllint
On Mac OS 10.11+:
.. code:: bash
brew install yamllint
Alternatively using pip, the Python package manager:
.. code:: bash .. code:: bash
pip install --user yamllint pip install --user yamllint
yamllint is also packaged for all major operating systems, see installation
examples (``dnf``, ``apt-get``...) `in the documentation
<https://yamllint.readthedocs.io/en/stable/quickstart.html>`_.
Usage Usage
^^^^^ ^^^^^

View File

@@ -48,7 +48,7 @@ man_pages = [
class Mock(MagicMock): class Mock(MagicMock):
@classmethod @classmethod
def __getattr__(cls, name): def __getattr__(cls, name):
return MagicMock() return MagicMock()
MOCK_MODULES = ['pathspec', 'yaml'] MOCK_MODULES = ['pathspec', 'yaml']

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,25 @@ 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
---------------------
To configure what yamllint should consider as YAML files, set ``yaml-files``
configuration option. The default is:
.. code-block:: yaml
yaml-files:
- '*.yaml'
- '*.yml'
- '.yamllint'
The same rules as for ignoring paths apply (``.gitignore``-style path pattern,
see below).
Ignoring paths Ignoring paths
-------------- --------------

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

@@ -4,7 +4,8 @@ Quickstart
Installing yamllint Installing yamllint
------------------- -------------------
On Fedora / CentOS: On Fedora / CentOS (note: `EPEL <https://fedoraproject.org/wiki/EPEL>`_ is
required on CentOS):
.. code:: bash .. code:: bash
@@ -22,6 +23,18 @@ On Mac OS 10.11+:
brew install yamllint brew install yamllint
On FreeBSD:
.. code:: sh
pkg install py36-yamllint
On OpenBSD:
.. code:: sh
doas pkg_add py3-yamllint
Alternatively using pip, the Python package manager: Alternatively using pip, the Python package manager:
.. code:: bash .. code:: bash
@@ -50,6 +63,12 @@ You can also lint all YAML files in a whole directory:
yamllint . yamllint .
Or lint a YAML stream from standard input:
.. code:: bash
echo -e 'this: is\nvalid: YAML' | yamllint -
The output will look like (colors are not displayed here): The output will look like (colors are not displayed here):
:: ::

View File

@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from setuptools import setup, find_packages from setuptools import find_packages, setup
from yamllint import (__author__, __license__, from yamllint import (__author__, __license__,
APP_NAME, APP_VERSION, APP_DESCRIPTION) APP_NAME, APP_VERSION, APP_DESCRIPTION)

View File

@@ -78,3 +78,22 @@ class EmptyLinesTestCase(RuleTestCase):
'document-start: disable\n') 'document-start: disable\n')
self.check('non empty\n', conf) self.check('non empty\n', conf)
self.check('non empty\n\n', conf, problem=(2, 1)) 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))

View File

@@ -171,3 +171,12 @@ class LineLengthTestCase(RuleTestCase):
'# This is a test to check if “line-length” works nice\n' '# This is a test to check if “line-length” works nice\n'
'with: “unicode characters” that span accross bytes! ↺\n', 'with: “unicode characters” that span accross bytes! ↺\n',
conf, problem1=(2, 53), problem2=(3, 53)) 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))

View File

@@ -31,16 +31,20 @@ class NewLinesTestCase(RuleTestCase):
self.check('---\r\ntext\r\n', conf) self.check('---\r\ntext\r\n', conf)
def test_unix_type(self): 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('', conf)
self.check('\r', conf)
self.check('\n', conf) self.check('\n', conf)
self.check('\r\n', conf, problem=(1, 1)) self.check('\r\n', conf, problem=(1, 1))
self.check('---\ntext\n', conf) self.check('---\ntext\n', conf)
self.check('---\r\ntext\r\n', conf, problem=(1, 4)) self.check('---\r\ntext\r\n', conf, problem=(1, 4))
def test_dos_type(self): 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('', conf)
self.check('\r', conf)
self.check('\n', conf, problem=(1, 1)) self.check('\n', conf, problem=(1, 1))
self.check('\r\n', conf) self.check('\r\n', conf)
self.check('---\ntext\n', conf, problem=(1, 4)) self.check('---\ntext\n', conf, problem=(1, 4))

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,18 +31,23 @@ 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'
@@ -55,7 +61,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 +70,24 @@ 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)) conf, problem1=(4, 10), problem2=(5, 10),
problem3=(6, 10), problem4=(7, 10))
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -86,7 +96,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 +105,162 @@ 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)) conf, problem1=(4, 10), problem2=(8, 10))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n' # fails
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n'
' word 2"\n',
conf, problem1=(9, 3))
def test_disallow_redundant_quotes(self):
conf = 'quoted-strings: {required: only-when-needed}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n' # fails
'string3: "true"\n'
'string4: "123"\n'
'string5: \'bar\'\n' # fails
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n',
conf, problem1=(5, 10), problem2=(8, 10))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
def test_disallow_redundant_single_quotes(self):
conf = 'quoted-strings: {quote-type: single, ' + \
'required: only-when-needed}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n' # fails
'string3: "true"\n' # fails
'string4: "123"\n' # fails
'string5: \'bar\'\n' # fails
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n',
conf, problem1=(5, 10), problem2=(6, 10),
problem3=(7, 10), problem4=(8, 10))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
def test_single_quotes_required(self):
conf = 'quoted-strings: {quote-type: single, required: true}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n' # fails
'string2: "foo"\n' # fails
'string3: "true"\n' # fails
'string4: "123"\n' # fails
'string5: \'bar\'\n'
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n',
conf, problem1=(4, 10), problem2=(5, 10),
problem3=(6, 10), problem4=(7, 10))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n' # fails
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(9, 3), problem2=(12, 3))
def test_any_quotes_relaxed(self):
conf = 'quoted-strings: {quote-type: any, required: false}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n'
'string3: "true"\n'
'string4: "123"\n'
'string5: \'bar\'\n'
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n',
conf)
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -122,4 +274,73 @@ class QuotedTestCase(RuleTestCase):
'multiline string 4:\n' 'multiline string 4:\n'
' "word 1\\\n' ' "word 1\\\n'
' word 2"\n', ' word 2"\n',
conf)
def test_single_quotes_relaxed(self):
conf = 'quoted-strings: {quote-type: single, required: false}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n' # fails
'string3: "true"\n' # fails
'string4: "123"\n' # fails
'string5: \'bar\'\n'
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n',
conf, problem2=(5, 10),
problem3=(6, 10), problem4=(7, 10))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
def test_quotes_required(self):
conf = 'quoted-strings: {quote-type: any, required: true}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n' # fails
'string2: "foo"\n'
'string3: "true"\n'
'string4: "123"\n'
'string5: \'bar\'\n'
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n',
conf, problem2=(4, 10))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n' # fails
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n'
' word 2"\n',
conf, problem1=(9, 3)) conf, problem1=(9, 3))

View File

@@ -49,6 +49,54 @@ class TruthyTestCase(RuleTestCase):
problem3=(7, 3), problem4=(7, 7), problem3=(7, 3), problem4=(7, 7),
problem5=(8, 3), problem6=(8, 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): def test_explicit_types(self):
conf = 'truthy: enable\n' conf = 'truthy: enable\n'
self.check('---\n' self.check('---\n'

View File

@@ -29,6 +29,30 @@ import unittest
from tests.common import build_temp_workspace from tests.common import build_temp_workspace
from yamllint import cli from yamllint import cli
from yamllint import config
class RunContext(object):
"""Context manager for ``cli.run()`` to capture exit code and streams."""
def __init__(self, case):
self.stdout = self.stderr = None
self._raises_ctx = case.assertRaises(SystemExit)
def __enter__(self):
self._raises_ctx.__enter__()
sys.stdout = self.outstream = StringIO()
sys.stderr = self.errstream = StringIO()
return self
def __exit__(self, *exc_info):
self.stdout, sys.stdout = self.outstream.getvalue(), sys.__stdout__
self.stderr, sys.stderr = self.errstream.getvalue(), sys.__stderr__
return self._raises_ctx.__exit__(*exc_info)
@property
def returncode(self):
return self._raises_ctx.exception.code
class CommandLineTestCase(unittest.TestCase): class CommandLineTestCase(unittest.TestCase):
@@ -58,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
@@ -73,9 +100,11 @@ class CommandLineTestCase(unittest.TestCase):
shutil.rmtree(cls.wd) shutil.rmtree(cls.wd)
def test_find_files_recursively(self): def test_find_files_recursively(self):
conf = config.YamlLintConfig('extends: default')
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd])), 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'),
@@ -85,14 +114,14 @@ class CommandLineTestCase(unittest.TestCase):
items = [os.path.join(self.wd, 'sub/ok.yaml'), items = [os.path.join(self.wd, 'sub/ok.yaml'),
os.path.join(self.wd, 'empty-dir')] os.path.join(self.wd, 'empty-dir')]
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively(items)), sorted(cli.find_files_recursively(items, conf)),
[os.path.join(self.wd, 'sub/ok.yaml')], [os.path.join(self.wd, 'sub/ok.yaml')],
) )
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')]
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively(items)), sorted(cli.find_files_recursively(items, conf)),
[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')],
) )
@@ -100,196 +129,219 @@ class CommandLineTestCase(unittest.TestCase):
items = [os.path.join(self.wd, 'sub'), items = [os.path.join(self.wd, 'sub'),
os.path.join(self.wd, '/etc/another/file')] os.path.join(self.wd, '/etc/another/file')]
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively(items)), sorted(cli.find_files_recursively(items, conf)),
[os.path.join(self.wd, '/etc/another/file'), [os.path.join(self.wd, '/etc/another/file'),
os.path.join(self.wd, 'sub/ok.yaml')], os.path.join(self.wd, 'sub/ok.yaml')],
) )
conf = config.YamlLintConfig('extends: default\n'
'yaml-files:\n'
' - \'*.yaml\' \n')
self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.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, 'warn.yaml')]
)
conf = config.YamlLintConfig('extends: default\n'
'yaml-files:\n'
' - \'*.yml\'\n')
self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'dos.yml'),
os.path.join(self.wd, 'empty.yml')]
)
conf = config.YamlLintConfig('extends: default\n'
'yaml-files:\n'
' - \'*.json\'\n')
self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'no-yaml.json')]
)
conf = config.YamlLintConfig('extends: default\n'
'yaml-files:\n'
' - \'*\'\n')
self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'),
os.path.join(self.wd, 'dos.yml'),
os.path.join(self.wd, 'empty.yml'),
os.path.join(self.wd, 'no-yaml.json'),
os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
os.path.join(self.wd, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')]
)
conf = config.YamlLintConfig('extends: default\n'
'yaml-files:\n'
' - \'*.yaml\'\n'
' - \'*\'\n'
' - \'**\'\n')
self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'),
os.path.join(self.wd, 'dos.yml'),
os.path.join(self.wd, 'empty.yml'),
os.path.join(self.wd, 'no-yaml.json'),
os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
os.path.join(self.wd, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')]
)
conf = config.YamlLintConfig('extends: default\n'
'yaml-files:\n'
' - \'s/**\'\n'
' - \'**/utf-8\'\n')
self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)),
[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
with RunContext(self) as ctx:
cli.run(('-', 'file'))
self.assertNotEqual(ctx.returncode, 0)
self.assertEqual(ctx.stdout, '')
self.assertRegexpMatches(ctx.stderr, 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.
@@ -298,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)
@@ -377,60 +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):
# prepares stdin with an invalid yaml string so that we can check
# for its specific error, and be assured that stdin was read
self.addCleanup(setattr, sys, 'stdin', sys.__stdin__)
sys.stdin = StringIO(
'I am a string\n'
'therefore: I am an error\n')
with RunContext(self) as ctx:
cli.run(('-', '-f', 'parsable'))
expected_out = (
'stdin:2:10: [error] syntax error: '
'mapping values are not allowed here (syntax)\n')
self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_run_no_warnings(self):
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

@@ -174,6 +174,25 @@ class SimpleConfigTestCase(unittest.TestCase):
self.assertRaises(config.YamlLintConfigError, self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': 'abc'}) 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): class ExtendedConfigTestCase(unittest.TestCase):
def test_extend_on_object(self): def test_extend_on_object(self):
@@ -429,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.14.0' APP_VERSION = '1.21.0'
APP_DESCRIPTION = __doc__ APP_DESCRIPTION = __doc__
__author__ = u'Adrien Vergé' __author__ = u'Adrien Vergé'

View File

@@ -16,24 +16,26 @@
from __future__ import print_function from __future__ import print_function
import os
import sys
import platform
import argparse import argparse
import io
import os
import platform
import sys
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import linter
from yamllint.config import YamlLintConfig, YamlLintConfigError from yamllint.config import YamlLintConfig, YamlLintConfigError
from yamllint.linter import PROBLEM_LEVELS from yamllint.linter import PROBLEM_LEVELS
from yamllint import linter
def find_files_recursively(items): def find_files_recursively(items, conf):
for item in items: for item in items:
if os.path.isdir(item): if os.path.isdir(item):
for root, dirnames, filenames in os.walk(item): for root, dirnames, filenames in os.walk(item):
for filename in [f for f in filenames for f in filenames:
if f.endswith(('.yml', '.yaml'))]: filepath = os.path.join(root, f)
yield os.path.join(root, filename) if conf.is_yaml_file(filepath):
yield filepath
else: else:
yield item yield item
@@ -83,11 +85,43 @@ class Format(object):
return line return line
def show_problems(problems, file, args_format, no_warn):
max_level = 0
first = True
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':
print(Format.parsable(problem, file))
elif args_format == 'colored' or \
(args_format == 'auto' and supports_color()):
if first:
print('\033[4m%s\033[0m' % file)
first = False
print(Format.standard_color(problem, file))
else:
if first:
print(file)
first = False
print(Format.standard(problem, file))
if not first and args_format != 'parsable':
print('')
return max_level
def run(argv=None): def run(argv=None):
parser = argparse.ArgumentParser(prog=APP_NAME, parser = argparse.ArgumentParser(prog=APP_NAME,
description=APP_DESCRIPTION) description=APP_DESCRIPTION)
parser.add_argument('files', metavar='FILE_OR_DIR', nargs='+', files_group = parser.add_mutually_exclusive_group(required=True)
help='files to check') files_group.add_argument('files', metavar='FILE_OR_DIR', nargs='*',
default=(),
help='files to check')
files_group.add_argument('-', action='store_true', dest='stdin',
help='read from standard input')
config_group = parser.add_mutually_exclusive_group() config_group = parser.add_mutually_exclusive_group()
config_group.add_argument('-c', '--config-file', dest='config_file', config_group.add_argument('-c', '--config-file', dest='config_file',
action='store', action='store',
@@ -102,11 +136,12 @@ 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))
# TODO: read from stdin when no filename?
args = parser.parse_args(argv) args = parser.parse_args(argv)
# User-global config is supposed to be in ~/.config/yamllint/config # User-global config is supposed to be in ~/.config/yamllint/config
@@ -125,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:
@@ -135,35 +174,28 @@ def run(argv=None):
max_level = 0 max_level = 0
for file in find_files_recursively(args.files): 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:
first = True with io.open(file, newline='') as f:
with open(file) as f: problems = linter.run(f, conf, filepath)
for problem in linter.run(f, conf, filepath):
if args.format == 'parsable':
print(Format.parsable(problem, file))
elif args.format == 'colored' or \
(args.format == 'auto' and supports_color()):
if first:
print('\033[4m%s\033[0m' % file)
first = False
print(Format.standard_color(problem, file))
else:
if first:
print(file)
first = False
print(Format.standard(problem, file))
max_level = max(max_level, PROBLEM_LEVELS[problem.level])
if not first and args.format != 'parsable':
print('')
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,
no_warn=args.no_warnings)
max_level = max(max_level, prob_level)
# read yaml from stdin
if args.stdin:
try:
problems = linter.run(sys.stdin, conf, '')
except EnvironmentError as e:
print(e, file=sys.stderr)
sys.exit(-1)
prob_level = show_problems(problems, 'stdin', args_format=args.format,
no_warn=args.no_warnings)
max_level = max(max_level, prob_level)
if max_level == PROBLEM_LEVELS['error']: if max_level == PROBLEM_LEVELS['error']:
return_code = 1 return_code = 1

View File

@@ -1,5 +1,10 @@
--- ---
yaml-files:
- '*.yaml'
- '*.yml'
- '.yamllint'
rules: rules:
braces: enable braces: enable
brackets: enable brackets: enable
@@ -13,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
@@ -21,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

@@ -32,6 +32,9 @@ class YamlLintConfig(object):
self.ignore = None self.ignore = None
self.yaml_files = pathspec.PathSpec.from_lines(
'gitwildmatch', ['*.yaml', '*.yml', '.yamllint'])
if file is not None: if file is not None:
with open(file) as f: with open(file) as f:
content = f.read() content = f.read()
@@ -42,6 +45,9 @@ class YamlLintConfig(object):
def is_file_ignored(self, filepath): def is_file_ignored(self, filepath):
return self.ignore and self.ignore.match_file(filepath) return self.ignore and self.ignore.match_file(filepath)
def is_yaml_file(self, filepath):
return self.yaml_files.match_file(filepath)
def enabled_rules(self, filepath): def enabled_rules(self, filepath):
return [yamllint.rules.get(id) for id, val in self.rules.items() return [yamllint.rules.get(id) for id, val in self.rules.items()
if val is not False and ( if val is not False and (
@@ -96,6 +102,15 @@ class YamlLintConfig(object):
self.ignore = pathspec.PathSpec.from_lines( self.ignore = pathspec.PathSpec.from_lines(
'gitwildmatch', conf['ignore'].splitlines()) 'gitwildmatch', conf['ignore'].splitlines())
if 'yaml-files' in conf:
if not (isinstance(conf['yaml-files'], list)
and all(isinstance(i, str) for i in conf['yaml-files'])):
raise YamlLintConfigError(
'invalid config: yaml-files '
'should be a list of file patterns')
self.yaml_files = pathspec.PathSpec.from_lines('gitwildmatch',
conf['yaml-files'])
def validate(self): def validate(self):
for id in self.rules: for id in self.rules:
try: try:
@@ -134,12 +149,26 @@ def validate_rule_conf(rule, conf):
raise YamlLintConfigError( raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' % 'invalid config: unknown option "%s" for rule "%s"' %
(optkey, rule.ID)) (optkey, rule.ID))
# Example: CONF = {option: (bool, 'mixed')}
# → {option: true} → {option: mixed}
if isinstance(options[optkey], tuple): if isinstance(options[optkey], tuple):
if (conf[optkey] not in options[optkey] and if (conf[optkey] not in options[optkey] and
type(conf[optkey]) not in options[optkey]): type(conf[optkey]) not in options[optkey]):
raise YamlLintConfigError( raise YamlLintConfigError(
'invalid config: option "%s" of "%s" should be in %s' 'invalid config: option "%s" of "%s" should be in %s'
% (optkey, rule.ID, options[optkey])) % (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: else:
if not isinstance(conf[optkey], options[optkey]): if not isinstance(conf[optkey], options[optkey]):
raise YamlLintConfigError( raise YamlLintConfigError(

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

@@ -77,7 +77,10 @@ def line_generator(buffer):
cur = 0 cur = 0
next = buffer.find('\n') next = buffer.find('\n')
while next != -1: 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 cur = next + 1
next = buffer.find('\n', cur) next = buffer.find('\n', cur)
line_no += 1 line_no += 1

View File

@@ -72,7 +72,7 @@ Use this rule to control the number of spaces before and after colons (``:``).
import yaml 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' ID = 'colons'

View File

@@ -66,27 +66,37 @@ DEFAULT = {'max': 2,
def check(conf, line): def check(conf, line):
if line.start == line.end and line.end < len(line.buffer): if line.start == line.end and line.end < len(line.buffer):
# Only alert on the last blank line of a series # Only alert on the last blank line of a series
if (line.end < len(line.buffer) - 1 and if (line.end + 2 <= len(line.buffer) and
line.buffer[line.end + 1] == '\n'): 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 return
blank_lines = 0 blank_lines = 0
while (line.start > blank_lines and start = line.start
line.buffer[line.start - blank_lines - 1] == '\n'): while start >= 2 and line.buffer[start - 2:start] == '\r\n':
blank_lines += 1 blank_lines += 1
start -= 2
while start >= 1 and line.buffer[start - 1] == '\n':
blank_lines += 1
start -= 1
max = conf['max'] max = conf['max']
# Special case: start of document # 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 blank_lines += 1 # first line doesn't have a preceding \n
max = conf['max-start'] max = conf['max-start']
# Special case: end of document # Special case: end of document
# NOTE: The last line of a file is always supposed to end with a new # NOTE: The last line of a file is always supposed to end with a new
# line. See POSIX definition of a line at: # 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' # Allow the exception of the one-byte file containing '\n'
if line.end == 0: if line.end == 0:
return return

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

@@ -193,7 +193,7 @@ Use this rule to control the indentation.
import yaml import yaml
from yamllint.linter import LintProblem 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' ID = 'indentation'

View File

@@ -36,10 +36,11 @@ DEFAULT = {'type': 'unix'}
def check(conf, line): def check(conf, line):
if line.start == 0 and len(line.buffer) > line.end: if line.start == 0 and len(line.buffer) > line.end:
if conf['type'] == 'dos': 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, yield LintProblem(1, line.end - line.start + 1,
'wrong new line character: expected \\r\\n') 'wrong new line character: expected \\r\\n')
else: else:
if line.end > 0 and line.buffer[line.end - 1] == '\r': if line.buffer[line.end] == '\r':
yield LintProblem(1, line.end - line.start, yield LintProblem(1, line.end - line.start + 1,
'wrong new line character: expected \\n') 'wrong new line character: expected \\n')

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,34 +71,76 @@ 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'
START_TOKENS = {'#', '*', '!', '?', '@', '`', '&',
',', '-', '{', '}', '[', ']', ':'}
def quote_match(quote_type, token_style):
return ((quote_type == 'any') or
(quote_type == 'single' and token_style == "'") or
(quote_type == 'double' and token_style == '"'))
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):
quote_type = conf['quote-type'] if not (isinstance(token, yaml.tokens.ScalarToken) and
if (isinstance(token, yaml.tokens.ScalarToken) and
isinstance(prev, (yaml.ValueToken, yaml.TagToken))): isinstance(prev, (yaml.ValueToken, yaml.TagToken))):
# Ignore explicit types, e.g. !!str testtest or !!int 42 return
if (prev and isinstance(prev, yaml.tokens.TagToken) and
prev.value[0] == '!!'):
return
# Ignore numbers, booleans, etc. # Ignore explicit types, e.g. !!str testtest or !!int 42
resolver = yaml.resolver.Resolver() if (prev and isinstance(prev, yaml.tokens.TagToken) and
if resolver.resolve(yaml.nodes.ScalarNode, token.value, prev.value[0] == '!!'):
(True, False)) != 'tag:yaml.org,2002:str': return
return
# Ignore multi-line strings # Ignore numbers, booleans, etc.
if (not token.plain) and (token.style == "|" or token.style == ">"): resolver = yaml.resolver.Resolver()
return tag = resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False))
if token.plain and tag != DEFAULT_SCALAR_TAG:
return
if ((quote_type == 'single' and token.style != "'") or # Ignore multi-line strings
(quote_type == 'double' and token.style != '"') or if (not token.plain) and (token.style == "|" or token.style == ">"):
(quote_type == 'any' and token.style is None)): return
yield LintProblem(
token.start_mark.line + 1, quote_type = conf['quote-type']
token.start_mark.column + 1, required = conf['required']
"string value is not quoted with %s quotes" % (quote_type))
# Completely relaxed about quotes (same as the rule being disabled)
if required is False and quote_type == 'any':
return
msg = None
if required is True:
# Quotes are mandatory and need to match config
if token.style is None or not quote_match(quote_type, token.style):
msg = "string value is not quoted with %s quotes" % (quote_type)
elif required is False:
# Quotes are not mandatory but when used need to match config
if token.style and not quote_match(quote_type, token.style):
msg = "string value is not quoted with %s quotes" % (quote_type)
elif not token.plain:
# Quotes are disallowed when not needed
if (tag == DEFAULT_SCALAR_TAG and token.value
and token.value[0] not in START_TOKENS):
msg = "string value is redundantly quoted with %s quotes" % (
quote_type)
# But when used need to match config
elif token.style and not quote_match(quote_type, token.style):
msg = "string value is not quoted with %s quotes" % (quote_type)
if msg is not None:
yield LintProblem(
token.start_mark.line + 1,
token.start_mark.column + 1,
msg)

View File

@@ -15,13 +15,22 @@
# 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 non-explictly typed truthy values other than ``true`` Use this rule to forbid non-explictly typed truthy values other than allowed
and ``false``, for example ``YES``, ``False`` and ``off``. ones (by default: ``true`` and ``false``), for example ``YES`` or ``off``.
This can be useful to prevent surprises from YAML parsers transforming This can be useful to prevent surprises from YAML parsers transforming
``[yes, FALSE, Off]`` into ``[true, false, false]`` or ``[yes, FALSE, Off]`` into ``[true, false, false]`` or
``{y: 1, yes: 2, on: 3, true: 4, True: 5}`` into ``{y: 1, true: 5}``. ``{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 .. rubric:: Examples
#. With ``truthy: {}`` #. With ``truthy: {}``
@@ -63,29 +72,55 @@ This can be useful to prevent surprises from YAML parsers transforming
yes: 1 yes: 1
on: 2 on: 2
True: 3 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 import yaml
from yamllint.linter import LintProblem from yamllint.linter import LintProblem
ID = 'truthy'
TYPE = 'token'
TRUTHY = ['YES', 'Yes', 'yes', TRUTHY = ['YES', 'Yes', 'yes',
'NO', 'No', 'no', 'NO', 'No', 'no',
'TRUE', 'True', # 'true' is a boolean 'TRUE', 'True', 'true',
'FALSE', 'False', # 'false' is a boolean 'FALSE', 'False', 'false',
'ON', 'On', 'on', 'ON', 'On', 'on',
'OFF', 'Off', 'off'] '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): 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 isinstance(token, yaml.tokens.ScalarToken): 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, yield LintProblem(token.start_mark.line + 1,
token.start_mark.column + 1, token.start_mark.column + 1,
"truthy value should be true or false") "truthy value should be one of [" +
", ".join(sorted(conf['allowed-values'])) + "]")