Compare commits

...

53 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
Adrien Vergé
16b939958d yamllint version 1.14.0 2019-01-14 09:47:07 +01:00
Adrien Vergé
b4740dc1fb comments: Fix ignore-shebangs option on corner cases 2019-01-14 09:40:31 +01:00
Mattias Bengtsson
b77f78f677 Support ignoring shebangs
Some usages of YAML (like Ansible) supports running the file as a script.

Support (by default) an ignore-shebangs setting for the comments module.

Fixes #116 - comments rule with require-starting-space: true should special case shebang
2019-01-14 09:40:31 +01:00
Adrien Vergé
0f073f7a09 config: Do not require all rule options to be set
Before, it was required to specify all the options when customizing a
rule. For instance, one could use `empty-lines: enable` or `empty-lines:
{max: 1, max-start: 2, max-end: 2}`, but not just `empty-lines: {max:
1}` (it would fail with *invalid config: missing option "max-start" for
rule "empty-lines"*).

This was a minor problem for users, but it prevented the addition of new
options to existing rules, see [1] for an example. If a new option was
added, updating yamllint for all users that customize the rule would
produce a crash (*invalid config: missing option ...*).

To avoid that, let's embed default values inside the rules themselves,
instead of keeping them in `conf/default.yaml`.

This refactor should not have any impact on existing projects. I've
manually checked that it did not change the output of tests, on
different projects:
- ansible/ansible: `test/runner/ansible-test sanity --python 3.7 --test yamllint`
- ansible/molecule: `yamllint -s test/ molecule/`
- Neo23x0/sigma: `make test-yaml`
- markstory/lint-review: `yamllint .`

[1]: https://github.com/adrienverge/yamllint/pull/151
2019-01-10 10:01:31 +01:00
cclauss
bc7ac81707 Travis CI: Add Python 3.7 and 3.8a
* Adds Python 3.7.1 and a current nightly build of Python 3.8 alpha.
* Python 3.3 reached its end of life in 2017 https://devguide.python.org/#branchstatus
2018-12-10 17:12:50 +01:00
Adrien Vergé
a56a1015f0 style(docs): Fix RST lint errors reported by doc8 2018-12-08 11:21:02 +01:00
Adrien Vergé
6cf5eecdac chore(CI): Lint RST (reStructuredText) files 2018-12-08 11:21:02 +01:00
Hugo
f4c56b8216 Upgrade Python syntax with pyupgrade
https://github.com/asottile/pyupgrade
2018-11-26 19:09:47 +01:00
Hugo
5852566ff0 Upgrade unit tests to use more useful asserts 2018-11-26 19:09:47 +01:00
Hugo
4a7986b4cf Remove redundant parentheses 2018-11-26 19:09:47 +01:00
Hugo
3d1ad9a176 Add explicit Trove classifers for PyPI 2018-11-26 19:09:47 +01:00
Hugo
8da6e36bf1 Add python_requires to help pip 2018-11-26 19:09:47 +01:00
Hugo
c281d48507 Drop support for EOL Python 2.6 2018-11-26 19:09:47 +01:00
Adrien Vergé
8bdddf6e89 docs: Warn about Python 2 and problems with line-length
Closes #146.
2018-11-23 14:19:55 +01:00
Adrien Vergé
c8032c086b line-length: Add tests for lines containing unicode characters
Some unicode characters span accross multiple bytes. Python 3 is OK with
that, but Python 2 reports an incorrect number of characters.

Related to https://github.com/adrienverge/yamllint/issues/146
2018-11-23 14:19:55 +01:00
Adrien Vergé
ea045c41b7 CI: Drop Python 3.3 support
The `pkg_resources` package inside `setuptools` explicitly [disallows
Python 3.3](7392f01ffc (diff-81de4a30a55fcc3fb944f8387ea9ec94)):

    if (3, 0) < sys.version_info < (3, 4):
        raise RuntimeError("Python 3.4 or later is required")

It's time to drop support for 3.3.
2018-11-23 14:10:00 +01:00
Adrien Vergé
c803dd5f6d docs(CHANGELOG): Fix RST format for code snippets 2018-11-14 19:08:39 +01:00
49 changed files with 1500 additions and 559 deletions

View File

@@ -1,27 +1,22 @@
--- ---
dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069)
language: python language: python
python: python:
- 2.6
- 2.7 - 2.7
- 3.3
- 3.4
- 3.5 - 3.5
- 3.6 - 3.6
- 3.7
- 3.8
- nightly - nightly
install: install:
- pip install pyyaml flake8 flake8-import-order - pip install pyyaml coveralls flake8 flake8-import-order doc8
- if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install unittest2; fi
- if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then pip install coveralls; fi
- if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then pip install sphinx; fi - if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then pip install sphinx; fi
- pip install . - pip install .
script: script:
- if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then flake8 .; fi - if [[ $TRAVIS_PYTHON_VERSION != nightly ]]; then flake8 .; fi
- if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then doc8 $(git ls-files '*.rst'); fi
- yamllint --strict $(git ls-files '*.yaml' '*.yml') - yamllint --strict $(git ls-files '*.yaml' '*.yml')
- if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then - coverage run --source=yamllint setup.py test
coverage run --source=yamllint setup.py test;
else
python setup.py test;
fi
- if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then - if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then
python setup.py build_sphinx; python setup.py build_sphinx;
fi fi

View File

