Compare commits

..

12 Commits

Author SHA1 Message Date
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
Adrien Vergé
3f264806b9 yamllint version 0.7.1 2016-02-03 14:43:09 +01:00
Adrien Vergé
9a82b99d4b Rules: indentation: Fix multi-line flows
To detect this as correct indentations:

    top:
      rules: [
        {
          foo: 1
        },
        {
          foo: 2
          bar: [
            a, b, c
          ],
        },
      ]
2016-02-03 12:05:22 +01:00
Adrien Vergé
ba140ad42c Tests: Remove ghost character from YAML spec example 2016-02-01 23:27:49 +01:00
Adrien Vergé
0e04ee29e6 Doc: Update description 2016-02-01 23:03:25 +01:00
19 changed files with 753 additions and 111 deletions

View File

@@ -3,8 +3,9 @@ yamllint
A linter for YAML files. A linter for YAML files.
yamllint does not only check for syntax validity, but for common cosmetic yamllint does not only check for syntax validity, but for weirdnesses like key
conventions such as lines length, trailing spaces, indentation, etc. repetition and cosmetic problems such as lines length, trailing spaces,
indentation, etc.
.. image:: .. image::
https://travis-ci.org/adrienverge/yamllint.svg?branch=master https://travis-ci.org/adrienverge/yamllint.svg?branch=master

View File

