Compare commits

..

85 Commits

Author SHA1 Message Date
Adrien Vergé
058fef7559 yamllint version 1.26.3 2021-08-21 19:26:15 +02:00
Adrien Vergé
f47d5318cf Restore setuptools requirement for Python < 3.8
This reverts commit 8f68248 "Remove runtime dep 'setuptools' for Python
< 3.8". It looks like removing setuptools induces problems on some
systems, see for example the linked discussion.

Fixes https://github.com/adrienverge/yamllint/issues/380.
2021-08-21 19:25:27 +02:00
Adrien Vergé
33ce0fa960 yamllint version 1.26.2 2021-08-03 12:57:43 +02:00
Kyle Finley
43744902e9 setup: update python_requires to comply with PEP 345/440
According to PEP 345 Requires-Python
(https://www.python.org/dev/peps/pep-0345/#requires-python), the value
of this field must be a valid Version Specifier
(https://www.python.org/dev/peps/pep-0345/#version-specifiers). Which
in turn expects this to comply with PEP 440
(https://www.python.org/dev/peps/pep-0440/).

While not an issue for those that directly use `pip`, this will cause
issues for `poetry` users in the next release (if their current stance
is maintained). Discussion of the issue and there stance can be found
here: https://github.com/python-poetry/poetry/issues/4095.
2021-06-10 16:16:22 +02:00
Adrien Vergé
85ccd625a3 yamllint version 1.26.1 2021-04-06 16:02:55 +02:00
Patryk Małek
e53ea093e2 line_length: skip all hash signs starting comment 2021-03-24 18:04:48 +01:00
Adrien Vergé
5d8ef2ea23 CI: Simplify 'pip' commands 2021-03-24 17:16:57 +01:00
Adrien Vergé
4515269233 CI: Fix failing 'coverage' command because of $PATH
Very probably due to:
https://github.com/actions/virtual-environments/issues/2455#issuecomment-787511010
2021-03-24 17:16:57 +01:00
Adrien Vergé
66bf76a362 CI: Switch to GitHub Actions
Because Travis CI is dead.
2021-03-16 16:13:01 +01:00
Daniel M. Capella
8f682481c7 Remove runtime dep 'setuptools' for Python < 3.8
> In recent versions of setuptools and Python, console-script entry
points are using stdlib importlib by default, thus setuptools is no
longer needed as a runtime dependency.

https://github.com/pypa/setuptools/pull/2197
https://github.com/pypa/setuptools/blob/main/CHANGES.rst#v4730
https://docs.python.org/3/library/importlib.metadata.html
2021-02-11 08:55:08 +01:00
Adrien Vergé
0fff4e29e4 yamllint version 1.26.0 2021-01-29 18:10:35 +01:00
Adrien Vergé
1b378ed5b9 quoted-strings: Fix explicit octal recognition
PyYAML implements YAML spec version 1.1, not 1.2. Hence, values starting
with `0o` are not considered as numbers: they are just strings, so they
need quotes when `quoted-strings: {required: true}`.

>>> import yaml
>>> yaml.resolver.Resolver().resolve(yaml.nodes.ScalarNode, '100', (True, False))
'tag:yaml.org,2002:int'
>>> yaml.resolver.Resolver().resolve(yaml.nodes.ScalarNode, '0100', (True, False))
'tag:yaml.org,2002:int'
>>> yaml.resolver.Resolver().resolve(yaml.nodes.ScalarNode, '0o100', (True, False))
'tag:yaml.org,2002:str'

Let's try to prevent that.

Fixes https://github.com/adrienverge/yamllint/issues/351.
2021-01-11 16:38:29 +01:00
Adrien Vergé
a3fc64d134 End support for Python 2
As planned and advertized, yamllint drops support for Python 2 on 2021.
2021-01-06 07:55:10 +01:00
Rusty Geldmacher
ee4d163ff8 Allow only non-empty brackets/braces
We'd like to disallow brackets and braces in our YAML, but there's a
catch: the only way to describe an empty array or hash in YAML is to
supply an empty one (`[]` or `{}`). Otherwise, the value will be null.

This commit adds a `non-empty` option to `forbid` for brackets and
braces. When it is set, all flow and sequence mappings will cause errors
_except_ for empty ones.
2020-12-15 11:52:21 +01:00
Jason Mobarak
22335b294d Add support for Python 3.9, drop Python 3.4
Add support for Python 3.9 since it was officially released in October
and drop support for Python 3.4 since it is end-of-life (EOL).
2020-12-04 10:10:10 +01:00
Rex Ledesma
0f9dffde23 docs: Add configuration for integration with Arcanist 2020-10-24 13:24:55 +02:00
Mathieu Couette
cef0b48993 tests: Add unittest aliases to Python 2.7 2020-10-12 11:24:01 +02:00
Mathieu Couette
11b1f1c14e tests: Fix indentation issues 2020-10-12 11:24:01 +02:00
Mathieu Couette
9ee8c27ac9 tests: Replace deprecated aliases
https://docs.python.org/3/library/unittest.html#deprecated-aliases
2020-10-12 11:24:01 +02:00
Florian Bruhin
8eebab68ab Fix typo in changelog 2020-10-05 09:42:42 +02:00
Per Lundberg
2103bd73de README.rst: fix typo 2020-10-02 12:51:01 +02:00
Adrien Vergé
85c8631183 tests: Stop using deprecated 'python setup.py test'
Using `python setup.py test` is now deprecated [1], users are encouraged
to be explicit about the test command.

Running yamllint tests using the Python standard library (`unittest`)
can be done using:

    python -m unittest discover

Why not nose, tox or pytest? Because they would add a dependency, make
tests running more complicated and verbose for new users, and their
benefit is not worth for this simple project (only 2 runtime
dependencies: PyYAML and pathspec).

Resolves https://github.com/adrienverge/yamllint/issues/328.

[1]: https://github.com/pypa/setuptools/pull/1878
2020-09-30 09:43:31 +02:00
Adrien Vergé
16e0f9d7b2 yamllint version 1.25.0 2020-09-29 08:52:34 +02:00
Mathieu Couette
1a4f9fe00f gitignore: Add /.eggs
Quick PR to ignore the `/.eggs` folder, which appears to be generated every
time the `python setup.py test` command is run.

The content of the `./.eggs/README.txt` file:

> This directory contains eggs that were downloaded by setuptools to build,
> test, and run plug-ins.
> 
> This directory caches those eggs to prevent repeated downloads.
> 
> However, it is safe to delete this directory.
2020-09-27 10:06:33 +02:00
Mathieu Couette
027d1b0a9a directives: Fix DOS lines messing with rule IDs
Fixes #325

The linter allows a directive to contain trailing whitespace characters like
\r, but does not trim them before iterating on the rules. As a result, the last
rule in the list contains the trailing whitespace characters and never matches
any existing rule.

I added the necessary trimming, as well as a test with 2 checks to go along
with it.
2020-09-26 11:12:26 +02:00
Andrew Imeson
67cb4eb24d Auto-change output format if GitHub Actions detected 2020-09-23 15:41:25 +02:00
Andrew Imeson
50c7453824 Add support for GitHub Annotations output format
Support the format used by GitHub Actions to annotate pull
requests with linter failures
2020-09-23 15:41:25 +02:00
Satoru SATOH
549b136a04 fix: add runtime dependency to setuptools
yamllint depends on pkg_resources.load_entry_point from setuptools to
make its command working, so this runtime dependency to setuptools is
necessary to be listed.
2020-09-14 10:14:10 +02:00
Satoru SATOH
333ae52c78 Add 'forbid' configurations to the braces and brackets rules
Add 'forbid' configuration parameters to the braces and brackets rules
to allow users to forbid the use of flow style collections, flow
mappings and flow sequences.
2020-09-09 20:01:25 +02:00
Julien Falque
0a88c55194 quoted-strings: Fix detecting strings with hashtag as requiring quotes 2020-09-08 11:53:06 +02:00
Julien Falque
ac19d1e427 octal-values: Prevent detection of 8 and 9 as octal values 2020-09-08 09:53:54 +02:00
Adrien Vergé
597e88bb7b docs: Make 'yaml-file' config documentation clearer
Related to https://github.com/adrienverge/yamllint/issues/311.
2020-09-02 18:23:51 +02:00
Satoru SATOH
29d2b50d50 enhancement: add some metadata to provide extra info in its PyPI page
Add some metadata (project_urls) to provide extra info in its PyPI page.

Signed-off-by: Satoru SATOH <satoru.satoh@gmail.com>
2020-09-02 18:02:27 +02:00
Satoru SATOH
4171cdafc9 Move setuptools' packaging configuration from setup.py to setup.cfg
Move setuptools' packaging configuration from setup.py to setup.cfg to
simplify setup.py and make its packaging more dedeclarative.

Signed-off-by: Satoru SATOH <satoru.satoh@gmail.com>
2020-09-02 18:02:27 +02:00
Sorin Sbarnea
d274543b72 docs: Add Python API usage example
Fixes: #297
2020-08-25 20:10:32 +02:00
Kirill Deyko
8da98f2122 commas: Fix example in documentation
Error in the example snippet, it would NOT pass otherwise actually:
```
$ cat test.yml
strange var:
  [10, 20,30, {x: 1, y: 2}]

$ yamllint -d "{extends: default, rules: {commas: {min-spaces-after: 1, max-spaces-after: 1}}}" test.yml
test.yml
  1:1       warning  missing document start "---"  (document-start)
  2:11      error    too few spaces after comma  (commas)
```
2020-08-18 20:07:54 +02:00
Benjamin Wuethrich
b65769c9d2 docs: Add default values to rules with options 2020-07-22 11:54:05 +02:00
Wolfgang Walther
b80997eba6 CI: Add build environment without UTF-8 locales to travis-ci
Preventing regressions like #285
2020-07-20 13:57:06 +02:00
Adrien Vergé
8b758d4e7e yamllint version 1.24.2 2020-07-16 09:35:08 +02:00
Wolfgang Walther
b5b436a3a4 Add global "locale" config option and make key-ordering rule locale-aware
Support sorting by locale with strcoll(). Properly handle case and accents.

Note: this is a second implementation, for context see:
https://github.com/adrienverge/yamllint/pull/280
https://github.com/adrienverge/yamllint/issues/285
https://github.com/adrienverge/yamllint/pull/288
2020-07-16 09:34:13 +02:00
Adrien Vergé
0fceca2354 yamllint version 1.24.1 2020-07-15 14:49:51 +02:00
Adrien Vergé
9403f1f3ec Revert "Add global "locale" config option"
This reverts commit 9e90c77, because it caused a bug that affected
different people just after being released:
https://github.com/adrienverge/yamllint/issues/285
https://github.com/adrienverge/yamllint/issues/286
2020-07-15 14:48:48 +02:00
Adrien Vergé
0016390e78 yamllint version 1.24.0 2020-07-15 11:50:36 +02:00
Wolfgang Walther
9e90c777cb Add global "locale" config option and make key-ordering rule locale-aware
Support sorting by locale with strcoll(). Properly handle case and accents.
2020-07-15 11:46:05 +02:00
Jonathan Sokolowski
a2218988ee config: Do no match directories that look like YAML files
Fixes #279
2020-07-10 09:27:34 +02:00
Adrien Vergé
954fdd5e8f style: Fix 'noqa' for flake8 3.8.0
There was a change in behavior of E402, see:
https://gitlab.com/pycqa/flake8/-/issues/638#note_345108633
2020-07-08 16:27:08 +02:00
Sorin Sbarnea
bbcad943b6 style: Ignore flake8 warnings W503 and W504
Avoid W503/W504 with current code as the current code not compliant
and they are contradictory.
2020-05-03 16:55:57 +02:00
Adrien Vergé
30c90dbf70 Add contribution instructions in CONTRIBUTING.rst
Closes https://github.com/adrienverge/yamllint/issues/263.
2020-05-03 16:51:22 +02:00
Brad Solomon
512fe17047 Fix bug with CRLF in new-lines and require-starting-space
Pound-signs followed by a lone CRLF should not
raise if require-starting-space is specified.

If require-starting-space is true, *and* either:
- new-lines: disbale, or
- newlines: type: dos
is specified, a line with `#\r` or `#\r\n` should
not raise a false positive.

This commit also uses a Set for O(1) membership testing
and uses the correct escape sequence for the nul byte.

If we find a CRLF when looking for Unix newlines, yamllint
should always raise, regardless of logic with
require-starting-space.

Closes: Issue #171.
2020-04-30 16:38:19 +02:00
Will Badart
278a79f093 Mention YAMLLINT_CONFIG_FILE in the documentation 2020-04-29 09:43:16 +02:00
Brad Solomon
e98aacf62c Add Python 3.8 to PyPI/trove classifier data
3.8 is now formally supported in .travis.yml
as of this commit.
2020-04-29 09:39:50 +02:00
Will Badart
94c0416f6b Specify config with environment variable YAMLLINT_CONFIG_FILE
Add option to specify config file with environment variable.
Add test case.
2020-04-28 11:13:32 +02:00
Adrien Vergé
a54cbce1b6 yamllint version 1.23.0 2020-04-17 10:31:52 +02:00
Adrien Vergé
b711fd993e quoted-strings: Add options extra-required and extra-allowed
Add ability to:
- require strings to be quoted if they match a pattern (PCRE regex)
- allow quoted strings if they match a pattern, while `require:
  only-when-needed` is enforced.

Co-Authored-By: Leo Feyer (https://github.com/adrienverge/yamllint/pull/246)
2020-04-17 10:29:55 +02:00
Adrien Vergé
d68022b846 config: Allow generic types inside lists
For example it's possible to define a conf like:

    rule:
      foo: [str],
      bar: [int, bool, 'magic'],
2020-04-17 10:29:55 +02:00
Adrien Vergé
851d34b9fd config: Allow rules to validate their configuration 2020-04-17 10:29:55 +02:00
Adrien Vergé
483a8d89a5 yamllint version 1.22.1 2020-04-15 07:55:57 +02:00
Adrien Vergé
fa87913566 quoted-strings: Fix only-when-needed on corner cases
Change implementation of `required: only-when-needed`, because
maintaining a list of `START_TOKENS` and just looking at the first
character of string values has proven to be partially broken.

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

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

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

But not for:

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

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

Fixes: #208.
2020-04-13 14:15:29 +02:00
Adrien Vergé
e284d74be1 quoted-strings: Rename tests names for clarity
And move only-when-needed tests at the end for readability.
2020-04-13 14:15:29 +02:00
Adrien Vergé
1a13837e84 docs: Sunset Python 2
Keep supporting Python 2.7 for one extra year after upstream dropped it:
https://www.python.org/doc/sunset-python-2/
2020-04-09 16:29:43 +02:00
Adrien Vergé
46ed0c02be truthy: Add missing test removed from PR
See https://github.com/adrienverge/yamllint/pull/247#discussion_r405421376.
2020-04-08 12:31:12 +02:00
ilyam8
6ce11dedb4 truthy: add check-keys option 2020-04-08 12:26:21 +02:00
Adrien Vergé
542ae758f5 yamllint version 1.21.0 2020-03-24 07:53:14 +01:00
Rui Pinge
3a6a09b7b6 Add support for redundant quotes in quoted-strings rule
Co-Authored-By: Adrien Vergé
2020-03-24 07:44:07 +01:00
Rui Pinge
15aea73fbe Fix quoted-strings rules not working for string values matching scalars 2020-03-14 14:22:29 +01:00
Martin Packman
91763f5476 Fix new-lines rule on Python 3
Use io.open() when reading files in cli which has the same behaviour
in Python 2 and Python 3, and supply the newline='' parameter which
handles but does not translate line endings.

Add dos.yml test file with windows newlines.

Also add to file finding test expected output.

Add test for new-lines rule through the cli.

Validates files are read with the correct universal newlines setting.

Fixes adrienverge/yamllint#228
2020-02-13 12:02:45 +01:00
Martin Packman
5b049e4229 Add RunContext helper for cli tests
Single context manager that includes exit code and output streams.

Use new RunContext throughout test_cli.

Largely non-functional change, saving some repetition of setup.

Also improve some failures by bundling multiple assertions into one.
2020-02-13 12:02:45 +01:00
Adrien Vergé
044c7f0248 cli: Test unicode chars in paths too 2020-01-17 16:01:05 +01:00
Adrien Vergé
734d5d5f73 CI: Run tests on Python 3.8
Python 3.8 was released in October 2019.
2020-01-03 09:33:35 +01:00
dhutty
fd86455076 CI: Disable building on Python 3.4
As can be seen in https://travis-ci.org/adrienverge/yamllint/builds/631325436?utm_source=github_status&utm_medium=notification
The dependency, pathspec, requires Python '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*' but the running Python is 3.4.8

This commit stops Travis building yamllint against 3.4 so that CI can pass again.
2020-01-03 09:29:33 +01:00
Adrien Vergé
13a0f11e7c yamllint version 1.20.0 2019-12-26 16:06:29 +01:00
Sylvestre Ledru
43b95e99d1 Use 'syntax' as rule name upon syntax errors 2019-12-17 19:29:49 +01:00
ffapitalle
8fa9eb3ced Add --no-warnings option to suppress warning messages
Use `--no-warnings` option to hide warning messages. It only shows
problems marked as errors.
2019-12-12 09:12:53 +01:00
Adrien Vergé
da3788e95a yamllint version 1.19.0 2019-11-19 11:28:21 +01:00
Joel Baranick
fb400dc64b Allow disabling all checks for a file
Allow disabling of a file, even if it is invalid YAML (syntax error) by
including `# yamllint disable-file` in the first line.
2019-11-19 11:26:31 +01:00
Adrien Vergé
92324ae730 yamllint version 1.18.0 2019-10-15 09:49:20 +02:00
Imran Iqbal
7359785ea0 fix(default.yaml): disable empty-values & octal-values by default
* Close #204
2019-10-15 09:41:32 +02:00
Hossein Zolfi
579a975b70 docs: Fix pre-commit config file
* pre-commit show warning for unsupported key (sha)
* Demonstrate how to use custom yamllint
2019-10-01 11:36:07 +02:00
Imran Iqbal
f3d9196aa0 docs(configuration): improve yaml-files code example
* A straight copy/paste of the existing example into the `.yamllint` file results in a `yamllint` error!
2019-09-10 19:47:25 +02:00
Ibrahim AshShohail
881d301883 feat: Support reading config from .yamllint.yml and .yamllint.yaml
Signed-off-by: Ibrahim AshShohail <me@ibrasho.com>
2019-08-27 09:49:09 +02:00
Adrien Vergé
b62b424dd4 feat: Lint .yamllint by default 2019-08-26 10:01:40 +02:00
49 changed files with 2050 additions and 398 deletions

54
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,54 @@
---
name: CI
on: # yamllint disable-line rule:truthy
push:
pull_request:
branches:
- master
jobs:
lint:
name: Linters
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- run: python -m pip install flake8 flake8-import-order doc8 sphinx
- run: python -m pip install .
- run: flake8 .
- run: doc8 $(git ls-files '*.rst')
- run: yamllint --strict $(git ls-files '*.yaml' '*.yml')
- run: python setup.py build_sphinx
test:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- 3.5
- 3.6
- 3.7
- 3.8
- 3.9
- nightly
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.pyver }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.pyver }}
- name: Fix GitHub Actions path
run: echo /home/runner/.local/bin >>$GITHUB_PATH
- run: pip install coveralls
- run: pip install .
- run: coverage run --source=yamllint -m unittest discover
- name: Coveralls
uses: AndreMiras/coveralls-python-action@develop

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ __pycache__
/dist /dist
/yamllint.egg-info /yamllint.egg-info
/build /build
/.eggs

View File

@@ -1,24 +0,0 @@
---
dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069)
language: python
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
- nightly
install:
- pip install pyyaml coveralls flake8 flake8-import-order doc8
- if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then pip install sphinx; fi
- pip install .
script:
- 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')
- coverage run --source=yamllint setup.py test
- if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then
python setup.py build_sphinx;
fi
after_success:
coveralls

