Commits

larry committed 932e986

Added prototype changes to Argument Clinic allowing redirecting its output.
See CLINIC.BUFFER.NOTES.TXT to learn how to experiment.

Comments (0)

Files changed (2)

CLINIC.BUFFER.NOTES.TXT

+Notes on experimenting with Clinic with buffers.
+
+There are three new "directives" for you to use.  Directives are like pragmas,
+they're special preprocessor-time live commands you can use in "clinic" clocks.
+In this example:
+    /*[clinic input]
+    module os
+    class foo
+    ...
+
+"module" and "class" are directives.
+
+------------------------------------------------------------------------------
+
+The first new directive is "output":
+    output <field> <destination>
+This lets you configure where Clinic writes a particular field.
+A "field" is a subsection of Clinic's output--more on those in
+a paragraph or two.  And "destination" is a place where you can
+send text.
+
+There are four built-in destinations:
+    block
+        Appends this field into the "output" area of the current block.
+
+    buffer
+        Appends this field to the "buffer", a text buffer that you can
+        dump into the current file using another new directive.
+
+    side
+        Appends this file to the "side file", a file that will be created
+        at the end of processing.  The file is named
+            {basename}.side{extension}
+        for example, if you are processing "_csv.c", the side file will be
+        named "_csv.side.c".
+
+    suppress
+        Suppresses the text--throws it away.
+
+There are seven "fields".  All the names are of the form "<a>_<b>",
+where "<a>" is the semantic object represented (the parsing function,
+the impl function, or the methoddef structure) and "<b>" represents
+what kind of statement the field is.  Field names that end in "_prototype"
+represent forward declarations of that thing, without the actual body/data
+of the thing; field names that end in "_definition" represent the actual
+definition of the thing, with the body/data of the thing.  ("methoddef"
+is special, it's the only one that ends with "_define" representing that
+it is a preprocessor #define.)
+
+Here then are the seven fields with their default destinations:
+
+    docstring_prototype = suppress
+    docstring_definition = block
+    impl_prototype = suppress
+    methoddef_define = block
+    parser_prototype = block
+    parser_definition = buffer
+    impl_definition = block
+
+Note that the order here is important, and is preserved whenever the
+fields are output.
+
+There's also a special eighth field name, "everything", which
+sets all the fields to the same destination.
+
+------------------------------------------------------------------------------
+
+The second new directive is "destination":
+    destination <name> <command> [...]
+
+This performs an operation on the buffer with name "name".
+
+There are two defined commands: "new" and "clear".
+
+The "new" command looks like this:
+    destination <name> new <type>
+This creates a new destination with name "<name>" and type "<type>".
+There are four destination types:
+    "suppress" - throws the text away.
+    "block" - writes the text to the block.
+    "buffer" - a simple text buffer, like the "buffer" destination above.
+    "file" - a text file.  The file destination takes an extra argument,
+        a template to use for building the filename, like so:
+            destination <name> new <type> <file_template>
+        The template can use three strings internally that will be replaced
+        by bits of the filename:
+            {filename} - the full filename
+            {basename} - everything up to but not including the last '.'
+            {extension} - the last '.' and everything after it
+        (If there are no periods in the filename, {basename} and {filename}
+        are the same, and {extension} is empty.  "{basename}{extension}"
+        is always exactly the same as "{filename}".")
+
+The "clear" command looks like this:
+    destination <name> clear
+It removes all the accumulated text up to this point in the destination.
+
+------------------------------------------------------------------------------
+
+The third new directive is "dump":
+    dump <name>
+This dumps out the current contents of the buffer by that name into the output
+section of the current clinic block.  This also empties the buffer.
+
+If a buffer is not empty at the end of processing a file, Clinic will complain,
+and append a
+    /*[clinic input]
+    dump <name of non-empty buffer>
+    [clinic start generated code]*/
+
+blob to the end of the file.
+
+------------------------------------------------------------------------------
+
+There have been a number of proposals on how to arrange the output.
+Here are the three I'm aware of, along with recipes on how you could
+produce it with this prototype.
+
+-----
+
+First, some people want to write everything to a separate file, which
+would presumably then be #included into the main file.  To try that,
+add this to the top of your file:
+    /*[clinic input]
+    output everything side
+    output docstring_prototype suppress
+    output parser_prototype suppress
+    output impl_definition block
+    [clinic start generated code]*/
+
+Then add
+    #include "<basename>.side<extension>"
+somewhere near the top of the file.  For example, if you tried this
+with "posixmodule.c", you'd add
+    #include "posixmodule.side.c"
+somewhere near the top of the file.
+
+Note that this is unlikely to happen, as Guido hates separate output
+files like this.
+
+-----
+
+Second, some people want to buffer up all the output, then write it all out
+in one blob near the end of the file.  To do that, add this to the top
+of your file:
+
+    /*[clinic input]
+    output everything buffer
+    output docstring_prototype suppress
+    output impl_prototype suppress
+    output parser_prototype suppress
+    output impl_definition block
+    [clinic start generated code]*/
+
+Then, near the end of your file, just above the methoddef
+structures
+
+    /*[clinic input]
+    dump buffer
+    [clinic start generated code]*/
+
+This won't work on every file, because some files files have methoddef
+structures in the middle of the file, and I believe some files reuse
+parser functions.
+
+However, this would work for most files, and perhaps we could fix
+the broken files on a file-by-file basis, either by adjusting the
+destinations for fields just for that file, or by dumping the
+buffer multiple times.
+
+-----
+
+Third, here's my alternate take on buffering up all the output.
+
+    /*[clinic input]
+    output everything buffer
+    output docstring_prototype block
+    output impl_prototype suppress
+    output parser_prototype block
+    output impl_definition block
+    [clinic start generated code]*/
+
+This should work for every file.
+
+------------------------------------------------------------------------------
+
+This prototype isn't ready for check-in.  The "file" destination is a little
+hacked up; there should probably be some sort of header to the file, and the
+checksum of the old output file should be checked before it is overwritten.
+
+Also, I don't propose to check in support for the "file" destination without
+Guido's explicit permission, and I doubt he will ever give it.