@@ -1,45 +1,101 @@
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)
-------------------
- Fix documentation code snippets
- Drop Python 2.6 and 3.3 support, add Python 3.7 support
- Update documentation and tests for ``line-length`` + Unicode + Python 2
- Allow rule configurations to lack options
- Add a new ``ignore-shebangs`` option for the ``comments`` rule
1.13.0 (2018-11-14) 1.13.0 (2018-11-14)
------------------- -------------------
- Use `isinstance(x, y)` instead of `type(x) == y` - Use ``isinstance(x, y)`` instead of ``type(x) == y``
- Add a new `-f colored` option - Add a new ``-f colored`` option
- Update documentation about colored output when run from CLI - Update documentation about colored output when run from CLI
1.12.1 (2018-10-17) 1.12.1 (2018-10-17)
------------------- -------------------
- Fix the `quoted-strings` rule, broken implementation - Fix the ``quoted-strings`` rule, broken implementation
- Fix missing documentation for the `quoted-strings` rule - Fix missing documentation for the ``quoted-strings`` rule
1.12.0 (2018-10-04) 1.12.0 (2018-10-04)
------------------- -------------------
- Add a new `quoted-strings` rule - Add a new ``quoted-strings`` rule
- Update installation documentation for pip, CentOS, Debian, Ubuntu, Mac OS - Update installation documentation for pip, CentOS, Debian, Ubuntu, Mac OS
1.11.1 (2018-04-06) 1.11.1 (2018-04-06)
------------------- -------------------
- Handle merge keys (`<<`) in the `key-duplicates` rule - Handle merge keys (``<<``) in the ``key-duplicates`` rule
- Update documentation about pre-commit - Update documentation about pre-commit
- Make examples for `ignore` rule clearer - Make examples for ``ignore`` rule clearer
- Clarify documentation on the 'truthy' rule - Clarify documentation on the 'truthy' rule
- Fix crash in parser due to a change in PyYAML > 3.12 - Fix crash in parser due to a change in PyYAML > 3.12
1.11.0 (2018-02-21) 1.11.0 (2018-02-21)
------------------- -------------------
- Add a new `octal-values` rule - Add a new ``octal-values`` rule
1.10.0 (2017-11-05) 1.10.0 (2017-11-05)
------------------- -------------------
- Fix colored output on Windows - Fix colored output on Windows
- Check documentation compilation on continuous integration - Check documentation compilation on continuous integration
- Add a new `empty-values` rule - Add a new ``empty-values`` rule
- Make sure test files are included in dist bundle - Make sure test files are included in dist bundle
- Tests: Use en_US.UTF-8 locale when C.UTF-8 not available - Tests: Use en_US.UTF-8 locale when C.UTF-8 not available
- Tests: Dynamically detect Python executable path - Tests: Dynamically detect Python executable path
@@ -47,13 +103,13 @@ Changelog
1.9.0 (2017-10-16) 1.9.0 (2017-10-16)
------------------ ------------------
- Add a new `key-ordering` rule - Add a new ``key-ordering`` rule
- Fix indentation rule for key following empty list - Fix indentation rule for key following empty list
1.8.2 (2017-10-10) 1.8.2 (2017-10-10)
------------------ ------------------
- Be clearer about the `ignore` conf type - Be clearer about the ``ignore`` conf type
- Update pre-commit hook file - Update pre-commit hook file
- Add documentation for pre-commit - Add documentation for pre-commit

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``
@@ -45,9 +46,9 @@ It can be chosen using:
Extending the default configuration Extending the default configuration
----------------------------------- -----------------------------------
When writing a custom configuration file, you don't need to redefine every rule. When writing a custom configuration file, you don't need to redefine every
Just extend the ``default`` configuration (or any already-existing configuration rule. Just extend the ``default`` configuration (or any already-existing
file). configuration file).
For instance, if you just want to disable the ``comments-indentation`` rule, For instance, if you just want to disable the ``comments-indentation`` rule,
your file could look like this: your file could look like this:
@@ -105,8 +106,8 @@ Problems detected by yamllint can be raised either as errors or as warnings.
The CLI will output them (with different colors when using the ``colored`` The CLI will output them (with different colors when using the ``colored``
output format, or ``auto`` when run from a terminal). output format, or ``auto`` when run from a terminal).
By default the script will exit with a return code ``1`` *only when* there is one or By default the script will exit with a return code ``1`` *only when* there is
more error(s). one or more error(s).
However if strict mode is enabled with the ``-s`` (or ``--strict``) option, the However if strict mode is enabled with the ``-s`` (or ``--strict``) option, the
return code will be: return code will be:
@@ -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

@@ -4,9 +4,9 @@ Disable with comments
Disabling checks for a specific line Disabling checks for a specific line
------------------------------------ ------------------------------------
To prevent yamllint from reporting problems for a specific line, add a directive To prevent yamllint from reporting problems for a specific line, add a
comment (``# yamllint disable-line ...``) on that line, or on the line above. directive comment (``# yamllint disable-line ...``) on that line, or on the
For instance: line above. For instance:
.. code-block:: yaml .. code-block:: yaml
@@ -46,9 +46,9 @@ If you need to disable multiple rules, it is allowed to chain rules like this:
Disabling checks for all (or part of) the file Disabling checks for all (or part of) the file
---------------------------------------------- ----------------------------------------------
To prevent yamllint from reporting problems for the whole file, or for a block of To prevent yamllint from reporting problems for the whole file, or for a block
lines within the file, use ``# yamllint disable ...`` and ``# yamllint enable of lines within the file, use ``# yamllint disable ...`` and ``# yamllint
...`` directive comments. For instance: enable ...`` directive comments. For instance:
.. code-block:: yaml .. code-block:: yaml
@@ -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):
:: ::
@@ -68,9 +87,9 @@ The output will look like (colors are not displayed here):
10:1 error too many blank lines (4 > 2) (empty-lines) 10:1 error too many blank lines (4 > 2) (empty-lines)
11:4 error too many spaces inside braces (braces) 11:4 error too many spaces inside braces (braces)
By default, the output of yamllint is colored when run from a terminal, and pure By default, the output of yamllint is colored when run from a terminal, and
text in other cases. Add the ``-f standard`` arguments to force non-colored output. pure text in other cases. Add the ``-f standard`` arguments to force
Use the ``-f colored`` arguments to force colored output. non-colored output. Use the ``-f colored`` arguments to force colored output.
Add the ``-f parsable`` arguments if you need an output format parsable by a Add the ``-f parsable`` arguments if you need an output format parsable by a
machine (for instance for :doc:`syntax highlighting in text editors machine (for instance for :doc:`syntax highlighting in text editors

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)
@@ -29,13 +29,19 @@ setup(
license=__license__, license=__license__,
keywords=['yaml', 'lint', 'linter', 'syntax', 'checker'], keywords=['yaml', 'lint', 'linter', 'syntax', 'checker'],
url='https://github.com/adrienverge/yamllint', url='https://github.com/adrienverge/yamllint',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Environment :: Console', 'Environment :: Console',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Software Development', 'Topic :: Software Development',
'Topic :: Software Development :: Debuggers', 'Topic :: Software Development :: Debuggers',
'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Quality Assurance',

View File

@@ -16,12 +16,7 @@
import os import os
import tempfile import tempfile
import sys import unittest
try:
assert sys.version_info >= (2, 7)
import unittest
except AssertionError:
import unittest2 as unittest
import yaml import yaml

View File

@@ -80,6 +80,48 @@ class CommentsTestCase(RuleTestCase):
problem3=(9, 2), problem4=(10, 4), problem3=(9, 2), problem4=(10, 4),
problem5=(15, 3)) problem5=(15, 3))
def test_shebang(self):
conf = ('comments:\n'
' require-starting-space: true\n'
' ignore-shebangs: false\n'
'comments-indentation: disable\n'
'document-start: disable\n')
self.check('#!/bin/env my-interpreter\n',
conf, problem1=(1, 2))
self.check('# comment\n'
'#!/bin/env my-interpreter\n', conf,
problem1=(2, 2))
self.check('#!/bin/env my-interpreter\n'
'---\n'
'#comment\n'
'#!/bin/env my-interpreter\n'
'', conf,
problem1=(1, 2), problem2=(3, 2), problem3=(4, 2))
self.check('#! not a shebang\n',
conf, problem1=(1, 2))
self.check('key: #!/not/a/shebang\n',
conf, problem1=(1, 8))
def test_ignore_shebang(self):
conf = ('comments:\n'
' require-starting-space: true\n'
' ignore-shebangs: true\n'
'comments-indentation: disable\n'
'document-start: disable\n')
self.check('#!/bin/env my-interpreter\n', conf)
self.check('# comment\n'
'#!/bin/env my-interpreter\n', conf,
problem1=(2, 2))
self.check('#!/bin/env my-interpreter\n'
'---\n'
'#comment\n'
'#!/bin/env my-interpreter\n', conf,
problem2=(3, 2), problem3=(4, 2))
self.check('#! not a shebang\n',
conf, problem1=(1, 2))
self.check('key: #!/not/a/shebang\n',
conf, problem1=(1, 8))
def test_spaces_from_content(self): def test_spaces_from_content(self):
conf = ('comments:\n' conf = ('comments:\n'
' require-starting-space: false\n' ' require-starting-space: false\n'

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

@@ -51,8 +51,8 @@ class IndentationStackTestCase(RuleTestCase):
.replace('Mapping', 'Map')) .replace('Mapping', 'Map'))
if token_type in ('StreamStart', 'StreamEnd'): if token_type in ('StreamStart', 'StreamEnd'):
continue continue
output += '%9s %s\n' % (token_type, output += '{:>9} {}\n'.format(token_type,
self.format_stack(context['stack'])) self.format_stack(context['stack']))
return output return output
def test_simple_mapping(self): def test_simple_mapping(self):