View File

@@ -1,6 +1,112 @@
Changelog Changelog
========= =========
1.26.3 (2021-08-21)
-------------------
- Restore runtime dependency ``setuptools`` for Python < 3.8
1.26.2 (2021-08-03)
-------------------
- Fix ``python_requires`` to comply with PEP 345 and PEP 440
1.26.1 (2021-04-06)
-------------------
- Remove runtime dependency ``setuptools`` for Python < 3.8
- Fix ``line_length`` to skip all hash signs starting comment
1.26.0 (2021-01-29)
-------------------
- End support for Python 2 and Python 3.4, add support for Python 3.9
- Add ``forbid: non-empty`` option to ``braces`` and ``brackets`` rules
- Fix ``quoted-strings`` for explicit octal recognition
- Add documentation for integration with Arcanist
- Fix typos in changelog and README
- Stop using deprecated ``python setup.py test`` in tests
1.25.0 (2020-09-29)
-------------------
- Run tests on Travis both with and without UTF-8 locales
- Improve documentation with default values to rules with options
- Improve documentation with a Python API usage example
- Fix documentation on ``commas`` examples
- Packaging: move setuptools' configuration from ``setup.py`` to ``setup.cfg``
- Packaging: add extra info in PyPI metadata
- Improve documentation on ``yaml-files``
- Fix ``octal-values`` to prevent detection of ``8`` and ``9`` as octal values
- Fix ``quoted-strings`` Fix detecting strings with hashtag as requiring quotes
- Add ``forbid`` configuration to the ``braces`` and ``brackets`` rules
- Fix runtime dependencies missing ``setuptools``
- Add a new output format for GitHub Annotations (``--format github``)
- Fix DOS lines messing with rule IDs in directives
1.24.2 (2020-07-16)
-------------------
- Add ``locale`` config option and make ``key-ordering`` locale-aware
1.24.1 (2020-07-15)
-------------------
- Revert ``locale`` config option from version 1.24.0 because of a bug
1.24.0 (2020-07-15)
-------------------
- Specify config with environment variable ``YAMLLINT_CONFIG_FILE``
- Fix bug with CRLF in ``new-lines`` and ``require-starting-space``
- Do not run linter on directories whose names look like YAML files
- Add ``locale`` config option and make ``key-ordering`` locale-aware
1.23.0 (2020-04-17)
-------------------
- Allow rules to validate their configuration
- Add options ``extra-required`` and ``extra-allowed`` to ``quoted-strings``
1.22.1 (2020-04-15)
-------------------
- Fix ``quoted-strings`` rule with ``only-when-needed`` on corner cases
1.22.0 (2020-04-13)
-------------------
- Add ``check-keys`` option to the ``truthy`` rule
- Fix ``quoted-strings`` rule not working on sequences items
- Sunset Python 2
1.21.0 (2020-03-24)
-------------------
- Fix ``new-lines`` rule on Python 3 with DOS line endings
- Fix ``quoted-strings`` rule not working for string values matching scalars
- Add ``required: only-when-needed`` option to the ``quoted-strings`` rule
1.20.0 (2019-12-26)
-------------------
- Add --no-warnings option to suppress warning messages
- Use 'syntax' as rule name upon syntax errors
1.19.0 (2019-11-19)
-------------------
- Allow disabling all checks for a file with ``# yamllint disable-file``
1.18.0 (2019-10-15)
-------------------
- Lint ``.yamllint`` config file by default
- Also read config from ``.yamllint.yml`` and ``.yamllint.yaml``
- Improve documentation for ``yaml-files``
- Update documentation for ``pre-commit``
- Explicitly disable ``empty-values`` and ``octal-values`` rules
1.17.0 (2019-08-12) 1.17.0 (2019-08-12)
------------------- -------------------

46
CONTRIBUTING.rst Normal file
View File

@@ -0,0 +1,46 @@
Contributing
============
Pull requests are the best way to propose changes to the codebase.
Contributions are welcome, but they have to meet some criteria.
Pull Request Process
--------------------
1. Fork this Git repository and create your branch from ``master``.
2. Make sure the tests pass:
.. code:: bash
pip install --user .
python -m unittest discover # all tests...
python -m unittest tests/rules/test_commas.py # or just some tests (faster)
3. If you add code that should be tested, add tests.
4. Make sure the linters pass:
.. code:: bash
flake8 .
If you added/modified documentation:
.. code:: bash
doc8 $(git ls-files '*.rst')
If you touched YAML files:
.. code:: bash
yamllint --strict $(git ls-files '*.yaml' '*.yml')
5. If relevant, update documentation (either in ``docs`` directly or in rules
files themselves).
6. Write a `good commit message
<http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_.
7. Then, open a pull request.

View File

@@ -19,7 +19,7 @@ indentation, etc.
:target: https://yamllint.readthedocs.io/en/latest/?badge=latest :target: https://yamllint.readthedocs.io/en/latest/?badge=latest
:alt: Documentation status :alt: Documentation status
Written in Python (compatible with Python 2 & 3). Written in Python (compatible with Python 3 only).
Documentation Documentation
------------- -------------

View File

@@ -6,9 +6,9 @@ import sys
import os import os
from unittest.mock import MagicMock from unittest.mock import MagicMock
sys.path.insert(0, os.path.abspath('..')) # noqa sys.path.insert(0, os.path.abspath('..'))
from yamllint import __copyright__, APP_NAME, APP_VERSION from yamllint import __copyright__, APP_NAME, APP_VERSION # noqa
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------

View File

@@ -14,7 +14,9 @@ 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
- the file referenced by ``$YAMLLINT_CONFIG_FILE``, if set
- ``$XDG_CONFIG_HOME/yamllint/config`` - ``$XDG_CONFIG_HOME/yamllint/config``
- ``~/.config/yamllint/config`` - ``~/.config/yamllint/config``
@@ -115,17 +117,21 @@ return code will be:
* ``1`` if one or more errors occur * ``1`` if one or more errors occur
* ``2`` if no errors occur, but one or more warnings occur * ``2`` if no errors occur, but one or more warnings occur
If the script is invoked with the ``--no-warnings`` option, it won't output
warning level problems, only error level ones.
YAML files extensions YAML files extensions
--------------------- ---------------------
To configure what yamllint should consider as YAML files, set ``yaml-files`` To configure what yamllint should consider as YAML files when listing
configuration option. The default is: directories, set ``yaml-files`` configuration option. The default is:
.. code-block:: yaml .. code-block:: yaml
yaml-files: yaml-files:
- '*.yaml' - '*.yaml'
- '*.yml' - '*.yml'
- '.yamllint'
The same rules as for ignoring paths apply (``.gitignore``-style path pattern, The same rules as for ignoring paths apply (``.gitignore``-style path pattern,
see below). see below).
@@ -183,3 +189,22 @@ Here is a more complex example:
ignore: | ignore: |
*.ignore-trailing-spaces.yaml *.ignore-trailing-spaces.yaml
ascii-art/* ascii-art/*
Setting the locale
------------------
It is possible to set the ``locale`` option globally. This is passed to Python's
`locale.setlocale
<https://docs.python.org/3/library/locale.html#locale.setlocale>`_,
so an empty string ``""`` will use the system default locale, while e.g.
``"en_US.UTF-8"`` will use that.
Currently this only affects the ``key-ordering`` rule. The default will order
by Unicode code point number, while locales will sort case and accents
properly as well.
.. code-block:: yaml
extends: default
locale: en_US.UTF-8