Tools/clinic/clinic.py

     return append, output
 
 
-def fail(*args, filename=None, line_number=None):
+def warn_or_fail(fail=False, *args, filename=None, line_number=None):
     joined = " ".join([str(a) for a in args])
     add, output = text_accumulator()
-    add("Error")
+    if fail:
+        add("Error")
+    else:
+        add("Warning")
     if clinic:
         if filename is None:
             filename = clinic.filename
     add(':\n')
     add(joined)
     print(output())
-    sys.exit(-1)
-
+    if fail:
+        sys.exit(-1)
+
+
+def warn(*args, filename=None, line_number=None):
+    return warn_or_fail(False, *args, filename=filename, line_number=line_number)
+
+def fail(*args, filename=None, line_number=None):
+    return warn_or_fail(True, *args, filename=filename, line_number=line_number)
 
 
 def quoted_for_c_string(s):
     checksum_line = ""
 
     @abc.abstractmethod
-    def render(self, block):
+    def render(self, clinic, signatures):
         pass
 
     def validate(self):
     stop_line     = "[{dsl_name} start generated code]*/"
     checksum_line = "/*[{dsl_name} end generated code: checksum={checksum}]*/"
 
-    def render(self, signatures):
+    def render(self, clinic, signatures):
         function = None
         for o in signatures:
             if isinstance(o, Function):
                 if function:
                     fail("You may specify at most one function per block.\nFound a block containing at least two:\n\t" + repr(function) + " and " + repr(o))
                 function = o
-        return self.render_function(function)
+        return self.render_function(clinic, function)
 
     def docstring_for_c_string(self, f):
         text, add, output = _text_accumulator()
         add('"')
         return ''.join(text)
 
-    impl_prototype_template = "{c_basename}_impl({impl_parameters})"
-
-    @staticmethod
-    def template_base(*args):
-        flags = '|'.join(f for f in args if f)
-        return """
+    def output_templates(self, f):
+        parameters = list(f.parameters.values())
+        converters = [p.converter for p in parameters]
+
+        has_option_groups = parameters and (parameters[0].group or parameters[-1].group)
+        default_return_converter = (not f.return_converter or
+            f.return_converter.type == 'PyObject *')
+        positional = parameters and (parameters[-1].kind == inspect.Parameter.POSITIONAL_ONLY)
+
+        # we have to set seven things before we're done:
+        #
+        # docstring_prototype
+        # docstring_definition
+        # impl_prototype
+        # methoddef_define
+        # parser_prototype
+        # parser_definition
+        # impl_definition
+        #
+        # since impl_prototype is always just impl_definition + ';'
+        # we just define impl_definition at the top
+
+        docstring_prototype = "PyDoc_VAR({c_basename}__doc__);"
+
+        docstring_definition = """
 PyDoc_STRVAR({c_basename}__doc__,
 {docstring});
