def chain(args):
    """
    %prog chain gffile > chained.gff

    Fill in parent features by chaining child features and return extent of the
    child coordinates.
    """
    valid_merge_op = ('sum', 'min', 'max', 'mean', 'collapse')

    p = OptionParser(chain.__doc__)
    p.add_option("--key", dest="attrib_key", default=None,
                help="Attribute to use as `key` for chaining operation")
    p.add_option("--chain_ftype", default="cDNA_match",
                help="GFF feature type to use for chaining operation")
    p.add_option("--parent_ftype", default=None,
                help="GFF feature type to use for the chained coordinates")
    p.add_option("--break", dest="break_chain", action="store_true",
                help="Break long chains which are non-contiguous")
    p.add_option("--transfer_attrib", dest="attrib_list",
                help="Attributes to transfer to parent feature; accepts comma" + \
                " separated list of attribute names [default: %default]")
    p.add_option("--transfer_score", dest="score_merge_op", choices=valid_merge_op,
                help="Transfer value stored in score field to parent feature." + \
                " Score is reported based on chosen operation")
    p.set_outfile()

    opts, args = p.parse_args(args)

    if len(args) != 1:
        sys.exit(not p.print_help())

    gffile, = args
    attrib_key = opts.attrib_key
    attrib_list = opts.attrib_list
    score_merge_op = opts.score_merge_op
    break_chain = opts.break_chain

    chain_ftype = opts.chain_ftype
    parent_ftype = opts.parent_ftype if opts.parent_ftype else chain_ftype

    gffdict = {}
    fw = must_open(opts.outfile, "w")
    gff = Gff(gffile)
    if break_chain:
        ctr, prev_gid = dict(), None
    for g in gff:
        if g.type != chain_ftype:
            print(g, file=fw)
            continue

        id = g.accn
        gid = id
        if attrib_key:
            assert attrib_key in g.attributes.keys(), \
                "Attribute `{0}` not present in GFF3".format(attrib_key)
            gid = g.get_attr(attrib_key)
        curr_gid = gid
        if break_chain:
            if prev_gid != curr_gid:
                if curr_gid not in ctr:
                    ctr[curr_gid] = 0
                else:
                    ctr[curr_gid] += 1
            gid = "{0}:{1}".format(gid, ctr[curr_gid])
        gkey = (g.seqid, gid)
        if gkey not in gffdict:
            gffdict[gkey] = { 'seqid': g.seqid,
                            'source': g.source,
                            'strand': g.strand,
                            'type': parent_ftype,
                            'coords': [],
                            'children': [],
                            'score': [],
                            'attrs': DefaultOrderedDict(set)
                          }
            gffdict[gkey]['attrs']['ID'].add(gid)

        if attrib_list:
            for a in attrib_list.split(","):
                if a in g.attributes:
                    [gffdict[gkey]['attrs'][a].add(x) for x in g.attributes[a]]
                    del g.attributes[a]

        if break_chain:
            _attrib = "Alias" if attrib_list and ("Name" not in attrib_list) else "Name"
            gffdict[gkey]['attrs'][_attrib].add(curr_gid)

        gffdict[gkey]['coords'].append((g.start, g.end))
        if score_merge_op:
            if is_number(g.score):
                gffdict[gkey]['score'].append(float(g.score))
                g.score = "."

        g.attributes["Parent"] = [gid]
        g.attributes["ID"] = ["{0}-{1}".\
                format(gid, len(gffdict[gkey]['children']) + 1)]
        g.type = valid_gff_parent_child[g.type]
        g.update_attributes()
        gffdict[gkey]['children'].append(g)
        if break_chain:
            prev_gid = curr_gid

    for gkey, v in sorted(gffdict.items()):
        gseqid, key = gkey
        seqid = v['seqid']
        source = v['source']
        type = v['type']
        strand = v['strand']
        start, stop = range_minmax(gffdict[gkey]['coords'])

        score = "."
        if score_merge_op:
            v['score'].sort()
            if score_merge_op == "sum":
                score = sum(v['score'])
            elif score_merge_op == "min":
                score = min(v['score'])
            elif score_merge_op == "max":
                score = max(v['score'])
            elif score_merge_op == "mean":
                score = sum(v['score'], 0.0)/len(v['score'])
            elif score_merge_op == "collapse":
                score = ",".join((str(x) for x in v['score']))

        g = GffLine("\t".join(str(x) for x in [seqid, source, type, start, stop, \
            score, strand, ".", None]))
        g.attributes = v['attrs']
        g.update_attributes()

        print(g, file=fw)

        for child in gffdict[gkey]['children']:
            print(child, file=fw)

    fw.close()
%prog chain gffile > chained.gff

    Fill in parent features by chaining child features and return extent of the
    child coordinates.