Compare commits

..

28 Commits

Author SHA1 Message Date
Adrien Vergé
d8d1d92545 yamllint version 1.1.0 2016-03-04 17:03:38 +01:00
Adrien Vergé
7688567faa cli: Add the -d option to provide inline conf 2016-03-04 16:53:26 +01:00
Adrien Vergé
4e188f8801 Conf: Add a new pre-defined conf 'relaxed'
It is more tolerant than 'default'.
2016-03-04 16:50:40 +01:00
Adrien Vergé
5693b1dddf Rules: indentation: Add 'consistent' option for 'spaces'
Using `spaces: consistent` allows any number of spaces, as long as it
remains the same within the file.
2016-03-04 16:03:53 +01:00
Adrien Vergé
fa420499c7 Config: Allow types in multiple choices
For instance, allow rules with:

    CONF = {'choice': (int, 'hardcoded-string'),
            'string-or-bool': (str, bool)}
2016-03-04 16:03:46 +01:00
Adrien Vergé
adefe38a0d yamllint version 1.0.4 2016-03-04 12:48:31 +01:00
Adrien Vergé
7e11082353 Distribution: Restore spec examples in package_data
Put `tests/yaml-1.2-spec-examples/*` back in `setup.py`'s `package_data`
because they need to be installed when running `python setup.py build`,
so Debian packaging script `dh_auto_test -O--buildsystem=pybuild`
doesn't fail.

See also commit e6dc67f.
2016-03-04 12:33:56 +01:00
Adrien Vergé
29c1c60143 Tests: Use absolute path to spec examples 2016-03-04 12:15:26 +01:00
Adrien Vergé
b879e9a98f Distribution: Add LICENSE and README to manifest 2016-02-26 09:57:06 +01:00
Adrien Vergé
5956b20545 yamllint version 1.0.3 2016-02-25 14:48:13 +01:00
Adrien Vergé
10ad302e2f Tests: Explicit encoding for spec examples
YAML specification examples contain unusual characters, let's explicit
`encoding='utf-8'` to prevent bugs.
2016-02-25 10:44:05 +01:00
Adrien Vergé
73d9322813 linter: Test run on str, unicode, bytes and stream
Previously it was not tested, and broke on Python 2 `unicode` inputs.
2016-02-25 10:41:17 +01:00
Adrien Vergé
ca0ebe4583 yamllint version 1.0.2 2016-02-24 21:21:02 +01:00
Adrien Vergé
e6dc67fd0a Distribution: Add MANIFEST.in
`yamllint/conf/*.yml` remains in `setup.py`'s `package_data` because it
needs to be installed when running `pip install .`.

`docs/*` and `tests/yaml-1.2-spec-examples/*` just need to be packaged,
they can go in the manifest.
2016-02-24 21:18:48 +01:00
Adrien Vergé
611a560082 yamllint version 1.0.1 2016-02-19 19:39:52 +01:00
Adrien Vergé
83384fa4cf Doc: Fix man page redundant description 2016-02-19 19:34:20 +01:00
Adrien Vergé
3ab3784a75 cli: Remove shebang
A shebang is present at the beginning of file, it dates from the time
when `yamllint/cli.py` was `bin/yamllint`, i.e. an executable launcher.
Since this is not the case anymore (see `entry_points` section in
`setup.py`), let's remove it.
2016-02-19 19:17:49 +01:00
Adrien Vergé
2f75e92a66 Doc: Add a configuration example in README 2016-02-19 10:37:52 +01:00
Adrien Vergé
64caa95b6a yamllint version 1.0.0 2016-02-19 10:15:23 +01:00
Adrien Vergé
fff09fa2df Distribution: Ship example files from spec in sdist
Closes: #1
2016-02-19 10:14:59 +01:00
Adrien Vergé
316bee8c98 yamllint version 0.7.2 2016-02-05 11:28:15 +01:00
Adrien Vergé
6c8af97a40 Tests: unblacklist remaining spec examples
Since !!tags are now supported.
2016-02-05 11:14:37 +01:00
Adrien Vergé
647d84ff94 Rules: indentation: Handle tags 2016-02-05 11:13:44 +01:00
Adrien Vergé
8eb0d0ad74 Tests: unblacklist spec example 7.16
As is it supported -- it just lacks some indentation.
2016-02-05 09:52:09 +01:00
Adrien Vergé
4bc3d5a01c Rules: indentation: Handle anchors 2016-02-04 22:10:40 +01:00
Adrien Vergé
48c7d65c54 parser: Provide nextnext for token rules
Because the indentation rule sometimes needs to look two tokens forward
(in case of anchors for instance).
2016-02-04 22:10:40 +01:00
Adrien Vergé
62fa4cbe39 Tests: indentation: Test the indent stack
The "indentation stack" is iteratively built by the `check()` function
of the indentation rule. It is important, since everything in the rule
relies on it.

This patch adds tests to make sure the stack is correctly built for some
known structures.
2016-02-04 22:10:40 +01:00
Adrien Vergé
8d38d349ac Rules: indentation: Rewrite stack generation
"Indentation stack" generation was not done properly, hence did not work
in all cases. This commit does a cleaner rewriting.
2016-02-04 21:47:08 +01:00
26 changed files with 963 additions and 120 deletions

3
MANIFEST.in Normal file
View File

