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