View File

@@ -14,6 +14,9 @@
# 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/>.
import sys
import unittest
from tests.common import RuleTestCase from tests.common import RuleTestCase
@@ -155,3 +158,25 @@ class LineLengthTestCase(RuleTestCase):
'content: |\n' 'content: |\n'
' {% this line is' + 99 * ' really' + ' long %}\n', ' {% this line is' + 99 * ' really' + ' long %}\n',
conf, problem=(3, 81)) conf, problem=(3, 81))
@unittest.skipIf(sys.version_info < (3, 0), 'Python 2 not supported')
def test_unicode(self):
conf = 'line-length: {max: 53}'
self.check('---\n'
'# This is a test to check if “line-length” works nice\n'
'with: “unicode characters” that span accross bytes! ↺\n',
conf)
conf = 'line-length: {max: 52}'
self.check('---\n'
'# This is a test to check if “line-length” works nice\n'
'with: “unicode characters” that span accross bytes! ↺\n',
conf, problem1=(2, 53), problem2=(3, 53))
def test_with_dos_newlines(self):
conf = ('line-length: {max: 10}\n'
'new-lines: {type: dos}\n'
'new-line-at-end-of-file: disable\n')
self.check('---\r\nABCD EFGHI', conf)
self.check('---\r\nABCD EFGHI\r\n', conf)
self.check('---\r\nABCD EFGHIJ', conf, problem=(2, 11))
self.check('---\r\nABCD EFGHIJ\r\n', conf, problem=(2, 11))

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

@@ -24,18 +24,37 @@ import os
import pty import pty
import shutil import shutil
import sys import sys
try: import unittest
assert sys.version_info >= (2, 7)
import unittest
except AssertionError:
import unittest2 as 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
@unittest.skipIf(sys.version_info < (2, 7), 'Python 2.6 not supported')
class CommandLineTestCase(unittest.TestCase): class CommandLineTestCase(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -63,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,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'),
@@ -90,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')],
) )
@@ -105,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.
@@ -303,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)
@@ -382,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

