-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtest_symbols.py
352 lines (314 loc) · 17.3 KB
/
test_symbols.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
from base.qsyntax import Keyword, Symbol
from base.qsyntax import sym, sym2, kw, kw2, check_sym_name_level, is_literal, is_symbol, is_keyword, is_clause, is_variable_or_clause, edn_factory2, QSError, edn_dumps
from base import qsyntax as qs
from base.test_qs import Base
import unittest, datetime
# https://github.com/edn-format/edn
e_empty = 'naming: name-or-prefix needs non-empty string'
e_start = 'naming: name-or-prefix cannot start with ":" or "#" or digit'
e_afterstart= 'naming: name-or-prefix cannot have digit after starting "." or "-" or "+"'
e_allowed = 'naming: name-or-prefix allows alphanumerics and' #...
e_1_slash = 'naming: only one / allowed'
name_errors = [ e_empty, e_start, e_afterstart, e_allowed, e_1_slash ]
class kw_levels( Base, unittest.TestCase):
def test_L1_ok_1_level(self):
ka = Keyword( 'a')
kw1tests = [ kw.a , kw('a') ]
for i in kw1tests:
self.assertIsInstance( i, Keyword)
self.assertEqual( i, ka)
self.assertEqual( str(i), ':a')
self.assertEqual( len( set( [ka] + kw1tests)), 1)
def test_L2_ok_1_level(self):
level1 = kw2.a
self.assertIsInstance( level1, edn_factory2._edn2_half)
self.assertNotIsInstance( level1, Keyword)
self.assertNotEqual( level1, kw.a)
self.assertIn( 'edn_factory2._edn2_half', str(level1))
kax = Keyword( 'a/x')
l1tests = [ level1.x , level1('x') ]
for i in l1tests:
self.assertIsInstance( i, Keyword)
self.assertEqual( i, kax)
self.assertEqual( str(i), ':a/x')
self.assertEqual( len( set( [kax] + l1tests)), 1)
def test_L2_ok_2_levels(self):
kw2tests = [ kw2.a.b , kw2('a', 'b') , kw2.a('b') , kw2('a').b , kw2('a')('b') ]
kab = Keyword( 'a/b')
for i in kw2tests:
self.assertIsInstance( i, Keyword)
self.assertEqual( i, kab)
self.assertEqual( str(i), ':a/b')
self.assertEqual( len( set( [kab] + kw2tests)), 1)
def test_no_more_levels(self):
with self.assertRaisesRegex(AttributeError, "'Keyword' object has no attribute 'b'"):
kw.a.b
with self.assertRaisesRegex(AttributeError, "'Keyword' object has no attribute 'c'"):
kw2.a.b.c
with self.assertRaisesRegex(TypeError, "'Keyword' object is not callable"):
kw.a('b')
with self.assertRaisesRegex(TypeError, "'Keyword' object is not callable"):
kw2.a.b('x')
def test_L1_names(self):
doc='''
https://github.com/edn-format/edn
Symbols begin with a non-numeric character and can contain alphanumeric
characters and . * + ! - _ ? $ % & = < >. If -, + or . are the first character,
the second character (if any) must be non-numeric. Additionally, : # are
allowed as constituent characters in symbols other than as the first
character.
/ has special meaning in symbols. It can be used once only in the middle of a
symbol to separate the prefix (often a namespace) from the name.
/ by itself is a legal symbol, but otherwise neither the
prefix nor the name part can be empty when the symbol contains /.
If a symbol has a prefix and /, the following name component should follow the
first-character restrictions for symbols as a whole. This is to avoid ambiguity
in reading contexts where prefixes might be presumed as implicitly included
namespaces and elided thereafter.
Keywords are identifiers that typically designate themselves. They are
semantically akin to enumeration values. Keywords follow the rules of symbols,
except they must begin with : , which is disallowed for symbols.
Per the symbol rules above, :/ and :/anything are not legal keywords.
A keyword cannot begin with ::
(::some/name1 is Clojure shorthand for :someverylong/name1 if 'some' is aliased to :xyzlong)
'''
def c( name , *, sym= 'result/ERR', kw= 'result/ERR', case= 'description'):
if kw == 'result/ERR': kw = sym
assert sym != 'result/ERR', sym
assert kw != 'result/ERR', kw
assert case and case != 'description', case
for kind,okerr in dict( sym= sym, kw= kw ).items():
func = getattr( qs, kind)
with self.subTest( f'{case}::::: {kind} {name!r}'):
if okerr not in name_errors:
self.assertEqual( edn_dumps( func( name)), okerr)
else:
with self.assert_qserror( okerr, exact= False ):
func( name)
# XXX =err?ok means it does err BUT spec allows it - e.g. names :a/:b and :# are spec-allowed
#plain
c( 'a', sym= 'a', kw=':a', case= 'name')
c( 'a1', sym= 'a1', kw=':a1', case= 'name')
c( '_', sym= '_', kw=':_', case= 'name-_')
c( '_a', sym= '_a', kw=':_a', case= 'name-_')
c( 'a_', sym= 'a_', kw=':a_', case= 'name-_')
c( '_1', sym= '_1', kw=':_1', case= 'name-_')
# 1 slash
c( '/', sym= '/', kw=e_empty, case= 'only / =ok but :/ =err')
c( 'a/b', sym= 'a/b', kw=':a/b', case= 'namespace/name')
c( 'a/_', sym= 'a/_', kw=':a/_', case= 'namespace/name-_')
c( 'a/_b', sym= 'a/_b', kw=':a/_b', case= 'namespace/name-_')
c( 'a/b_', sym= 'a/b_', kw=':a/b_', case= 'namespace/name-_')
c( 'a1/b2', sym= 'a1/b2', kw=':a1/b2',case= 'namespace/name')
c( 'a_/b_', sym= 'a_/b_', kw=':a_/b_',case= 'namespace/name-_')
c( '_a/_b', sym= '_a/_b', kw=':_a/_b',case= 'namespace/name-_')
c( '/a', sym= e_empty, case= '/a :/a =err')
c( '/2', sym= e_empty, case= '/2 :/2 =err')
c( 'a/', sym= e_empty, case= 'a/ :a/ =err')
c( '1/', sym= e_start, case= '1/ :1/ =err')
#start-with/with-many . - +
c( '.', sym= '.', kw=':.', case= 'name-start-.')
c( '+a', sym= '+a', kw=':+a', case= 'name-start-+')
c( '.a', sym= '.a', kw=':.a', case= 'name-start-.')
c( '-a', sym= '-a', kw=':-a', case= 'name-start--')
c( '..', sym= '..', kw=':..', case= 'name-start-.')
c( '---', sym= '---', kw=':---', case= 'name-start--')
c( '.a/.b', sym= '.a/.b', kw=':.a/.b', case= 'namespace/name-with-.')
c( '.a/..', sym= '.a/..', kw=':.a/..', case= 'namespace/name-with-.')
c( 'a.b/.c.d', sym= 'a.b/.c.d', kw=':a.b/.c.d', case= 'namespace/name-with-.')
c( 'a/+', sym= 'a/+', kw=':a/+', case= 'namespace/name-start-+')
c( '+/+', sym= '+/+', kw=':+/+', case= 'namespace/name-start-+')
# colon : --- hash #
c( ':', sym= e_start, case= ': :: =err')
c( ':a', sym= e_start, case= ':a ::a =err')
c( 'a:', sym= 'a:', kw=':a:', case= 'a: :a: =ok')
c( '.:', sym= '.:', kw=':.:', case= '.: :.: =ok')
c( ':a/b', sym= e_start, case= ':a/b ::a/b =err')
c( '#', sym= e_start, case= '# =err :# =err?ok')
c( '#a', sym= e_start, case= '#a =err :#a =err?ok')
c( 'a#', sym= 'a#', kw=':a#', case= 'a# :a# =ok')
c( '.#', sym= '.#', kw=':.#', case= '.# :.# =ok')
c( '/:', sym= e_empty, case= '/: :/: =err')
c( '/:a', sym= e_empty, case= '/:a :/:a =err')
c( 'a/:', sym= e_start, case= 'a/: :a/: =err')
c( 'a/:b', sym= e_start, case= 'a/:b :a/:b =err?ok')
c( '/#', sym= e_empty, case= '/# :/# =err')
c( '/#a', sym= e_empty, case= '/#a :/#a =err')
c( 'a/#', sym= e_start, case= 'a/# :a/# =err?ok')
c( 'a/#b', sym= e_start, case= 'a/#b :a/#b =err?ok')
#empty/whitespace
c( '', sym= e_empty, case= 'empty =err')
c( ' ', sym= e_allowed, case= 'whitespace =err')
c( ',', sym= e_allowed, case= ', is whitespace =err')
c( ' x', sym= e_allowed, case= 'whitespace =err')
c( 'x ', sym= e_allowed, case= 'whitespace =err')
c( 'x y', sym= e_allowed, case= 'whitespace =err')
c( ',', sym= e_allowed, case= ', is whitespace =err')
c( ',x', sym= e_allowed, case= ', is whitespace =err')
c( 'x,', sym= e_allowed, case= ', is whitespace =err')
c( 'x,y', sym= e_allowed, case= ', is whitespace =err')
#non-str
c( 1, sym= e_empty, case= 'non-str:int =err')
c( None, sym= e_empty, case= 'non-str:None =err')
c( (), sym= e_empty, case= 'non-str:tuple =err')
c( (1,2), sym= e_empty, case= 'non-str:tuple =err')
c( [1], sym= e_empty, case= 'non-str:list =err')
#digit-start
c( '1', sym= e_start, case= 'startdigit =err but err?ok')
c( '1a', sym= e_start, case= 'startdigit =err but err?ok')
c( '1+2', sym= e_start, case= 'startdigit =err but err?ok')
c( '0x2', sym= e_start, case= 'startdigit =err but err?ok')
c( '123', sym= e_start, case= 'startdigit =err but err?ok')
c( '1/a', sym= e_start, case= 'startdigit =err but err?ok')
c( 'a/1', sym= e_start, case= 'startdigit =err')
c( '1/2', sym= e_start, case= 'startdigit =err')
#2+ slashes
c( 'a/b/c', sym= e_1_slash, case= '2+ slashes =err')
c( '//', sym= e_1_slash, case= '2+ slashes =err')
c( 'a/b/', sym= e_1_slash, case= '2+ slashes =err')
c( '/a/b', sym= e_1_slash, case= '2+ slashes =err')
c( '/ab/', sym= e_1_slash, case= '2+ slashes =err')
c( 'a/b/c/d', sym= e_1_slash, case= '2+ slashes =err')
#not in allowed
c( '~', sym= e_allowed, case= 'disallowed:~ =err')
c( 'a~', sym= e_allowed, case= 'disallowed:~ =err')
c( '~a', sym= e_allowed, case= 'disallowed:~ =err')
c( 'a/~', sym= e_allowed, case= 'disallowed:~ =err')
c( 'a/~b', sym= e_allowed, case= 'disallowed:~ =err')
c( 'a/b~', sym= e_allowed, case= 'disallowed:~ =err')
c( '(a)', sym= e_allowed, case= 'disallowed:() =err')
c( 'a[1', sym= e_allowed, case= 'disallowed:[ =err')
c( 'a[1]', sym= e_allowed, case= 'disallowed:[] =err')
#digits after starting .+-
c( '+1', sym= e_afterstart, case= 'number-like =err')
c( '-1', sym= e_afterstart, case= 'number-like =err')
c( '.1', sym= e_afterstart, case= 'number-like =err')
c( 'a/+1', sym= e_afterstart, case= 'number-like =err')
c( 'a/.1', sym= e_afterstart, case= 'number-like =err')
c( '-.1', sym= '-.1', kw= ':-.1', case= 'weird almost number-like =ok')
c( '+.1', sym= '+.1', kw= ':+.1', case= 'weird almost number-like =ok')
c( '+-1', sym= '+-1', kw= ':+-1', case= 'weird =ok')
c( '--1', sym= '--1', kw= ':--1', case= 'weird =ok')
c( '.-1', sym= '.-1', kw= ':.-1', case= 'weird =ok')
c( '.+1', sym= '.+1', kw= ':.+1', case= 'weird =ok')
c( '..1', sym= '..1', kw= ':..1', case= 'weird =ok')
def test_L2_names_level1(self):
#TODO something around this + above table?
for func in sym2, kw2:
for err in [ '', ' ', 'a/', '/a',
'1/a', 'a/2', '1/2', '1/',
':', ':a', 'a/:', 'a/+1', '#', '#1', '#x',
'a/b', '/', 2, ' /', 'a/ ', ' /a'
]:
#print( 222, repr(err))
with self.assert_qserror( repr(err), exact= False):
func( err)
if err: #same at level2
with self.assert_qserror( repr(err), exact= False):
func( 'x', err)
def test_L2_names_level2(self):
level1 = kw2.a
for err in [ '', ' ', 2, None, [1],
*' , a/ /a / a/b 1 1a : # :a #a +1 .0 ~ [ '.split()]:
with self.assert_qserror( repr(err), exact= False):
level1( err)
class symbols_keywords( Base, unittest.TestCase):
def test_check_sym_name(self):
'maybe this is redundant.. implementation-detgail ; above sym-vs-kw is the appropriate'
# begin with non-numeric and can contain alphanumeric
for ok in 'x x1 xy xy1 '.split():
check_sym_name_level( ok)
# also special characters without :#
allowed_anywhere = '. * + ! - _ ? $ % & = < >'.split()
for char in allowed_anywhere:
check_sym_name_level( f'x{char}') # as second char
check_sym_name_level( f'{char}x') # as first char
check_sym_name_level( f'{char}{char}') # both first+second char
check_sym_name_level( char) # alone
if char not in '-+.':
check_sym_name_level( f'{char}1') # as first char then digit
else:
with self.assert_qserror( e_afterstart, exact= False):
check_sym_name_level( f'{char}1')
# non-empty str
for err in [ None, 1, '', ]:
with self.assert_qserror( f'{e_empty}: {err!r}'):
check_sym_name_level( err)
#invalid chars
for err in [
' ', ' x', 'x ', 'x y',
',', 'a,b', '(a)', 'a[2]',
'x/y', '/', 'x/', '/x', #does not allow /
' /', '/ ',
]:
with self.assert_qserror( e_allowed, exact= False):
check_sym_name_level( err)
# !(begin with non-numeric)
for err in [ '1', '102', '1x', '0.',]:
with self.assert_qserror( e_start, exact= False):
check_sym_name_level( err)
allowed_not_first = ': #'.split()
for char in allowed_not_first:
check_sym_name_level( f'x{char}') # ok as second char
for err in [
f'{char}x', # err as first char
f'{char}{char}', # err both first+second char
char ]: # err alone
with self.assert_qserror( e_start, exact= False):
check_sym_name_level( err)
# If some of - + . is the first character, the second (if any) must be non-numeric
for err in [ '.1', '-1', '+1', '+0', '.0' ]:
with self.assert_qserror( e_afterstart, exact= False):
check_sym_name_level( err)
# weird but ok
for sym in ['-.1', '..', '-.', '.', '.-', '-+']:
check_sym_name_level( sym)
def test_is_literal(self):
for ok in [ None, 'x', 1, 2.5, 0, '0', '3', datetime.datetime.now(), datetime.datetime.today() ]:
self.assertTrue( is_literal( ok), repr(ok) )
for err in [ sym.x, sym2.a.b, kw.a, kw2.x.y, (), (1,2), [], [1,2], {} ]:
self.assertFalse( is_literal( err), repr(err) )
def test_is_symbol(self):
for ok in [ sym.x, sym2.x.y, sym('a'), sym('a/b'), Symbol('x') ]:
self.assertTrue( is_symbol( ok), repr(ok))
for err in [ kw.x, 'sym.x', sym2.x, kw2.x, 3, 'x', ':x', [], (sym.x,), [sym.x], None ]:
self.assertFalse( is_symbol( err), repr(err))
def test_is_keyword(self):
for ok in [ kw.x, kw2.x.y, kw('a'), kw('a/b'), Keyword('x') ]:
self.assertTrue( is_keyword( ok), repr(ok))
for err in [ sym.x, 'kw.x', sym2.x, kw2.x, 3, 'x', ':x', [], (kw.x,), [kw.x], None ]:
self.assertFalse( is_keyword( err), repr(err))
def test_is_clause(self):
def is__clause( *x): self.assertTrue( is_clause( *x))
def not_clause( *x): self.assertFalse( is_clause( *x))
# one argument
is__clause( [sym.x])
is__clause( [kw.x])
is__clause( ( 'x', 'y'))
is__clause( [1, 2, 3])
is__clause( [1, 2])
not_clause( 'x')
not_clause( 1,)
not_clause( [])
not_clause( ())
not_clause( sym.x)
# more arguments
is__clause( ( 'x', 'y'), ('f', 'a'))
is__clause( [1, 2, 3], [4, 5, 6])
not_clause( 'x', 'y')
not_clause( 'x', ( 'x', 'y'))
not_clause( ( 'x', 'y'), 'x')
not_clause( 1, 2)
def test_is_variable_or_clause(self):
# ok
self.assertTrue( is_variable_or_clause( sym.x))
self.assertTrue( is_variable_or_clause( ['x', 'y', 'f']))
self.assertTrue( is_variable_or_clause( (1, 2 )))
self.assertTrue( is_variable_or_clause( [sym.x]))
self.assertTrue( is_variable_or_clause( [kw.x]))
self.assertFalse( is_variable_or_clause( 'x'))
self.assertFalse( is_variable_or_clause( kw.x))
self.assertFalse( is_variable_or_clause( 1))
if __name__ == '__main__':
unittest.main() #verbosity=2)
# vim:ts=4:sw=4:expandtab