-
-#define {methoddef_name}    \\
-    {{"{name}", (PyCFunction){c_basename}, {methoddef_flags}, {c_basename}__doc__}},
-""".replace('{methoddef_flags}', flags)
-
-    def meth_noargs_template(self, methoddef_flags=""):
-        return self.template_base("METH_NOARGS", methoddef_flags) + """
+""".strip()
+
+        impl_definition = """
 static {impl_return_type}
-{impl_prototype};
-
+{c_basename}_impl({impl_parameters})""".strip()
+
+        impl_prototype = parser_prototype = parser_definition = None
+
+        if not parameters:
+            # no parameters, METH_NOARGS
+
+            flags = "METH_NOARGS"
+
+            parser_prototype = """
 static PyObject *
 {c_basename}({self_type}{self_name}, PyObject *Py_UNUSED(ignored))
+""".strip()
+
+            parser_definition = parser_prototype + """
 {{
     PyObject *return_value = NULL;
     {declarations}
     {cleanup}
     return return_value;
 }}
-
-static {impl_return_type}
-{impl_prototype}
-"""
-
-    def meth_o_template(self, methoddef_flags=""):
-        return self.template_base("METH_O", methoddef_flags) + """
+""".rstrip()
+
+        elif (len(parameters) == 1 and
+              parameters[0].kind == inspect.Parameter.POSITIONAL_ONLY and
+              not converters[0].is_optional() and
+              isinstance(converters[0], object_converter) and
+              converters[0].format_unit == 'O'):
+            if default_return_converter:
+                # maps perfectly to METH_O, doesn't need a return converter,
+                # so we skip the parse function and call
+                # directly into the impl function
+
+                # SLIGHT HACK
+                # METH_O uses {impl_parameters} for the parser.
+
+                flags = "METH_O"
+
+                impl_definition = """
 static PyObject *
 {c_basename}({impl_parameters})
-"""
-
-    def meth_o_return_converter_template(self, methoddef_flags=""):
-        return self.template_base("METH_O", methoddef_flags) + """
-static {impl_return_type}
-{impl_prototype};
-
+""".strip()
+
+                impl_prototype = parser_prototype = parser_definition = ''
+
+            else:
+                # SLIGHT HACK
+                # METH_O uses {impl_parameters} for the parser.
+
+                flags = "METH_O"
+
+                parser_prototype = """
 static PyObject *
 {c_basename}({impl_parameters})
+""".strip()
+
+                parser_definition = parser_prototype + """
 {{
     PyObject *return_value = NULL;
     {declarations}
     {cleanup}
     return return_value;
 }}
-
-static {impl_return_type}
-{impl_prototype}
-"""
-
-    def option_group_template(self, methoddef_flags=""):
-        return self.template_base("METH_VARARGS", methoddef_flags) + """
-static {impl_return_type}
-{impl_prototype};
-
+""".rstrip()
+
+        elif has_option_groups:
+            # positional parameters with option groups
+            # (we have to generate lots of PyArg_ParseTuple calls
+            #  in a big switch statement)
+
+            flags = "METH_VARARGS"
+
+            parser_prototype = """
 static PyObject *
 {c_basename}({self_type}{self_name}, PyObject *args)
+""".strip()
+
+            parser_definition = parser_prototype + """
 {{
     PyObject *return_value = NULL;
     {declarations}
     {cleanup}
     return return_value;
 }}
-
-static {impl_return_type}
-{impl_prototype}
-"""
-
-    def keywords_template(self, methoddef_flags=""):
-        return self.template_base("METH_VARARGS|METH_KEYWORDS", methoddef_flags) + """
-static {impl_return_type}
-{impl_prototype};
-
+""".rstrip()
+
+        elif positional:
+            # positional-only, but no option groups
+            # we only need one call to PyArg_ParseTuple
+            flags = "METH_VARARGS"
+
+            parser_prototype = """
+static PyObject *
+{c_basename}({self_type}{self_name}, PyObject *args)
+""".strip()
+
+            parser_definition = parser_prototype + """
+{{
+    PyObject *return_value = NULL;
+    {declarations}
+    {initializers}
+
+    if (!PyArg_ParseTuple(args,
+        "{format_units}:{name}",
+        {parse_arguments}))
+        goto exit;
+    {return_value} = {c_basename}_impl({impl_arguments});
+    {return_conversion}
+
+exit:
+    {cleanup}
+    return return_value;
+}}
+""".rstrip()
+
+        else:
+            # positional-or-keyword arguments
+            flags = "METH_VARARGS|METH_KEYWORDS"
+
+            parser_prototype = """
 static PyObject *
 {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs)