@@ -1,10 +1,7 @@
yamllint documentation yamllint documentation
====================== ======================
A linter for YAML files. .. automodule:: yamllint
yamllint does not only check for syntax validity, but for common cosmetic
conventions such as lines length, trailing spaces, indentation, etc.
Screenshot Screenshot
---------- ----------

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):
@@ -169,7 +534,9 @@ class IndentationTestCase(RuleTestCase):
'- b\n' '- b\n'
'- c\n', conf, problem=(6, 1, 'syntax')) '- c\n', conf, problem=(6, 1, 'syntax'))
def test_flow_mappings(self): def test_direct_flows(self):
# flow: [ ...
# ]
conf = 'indentation: {spaces: 2}' conf = 'indentation: {spaces: 2}'
self.check('---\n' self.check('---\n'
'a: {x: 1,\n' 'a: {x: 1,\n'
@@ -183,10 +550,46 @@ class IndentationTestCase(RuleTestCase):
'a: {x: 1,\n' 'a: {x: 1,\n'
' y,\n' ' y,\n'
' z: 1}\n', conf, problem=(3, 6)) ' z: 1}\n', conf, problem=(3, 6))
self.check('---\n'
'a: {x: 1,\n'
' y, z: 1}\n', conf, problem=(3, 3))
self.check('---\n'
'a: {x: 1,\n'
' y, z: 1\n'
'}\n', conf)
self.check('---\n' self.check('---\n'
'a: {x: 1,\n' 'a: {x: 1,\n'
' y, z: 1\n' ' y, z: 1\n'
'}\n', conf, problem=(3, 3)) '}\n', conf, problem=(3, 3))
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf)
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf, problem=(3, 4))
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf, problem=(3, 6))
self.check('---\n'
'a: [x,\n'
' y, z]\n', conf, problem=(3, 3))
self.check('---\n'
'a: [x,\n'
' y, z\n'
']\n', conf)
self.check('---\n'
'a: [x,\n'
' y, z\n'
']\n', conf, problem=(3, 3))
def test_broken_flows(self):
# flow: [
# ...
# ]
conf = 'indentation: {spaces: 2}'
self.check('---\n' self.check('---\n'
'a: {\n' 'a: {\n'
' x: 1,\n' ' x: 1,\n'
@@ -206,25 +609,6 @@ class IndentationTestCase(RuleTestCase):
' x: 1,\n' ' x: 1,\n'
' y, z: 1\n' ' y, z: 1\n'
' }\n', conf, problem=(5, 3)) ' }\n', conf, problem=(5, 3))
def test_flow_sequences(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf)
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf, problem=(3, 4))
self.check('---\n'
'a: [x,\n'
' y,\n'
' z]\n', conf, problem=(3, 6))
self.check('---\n'
'a: [x,\n'
' y, z\n'
']\n', conf, problem=(3, 3))
self.check('---\n' self.check('---\n'
'a: [\n' 'a: [\n'
' x,\n' ' x,\n'
@@ -244,6 +628,138 @@ class IndentationTestCase(RuleTestCase):
' x,\n' ' x,\n'
' y, z\n' ' y, z\n'
' ]\n', conf, problem=(5, 3)) ' ]\n', conf, problem=(5, 3))
self.check('---\n'
'obj: {\n'
' a: 1,\n'
' b: 2,\n'
' c: 3\n'
'}\n', conf, problem1=(4, 4), problem2=(5, 2))
self.check('---\n'
'list: [\n'
' 1,\n'
' 2,\n'
' 3\n'
']\n', conf, problem1=(4, 4), problem2=(5, 2))
self.check('---\n'
'top:\n'
' rules: [\n'
' 1, 2,\n'
' ]\n', conf)
self.check('---\n'
'top:\n'
' rules: [\n'
' 1, 2,\n'
']\n'
' rulez: [\n'
' 1, 2,\n'
' ]\n', conf, problem1=(5, 1), problem2=(8, 5))
self.check('---\n'
'top:\n'
' rules:\n'
' here: {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n', conf)
self.check('---\n'
'top:\n'
' rules:\n'
' here: {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n'
' there: {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n', conf, problem1=(7, 7), problem2=(11, 3))
def test_cleared_flows(self):
# flow:
# [
# ...
# ]
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'top:\n'
' rules:\n'
' {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n', conf)
self.check('---\n'
'top:\n'
' rules:\n'
' {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n', conf, problem=(5, 8))
self.check('---\n'
'top:\n'
' rules:\n'
' {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n', conf, problem=(4, 4))
self.check('---\n'
'top:\n'
' rules:\n'
' {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n', conf, problem=(7, 4))
self.check('---\n'
'top:\n'
' rules:\n'
' {\n'
' foo: 1,\n'
' bar: 2\n'
' }\n', conf, problem=(7, 6))
self.check('---\n'
'top:\n'
' [\n'
' a, b, c\n'
' ]\n', conf)
self.check('---\n'
'top:\n'
' [\n'
' a, b, c\n'
' ]\n', conf, problem=(4, 6))
self.check('---\n'
'top:\n'
' [\n'
' a, b, c\n'
' ]\n', conf, problem=(3, 4))
self.check('---\n'
'top:\n'
' [\n'
' a, b, c\n'
' ]\n', conf, problem=(5, 4))
self.check('---\n'
'top:\n'
' rules: [\n'
' {\n'
' foo: 1\n'
' },\n'
' {\n'
' foo: 2,\n'
' bar: [\n'
' a, b, c\n'
' ],\n'
' },\n'
' ]\n', conf)
self.check('---\n'
'top:\n'
' rules: [\n'
' {\n'
' foo: 1\n'
' },\n'
' {\n'
' foo: 2,\n'
' bar: [\n'
' a, b, c\n'
' ],\n'
' },\n'
']\n', conf, problem1=(5, 6), problem2=(6, 6),
problem3=(9, 9), problem4=(11, 7), problem5=(13, 1))
def test_under_indented(self): def test_under_indented(self):
conf = 'indentation: {spaces: 2, indent-sequences: yes}' conf = 'indentation: {spaces: 2, indent-sequences: yes}'
@@ -438,21 +954,6 @@ class IndentationTestCase(RuleTestCase):
'document-start: disable\n') 'document-start: disable\n')
self.check(' a: 1\n', conf, problem=(1, 3)) self.check(' a: 1\n', conf, problem=(1, 3))
def test_broken_inline_flows(self):
conf = 'indentation: {spaces: 2}'
self.check('---\n'
'obj: {\n'
' a: 1,\n'
' b: 2,\n'
' c: 3\n'
'}\n', conf, problem1=(4, 4), problem2=(5, 2))
self.check('---\n'
'list: [\n'
' 1,\n'
' 2,\n'
' 3\n'
']\n', conf, problem1=(4, 4), problem2=(5, 2))
def test_explicit_block_mappings(self): def test_explicit_block_mappings(self):
conf = 'indentation: {spaces: 4}' conf = 'indentation: {spaces: 4}'
self.check('---\n' self.check('---\n'
@@ -534,6 +1035,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'

View File

@@ -35,6 +35,7 @@ from tests.common import RuleTestCase
# for br in span.find_all("br"): # for br in span.find_all("br"):
# br.replace_with("\n") # br.replace_with("\n")
# text = text.replace('\u2193', '') # downwards arrow # text = text.replace('\u2193', '') # downwards arrow
# text = text.replace('\u21d3', '') # double downwards arrow
# text = text.replace('\u00b7', ' ') # visible space # text = text.replace('\u00b7', ' ') # visible space
# text = text.replace('\u21d4', '') # byte order mark # text = text.replace('\u21d4', '') # byte order mark
# text = text.replace('\u2192', '\t') # right arrow # text = text.replace('\u2192', '\t') # right arrow
@@ -95,6 +96,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'),
@@ -111,6 +113,11 @@ 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('tests/yaml-1.2-spec-examples')
@@ -122,15 +129,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()).
@@ -177,7 +175,7 @@ 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) as f:

