blob: 7baf3801bdac4d6ffc6af343a19c36ed6564c51b [file] [log] [blame]
Larry Hastings31826802013-10-19 00:09:25 -07001#!/usr/bin/env python3
2#
3# Argument Clinic
4# Copyright 2012-2013 by Larry Hastings.
5# Licensed to the PSF under a contributor agreement.
6#
7
8import builtins
9import clinic
10from clinic import DSLParser
11import collections
12import inspect
13from test import support
14import unittest
15from unittest import TestCase
16
17class FakeConverter:
18 def __init__(self, name, args):
19 self.name = name
20 self.args = args
21
22
23class FakeConverterFactory:
24 def __init__(self, name):
25 self.name = name
26
27 def __call__(self, name, default, **kwargs):
28 return FakeConverter(self.name, kwargs)
29
30
31class FakeConvertersDict:
32 def __init__(self):
33 self.used_converters = {}
34
35 def get(self, name, default):
36 return self.used_converters.setdefault(name, FakeConverterFactory(name))
37
38class FakeClinic:
39 def __init__(self):
40 self.converters = FakeConvertersDict()
41 self.legacy_converters = FakeConvertersDict()
42 self.language = clinic.CLanguage()
43 self.filename = None
44 self.block_parser = clinic.BlockParser('', self.language)
45 self.modules = collections.OrderedDict()
46 clinic.clinic = self
47 self.name = "FakeClinic"
48
49 def is_directive(self, name):
50 return name == "module"
51
52 def directive(self, name, args):
53 self.called_directives[name] = args
54
55 _module_and_class = clinic.Clinic._module_and_class
56
57
58
59class ClinicGroupPermuterTest(TestCase):
60 def _test(self, l, m, r, output):
61 computed = clinic.permute_optional_groups(l, m, r)
62 self.assertEqual(output, computed)
63
64 def test_range(self):
65 self._test([['start']], ['stop'], [['step']],
66 (
67 ('stop',),
68 ('start', 'stop',),
69 ('start', 'stop', 'step',),
70 ))
71
72 def test_add_window(self):
73 self._test([['x', 'y']], ['ch'], [['attr']],
74 (
75 ('ch',),
76 ('ch', 'attr'),
77 ('x', 'y', 'ch',),
78 ('x', 'y', 'ch', 'attr'),
79 ))
80
81 def test_ludicrous(self):
82 self._test([['a1', 'a2', 'a3'], ['b1', 'b2']], ['c1'], [['d1', 'd2'], ['e1', 'e2', 'e3']],
83 (
84 ('c1',),
85 ('b1', 'b2', 'c1'),
86 ('b1', 'b2', 'c1', 'd1', 'd2'),
87 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1'),
88 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1', 'd1', 'd2'),
89 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1', 'd1', 'd2', 'e1', 'e2', 'e3'),
90 ))
91
92 def test_right_only(self):
93 self._test([], [], [['a'],['b'],['c']],
94 (
95 (),
96 ('a',),
97 ('a', 'b'),
98 ('a', 'b', 'c')
99 ))
100
101 def test_have_left_options_but_required_is_empty(self):
102 def fn():
103 clinic.permute_optional_groups(['a'], [], [])
104 self.assertRaises(AssertionError, fn)
105
106
107class ClinicLinearFormatTest(TestCase):
108 def _test(self, input, output, **kwargs):
109 computed = clinic.linear_format(input, **kwargs)
110 self.assertEqual(output, computed)
111
112 def test_empty_strings(self):
113 self._test('', '')
114
115 def test_solo_newline(self):
116 self._test('\n', '\n')
117
118 def test_no_substitution(self):
119 self._test("""
120 abc
121 """, """
122 abc
123 """)
124
125 def test_empty_substitution(self):
126 self._test("""
127 abc
128 {name}
129 def
130 """, """
131 abc
132 def
133 """, name='')
134
135 def test_single_line_substitution(self):
136 self._test("""
137 abc
138 {name}
139 def
140 """, """
141 abc
142 GARGLE
143 def
144 """, name='GARGLE')
145
146 def test_multiline_substitution(self):
147 self._test("""
148 abc
149 {name}
150 def
151 """, """
152 abc
153 bingle
154 bungle
155
156 def
157 """, name='bingle\nbungle\n')
158
159class InertParser:
160 def __init__(self, clinic):
161 pass
162
163 def parse(self, block):
164 pass
165
166class CopyParser:
167 def __init__(self, clinic):
168 pass
169
170 def parse(self, block):
171 block.output = block.input
172
173
174class ClinicBlockParserTest(TestCase):
175 def _test(self, input, output):
176 language = clinic.CLanguage()
177
178 blocks = list(clinic.BlockParser(input, language))
179 writer = clinic.BlockPrinter(language)
180 for block in blocks:
181 writer.print_block(block)
182 output = writer.f.getvalue()
183 assert output == input, "output != input!\n\noutput " + repr(output) + "\n\n input " + repr(input)
184
185 def round_trip(self, input):
186 return self._test(input, input)
187
188 def test_round_trip_1(self):
189 self.round_trip("""
190 verbatim text here
191 lah dee dah
192""")
193 def test_round_trip_2(self):
194 self.round_trip("""
195 verbatim text here
196 lah dee dah
197/*[inert]
198abc
199[inert]*/
200def
201/*[inert checksum: 7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/
202xyz
203""")
204
205 def _test_clinic(self, input, output):
206 language = clinic.CLanguage()
207 c = clinic.Clinic(language)
208 c.parsers['inert'] = InertParser(c)
209 c.parsers['copy'] = CopyParser(c)
210 computed = c.parse(input)
211 self.assertEqual(output, computed)
212
213 def test_clinic_1(self):
214 self._test_clinic("""
215 verbatim text here
216 lah dee dah
217/*[copy]
218def
219[copy]*/
220abc
221/*[copy checksum: 03cfd743661f07975fa2f1220c5194cbaff48451]*/
222xyz
223""", """
224 verbatim text here
225 lah dee dah
226/*[copy]
227def
228[copy]*/
229def
230/*[copy checksum: 7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/
231xyz
232""")
233
234
235class ClinicParserTest(TestCase):
236 def test_trivial(self):
237 parser = DSLParser(FakeClinic())
238 block = clinic.Block("module os\nos.access")
239 parser.parse(block)
240 module, function = block.signatures
241 self.assertEqual("access", function.name)
242 self.assertEqual("os", module.name)
243
244 def test_ignore_line(self):
245 block = self.parse("#\nmodule os\nos.access")
246 module, function = block.signatures
247 self.assertEqual("access", function.name)
248 self.assertEqual("os", module.name)
249
250 def test_param(self):
251 function = self.parse_function("module os\nos.access\n path: int")
252 self.assertEqual("access", function.name)
253 self.assertEqual(1, len(function.parameters))
254 p = function.parameters['path']
255 self.assertEqual('path', p.name)
256 self.assertIsInstance(p.converter, clinic.int_converter)
257
258 def test_param_default(self):
259 function = self.parse_function("module os\nos.access\n follow_symlinks: bool = True")
260 p = function.parameters['follow_symlinks']
261 self.assertEqual(True, p.default)
262
263 def test_param_no_docstring(self):
264 function = self.parse_function("""
265module os
266os.access
267 follow_symlinks: bool = True
268 something_else: str""")
269 p = function.parameters['follow_symlinks']
270 self.assertEqual(2, len(function.parameters))
271 self.assertIsInstance(function.parameters['something_else'].converter, clinic.str_converter)
272
273 def disabled_test_converter_arguments(self):
274 function = self.parse_function("module os\nos.access\n path: path_t(allow_fd=1)")
275 p = function.parameters['path']
276 self.assertEqual(1, p.converter.args['allow_fd'])
277
278 def test_param_docstring(self):
279 function = self.parse_function("""
280module os
281os.stat as os_stat_fn -> object(doc_default='stat_result')
282
283 path: str
284 Path to be examined""")
285 p = function.parameters['path']
286 self.assertEqual("Path to be examined", p.docstring)
287 self.assertEqual(function.return_converter.doc_default, 'stat_result')
288
289 def test_function_docstring(self):
290 function = self.parse_function("""
291module os
292os.stat as os_stat_fn
293
294 path: str
295 Path to be examined
296
297Perform a stat system call on the given path.""")
298 self.assertEqual("""
299Perform a stat system call on the given path.
300
301os.stat(path)
302 path
303 Path to be examined
304""".strip(), function.docstring)
305
306 def test_explicit_parameters_in_docstring(self):
307 function = self.parse_function("""
308module foo
309foo.bar
310 x: int
311 Documentation for x.
312 y: int
313
314This is the documentation for foo.
315
316Okay, we're done here.
317""")
318 self.assertEqual("""
319This is the documentation for foo.
320
321foo.bar(x, y)
322 x
323 Documentation for x.
324
325Okay, we're done here.
326""".strip(), function.docstring)
327
328 def test_parser_regression_special_character_in_parameter_column_of_docstring_first_line(self):
329 function = self.parse_function("""
330module os
331os.stat
332 path: str
333This/used to break Clinic!
334""")
335 self.assertEqual("os.stat(path)\n\nThis/used to break Clinic!", function.docstring)
336
337 def test_c_name(self):
338 function = self.parse_function("module os\nos.stat as os_stat_fn")
339 self.assertEqual("os_stat_fn", function.c_basename)
340
341 def test_return_converter(self):
342 function = self.parse_function("module os\nos.stat -> int")
343 self.assertIsInstance(function.return_converter, clinic.int_return_converter)
344
345 def test_star(self):
346 function = self.parse_function("module os\nos.access\n *\n follow_symlinks: bool = True")
347 p = function.parameters['follow_symlinks']
348 self.assertEqual(inspect.Parameter.KEYWORD_ONLY, p.kind)
349 self.assertEqual(0, p.group)
350
351 def test_group(self):
352 function = self.parse_function("module window\nwindow.border\n [\n ls : int\n ]\n /\n")
353 p = function.parameters['ls']
354 self.assertEqual(1, p.group)
355
356 def test_left_group(self):
357 function = self.parse_function("""
358module curses
359curses.window.addch
360 [
361 y: int
362 Y-coordinate.
363 x: int
364 X-coordinate.
365 ]
366 ch: char
367 Character to add.
368 [
369 attr: long
370 Attributes for the character.
371 ]
372 /
373""")
374 for name, group in (
375 ('y', -1), ('x', -1),
376 ('ch', 0),
377 ('attr', 1),
378 ):
379 p = function.parameters[name]
380 self.assertEqual(p.group, group)
381 self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
382 self.assertEqual(function.docstring.strip(), """
383curses.window.addch([y, x,] ch, [attr])
384 y
385 Y-coordinate.
386 x
387 X-coordinate.
388 ch
389 Character to add.
390 attr
391 Attributes for the character.
392 """.strip())
393
394 def test_nested_groups(self):
395 function = self.parse_function("""
396module curses
397curses.window.imaginary
398 [
399 [
400 y1: int
401 Y-coordinate.
402 y2: int
403 Y-coordinate.
404 ]
405 x1: int
406 X-coordinate.
407 x2: int
408 X-coordinate.
409 ]
410 ch: char
411 Character to add.
412 [
413 attr1: long
414 Attributes for the character.
415 attr2: long
416 Attributes for the character.
417 attr3: long
418 Attributes for the character.
419 [
420 attr4: long
421 Attributes for the character.
422 attr5: long
423 Attributes for the character.
424 attr6: long
425 Attributes for the character.
426 ]
427 ]
428 /
429""")
430 for name, group in (
431 ('y1', -2), ('y2', -2),
432 ('x1', -1), ('x2', -1),
433 ('ch', 0),
434 ('attr1', 1), ('attr2', 1), ('attr3', 1),
435 ('attr4', 2), ('attr5', 2), ('attr6', 2),
436 ):
437 p = function.parameters[name]
438 self.assertEqual(p.group, group)
439 self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
440
441 self.assertEqual(function.docstring.strip(), """
442curses.window.imaginary([[y1, y2,] x1, x2,] ch, [attr1, attr2, attr3, [attr4, attr5, attr6]])
443 y1
444 Y-coordinate.
445 y2
446 Y-coordinate.
447 x1
448 X-coordinate.
449 x2
450 X-coordinate.
451 ch
452 Character to add.
453 attr1
454 Attributes for the character.
455 attr2
456 Attributes for the character.
457 attr3
458 Attributes for the character.
459 attr4
460 Attributes for the character.
461 attr5
462 Attributes for the character.
463 attr6
464 Attributes for the character.
465 """.strip())
466
467 def parse_function_should_fail(self, s):
468 with support.captured_stdout() as stdout:
469 with self.assertRaises(SystemExit):
470 self.parse_function(s)
471 return stdout.getvalue()
472
473 def test_disallowed_grouping__two_top_groups_on_left(self):
474 s = self.parse_function_should_fail("""
475module foo
476foo.two_top_groups_on_left
477 [
478 group1 : int
479 ]
480 [
481 group2 : int
482 ]
483 param: int
484 """)
485 self.assertEqual(s,
486 ('Error on line 0:\n'
487 'Function two_top_groups_on_left has an unsupported group configuration. (Unexpected state 2)\n'))
488
489 def test_disallowed_grouping__two_top_groups_on_right(self):
490 self.parse_function_should_fail("""
491module foo
492foo.two_top_groups_on_right
493 param: int
494 [
495 group1 : int
496 ]
497 [
498 group2 : int
499 ]
500 """)
501
502 def test_disallowed_grouping__parameter_after_group_on_right(self):
503 self.parse_function_should_fail("""
504module foo
505foo.parameter_after_group_on_right
506 param: int
507 [
508 [
509 group1 : int
510 ]
511 group2 : int
512 ]
513 """)
514
515 def test_disallowed_grouping__group_after_parameter_on_left(self):
516 self.parse_function_should_fail("""
517module foo
518foo.group_after_parameter_on_left
519 [
520 group2 : int
521 [
522 group1 : int
523 ]
524 ]
525 param: int
526 """)
527
528 def test_disallowed_grouping__empty_group_on_left(self):
529 self.parse_function_should_fail("""
530module foo
531foo.empty_group
532 [
533 [
534 ]
535 group2 : int
536 ]
537 param: int
538 """)
539
540 def test_disallowed_grouping__empty_group_on_right(self):
541 self.parse_function_should_fail("""
542module foo
543foo.empty_group
544 param: int
545 [
546 [
547 ]
548 group2 : int
549 ]
550 """)
551
552 def test_no_parameters(self):
553 function = self.parse_function("""
554module foo
555foo.bar
556
557Docstring
558
559""")
560 self.assertEqual("Docstring\n\nfoo.bar()", function.docstring)
561 self.assertEqual(0, len(function.parameters))
562
Larry Hastingsdfcd4672013-10-27 02:49:39 -0700563 def test_illegal_module_line(self):
564 self.parse_function_should_fail("""
565module foo
566foo.bar => int
567 /
568""")
569
570 def test_illegal_c_basename(self):
571 self.parse_function_should_fail("""
572module foo
573foo.bar as 935
574 /
575""")
576
Larry Hastings31826802013-10-19 00:09:25 -0700577 def test_single_star(self):
578 self.parse_function_should_fail("""
579module foo
580foo.bar
581 *
582 *
583""")
584
585 def test_parameters_required_after_star_without_initial_parameters_or_docstring(self):
586 self.parse_function_should_fail("""
587module foo
588foo.bar
589 *
590""")
591
592 def test_parameters_required_after_star_without_initial_parameters_with_docstring(self):
593 self.parse_function_should_fail("""
594module foo
595foo.bar
596 *
597Docstring here.
598""")
599
600 def test_parameters_required_after_star_with_initial_parameters_without_docstring(self):
601 self.parse_function_should_fail("""
602module foo
603foo.bar
604 this: int
605 *
606""")
607
608 def test_parameters_required_after_star_with_initial_parameters_and_docstring(self):
609 self.parse_function_should_fail("""
610module foo
611foo.bar
612 this: int
613 *
614Docstring.
615""")
616
617 def test_single_slash(self):
618 self.parse_function_should_fail("""
619module foo
620foo.bar
621 /
622 /
623""")
624
625 def test_mix_star_and_slash(self):
626 self.parse_function_should_fail("""
627module foo
628foo.bar
629 x: int
630 y: int
631 *
632 z: int
633 /
634""")
635
636 def test_parameters_not_permitted_after_slash_for_now(self):
637 self.parse_function_should_fail("""
638module foo
639foo.bar
640 /
641 x: int
642""")
643
644 def test_function_not_at_column_0(self):
645 function = self.parse_function("""
646 module foo
647 foo.bar
648 x: int
649 Nested docstring here, goeth.
650 *
651 y: str
652 Not at column 0!
653""")
654 self.assertEqual("""
655Not at column 0!
656
657foo.bar(x, *, y)
658 x
659 Nested docstring here, goeth.
660""".strip(), function.docstring)
661
662 def test_parser_regression_special_character_in_parameter_column_of_docstring_first_line(self):
663 function = self.parse_function("""
664module os
665os.stat
666 path: str
667This/used to break Clinic!
668""")
669 self.assertEqual("This/used to break Clinic!\n\nos.stat(path)", function.docstring)
670
671 def test_directive(self):
672 c = FakeClinic()
673 parser = DSLParser(c)
674 parser.flag = False
675 parser.directives['setflag'] = lambda : setattr(parser, 'flag', True)
676 block = clinic.Block("setflag")
677 parser.parse(block)
678 self.assertTrue(parser.flag)
679
680 def test_legacy_converters(self):
681 block = self.parse('module os\nos.access\n path: "s"')
682 module, function = block.signatures
683 self.assertIsInstance((function.parameters['path']).converter, clinic.str_converter)
684
685 def parse(self, text):
686 c = FakeClinic()
687 parser = DSLParser(c)
688 block = clinic.Block(text)
689 parser.parse(block)
690 return block
691
692 def parse_function(self, text):
693 block = self.parse(text)
694 s = block.signatures
695 assert len(s) == 2
696 assert isinstance(s[0], clinic.Module)
697 assert isinstance(s[1], clinic.Function)
698 return s[1]
699
700 def test_scaffolding(self):
701 # test repr on special values
702 self.assertEqual(repr(clinic.unspecified), '<Unspecified>')
703 self.assertEqual(repr(clinic.NULL), '<Null>')
704
705 # test that fail fails
706 with support.captured_stdout() as stdout:
707 with self.assertRaises(SystemExit):
708 clinic.fail('The igloos are melting!', filename='clown.txt', line_number=69)
709 self.assertEqual(stdout.getvalue(), 'Error in file "clown.txt" on line 69:\nThe igloos are melting!\n')
710
711
712if __name__ == "__main__":
713 unittest.main()