Home | History | Annotate | Download | only in clinic
      1 # Argument Clinic
      2 # Copyright 2012-2013 by Larry Hastings.
      3 # Licensed to the PSF under a contributor agreement.
      4 #
      5 
      6 import clinic
      7 from clinic import DSLParser
      8 import collections
      9 import inspect
     10 from test import support
     11 import sys
     12 import unittest
     13 from unittest import TestCase
     14 
     15 
     16 class FakeConverter:
     17     def __init__(self, name, args):
     18         self.name = name
     19         self.args = args
     20 
     21 
     22 class 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 
     30 class 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 
     37 clinic.Clinic.presets_text = ''
     38 c = clinic.Clinic(language='C')
     39 
     40 class 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 
     86 class 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 
    107 class 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 
    155 class 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 
    207 class InertParser:
    208     def __init__(self, clinic):
    209         pass
    210 
    211     def parse(self, block):
    212         pass
    213 
    214 class CopyParser:
    215     def __init__(self, clinic):
    216         pass
    217 
    218     def parse(self, block):
    219         block.output = block.input
    220 
    221 
    222 class 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]
    246 abc
    247 [inert]*/
    248 def
    249 /*[inert checksum: 7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/
    250 xyz
    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]
    266 def
    267 [copy start generated code]*/
    268 abc
    269 /*[copy end generated code: output=03cfd743661f0797 input=7b18d017f89f61cf]*/
    270 xyz
    271 """, """
    272     verbatim text here
    273     lah dee dah
    274 /*[copy input]
    275 def
    276 [copy start generated code]*/
    277 def
    278 /*[copy end generated code: output=7b18d017f89f61cf input=7b18d017f89f61cf]*/
    279 xyz
    280 """)
    281 
    282 
    283 class 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("""
    327 module os
    328 os.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("""
    337 module os
    338 os.access
    339     follow_symlinks: bool = True
    340     something_else: str""")
    341         self.assertEqual(s, """Error on line 0:
    342 Can't have a parameter without a default ('something_else')
    343 after 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("""
    353 module os
    354 os.stat as os_stat_fn
    355 
    356    path: str
    357        Path to be examined
    358 
    359 Perform a stat system call on the given path.""")
    360         self.assertEqual("""
    361 stat($module, /, path)
    362 --
    363 
    364 Perform 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("""
    372 module foo
    373 foo.bar
    374   x: int
    375      Documentation for x.
    376   y: int
    377 
    378 This is the documentation for foo.
    379 
    380 Okay, we're done here.
    381 """)
    382         self.assertEqual("""
    383 bar($module, /, x, y)
    384 --
    385 
    386 This is the documentation for foo.
    387 
    388   x
    389     Documentation for x.
    390 
    391 Okay, 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("""
    396 module os
    397 os.stat
    398     path: str
    399 This/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("""
    424 module curses
    425 curses.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(), """
    449 addch([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("""
    464 module curses
    465 curses.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(), """
    510 imaginary([[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("""
    546 module foo
    547 foo.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("""
    562 module foo
    563 foo.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("""
    575 module foo
    576 foo.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("""
    588 module foo
    589 foo.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("""
    601 module foo
    602 foo.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("""
    613 module foo
    614 foo.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("""
    625 module foo
    626 foo.bar
    627 
    628 Docstring
    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("""
    636 module foo
    637 class foo.Bar "unused" "notneeded"
    638 foo.Bar.__init__
    639 
    640 Docstring
    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("""
    650 module foo
    651 foo.bar => int
    652     /
    653 """)
    654 
    655     def test_illegal_c_basename(self):
    656         self.parse_function_should_fail("""
    657 module foo
    658 foo.bar as 935
    659     /
    660 """)
    661 
    662     def test_single_star(self):
    663         self.parse_function_should_fail("""
    664 module foo
    665 foo.bar
    666     *
    667     *
    668 """)
    669 
    670     def test_parameters_required_after_star_without_initial_parameters_or_docstring(self):
    671         self.parse_function_should_fail("""
    672 module foo
    673 foo.bar
    674     *
    675 """)
    676 
    677     def test_parameters_required_after_star_without_initial_parameters_with_docstring(self):
    678         self.parse_function_should_fail("""
    679 module foo
    680 foo.bar
    681     *
    682 Docstring here.
    683 """)
    684 
    685     def test_parameters_required_after_star_with_initial_parameters_without_docstring(self):
    686         self.parse_function_should_fail("""
    687 module foo
    688 foo.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("""
    695 module foo
    696 foo.bar
    697     this: int
    698     *
    699 Docstring.
    700 """)
    701 
    702     def test_single_slash(self):
    703         self.parse_function_should_fail("""
    704 module foo
    705 foo.bar
    706     /
    707     /
    708 """)
    709 
    710     def test_mix_star_and_slash(self):
    711         self.parse_function_should_fail("""
    712 module foo
    713 foo.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("""
    723 module foo
    724 foo.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("""
    740 bar($module, /, x, *, y)
    741 --
    742 
    743 Not 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 
    790 if __name__ == "__main__":
    791     unittest.main()
    792