Iterate over B6/M8 file and return B6/M8 entries

        Args:
            start_line (str): Next B6/M8 entry. If 'handle' has been partially
                read and you want to start iterating at the next entry, read 
                the next B6/M8 entry and pass it to this variable when calling 
                b6_iter. See 'Examples' for proper usage.

            header (list): List of custom format specifiers if B6 file not in 
                default Blast+ 6 format

            comments (bool): Yields comments if True, else skips lines starting
                with "#"

        Yields:
            B6Entry: class containing all B6/M8 data

        Examples:
            The following two examples demonstrate how to use b6_iter.
            Note: These doctests will not pass, examples are only in doctest
            format as per convention. bio_utils uses pytests for testing.

            >>> for entry in b6_iter(open('test.b6out')):
            ...     print(entry.query)  # Query ID
            ...     print(entry.subject)  # Subject ID
            ...     print(entry.identity)  # Percent identity between seqs
            ...     print(entry.mismatches)  # Number mismatches in alignment
            ...     print(entry.gaps)  # Number gaps in alignment
            ...     print(entry.query_start)  # Start of alignment on query
            ...     print(entry.query_end)  # End of alignment on query
            ...     print(entry.subject_start)  # Start of align on subject
            ...     print(entry.subject_end)  # End of alignment on subject
            ...     print(entry.evalue)  # E-value of alignment
            ...     print(entry.bitscore)  # Bitscore of alignment
            ...     print(entry.write())  # Reconsitituted B6 entry

            >>> b6_handle = open('test.b6out')
            >>> next(b6_handle)  # Skip first line/entry
            >>> next_line = next(b6_handle)  # Store next entry
            >>> for entry in b6_iter(b6_handle, start_line=next_line):
            ...     print(entry.query)  # Query ID
            ...     print(entry.subject)  # Subject ID
            ...     print(entry.identity)  # Percent identity between seqs
            ...     print(entry.mismatches)  # Number mismatches in alignment
            ...     print(entry.gaps)  # Number gaps in alignment
            ...     print(entry.query_start)  # Start of alignment on query
            ...     print(entry.query_end)  # End of alignment on query
            ...     print(entry.subject_start)  # Start of align on subject
            ...     print(entry.subject_end)  # End of alignment on subject
            ...     print(entry.evalue)  # E-value of alignment
            ...     print(entry.bitscore)  # Bitscore of alignment
            ...     print(entry.write())  # Reconstituted B6 entry