+""".strip()
+
+            parser_definition = parser_prototype + """
 {{
     PyObject *return_value = NULL;
     static char *_keywords[] = {{{keywords}, NULL}};
     {return_value} = {c_basename}_impl({impl_arguments});
     {return_conversion}
 
-{exit_label}
+exit:
     {cleanup}
     return return_value;
 }}
-
-static {impl_return_type}
-{impl_prototype}
-"""
-
-    def positional_only_template(self, methoddef_flags=""):
-        return self.template_base("METH_VARARGS", methoddef_flags) + """
-static {impl_return_type}
-{impl_prototype};
-
-static PyObject *
-{c_basename}({self_type}{self_name}, PyObject *args)
-{{
-    PyObject *return_value = NULL;
-    {declarations}
-    {initializers}
-
-    if (!PyArg_ParseTuple(args,
-        "{format_units}:{name}",
-        {parse_arguments}))
-        goto exit;
-    {return_value} = {c_basename}_impl({impl_arguments});
-    {return_conversion}
-
-{exit_label}
-    {cleanup}
-    return return_value;
-}}
-
-static {impl_return_type}
-{impl_prototype}
-"""
+""".rstrip()
+
+        if f.methoddef_flags:
+            if flags:
+                flags += '|'
+            flags += f.methoddef_flags
+
+        methoddef_define = """
+#define {methoddef_name}    \\
+    {{"{name}", (PyCFunction){c_basename}, {methoddef_flags}, {c_basename}__doc__}},
+""".strip().replace('{methoddef_flags}', flags)
+
+        # parser_prototype mustn't be None, but it could be an empty string.
+        assert parser_prototype is not None
+        assert not parser_prototype.endswith(';')
+
+        if parser_prototype:
+            parser_prototype += ';'
+
+        assert impl_definition
+        if impl_prototype is None:
+            impl_prototype = impl_definition + ";"
+
+        d = {
+            "docstring_prototype" : docstring_prototype,
+            "docstring_definition" : docstring_definition,
+            "impl_prototype" : impl_prototype,
+            "methoddef_define" : methoddef_define,
+            "parser_prototype" : parser_prototype,
+            "parser_definition" : parser_definition,
+            "impl_definition" : impl_definition,
+        }
+
+        d2 = {}
+        for name, value in d.items():
+            if value:
+                value = '\n' + value + '\n'
+            d2[name] = value
+        return d2
 
     @staticmethod
     def group_to_variable_name(group):
         add("}}")
         template_dict['option_group_parsing'] = output()
 
-    def render_function(self, f):
+    def render_function(self, clinic, f):
         if not f:
             return ""
 
             if has_option_groups:
                 assert positional
 
+        # HACK
+        # when we're METH_O, but have a custom
+        # return converter, we use
+        # "impl_parameters" for the parsing
+        # function because that works better.
+        # but that means we must supress actually
+        # declaring the impl's parameters as variables
+        # in the parsing function.  but since it's
+        # METH_O, we only have one anyway, so we don't
+        # have any problem finding it.
+        default_return_converter = (not f.return_converter or
+            f.return_converter.type == 'PyObject *')
+        if (len(parameters) == 1 and
+              parameters[0].kind == inspect.Parameter.POSITIONAL_ONLY and
+              not converters[0].is_optional() and
+              isinstance(converters[0], object_converter) and
+              converters[0].format_unit == 'O' and
+              not default_return_converter):
+
+            data.declarations.pop(0)
+
         # now insert our "self" (or whatever) parameters
         # (we deliberately don't call render on self converters)
         stock_self = self_converter('self', f)
         template_dict['cleanup'] = "".join(data.cleanup)
         template_dict['return_value'] = data.return_value
 
