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