View File

@@ -2,7 +2,17 @@ Development
=========== ===========
yamllint provides both a script and a Python module. The latter can be used to yamllint provides both a script and a Python module. The latter can be used to
write your own linting tools: write your own linting tools.
Basic example of running the linter from Python:
.. code-block:: python
import yamllint
yaml_config = yamllint.config.YamlLintConfig("extends: default")
for p in yamllint.linter.run("example.yaml", yaml_config):
print(p.desc, p.line, p.rule)
.. automodule:: yamllint.linter .. automodule:: yamllint.linter
:members: :members:

View File

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

View File

@@ -10,8 +10,63 @@ 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]
Integration with GitHub Actions
-------------------------------
yamllint auto-detects when it's running inside of `GitHub
Actions<https://github.com/features/actions>` and automatically uses the suited
output format to decorate code with linting errors automatically. You can also
force the GitHub Actions output with ``yamllint --format github``.
An example workflow using GitHub Actions:
.. code:: yaml
---
name: yamllint test
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install yamllint
run: pip install yamllint
- name: Lint YAML files
run: yamllint .
Integration with Arcanist
-------------------------
You can configure yamllint to run on ``arc lint``. Here is an example
``.arclint`` file that makes use of this configuration.
.. code:: json
{
"linters": {
"yamllint": {
"type": "script-and-regex",
"script-and-regex.script": "yamllint",
"script-and-regex.regex": "/^(?P<line>\\d+):(?P<offset>\\d+) +(?P<severity>warning|error) +(?P<message>.*) +\\((?P<name>.*)\\)$/m",
"include": "(\\.(yml|yaml)$)"
}
}
}

View File