@@ -0,0 +1,3 @@
include LICENSE
include README.rst
include docs/*

View File

@@ -57,6 +57,9 @@ Usage
.. code:: bash .. code:: bash
# Use a pre-defined lint configuration
yamllint -d relaxed file.yml
# Use a custom lint configuration # Use a custom lint configuration
yamllint -c ~/myconfig file.yml yamllint -c ~/myconfig file.yml
@@ -64,3 +67,19 @@ Usage
# Output a parsable format (for syntax checking in editors like Vim, emacs...) # Output a parsable format (for syntax checking in editors like Vim, emacs...)
yamllint -f parsable file.yml yamllint -f parsable file.yml
Configuration example
^^^^^^^^^^^^^^^^^^^^^
.. code:: yaml
extends: default
rules:
# 80 chars should be enough, but don't fail if a line is longer
line-length:
max: 80
level: warning
# don't bother me with this rule
indentation: disable

View File

@@ -38,6 +38,5 @@ htmlhelp_basename = 'yamllintdoc'
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'yamllint', u'yamllint Documentation', ('index', 'yamllint', '', [u'Adrien Vergé'], 1)
[u'Adrien Vergé'], 1)
] ]

View File

@@ -8,7 +8,7 @@ settings can be gathered in a configuration file.
To use a custom configuration file, either name it ``.yamllint`` in your working To use a custom configuration file, either name it ``.yamllint`` in your working
directory, or use the ``-c`` option: directory, or use the ``-c`` option:
:: .. code:: bash
yamllint -c ~/myconfig file.yml yamllint -c ~/myconfig file.yml
@@ -22,6 +22,15 @@ Unless told otherwise, yamllint uses its ``default`` configuration:
Details on rules can be found on :doc:`the rules page <rules>`. Details on rules can be found on :doc:`the rules page <rules>`.
There is another pre-defined configuration named ``relaxed``. As its name
suggests, it is more tolerant.
It can be chosen using:
.. code:: bash
yamllint -d relaxed file.yml
Extending the default configuration Extending the default configuration
----------------------------------- -----------------------------------
@@ -50,7 +59,7 @@ strict on block sequences indentation:
extends: default extends: default
rules: rules:
# 80 should be enough, but don't fail if a line is longer # 80 chars should be enough, but don't fail if a line is longer
line-length: line-length:
max: 80 max: 80
level: warning level: warning
@@ -63,6 +72,21 @@ strict on block sequences indentation:
indentation: indentation:
indent-sequences: whatever indent-sequences: whatever
Custom configuration without a config file
------------------------------------------
It is possible -- although not recommended -- to pass custom configuration
options to yamllint with the ``-d`` (short for ``--config-data``) option.
Its content can either be the name of a pre-defined conf (example: ``default``
or ``relaxed``) or a serialized YAML object describing the configuration.
For instance:
.. code:: bash
yamllint -d "{extends: relaxed, rules: {line-length: {max: 120}}}" file.yml
Errors and warnings Errors and warnings
------------------- -------------------

View File

@@ -30,7 +30,7 @@ setup(
keywords=['yaml', 'lint', 'linter', 'syntax', 'checker'], keywords=['yaml', 'lint', 'linter', 'syntax', 'checker'],
url='https://github.com/adrienverge/yamllint', url='https://github.com/adrienverge/yamllint',
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 5 - Production/Stable',
'Environment :: Console', 'Environment :: Console',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
@@ -44,7 +44,8 @@ setup(
packages=find_packages(), packages=find_packages(),
entry_points={'console_scripts': ['yamllint=yamllint.cli:run']}, entry_points={'console_scripts': ['yamllint=yamllint.cli:run']},
package_data={'yamllint': ['conf/*.yml']}, package_data={'yamllint': ['conf/*.yml'],
'tests': ['yaml-1.2-spec-examples/*']},
install_requires=['pyyaml'], install_requires=['pyyaml'],
tests_require=['nose'], tests_require=['nose'],
test_suite='nose.collector', test_suite='nose.collector',

View File

@@ -15,6 +15,371 @@
# 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 tests.common import RuleTestCase from tests.common import RuleTestCase
from yamllint.parser import token_generator
from yamllint.rules.indentation import check
class IndentationStackTestCase(RuleTestCase):
# This test suite checks that the "indentation stack" built by the
# indentation rule is valid. It is important, since everything else in the
# rule relies on this stack.
maxDiff = None
def format_stack(self, stack):
"""Transform the stack at a given moment into a printable string like:
B_MAP:0 KEY:0 VAL:5
"""
return ' '.join(map(str, stack[1:]))
def full_stack(self, source):
conf = {'spaces': 2, 'indent-sequences': True,
'check-multi-line-strings': False}
context = {}
output = ''
for elem in token_generator(source):
list(check(conf, elem.curr, elem.prev, elem.next, elem.nextnext,
context))
token_type = (elem.curr.__class__.__name__
.replace('Token', '')
.replace('Block', 'B').replace('Flow', 'F')
.replace('Sequence', 'Seq')
.replace('Mapping', 'Map'))
if token_type in ('StreamStart', 'StreamEnd'):
continue
output += '%9s %s\n' % (token_type,
self.format_stack(context['stack']))
return output
def test_simple_mapping(self):
self.assertMultiLineEqual(
self.full_stack('key: val\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:5\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack(' key: val\n'),
'BMapStart B_MAP:5\n'
' Key B_MAP:5 KEY:5\n'
' Scalar B_MAP:5 KEY:5\n'
' Value B_MAP:5 KEY:5 VAL:10\n'
' Scalar B_MAP:5\n'
' BEnd \n')
def test_simple_sequence(self):
self.assertMultiLineEqual(
self.full_stack('- 1\n'
'- 2\n'
'- 3\n'),
'BSeqStart B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Scalar B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Scalar B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Scalar B_SEQ:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('key:\n'
' - 1\n'
' - 2\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
' BEnd B_MAP:0\n'
' BEnd \n')
def test_non_indented_sequences(self):
# There seems to be a bug in pyyaml: depending on the indentation, a
# sequence does not produce the same tokens. More precisely, the
# following YAML:
# usr:
# - lib
# produces a BlockSequenceStartToken and a BlockEndToken around the
# "lib" sequence, whereas the following:
# usr:
# - lib
# does not (both two tokens are omitted).
# So, yamllint must create fake 'B_SEQ'. This test makes sure it does.
self.assertMultiLineEqual(
self.full_stack('usr:\n'
' - lib\n'
'var: cache\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:2\n'
' BEnd B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:5\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('usr:\n'
'- lib\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
# missing BSeqStart here
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
' Scalar B_MAP:0\n'
# missing BEnd here
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('usr:\n'
'- lib\n'
'var: cache\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
# missing BSeqStart here
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
' Scalar B_MAP:0\n'
# missing BEnd here
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:5\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('usr:\n'
'- []\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
# missing BSeqStart here
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
'FSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 F_SEQ:3\n'
' FSeqEnd B_MAP:0\n'
# missing BEnd here
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('usr:\n'
'- k:\n'
' v\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
# missing BSeqStart here
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
'BMapStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2\n'
' Key B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
' Value B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2 VAL:4\n' # noqa
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2\n'
' BEnd B_MAP:0\n'
# missing BEnd here
' BEnd \n')
def test_flows(self):
self.assertMultiLineEqual(
self.full_stack('usr: [\n'
' {k:\n'
' v}\n'
' ]\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:5\n'
'FSeqStart B_MAP:0 KEY:0 VAL:5 F_SEQ:2\n'
'FMapStart B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3\n'
' Key B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3 KEY:3\n'
' Scalar B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3 KEY:3\n'
' Value B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3 KEY:3 VAL:5\n'
' Scalar B_MAP:0 KEY:0 VAL:5 F_SEQ:2 F_MAP:3\n'
' FMapEnd B_MAP:0 KEY:0 VAL:5 F_SEQ:2\n'
' FSeqEnd B_MAP:0\n'
' BEnd \n')
def test_anchors(self):
self.assertMultiLineEqual(
self.full_stack('key: &anchor value\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:5\n'
' Anchor B_MAP:0 KEY:0 VAL:5\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('key: &anchor\n'
' value\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
' Anchor B_MAP:0 KEY:0 VAL:2\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('- &anchor value\n'),
'BSeqStart B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Anchor B_SEQ:0 B_ENT:2\n'
' Scalar B_SEQ:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('- &anchor\n'
' value\n'),
'BSeqStart B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Anchor B_SEQ:0 B_ENT:2\n'
' Scalar B_SEQ:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('- &anchor\n'
' - 1\n'
' - 2\n'),
'BSeqStart B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Anchor B_SEQ:0 B_ENT:2\n'
'BSeqStart B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEntry B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEntry B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEnd B_SEQ:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('&anchor key:\n'
' value\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Anchor B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('pre:\n'
' &anchor1 0\n'
'&anchor2 key:\n'
' value\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
' Anchor B_MAP:0 KEY:0 VAL:2\n'
' Scalar B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Anchor B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('sequence: &anchor\n'
'- entry\n'
'- &anchor\n'
' - nested\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
' Anchor B_MAP:0 KEY:0 VAL:2\n'
# missing BSeqStart here
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0\n'
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
' Anchor B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEnd B_MAP:0\n'
# missing BEnd here
' BEnd \n')
def test_tags(self):
self.assertMultiLineEqual(
self.full_stack('key: !!tag value\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:5\n'
' Tag B_MAP:0 KEY:0 VAL:5\n'
' Scalar B_MAP:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('- !!map # Block collection\n'
' foo : bar\n'),
'BSeqStart B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Tag B_SEQ:0 B_ENT:2\n'
'BMapStart B_SEQ:0 B_ENT:2 B_MAP:2\n'
' Key B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
' Scalar B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n'
' Value B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2 VAL:8\n'
' Scalar B_SEQ:0 B_ENT:2 B_MAP:2\n'
' BEnd B_SEQ:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('- !!seq\n'
' - nested item\n'),
'BSeqStart B_SEQ:0\n'
' BEntry B_SEQ:0 B_ENT:2\n'
' Tag B_SEQ:0 B_ENT:2\n'
'BSeqStart B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEntry B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEnd B_SEQ:0\n'
' BEnd \n')
self.assertMultiLineEqual(
self.full_stack('sequence: !!seq\n'
'- entry\n'
'- !!seq\n'
' - nested\n'),
'BMapStart B_MAP:0\n'
' Key B_MAP:0 KEY:0\n'
' Scalar B_MAP:0 KEY:0\n'
' Value B_MAP:0 KEY:0 VAL:2\n'
' Tag B_MAP:0 KEY:0 VAL:2\n'
# missing BSeqStart here
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0\n'
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
' Tag B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2\n'
'BSeqStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEntry B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2 B_ENT:4\n'
' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_SEQ:2\n'
' BEnd B_MAP:0\n'
# missing BEnd here
' BEnd \n')
class IndentationTestCase(RuleTestCase): class IndentationTestCase(RuleTestCase):
@@ -131,6 +496,87 @@ class IndentationTestCase(RuleTestCase):
' date: 1991\n' ' date: 1991\n'
'...\n', conf) '...\n', conf)
def test_consistent(self):
conf = ('indentation: {spaces: consistent,\n'
' indent-sequences: whatever}\n'
'document-start: disable\n')
self.check('---\n'
'object:\n'
' k1:\n'
' - a\n'
' - b\n'
' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf)
self.check('---\n'
'object:\n'
' k1:\n'
' - a\n'
' - b\n'
' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf)
self.check('---\n'
'object:\n'
' k1:\n'
' - a\n'
' - b\n'
' k2: v2\n'
' k3:\n'
' - name: Unix\n'
' date: 1969\n'
' - name: Linux\n'
' date: 1991\n'
'...\n', conf)
self.check('first is not indented:\n'
' value is indented\n', conf)
self.check('first is not indented:\n'
' value:\n'
' is indented\n', conf)
self.check('- first is already indented:\n'
' value is indented too\n', conf)
self.check('- first is already indented:\n'
' value:\n'
' is indented too\n', conf)
self.check('- first is already indented:\n'
' value:\n'
' is indented too\n', conf, problem=(3, 14))
self.check('---\n'
'list one:\n'
' - 1\n'
' - 2\n'
' - 3\n'
'list two:\n'
' - a\n'
' - b\n'
' - c\n', conf, problem=(7, 5))
self.check('---\n'
'list one:\n'
'- 1\n'
'- 2\n'
'- 3\n'
'list two:\n'
' - a\n'
' - b\n'
' - c\n', conf)
self.check('---\n'
'list one:\n'
' - 1\n'
' - 2\n'
' - 3\n'
'list two:\n'
'- a\n'
'- b\n'
'- c\n', conf)
def test_indent_sequences_whatever(self): def test_indent_sequences_whatever(self):
conf = 'indentation: {spaces: 4, indent-sequences: whatever}' conf = 'indentation: {spaces: 4, indent-sequences: whatever}'
self.check('---\n' self.check('---\n'
@@ -670,6 +1116,95 @@ class IndentationTestCase(RuleTestCase):
' line\n' ' line\n'
'...\n', conf, problem=(4, 6)) '...\n', conf, problem=(4, 6))
def test_anchors(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'key: &anchor value\n', conf)
self.check('---\n'
'key: &anchor\n'
' value\n', conf)
self.check('---\n'
'- &anchor value\n', conf)
self.check('---\n'
'- &anchor\n'
' value\n', conf)
self.check('---\n'
'key: &anchor [1,\n'
' 2]\n', conf)
self.check('---\n'
'key: &anchor\n'
' [1,\n'
' 2]\n', conf)
self.check('---\n'
'key: &anchor\n'
' - 1\n'
' - 2\n', conf)
self.check('---\n'
'- &anchor [1,\n'
' 2]\n', conf)
self.check('---\n'
'- &anchor\n'
' [1,\n'
' 2]\n', conf)
self.check('---\n'
'- &anchor\n'
' - 1\n'
' - 2\n', conf)
self.check('---\n'
'key:\n'
' &anchor1\n'
' value\n', conf)
self.check('---\n'
'pre:\n'
' &anchor1 0\n'
'&anchor2 key:\n'
' value\n', conf)
self.check('---\n'
'machine0:\n'
' /etc/hosts: &ref-etc-hosts\n'
' content:\n'
' - 127.0.0.1: localhost\n'
' - ::1: localhost\n'
' mode: 0644\n'
'machine1:\n'
' /etc/hosts: *ref-etc-hosts\n', conf)
self.check('---\n'
'list:\n'
' - k: v\n'
' - &a truc\n'
' - &b\n'
' truc\n'
' - k: *a\n', conf)
def test_tags(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'-\n'
' "flow in block"\n'
'- >\n'
' Block scalar\n'
'- !!map # Block collection\n'
' foo: bar\n', conf)
conf = 'indentation: {spaces: 2, indent-sequences: no}'
self.check('---\n'
'sequence: !!seq\n'
'- entry\n'
'- !!seq\n'
' - nested\n', conf)
self.check('---\n'
'mapping: !!map\n'
' foo: bar\n'
'Block style: !!map\n'
' Clark: Evans\n'
' Ingy: döt Net\n'
' Oren: Ben-Kiki\n', conf)
self.check('---\n'
'Flow style: !!map {Clark: Evans, Ingy: döt Net}\n'
'Block style: !!seq\n'
'- Clark Evans\n'
'- Ingy döt Net\n', conf)
class ScalarIndentationTestCase(RuleTestCase): class ScalarIndentationTestCase(RuleTestCase):
rule_id = 'indentation' rule_id = 'indentation'
@@ -1074,3 +1609,39 @@ class ScalarIndentationTestCase(RuleTestCase):
' paragraph gap, \\n and\n' ' paragraph gap, \\n and\n'
' spaces.\n', conf, ' spaces.\n', conf,
problem1=(3, 6), problem2=(5, 7), problem3=(6, 8)) problem1=(3, 6), problem2=(5, 7), problem3=(6, 8))
def test_consistent(self):
conf = ('indentation: {spaces: consistent,\n'
' check-multi-line-strings: yes}\n'
'document-start: disable\n')
self.check('multi\n'
'line\n', conf)
self.check('multi\n'
' line\n', conf, problem=(2, 2))
self.check('- multi\n'
' line\n', conf)
self.check('- multi\n'
' line\n', conf, problem=(2, 4))
self.check('a key: multi\n'
' line\n', conf, problem=(2, 3))
self.check('a key: multi\n'
' line\n', conf, problem=(2, 9))
self.check('a key:\n'
' multi\n'
' line\n', conf, problem=(3, 4))
self.check('- C code: void main() {\n'
' printf("foo");\n'
' }\n', conf, problem=(2, 15))
self.check('- C code:\n'
' void main() {\n'
' printf("foo");\n'
' }\n', conf, problem=(3, 9))
self.check('>\n'
' multi\n'
' line\n', conf)
self.check('>\n'
' multi\n'
' line\n', conf)
self.check('>\n'
' multi\n'
' line\n', conf, problem=(3, 7))

View File

@@ -58,6 +58,53 @@ class SimpleConfigTestCase(unittest.TestCase):
' max-spaces-after: 1\n' ' max-spaces-after: 1\n'
' abcdef: yes\n') ' abcdef: yes\n')
def test_validate_rule_conf(self):
class Rule(object):
ID = 'fake'
self.assertEqual(config.validate_rule_conf(Rule, False), False)
self.assertEqual(config.validate_rule_conf(Rule, 'disable'), False)
config.validate_rule_conf(Rule, {'level': 'error'})
config.validate_rule_conf(Rule, {'level': 'warning'})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'level': 'warn'})
Rule.CONF = {'length': int}
config.validate_rule_conf(Rule, {'length': 8})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'height': 8})
Rule.CONF = {'a': bool, 'b': int}
config.validate_rule_conf(Rule, {'a': True, 'b': 0})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'a': True})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'b': 0})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'a': 1, 'b': 0})
Rule.CONF = {'choice': (True, 88, 'str')}
config.validate_rule_conf(Rule, {'choice': True})
config.validate_rule_conf(Rule, {'choice': 88})
config.validate_rule_conf(Rule, {'choice': 'str'})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': False})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': 99})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': 'abc'})
Rule.CONF = {'choice': (int, 'hardcoded')}
config.validate_rule_conf(Rule, {'choice': 42})
config.validate_rule_conf(Rule, {'choice': 'hardcoded'})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': False})
self.assertRaises(config.YamlLintConfigError,
config.validate_rule_conf, Rule, {'choice': 'abc'})
class ExtendedConfigTestCase(unittest.TestCase): class ExtendedConfigTestCase(unittest.TestCase):
def test_extend_add_rule(self): def test_extend_add_rule(self):

46
tests/test_linter.py Normal file
View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import unittest
from yamllint.config import YamlLintConfig
from yamllint import linter
class LinterTestCase(unittest.TestCase):
def fake_config(self):
return YamlLintConfig('extends: default')
def test_run_on_string(self):
linter.run('test: document', self.fake_config())
def test_run_on_bytes(self):
linter.run(b'test: document', self.fake_config())
def test_run_on_unicode(self):
linter.run(u'test: document', self.fake_config())
def test_run_on_stream(self):
linter.run(io.StringIO(u'hello'), self.fake_config())
def test_run_on_int(self):
self.assertRaises(TypeError, linter.run, 42, self.fake_config())
def test_run_on_list(self):
self.assertRaises(TypeError, linter.run,
['h', 'e', 'l', 'l', 'o'], self.fake_config())

View File

@@ -14,6 +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 io import open
import os import os
from tests.common import RuleTestCase from tests.common import RuleTestCase
@@ -96,6 +97,7 @@ conf_overrides = {
'example-7.15': ('braces: {min-spaces-inside: 0, max-spaces-inside: 1}\n' 'example-7.15': ('braces: {min-spaces-inside: 0, max-spaces-inside: 1}\n'
'commas: {max-spaces-before: 1, min-spaces-after: 0}\n' 'commas: {max-spaces-before: 1, min-spaces-after: 0}\n'
'colons: {max-spaces-before: 1}\n'), 'colons: {max-spaces-before: 1}\n'),
'example-7.16': ('indentation: disable\n'),
'example-7.17': ('indentation: disable\n'), 'example-7.17': ('indentation: disable\n'),
'example-7.18': ('indentation: disable\n'), 'example-7.18': ('indentation: disable\n'),
'example-7.19': ('indentation: disable\n'), 'example-7.19': ('indentation: disable\n'),
@@ -112,9 +114,15 @@ conf_overrides = {
'example-8.14': ('colons: {max-spaces-before: 1}\n'), 'example-8.14': ('colons: {max-spaces-before: 1}\n'),
'example-8.16': ('indentation: {spaces: 1}\n'), 'example-8.16': ('indentation: {spaces: 1}\n'),
'example-8.17': ('indentation: disable\n'), 'example-8.17': ('indentation: disable\n'),
'example-8.20': ('indentation: {indent-sequences: no}\n'
'colons: {max-spaces-before: 1}\n'),
'example-8.22': ('indentation: disable\n'),
'example-10.1': ('colons: {max-spaces-before: 2}\n'),
'example-10.2': ('indentation: {indent-sequences: no}\n'),
} }
files = os.listdir('tests/yaml-1.2-spec-examples') files = os.listdir(os.path.join(os.path.dirname(os.path.realpath(__file__)),
'yaml-1.2-spec-examples'))
assert len(files) == 132 assert len(files) == 132
@@ -123,15 +131,6 @@ def _gen_test(buffer, conf):
self.check(buffer, conf) self.check(buffer, conf)
return test return test
# TODO
# The following tests are blacklisted because they contain rarely-used formats
# that yamllint does not handle yet.
tmp_blacklist = (
'example-7.16',
'example-8.20',
'example-8.22',
'example-10.1',
)
# The following tests are blacklisted (i.e. will not be checked against # The following tests are blacklisted (i.e. will not be checked against
# yamllint), because pyyaml is currently not able to parse the contents # yamllint), because pyyaml is currently not able to parse the contents
# (using yaml.parse()). # (using yaml.parse()).
@@ -178,10 +177,10 @@ pyyaml_blacklist = (
) )
for file in files: for file in files:
if file in tmp_blacklist or file in pyyaml_blacklist: if file in pyyaml_blacklist:
continue continue
with open('tests/yaml-1.2-spec-examples/' + file) as f: with open('tests/yaml-1.2-spec-examples/' + file, encoding='utf-8') as f:
conf = conf_general + conf_overrides.get(file, '') conf = conf_general + conf_overrides.get(file, '')
setattr(SpecificationTestCase, 'test_' + file, setattr(SpecificationTestCase, 'test_' + file,
_gen_test(f.read(), conf)) _gen_test(f.read(), conf))

View File

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

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (C) 2016 Adrien Vergé # Copyright (C) 2016 Adrien Vergé
# #
@@ -67,8 +66,11 @@ def run(argv=None):
description=APP_DESCRIPTION) description=APP_DESCRIPTION)
parser.add_argument('files', metavar='FILE_OR_DIR', nargs='+', parser.add_argument('files', metavar='FILE_OR_DIR', nargs='+',
help='files to check') help='files to check')
parser.add_argument('-c', '--config', dest='config_file', action='store', parser.add_argument('-c', '--config-file', dest='config_file',
help='path to a custom configuration') action='store', help='path to a custom configuration')
parser.add_argument('-d', '--config-data', dest='config_data',
action='store',
help='custom configuration (as YAML source)')
parser.add_argument('-f', '--format', parser.add_argument('-f', '--format',
choices=('parsable', 'standard'), default='standard', choices=('parsable', 'standard'), default='standard',
help='format for parsing output') help='format for parsing output')
@@ -79,8 +81,17 @@ def run(argv=None):
args = parser.parse_args(argv) args = parser.parse_args(argv)
if args.config_file is not None and args.config_data is not None:
print('Options --config-file and --config-data cannot be used '
'simultaneously.', file=sys.stderr)
sys.exit(-1)
try: try:
if args.config_file is not None: if args.config_data is not None:
if ':' not in args.config_data:
args.config_data = 'extends: ' + args.config_data
conf = YamlLintConfig(content=args.config_data)
elif args.config_file is not 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')

29
yamllint/conf/relaxed.yml Normal file
View File

@@ -0,0 +1,29 @@
---
extends: default
rules:
braces:
level: warning
max-spaces-inside: 1
brackets:
level: warning
max-spaces-inside: 1
colons:
level: warning
commas:
level: warning
comments: disable
comments-indentation: disable
document-end: disable
document-start: disable
empty-lines:
level: warning
hyphens: disable
indentation:
spaces: consistent
indent-sequences: whatever
check-multi-line-strings: no
line-length:
level: warning
max: 100

View File

@@ -100,7 +100,8 @@ def validate_rule_conf(rule, conf):
'invalid config: unknown option "%s" for rule "%s"' % 'invalid config: unknown option "%s" for rule "%s"' %
(optkey, rule.ID)) (optkey, rule.ID))
if type(options[optkey]) == tuple: if type(options[optkey]) == tuple:
if conf[optkey] not in options[optkey]: if (conf[optkey] not in options[optkey] and
type(conf[optkey]) not in options[optkey]):
raise YamlLintConfigError( raise YamlLintConfigError(
'invalid config: option "%s" of "%s" should be in %s' 'invalid config: option "%s" of "%s" should be in %s'
% (optkey, rule.ID, options[optkey])) % (optkey, rule.ID, options[optkey]))

View File

@@ -68,6 +68,7 @@ def get_costemic_problems(buffer, conf):
rule_conf = conf.rules[rule.ID] rule_conf = conf.rules[rule.ID]
for problem in rule.check(rule_conf, for problem in rule.check(rule_conf,
elem.curr, elem.prev, elem.next, elem.curr, elem.prev, elem.next,
elem.nextnext,
context[rule.ID]): context[rule.ID]):
problem.rule = rule.ID problem.rule = rule.ID
problem.level = rule_conf['level'] problem.level = rule_conf['level']
@@ -127,7 +128,7 @@ def run(input, conf):
:param input: buffer, string or stream to read from :param input: buffer, string or stream to read from
:param conf: yamllint configuration object :param conf: yamllint configuration object
""" """
if type(input) == str: if type(input) in (type(b''), type(u'')): # compat with Python 2 & 3
return _run(input, conf) return _run(input, conf)
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

@@ -30,11 +30,12 @@ class Line(object):
class Token(object): class Token(object):
def __init__(self, line_no, curr, prev, next): def __init__(self, line_no, curr, prev, next, nextnext):
self.line_no = line_no self.line_no = line_no
self.curr = curr self.curr = curr
self.prev = prev self.prev = prev
self.next = next self.next = next
self.nextnext = nextnext
def line_generator(buffer): def line_generator(buffer):
@@ -55,14 +56,16 @@ def token_generator(buffer):
try: try:
prev = None prev = None
next = yaml_loader.peek_token() curr = yaml_loader.get_token()
while next is not None: while curr is not None:
curr = yaml_loader.get_token() next = yaml_loader.get_token()
next = yaml_loader.peek_token() nextnext = yaml_loader.peek_token()
yield Token(curr.start_mark.line + 1, curr, prev, next) yield Token(curr.start_mark.line + 1, curr, prev, next, nextnext)
prev = curr prev = curr
curr = next
except yaml.scanner.ScannerError: except yaml.scanner.ScannerError:
pass pass

View File

@@ -73,7 +73,7 @@ CONF = {'min-spaces-inside': int,
'max-spaces-inside': int} 'max-spaces-inside': int}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.FlowMappingStartToken): if isinstance(token, yaml.FlowMappingStartToken):
problem = spaces_after(token, prev, next, problem = spaces_after(token, prev, next,
min=conf['min-spaces-inside'], min=conf['min-spaces-inside'],

View File

@@ -74,7 +74,7 @@ CONF = {'min-spaces-inside': int,
'max-spaces-inside': int} 'max-spaces-inside': int}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.FlowSequenceStartToken): if isinstance(token, yaml.FlowSequenceStartToken):
problem = spaces_after(token, prev, next, problem = spaces_after(token, prev, next,
min=conf['min-spaces-inside'], min=conf['min-spaces-inside'],

View File

@@ -81,7 +81,7 @@ CONF = {'max-spaces-before': int,
'max-spaces-after': int} 'max-spaces-after': int}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.ValueToken): if isinstance(token, yaml.ValueToken):
problem = spaces_before(token, prev, next, problem = spaces_before(token, prev, next,
max=conf['max-spaces-before'], max=conf['max-spaces-before'],

View File

@@ -105,7 +105,7 @@ CONF = {'max-spaces-before': int,
'max-spaces-after': int} 'max-spaces-after': int}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.FlowEntryToken): if isinstance(token, yaml.FlowEntryToken):
if (prev is not None and conf['max-spaces-before'] != -1 and if (prev is not None and conf['max-spaces-before'] != -1 and
prev.end_mark.line < token.start_mark.line): prev.end_mark.line < token.start_mark.line):

View File

@@ -67,7 +67,7 @@ CONF = {'require-starting-space': bool,
'min-spaces-from-content': int} 'min-spaces-from-content': int}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
for comment in get_comments_between_tokens(token, next): for comment in get_comments_between_tokens(token, next):
if (conf['min-spaces-from-content'] != -1 and if (conf['min-spaces-from-content'] != -1 and
not isinstance(token, yaml.StreamStartToken) and not isinstance(token, yaml.StreamStartToken) and

View File

@@ -98,7 +98,7 @@ TYPE = 'token'
# # commented line 2 # # commented line 2
# current: line # current: line
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if prev is None: if prev is None:
return return

View File

@@ -84,7 +84,7 @@ TYPE = 'token'
CONF = {'present': bool} CONF = {'present': bool}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if conf['present']: if conf['present']:
if (isinstance(token, yaml.StreamEndToken) and if (isinstance(token, yaml.StreamEndToken) and
not (isinstance(prev, yaml.DocumentEndToken) or not (isinstance(prev, yaml.DocumentEndToken) or

View File

@@ -74,7 +74,7 @@ TYPE = 'token'
CONF = {'present': bool} CONF = {'present': bool}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if conf['present']: if conf['present']:
if (isinstance(prev, (yaml.StreamStartToken, if (isinstance(prev, (yaml.StreamStartToken,
yaml.DocumentEndToken, yaml.DocumentEndToken,

View File

@@ -78,7 +78,7 @@ TYPE = 'token'
CONF = {'max-spaces-after': int} CONF = {'max-spaces-after': int}
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.BlockEntryToken): if isinstance(token, yaml.BlockEntryToken):
problem = spaces_after(token, prev, next, problem = spaces_after(token, prev, next,
max=conf['max-spaces-after'], max=conf['max-spaces-after'],

View File

@@ -19,7 +19,10 @@ Use this rule to control the indentation.
.. rubric:: Options .. rubric:: Options
* ``spaces`` defines the number of spaces that represent an indentation level. * ``spaces`` defines the indentation width, in spaces. Set either to an integer
(e.g. ``2`` or ``4``, representing the number of spaces in an indentation
level) or to ``consistent`` to allow any number, as long as it remains the
same within the file.
* ``indent-sequences`` defines whether block sequences should be indented or * ``indent-sequences`` defines whether block sequences should be indented or
not (when in a mapping, this indentation is not mandatory -- some people not (when in a mapping, this indentation is not mandatory -- some people
perceive the ``-`` as part of the indentation). Possible values: ``yes``, perceive the ``-`` as part of the indentation). Possible values: ``yes``,
@@ -73,6 +76,28 @@ Use this rule to control the indentation.
- haystack: - haystack:
needle needle
#. With ``indentation: {spaces: consistent}``
the following code snippet would **PASS**:
::
history:
- name: Unix
date: 1969
- name: Linux
date: 1991
nest:
recurse:
- haystack:
needle
the following code snippet would **FAIL**:
::
some:
Russian:
dolls
#. With ``indentation: {spaces: 2, indent-sequences: no}`` #. With ``indentation: {spaces: 2, indent-sequences: no}``
the following code snippet would **PASS**: the following code snippet would **PASS**:
@@ -150,11 +175,12 @@ from yamllint.rules.common import is_explicit_key, get_real_end_line
ID = 'indentation' ID = 'indentation'
TYPE = 'token' TYPE = 'token'
CONF = {'spaces': int, CONF = {'spaces': (int, 'consistent'),
'indent-sequences': (True, False, 'whatever'), 'indent-sequences': (True, False, 'whatever'),
'check-multi-line-strings': bool} 'check-multi-line-strings': bool}
ROOT, MAP, B_SEQ, F_SEQ, B_ENT, KEY, VAL = range(7) ROOT, B_MAP, F_MAP, B_SEQ, F_SEQ, B_ENT, KEY, VAL = range(8)
labels = ('ROOT', 'B_MAP', 'F_MAP', 'B_SEQ', 'F_SEQ', 'B_ENT', 'KEY', 'VAL')
class Parent(object): class Parent(object):
@@ -163,51 +189,63 @@ class Parent(object):
self.indent = indent self.indent = indent
self.line_indent = line_indent self.line_indent = line_indent
self.explicit_key = False self.explicit_key = False
self.implicit_block_seq = False
def __repr__(self):
return '%s:%d' % (labels[self.type], self.indent)
def check_scalar_indentation(conf, token, context): def check_scalar_indentation(conf, token, context):
if token.start_mark.line == token.end_mark.line: if token.start_mark.line == token.end_mark.line:
return return
if token.plain: def compute_expected_indent(found_indent):
expected_indent = token.start_mark.column def detect_indent(base_indent):
elif token.style in ('"', "'"): if type(context['spaces']) is not int:
expected_indent = token.start_mark.column + 1 context['spaces'] = found_indent - base_indent
elif token.style in ('>', '|'): return base_indent + context['spaces']
if context['stack'][-1].type == B_SEQ:
# - > if token.plain:
# multi return token.start_mark.column
# line elif token.style in ('"', "'"):
expected_indent = token.start_mark.column + conf['spaces'] return token.start_mark.column + 1
elif context['stack'][-1].type == KEY: elif token.style in ('>', '|'):
assert context['stack'][-1].explicit_key if context['stack'][-1].type == B_SEQ:
# - ? > # - >
# multi-line # multi
# key # line
# : > return detect_indent(token.start_mark.column)
# multi-line elif context['stack'][-1].type == KEY:
# value assert context['stack'][-1].explicit_key
expected_indent = token.start_mark.column + conf['spaces'] # - ? >
elif context['stack'][-1].type == VAL: # multi-line
if token.start_mark.line + 1 > context['cur_line']: # key
# - key:
# >
# multi
# line
expected_indent = context['stack'][-1].indent + conf['spaces']
elif context['stack'][-2].explicit_key:
# - ? key
# : > # : >
# multi-line # multi-line
# value # value
expected_indent = token.start_mark.column + conf['spaces'] return detect_indent(token.start_mark.column)
elif context['stack'][-1].type == VAL:
if token.start_mark.line + 1 > context['cur_line']:
# - key:
# >
# multi
# line
return detect_indent(context['stack'][-1].indent)
elif context['stack'][-2].explicit_key:
# - ? key
# : >
# multi-line
# value
return detect_indent(token.start_mark.column)
else:
# - key: >
# multi
# line
return detect_indent(context['stack'][-2].indent)
else: else:
# - key: > return detect_indent(context['stack'][-1].indent)
# multi
# line expected_indent = None
expected_indent = context['stack'][-2].indent + conf['spaces']
else:
expected_indent = context['stack'][-1].indent + conf['spaces']
line_no = token.start_mark.line + 1 line_no = token.start_mark.line + 1
@@ -225,16 +263,20 @@ def check_scalar_indentation(conf, token, context):
if token.start_mark.buffer[line_start + indent] == '\n': if token.start_mark.buffer[line_start + indent] == '\n':
continue continue
if expected_indent is None:
expected_indent = compute_expected_indent(indent)
if indent != expected_indent: if indent != expected_indent:
yield LintProblem(line_no, indent + 1, yield LintProblem(line_no, indent + 1,
'wrong indentation: expected %d but found %d' % 'wrong indentation: expected %d but found %d' %
(expected_indent, indent)) (expected_indent, indent))
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if 'stack' not in context: if 'stack' not in context:
context['stack'] = [Parent(ROOT, 0)] context['stack'] = [Parent(ROOT, 0)]
context['cur_line'] = -1 context['cur_line'] = -1
context['spaces'] = conf['spaces']
# Step 1: Lint # Step 1: Lint
@@ -245,6 +287,12 @@ def check(conf, token, prev, next, context):
first_in_line = (is_visible and first_in_line = (is_visible and
token.start_mark.line + 1 > context['cur_line']) token.start_mark.line + 1 > context['cur_line'])
def detect_indent(next):
if type(context['spaces']) is not int:
context['spaces'] = (next.start_mark.column -
context['stack'][-1].indent)
return context['spaces']
if first_in_line: if first_in_line:
found_indentation = token.start_mark.column found_indentation = token.start_mark.column
expected = context['stack'][-1].indent expected = context['stack'][-1].indent
@@ -255,7 +303,7 @@ def check(conf, token, prev, next, context):
elif (context['stack'][-1].type == KEY and elif (context['stack'][-1].type == KEY and
context['stack'][-1].explicit_key and context['stack'][-1].explicit_key and
not isinstance(token, yaml.ValueToken)): not isinstance(token, yaml.ValueToken)):
expected += conf['spaces'] expected += detect_indent(token)
if found_indentation != expected: if found_indentation != expected:
yield LintProblem(token.start_mark.line + 1, found_indentation + 1, yield LintProblem(token.start_mark.line + 1, found_indentation + 1,
@@ -276,9 +324,6 @@ def check(conf, token, prev, next, context):
# Step 2.b: Update state # Step 2.b: Update state
if context['stack'][-1].type == B_ENT:
context['stack'].pop()
if isinstance(token, yaml.BlockMappingStartToken): if isinstance(token, yaml.BlockMappingStartToken):
assert isinstance(next, yaml.KeyToken) assert isinstance(next, yaml.KeyToken)
if next.start_mark.line == token.start_mark.line: if next.start_mark.line == token.start_mark.line:
@@ -292,9 +337,9 @@ def check(conf, token, prev, next, context):
# - ? # - ?
# a # a
# : 1 # : 1
indent = token.start_mark.column + conf['spaces'] indent = token.start_mark.column + detect_indent(next)
context['stack'].append(Parent(MAP, indent)) context['stack'].append(Parent(B_MAP, indent))
elif isinstance(token, yaml.FlowMappingStartToken): elif isinstance(token, yaml.FlowMappingStartToken):
if next.start_mark.line == token.start_mark.line: if next.start_mark.line == token.start_mark.line:
@@ -304,9 +349,9 @@ def check(conf, token, prev, next, context):
# - { # - {
# a: 1, b: 2 # a: 1, b: 2
# } # }
indent = context['cur_line_indent'] + conf['spaces'] indent = context['cur_line_indent'] + detect_indent(next)
context['stack'].append(Parent(MAP, indent, context['stack'].append(Parent(F_MAP, indent,
line_indent=context['cur_line_indent'])) line_indent=context['cur_line_indent']))
elif isinstance(token, yaml.BlockSequenceStartToken): elif isinstance(token, yaml.BlockSequenceStartToken):
@@ -322,6 +367,12 @@ def check(conf, token, prev, next, context):
elif (isinstance(token, yaml.BlockEntryToken) and elif (isinstance(token, yaml.BlockEntryToken) and
# in case of an empty entry # in case of an empty entry
not isinstance(next, (yaml.BlockEntryToken, yaml.BlockEndToken))): not isinstance(next, (yaml.BlockEntryToken, yaml.BlockEndToken))):
# It looks like pyyaml doesn't issue BlockSequenceStartTokens when the
# list is not indented. We need to compensate that.
if context['stack'][-1].type != B_SEQ:
context['stack'].append(Parent(B_SEQ, token.start_mark.column))
context['stack'][-1].implicit_block_seq = True
if next.start_mark.line == token.end_mark.line: if next.start_mark.line == token.end_mark.line:
# - item 1 # - item 1
# - item 2 # - item 2
@@ -332,7 +383,7 @@ def check(conf, token, prev, next, context):
# - # -
# key: # key:
# value # value
indent = token.start_mark.column + conf['spaces'] indent = token.start_mark.column + detect_indent(next)
context['stack'].append(Parent(B_ENT, indent)) context['stack'].append(Parent(B_ENT, indent))
@@ -344,17 +395,11 @@ def check(conf, token, prev, next, context):
# - [ # - [
# a, b # a, b
# ] # ]
indent = context['cur_line_indent'] + conf['spaces'] indent = context['cur_line_indent'] + detect_indent(next)
context['stack'].append(Parent(F_SEQ, indent, context['stack'].append(Parent(F_SEQ, indent,
line_indent=context['cur_line_indent'])) line_indent=context['cur_line_indent']))
elif isinstance(token, (yaml.BlockEndToken,
yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken)):
assert context['stack'][-1].type in (MAP, B_SEQ, F_SEQ)
context['stack'].pop()
elif isinstance(token, yaml.KeyToken): elif isinstance(token, yaml.KeyToken):
indent = context['stack'][-1].indent indent = context['stack'][-1].indent
@@ -362,21 +407,25 @@ def check(conf, token, prev, next, context):
context['stack'][-1].explicit_key = is_explicit_key(token) context['stack'][-1].explicit_key = is_explicit_key(token)
if context['stack'][-1].type == VAL:
context['stack'].pop()
assert context['stack'][-1].type == KEY
context['stack'].pop()
elif isinstance(token, yaml.ValueToken): elif isinstance(token, yaml.ValueToken):
assert context['stack'][-1].type == KEY assert context['stack'][-1].type == KEY
# Discard empty values # Special cases:
if isinstance(next, (yaml.BlockEndToken, # key: &anchor
yaml.FlowMappingEndToken, # value
yaml.FlowSequenceEndToken, # and:
yaml.KeyToken)): # key: !!tag
context['stack'].pop() # value
else: if isinstance(next, (yaml.AnchorToken, yaml.TagToken)):
if (next.start_mark.line == prev.start_mark.line and
next.start_mark.line < nextnext.start_mark.line):
next = nextnext
# Only if value is not empty
if not isinstance(next, (yaml.BlockEndToken,
yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken,
yaml.KeyToken)):
if context['stack'][-1].explicit_key: if context['stack'][-1].explicit_key:
# ? k # ? k
# : value # : value
@@ -384,7 +433,7 @@ def check(conf, token, prev, next, context):
# ? k # ? k
# : # :
# value # value
indent = context['stack'][-1].indent + conf['spaces'] indent = context['stack'][-1].indent + detect_indent(next)
elif next.start_mark.line == prev.start_mark.line: elif next.start_mark.line == prev.start_mark.line:
# k: value # k: value
indent = next.start_mark.column indent = next.start_mark.column
@@ -398,7 +447,7 @@ def check(conf, token, prev, next, context):
if conf['indent-sequences'] is False: if conf['indent-sequences'] is False:
indent = context['stack'][-1].indent indent = context['stack'][-1].indent
elif conf['indent-sequences'] is True: elif conf['indent-sequences'] is True:
indent = context['stack'][-1].indent + conf['spaces'] indent = context['stack'][-1].indent + detect_indent(next)
else: # 'whatever' else: # 'whatever'
if next.start_mark.column == context['stack'][-1].indent: if next.start_mark.column == context['stack'][-1].indent:
# key: # key:
@@ -409,19 +458,59 @@ def check(conf, token, prev, next, context):
# key: # key:
# - e1 # - e1
# - e2 # - e2
indent = context['stack'][-1].indent + conf['spaces'] indent = (context['stack'][-1].indent +
detect_indent(next))
else: else:
# k: # k:
# value # value
indent = context['stack'][-1].indent + conf['spaces'] indent = context['stack'][-1].indent + detect_indent(next)
context['stack'].append(Parent(VAL, indent)) context['stack'].append(Parent(VAL, indent))
if (context['stack'][-1].type == KEY and consumed_current_token = False
isinstance(next, (yaml.BlockEndToken, while True:
yaml.FlowMappingEndToken, if (context['stack'][-1].type == F_SEQ and
yaml.FlowSequenceEndToken, isinstance(token, yaml.FlowSequenceEndToken)):
yaml.KeyToken))): context['stack'].pop()
# A key without a value: it's part of a set. Let's drop this key
# and leave room for the next one. elif (context['stack'][-1].type == F_MAP and
context['stack'].pop() isinstance(token, yaml.FlowMappingEndToken)):
context['stack'].pop()
elif (context['stack'][-1].type in (B_MAP, B_SEQ) and
isinstance(token, yaml.BlockEndToken) and
not context['stack'][-1].implicit_block_seq and
not consumed_current_token):
context['stack'].pop()
consumed_current_token = True
elif (context['stack'][-1].type == B_ENT and
not isinstance(token, yaml.BlockEntryToken) and
context['stack'][-2].implicit_block_seq and
not isinstance(token, (yaml.AnchorToken, yaml.TagToken)) and
not isinstance(next, yaml.BlockEntryToken)):
context['stack'].pop()
context['stack'].pop()
elif (context['stack'][-1].type == B_ENT and
isinstance(next, (yaml.BlockEntryToken, yaml.BlockEndToken))):
context['stack'].pop()
elif (context['stack'][-1].type == VAL and
not isinstance(token, yaml.ValueToken) and
not isinstance(token, (yaml.AnchorToken, yaml.TagToken))):
assert context['stack'][-2].type == KEY
context['stack'].pop()
context['stack'].pop()
elif (context['stack'][-1].type == KEY and
isinstance(next, (yaml.BlockEndToken,
yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken,
yaml.KeyToken))):
# A key without a value: it's part of a set. Let's drop this key
# and leave room for the next one.
context['stack'].pop()
else:
break

View File

@@ -72,7 +72,7 @@ class Parent(object):
self.keys = [] self.keys = []
def check(conf, token, prev, next, context): def check(conf, token, prev, next, nextnext, context):
if 'stack' not in context: if 'stack' not in context:
context['stack'] = [] context['stack'] = []