-        template_dict['impl_prototype'] = self.impl_prototype_template.format_map(template_dict)
-
-        default_return_converter = (not f.return_converter or
-            f.return_converter.type == 'PyObject *')
-
-        if not parameters:
-            template = self.meth_noargs_template(f.methoddef_flags)
-        elif (len(parameters) == 1 and
-              parameters[0].kind == inspect.Parameter.POSITIONAL_ONLY and
-              not converters[0].is_optional() and
-              isinstance(converters[0], object_converter) and
-              converters[0].format_unit == 'O'):
-            if default_return_converter:
-                template = self.meth_o_template(f.methoddef_flags)
-            else:
-                # HACK
-                # we're using "impl_parameters" for the
-                # non-impl function, because that works
-                # better for METH_O.  but that means we
-                # must supress actually declaring the
-                # impl's parameters as variables in the
-                # non-impl.  but since it's METH_O, we
-                # only have one anyway, so
-                # we don't have any problem finding it.
-                declarations_copy = list(data.declarations)
-                before, pyobject, after = declarations_copy[0].partition('PyObject *')
-                assert not before, "hack failed, see comment"
-                assert pyobject, "hack failed, see comment"
-                assert after and after[0].isalpha(), "hack failed, see comment"
-                del declarations_copy[0]
-                template_dict['declarations'] = "\n".join(declarations_copy)
-                template = self.meth_o_return_converter_template(f.methoddef_flags)
-        elif has_option_groups:
+        if has_option_groups:
             self.render_option_group_parsing(f, template_dict)
-            template = self.option_group_template(f.methoddef_flags)
+
+        templates = self.output_templates(f)
+
+        for name, destination in clinic.field_destinations.items():
+            template = templates[name]
+            if has_option_groups:
+                template = linear_format(template,
+                        option_group_parsing=template_dict['option_group_parsing'])
             template = linear_format(template,
-                option_group_parsing=template_dict['option_group_parsing'])
-        elif positional:
-            template = self.positional_only_template(f.methoddef_flags)
-        else:
-            template = self.keywords_template(f.methoddef_flags)
-
-        template = linear_format(template,
-            declarations=template_dict['declarations'],
-            return_conversion=template_dict['return_conversion'],
-            initializers=template_dict['initializers'],
-            cleanup=template_dict['cleanup'],
-            )
-
-        # Only generate the "exit:" label
-        # if we have any gotos
-        need_exit_label = "goto exit;" in template
-        template = linear_format(template,
-            exit_label="exit:" if need_exit_label else ''
-            )
-
-        return template.format_map(template_dict)
+                declarations=template_dict['declarations'],
+                return_conversion=template_dict['return_conversion'],
+                initializers=template_dict['initializers'],
+                cleanup=template_dict['cleanup'],
+                )
+
+            # Only generate the "exit:" label
+            # if we have any gotos
+            need_exit_label = "goto exit;" in template
+            template = linear_format(template,
+                exit_label="exit:" if need_exit_label else ''
+                )
+
+            s = template.format_map(template_dict)
+
+            destination.append(s)
+
+        return clinic.get_destination('block').output()
+
 
 
 @contextlib.contextmanager
         write(self.language.stop_line.format(dsl_name=dsl_name))
         write("\n")
 
-        output = block.output
+        output = ''.join(block.output)
         if output:
+            if not output.endswith('\n'):
+                output += '\n'
             write(output)
-            if not output.endswith('\n'):
-                write('\n')
 
         write(self.language.checksum_line.format(dsl_name=dsl_name, checksum=compute_checksum(output)))
         write("\n")
 
+    def write(self, text):
+        self.f.write(text)
+
+
+class Destination:
+    def __init__(self, name, type, clinic, *args):
+        self.name = name
+        self.type = type
+        self.clinic = clinic
+        valid_types = ('buffer', 'file', 'suppress')
+        if type not in valid_types:
+            fail("Invalid destination type " + repr(type) + " for " + name + " , must be " + ', '.join(valid_types))
+        extra_arguments = 1 if type == "file" else 0
+        if len(args) < extra_arguments:
+            fail("Not enough arguments for destination " + name + " new " + type)
+        if len(args) > extra_arguments:
+            fail("Too many arguments for destination " + name + " new " + type)
+        if type =='file':
+            d = {}
+            d['filename'] = filename = clinic.filename
+            d['basename'], d['extension'] = os.path.splitext(filename)
+            self.filename = args[0].format_map(d)
+
+        self.text, self.append, self.output = _text_accumulator()
+
+    def __repr__(self):
+        return "".join(("<Destination ", self.name, " ", hex(id(self)), ">"))
+    def clear(self):
+        if self.type != 'buffer':
+            fail("Can't clear destination" + self.name + " , it's not of type buffer")
+        self.text.clear()
+
 
 # maps strings to Language objects.
 # "languages" maps the name of the language ("C", "Python").
         self.modules = collections.OrderedDict()
         self.classes = collections.OrderedDict()
 