@@ -4,9 +4,69 @@ universal = 1
[flake8] [flake8]
import-order-style = pep8 import-order-style = pep8
application-import-names = yamllint application-import-names = yamllint
ignore = W503,W504
[build_sphinx] [build_sphinx]
all-files = 1 all-files = 1
source-dir = docs source-dir = docs
build-dir = docs/_build build-dir = docs/_build
warning-is-error = 1 warning-is-error = 1
[metadata]
keywords =
yaml
lint
linter
syntax
checker
url = https://github.com/adrienverge/yamllint
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: Software Development
Topic :: Software Development :: Debuggers
Topic :: Software Development :: Quality Assurance
Topic :: Software Development :: Testing
project_urls =
Documentation = https://yamllint.readthedocs.io
Download = https://pypi.org/project/yamllint/#files
Bug Tracker = https://github.com/adrienverge/yamllint/issues
Source Code = https://github.com/adrienverge/yamllint
[options]
packages = find:
python_requires = >=3.5
include_package_data = True
install_requires =
pathspec >= 0.5.3
pyyaml
setuptools
test_suite = tests
[options.packages.find]
exclude =
tests
tests.*
[options.package_data]
yamllint = conf/*.yaml
[options.entry_points]
console_scripts =
yamllint = yamllint.cli:run
[coverage:run]
relative_files = True

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 find_packages, setup from setuptools import setup
from yamllint import (__author__, __license__, from yamllint import (__author__, __license__,
APP_NAME, APP_VERSION, APP_DESCRIPTION) APP_NAME, APP_VERSION, APP_DESCRIPTION)
@@ -27,30 +27,4 @@ setup(
description=APP_DESCRIPTION.split('\n')[0], description=APP_DESCRIPTION.split('\n')[0],
long_description=APP_DESCRIPTION, long_description=APP_DESCRIPTION,
license=__license__, license=__license__,
keywords=['yaml', 'lint', 'linter', 'syntax', 'checker'],
url='https://github.com/adrienverge/yamllint',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'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 :: Debuggers',
'Topic :: Software Development :: Quality Assurance',
'Topic :: Software Development :: Testing',
],
packages=find_packages(exclude=['tests', 'tests.*']),
entry_points={'console_scripts': ['yamllint=yamllint.cli:run']},
package_data={'yamllint': ['conf/*.yaml']},
install_requires=['pathspec >=0.5.3', 'pyyaml'],
test_suite='tests',
) )

View File

@@ -57,7 +57,7 @@ def build_temp_workspace(files):
tempdir = tempfile.mkdtemp(prefix='yamllint-tests-') tempdir = tempfile.mkdtemp(prefix='yamllint-tests-')
for path, content in files.items(): for path, content in files.items():
path = os.path.join(tempdir, path) path = os.path.join(tempdir, path).encode('utf-8')
if not os.path.exists(os.path.dirname(path)): if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path)) os.makedirs(os.path.dirname(path))

View File

@@ -31,6 +31,60 @@ class ColonTestCase(RuleTestCase):
'dict6: { a: 1, b, c: 3 }\n' 'dict6: { a: 1, b, c: 3 }\n'
'dict7: { a: 1, b, c: 3 }\n', conf) 'dict7: { a: 1, b, c: 3 }\n', conf)
def test_forbid(self):
conf = ('braces:\n'
' forbid: false\n')
self.check('---\n'
'dict: {}\n', conf)
self.check('---\n'
'dict: {a}\n', conf)
self.check('---\n'
'dict: {a: 1}\n', conf)
self.check('---\n'
'dict: {\n'
' a: 1\n'
'}\n', conf)
conf = ('braces:\n'
' forbid: true\n')
self.check('---\n'
'dict:\n'
' a: 1\n', conf)
self.check('---\n'
'dict: {}\n', conf, problem=(2, 8))
self.check('---\n'
'dict: {a}\n', conf, problem=(2, 8))
self.check('---\n'
'dict: {a: 1}\n', conf, problem=(2, 8))
self.check('---\n'
'dict: {\n'
' a: 1\n'
'}\n', conf, problem=(2, 8))
conf = ('braces:\n'
' forbid: non-empty\n')
self.check('---\n'
'dict:\n'
' a: 1\n', conf)
self.check('---\n'
'dict: {}\n', conf)
self.check('---\n'
'dict: {\n'
'}\n', conf)
self.check('---\n'
'dict: {\n'
'# commented: value\n'
'# another: value2\n'
'}\n', conf)
self.check('---\n'
'dict: {a}\n', conf, problem=(2, 8))
self.check('---\n'
'dict: {a: 1}\n', conf, problem=(2, 8))
self.check('---\n'
'dict: {\n'
' a: 1\n'
'}\n', conf, problem=(2, 8))
def test_min_spaces(self): def test_min_spaces(self):
conf = ('braces:\n' conf = ('braces:\n'
' max-spaces-inside: -1\n' ' max-spaces-inside: -1\n'

View File

@@ -31,6 +31,58 @@ class ColonTestCase(RuleTestCase):
'array6: [ a, b, c ]\n' 'array6: [ a, b, c ]\n'
'array7: [ a, b, c ]\n', conf) 'array7: [ a, b, c ]\n', conf)
def test_forbid(self):
conf = ('brackets:\n'
' forbid: false\n')
self.check('---\n'
'array: []\n', conf)
self.check('---\n'
'array: [a, b]\n', conf)
self.check('---\n'
'array: [\n'
' a,\n'
' b\n'
']\n', conf)
conf = ('brackets:\n'
' forbid: true\n')
self.check('---\n'
'array:\n'
' - a\n'
' - b\n', conf)
self.check('---\n'
'array: []\n', conf, problem=(2, 9))
self.check('---\n'
'array: [a, b]\n', conf, problem=(2, 9))
self.check('---\n'
'array: [\n'
' a,\n'
' b\n'
']\n', conf, problem=(2, 9))
conf = ('brackets:\n'
' forbid: non-empty\n')
self.check('---\n'
'array:\n'
' - a\n'
' - b\n', conf)
self.check('---\n'
'array: []\n', conf)
self.check('---\n'
'array: [\n\n'
']\n', conf)
self.check('---\n'
'array: [\n'
'# a comment\n'
']\n', conf)
self.check('---\n'
'array: [a, b]\n', conf, problem=(2, 9))
self.check('---\n'
'array: [\n'
' a,\n'
' b\n'
']\n', conf, problem=(2, 9))
def test_min_spaces(self): def test_min_spaces(self):
conf = ('brackets:\n' conf = ('brackets:\n'
' max-spaces-inside: -1\n' ' max-spaces-inside: -1\n'

View File

@@ -186,6 +186,27 @@ class CommentsTestCase(RuleTestCase):
'inline: comment #\n' 'inline: comment #\n'
'foo: bar\n', conf) 'foo: bar\n', conf)
def test_empty_comment_crlf_dos_newlines(self):
conf = ('comments:\n'
' require-starting-space: true\n'
' min-spaces-from-content: 2\n'
'new-lines:\n'
' type: dos\n')
self.check('---\r\n'
'# This is paragraph 1.\r\n'
'#\r\n'
'# This is paragraph 2.\r\n', conf)
def test_empty_comment_crlf_disabled_newlines(self):
conf = ('comments:\n'
' require-starting-space: true\n'
' min-spaces-from-content: 2\n'
'new-lines: disable\n')
self.check('---\r\n'
'# This is paragraph 1.\r\n'
'#\r\n'
'# This is paragraph 2.\r\n', conf)
def test_first_line(self): def test_first_line(self):
conf = ('comments:\n' conf = ('comments:\n'
' require-starting-space: true\n' ' require-starting-space: true\n'

View File

@@ -14,6 +14,8 @@
# 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 locale
from tests.common import RuleTestCase from tests.common import RuleTestCase
@@ -103,10 +105,6 @@ class KeyOrderingTestCase(RuleTestCase):
'haïr: true\n' 'haïr: true\n'
'hais: true\n', conf, 'hais: true\n', conf,
problem=(3, 1)) problem=(3, 1))
self.check('---\n'
'haïr: true\n'
'hais: true\n', conf,
problem=(3, 1))
def test_key_tokens_in_flow_sequences(self): def test_key_tokens_in_flow_sequences(self):
conf = 'key-ordering: enable' conf = 'key-ordering: enable'
@@ -114,3 +112,39 @@ class KeyOrderingTestCase(RuleTestCase):
'[\n' '[\n'
' key: value, mappings, in, flow: sequence\n' ' key: value, mappings, in, flow: sequence\n'
']\n', conf) ']\n', conf)
def test_locale_case(self):
self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
try:
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
except locale.Error:
self.skipTest('locale en_US.UTF-8 not available')
conf = ('key-ordering: enable')
self.check('---\n'
't-shirt: 1\n'
'T-shirt: 2\n'
't-shirts: 3\n'
'T-shirts: 4\n', conf)
self.check('---\n'
't-shirt: 1\n'
't-shirts: 2\n'
'T-shirt: 3\n'
'T-shirts: 4\n', conf,
problem=(4, 1))
def test_locale_accents(self):
self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
try:
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
except locale.Error:
self.skipTest('locale en_US.UTF-8 not available')
conf = ('key-ordering: enable')
self.check('---\n'
'hair: true\n'
'haïr: true\n'
'hais: true\n'
'haïssable: true\n', conf)
self.check('---\n'
'hais: true\n'
'haïr: true\n', conf,
problem=(3, 1))

View File

@@ -14,9 +14,6 @@
# 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
@@ -119,6 +116,27 @@ class LineLengthTestCase(RuleTestCase):
'long_line: http://localhost/very/very/long/url\n' 'long_line: http://localhost/very/very/long/url\n'
'...\n', conf, problem=(2, 21)) '...\n', conf, problem=(2, 21))
conf = 'line-length: {max: 20, allow-non-breakable-words: true}'
self.check('---\n'
'# http://www.verylongurlurlurlurlurlurlurlurl.com\n'
'key:\n'
' subkey: value\n', conf)
self.check('---\n'
'## http://www.verylongurlurlurlurlurlurlurlurl.com\n'
'key:\n'
' subkey: value\n', conf)
self.check('---\n'
'# # http://www.verylongurlurlurlurlurlurlurlurl.com\n'
'key:\n'
' subkey: value\n', conf,
problem=(2, 21))
self.check('---\n'
'#A http://www.verylongurlurlurlurlurlurlurlurl.com\n'
'key:\n'
' subkey: value\n', conf,
problem1=(2, 2, 'comments'),
problem2=(2, 21, 'line-length'))
conf = ('line-length: {max: 20, allow-non-breakable-words: true}\n' conf = ('line-length: {max: 20, allow-non-breakable-words: true}\n'
'trailing-spaces: disable') 'trailing-spaces: disable')
self.check('---\n' self.check('---\n'
@@ -159,7 +177,6 @@ class LineLengthTestCase(RuleTestCase):
' {% 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): def test_unicode(self):
conf = 'line-length: {max: 53}' conf = 'line-length: {max: 53}'
self.check('---\n' self.check('---\n'

View File

@@ -40,6 +40,16 @@ class NewLinesTestCase(RuleTestCase):
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_unix_type_required_st_sp(self):
# If we find a CRLF when looking for Unix newlines, yamllint
# should always raise, regardless of logic with
# require-starting-space.
conf = ('new-line-at-end-of-file: disable\n'
'new-lines: {type: unix}\n'
'comments:\n'
' require-starting-space: true\n')
self.check('---\r\n#\r\n', conf, problem=(1, 4))
def test_dos_type(self): def test_dos_type(self):
conf = ('new-line-at-end-of-file: disable\n' conf = ('new-line-at-end-of-file: disable\n'
'new-lines: {type: dos}\n') 'new-lines: {type: dos}\n')

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))
@@ -48,9 +50,13 @@ class OctalValuesTestCase(RuleTestCase):
' - 0.10\n' ' - 0.10\n'
' - .01\n' ' - .01\n'
' - 0e3\n', conf) ' - 0e3\n', conf)
self.check('with-decimal-digits: 012345678', conf)
self.check('with-decimal-digits: 012345679', 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))
@@ -70,3 +76,5 @@ class OctalValuesTestCase(RuleTestCase):
' - .01\n' ' - .01\n'
' - 0e3\n', conf) ' - 0e3\n', conf)
self.check('user-city: "010"', conf) self.check('user-city: "010"', conf)
self.check('with-decimal-digits: 0o012345678', conf)
self.check('with-decimal-digits: 0o012345679', conf)

View File

@@ -16,12 +16,15 @@
from tests.common import RuleTestCase from tests.common import RuleTestCase
from yamllint import config
class QuotedTestCase(RuleTestCase): class QuotedTestCase(RuleTestCase):
rule_id = 'quoted-strings' rule_id = 'quoted-strings'
def test_disabled(self): def test_disabled(self):
conf = 'quoted-strings: disable' conf = 'quoted-strings: disable'
self.check('---\n' self.check('---\n'
'foo: bar\n', conf) 'foo: bar\n', conf)
self.check('---\n' self.check('---\n'
@@ -30,23 +33,34 @@ class QuotedTestCase(RuleTestCase):
'foo: \'bar\'\n', conf) 'foo: \'bar\'\n', conf)
self.check('---\n' self.check('---\n'
'bar: 123\n', conf) 'bar: 123\n', conf)
self.check('---\n'
'bar: "123"\n', conf)
def test_quote_type_any(self): def test_quote_type_any(self):
conf = 'quoted-strings: {quote-type: any}\n' conf = 'quoted-strings: {quote-type: any}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' # fails 'string1: foo\n' # fails
'string2: "foo"\n' 'string2: "foo"\n'
'string3: \'bar\'\n' 'string3: "true"\n'
'string4: !!str genericstring\n' 'string4: "123"\n'
'string5: !!str 456\n' 'string5: \'bar\'\n'
'string6: !!str "quotedgenericstring"\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem=(4, 10)) 'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(4, 10), problem2=(17, 5),
problem3=(19, 12), problem4=(20, 15))
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -55,7 +69,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 +78,31 @@ class QuotedTestCase(RuleTestCase):
def test_quote_type_single(self): def test_quote_type_single(self):
conf = 'quoted-strings: {quote-type: single}\n' conf = 'quoted-strings: {quote-type: single}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' # fails 'string1: foo\n' # fails
'string2: "foo"\n' # fails 'string2: "foo"\n' # fails
'string3: \'bar\'\n' 'string3: "true"\n' # fails
'string4: !!str genericstring\n' 'string4: "123"\n' # fails
'string5: !!str 456\n' 'string5: \'bar\'\n'
'string6: !!str "quotedgenericstring"\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(4, 10), problem2=(5, 10)) 'block-seq:\n'
' - foo\n' # fails
' - "foo"\n' # fails
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(4, 10), problem2=(5, 10), problem3=(6, 10),
problem4=(7, 10), problem5=(17, 5), problem6=(18, 5),
problem7=(19, 12), problem8=(19, 17), problem9=(20, 15),
problem10=(20, 23))
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -86,7 +111,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 +120,68 @@ class QuotedTestCase(RuleTestCase):
def test_quote_type_double(self): def test_quote_type_double(self):
conf = 'quoted-strings: {quote-type: double}\n' conf = 'quoted-strings: {quote-type: double}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' # fails 'string1: foo\n' # fails
'string2: "foo"\n' 'string2: "foo"\n'
'string3: \'bar\'\n' # fails 'string3: "true"\n'
'string4: !!str genericstring\n' 'string4: "123"\n'
'string5: !!str 456\n' 'string5: \'bar\'\n' # fails
'string6: !!str "quotedgenericstring"\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(4, 10), problem2=(6, 10)) 'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(4, 10), problem2=(8, 10), problem3=(17, 5),
problem4=(19, 12), problem5=(20, 15))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n' # fails
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n'
' word 2"\n',
conf, problem1=(9, 3))
def test_any_quotes_not_required(self):
conf = 'quoted-strings: {quote-type: any, required: false}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n'
'string3: "true"\n'
'string4: "123"\n'
'string5: \'bar\'\n'
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n'
'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf)
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@@ -122,4 +195,262 @@ class QuotedTestCase(RuleTestCase):
'multiline string 4:\n' 'multiline string 4:\n'
' "word 1\\\n' ' "word 1\\\n'
' word 2"\n', ' word 2"\n',
conf, problem1=(9, 3)) conf)
def test_single_quotes_not_required(self):
conf = 'quoted-strings: {quote-type: single, required: false}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n' # fails
'string3: "true"\n' # fails
'string4: "123"\n' # fails
'string5: \'bar\'\n'
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n'
'block-seq:\n'
' - foo\n' # fails
' - "foo"\n'
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10),
problem4=(18, 5), problem5=(19, 17), problem6=(20, 23))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
def test_only_when_needed(self):
conf = 'quoted-strings: {required: only-when-needed}\n'
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n' # fails
'string3: "true"\n'
'string4: "123"\n'
'string5: \'bar\'\n' # fails
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n'
'block-seq:\n'
' - foo\n'
' - "foo"\n' # fails
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(5, 10), problem2=(8, 10), problem3=(18, 5),
problem4=(19, 17), problem5=(20, 23))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
def test_only_when_needed_single_quotes(self):
conf = ('quoted-strings: {quote-type: single,\n'
' required: only-when-needed}\n')
self.check('---\n'
'boolean1: true\n'
'number1: 123\n'
'string1: foo\n'
'string2: "foo"\n' # fails
'string3: "true"\n' # fails
'string4: "123"\n' # fails
'string5: \'bar\'\n' # fails
'string6: !!str genericstring\n'
'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n'
'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n'
'block-seq:\n'
' - foo\n'
' - "foo"\n' # fails
'flow-seq: [foo, "foo"]\n' # fails
'flow-map: {a: foo, b: "foo"}\n', # fails
conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10),
problem4=(8, 10), problem5=(18, 5), problem6=(19, 17),
problem7=(20, 23))
self.check('---\n'
'multiline string 1: |\n'
' line 1\n'
' line 2\n'
'multiline string 2: >\n'
' word 1\n'
' word 2\n'
'multiline string 3:\n'
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
def test_only_when_needed_corner_cases(self):
conf = 'quoted-strings: {required: only-when-needed}\n'
self.check('---\n'
'- ""\n'
'- "- item"\n'
'- "key: value"\n'
'- "%H:%M:%S"\n'
'- "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
'- \'"quoted"\'\n'
'- "\'foo\' == \'bar\'"\n'
'- "\'Mac\' in ansible_facts.product_name"\n'
'- \'foo # bar\'\n',
conf)
self.check('---\n'
'k1: ""\n'
'k2: "- item"\n'
'k3: "key: value"\n'
'k4: "%H:%M:%S"\n'
'k5: "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
'k6: \'"quoted"\'\n'
'k7: "\'foo\' == \'bar\'"\n'
'k8: "\'Mac\' in ansible_facts.product_name"\n',
conf)
self.check('---\n'
'- ---\n'
'- "---"\n' # fails
'- ----------\n'
'- "----------"\n' # fails
'- :wq\n'
'- ":wq"\n', # fails
conf, problem1=(3, 3), problem2=(5, 3), problem3=(7, 3))
self.check('---\n'
'k1: ---\n'
'k2: "---"\n' # fails
'k3: ----------\n'
'k4: "----------"\n' # fails
'k5: :wq\n'
'k6: ":wq"\n', # fails
conf, problem1=(3, 5), problem2=(5, 5), problem3=(7, 5))
def test_only_when_needed_extras(self):
conf = ('quoted-strings:\n'
' required: true\n'
' extra-allowed: [^http://]\n')
self.assertRaises(config.YamlLintConfigError, self.check, '', conf)
conf = ('quoted-strings:\n'
' required: true\n'
' extra-required: [^http://]\n')
self.assertRaises(config.YamlLintConfigError, self.check, '', conf)
conf = ('quoted-strings:\n'
' required: false\n'
' extra-allowed: [^http://]\n')
self.assertRaises(config.YamlLintConfigError, self.check, '', conf)
conf = ('quoted-strings:\n'
' required: true\n')
self.check('---\n'
'- 123\n'
'- "123"\n'
'- localhost\n' # fails
'- "localhost"\n'
'- http://localhost\n' # fails
'- "http://localhost"\n'
'- ftp://localhost\n' # fails
'- "ftp://localhost"\n',
conf, problem1=(4, 3), problem2=(6, 3), problem3=(8, 3))
conf = ('quoted-strings:\n'
' required: only-when-needed\n'
' extra-allowed: [^ftp://]\n'
' extra-required: [^http://]\n')
self.check('---\n'
'- 123\n'
'- "123"\n'
'- localhost\n'
'- "localhost"\n' # fails
'- http://localhost\n' # fails
'- "http://localhost"\n'
'- ftp://localhost\n'
'- "ftp://localhost"\n',
conf, problem1=(5, 3), problem2=(6, 3))
conf = ('quoted-strings:\n'
' required: false\n'
' extra-required: [^http://, ^ftp://]\n')
self.check('---\n'
'- 123\n'
'- "123"\n'
'- localhost\n'
'- "localhost"\n'
'- http://localhost\n' # fails
'- "http://localhost"\n'
'- ftp://localhost\n' # fails
'- "ftp://localhost"\n',
conf, problem1=(6, 3), problem2=(8, 3))
conf = ('quoted-strings:\n'
' required: only-when-needed\n'
' extra-allowed: [^ftp://, ";$", " "]\n')
self.check('---\n'
'- localhost\n'
'- "localhost"\n' # fails
'- ftp://localhost\n'
'- "ftp://localhost"\n'
'- i=i+1\n'
'- "i=i+1"\n' # fails
'- i=i+2;\n'
'- "i=i+2;"\n'
'- foo\n'
'- "foo"\n' # fails
'- foo bar\n'
'- "foo bar"\n',
conf, problem1=(3, 3), problem2=(7, 3), problem3=(11, 3))
def test_octal_values(self):
conf = 'quoted-strings: {required: true}\n'
self.check('---\n'
'- 100\n'
'- 0100\n'
'- 0o100\n'
'- 777\n'
'- 0777\n'
'- 0o777\n'
'- 800\n'
'- 0800\n'
'- 0o800\n'
'- "0800"\n'
'- "0o800"\n',
conf,
problem1=(9, 3), problem2=(10, 3))

View File

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

View File

@@ -14,16 +14,14 @@
# 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/>.
try: from io import StringIO
from cStringIO import StringIO
except ImportError:
from io import StringIO
import fcntl import fcntl
import locale import locale
import os import os
import pty import pty
import shutil import shutil
import sys import sys
import tempfile
import unittest import unittest
from tests.common import build_temp_workspace from tests.common import build_temp_workspace
@@ -32,6 +30,39 @@ from yamllint import cli
from yamllint import config from yamllint import config
class RunContext(object):
"""Context manager for ``cli.run()`` to capture exit code and streams."""
def __init__(self, case):
self.stdout = self.stderr = None
self._raises_ctx = case.assertRaises(SystemExit)
def __enter__(self):
self._raises_ctx.__enter__()
sys.stdout = self.outstream = StringIO()
sys.stderr = self.errstream = StringIO()
return self
def __exit__(self, *exc_info):
self.stdout, sys.stdout = self.outstream.getvalue(), sys.__stdout__
self.stderr, sys.stderr = self.errstream.getvalue(), sys.__stderr__
return self._raises_ctx.__exit__(*exc_info)
@property
def returncode(self):
return self._raises_ctx.exception.code
# Check system's UTF-8 availability
def utf8_available():
try:
locale.setlocale(locale.LC_ALL, 'C.UTF-8')
locale.setlocale(locale.LC_ALL, (None, None))
return True
except locale.Error:
return False
class CommandLineTestCase(unittest.TestCase): class CommandLineTestCase(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -49,6 +80,9 @@ class CommandLineTestCase(unittest.TestCase):
# file in dir # file in dir
'sub/ok.yaml': '---\n' 'sub/ok.yaml': '---\n'
'key: value\n', 'key: value\n',
# directory that looks like a yaml file
'sub/directory.yaml/not-yaml.txt': '',
'sub/directory.yaml/empty.yml': '',
# file in very nested dir # file in very nested dir
's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml': '---\n' 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml': '---\n'
'key: value\n' 'key: value\n'
@@ -59,12 +93,22 @@ 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': ( u'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',
# different key-ordering by locale
'c.yaml': '---\n'
'A: true\n'
'a: true',
'en.yaml': '---\n'
'a: true\n'
'A: true'
}) })
@classmethod @classmethod
@@ -73,13 +117,19 @@ class CommandLineTestCase(unittest.TestCase):
shutil.rmtree(cls.wd) shutil.rmtree(cls.wd)
@unittest.skipIf(not utf8_available() and sys.version_info < (3, 7),
'UTF-8 paths not supported')
def test_find_files_recursively(self): def test_find_files_recursively(self):
conf = config.YamlLintConfig('extends: default') conf = config.YamlLintConfig('extends: default')
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'), [os.path.join(self.wd, 'a.yaml'),
os.path.join(self.wd, 'c.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, 'en.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, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
os.path.join(self.wd, 'sub/ok.yaml'), os.path.join(self.wd, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')], os.path.join(self.wd, 'warn.yaml')],
) )
@@ -104,6 +154,7 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively(items, conf)), 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/directory.yaml/empty.yml'),
os.path.join(self.wd, 'sub/ok.yaml')], os.path.join(self.wd, 'sub/ok.yaml')],
) )
@@ -113,6 +164,8 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'), [os.path.join(self.wd, 'a.yaml'),
os.path.join(self.wd, 'c.yaml'),
os.path.join(self.wd, 'en.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, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
os.path.join(self.wd, 'sub/ok.yaml'), os.path.join(self.wd, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')] os.path.join(self.wd, 'warn.yaml')]
@@ -123,7 +176,9 @@ class CommandLineTestCase(unittest.TestCase):
' - \'*.yml\'\n') ' - \'*.yml\'\n')
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'empty.yml')] [os.path.join(self.wd, 'dos.yml'),
os.path.join(self.wd, 'empty.yml'),
os.path.join(self.wd, 'sub/directory.yaml/empty.yml')]
) )
conf = config.YamlLintConfig('extends: default\n' conf = config.YamlLintConfig('extends: default\n'
@@ -140,10 +195,15 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'), [os.path.join(self.wd, 'a.yaml'),
os.path.join(self.wd, 'c.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, 'en.yaml'),
os.path.join(self.wd, 'no-yaml.json'), os.path.join(self.wd, 'no-yaml.json'),
os.path.join(self.wd, 'non-ascii/utf-8'), os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'), os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
os.path.join(self.wd, 'sub/ok.yaml'), os.path.join(self.wd, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')] os.path.join(self.wd, 'warn.yaml')]
) )
@@ -156,10 +216,15 @@ class CommandLineTestCase(unittest.TestCase):
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'a.yaml'), [os.path.join(self.wd, 'a.yaml'),
os.path.join(self.wd, 'c.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, 'en.yaml'),
os.path.join(self.wd, 'no-yaml.json'), os.path.join(self.wd, 'no-yaml.json'),
os.path.join(self.wd, 'non-ascii/utf-8'), os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8'),
os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'), os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'),
os.path.join(self.wd, 'sub/directory.yaml/empty.yml'),
os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'),
os.path.join(self.wd, 'sub/ok.yaml'), os.path.join(self.wd, 'sub/ok.yaml'),
os.path.join(self.wd, 'warn.yaml')] os.path.join(self.wd, 'warn.yaml')]
) )
@@ -170,270 +235,250 @@ class CommandLineTestCase(unittest.TestCase):
' - \'**/utf-8\'\n') ' - \'**/utf-8\'\n')
self.assertEqual( self.assertEqual(
sorted(cli.find_files_recursively([self.wd], conf)), sorted(cli.find_files_recursively([self.wd], conf)),
[os.path.join(self.wd, 'non-ascii/utf-8')] [os.path.join(self.wd, 'non-ascii/éçäγλνπ¥/utf-8')]
) )
def test_run_with_bad_arguments(self): def test_run_with_bad_arguments(self):
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(()) cli.run(())
self.assertNotEqual(ctx.returncode, 0)
self.assertEqual(ctx.stdout, '')
self.assertRegex(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.assertRegex(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, '')
self.assertRegex(
out, err = sys.stdout.getvalue(), sys.stderr.getvalue() ctx.stderr.splitlines()[-1],
self.assertEqual(out, '')
self.assertRegexpMatches(
err.splitlines()[-1],
r'^yamllint: error: argument -d\/--config-data: ' r'^yamllint: error: argument -d\/--config-data: '
r'not allowed with argument -c\/--config-file$' r'not allowed with argument -c\/--config-file$'
) )
# checks if reading from stdin and files are mutually exclusive # checks if reading from stdin and files are mutually exclusive
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(('-', 'file')) cli.run(('-', 'file'))
self.assertNotEqual(ctx.returncode, 0)
self.assertNotEqual(ctx.exception.code, 0) self.assertEqual(ctx.stdout, '')
self.assertRegex(ctx.stderr, r'^usage')
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertRegexpMatches(err, r'^usage')
def test_run_with_bad_config(self): def test_run_with_bad_config(self):
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(('-d', 'rules: {a: b}', 'file')) cli.run(('-d', 'rules: {a: b}', 'file'))
self.assertEqual(ctx.returncode, -1)
self.assertEqual(ctx.exception.code, -1) self.assertEqual(ctx.stdout, '')
self.assertRegex(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.assertRegex(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)
@unittest.skipIf(os.environ.get('GITHUB_RUN_ID'), '$HOME not overridable')
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_with_user_yamllint_config_file_in_env(self):
self.addCleanup(os.environ.__delitem__, 'YAMLLINT_CONFIG_FILE')
with tempfile.NamedTemporaryFile('w') as f:
os.environ['YAMLLINT_CONFIG_FILE'] = f.name
f.write('rules: {trailing-spaces: disable}')
f.flush()
with RunContext(self) as ctx:
cli.run((os.path.join(self.wd, 'a.yaml'), ))
self.assertEqual(ctx.returncode, 0)
with tempfile.NamedTemporaryFile('w') as f:
os.environ['YAMLLINT_CONFIG_FILE'] = f.name
f.write('rules: {trailing-spaces: enable}')
f.flush()
with RunContext(self) as ctx:
cli.run((os.path.join(self.wd, 'a.yaml'), ))
self.assertEqual(ctx.returncode, 1)
def test_run_with_locale(self):
# check for availability of locale, otherwise skip the test
# reset to default before running the test,
# as the first two runs don't use setlocale()
try:
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
except locale.Error:
self.skipTest('locale en_US.UTF-8 not available')
locale.setlocale(locale.LC_ALL, (None, None))
# C + en.yaml should fail
with RunContext(self) as ctx:
cli.run(('-d', 'rules: { key-ordering: enable }',
os.path.join(self.wd, 'en.yaml')))
self.assertEqual(ctx.returncode, 1)
# C + c.yaml should pass
with RunContext(self) as ctx:
cli.run(('-d', 'rules: { key-ordering: enable }',
os.path.join(self.wd, 'c.yaml')))
self.assertEqual(ctx.returncode, 0)
# the next two runs use setlocale() inside,
# so we need to clean up afterwards
self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
# en_US + en.yaml should pass
with RunContext(self) as ctx:
cli.run(('-d', 'locale: en_US.UTF-8\n'
'rules: { key-ordering: enable }',
os.path.join(self.wd, 'en.yaml')))
self.assertEqual(ctx.returncode, 0)
# en_US + c.yaml should fail
with RunContext(self) as ctx:
cli.run(('-d', 'locale: en_US.UTF-8\n'
'rules: { key-ordering: enable }',
os.path.join(self.wd, 'c.yaml')))
self.assertEqual(ctx.returncode, 1)
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.assertRegex(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.assertRegex(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, '')
@unittest.skipIf(not utf8_available(), 'C.UTF-8 not available')
def test_run_non_ascii_file(self): def test_run_non_ascii_file(self):
file = os.path.join(self.wd, 'non-ascii', 'utf-8') locale.setlocale(locale.LC_ALL, 'C.UTF-8')
self.addCleanup(locale.setlocale, locale.LC_ALL, (None, None))
# Make sure the default localization conditions on this "system" path = os.path.join(self.wd, 'non-ascii', 'éçäγλνπ¥', 'utf-8')
# support UTF-8 encoding. with RunContext(self) as ctx:
loc = locale.getlocale() cli.run(('-f', 'parsable', path))
try: self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
locale.setlocale(locale.LC_ALL, 'C.UTF-8')
except locale.Error:
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
sys.stdout, sys.stderr = StringIO(), StringIO()
with self.assertRaises(SystemExit) as ctx:
cli.run(('-f', 'parsable', file))
locale.setlocale(locale.LC_ALL, loc)
self.assertEqual(ctx.exception.code, 0)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, '')
self.assertEqual(err, '')
def test_run_multiple_files(self): def test_run_multiple_files(self):
items = [os.path.join(self.wd, 'empty.yml'), items = [os.path.join(self.wd, 'empty.yml'),
os.path.join(self.wd, 's')] os.path.join(self.wd, 's')]
file = items[1] + '/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml' path = items[1] + '/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx:
cli.run(['-f', 'parsable'] + items) cli.run(['-f', 'parsable'] + items)
self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
self.assertEqual(ctx.exception.code, 1) self.assertEqual(ctx.stdout, (
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'%s:3:1: [error] duplication of key "key" in mapping ' '%s:3:1: [error] duplication of key "key" in mapping '
'(key-duplicates)\n') % file) '(key-duplicates)\n') % path)
self.assertEqual(err, '')
def test_run_piped_output_nocolor(self): def test_run_piped_output_nocolor(self):
file = os.path.join(self.wd, 'a.yaml') path = os.path.join(self.wd, 'a.yaml')
sys.stdout, sys.stderr = StringIO(), StringIO() with RunContext(self) as ctx:
with self.assertRaises(SystemExit) as ctx: cli.run((path, ))
cli.run((file, )) self.assertEqual((ctx.returncode, ctx.stderr), (1, ''))
self.assertEqual(ctx.stdout, (
self.assertEqual(ctx.exception.code, 1)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'%s\n' '%s\n'
' 2:4 error trailing spaces (trailing-spaces)\n' ' 2:4 error trailing spaces (trailing-spaces)\n'
' 3:4 error no new line character at the end of file ' ' 3:4 error no new line character at the end of file '
'(new-line-at-end-of-file)\n' '(new-line-at-end-of-file)\n'
'\n' % file)) '\n' % path))
self.assertEqual(err, '')
def test_run_default_format_output_in_tty(self): def test_run_default_format_output_in_tty(self):
file = os.path.join(self.wd, 'a.yaml') path = os.path.join(self.wd, 'a.yaml')
# Create a pseudo-TTY and redirect stdout to it # Create a pseudo-TTY and redirect stdout to it
master, slave = pty.openpty() master, slave = pty.openpty()
sys.stdout = sys.stderr = os.fdopen(slave, 'w') sys.stdout = sys.stderr = os.fdopen(slave, 'w')
with self.assertRaises(SystemExit) as ctx: with self.assertRaises(SystemExit) as ctx:
cli.run((file, )) cli.run((path, ))
sys.stdout.flush() sys.stdout.flush()
self.assertEqual(ctx.exception.code, 1) self.assertEqual(ctx.exception.code, 1)
@@ -456,79 +501,140 @@ 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_format_github(self):
path = os.path.join(self.wd, 'a.yaml')
with RunContext(self) as ctx:
cli.run((path, '--format', 'github'))
expected_out = (
'::error file=%s,line=2,col=4::[trailing-spaces] trailing'
' spaces\n'
'::error file=%s,line=3,col=4::[new-line-at-end-of-file] no'
' new line character at the end of file\n'
% (path, path))
self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_github_actions_detection(self):
path = os.path.join(self.wd, 'a.yaml')
self.addCleanup(os.environ.__delitem__, 'GITHUB_ACTIONS')
self.addCleanup(os.environ.__delitem__, 'GITHUB_WORKFLOW')
with RunContext(self) as ctx:
os.environ['GITHUB_ACTIONS'] = 'something'
os.environ['GITHUB_WORKFLOW'] = 'something'
cli.run((path, ))
expected_out = (
'::error file=%s,line=2,col=4::[trailing-spaces] trailing'
' spaces\n'
'::error file=%s,line=3,col=4::[new-line-at-end-of-file] no'
' new line character at the end of file\n'
% (path, path))
self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_run_read_from_stdin(self): def test_run_read_from_stdin(self):
# prepares stdin with an invalid yaml string so that we can check # prepares stdin with an invalid yaml string so that we can check
# for its specific error, and be assured that stdin was read # for its specific error, and be assured that stdin was read
sys.stdout, sys.stderr = StringIO(), StringIO() self.addCleanup(setattr, sys, 'stdin', sys.__stdin__)
sys.stdin = StringIO( sys.stdin = StringIO(
'I am a string\n' 'I am a string\n'
'therefore: I am an error\n') 'therefore: I am an error\n')
with self.assertRaises(SystemExit) as ctx: with RunContext(self) as ctx:
cli.run(('-', '-f', 'parsable')) cli.run(('-', '-f', 'parsable'))
expected_out = (
self.assertNotEqual(ctx.exception.code, 0)
out, err = sys.stdout.getvalue(), sys.stderr.getvalue()
self.assertEqual(out, (
'stdin:2:10: [error] syntax error: ' 'stdin:2:10: [error] syntax error: '
'mapping values are not allowed here\n')) 'mapping values are not allowed here (syntax)\n')
self.assertEqual(err, '') self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
def test_run_no_warnings(self):
path = os.path.join(self.wd, 'a.yaml')
with RunContext(self) as ctx:
cli.run((path, '--no-warnings', '-f', 'auto'))
expected_out = (
'%s\n'
' 2:4 error trailing spaces (trailing-spaces)\n'
' 3:4 error no new line character at the end of file '
'(new-line-at-end-of-file)\n'
'\n' % path)
self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))
path = os.path.join(self.wd, 'warn.yaml')
with RunContext(self) as ctx:
cli.run((path, '--no-warnings', '-f', 'auto'))
self.assertEqual(ctx.returncode, 0)
def test_run_no_warnings_and_strict(self):
path = os.path.join(self.wd, 'warn.yaml')
with RunContext(self) as ctx:
cli.run((path, '--no-warnings', '-s'))
self.assertEqual(ctx.returncode, 2)
def test_run_non_universal_newline(self):
path = os.path.join(self.wd, 'dos.yml')
with RunContext(self) as ctx:
cli.run(('-d', 'rules:\n new-lines:\n type: dos', path))
self.assertEqual((ctx.returncode, ctx.stdout, ctx.stderr), (0, '', ''))
with RunContext(self) as ctx:
cli.run(('-d', 'rules:\n new-lines:\n type: unix', path))
expected_out = (
'%s\n'
' 1:4 error wrong new line character: expected \\n'
' (new-lines)\n'
'\n' % path)
self.assertEqual(
(ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, ''))