@@ -21,11 +21,8 @@ except ImportError:
import os import os
import shutil import shutil
import sys import sys
try: import tempfile
assert sys.version_info >= (2, 7) import unittest
import unittest
except AssertionError:
import unittest2 as unittest
from tests.common import build_temp_workspace from tests.common import build_temp_workspace
@@ -58,13 +55,16 @@ class SimpleConfigTestCase(unittest.TestCase):
' this-one-does-not-exist: enable\n') ' this-one-does-not-exist: enable\n')
def test_missing_option(self): def test_missing_option(self):
with self.assertRaisesRegexp( c = config.YamlLintConfig('rules:\n'
config.YamlLintConfigError, ' colons: enable\n')
'invalid config: missing option "max-spaces-before" ' self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
'for rule "colons"'): self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
config.YamlLintConfig('rules:\n'
c = config.YamlLintConfig('rules:\n'
' colons:\n' ' colons:\n'
' max-spaces-after: 1\n') ' max-spaces-before: 9\n')
self.assertEqual(c.rules['colons']['max-spaces-before'], 9)
self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
def test_unknown_option(self): def test_unknown_option(self):
with self.assertRaisesRegexp( with self.assertRaisesRegexp(
@@ -82,7 +82,7 @@ class SimpleConfigTestCase(unittest.TestCase):
' spaces: 2\n' ' spaces: 2\n'
' indent-sequences: true\n' ' indent-sequences: true\n'
' check-multi-line-strings: false\n') ' check-multi-line-strings: false\n')
self.assertEqual(c.rules['indentation']['indent-sequences'], True) self.assertTrue(c.rules['indentation']['indent-sequences'])
self.assertEqual(c.rules['indentation']['check-multi-line-strings'], self.assertEqual(c.rules['indentation']['check-multi-line-strings'],
False) False)
@@ -91,7 +91,7 @@ class SimpleConfigTestCase(unittest.TestCase):
' spaces: 2\n' ' spaces: 2\n'
' indent-sequences: yes\n' ' indent-sequences: yes\n'
' check-multi-line-strings: false\n') ' check-multi-line-strings: false\n')
self.assertEqual(c.rules['indentation']['indent-sequences'], True) self.assertTrue(c.rules['indentation']['indent-sequences'])
self.assertEqual(c.rules['indentation']['check-multi-line-strings'], self.assertEqual(c.rules['indentation']['check-multi-line-strings'],
False) False)
@@ -115,17 +115,22 @@ class SimpleConfigTestCase(unittest.TestCase):
' indent-sequences: YES!\n' ' indent-sequences: YES!\n'
' check-multi-line-strings: false\n') ' check-multi-line-strings: false\n')
def test_enable_disable_keywords(self):
c = config.YamlLintConfig('rules:\n'
' colons: enable\n'
' hyphens: disable\n')
self.assertEqual(c.rules['colons'], {'level': 'error',
'max-spaces-after': 1,
'max-spaces-before': 0})
self.assertEqual(c.rules['hyphens'], False)
def test_validate_rule_conf(self): def test_validate_rule_conf(self):
class Rule(object): class Rule(object):
ID = 'fake' ID = 'fake'
self.assertEqual(config.validate_rule_conf(Rule, False), False) self.assertFalse(config.validate_rule_conf(Rule, False))
self.assertEqual(config.validate_rule_conf(Rule, 'disable'), False)
self.assertEqual(config.validate_rule_conf(Rule, {}), self.assertEqual(config.validate_rule_conf(Rule, {}),
{'level': 'error'}) {'level': 'error'})
self.assertEqual(config.validate_rule_conf(Rule, 'enable'),
{'level': 'error'})
config.validate_rule_conf(Rule, {'level': 'error'}) config.validate_rule_conf(Rule, {'level': 'error'})
config.validate_rule_conf(Rule, {'level': 'warning'}) config.validate_rule_conf(Rule, {'level': 'warning'})
@@ -133,22 +138,22 @@ class SimpleConfigTestCase(unittest.TestCase):
config.validate_rule_conf, Rule, {'level': 'warn'}) config.validate_rule_conf, Rule, {'level': 'warn'})
Rule.CONF = {'length': int} Rule.CONF = {'length': int}
Rule.DEFAULT = {'length': 80}
config.validate_rule_conf(Rule, {'length': 8}) config.validate_rule_conf(Rule, {'length': 8})
self.assertRaises(config.YamlLintConfigError, config.validate_rule_conf(Rule, {})
config.validate_rule_conf, Rule, {})
self.assertRaises(config.YamlLintConfigError, self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'height': 8}) config.validate_rule_conf, Rule, {'height': 8})
Rule.CONF = {'a': bool, 'b': int} Rule.CONF = {'a': bool, 'b': int}
Rule.DEFAULT = {'a': True, 'b': -42}
config.validate_rule_conf(Rule, {'a': True, 'b': 0}) config.validate_rule_conf(Rule, {'a': True, 'b': 0})
self.assertRaises(config.YamlLintConfigError, config.validate_rule_conf(Rule, {'a': True})
config.validate_rule_conf, Rule, {'a': True}) config.validate_rule_conf(Rule, {'b': 0})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'b': 0})
self.assertRaises(config.YamlLintConfigError, self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'a': 1, 'b': 0}) config.validate_rule_conf, Rule, {'a': 1, 'b': 0})
Rule.CONF = {'choice': (True, 88, 'str')} Rule.CONF = {'choice': (True, 88, 'str')}
Rule.DEFAULT = {'choice': 88}
config.validate_rule_conf(Rule, {'choice': True}) config.validate_rule_conf(Rule, {'choice': True})
config.validate_rule_conf(Rule, {'choice': 88}) config.validate_rule_conf(Rule, {'choice': 88})
config.validate_rule_conf(Rule, {'choice': 'str'}) config.validate_rule_conf(Rule, {'choice': 'str'})
@@ -160,16 +165,37 @@ class SimpleConfigTestCase(unittest.TestCase):
config.validate_rule_conf, Rule, {'choice': 'abc'}) config.validate_rule_conf, Rule, {'choice': 'abc'})
Rule.CONF = {'choice': (int, 'hardcoded')} Rule.CONF = {'choice': (int, 'hardcoded')}
Rule.DEFAULT = {'choice': 1337}
config.validate_rule_conf(Rule, {'choice': 42}) config.validate_rule_conf(Rule, {'choice': 42})
config.validate_rule_conf(Rule, {'choice': 'hardcoded'}) config.validate_rule_conf(Rule, {'choice': 'hardcoded'})
config.validate_rule_conf(Rule, {})
self.assertRaises(config.YamlLintConfigError, self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': False}) config.validate_rule_conf, Rule, {'choice': False})
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_add_rule(self): def test_extend_on_object(self):
old = config.YamlLintConfig('rules:\n' old = config.YamlLintConfig('rules:\n'
' colons:\n' ' colons:\n'
' max-spaces-before: 0\n' ' max-spaces-before: 0\n'
@@ -186,60 +212,130 @@ class ExtendedConfigTestCase(unittest.TestCase):
self.assertEqual(len(new.enabled_rules(None)), 2) self.assertEqual(len(new.enabled_rules(None)), 2)
def test_extend_on_file(self):
with tempfile.NamedTemporaryFile('w') as f:
f.write('rules:\n'
' colons:\n'
' max-spaces-before: 0\n'
' max-spaces-after: 1\n')
f.flush()
c = config.YamlLintConfig('extends: ' + f.name + '\n'
'rules:\n'
' hyphens:\n'
' max-spaces-after: 2\n')
self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(c.enabled_rules(None)), 2)
def test_extend_remove_rule(self): def test_extend_remove_rule(self):
old = config.YamlLintConfig('rules:\n' with tempfile.NamedTemporaryFile('w') as f:
' colons:\n' f.write('rules:\n'
' max-spaces-before: 0\n' ' colons:\n'
' max-spaces-after: 1\n' ' max-spaces-before: 0\n'
' hyphens:\n' ' max-spaces-after: 1\n'
' max-spaces-after: 2\n') ' hyphens:\n'
new = config.YamlLintConfig('rules:\n' ' max-spaces-after: 2\n')
' colons: disable\n') f.flush()
new.extend(old) c = config.YamlLintConfig('extends: ' + f.name + '\n'
'rules:\n'
' colons: disable\n')
self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens']) self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(new.rules['colons'], False) self.assertFalse(c.rules['colons'])
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2) self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(new.enabled_rules(None)), 1) self.assertEqual(len(c.enabled_rules(None)), 1)
def test_extend_edit_rule(self): def test_extend_edit_rule(self):
old = config.YamlLintConfig('rules:\n' with tempfile.NamedTemporaryFile('w') as f:
' colons:\n' f.write('rules:\n'
' max-spaces-before: 0\n' ' colons:\n'
' max-spaces-after: 1\n' ' max-spaces-before: 0\n'
' hyphens:\n' ' max-spaces-after: 1\n'
' max-spaces-after: 2\n') ' hyphens:\n'
new = config.YamlLintConfig('rules:\n' ' max-spaces-after: 2\n')
' colons:\n' f.flush()
' max-spaces-before: 3\n' c = config.YamlLintConfig('extends: ' + f.name + '\n'
' max-spaces-after: 4\n') 'rules:\n'
new.extend(old) ' colons:\n'
' max-spaces-before: 3\n'
' max-spaces-after: 4\n')
self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens']) self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(new.rules['colons']['max-spaces-before'], 3) self.assertEqual(c.rules['colons']['max-spaces-before'], 3)
self.assertEqual(new.rules['colons']['max-spaces-after'], 4) self.assertEqual(c.rules['colons']['max-spaces-after'], 4)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2) self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(new.enabled_rules(None)), 2) self.assertEqual(len(c.enabled_rules(None)), 2)
def test_extend_reenable_rule(self): def test_extend_reenable_rule(self):
old = config.YamlLintConfig('rules:\n' with tempfile.NamedTemporaryFile('w') as f:
' colons:\n' f.write('rules:\n'
' max-spaces-before: 0\n' ' colons:\n'
' max-spaces-after: 1\n' ' max-spaces-before: 0\n'
' hyphens: disable\n') ' max-spaces-after: 1\n'
new = config.YamlLintConfig('rules:\n' ' hyphens: disable\n')
' hyphens:\n' f.flush()
' max-spaces-after: 2\n') c = config.YamlLintConfig('extends: ' + f.name + '\n'
new.extend(old) 'rules:\n'
' hyphens:\n'
' max-spaces-after: 2\n')
self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens']) self.assertEqual(sorted(c.rules.keys()), ['colons', 'hyphens'])
self.assertEqual(new.rules['colons']['max-spaces-before'], 0) self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
self.assertEqual(new.rules['colons']['max-spaces-after'], 1) self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2) self.assertEqual(c.rules['hyphens']['max-spaces-after'], 2)
self.assertEqual(len(new.enabled_rules(None)), 2) self.assertEqual(len(c.enabled_rules(None)), 2)
def test_extend_recursive_default_values(self):
with tempfile.NamedTemporaryFile('w') as f:
f.write('rules:\n'
' braces:\n'
' max-spaces-inside: 1248\n')
f.flush()
c = config.YamlLintConfig('extends: ' + f.name + '\n'
'rules:\n'
' braces:\n'
' min-spaces-inside-empty: 2357\n')
self.assertEqual(c.rules['braces']['min-spaces-inside'], 0)
self.assertEqual(c.rules['braces']['max-spaces-inside'], 1248)
self.assertEqual(c.rules['braces']['min-spaces-inside-empty'], 2357)
self.assertEqual(c.rules['braces']['max-spaces-inside-empty'], -1)
with tempfile.NamedTemporaryFile('w') as f:
f.write('rules:\n'
' colons:\n'
' max-spaces-before: 1337\n')
f.flush()
c = config.YamlLintConfig('extends: ' + f.name + '\n'
'rules:\n'
' colons: enable\n')
self.assertEqual(c.rules['colons']['max-spaces-before'], 1337)
self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
with tempfile.NamedTemporaryFile('w') as f1, \
tempfile.NamedTemporaryFile('w') as f2:
f1.write('rules:\n'
' colons:\n'
' max-spaces-before: 1337\n')
f1.flush()
f2.write('extends: ' + f1.name + '\n'
'rules:\n'
' colons: disable\n')
f2.flush()
c = config.YamlLintConfig('extends: ' + f2.name + '\n'
'rules:\n'
' colons: enable\n')
self.assertEqual(c.rules['colons']['max-spaces-before'], 0)
self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
class ExtendedLibraryConfigTestCase(unittest.TestCase): class ExtendedLibraryConfigTestCase(unittest.TestCase):
@@ -271,6 +367,9 @@ class ExtendedLibraryConfigTestCase(unittest.TestCase):
self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new.rules: for rule in new.rules:
self.assertEqual(new.rules[rule], old.rules[rule]) self.assertEqual(new.rules[rule], old.rules[rule])
self.assertEqual(new.rules['empty-lines']['max'], 42)
self.assertEqual(new.rules['empty-lines']['max-start'], 43)
self.assertEqual(new.rules['empty-lines']['max-end'], 44)
def test_extend_config_override_rule_partly(self): def test_extend_config_override_rule_partly(self):
old = config.YamlLintConfig('extends: default') old = config.YamlLintConfig('extends: default')
@@ -284,6 +383,9 @@ class ExtendedLibraryConfigTestCase(unittest.TestCase):
self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new.rules: for rule in new.rules:
self.assertEqual(new.rules[rule], old.rules[rule]) self.assertEqual(new.rules[rule], old.rules[rule])
self.assertEqual(new.rules['empty-lines']['max'], 2)
self.assertEqual(new.rules['empty-lines']['max-start'], 42)
self.assertEqual(new.rules['empty-lines']['max-end'], 0)
class IgnorePathConfigTestCase(unittest.TestCase): class IgnorePathConfigTestCase(unittest.TestCase):
@@ -338,7 +440,6 @@ class IgnorePathConfigTestCase(unittest.TestCase):
shutil.rmtree(cls.wd) shutil.rmtree(cls.wd)
@unittest.skipIf(sys.version_info < (2, 7), 'Python 2.6 not supported')
def test_run_with_ignored_path(self): def test_run_with_ignored_path(self):
sys.stdout = StringIO() sys.stdout = StringIO()
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):
@@ -347,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