def iterate(self, start_line=None, header: list=['qaccver', 'saccver', 
        'pident', 'length', 'mismatch', 'gapopen', 'qstart', 'qend', 'sstart',
        'send', 'evalue', 'bitscore'], comments: bool=False):
        """Iterate over B6/M8 file and return B6/M8 entries

        Args:
            start_line (str): Next B6/M8 entry. If 'handle' has been partially
                read and you want to start iterating at the next entry, read 
                the next B6/M8 entry and pass it to this variable when calling 
                b6_iter. See 'Examples' for proper usage.

            header (list): List of custom format specifiers if B6 file not in 
                default Blast+ 6 format

            comments (bool): Yields comments if True, else skips lines starting
                with "#"

        Yields:
            B6Entry: class containing all B6/M8 data

        Examples:
            The following two examples demonstrate how to use b6_iter.
            Note: These doctests will not pass, examples are only in doctest
            format as per convention. bio_utils uses pytests for testing.

            >>> for entry in b6_iter(open('test.b6out')):
            ...     print(entry.query)  # Query ID
            ...     print(entry.subject)  # Subject ID
            ...     print(entry.identity)  # Percent identity between seqs
            ...     print(entry.mismatches)  # Number mismatches in alignment
            ...     print(entry.gaps)  # Number gaps in alignment
            ...     print(entry.query_start)  # Start of alignment on query
            ...     print(entry.query_end)  # End of alignment on query
            ...     print(entry.subject_start)  # Start of align on subject
            ...     print(entry.subject_end)  # End of alignment on subject
            ...     print(entry.evalue)  # E-value of alignment
            ...     print(entry.bitscore)  # Bitscore of alignment
            ...     print(entry.write())  # Reconsitituted B6 entry

            >>> b6_handle = open('test.b6out')
            >>> next(b6_handle)  # Skip first line/entry
            >>> next_line = next(b6_handle)  # Store next entry
            >>> for entry in b6_iter(b6_handle, start_line=next_line):
            ...     print(entry.query)  # Query ID
            ...     print(entry.subject)  # Subject ID
            ...     print(entry.identity)  # Percent identity between seqs
            ...     print(entry.mismatches)  # Number mismatches in alignment
            ...     print(entry.gaps)  # Number gaps in alignment
            ...     print(entry.query_start)  # Start of alignment on query
            ...     print(entry.query_end)  # End of alignment on query
            ...     print(entry.subject_start)  # Start of align on subject
            ...     print(entry.subject_end)  # End of alignment on subject
            ...     print(entry.evalue)  # E-value of alignment
            ...     print(entry.bitscore)  # Bitscore of alignment
            ...     print(entry.write())  # Reconstituted B6 entry
        """

        handle = self.handle

        # Speed tricks: reduces function calls
        split = str.split
        strip = str.strip

        # Map attribute names to default format specifier names
        def_map = {'query_end': ('qend', str), 
                   'mismatches': ('mismatch', int), 
                   'identity': ('pident', float), 
                   'query': ('qaccver', str),
                   'query_start': ('qstart', int), 
                   'subject_start': ('sstart', int), 
                   'bitscore': ('bitscore', float), 
                   'evalue': ('evalue', float), 
                   'gaps': ('gapopen', int), 
                   'subject_end': ('send', int),
                   'length': ('length', int), 
                   'subject': ('saccver', str)
                  }

        def_map_rev = {j[0]: k for k, j in def_map.items()}
        def_specs = list(def_map_rev.keys())

        uheader = list(OrderedDict.fromkeys(header))
        spec_order = [def_map_rev[i] if i in def_map_rev else i \
                      for i in uheader]

        # Store order of format specifiers
        h = {}
        for index, specifier in enumerate(header):
            if specifier not in h:  # Ignor duplicate columns
                h[specifier] = index

        # Begin reading text
        if start_line is None:
            line = next(handle)  # Read first B6/M8 entry
        else:
            line = start_line  # Set header to given header

        # Check if input is text or bytestream
        if (isinstance(line, bytes)):
            def next_line(i):
                return next(i).decode('utf-8')

            line = strip(line.decode('utf-8'))
        else:
            next_line = next
            line = strip(line)

        # Manual 'for' loop isn't needed to read the file properly and quickly,
        # unlike fasta_iter and fastq_iter, but it is necessary begin iterating
        # partway through a file when the user gives a starting line.
        try:  # Manually construct a for loop to improve speed by using 'next'

            while True:  # Loop until StopIteration Exception raised

                self.current_line += 1

                data = B6Entry()
                data.fs_order = spec_order  # All entries store original order

                if line.startswith('#') and not comments:
                    line = strip(next_line(handle))
                    continue
                elif line.startswith('#') and comments:
                    yield line
                    line = strip(next_line(handle))
                    continue

                split_line = split(line, '\t')

                # Replace empty values with None
                spec_values = [None if i == '-' else i for i in split_line]

                # Add default specifiers
                def_attrs = data.__dict__.keys()
                for attr in def_attrs:
                    try:
                        def_spec, spec_type = def_map[attr]
                    except KeyError:
                        continue

                    try:
                        value = spec_values[h[def_spec]]
                    except KeyError:  # Custom format, no value
                        continue
                    except IndexError:
                        current_line = self.current_line
                        raise FormatError("line {!s}: the number of columns "
                            "is less than the number of specifiers"\
                            .format(current_line))

                    if type(value) != type(None):
                        try:
                            value = spec_type(value)
                        except ValueError:
                            current_line = self.current_line
                            raise FormatError("line {!s}: {} is of wrong type"\
                                .format(current_line, def_spec))
 
                    setattr(data, attr, value)

                # Add non-default specifiers to custom_fs attribute
                custom_specs = [i for i in sorted(h, key=h.get, reverse=False) \
                                if i not in def_specs]

                if custom_specs:
                    data.custom_fs = OrderedDict()
                    for key in custom_specs:
                        try:
                            value = spec_values[h[key]]
                        except IndexError:
                            current_line = self.current_line
                            raise FormatError("line {!s}: the number of "
                                "columns is less than the number of "
                                "specifiers".format(current_line))

                        data.custom_fs[key] = value

                line = strip(next_line(handle))  # Raises StopIteration at EOF

                yield data

        except StopIteration:  # Yield last B6/M8 entry
            yield data