View File

@@ -2,7 +2,7 @@
# Comments: # Comments:
strip: |- strip: |-
# text # text
# Clip # Clip
# comments: # comments:

View File

@@ -14,12 +14,16 @@
# 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/>.
APP_NAME = 'yamllint' """A linter for YAML files.
APP_VERSION = '0.7.0'
APP_DESCRIPTION = """A linter for YAML files.
yamllint does not only check for syntax validity, but for common cosmetic yamllint does not only check for syntax validity, but for weirdnesses like key
conventions such as lines length, trailing spaces, indentation, etc.""" repetition and cosmetic problems such as lines length, trailing spaces,
indentation, etc."""
APP_NAME = 'yamllint'
APP_VERSION = '0.7.2'
APP_DESCRIPTION = __doc__
__author__ = u'Adrien Vergé' __author__ = u'Adrien Vergé'
__copyright__ = u'Copyright 2016, Adrien Vergé' __copyright__ = u'Copyright 2016, Adrien Vergé'

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']

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

@@ -154,14 +154,20 @@ CONF = {'spaces': int,
'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):
def __init__(self, type, indent): def __init__(self, type, indent, line_indent=None):
self.type = type self.type = type
self.indent = indent self.indent = 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):
@@ -230,7 +236,7 @@ def check_scalar_indentation(conf, token, context):
(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
@@ -250,7 +256,7 @@ def check(conf, token, prev, next, context):
if isinstance(token, (yaml.FlowMappingEndToken, if isinstance(token, (yaml.FlowMappingEndToken,
yaml.FlowSequenceEndToken)): yaml.FlowSequenceEndToken)):
expected = 0 expected = context['stack'][-1].line_indent
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)):
@@ -275,9 +281,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:
@@ -293,7 +296,7 @@ def check(conf, token, prev, next, context):
# : 1 # : 1
indent = token.start_mark.column + conf['spaces'] indent = token.start_mark.column + conf['spaces']
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:
@@ -301,11 +304,12 @@ def check(conf, token, prev, next, context):
indent = next.start_mark.column indent = next.start_mark.column
else: else:
# - { # - {
# a: 1, b: 2 # a: 1, b: 2
# } # }
indent = context['cur_line_indent'] + conf['spaces'] indent = context['cur_line_indent'] + conf['spaces']
context['stack'].append(Parent(MAP, indent)) context['stack'].append(Parent(F_MAP, indent,
line_indent=context['cur_line_indent']))
elif isinstance(token, yaml.BlockSequenceStartToken): elif isinstance(token, yaml.BlockSequenceStartToken):
# - - a # - - a
@@ -320,6 +324,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
@@ -344,13 +354,8 @@ def check(conf, token, prev, next, context):
# ] # ]
indent = context['cur_line_indent'] + conf['spaces'] indent = context['cur_line_indent'] + conf['spaces']
context['stack'].append(Parent(F_SEQ, indent)) context['stack'].append(Parent(F_SEQ, 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
@@ -359,21 +364,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
@@ -414,11 +423,50 @@ def check(conf, token, prev, next, context):
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'] = []