+        self.destinations = {}
+        self.add_destination("block", "buffer")
+        self.add_destination("suppress", "suppress")
+        self.add_destination("buffer", "buffer")
+        self.add_destination("side", "file", "{basename}.side{extension}")
+
+        d = self.destinations.get
+        self.field_destinations = collections.OrderedDict((
+            ('docstring_prototype', d('suppress')),
+            ('docstring_definition', d('block')),
+            ('methoddef_define', d('block')),
+            ('impl_prototype', d('block')),
+            ('parser_prototype', d('suppress')),
+            ('parser_definition', d('block')),
+            ('impl_definition', d('block')),
+        ))
+
         global clinic
         clinic = self
 
+    def get_destination(self, name):
+        d = self.destinations.get(name)
+        if not d:
+            fail("Destination does not exist: " + repr(name))
+        return d
+
+    def add_destination(self, name, type, *args):
+        if name in self.destinations:
+            fail("Destination already exists: " + repr(name))
+        self.destinations[name] = Destination(name, type, self, *args)
+
     def parse(self, input):
         printer = self.printer
         self.block_parser = BlockParser(input, self.language, verify=self.verify)
                 parser = self.parsers[dsl_name]
                 parser.parse(block)
             printer.print_block(block)
+        for name, destination in self.destinations.items():
+            if destination.type == 'suppress':
+                continue
+            output = destination.output()
+            if output:
+                checksum = compute_checksum(output)
+                checksum_line = self.language.checksum_line.format(dsl_name="{dsl_name}", checksum=checksum)
+                if destination.type == 'buffer':
+                    warn("Destination buffer " + repr(name) + " not empty at end of file, emptying." + "\n" + repr(output))
+
+
+                    a, o = text_accumulator()
+
+                    a('\n')
+                    a(self.language.start_line.format(dsl_name='clinic'))
+                    a('\n')
+
+                    a('dump ')
+                    a(name)
+                    a('\n')
+
+                    a(self.language.stop_line.format(dsl_name='clinic'))
+                    a('\n')
+
+                    printer.write(o())
+                    printer.write(output)
+
+                    a(checksum_line.format(dsl_name='clinic'))
+                    a('\n')
+
+                    printer.write(o())
+                    continue
+
+                if destination.type == 'file':
+                    with open(destination.filename, "wt") as f:
+                        f.write(output)
+                    continue
         return printer.f.getvalue()
 
     def _module_and_class(self, fields):
         self.indent = IndentStack()
         self.kind = CALLABLE
         self.coexist = False
+        o = self.clinic.get_destination('block')
+
 
     def directive_version(self, required):
         global version
             module.classes[name] = c
         self.block.signatures.append(c)
 
+    def directive_destination(self, name, command, *args):
+        if command is 'new':
+            self.clinic.add_destination(name, command, *args)
+            return
+
+        if command is 'clear':
+            self.clinic.get_destination(name).clear()
+
+    def directive_output(self, field, destination):
+        d = self.clinic.get_destination(destination)
+        fd = self.clinic.field_destinations
+
+        if field == "everything":
+            for name in list(fd):
+                fd[name] = d
+            return
+
+        if field not in fd:
+            fail("Invalid field " + repr(field) + ", must be one of:\n  " + ", ".join(valid_fields))
+        fd[field] = d
+
+    def directive_dump(self, name):
+        self.block.output.append(self.clinic.get_destination(name).output())
+
     def at_classmethod(self):
         assert self.kind is CALLABLE
         self.kind = CLASS_METHOD
         assert self.coexist == False
         self.coexist = True
 
-
     def parse(self, block):
         self.reset()
         self.block = block
+        block.output = []
         block_start = self.clinic.block_parser.line_number
         lines = block.input.split('\n')
         for line_number, line in enumerate(lines, self.clinic.block_parser.block_start_line_number):
         self.next(self.state_terminal)
         self.state(None)
 
-        block.output = self.clinic.language.render(block.signatures)
+        block.output.extend(self.clinic.language.render(clinic, block.signatures))
 
     @staticmethod
     def ignore_line(line):