View File

@@ -14,10 +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/>.
try: from io import StringIO
from cStringIO import StringIO
except ImportError:
from io import StringIO
import os import os
import shutil import shutil
import sys import sys
@@ -48,7 +45,7 @@ class SimpleConfigTestCase(unittest.TestCase):
config.YamlLintConfig('not: valid: yaml') config.YamlLintConfig('not: valid: yaml')
def test_unknown_rule(self): def test_unknown_rule(self):
with self.assertRaisesRegexp( with self.assertRaisesRegex(
config.YamlLintConfigError, config.YamlLintConfigError,
'invalid config: no such rule: "this-one-does-not-exist"'): 'invalid config: no such rule: "this-one-does-not-exist"'):
config.YamlLintConfig('rules:\n' config.YamlLintConfig('rules:\n'
@@ -67,7 +64,7 @@ class SimpleConfigTestCase(unittest.TestCase):
self.assertEqual(c.rules['colons']['max-spaces-after'], 1) self.assertEqual(c.rules['colons']['max-spaces-after'], 1)
def test_unknown_option(self): def test_unknown_option(self):
with self.assertRaisesRegexp( with self.assertRaisesRegex(
config.YamlLintConfigError, config.YamlLintConfigError,
'invalid config: unknown option "abcdef" for rule "colons"'): 'invalid config: unknown option "abcdef" for rule "colons"'):
config.YamlLintConfig('rules:\n' config.YamlLintConfig('rules:\n'
@@ -105,7 +102,7 @@ class SimpleConfigTestCase(unittest.TestCase):
self.assertEqual(c.rules['indentation']['check-multi-line-strings'], self.assertEqual(c.rules['indentation']['check-multi-line-strings'],
False) False)
with self.assertRaisesRegexp( with self.assertRaisesRegex(
config.YamlLintConfigError, config.YamlLintConfigError,
'invalid config: option "indent-sequences" of "indentation" ' 'invalid config: option "indent-sequences" of "indentation" '
'should be in '): 'should be in '):
@@ -448,11 +445,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

@@ -47,16 +47,15 @@ class ModuleTestCase(unittest.TestCase):
subprocess.check_output([PYTHON, '-m', 'yamllint'], subprocess.check_output([PYTHON, '-m', 'yamllint'],
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
self.assertEqual(ctx.exception.returncode, 2) self.assertEqual(ctx.exception.returncode, 2)
self.assertRegexpMatches(ctx.exception.output.decode(), self.assertRegex(ctx.exception.output.decode(), r'^usage: yamllint')
r'^usage: yamllint')
def test_run_module_on_bad_dir(self): def test_run_module_on_bad_dir(self):
with self.assertRaises(subprocess.CalledProcessError) as ctx: with self.assertRaises(subprocess.CalledProcessError) as ctx:
subprocess.check_output([PYTHON, '-m', 'yamllint', subprocess.check_output([PYTHON, '-m', 'yamllint',
'/does/not/exist'], '/does/not/exist'],
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
self.assertRegexpMatches(ctx.exception.output.decode(), self.assertRegex(ctx.exception.output.decode(),
r'No such file or directory') r'No such file or directory')
def test_run_module_on_file(self): def test_run_module_on_file(self):
out = subprocess.check_output( out = subprocess.check_output(

View File

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

View File

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

View File

@@ -17,6 +17,8 @@
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import io
import locale
import os import os
import platform import platform
import sys import sys
@@ -83,14 +85,34 @@ class Format(object):
line += ' \033[2m(%s)\033[0m' % problem.rule line += ' \033[2m(%s)\033[0m' % problem.rule
return line return line
@staticmethod
def github(problem, filename):
line = '::'
line += problem.level
line += ' file=' + filename + ','
line += 'line=' + format(problem.line) + ','
line += 'col=' + format(problem.column)
line += '::'
if problem.rule:
line += '[' + problem.rule + '] '
line += problem.desc
return line
def show_problems(problems, file, args_format):
def show_problems(problems, file, args_format, no_warn):
max_level = 0 max_level = 0
first = True first = True
for problem in problems: for problem in problems:
max_level = max(max_level, PROBLEM_LEVELS[problem.level])
if no_warn and (problem.level != 'error'):
continue
if args_format == 'parsable': if args_format == 'parsable':
print(Format.parsable(problem, file)) print(Format.parsable(problem, file))
elif args_format == 'github' or (args_format == 'auto' and
'GITHUB_ACTIONS' in os.environ and
'GITHUB_WORKFLOW' in os.environ):
print(Format.github(problem, file))
elif args_format == 'colored' or \ elif args_format == 'colored' or \
(args_format == 'auto' and supports_color()): (args_format == 'auto' and supports_color()):
if first: if first:
@@ -102,7 +124,6 @@ def show_problems(problems, file, args_format):
print(file) print(file)
first = False first = False
print(Format.standard(problem, file)) print(Format.standard(problem, file))
max_level = max(max_level, PROBLEM_LEVELS[problem.level])
if not first and args_format != 'parsable': if not first and args_format != 'parsable':
print('') print('')
@@ -127,19 +148,26 @@ def run(argv=None):
action='store', action='store',
help='custom configuration (as YAML source)') help='custom configuration (as YAML source)')
parser.add_argument('-f', '--format', parser.add_argument('-f', '--format',
choices=('parsable', 'standard', 'colored', 'auto'), choices=('parsable', 'standard', 'colored', 'github',
'auto'),
default='auto', help='format for parsing output') default='auto', help='format for parsing output')
parser.add_argument('-s', '--strict', parser.add_argument('-s', '--strict',
action='store_true', action='store_true',
help='return non-zero exit code on warnings ' help='return non-zero exit code on warnings '
'as well as errors') 'as well as errors')
parser.add_argument('--no-warnings',
action='store_true',
help='output only error level problems')
parser.add_argument('-v', '--version', action='version', parser.add_argument('-v', '--version', action='version',
version='{} {}'.format(APP_NAME, APP_VERSION)) version='{} {}'.format(APP_NAME, APP_VERSION))
args = parser.parse_args(argv) args = parser.parse_args(argv)
if 'YAMLLINT_CONFIG_FILE' in os.environ:
user_global_config = os.path.expanduser(
os.environ['YAMLLINT_CONFIG_FILE'])
# User-global config is supposed to be in ~/.config/yamllint/config # User-global config is supposed to be in ~/.config/yamllint/config
if 'XDG_CONFIG_HOME' in os.environ: elif 'XDG_CONFIG_HOME' in os.environ:
user_global_config = os.path.join( user_global_config = os.path.join(
os.environ['XDG_CONFIG_HOME'], 'yamllint', 'config') os.environ['XDG_CONFIG_HOME'], 'yamllint', 'config')
else: else:
@@ -154,6 +182,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:
@@ -162,17 +194,21 @@ def run(argv=None):
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
if conf.locale is not None:
locale.setlocale(locale.LC_ALL, conf.locale)
max_level = 0 max_level = 0
for file in find_files_recursively(args.files, conf): for file in find_files_recursively(args.files, conf):
filepath = file[2:] if file.startswith('./') else file filepath = file[2:] if file.startswith('./') else file
try: try:
with open(file) as f: with io.open(file, newline='') as f:
problems = linter.run(f, conf, filepath) problems = linter.run(f, conf, filepath)
except EnvironmentError as e: except EnvironmentError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
prob_level = show_problems(problems, file, args_format=args.format) prob_level = show_problems(problems, file, args_format=args.format,
no_warn=args.no_warnings)
max_level = max(max_level, prob_level) max_level = max(max_level, prob_level)
# read yaml from stdin # read yaml from stdin
@@ -182,7 +218,8 @@ def run(argv=None):
except EnvironmentError as e: except EnvironmentError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)
prob_level = show_problems(problems, 'stdin', args_format=args.format) prob_level = show_problems(problems, 'stdin', args_format=args.format,
no_warn=args.no_warnings)
max_level = max(max_level, prob_level) max_level = max(max_level, prob_level)
if max_level == PROBLEM_LEVELS['error']: if max_level == PROBLEM_LEVELS['error']:

View File

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

View File

@@ -33,7 +33,9 @@ class YamlLintConfig(object):
self.ignore = None self.ignore = None
self.yaml_files = pathspec.PathSpec.from_lines( self.yaml_files = pathspec.PathSpec.from_lines(
'gitwildmatch', ['*.yaml', '*.yml']) 'gitwildmatch', ['*.yaml', '*.yml', '.yamllint'])
self.locale = None
if file is not None: if file is not None:
with open(file) as f: with open(file) as f:
@@ -46,7 +48,7 @@ class YamlLintConfig(object):
return self.ignore and self.ignore.match_file(filepath) return self.ignore and self.ignore.match_file(filepath)
def is_yaml_file(self, filepath): def is_yaml_file(self, filepath):
return self.yaml_files.match_file(filepath) return self.yaml_files.match_file(os.path.basename(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()
@@ -111,6 +113,12 @@ class YamlLintConfig(object):
self.yaml_files = pathspec.PathSpec.from_lines('gitwildmatch', self.yaml_files = pathspec.PathSpec.from_lines('gitwildmatch',
conf['yaml-files']) conf['yaml-files'])
if 'locale' in conf:
if not isinstance(conf['locale'], str):
raise YamlLintConfigError(
'invalid config: locale should be a string')
self.locale = conf['locale']
def validate(self): def validate(self):
for id in self.rules: for id in self.rules:
try: try:
@@ -157,11 +165,12 @@ def validate_rule_conf(rule, conf):
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']} # Example: CONF = {option: ['flag1', 'flag2', int]}
# → {option: [flag1]} → {option: [flag1, flag2]} # → {option: [flag1]} → {option: [42, flag1, flag2]}
elif isinstance(options[optkey], list): elif isinstance(options[optkey], list):
if (type(conf[optkey]) is not list or if (type(conf[optkey]) is not list or
any(flag not in options[optkey] any(flag not in options[optkey] and
type(flag) not in options[optkey]
for flag in conf[optkey])): for flag in conf[optkey])):
raise YamlLintConfigError( raise YamlLintConfigError(
('invalid config: option "%s" of "%s" should only ' ('invalid config: option "%s" of "%s" should only '
@@ -177,6 +186,12 @@ def validate_rule_conf(rule, conf):
for optkey in options: for optkey in options:
if optkey not in conf: if optkey not in conf:
conf[optkey] = options_default[optkey] conf[optkey] = options_default[optkey]
if hasattr(rule, 'VALIDATE'):
res = rule.VALIDATE(conf)
if res:
raise YamlLintConfigError('invalid config: %s: %s' %
(rule.ID, res))
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

@@ -87,7 +87,8 @@ def get_cosmetic_problems(buffer, conf, filepath):
return # this certainly wasn't a yamllint directive comment return # this certainly wasn't a yamllint directive comment
if re.match(r'^# yamllint disable( rule:\S+)*\s*$', comment): if re.match(r'^# yamllint disable( rule:\S+)*\s*$', comment):
rules = [item[5:] for item in comment[18:].split(' ')][1:] items = comment[18:].rstrip().split(' ')
rules = [item[5:] for item in items][1:]
if len(rules) == 0: if len(rules) == 0:
self.rules = self.all_rules.copy() self.rules = self.all_rules.copy()
else: else:
@@ -96,7 +97,8 @@ def get_cosmetic_problems(buffer, conf, filepath):
self.rules.add(id) self.rules.add(id)
elif re.match(r'^# yamllint enable( rule:\S+)*\s*$', comment): elif re.match(r'^# yamllint enable( rule:\S+)*\s*$', comment):
rules = [item[5:] for item in comment[17:].split(' ')][1:] items = comment[17:].rstrip().split(' ')
rules = [item[5:] for item in items][1:]
if len(rules) == 0: if len(rules) == 0:
self.rules.clear() self.rules.clear()
else: else:
@@ -114,7 +116,8 @@ def get_cosmetic_problems(buffer, conf, filepath):
return # this certainly wasn't a yamllint directive comment return # this certainly wasn't a yamllint directive comment
if re.match(r'^# yamllint disable-line( rule:\S+)*\s*$', comment): if re.match(r'^# yamllint disable-line( rule:\S+)*\s*$', comment):
rules = [item[5:] for item in comment[23:].split(' ')][1:] items = comment[23:].rstrip().split(' ')
rules = [item[5:] for item in items][1:]
if len(rules) == 0: if len(rules) == 0:
self.rules = self.all_rules.copy() self.rules = self.all_rules.copy()
else: else:
@@ -180,7 +183,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 +192,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)
@@ -226,7 +233,7 @@ def run(input, conf, filepath=None):
if conf.is_file_ignored(filepath): if conf.is_file_ignored(filepath):
return () return ()
if isinstance(input, (type(b''), type(u''))): # compat with Python 2 & 3 if isinstance(input, (bytes, str)):
return _run(input, conf, filepath) return _run(input, conf, filepath)
elif hasattr(input, 'read'): # Python 2's file or Python 3's io.IOBase elif hasattr(input, 'read'): # Python 2's file or Python 3's io.IOBase
# We need to have everything in memory to parse correctly # We need to have everything in memory to parse correctly

View File

@@ -15,10 +15,15 @@
# 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 control the number of spaces inside braces (``{`` and ``}``). Use this rule to control the use of flow mappings or number of spaces inside
braces (``{`` and ``}``).
.. rubric:: Options .. rubric:: Options
* ``forbid`` is used to forbid the use of flow mappings which are denoted by
surrounding braces (``{`` and ``}``). Use ``true`` to forbid the use of flow
mappings completely. Use ``non-empty`` to forbid the use of all flow
mappings except for empty ones.
* ``min-spaces-inside`` defines the minimal number of spaces required inside * ``min-spaces-inside`` defines the minimal number of spaces required inside
braces. braces.
* ``max-spaces-inside`` defines the maximal number of spaces allowed inside * ``max-spaces-inside`` defines the maximal number of spaces allowed inside
@@ -28,8 +33,46 @@ Use this rule to control the number of spaces inside braces (``{`` and ``}``).
* ``max-spaces-inside-empty`` defines the maximal number of spaces allowed * ``max-spaces-inside-empty`` defines the maximal number of spaces allowed
inside empty braces. inside empty braces.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
braces:
forbid: false
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
.. rubric:: Examples .. rubric:: Examples
#. With ``braces: {forbid: true}``
the following code snippet would **PASS**:
::
object:
key1: 4
key2: 8
the following code snippet would **FAIL**:
::
object: { key1: 4, key2: 8 }
#. With ``braces: {forbid: non-empty}``
the following code snippet would **PASS**:
::
object: {}
the following code snippet would **FAIL**:
::
object: { key1: 4, key2: 8 }
#. With ``braces: {min-spaces-inside: 0, max-spaces-inside: 0}`` #. With ``braces: {min-spaces-inside: 0, max-spaces-inside: 0}``
the following code snippet would **PASS**: the following code snippet would **PASS**:
@@ -92,23 +135,39 @@ Use this rule to control the number of spaces inside braces (``{`` and ``}``).
import yaml import yaml
from yamllint.linter import LintProblem
from yamllint.rules.common import spaces_after, spaces_before from yamllint.rules.common import spaces_after, spaces_before
ID = 'braces' ID = 'braces'
TYPE = 'token' TYPE = 'token'
CONF = {'min-spaces-inside': int, CONF = {'forbid': (bool, 'non-empty'),
'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, DEFAULT = {'forbid': False,
'min-spaces-inside': 0,
'max-spaces-inside': 0, 'max-spaces-inside': 0,
'min-spaces-inside-empty': -1, 'min-spaces-inside-empty': -1,
'max-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):
if (isinstance(token, yaml.FlowMappingStartToken) and if (conf['forbid'] is True and
isinstance(token, yaml.FlowMappingStartToken)):
yield LintProblem(token.start_mark.line + 1,
token.end_mark.column + 1,
'forbidden flow mapping')
elif (conf['forbid'] == 'non-empty' and
isinstance(token, yaml.FlowMappingStartToken) and
not isinstance(next, yaml.FlowMappingEndToken)):
yield LintProblem(token.start_mark.line + 1,
token.end_mark.column + 1,
'forbidden flow mapping')
elif (isinstance(token, yaml.FlowMappingStartToken) and
isinstance(next, yaml.FlowMappingEndToken)): isinstance(next, yaml.FlowMappingEndToken)):
problem = spaces_after(token, prev, next, problem = spaces_after(token, prev, next,
min=(conf['min-spaces-inside-empty'] min=(conf['min-spaces-inside-empty']

View File

@@ -15,11 +15,15 @@
# 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 control the number of spaces inside brackets (``[`` and Use this rule to control the use of flow sequences or the number of spaces
``]``). inside brackets (``[`` and ``]``).
.. rubric:: Options .. rubric:: Options
* ``forbid`` is used to forbid the use of flow sequences which are denoted by
surrounding brackets (``[`` and ``]``). Use ``true`` to forbid the use of
flow sequences completely. Use ``non-empty`` to forbid the use of all flow
sequences except for empty ones.
* ``min-spaces-inside`` defines the minimal number of spaces required inside * ``min-spaces-inside`` defines the minimal number of spaces required inside
brackets. brackets.
* ``max-spaces-inside`` defines the maximal number of spaces allowed inside * ``max-spaces-inside`` defines the maximal number of spaces allowed inside
@@ -29,8 +33,47 @@ Use this rule to control the number of spaces inside brackets (``[`` and
* ``max-spaces-inside-empty`` defines the maximal number of spaces allowed * ``max-spaces-inside-empty`` defines the maximal number of spaces allowed
inside empty brackets. inside empty brackets.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
brackets:
forbid: false
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
.. rubric:: Examples .. rubric:: Examples
#. With ``brackets: {forbid: true}``
the following code snippet would **PASS**:
::
object:
- 1
- 2
- abc
the following code snippet would **FAIL**:
::
object: [ 1, 2, abc ]
#. With ``brackets: {forbid: non-empty}``
the following code snippet would **PASS**:
::
object: []
the following code snippet would **FAIL**:
::
object: [ 1, 2, abc ]
#. With ``brackets: {min-spaces-inside: 0, max-spaces-inside: 0}`` #. With ``brackets: {min-spaces-inside: 0, max-spaces-inside: 0}``
the following code snippet would **PASS**: the following code snippet would **PASS**:
@@ -93,23 +136,39 @@ Use this rule to control the number of spaces inside brackets (``[`` and
import yaml import yaml
from yamllint.linter import LintProblem
from yamllint.rules.common import spaces_after, spaces_before from yamllint.rules.common import spaces_after, spaces_before
ID = 'brackets' ID = 'brackets'
TYPE = 'token' TYPE = 'token'
CONF = {'min-spaces-inside': int, CONF = {'forbid': (bool, 'non-empty'),
'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, DEFAULT = {'forbid': False,
'min-spaces-inside': 0,
'max-spaces-inside': 0, 'max-spaces-inside': 0,
'min-spaces-inside-empty': -1, 'min-spaces-inside-empty': -1,
'max-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):
if (isinstance(token, yaml.FlowSequenceStartToken) and if (conf['forbid'] is True and
isinstance(token, yaml.FlowSequenceStartToken)):
yield LintProblem(token.start_mark.line + 1,
token.end_mark.column + 1,
'forbidden flow sequence')
elif (conf['forbid'] == 'non-empty' and
isinstance(token, yaml.FlowSequenceStartToken) and
not isinstance(next, yaml.FlowSequenceEndToken)):
yield LintProblem(token.start_mark.line + 1,
token.end_mark.column + 1,
'forbidden flow sequence')
elif (isinstance(token, yaml.FlowSequenceStartToken) and
isinstance(next, yaml.FlowSequenceEndToken)): isinstance(next, yaml.FlowSequenceEndToken)):
problem = spaces_after(token, prev, next, problem = spaces_after(token, prev, next,
min=(conf['min-spaces-inside-empty'] min=(conf['min-spaces-inside-empty']

View File

@@ -24,6 +24,15 @@ Use this rule to control the number of spaces before and after colons (``:``).
* ``max-spaces-after`` defines the maximal number of spaces allowed after * ``max-spaces-after`` defines the maximal number of spaces allowed after
colons (use ``-1`` to disable). colons (use ``-1`` to disable).
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
colons:
max-spaces-before: 0
max-spaces-after: 1
.. rubric:: Examples .. rubric:: Examples
#. With ``colons: {max-spaces-before: 0, max-spaces-after: 1}`` #. With ``colons: {max-spaces-before: 0, max-spaces-after: 1}``

View File

@@ -26,6 +26,16 @@ Use this rule to control the number of spaces before and after commas (``,``).
* ``max-spaces-after`` defines the maximal number of spaces allowed after * ``max-spaces-after`` defines the maximal number of spaces allowed after
commas (use ``-1`` to disable). commas (use ``-1`` to disable).
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
.. rubric:: Examples .. rubric:: Examples
#. With ``commas: {max-spaces-before: 0}`` #. With ``commas: {max-spaces-before: 0}``
@@ -66,7 +76,7 @@ Use this rule to control the number of spaces before and after commas (``,``).
:: ::
strange var: strange var:
[10, 20,30, {x: 1, y: 2}] [10, 20, 30, {x: 1, y: 2}]
the following code snippet would **FAIL**: the following code snippet would **FAIL**:
:: ::

View File

@@ -28,6 +28,16 @@ Use this rule to control the position and formatting of comments.
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.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
comments:
require-starting-space: true
ignore-shebangs: true
min-spaces-from-content: 2
.. rubric:: Examples .. rubric:: Examples
#. With ``comments: {require-starting-space: true}`` #. With ``comments: {require-starting-space: true}``
@@ -97,7 +107,9 @@ def check(conf, comment):
comment.column_no == 1 and comment.column_no == 1 and
re.match(r'^!\S', comment.buffer[text_start:])): re.match(r'^!\S', comment.buffer[text_start:])):
return return
elif comment.buffer[text_start] not in (' ', '\n', '\0'): # We can test for both \r and \r\n just by checking first char
# \r itself is a valid newline on some older OS.
elif comment.buffer[text_start] not in {' ', '\n', '\r', '\x00'}:
column = comment.column_no + text_start - comment.pointer column = comment.column_no + text_start - comment.pointer
yield LintProblem(comment.line_no, yield LintProblem(comment.line_no,
column, column,

View File

@@ -22,6 +22,14 @@ Use this rule to require or forbid the use of document end marker (``...``).
* Set ``present`` to ``true`` when the document end marker is required, or to * Set ``present`` to ``true`` when the document end marker is required, or to
``false`` when it is forbidden. ``false`` when it is forbidden.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
document-end:
present: true
.. rubric:: Examples .. rubric:: Examples
#. With ``document-end: {present: true}`` #. With ``document-end: {present: true}``

View File

@@ -22,6 +22,14 @@ Use this rule to require or forbid the use of document start marker (``---``).
* Set ``present`` to ``true`` when the document start marker is required, or to * Set ``present`` to ``true`` when the document start marker is required, or to
``false`` when it is forbidden. ``false`` when it is forbidden.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
document-start:
present: true
.. rubric:: Examples .. rubric:: Examples
#. With ``document-start: {present: true}`` #. With ``document-start: {present: true}``

View File

@@ -25,6 +25,16 @@ Use this rule to set a maximal number of allowed consecutive blank lines.
* ``max-end`` defines the maximal number of empty lines allowed at the end of * ``max-end`` defines the maximal number of empty lines allowed at the end of
the file. This option takes precedence over ``max``. the file. This option takes precedence over ``max``.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
empty-lines:
max: 2
max-start: 0
max-end: 0
.. rubric:: Examples .. rubric:: Examples
#. With ``empty-lines: {max: 1}`` #. With ``empty-lines: {max: 1}``

View File

@@ -23,6 +23,15 @@ Use this rule to prevent nodes with empty content, that implicitly result in
* Use ``forbid-in-block-mappings`` to prevent empty values in block mappings. * Use ``forbid-in-block-mappings`` to prevent empty values in block mappings.
* Use ``forbid-in-flow-mappings`` to prevent empty values in flow mappings. * Use ``forbid-in-flow-mappings`` to prevent empty values in flow mappings.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
empty-values:
forbid-in-block-mappings: true
forbid-in-flow-mappings: true
.. rubric:: Examples .. rubric:: Examples
#. With ``empty-values: {forbid-in-block-mappings: true}`` #. With ``empty-values: {forbid-in-block-mappings: true}``
@@ -75,8 +84,8 @@ ID = 'empty-values'
TYPE = 'token' TYPE = 'token'
CONF = {'forbid-in-block-mappings': bool, CONF = {'forbid-in-block-mappings': bool,
'forbid-in-flow-mappings': bool} 'forbid-in-flow-mappings': bool}
DEFAULT = {'forbid-in-block-mappings': False, DEFAULT = {'forbid-in-block-mappings': True,
'forbid-in-flow-mappings': False} 'forbid-in-flow-mappings': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):

View File

@@ -22,6 +22,14 @@ Use this rule to control the number of spaces after hyphens (``-``).
* ``max-spaces-after`` defines the maximal number of spaces allowed after * ``max-spaces-after`` defines the maximal number of spaces allowed after
hyphens. hyphens.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
hyphens:
max-spaces-after: 1
.. rubric:: Examples .. rubric:: Examples
#. With ``hyphens: {max-spaces-after: 1}`` #. With ``hyphens: {max-spaces-after: 1}``

View File

@@ -32,6 +32,16 @@ Use this rule to control the indentation.
* ``check-multi-line-strings`` defines whether to lint indentation in * ``check-multi-line-strings`` defines whether to lint indentation in
multi-line strings. Set to ``true`` to enable, ``false`` to disable. multi-line strings. Set to ``true`` to enable, ``false`` to disable.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
indentation:
spaces: consistent
indent-sequences: true
check-multi-line-strings: false
.. rubric:: Examples .. rubric:: Examples
#. With ``indentation: {spaces: 1}`` #. With ``indentation: {spaces: 1}``

View File

@@ -16,8 +16,10 @@
""" """
Use this rule to enforce alphabetical ordering of keys in mappings. The sorting Use this rule to enforce alphabetical ordering of keys in mappings. The sorting
order uses the Unicode code point number. As a result, the ordering is order uses the Unicode code point number as a default. As a result, the
case-sensitive and not accent-friendly (see examples below). ordering is case-sensitive and not accent-friendly (see examples below).
This can be changed by setting the global ``locale`` option. This allows to
sort case and accents properly.
.. rubric:: Examples .. rubric:: Examples
@@ -63,8 +65,24 @@ case-sensitive and not accent-friendly (see examples below).
- haïr: true - haïr: true
hais: true hais: true
#. With global option ``locale: "en_US.UTF-8"`` and rule ``key-ordering: {}``
as opposed to before, the following code snippet would now **PASS**:
::
- t-shirt: 1
T-shirt: 2
t-shirts: 3
T-shirts: 4
- hair: true
haïr: true
hais: true
haïssable: true
""" """
from locale import strcoll
import yaml import yaml
from yamllint.linter import LintProblem from yamllint.linter import LintProblem
@@ -101,7 +119,8 @@ def check(conf, token, prev, next, nextnext, context):
# This check is done because KeyTokens can be found inside flow # This check is done because KeyTokens can be found inside flow
# sequences... strange, but allowed. # sequences... strange, but allowed.
if len(context['stack']) > 0 and context['stack'][-1].type == MAP: if len(context['stack']) > 0 and context['stack'][-1].type == MAP:
if any(next.value < key for key in context['stack'][-1].keys): if any(strcoll(next.value, key) < 0
for key in context['stack'][-1].keys):
yield LintProblem( yield LintProblem(
next.start_mark.line + 1, next.start_mark.column + 1, next.start_mark.line + 1, next.start_mark.column + 1,
'wrong ordering of key "%s" in mapping' % next.value) 'wrong ordering of key "%s" in mapping' % next.value)

View File

@@ -17,10 +17,6 @@
""" """
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.
@@ -30,6 +26,16 @@ recommend running yamllint with Python 3.
* ``allow-non-breakable-inline-mappings`` implies ``allow-non-breakable-words`` * ``allow-non-breakable-inline-mappings`` implies ``allow-non-breakable-words``
and extends it to also allow non-breakable words in inline mappings. and extends it to also allow non-breakable words in inline mappings.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
line-length:
max: 80
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: false
.. rubric:: Examples .. rubric:: Examples
#. With ``line-length: {max: 70}`` #. With ``line-length: {max: 70}``
@@ -134,7 +140,11 @@ def check(conf, line):
start += 1 start += 1
if start != line.end: if start != line.end:
if line.buffer[start] in ('#', '-'): if line.buffer[start] == '#':
while line.buffer[start] == '#':
start += 1
start += 1
elif line.buffer[start] == '-':
start += 2 start += 2
if line.buffer.find(' ', start, line.end) == -1: if line.buffer.find(' ', start, line.end) == -1:

View File

@@ -21,6 +21,14 @@ Use this rule to force the type of new line characters.
* Set ``type`` to ``unix`` to use UNIX-typed new line characters (``\\n``), or * Set ``type`` to ``unix`` to use UNIX-typed new line characters (``\\n``), or
``dos`` to use DOS-typed new line characters (``\\r\\n``). ``dos`` to use DOS-typed new line characters (``\\r\\n``).
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
new-lines:
type: unix
""" """

View File

@@ -20,6 +20,20 @@ start with ``0`` are interpreted as octal, but this is not always wanted.
For instance ``010`` is the city code of Beijing, and should not be For instance ``010`` is the city code of Beijing, and should not be
converted to ``8``. converted to ``8``.
.. rubric:: Options
* Use ``forbid-implicit-octal`` to prevent numbers starting with ``0``.
* Use ``forbid-explicit-octal`` to prevent numbers starting with ``0o``.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
.. rubric:: Examples .. rubric:: Examples
#. With ``octal-values: {forbid-implicit-octal: true}`` #. With ``octal-values: {forbid-implicit-octal: true}``
@@ -57,6 +71,8 @@ converted to ``8``.
city-code: 0o10 city-code: 0o10
""" """
import re
import yaml import yaml
from yamllint.linter import LintProblem from yamllint.linter import LintProblem
@@ -66,8 +82,12 @@ ID = 'octal-values'
TYPE = 'token' TYPE = 'token'
CONF = {'forbid-implicit-octal': bool, CONF = {'forbid-implicit-octal': bool,
'forbid-explicit-octal': bool} 'forbid-explicit-octal': bool}
DEFAULT = {'forbid-implicit-octal': False, DEFAULT = {'forbid-implicit-octal': True,
'forbid-explicit-octal': False} 'forbid-explicit-octal': True}
def _is_octal_number(string):
return re.match(r'^[0-7]+$', string) is not None
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):
@@ -78,7 +98,8 @@ def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.tokens.ScalarToken): if isinstance(token, yaml.tokens.ScalarToken):
if not token.style: if not token.style:
val = token.value val = token.value
if val.isdigit() and len(val) > 1 and val[0] == '0': if (val.isdigit() and len(val) > 1 and val[0] == '0' and
_is_octal_number(val[1:])):
yield LintProblem( yield LintProblem(
token.start_mark.line + 1, token.end_mark.column + 1, token.start_mark.line + 1, token.end_mark.column + 1,
'forbidden implicit octal value "%s"' % 'forbidden implicit octal value "%s"' %
@@ -88,7 +109,8 @@ def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.tokens.ScalarToken): if isinstance(token, yaml.tokens.ScalarToken):
if not token.style: if not token.style:
val = token.value val = token.value
if len(val) > 2 and val[:2] == '0o' and val[2:].isdigit(): if (len(val) > 2 and val[:2] == '0o' and
_is_octal_number(val[2:])):
yield LintProblem( yield LintProblem(
token.start_mark.line + 1, token.end_mark.column + 1, token.start_mark.line + 1, token.end_mark.column + 1,
'forbidden explicit octal value "%s"' % 'forbidden explicit octal value "%s"' %

View File

@@ -15,15 +15,39 @@
# 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``).
* ``extra-required`` is a list of PCRE regexes to force string values to be
quoted, if they match any regex. This option can only be used with
``required: false`` and ``required: only-when-needed``.
* ``extra-allowed`` is a list of PCRE regexes to allow quoted string values,
even if ``required: only-when-needed`` is set.
**Note**: Multi-line strings (with ``|`` or ``>``) will not be checked. **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked.
.. rubric:: Default values (when enabled)
.. code-block:: yaml
rules:
quoted-strings:
quote-type: any
required: true
extra-required: []
extra-allowed: []
.. 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,42 +61,192 @@ 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'
#. With ``quoted-strings: {required: false, extra-required: [^http://,
^ftp://]}``
the following code snippet would **PASS**:
::
- localhost
- "localhost"
- "http://localhost"
- "ftp://localhost"
the following code snippet would **FAIL**:
::
- http://localhost
- ftp://localhost
#. With ``quoted-strings: {required: only-when-needed, extra-allowed:
[^http://, ^ftp://], extra-required: [QUOTED]}``
the following code snippet would **PASS**:
::
- localhost
- "http://localhost"
- "ftp://localhost"
- "this is a string that needs to be QUOTED"
the following code snippet would **FAIL**:
::
- "localhost"
- this is a string that needs to be QUOTED
""" """
import re
import yaml import yaml
from yamllint.linter import LintProblem from yamllint.linter import LintProblem
ID = 'quoted-strings' ID = 'quoted-strings'
TYPE = 'token' TYPE = 'token'
CONF = {'quote-type': ('any', 'single', 'double')} CONF = {'quote-type': ('any', 'single', 'double'),
DEFAULT = {'quote-type': 'any'} 'required': (True, False, 'only-when-needed'),
'extra-required': [str],
'extra-allowed': [str]}
DEFAULT = {'quote-type': 'any',
'required': True,
'extra-required': [],
'extra-allowed': []}
def VALIDATE(conf):
if conf['required'] is True and len(conf['extra-allowed']) > 0:
return 'cannot use both "required: true" and "extra-allowed"'
if conf['required'] is True and len(conf['extra-required']) > 0:
return 'cannot use both "required: true" and "extra-required"'
if conf['required'] is False and len(conf['extra-allowed']) > 0:
return 'cannot use both "required: false" and "extra-allowed"'
DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str'
# https://stackoverflow.com/a/36514274
yaml.resolver.Resolver.add_implicit_resolver(
'tag:yaml.org,2002:int',
re.compile(r'''^(?:[-+]?0b[0-1_]+
|[-+]?0o?[0-7_]+
|[-+]?0[0-7_]+
|[-+]?(?:0|[1-9][0-9_]*)
|[-+]?0x[0-9a-fA-F_]+
|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X),
list('-+0123456789'))
def _quote_match(quote_type, token_style):
return ((quote_type == 'any') or
(quote_type == 'single' and token_style == "'") or
(quote_type == 'double' and token_style == '"'))
def _quotes_are_needed(string):
loader = yaml.BaseLoader('key: ' + string)
# Remove the 5 first tokens corresponding to 'key: ' (StreamStartToken,
# BlockMappingStartToken, KeyToken, ScalarToken(value=key), ValueToken)
for _ in range(5):
loader.get_token()
try:
a, b = loader.get_token(), loader.get_token()
if (isinstance(a, yaml.ScalarToken) and a.style is None and
isinstance(b, yaml.BlockEndToken) and a.value == string):
return False
return True
except yaml.scanner.ScannerError:
return True
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):
if not (isinstance(token, yaml.tokens.ScalarToken) and
isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken,
yaml.FlowSequenceStartToken, yaml.TagToken,
yaml.ValueToken))):
return
# Ignore explicit types, e.g. !!str testtest or !!int 42
if (prev and isinstance(prev, yaml.tokens.TagToken) and
prev.value[0] == '!!'):
return
# Ignore numbers, booleans, etc.
resolver = yaml.resolver.Resolver()
tag = resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False))
if token.plain and tag != DEFAULT_SCALAR_TAG:
return
# Ignore multi-line strings
if (not token.plain) and (token.style == "|" or token.style == ">"):
return
quote_type = conf['quote-type'] quote_type = conf['quote-type']
if (isinstance(token, yaml.tokens.ScalarToken) and msg = None
isinstance(prev, (yaml.ValueToken, yaml.TagToken))): if conf['required'] is True:
# Ignore explicit types, e.g. !!str testtest or !!int 42
if (prev and isinstance(prev, yaml.tokens.TagToken) and
prev.value[0] == '!!'):
return
# Ignore numbers, booleans, etc. # Quotes are mandatory and need to match config
resolver = yaml.resolver.Resolver() if token.style is None or not _quote_match(quote_type, token.style):
if resolver.resolve(yaml.nodes.ScalarNode, token.value, msg = "string value is not quoted with %s quotes" % quote_type
(True, False)) != 'tag:yaml.org,2002:str':
return
# Ignore multi-line strings elif conf['required'] is False:
if (not token.plain) and (token.style == "|" or token.style == ">"):
return
if ((quote_type == 'single' and token.style != "'") or # Quotes are not mandatory but when used need to match config
(quote_type == 'double' and token.style != '"') or if token.style and not _quote_match(quote_type, token.style):
(quote_type == 'any' and token.style is None)): msg = "string value is not quoted with %s quotes" % quote_type
yield LintProblem(
token.start_mark.line + 1, elif not token.style:
token.start_mark.column + 1, is_extra_required = any(re.search(r, token.value)
"string value is not quoted with %s quotes" % (quote_type)) for r in conf['extra-required'])
if is_extra_required:
msg = "string value is not quoted"
elif conf['required'] == 'only-when-needed':
# Quotes are not strictly needed here
if (token.style and tag == DEFAULT_SCALAR_TAG and token.value and
not _quotes_are_needed(token.value)):
is_extra_required = any(re.search(r, token.value)
for r in conf['extra-required'])
is_extra_allowed = any(re.search(r, token.value)
for r in conf['extra-allowed'])
if not (is_extra_required or is_extra_allowed):
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
elif not token.style:
is_extra_required = len(conf['extra-required']) and any(
re.search(r, token.value) for r in conf['extra-required'])
if is_extra_required:
msg = "string value is not quoted"
if msg is not None:
yield LintProblem(
token.start_mark.line + 1,
token.start_mark.column + 1,
msg)

View File

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