@@ -15,12 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import io import io
import sys import unittest
try:
assert sys.version_info >= (2, 7)
import unittest
except AssertionError:
import unittest2 as unittest
from yamllint.config import YamlLintConfig from yamllint.config import YamlLintConfig
from yamllint import linter from yamllint import linter

View File

@@ -19,17 +19,12 @@ import shutil
import subprocess import subprocess
import tempfile import tempfile
import sys import sys
try: import unittest
assert sys.version_info >= (2, 7)
import unittest
except AssertionError:
import unittest2 as unittest
PYTHON = sys.executable or 'python' PYTHON = sys.executable or 'python'
@unittest.skipIf(sys.version_info < (2, 7), 'Python 2.6 not supported')
class ModuleTestCase(unittest.TestCase): class ModuleTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.wd = tempfile.mkdtemp(prefix='yamllint-tests-') self.wd = tempfile.mkdtemp(prefix='yamllint-tests-')

View File

@@ -14,12 +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/>.
import sys import unittest
try:
assert sys.version_info >= (2, 7)
import unittest
except AssertionError:
import unittest2 as unittest
import yaml import yaml
@@ -70,12 +65,12 @@ class ParserTestCase(unittest.TestCase):
def test_token_or_comment_generator(self): def test_token_or_comment_generator(self):
e = list(token_or_comment_generator('')) e = list(token_or_comment_generator(''))
self.assertEqual(len(e), 2) self.assertEqual(len(e), 2)
self.assertEqual(e[0].prev, None) self.assertIsNone(e[0].prev)
self.assertIsInstance(e[0].curr, yaml.Token) self.assertIsInstance(e[0].curr, yaml.Token)
self.assertIsInstance(e[0].next, yaml.Token) self.assertIsInstance(e[0].next, yaml.Token)
self.assertEqual(e[1].prev, e[0].curr) self.assertEqual(e[1].prev, e[0].curr)
self.assertEqual(e[1].curr, e[0].next) self.assertEqual(e[1].curr, e[0].next)
self.assertEqual(e[1].next, None) self.assertIsNone(e[1].next)
e = list(token_or_comment_generator('---\n' e = list(token_or_comment_generator('---\n'
'k: v\n')) 'k: v\n'))

View File

