Skip to content

Commit 9f0ad49

Browse files
authored
Keywords must be str (#336)
* [test] upgrade keyword_test to pytest * [test] add unit test to check that keywords are str * [grammar] make keywords always have type str fixes #335 * [dist] bump up version for release
1 parent 4aedb3e commit 9f0ad49

File tree

4 files changed

+153
-127
lines changed

4 files changed

+153
-127
lines changed

grammar/tatsu.ebnf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ keywords
6565
6666
keyword
6767
=
68-
'@@keyword' ~ '::' ~ {@+:literal !(':'|'=')}
68+
'@@keyword' ~ '::' ~ {@+:(word|string) !(':'|'=')}
6969
;
7070
7171

tatsu/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '5.11.4b1'
1+
__version__ = '5.12.0'

tatsu/bootstrap.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,16 @@ def _keyword_(self):
238238
self._cut()
239239

240240
def block0():
241-
self._literal_()
241+
with self._group():
242+
with self._choice():
243+
with self._option():
244+
self._word_()
245+
with self._option():
246+
self._string_()
247+
self._error(
248+
'expecting one of: '
249+
'<string> <word>'
250+
)
242251
self.add_last_node_to_name('@')
243252
with self._ifnot():
244253
with self._group():

test/grammar/keyword_test.py

Lines changed: 141 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,169 @@
1-
import unittest
21
from ast import parse
32

3+
import pytest
4+
45
from tatsu.exceptions import FailedParse
56
from tatsu.ngcodegen import codegen
67
from tatsu.tool import compile
78

89

9-
class KeywordTests(unittest.TestCase):
10-
def test_keywords_in_rule_names(self):
11-
grammar = """
12-
start
13-
=
14-
whitespace
15-
;
16-
10+
def test_keywords_in_rule_names():
11+
grammar = """
12+
start
13+
=
1714
whitespace
18-
=
19-
{'x'}+
20-
;
21-
"""
22-
m = compile(grammar, 'Keywords')
23-
m.parse('x')
24-
25-
def test_python_keywords_in_rule_names(self):
26-
# This is a regression test for
27-
# https://bitbucket.org/neogeny/tatsu/issues/59
28-
# (semantic actions not called for rules with the same name as a python
29-
# keyword).
30-
grammar = """
31-
not = 'x' ;
32-
"""
33-
m = compile(grammar, 'Keywords')
34-
35-
class Semantics:
36-
def __init__(self):
37-
self.called = False
38-
39-
def not_(self, ast):
40-
self.called = True
41-
42-
semantics = Semantics()
43-
m.parse('x', semantics=semantics)
44-
assert semantics.called
45-
46-
def test_define_keywords(self):
47-
grammar = """
48-
@@keyword :: B C
49-
@@keyword :: 'A'
50-
51-
start = ('a' 'b').{'x'}+ ;
52-
"""
53-
model = compile(grammar, 'test')
54-
c = codegen(model)
55-
parse(c)
56-
57-
grammar2 = str(model)
58-
model2 = compile(grammar2, 'test')
59-
c2 = codegen(model2)
60-
parse(c2)
61-
62-
self.assertEqual(grammar2, str(model2))
63-
64-
def test_check_keywords(self):
65-
grammar = r"""
66-
@@keyword :: A
67-
68-
start = {id}+ $ ;
69-
70-
@name
71-
id = /\w+/ ;
72-
"""
73-
model = compile(grammar, 'test')
74-
c = codegen(model)
75-
print(c)
76-
parse(c)
77-
78-
ast = model.parse('hello world')
79-
self.assertEqual(['hello', 'world'], ast)
15+
;
8016
81-
try:
82-
ast = model.parse('hello A world')
83-
self.assertEqual(['hello', 'A', 'world'], ast)
84-
self.fail('accepted keyword as name')
85-
except FailedParse as e:
86-
self.assertTrue('"A" is a reserved word' in str(e))
17+
whitespace
18+
=
19+
{'x'}+
20+
;
21+
"""
22+
m = compile(grammar, 'Keywords')
23+
m.parse('x')
24+
25+
26+
def test_python_keywords_in_rule_names():
27+
# This is a regression test for
28+
# https://bitbucket.org/neogeny/tatsu/issues/59
29+
# (semantic actions not called for rules with the same name as a python
30+
# keyword).
31+
grammar = """
32+
not = 'x' ;
33+
"""
34+
m = compile(grammar, 'Keywords')
35+
36+
class Semantics:
37+
def __init__(self):
38+
self.called = False
39+
40+
def not_(self, ast):
41+
self.called = True
42+
43+
semantics = Semantics()
44+
m.parse('x', semantics=semantics)
45+
assert semantics.called
46+
47+
48+
def test_define_keywords():
49+
grammar = """
50+
@@keyword :: B C
51+
@@keyword :: 'A'
52+
53+
start = ('a' 'b').{'x'}+ ;
54+
"""
55+
model = compile(grammar, 'test')
56+
c = codegen(model)
57+
parse(c)
58+
59+
grammar2 = str(model)
60+
model2 = compile(grammar2, 'test')
61+
c2 = codegen(model2)
62+
parse(c2)
63+
64+
assert grammar2 == str(model2)
8765

88-
def test_check_unicode_name(self):
89-
grammar = r"""
90-
@@keyword :: A
9166

92-
start = {id}+ $ ;
67+
def test_check_keywords():
68+
grammar = r"""
69+
@@keyword :: A
9370
94-
@name
95-
id = /\w+/ ;
96-
"""
97-
model = compile(grammar, 'test')
98-
model.parse('hello Øresund')
71+
start = {id}+ $ ;
9972
100-
def test_sparse_keywords(self):
101-
grammar = r"""
102-
@@keyword :: A
73+
@name
74+
id = /\w+/ ;
75+
"""
76+
model = compile(grammar, 'test')
77+
c = codegen(model)
78+
print(c)
79+
parse(c)
10380

104-
@@ignorecase :: False
81+
ast = model.parse('hello world')
82+
assert ast == ['hello', 'world']
10583

106-
start = {id}+ $ ;
84+
try:
85+
ast = model.parse('hello A world')
86+
assert ast == ['hello', 'A', 'world']
87+
pytest.fail('accepted keyword as name')
88+
except FailedParse as e:
89+
assert '"A" is a reserved word' in str(e)
90+
91+
92+
def test_check_unicode_name():
93+
grammar = r"""
94+
@@keyword :: A
95+
96+
start = {id}+ $ ;
97+
98+
@name
99+
id = /\w+/ ;
100+
"""
101+
model = compile(grammar, 'test')
102+
model.parse('hello Øresund')
103+
104+
105+
def test_sparse_keywords():
106+
grammar = r"""
107+
@@keyword :: A
108+
109+
@@ignorecase :: False
110+
111+
start = {id}+ $ ;
112+
113+
@@keyword :: B
114+
115+
@name
116+
id = /\w+/ ;
117+
"""
118+
model = compile(grammar, 'test', trace=False, colorize=True)
119+
c = codegen(model)
120+
parse(c)
121+
122+
ast = model.parse('hello world')
123+
assert ast == ['hello', 'world']
124+
125+
for k in ('A', 'B'):
126+
try:
127+
ast = model.parse('hello %s world' % k)
128+
assert ['hello', k, 'world'] == ast
129+
pytest.fail('accepted keyword "%s" as name' % k)
130+
except FailedParse as e:
131+
assert '"%s" is a reserved word' % k in str(e)
107132

108-
@@keyword :: B
109133

110-
@name
111-
id = /\w+/ ;
112-
"""
113-
model = compile(grammar, 'test', trace=False, colorize=True)
114-
c = codegen(model)
115-
parse(c)
134+
def test_ignorecase_keywords():
135+
grammar = r"""
136+
@@ignorecase :: True
137+
@@keyword :: if
116138
117-
ast = model.parse('hello world')
118-
self.assertEqual(['hello', 'world'], ast)
139+
start = rule ;
119140
120-
for k in ('A', 'B'):
121-
try:
122-
ast = model.parse('hello %s world' % k)
123-
self.assertEqual(['hello', k, 'world'], ast)
124-
self.fail('accepted keyword "%s" as name' % k)
125-
except FailedParse as e:
126-
self.assertTrue('"%s" is a reserved word' % k in str(e))
141+
@name
142+
rule = @:word if_exp $ ;
127143
128-
def test_ignorecase_keywords(self):
129-
grammar = r"""
130-
@@ignorecase :: True
131-
@@keyword :: if
144+
if_exp = 'if' digit ;
132145
133-
start = rule ;
146+
word = /\w+/ ;
147+
digit = /\d/ ;
148+
"""
134149

135-
@name
136-
rule = @:word if_exp $ ;
150+
model = compile(grammar, 'test')
137151

138-
if_exp = 'if' digit ;
152+
model.parse('nonIF if 1', trace=False)
139153

140-
word = /\w+/ ;
141-
digit = /\d/ ;
142-
"""
154+
with pytest.raises(FailedParse):
155+
model.parse('i rf if 1', trace=False)
143156

144-
model = compile(grammar, 'test')
157+
with pytest.raises(FailedParse):
158+
model.parse('IF if 1', trace=False)
145159

146-
model.parse('nonIF if 1', trace=False)
147160

148-
with self.assertRaises(FailedParse):
149-
model.parse('i rf if 1', trace=False)
161+
def test_keywords_are_str():
162+
grammar = r"""
163+
@@keyword :: True False
150164
151-
with self.assertRaises(FailedParse):
152-
model.parse('IF if 1', trace=False)
165+
start = $ ;
166+
"""
167+
model = compile(grammar, 'test')
168+
assert model.keywords == ['True', 'False']
169+
assert all(isinstance(k, str) for k in model.keywords)

0 commit comments

Comments
 (0)