@@ -54,8 +54,8 @@ conf_general = ('document-start: disable\n'
'braces: {min-spaces-inside: 1, max-spaces-inside: 1}\n' 'braces: {min-spaces-inside: 1, max-spaces-inside: 1}\n'
'brackets: {min-spaces-inside: 1, max-spaces-inside: 1}\n') 'brackets: {min-spaces-inside: 1, max-spaces-inside: 1}\n')
conf_overrides = { conf_overrides = {
'example-2.2': ('colons: {max-spaces-after: 2}\n'), 'example-2.2': 'colons: {max-spaces-after: 2}\n',
'example-2.4': ('colons: {max-spaces-after: 3}\n'), 'example-2.4': 'colons: {max-spaces-after: 3}\n',
'example-2.5': ('empty-lines: {max-end: 2}\n' 'example-2.5': ('empty-lines: {max-end: 2}\n'
'brackets: {min-spaces-inside: 0, max-spaces-inside: 2}\n' 'brackets: {min-spaces-inside: 0, max-spaces-inside: 2}\n'
'commas: {max-spaces-before: -1}\n'), 'commas: {max-spaces-before: -1}\n'),
@@ -63,65 +63,65 @@ conf_overrides = {
'indentation: disable\n'), 'indentation: disable\n'),
'example-2.12': ('empty-lines: {max-end: 1}\n' 'example-2.12': ('empty-lines: {max-end: 1}\n'
'colons: {max-spaces-before: -1}\n'), 'colons: {max-spaces-before: -1}\n'),
'example-2.16': ('empty-lines: {max-end: 1}\n'), 'example-2.16': 'empty-lines: {max-end: 1}\n',
'example-2.18': ('empty-lines: {max-end: 1}\n'), 'example-2.18': 'empty-lines: {max-end: 1}\n',
'example-2.19': ('empty-lines: {max-end: 1}\n'), 'example-2.19': 'empty-lines: {max-end: 1}\n',
'example-2.28': ('empty-lines: {max-end: 3}\n'), 'example-2.28': 'empty-lines: {max-end: 3}\n',
'example-5.3': ('indentation: {indent-sequences: false}\n' 'example-5.3': ('indentation: {indent-sequences: false}\n'
'colons: {max-spaces-before: 1}\n'), 'colons: {max-spaces-before: 1}\n'),
'example-6.4': ('trailing-spaces: disable\n'), 'example-6.4': 'trailing-spaces: disable\n',
'example-6.5': ('trailing-spaces: disable\n'), 'example-6.5': 'trailing-spaces: disable\n',
'example-6.6': ('trailing-spaces: disable\n'), 'example-6.6': 'trailing-spaces: disable\n',
'example-6.7': ('trailing-spaces: disable\n'), 'example-6.7': 'trailing-spaces: disable\n',
'example-6.8': ('trailing-spaces: disable\n'), 'example-6.8': 'trailing-spaces: disable\n',
'example-6.10': ('empty-lines: {max-end: 2}\n' 'example-6.10': ('empty-lines: {max-end: 2}\n'
'trailing-spaces: disable\n' 'trailing-spaces: disable\n'
'comments-indentation: disable\n'), 'comments-indentation: disable\n'),
'example-6.11': ('empty-lines: {max-end: 1}\n' 'example-6.11': ('empty-lines: {max-end: 1}\n'
'comments-indentation: disable\n'), 'comments-indentation: disable\n'),
'example-6.13': ('comments-indentation: disable\n'), 'example-6.13': 'comments-indentation: disable\n',
'example-6.14': ('comments-indentation: disable\n'), 'example-6.14': 'comments-indentation: disable\n',
'example-6.23': ('colons: {max-spaces-before: 1}\n'), 'example-6.23': 'colons: {max-spaces-before: 1}\n',
'example-7.4': ('colons: {max-spaces-before: 1}\n' 'example-7.4': ('colons: {max-spaces-before: 1}\n'
'indentation: disable\n'), 'indentation: disable\n'),
'example-7.5': ('trailing-spaces: disable\n'), 'example-7.5': 'trailing-spaces: disable\n',
'example-7.6': ('trailing-spaces: disable\n'), 'example-7.6': 'trailing-spaces: disable\n',
'example-7.7': ('indentation: disable\n'), 'example-7.7': 'indentation: disable\n',
'example-7.8': ('colons: {max-spaces-before: 1}\n' 'example-7.8': ('colons: {max-spaces-before: 1}\n'
'indentation: disable\n'), 'indentation: disable\n'),
'example-7.9': ('trailing-spaces: disable\n'), 'example-7.9': 'trailing-spaces: disable\n',
'example-7.11': ('colons: {max-spaces-before: 1}\n' 'example-7.11': ('colons: {max-spaces-before: 1}\n'
'indentation: disable\n'), 'indentation: disable\n'),
'example-7.13': ('brackets: {min-spaces-inside: 0, max-spaces-inside: 1}\n' 'example-7.13': ('brackets: {min-spaces-inside: 0, max-spaces-inside: 1}\n'
'commas: {max-spaces-before: 1, min-spaces-after: 0}\n'), 'commas: {max-spaces-before: 1, min-spaces-after: 0}\n'),
'example-7.14': ('indentation: disable\n'), 'example-7.14': 'indentation: disable\n',
'example-7.15': ('braces: {min-spaces-inside: 0, max-spaces-inside: 1}\n' 'example-7.15': ('braces: {min-spaces-inside: 0, max-spaces-inside: 1}\n'
'commas: {max-spaces-before: 1, min-spaces-after: 0}\n' 'commas: {max-spaces-before: 1, min-spaces-after: 0}\n'
'colons: {max-spaces-before: 1}\n'), 'colons: {max-spaces-before: 1}\n'),
'example-7.16': ('indentation: disable\n'), 'example-7.16': 'indentation: disable\n',
'example-7.17': ('indentation: disable\n'), 'example-7.17': 'indentation: disable\n',
'example-7.18': ('indentation: disable\n'), 'example-7.18': 'indentation: disable\n',
'example-7.19': ('indentation: disable\n'), 'example-7.19': 'indentation: disable\n',
'example-7.20': ('colons: {max-spaces-before: 1}\n' 'example-7.20': ('colons: {max-spaces-before: 1}\n'
'indentation: disable\n'), 'indentation: disable\n'),
'example-8.1': ('empty-lines: {max-end: 1}\n'), 'example-8.1': 'empty-lines: {max-end: 1}\n',
'example-8.2': ('trailing-spaces: disable\n'), 'example-8.2': 'trailing-spaces: disable\n',
'example-8.5': ('comments-indentation: disable\n' 'example-8.5': ('comments-indentation: disable\n'
'trailing-spaces: disable\n'), 'trailing-spaces: disable\n'),
'example-8.6': ('empty-lines: {max-end: 1}\n'), 'example-8.6': 'empty-lines: {max-end: 1}\n',
'example-8.7': ('empty-lines: {max-end: 1}\n'), 'example-8.7': 'empty-lines: {max-end: 1}\n',
'example-8.8': ('trailing-spaces: disable\n'), 'example-8.8': 'trailing-spaces: disable\n',
'example-8.9': ('empty-lines: {max-end: 1}\n'), 'example-8.9': 'empty-lines: {max-end: 1}\n',
'example-8.14': ('colons: {max-spaces-before: 1}\n'), 'example-8.14': 'colons: {max-spaces-before: 1}\n',
'example-8.16': ('indentation: {spaces: 1}\n'), 'example-8.16': 'indentation: {spaces: 1}\n',
'example-8.17': ('indentation: disable\n'), 'example-8.17': 'indentation: disable\n',
'example-8.20': ('indentation: {indent-sequences: false}\n' 'example-8.20': ('indentation: {indent-sequences: false}\n'
'colons: {max-spaces-before: 1}\n'), 'colons: {max-spaces-before: 1}\n'),
'example-8.22': ('indentation: disable\n'), 'example-8.22': 'indentation: disable\n',
'example-10.1': ('colons: {max-spaces-before: 2}\n'), 'example-10.1': 'colons: {max-spaces-before: 2}\n',
'example-10.2': ('indentation: {indent-sequences: false}\n'), 'example-10.2': 'indentation: {indent-sequences: false}\n',
'example-10.8': ('truthy: disable\n'), 'example-10.8': 'truthy: disable\n',
'example-10.9': ('truthy: disable\n'), 'example-10.9': 'truthy: disable\n',
} }
files = os.listdir(os.path.join(os.path.dirname(os.path.realpath(__file__)), files = os.listdir(os.path.join(os.path.dirname(os.path.realpath(__file__)),

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.13.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,10 +136,11 @@ 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='%s %s' % (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)
@@ -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,59 +1,33 @@
--- ---
yaml-files:
- '*.yaml'
- '*.yml'
- '.yamllint'
rules: rules:
braces: braces: enable
min-spaces-inside: 0 brackets: enable
max-spaces-inside: 0 colons: enable
min-spaces-inside-empty: -1 commas: enable
max-spaces-inside-empty: -1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
colons:
max-spaces-before: 0
max-spaces-after: 1
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments: comments:
level: warning level: warning
require-starting-space: true
min-spaces-from-content: 2
comments-indentation: comments-indentation:
level: warning level: warning
document-end: disable document-end: disable
document-start: document-start:
level: warning level: warning
present: true empty-lines: enable
empty-lines: empty-values: disable
max: 2 hyphens: enable
max-start: 0 indentation: enable
max-end: 0
quoted-strings: disable
empty-values:
forbid-in-block-mappings: false
forbid-in-flow-mappings: false
hyphens:
max-spaces-after: 1
indentation:
spaces: consistent
indent-sequences: true
check-multi-line-strings: false
key-duplicates: enable key-duplicates: enable
key-ordering: disable key-ordering: disable
line-length: line-length: enable
max: 80
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: false
new-line-at-end-of-file: enable new-line-at-end-of-file: enable
new-lines: new-lines: enable
type: unix octal-values: disable
octal-values: quoted-strings: disable
forbid-implicit-octal: false
forbid-explicit-octal: false
trailing-spaces: enable trailing-spaces: enable
truthy: truthy:
level: warning level: warning

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 (
@@ -74,6 +80,11 @@ class YamlLintConfig(object):
raise YamlLintConfigError('invalid config: not a dict') raise YamlLintConfigError('invalid config: not a dict')
self.rules = conf.get('rules', {}) self.rules = conf.get('rules', {})
for rule in self.rules:
if self.rules[rule] == 'enable':
self.rules[rule] = {}
elif self.rules[rule] == 'disable':
self.rules[rule] = False
# Does this conf override another conf that we need to load? # Does this conf override another conf that we need to load?
if 'extends' in conf: if 'extends' in conf:
@@ -91,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:
@@ -102,10 +122,8 @@ class YamlLintConfig(object):
def validate_rule_conf(rule, conf): def validate_rule_conf(rule, conf):
if conf is False or conf == 'disable': if conf is False: # disable
return False return False
elif conf == 'enable':
conf = {}
if isinstance(conf, dict): if isinstance(conf, dict):
if ('ignore' in conf and if ('ignore' in conf and
@@ -123,6 +141,7 @@ def validate_rule_conf(rule, conf):
'invalid config: level should be "error" or "warning"') 'invalid config: level should be "error" or "warning"')
options = getattr(rule, 'CONF', {}) options = getattr(rule, 'CONF', {})
options_default = getattr(rule, 'DEFAULT', {})
for optkey in conf: for optkey in conf:
if optkey in ('ignore', 'level'): if optkey in ('ignore', 'level'):
continue continue
@@ -130,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(
@@ -143,9 +176,7 @@ def validate_rule_conf(rule, conf):
% (optkey, rule.ID, options[optkey].__name__)) % (optkey, rule.ID, options[optkey].__name__))
for optkey in options: for optkey in options:
if optkey not in conf: if optkey not in conf:
raise YamlLintConfigError( conf[optkey] = options_default[optkey]
'invalid config: missing option "%s" for rule "%s"' %
(optkey, rule.ID))
else: else:
raise YamlLintConfigError(('invalid config: rule "%s": should be ' raise YamlLintConfigError(('invalid config: rule "%s": should be '
'either "enable", "disable" or a dict') 'either "enable", "disable" or a dict')

View File

@@ -47,7 +47,7 @@ class LintProblem(object):
@property @property
def message(self): def message(self):
if self.rule is not None: if self.rule is not None:
return '%s (%s)' % (self.desc, self.rule) return '{} ({})'.format(self.desc, self.rule)
return self.desc return self.desc
def __eq__(self, other): def __eq__(self, other):
@@ -75,10 +75,10 @@ def get_cosmetic_problems(buffer, conf, filepath):
for rule in token_rules: for rule in token_rules:
context[rule.ID] = {} context[rule.ID] = {}
class DisableDirective(): class DisableDirective:
def __init__(self): def __init__(self):
self.rules = set() self.rules = set()
self.all_rules = set([r.ID for r in rules]) self.all_rules = {r.ID for r in rules}
def process_comment(self, comment): def process_comment(self, comment):
try: try:
@@ -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

@@ -101,6 +101,10 @@ CONF = {'min-spaces-inside': int,
'max-spaces-inside': int, 'max-spaces-inside': int,
'min-spaces-inside-empty': int, 'min-spaces-inside-empty': int,
'max-spaces-inside-empty': int} 'max-spaces-inside-empty': int}
DEFAULT = {'min-spaces-inside': 0,
'max-spaces-inside': 0,
'min-spaces-inside-empty': -1,
'max-spaces-inside-empty': -1}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -102,6 +102,10 @@ CONF = {'min-spaces-inside': int,
'max-spaces-inside': int, 'max-spaces-inside': int,
'min-spaces-inside-empty': int, 'min-spaces-inside-empty': int,
'max-spaces-inside-empty': int} 'max-spaces-inside-empty': int}
DEFAULT = {'min-spaces-inside': 0,
'max-spaces-inside': 0,
'min-spaces-inside-empty': -1,
'max-spaces-inside-empty': -1}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -72,13 +72,15 @@ 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'
TYPE = 'token' TYPE = 'token'
CONF = {'max-spaces-before': int, CONF = {'max-spaces-before': int,
'max-spaces-after': int} 'max-spaces-after': int}
DEFAULT = {'max-spaces-before': 0,
'max-spaces-after': 1}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -103,6 +103,9 @@ TYPE = 'token'
CONF = {'max-spaces-before': int, CONF = {'max-spaces-before': int,
'min-spaces-after': int, 'min-spaces-after': int,
'max-spaces-after': int} 'max-spaces-after': int}
DEFAULT = {'max-spaces-before': 0,
'min-spaces-after': 1,
'max-spaces-after': 1}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -21,6 +21,9 @@ Use this rule to control the position and formatting of comments.
* Use ``require-starting-space`` to require a space character right after the * Use ``require-starting-space`` to require a space character right after the
``#``. Set to ``true`` to enable, ``false`` to disable. ``#``. Set to ``true`` to enable, ``false`` to disable.
* Use ``ignore-shebangs`` to ignore a
`shebang <https://en.wikipedia.org/wiki/Shebang_(Unix)>`_ at the beginning of
the file when ``require-starting-space`` is set.
* ``min-spaces-from-content`` is used to visually separate inline comments from * ``min-spaces-from-content`` is used to visually separate inline comments from
content. It defines the minimal required number of spaces between a comment content. It defines the minimal required number of spaces between a comment
and its preceding content. and its preceding content.
@@ -61,13 +64,19 @@ Use this rule to control the position and formatting of comments.
""" """
import re
from yamllint.linter import LintProblem from yamllint.linter import LintProblem
ID = 'comments' ID = 'comments'
TYPE = 'comment' TYPE = 'comment'
CONF = {'require-starting-space': bool, CONF = {'require-starting-space': bool,
'ignore-shebangs': bool,
'min-spaces-from-content': int} 'min-spaces-from-content': int}
DEFAULT = {'require-starting-space': True,
'ignore-shebangs': True,
'min-spaces-from-content': 2}
def check(conf, comment): def check(conf, comment):
@@ -82,8 +91,14 @@ def check(conf, comment):
while (comment.buffer[text_start] == '#' and while (comment.buffer[text_start] == '#' and
text_start < len(comment.buffer)): text_start < len(comment.buffer)):
text_start += 1 text_start += 1
if (text_start < len(comment.buffer) and if text_start < len(comment.buffer):
comment.buffer[text_start] not in (' ', '\n', '\0')): if (conf['ignore-shebangs'] and
yield LintProblem(comment.line_no, comment.line_no == 1 and
comment.column_no + text_start - comment.pointer, comment.column_no == 1 and
'missing starting space in comment') re.match(r'^!\S', comment.buffer[text_start:])):
return
elif comment.buffer[text_start] not in (' ', '\n', '\0'):
column = comment.column_no + text_start - comment.pointer
yield LintProblem(comment.line_no,
column,
'missing starting space in comment')

View File

@@ -82,6 +82,7 @@ from yamllint.linter import LintProblem
ID = 'document-end' ID = 'document-end'
TYPE = 'token' TYPE = 'token'
CONF = {'present': bool} CONF = {'present': bool}
DEFAULT = {'present': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -72,6 +72,7 @@ from yamllint.linter import LintProblem
ID = 'document-start' ID = 'document-start'
TYPE = 'token' TYPE = 'token'
CONF = {'present': bool} CONF = {'present': bool}
DEFAULT = {'present': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -58,32 +58,45 @@ TYPE = 'line'
CONF = {'max': int, CONF = {'max': int,
'max-start': int, 'max-start': int,
'max-end': int} 'max-end': int}
DEFAULT = {'max': 2,
'max-start': 0,
'max-end': 0}
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,6 +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': True,
'forbid-in-flow-mappings': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -76,6 +76,7 @@ from yamllint.rules.common import spaces_after
ID = 'hyphens' ID = 'hyphens'
TYPE = 'token' TYPE = 'token'
CONF = {'max-spaces-after': int} CONF = {'max-spaces-after': int}
DEFAULT = {'max-spaces-after': 1}
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'
@@ -201,6 +201,9 @@ TYPE = 'token'
CONF = {'spaces': (int, 'consistent'), CONF = {'spaces': (int, 'consistent'),
'indent-sequences': (bool, 'whatever', 'consistent'), 'indent-sequences': (bool, 'whatever', 'consistent'),
'check-multi-line-strings': bool} 'check-multi-line-strings': bool}
DEFAULT = {'spaces': 'consistent',
'indent-sequences': True,
'check-multi-line-strings': False}
ROOT, B_MAP, F_MAP, B_SEQ, F_SEQ, B_ENT, KEY, VAL = range(8) ROOT, B_MAP, F_MAP, B_SEQ, F_SEQ, B_ENT, KEY, VAL = range(8)
labels = ('ROOT', 'B_MAP', 'F_MAP', 'B_SEQ', 'F_SEQ', 'B_ENT', 'KEY', 'VAL') labels = ('ROOT', 'B_MAP', 'F_MAP', 'B_SEQ', 'F_SEQ', 'B_ENT', 'KEY', 'VAL')

View File

@@ -61,7 +61,6 @@ from yamllint.linter import LintProblem
ID = 'key-duplicates' ID = 'key-duplicates'
TYPE = 'token' TYPE = 'token'
CONF = {}
MAP, SEQ = range(2) MAP, SEQ = range(2)

View File

@@ -72,7 +72,6 @@ from yamllint.linter import LintProblem
ID = 'key-ordering' ID = 'key-ordering'
TYPE = 'token' TYPE = 'token'
CONF = {}
MAP, SEQ = range(2) MAP, SEQ = range(2)

View File

@@ -17,6 +17,10 @@
""" """
Use this rule to set a limit to lines length. Use this rule to set a limit to lines length.
Note: with Python 2, the ``line-length`` rule may not work properly with
unicode characters because of the way strings are represented in bytes. We
recommend running yamllint with Python 3.
.. rubric:: Options .. rubric:: Options
* ``max`` defines the maximal (inclusive) length of lines. * ``max`` defines the maximal (inclusive) length of lines.
@@ -98,6 +102,9 @@ TYPE = 'line'
CONF = {'max': int, CONF = {'max': int,
'allow-non-breakable-words': bool, 'allow-non-breakable-words': bool,
'allow-non-breakable-inline-mappings': bool} 'allow-non-breakable-inline-mappings': bool}
DEFAULT = {'max': 80,
'allow-non-breakable-words': True,
'allow-non-breakable-inline-mappings': False}
def check_inline_mapping(line): def check_inline_mapping(line):

View File

@@ -30,15 +30,17 @@ from yamllint.linter import LintProblem
ID = 'new-lines' ID = 'new-lines'
TYPE = 'line' TYPE = 'line'
CONF = {'type': ('unix', 'dos')} CONF = {'type': ('unix', 'dos')}
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,6 +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': True,
'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,33 +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'),
'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,30 +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'
CONF = {}
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'])) + "]")