class Hit(_BaseSearchObject):
    """Class representing a single database hit of a search result.

    Hit objects are the second-level container in the SearchIO module. They
    are the objects contained within a QueryResult (see QueryResult). They
    themselves are container for HSP objects and will contain at least one
    HSP.

    To have a quick look at a Hit and its contents, invoke ``print`` on it::

        >>> from Bio import SearchIO
        >>> qresult = next(SearchIO.parse('Blast/mirna.xml', 'blast-xml'))
        >>> hit = qresult[3]
        >>> print(hit)
        Query: 33211
               mir_1
          Hit: gi|301171322|ref|NR_035857.1| (86)
               Pan troglodytes microRNA mir-520c (MIR520C), microRNA
         HSPs: ----  --------  ---------  ------  ---------------  ---------------------
                  #   E-value  Bit score    Span      Query range              Hit range
               ----  --------  ---------  ------  ---------------  ---------------------
                  0   8.9e-20     100.47      60           [1:61]                [13:73]
                  1   3.3e-06      55.39      60           [0:60]                [13:73]

    You can invoke ``len`` on a Hit object to see how many HSP objects it contains::

        >>> len(hit)
        2

    Hit objects behave very similar to Python lists. You can retrieve the HSP
    object inside a Hit using the HSP's integer index. Hit objects can also be
    sliced, which will return a new Hit objects containing only the sliced HSPs::

        # HSP items inside the Hit can be retrieved using its integer index
        >>> hit[0]
        HSP(hit_id='gi|301171322|ref|NR_035857.1|', query_id='33211', 1 fragments)

        # slicing returns a new Hit
        >>> hit
        Hit(id='gi|301171322|ref|NR_035857.1|', query_id='33211', 2 hsps)
        >>> hit[:1]
        Hit(id='gi|301171322|ref|NR_035857.1|', query_id='33211', 1 hsps)
        >>> print(hit[1:])
        Query: 33211
               mir_1
          Hit: gi|301171322|ref|NR_035857.1| (86)
               Pan troglodytes microRNA mir-520c (MIR520C), microRNA
         HSPs: ----  --------  ---------  ------  ---------------  ---------------------
                  #   E-value  Bit score    Span      Query range              Hit range
               ----  --------  ---------  ------  ---------------  ---------------------
                  0   3.3e-06      55.39      60           [0:60]                [13:73]

    Hit objects provide ``filter`` and ``map`` methods, which are analogous to
    Python's built-in ``filter`` and ``map`` except that they return a new Hit
    object instead of a list.

    Here is an example of using ``filter`` to select for HSPs whose e-value is
    less than 1e-10::

        >>> evalue_filter = lambda hsp: hsp.evalue < 1e-10
        >>> filtered_hit = hit.filter(evalue_filter)
        >>> len(hit)
        2
        >>> len(filtered_hit)
        1
        >>> print(filtered_hit)
        Query: 33211
               mir_1
          Hit: gi|301171322|ref|NR_035857.1| (86)
               Pan troglodytes microRNA mir-520c (MIR520C), microRNA
         HSPs: ----  --------  ---------  ------  ---------------  ---------------------
                  #   E-value  Bit score    Span      Query range              Hit range
               ----  --------  ---------  ------  ---------------  ---------------------
                  0   8.9e-20     100.47      60           [1:61]                [13:73]

    There are also other methods which are counterparts of Python lists' methods
    with the same names: ``append``, ``index``, ``pop``, and ``sort``. Consult their
    respective documentations for more details and examples of their usage.

    """

    # attributes we don't want to transfer when creating a new Hit class
    # from this one
    _NON_STICKY_ATTRS = ("_items", )

    def __init__(self, hsps=(), id=None, query_id=None):
        """Initialize a Hit object.

        :param hsps: HSP objects contained in the Hit object
        :type hsps: iterable yielding HSP
        :param id: hit ID
        :type id: string
        :param query_id: query ID
        :type query_id: string

        If multiple HSP objects are used for initialization, they must all
        have the same ``query_id``, ``query_description``, ``hit_id``, and
        ``hit_description`` properties.
        """
        # default attribute values
        self._id = id
        self._id_alt = []
        self._query_id = query_id
        self._description = None
        self._description_alt = []
        self._query_description = None
        self.attributes = {}
        self.dbxrefs = []

        # TODO - Move this into the for look below in case
        # hsps is a single use iterator?
        for attr in ("query_id", "query_description", "hit_id",
                     "hit_description"):
            # HACK: setting the if clause to '> 1' allows for empty hit objects.
            # This makes it easier to work with file formats with unpredictable
            # hit-hsp ordering. The empty hit object itself is nonfunctional,
            # however, since all its cascading properties are empty.
            if len({getattr(hsp, attr) for hsp in hsps}) > 1:
                raise ValueError(
                    "Hit object can not contain HSPs with more than one %s." %
                    attr)

        self._items = []
        for hsp in hsps:
            # validate each HSP
            self._validate_hsp(hsp)
            # and store it them as an instance attribute
            self.append(hsp)

    def __repr__(self):
        """Return string representation of Hit object."""
        return "Hit(id=%r, query_id=%r, %r hsps)" % (self.id, self.query_id,
                                                     len(self))

    def __iter__(self):
        """Iterate over hsps."""
        return iter(self.hsps)

    def __len__(self):
        """Return number of hsps."""
        return len(self.hsps)

    # Python 3:
    def __bool__(self):
        """Return True if there are hsps."""
        return bool(self.hsps)

    # Python 2:
    __nonzero__ = __bool__

    def __contains__(self, hsp):
        """Return True if hsp in items."""
        return hsp in self._items

    def __str__(self):
        """Return a human readable summary of the Hit object."""
        lines = []

        # set query id line
        qid_line = "Query: %s" % self.query_id
        if self.query_description:
            qid_line += trim_str("\n       %s" % self.query_description, 80,
                                 "...")
        lines.append(qid_line)

        # set hit id line
        hid_line = "  Hit: %s" % self.id
        if hasattr(self, "seq_len"):
            hid_line += " (%i)" % self.seq_len
        if self.description:
            hid_line += trim_str("\n       %s" % self.description, 80, "...")
        lines.append(hid_line)

        # set attributes lines
        for key, value in sorted(self.attributes.items()):
            lines.append(" %s: %s" % (key, value))

        # set dbxrefs line
        if self.dbxrefs:
            lines.append("Database cross-references: " +
                         ", ".join(self.dbxrefs))

        # set hsp line and table
        if not self.hsps:
            lines.append(" HSPs: ?")
        else:
            lines.append(
                " HSPs: %s  %s  %s  %s  %s  %s" %
                ("-" * 4, "-" * 8, "-" * 9, "-" * 6, "-" * 15, "-" * 21))
            pattern = "%11s  %8s  %9s  %6s  %15s  %21s"
            lines.append(pattern % ("#", "E-value", "Bit score", "Span",
                                    "Query range", "Hit range"))
            lines.append(
                pattern %
                ("-" * 4, "-" * 8, "-" * 9, "-" * 6, "-" * 15, "-" * 21))
            for idx, hsp in enumerate(self.hsps):
                # evalue
                evalue = getattr_str(hsp, "evalue", fmt="%.2g")
                # bitscore
                bitscore = getattr_str(hsp, "bitscore", fmt="%.2f")
                # alignment length
                aln_span = getattr_str(hsp, "aln_span")
                # query region
                query_start = getattr_str(hsp, "query_start")
                query_end = getattr_str(hsp, "query_end")
                query_range = "[%s:%s]" % (query_start, query_end)
                # max column length is 18
                query_range = trim_str(query_range, 15, "~]")
                # hit region
                hit_start = getattr_str(hsp, "hit_start")
                hit_end = getattr_str(hsp, "hit_end")
                hit_range = "[%s:%s]" % (hit_start, hit_end)
                hit_range = trim_str(hit_range, 21, "~]")
                # append the hsp row
                lines.append(pattern % (str(idx), evalue, bitscore, aln_span,
                                        query_range, hit_range))

        return "\n".join(lines)

    def __getitem__(self, idx):
        """Return the HSP object at the given index."""
        # if key is slice, return a new Hit instance
        if isinstance(idx, slice):
            obj = self.__class__(self.hsps[idx])
            self._transfer_attrs(obj)
            return obj
        return self._items[idx]

    def __setitem__(self, idx, hsps):
        """Assign hsps to index idx."""
        # handle case if hsps is a list of hsp
        if isinstance(hsps, (list, tuple)):
            for hsp in hsps:
                self._validate_hsp(hsp)
        else:
            self._validate_hsp(hsps)

        self._items[idx] = hsps

    def __delitem__(self, idx):
        """Delete item of index idx."""
        del self._items[idx]

    # hsp properties #
    def _validate_hsp(self, hsp):
        """Validate an HSP object (PRIVATE).

        Valid HSP objects have the same hit_id as the Hit object ID and the
        same query_id as the Hit object's query_id.

        """
        if not isinstance(hsp, HSP):
            raise TypeError("Hit objects can only contain HSP objects.")
        # HACK: to make validation during __init__ work
        if self._items:
            if self.id is not None:
                if hsp.hit_id != self.id:
                    raise ValueError(
                        "Expected HSP with hit ID %r, found %r instead." %
                        (self.id, hsp.hit_id))
            else:
                self.id = hsp.hit_id

            if self.description is not None:
                if hsp.hit_description != self.description:
                    raise ValueError(
                        "Expected HSP with hit description %r, found %r instead."
                        % (self.description, hsp.hit_description))
            else:
                self.description = hsp.hit_description

            if self.query_id is not None:
                if hsp.query_id != self.query_id:
                    raise ValueError(
                        "Expected HSP with query ID %r, found %r instead." %
                        (self.query_id, hsp.query_id))
            else:
                self.query_id = hsp.query_id

            if self.query_description is not None:
                if hsp.query_description != self.query_description:
                    raise ValueError(
                        "Expected HSP with query description %r, found %r instead."
                        % (self.query_description, hsp.query_description))
            else:
                self.query_description = hsp.query_description

    # properties #
    description = optionalcascade("_description", "hit_description",
                                  """Hit description""")
    query_description = optionalcascade(
        "_query_description",
        "query_description",
        """Description of the query that produced the hit""",
    )
    id = optionalcascade("_id", "hit_id", """Hit ID string.""")
    query_id = optionalcascade(
        "_query_id", "query_id",
        """ID string of the query that produced the hit""")
    # returns all hsps
    hsps = allitems(doc="""HSP objects contained in the Hit""")

    @property
    def id_all(self):
        """Alternative ID(s) of the Hit."""
        return [self.id] + self._id_alt

    @property
    def description_all(self):
        """Alternative descriptions of the Hit."""
        return [self.description] + self._description_alt

    @property
    def fragments(self):
        """Access the HSPFragment objects contained in the Hit."""
        return list(chain(*self._items))

    # public methods #
    def append(self, hsp):
        """Add a HSP object to the end of Hit.

        Parameters
        hsp -- HSP object to append.

        Any HSP object appended must have the same ``hit_id`` property as the
        Hit object's ``id`` property and the same ``query_id`` property as the
        Hit object's ``query_id`` property.

        """
        self._validate_hsp(hsp)
        self._items.append(hsp)

    def filter(self, func=None):
        """Create new Hit object whose HSP objects pass the filter function.

        :param func: function for filtering
        :type func: callable, accepts HSP, returns bool

        ``filter`` is analogous to Python's built-in ``filter`` function, except
        that instead of returning a list it returns a ``Hit`` object. Here is an
        example of using ``filter`` to select for HSPs having bitscores bigger
        than 60::

            >>> from Bio import SearchIO
            >>> qresult = next(SearchIO.parse('Blast/mirna.xml', 'blast-xml'))
            >>> hit = qresult[3]
            >>> evalue_filter = lambda hsp: hsp.bitscore > 60
            >>> filtered_hit = hit.filter(evalue_filter)
            >>> len(hit)
            2
            >>> len(filtered_hit)
            1
            >>> print(filtered_hit)
            Query: 33211
                   mir_1
              Hit: gi|301171322|ref|NR_035857.1| (86)
                   Pan troglodytes microRNA mir-520c (MIR520C), microRNA
             HSPs: ----  --------  ---------  ------  ---------------  ---------------------
                      #   E-value  Bit score    Span      Query range              Hit range
                   ----  --------  ---------  ------  ---------------  ---------------------
                      0   8.9e-20     100.47      60           [1:61]                [13:73]

        """
        hsps = list(filter(func, self.hsps))
        if hsps:
            obj = self.__class__(hsps)
            self._transfer_attrs(obj)
            return obj

    def index(self, hsp):
        """Return the index of a given HSP object, zero-based.

        :param hsp: object to look up
        :type hsp: HSP

        """
        return self._items.index(hsp)

    def map(self, func=None):
        """Create new Hit object, mapping the given function to its HSPs.

        :param func: function for mapping
        :type func: callable, accepts HSP, returns HSP

        ``map`` is analogous to Python's built-in ``map`` function. It is applied to
        all HSPs contained in the Hit object and returns a new Hit object.

        """
        if func is not None:
            hsps = [func(x)
                    for x in self.hsps[:]]  # this creates a shallow copy
        else:
            hsps = self.hsps[:]
        if hsps:
            obj = self.__class__(hsps)
            self._transfer_attrs(obj)
            return obj

    def pop(self, index=-1):
        """Remove and returns the HSP object at the specified index.

        :param index: index of HSP object to pop
        :type index: int

        """
        return self._items.pop(index)

    def sort(self, key=None, reverse=False, in_place=True):
        """Sort the HSP objects.

        :param key: sorting function
        :type key: callable, accepts HSP, returns key for sorting
        :param reverse: whether to reverse sorting results or no
        :type reverse: bool
        :param in_place: whether to do in-place sorting or no
        :type in_place: bool

        ``sort`` defaults to sorting in-place, to mimick Python's ``list.sort``
        method. If you set the ``in_place`` argument to False, it will treat
        return a new, sorted Hit object and keep the initial one unsorted

        """
        if in_place:
            self._items.sort(key=key, reverse=reverse)
        else:
            hsps = self.hsps[:]
            hsps.sort(key=key, reverse=reverse)
            obj = self.__class__(hsps)
            self._transfer_attrs(obj)
            return obj
Ejemplo n.º 2
0
class QueryResult(_BaseSearchObject):
    """Class representing search results from a single query.

    QueryResult is the container object that stores all search hits from a
    single search query. It is the top-level object returned by SearchIO's two
    main functions, ``read`` and ``parse``. Depending on the search results and
    search output format, a QueryResult object will contain zero or more Hit
    objects (see Hit).

    You can take a quick look at a QueryResult's contents and attributes by
    invoking ``print`` on it::

        >>> from Bio import SearchIO
        >>> qresult = next(SearchIO.parse('Blast/mirna.xml', 'blast-xml'))
        >>> print(qresult)
        Program: blastn (2.2.27+)
          Query: 33211 (61)
                 mir_1
         Target: refseq_rna
           Hits: ----  -----  ----------------------------------------------------------
                    #  # HSP  ID + description
                 ----  -----  ----------------------------------------------------------
                    0      1  gi|262205317|ref|NR_030195.1|  H**o sapiens microRNA 52...
                    1      1  gi|301171311|ref|NR_035856.1|  Pan troglodytes microRNA...
                    2      1  gi|270133242|ref|NR_032573.1|  Macaca mulatta microRNA ...
                    3      2  gi|301171322|ref|NR_035857.1|  Pan troglodytes microRNA...
                    4      1  gi|301171267|ref|NR_035851.1|  Pan troglodytes microRNA...
                    5      2  gi|262205330|ref|NR_030198.1|  H**o sapiens microRNA 52...
                    6      1  gi|262205302|ref|NR_030191.1|  H**o sapiens microRNA 51...
                    7      1  gi|301171259|ref|NR_035850.1|  Pan troglodytes microRNA...
                    8      1  gi|262205451|ref|NR_030222.1|  H**o sapiens microRNA 51...
                    9      2  gi|301171447|ref|NR_035871.1|  Pan troglodytes microRNA...
                   10      1  gi|301171276|ref|NR_035852.1|  Pan troglodytes microRNA...
                   11      1  gi|262205290|ref|NR_030188.1|  H**o sapiens microRNA 51...
        ...

    If you just want to know how many hits a QueryResult has, you can invoke
    ``len`` on it. Alternatively, you can simply type its name in the interpreter::

        >>> len(qresult)
        100
        >>> qresult
        QueryResult(id='33211', 100 hits)

    QueryResult behaves like a hybrid of Python's built-in list and dictionary.
    You can retrieve its items (Hit objects) using the integer index of the
    item, just like regular Python lists::

        >>> first_hit = qresult[0]
        >>> first_hit
        Hit(id='gi|262205317|ref|NR_030195.1|', query_id='33211', 1 hsps)

    You can slice QueryResult objects as well. Slicing will return a new
    QueryResult object containing only the sliced hits::

        >>> sliced_qresult = qresult[:3]    # slice the first three hits
        >>> len(qresult)
        100
        >>> len(sliced_qresult)
        3
        >>> print(sliced_qresult)
        Program: blastn (2.2.27+)
          Query: 33211 (61)
                 mir_1
         Target: refseq_rna
           Hits: ----  -----  ----------------------------------------------------------
                    #  # HSP  ID + description
                 ----  -----  ----------------------------------------------------------
                    0      1  gi|262205317|ref|NR_030195.1|  H**o sapiens microRNA 52...
                    1      1  gi|301171311|ref|NR_035856.1|  Pan troglodytes microRNA...
                    2      1  gi|270133242|ref|NR_032573.1|  Macaca mulatta microRNA ...

    Like Python dictionaries, you can also retrieve hits using the hit's ID.
    This is useful for retrieving hits that you know should exist in a given
    search::

        >>> hit = qresult['gi|262205317|ref|NR_030195.1|']
        >>> hit
        Hit(id='gi|262205317|ref|NR_030195.1|', query_id='33211', 1 hsps)

    You can also replace a Hit in QueryResult with another Hit using either the
    integer index or hit key string. Note that the replacing object must be a
    Hit that has the same ``query_id`` property as the QueryResult object.

    If you're not sure whether a QueryResult contains a particular hit, you can
    use the hit ID to check for membership first::

        >>> 'gi|262205317|ref|NR_030195.1|' in qresult
        True
        >>> 'gi|262380031|ref|NR_023426.1|' in qresult
        False

    Or, if you just want to know the rank / position of a given hit, you can
    use the hit ID as an argument for the ``index`` method. Note that the values
    returned will be zero-based. So zero (0) means the hit is the first in the
    QueryResult, three (3) means the hit is the fourth item, and so on. If the
    hit does not exist in the QueryResult, a ``ValueError`` will be raised.

        >>> qresult.index('gi|262205317|ref|NR_030195.1|')
        0
        >>> qresult.index('gi|262205330|ref|NR_030198.1|')
        5
        >>> qresult.index('gi|262380031|ref|NR_023426.1|')
        Traceback (most recent call last):
        ...
        ValueError: ...

    To ease working with a large number of hits, QueryResult has several
    ``filter`` and ``map`` methods, analogous to Python's built-in functions with
    the same names. There are ``filter`` and ``map`` methods available for
    operations over both Hit objects or HSP objects. As an example, here we are
    using the ``hit_map`` method to rename all hit IDs within a QueryResult::

        >>> def renamer(hit):
        ...     hit.id = hit.id.split('|')[3]
        ...     return hit
        >>> mapped_qresult = qresult.hit_map(renamer)
        >>> print(mapped_qresult)
        Program: blastn (2.2.27+)
          Query: 33211 (61)
                 mir_1
         Target: refseq_rna
           Hits: ----  -----  ----------------------------------------------------------
                    #  # HSP  ID + description
                 ----  -----  ----------------------------------------------------------
                    0      1  NR_030195.1  H**o sapiens microRNA 520b (MIR520B), micr...
                    1      1  NR_035856.1  Pan troglodytes microRNA mir-520b (MIR520B...
                    2      1  NR_032573.1  Macaca mulatta microRNA mir-519a (MIR519A)...
        ...

    The principle for other ``map`` and ``filter`` methods are similar: they accept
    a function, applies it, and returns a new QueryResult object.

    There are also other methods useful for working with list-like objects:
    ``append``, ``pop``, and ``sort``. More details and examples are available in
    their respective documentations.

    Finally, just like Python lists and dictionaries, QueryResult objects are
    iterable. Iteration over QueryResults will yield Hit objects::

        >>> for hit in qresult[:4]:     # iterate over the first four items
        ...     hit
        ...
        Hit(id='gi|262205317|ref|NR_030195.1|', query_id='33211', 1 hsps)
        Hit(id='gi|301171311|ref|NR_035856.1|', query_id='33211', 1 hsps)
        Hit(id='gi|270133242|ref|NR_032573.1|', query_id='33211', 1 hsps)
        Hit(id='gi|301171322|ref|NR_035857.1|', query_id='33211', 2 hsps)

    If you need access to all the hits in a QueryResult object, you can get
    them in a list using the ``hits`` property. Similarly, access to all hit IDs is
    available through the ``hit_keys`` property.

        >>> qresult.hits
        [Hit(id='gi|262205317|ref|NR_030195.1|', query_id='33211', 1 hsps), ...]
        >>> qresult.hit_keys
        ['gi|262205317|ref|NR_030195.1|', 'gi|301171311|ref|NR_035856.1|', ...]

    """

    # attributes we don't want to transfer when creating a new QueryResult class
    # from this one
    _NON_STICKY_ATTRS = (
        '_items',
        '__alt_hit_ids',
    )

    def __init__(self, hits=(), id=None, hit_key_function=None):
        """Initializes a QueryResult object.

        :param id: query sequence ID
        :type id: string
        :param hits: iterator yielding Hit objects
        :type hits: iterable
        :param hit_key_function: function to define hit keys
        :type hit_key_function: callable, accepts Hit objects, returns string

        """
        # default values
        self._id = id
        self._hit_key_function = hit_key_function or _hit_key_func
        self._items = OrderedDict()
        self._description = None
        self.__alt_hit_ids = {}
        self.program = '<unknown program>'
        self.target = '<unknown target>'
        self.version = '<unknown version>'

        # validate Hit objects and fill up self._items
        for hit in hits:
            # validation is handled by __setitem__
            self.append(hit)

    # handle Python 2 OrderedDict behavior
    if hasattr(OrderedDict, 'iteritems'):

        def __iter__(self):
            return self.iterhits()

        @property
        def hits(self):
            """Hit objects contained in the QueryResult."""
            return self._items.values()

        @property
        def hit_keys(self):
            """Hit IDs of the Hit objects contained in the QueryResult."""
            return self._items.keys()

        @property
        def items(self):
            """List of tuples of Hit IDs and Hit objects."""
            return self._items.items()

        def iterhits(self):
            """Returns an iterator over the Hit objects."""
            for hit in self._items.itervalues():
                yield hit

        def iterhit_keys(self):
            """Returns an iterator over the ID of the Hit objects."""
            for hit_id in self._items:
                yield hit_id

        def iteritems(self):
            """Returns an iterator yielding tuples of Hit ID and Hit objects."""
            for item in self._items.iteritems():
                yield item

    else:

        def __iter__(self):
            return iter(self.hits)

        @property
        def hits(self):
            """Hit objects contained in the QueryResult."""
            return list(self._items.values())

        @property
        def hit_keys(self):
            """Hit IDs of the Hit objects contained in the QueryResult."""
            return list(self._items.keys())

        @property
        def items(self):
            """List of tuples of Hit IDs and Hit objects."""
            return list(self._items.items())

        def iterhits(self):
            """Returns an iterator over the Hit objects."""
            for hit in self._items.values():
                yield hit

        def iterhit_keys(self):
            """Returns an iterator over the ID of the Hit objects."""
            for hit_id in self._items:
                yield hit_id

        def iteritems(self):
            """Returns an iterator yielding tuples of Hit ID and Hit objects."""
            for item in self._items.items():
                yield item

    def __contains__(self, hit_key):
        if isinstance(hit_key, Hit):
            return self._hit_key_function(hit_key) in self._items
        return hit_key in self._items or hit_key in self.__alt_hit_ids

    def __len__(self):
        return len(self._items)

    # Python 3:
    def __bool__(self):
        return bool(self._items)

    # Python 2:
    __nonzero__ = __bool__

    def __repr__(self):
        return "QueryResult(id=%r, %r hits)" % (self.id, len(self))

    def __str__(self):
        lines = []

        # set program and version line
        lines.append('Program: %s (%s)' % (self.program, self.version))

        # set query id line
        qid_line = '  Query: %s' % self.id
        if hasattr(self, 'seq_len'):
            qid_line += ' (%i)' % self.seq_len
        if self.description:
            qid_line += trim_str('\n         %s' % self.description, 80, '...')
        lines.append(qid_line)

        # set target line
        lines.append(' Target: %s' % self.target)

        # set hit lines
        if not self.hits:
            lines.append('   Hits: 0')
        else:
            lines.append('   Hits: %s  %s  %s' % ('-' * 4, '-' * 5, '-' * 58))
            pattern = '%13s  %5s  %s'
            lines.append(pattern % ('#', '# HSP', 'ID + description'))
            lines.append(pattern % ('-' * 4, '-' * 5, '-' * 58))
            for idx, hit in enumerate(self.hits):
                if idx < 30:
                    hid_line = '%s  %s' % (hit.id, hit.description)
                    if len(hid_line) > 58:
                        hid_line = hid_line[:55] + '...'
                    lines.append(pattern % (idx, str(len(hit)), hid_line))
                elif idx > len(self.hits) - 4:
                    hid_line = '%s  %s' % (hit.id, hit.description)
                    if len(hid_line) > 58:
                        hid_line = hid_line[:55] + '...'
                    lines.append(pattern % (idx, str(len(hit)), hid_line))
                elif idx == 30:
                    lines.append('%14s' % '~~~')

        return '\n'.join(lines)

    def __getitem__(self, hit_key):
        # retrieval using slice objects returns another QueryResult object
        if isinstance(hit_key, slice):
            # should we return just a list of Hits instead of a full blown
            # QueryResult object if it's a slice?
            hits = list(self.hits)[hit_key]
            obj = self.__class__(hits, self.id, self._hit_key_function)
            self._transfer_attrs(obj)
            return obj

        # if key is an int, then retrieve the Hit at the int index
        elif isinstance(hit_key, int):
            length = len(self)
            if 0 <= hit_key < length:
                for idx, item in enumerate(self.iterhits()):
                    if idx == hit_key:
                        return item
            elif -1 * length <= hit_key < 0:
                for idx, item in enumerate(self.iterhits()):
                    if length + hit_key == idx:
                        return item
            raise IndexError("list index out of range")

        # if key is a string, then do a regular dictionary retrieval
        # falling back on alternative hit IDs
        try:
            return self._items[hit_key]
        except KeyError:
            return self._items[self.__alt_hit_ids[hit_key]]

    def __setitem__(self, hit_key, hit):
        # only accept string keys
        if not isinstance(hit_key, basestring):
            raise TypeError("QueryResult object keys must be a string.")
        # hit must be a Hit object
        if not isinstance(hit, Hit):
            raise TypeError(
                "QueryResult objects can only contain Hit objects.")
        qid = self.id
        hqid = hit.query_id
        # and it must have the same query ID as this object's ID
        # unless it's the query ID is None (default for empty objects), in which
        # case we want to use the hit's query ID as the query ID
        if qid is not None:
            if hqid != qid:
                raise ValueError("Expected Hit with query ID %r, found %r "
                                 "instead." % (qid, hqid))
        else:
            self.id = hqid
        # same thing with descriptions
        qdesc = self.description
        hqdesc = hit.query_description
        if qdesc is not None:
            if hqdesc != qdesc:
                raise ValueError("Expected Hit with query description %r, "
                                 "found %r instead." % (qdesc, hqdesc))
        else:
            self.description = hqdesc

        # remove existing alt_id references, if hit_key already exists
        if hit_key in self._items:
            for alt_key in self._items[hit_key].id_all[1:]:
                del self.__alt_hit_ids[alt_key]

        # if hit_key is already present as an alternative ID
        # delete it from the alternative ID dict
        if hit_key in self.__alt_hit_ids:
            del self.__alt_hit_ids[hit_key]

        self._items[hit_key] = hit
        for alt_id in hit.id_all[1:]:
            self.__alt_hit_ids[alt_id] = hit_key

    def __delitem__(self, hit_key):
        # if hit_key an integer or slice, get the corresponding key first
        # and put it into a list
        if isinstance(hit_key, int):
            hit_keys = [list(self.hit_keys)[hit_key]]
        # the same, if it's a slice
        elif isinstance(hit_key, slice):
            hit_keys = list(self.hit_keys)[hit_key]
        # otherwise put it in a list
        else:
            hit_keys = [hit_key]

        for key in hit_keys:
            deleted = False
            if key in self._items:
                del self._items[key]
                deleted = True
            if key in self.__alt_hit_ids:
                del self._items[self.__alt_hit_ids[key]]
                del self.__alt_hit_ids[key]
                deleted = True
            if not deleted:
                raise KeyError(repr(key))
        return

    # properties #
    id = optionalcascade('_id', 'query_id', """QueryResult ID string""")
    description = optionalcascade('_description', 'query_description',
                                  """QueryResult description""")

    @property
    def hsps(self):
        """HSP objects contained in the QueryResult."""
        return [hsp for hsp in chain(*self.hits)]

    @property
    def fragments(self):
        """HSPFragment objects contained in the QueryResult."""
        return [frag for frag in chain(*self.hsps)]

    # public methods #
    def absorb(self, hit):
        """Adds a Hit object to the end of QueryResult. If the QueryResult
        already has a Hit with the same ID, append the new Hit's HSPs into
        the existing Hit.

        :param hit: object to absorb
        :type hit: Hit

        This method is used for file formats that may output the same Hit in
        separate places, such as BLAT or Exonerate. In both formats, Hit
        with different strands are put in different places. However, SearchIO
        considers them to be the same as a Hit object should be all database
        entries with the same ID, regardless of strand orientation.

        """
        try:
            self.append(hit)
        except ValueError:
            assert hit.id in self
            for hsp in hit:
                self[hit.id].append(hsp)

    def append(self, hit):
        """Adds a Hit object to the end of QueryResult.

        :param hit: object to append
        :type hit: Hit

        Any Hit object appended must have the same ``query_id`` property as the
        QueryResult's ``id`` property. If the hit key already exists, a
        ``ValueError`` will be raised.

        """
        # if a custom hit_key_function is supplied, use it to define th hit key
        if self._hit_key_function is not None:
            hit_key = self._hit_key_function(hit)
        else:
            hit_key = hit.id

        if hit_key not in self and all(pid not in self
                                       for pid in hit.id_all[1:]):
            self[hit_key] = hit
        else:
            raise ValueError("The ID or alternative IDs of Hit %r exists in "
                             "this QueryResult." % hit_key)

    def hit_filter(self, func=None):
        """Creates a new QueryResult object whose Hit objects pass the filter
        function.

        :param func: filter function
        :type func: callable, accepts Hit, returns bool

        Here is an example of using ``hit_filter`` to select Hits whose
        description begins with the string 'H**o sapiens', case sensitive::

            >>> from Bio import SearchIO
            >>> qresult = next(SearchIO.parse('Blast/mirna.xml', 'blast-xml'))
            >>> def desc_filter(hit):
            ...     return hit.description.startswith('H**o sapiens')
            ...
            >>> len(qresult)
            100
            >>> filtered = qresult.hit_filter(desc_filter)
            >>> len(filtered)
            39
            >>> print(filtered[:4])
            Program: blastn (2.2.27+)
              Query: 33211 (61)
                     mir_1
             Target: refseq_rna
               Hits: ----  -----  ----------------------------------------------------------
                        #  # HSP  ID + description
                     ----  -----  ----------------------------------------------------------
                        0      1  gi|262205317|ref|NR_030195.1|  H**o sapiens microRNA 52...
                        1      2  gi|262205330|ref|NR_030198.1|  H**o sapiens microRNA 52...
                        2      1  gi|262205302|ref|NR_030191.1|  H**o sapiens microRNA 51...
                        3      1  gi|262205451|ref|NR_030222.1|  H**o sapiens microRNA 51...

        Note that instance attributes (other than the hits) from the unfiltered
        QueryResult are retained in the filtered object.

            >>> qresult.program == filtered.program
            True
            >>> qresult.target == filtered.target
            True

        """
        hits = list(filter(func, self.hits))
        obj = self.__class__(hits, self.id, self._hit_key_function)
        self._transfer_attrs(obj)
        return obj

    def hit_map(self, func=None):
        """Creates a new QueryResult object, mapping the given function to its
        Hits.

        :param func: map function
        :type func: callable, accepts Hit, returns Hit

        Here is an example of using ``hit_map`` with a function that discards all
        HSPs in a Hit except for the first one::

            >>> from Bio import SearchIO
            >>> qresult = next(SearchIO.parse('Blast/mirna.xml', 'blast-xml'))
            >>> print(qresult[:8])
            Program: blastn (2.2.27+)
              Query: 33211 (61)
                     mir_1
             Target: refseq_rna
               Hits: ----  -----  ----------------------------------------------------------
                        #  # HSP  ID + description
                     ----  -----  ----------------------------------------------------------
                        0      1  gi|262205317|ref|NR_030195.1|  H**o sapiens microRNA 52...
                        1      1  gi|301171311|ref|NR_035856.1|  Pan troglodytes microRNA...
                        2      1  gi|270133242|ref|NR_032573.1|  Macaca mulatta microRNA ...
                        3      2  gi|301171322|ref|NR_035857.1|  Pan troglodytes microRNA...
                        4      1  gi|301171267|ref|NR_035851.1|  Pan troglodytes microRNA...
                        5      2  gi|262205330|ref|NR_030198.1|  H**o sapiens microRNA 52...
                        6      1  gi|262205302|ref|NR_030191.1|  H**o sapiens microRNA 51...
                        7      1  gi|301171259|ref|NR_035850.1|  Pan troglodytes microRNA...

            >>> top_hsp = lambda hit: hit[:1]
            >>> mapped_qresult = qresult.hit_map(top_hsp)
            >>> print(mapped_qresult[:8])
            Program: blastn (2.2.27+)
              Query: 33211 (61)
                     mir_1
             Target: refseq_rna
               Hits: ----  -----  ----------------------------------------------------------
                        #  # HSP  ID + description
                     ----  -----  ----------------------------------------------------------
                        0      1  gi|262205317|ref|NR_030195.1|  H**o sapiens microRNA 52...
                        1      1  gi|301171311|ref|NR_035856.1|  Pan troglodytes microRNA...
                        2      1  gi|270133242|ref|NR_032573.1|  Macaca mulatta microRNA ...
                        3      1  gi|301171322|ref|NR_035857.1|  Pan troglodytes microRNA...
                        4      1  gi|301171267|ref|NR_035851.1|  Pan troglodytes microRNA...
                        5      1  gi|262205330|ref|NR_030198.1|  H**o sapiens microRNA 52...
                        6      1  gi|262205302|ref|NR_030191.1|  H**o sapiens microRNA 51...
                        7      1  gi|301171259|ref|NR_035850.1|  Pan troglodytes microRNA...

        """
        hits = [deepcopy(hit) for hit in self.hits]
        if func is not None:
            hits = [func(x) for x in hits]
        obj = self.__class__(hits, self.id, self._hit_key_function)
        self._transfer_attrs(obj)
        return obj

    def hsp_filter(self, func=None):
        """Creates a new QueryResult object whose HSP objects pass the filter
        function.

        ``hsp_filter`` is the same as ``hit_filter``, except that it filters
        directly on each HSP object in every Hit. If the filtering removes
        all HSP objects in a given Hit, the entire Hit will be discarded. This
        will result in the QueryResult having less Hit after filtering.

        """
        hits = [x for x in (hit.filter(func) for hit in self.hits) if x]
        obj = self.__class__(hits, self.id, self._hit_key_function)
        self._transfer_attrs(obj)
        return obj

    def hsp_map(self, func=None):
        """Creates a new QueryResult object, mapping the given function to its
        HSPs.

        ``hsp_map`` is the same as ``hit_map``, except that it applies the given
        function to all HSP objects in every Hit, instead of the Hit objects.

        """
        hits = [x for x in (hit.map(func) for hit in list(self.hits)[:]) if x]
        obj = self.__class__(hits, self.id, self._hit_key_function)
        self._transfer_attrs(obj)
        return obj

    # marker for default self.pop() return value
    # this method is adapted from Python's built in OrderedDict.pop
    # implementation
    __marker = object()

    def pop(self, hit_key=-1, default=__marker):
        """Removes the specified hit key and return the Hit object.

        :param hit_key: key of the Hit object to return
        :type hit_key: int or string
        :param default: return value if no Hit exists with the given key
        :type default: object

        By default, ``pop`` will remove and return the last Hit object in the
        QueryResult object. To remove specific Hit objects, you can use its
        integer index or hit key.

            >>> from Bio import SearchIO
            >>> qresult = next(SearchIO.parse('Blast/mirna.xml', 'blast-xml'))
            >>> len(qresult)
            100
            >>> for hit in qresult[:5]:
            ...     print(hit.id)
            ...
            gi|262205317|ref|NR_030195.1|
            gi|301171311|ref|NR_035856.1|
            gi|270133242|ref|NR_032573.1|
            gi|301171322|ref|NR_035857.1|
            gi|301171267|ref|NR_035851.1|

            # remove the last hit
            >>> qresult.pop()
            Hit(id='gi|397513516|ref|XM_003827011.1|', query_id='33211', 1 hsps)

            # remove the first hit
            >>> qresult.pop(0)
            Hit(id='gi|262205317|ref|NR_030195.1|', query_id='33211', 1 hsps)

            # remove hit with the given ID
            >>> qresult.pop('gi|301171322|ref|NR_035857.1|')
            Hit(id='gi|301171322|ref|NR_035857.1|', query_id='33211', 2 hsps)

        """
        # if key is an integer (index)
        # get the ID for the Hit object at that index
        if isinstance(hit_key, int):
            # raise the appropriate error if there is no hit
            if not self:
                raise IndexError("pop from empty list")
            hit_key = list(self.hit_keys)[hit_key]

        try:
            hit = self._items.pop(hit_key)
            # remove all alternative IDs of the popped hit
            for alt_id in hit.id_all[1:]:
                try:
                    del self.__alt_hit_ids[alt_id]
                except KeyError:
                    pass
            return hit
        except KeyError:
            if hit_key in self.__alt_hit_ids:
                return self.pop(self.__alt_hit_ids[hit_key], default)
            # if key doesn't exist and no default is set, raise a KeyError
            if default is self.__marker:
                raise KeyError(hit_key)
        # if key doesn't exist but a default is set, return the default value
        return default

    def index(self, hit_key):
        """Returns the index of a given hit key, zero-based.

        :param hit_key: hit ID
        :type hit_key: string

        This method is useful for finding out the integer index (usually
        correlated with search rank) of a given hit key.

            >>> from Bio import SearchIO
            >>> qresult = next(SearchIO.parse('Blast/mirna.xml', 'blast-xml'))
            >>> qresult.index('gi|301171259|ref|NR_035850.1|')
            7

        """
        if isinstance(hit_key, Hit):
            return list(self.hit_keys).index(hit_key.id)
        try:
            return list(self.hit_keys).index(hit_key)
        except ValueError:
            if hit_key in self.__alt_hit_ids:
                return self.index(self.__alt_hit_ids[hit_key])
            raise

    def sort(self, key=None, reverse=False, in_place=True):
        # no cmp argument to make sort more Python 3-like
        """Sorts the Hit objects.

        :param key: sorting function
        :type key: callable, accepts Hit, returns key for sorting
        :param reverse: whether to reverse sorting results or no
        :type reverse: bool
        :param in_place: whether to do in-place sorting or no
        :type in_place: bool

        ``sort`` defaults to sorting in-place, to mimick Python's ``list.sort``
        method. If you set the ``in_place`` argument to False, it will treat
        return a new, sorted QueryResult object and keep the initial one
        unsorted.

        """
        if key is None:
            # if reverse is True, reverse the hits
            if reverse:
                sorted_hits = list(self.hits)[::-1]
            # otherwise (default options) make a copy of the hits
            else:
                sorted_hits = list(self.hits)[:]
        else:
            sorted_hits = sorted(self.hits, key=key, reverse=reverse)

        # if sorting is in-place, don't create a new QueryResult object
        if in_place:
            new_hits = OrderedDict()
            for hit in sorted_hits:
                new_hits[self._hit_key_function(hit)] = hit
            self._items = new_hits
        # otherwise, return a new sorted QueryResult object
        else:
            obj = self.__class__(sorted_hits, self.id, self._hit_key_function)
            self._transfer_attrs(obj)
            return obj
Ejemplo n.º 3
0
class Hit(_BaseSearchObject):
    """Class representing a single database hit of a search result.

    Hit objects are the second-level container in the SearchIO module. They
    are the objects contained within a QueryResult (see QueryResult). They
    themselves are container for HSP objects and will contain at least one
    HSP.

    To have a quick look at a Hit and its contents, invoke `print` on it:

    >>> from Bio import SearchIO
    >>> qresult = SearchIO.parse('Blast/mirna.xml', 'blast-xml').next()
    >>> hit = qresult[3]
    >>> print hit
    Query: 33211
           mir_1
      Hit: gi|301171322|ref|NR_035857.1| (86)
           Pan troglodytes microRNA mir-520c (MIR520C), microRNA
     HSPs: ----  --------  ---------  ------  ---------------  ---------------------
              #   E-value  Bit score    Span      Query range              Hit range
           ----  --------  ---------  ------  ---------------  ---------------------
              0   8.9e-20     100.47      60           [1:61]                [13:73]
              1   3.3e-06      55.39      60           [0:60]                [13:73]

    You can invoke `len` on a Hit object to see how many HSP objects it contains:

    >>> len(hit)
    2

    Hit objects behave very similar to Python lists. You can retrieve the HSP
    object inside a Hit using the HSP's integer index. Hit objects can also be
    sliced, which will return a new Hit objects containing only the sliced HSPs:

    # HSP items inside the Hit can be retrieved using its integer index
    >>> hit[0]
    HSP(hit_id='gi|301171322|ref|NR_035857.1|', query_id='33211', 1 fragments)

    # slicing returns a new Hit
    >>> hit
    Hit(id='gi|301171322|ref|NR_035857.1|', query_id='33211', 2 hsps)
    >>> hit[:1]
    Hit(id='gi|301171322|ref|NR_035857.1|', query_id='33211', 1 hsps)
    >>> print hit[1:]
    Query: 33211
           mir_1
      Hit: gi|301171322|ref|NR_035857.1| (86)
           Pan troglodytes microRNA mir-520c (MIR520C), microRNA
     HSPs: ----  --------  ---------  ------  ---------------  ---------------------
              #   E-value  Bit score    Span      Query range              Hit range
           ----  --------  ---------  ------  ---------------  ---------------------
              0   3.3e-06      55.39      60           [0:60]                [13:73]

    Hit objects provide `filter` and `map` methods, which are analogous to
    Python's built-in `filter` and `map` except that they return a new Hit
    object instead of a list.

    Here is an example of using `filter` to select for HSPs whose e-value is
    less than 1e-10:

    >>> evalue_filter = lambda hsp: hsp.evalue < 1e-10
    >>> filtered_hit = hit.filter(evalue_filter)
    >>> len(hit)
    2
    >>> len(filtered_hit)
    1
    >>> print filtered_hit
    Query: 33211
           mir_1
      Hit: gi|301171322|ref|NR_035857.1| (86)
           Pan troglodytes microRNA mir-520c (MIR520C), microRNA
     HSPs: ----  --------  ---------  ------  ---------------  ---------------------
              #   E-value  Bit score    Span      Query range              Hit range
           ----  --------  ---------  ------  ---------------  ---------------------
              0   8.9e-20     100.47      60           [1:61]                [13:73]

    There are also other methods which are counterparts of Python lists' methods
    with the same names: `append`, `index`, `pop`, and `sort`. Consult their
    respective documentations for more details and examples of their usage.

    """

    # attributes we don't want to transfer when creating a new Hit class
    # from this one
    _NON_STICKY_ATTRS = ('_items', )

    def __init__(self, hsps=[], id=None, query_id=None):
        """Initializes a Hit object.

        Arguments:
        hsps -- List containing HSP objects.
        id -- String of the Hit ID
        query_id -- String of the Hit's query ID

        If multiple HSP objects are used for initialization, they must all
        have the same `query_id`, `query_description`, `hit_id`, and
        `hit_description` properties.
        """
        # default attribute values
        self._id = id
        self._query_id = query_id
        self._description = None
        self._query_description = None

        for attr in ('query_id', 'query_description', 'hit_id',
                     'hit_description'):
            # HACK: setting the if clause to '> 1' allows for empty hit objects.
            # This makes it easier to work with file formats with unpredictable
            # hit-hsp ordering. The empty hit object itself is nonfunctional,
            # however, since all its cascading properties are empty.
            if len(set([getattr(hsp, attr) for hsp in hsps])) > 1:
                raise ValueError("Hit object can not contain HSPs with "
                                 "more than one %s." % attr)

        self._items = []
        for hsp in hsps:
            # validate each HSP
            self._validate_hsp(hsp)
            # and store it them as an instance attribute
            self.append(hsp)

    def __repr__(self):
        return "Hit(id=%r, query_id=%r, %r hsps)" % (self.id, self.query_id,
                                                     len(self))

    def __iter__(self):
        return iter(self.hsps)

    def __len__(self):
        return len(self.hsps)

    def __nonzero__(self):
        return bool(self.hsps)

    def __contains__(self, hsp):
        return hsp in self._items

    def __str__(self):
        lines = []

        # set query id line
        qid_line = 'Query: %s' % self.query_id
        if self.query_description:
            qid_line += trim_str('\n       %s' % self.query_description, 80,
                                 '...')
        lines.append(qid_line)

        # set hit id line
        hid_line = '  Hit: %s' % self.id
        if hasattr(self, 'seq_len'):
            hid_line += ' (%i)' % self.seq_len
        if self.description:
            hid_line += trim_str('\n       %s' % self.description, 80, '...')
        lines.append(hid_line)

        # set hsp line and table
        if not self.hsps:
            lines.append(' HSPs: ?')
        else:
            lines.append(
                ' HSPs: %s  %s  %s  %s  %s  %s' %
                ('-' * 4, '-' * 8, '-' * 9, '-' * 6, '-' * 15, '-' * 21))
            pattern = '%11s  %8s  %9s  %6s  %15s  %21s'
            lines.append(pattern % ('#', 'E-value', 'Bit score', 'Span',
                                    'Query range', 'Hit range'))
            lines.append(
                pattern %
                ('-' * 4, '-' * 8, '-' * 9, '-' * 6, '-' * 15, '-' * 21))
            for idx, hsp in enumerate(self.hsps):
                # evalue
                evalue = getattr_str(hsp, 'evalue', fmt='%.2g')
                # bitscore
                bitscore = getattr_str(hsp, 'bitscore', fmt='%.2f')
                # alignment length
                aln_span = getattr_str(hsp, 'aln_span')
                # query region
                query_start = getattr_str(hsp, 'query_start')
                query_end = getattr_str(hsp, 'query_end')
                query_range = '[%s:%s]' % (query_start, query_end)
                # max column length is 18
                query_range = trim_str(query_range, 15, '~]')
                # hit region
                hit_start = getattr_str(hsp, 'hit_start')
                hit_end = getattr_str(hsp, 'hit_end')
                hit_range = '[%s:%s]' % (hit_start, hit_end)
                hit_range = trim_str(hit_range, 21, '~]')
                # append the hsp row
                lines.append(pattern % (str(idx), evalue, bitscore, aln_span,
                                        query_range, hit_range))

        return '\n'.join(lines)

    def __getitem__(self, idx):
        # if key is slice, return a new Hit instance
        if isinstance(idx, slice):
            obj = self.__class__(self.hsps[idx])
            self._transfer_attrs(obj)
            return obj
        return self._items[idx]

    def __setitem__(self, idx, hsps):
        # handle case if hsps is a list of hsp
        if isinstance(hsps, (list, tuple)):
            for hsp in hsps:
                self._validate_hsp(hsp)
        else:
            self._validate_hsp(hsps)

        self._items[idx] = hsps

    def __delitem__(self, idx):
        del self._items[idx]

    ## hsp properties ##
    def _validate_hsp(self, hsp):
        """Validates an HSP object.

        Valid HSP objects have the same hit_id as the Hit object ID and the
        same query_id as the Hit object's query_id.

        """
        if not isinstance(hsp, HSP):
            raise TypeError("Hit objects can only contain HSP objects.")
        # HACK: to make validation during __init__ work
        if self._items:
            if self.id is not None:
                if hsp.hit_id != self.id:
                    raise ValueError("Expected HSP with hit ID %r, "
                                     "found %r instead." %
                                     (self.id, hsp.hit_id))
            else:
                self.id = hsp.hit_id

            if self.description is not None:
                if hsp.hit_description != self.description:
                    raise ValueError("Expected HSP with hit description %r, "
                                     "found %r instead." %
                                     (self.description, hsp.hit_description))
            else:
                self.description = hsp.hit_description

            if self.query_id is not None:
                if hsp.query_id != self.query_id:
                    raise ValueError("Expected HSP with query ID %r, "
                                     "found %r instead." %
                                     (self.query_id, hsp.query_id))
            else:
                self.query_id = hsp.query_id

            if self.query_description is not None:
                if hsp.query_description != self.query_description:
                    raise ValueError(
                        "Expected HSP with query description %r, "
                        "found %r instead." %
                        (self.query_description, hsp.query_description))
            else:
                self.query_description = hsp.query_description

    ## properties ##
    description = optionalcascade('_description', 'hit_description',
                                  """Hit description""")
    query_description = optionalcascade(
        '_query_description', 'query_description',
        """Description of the query that produced the hit""")
    id = optionalcascade('_id', 'hit_id', """Hit ID string.""")
    query_id = optionalcascade(
        '_query_id', 'query_id',
        """ID string of the query that produced the hit""")
    # returns all hsps
    hsps = allitems(doc="""HSP objects contained in the Hit""")

    @property
    def fragments(self):
        """HSPFragment objects contained in the Hit"""
        return [frag for frag in chain(*self._items)]

    ## public methods ##
    def append(self, hsp):
        """Adds a HSP object to the end of Hit.

        Parameters
        hsp -- HSP object to append.

        Any HSP object appended must have the same `hit_id` property as the
        Hit object's `id` property and the same `query_id` property as the
        Hit object's `query_id` property.

        """
        self._validate_hsp(hsp)
        self._items.append(hsp)

    def filter(self, func=None):
        """Creates a new Hit object whose HSP objects pass the filter
        function.

        Arguments:
        func -- Callback function that accepts a HSP object as its parameter,
                does a boolean check, and returns True or False.

        `filter` is analogous to Python's built-in `filter` function, except
        that instead of returning a list it returns a `Hit` object. Here is an
        example of using `filter` to select for HSPs having bitscores bigger
        than 60:

        >>> from Bio import SearchIO
        >>> qresult = SearchIO.parse('Blast/mirna.xml', 'blast-xml').next()
        >>> hit = qresult[3]
        >>> evalue_filter = lambda hsp: hsp.bitscore > 60
        >>> filtered_hit = hit.filter(evalue_filter)
        >>> len(hit)
        2
        >>> len(filtered_hit)
        1
        >>> print filtered_hit
        Query: 33211
               mir_1
          Hit: gi|301171322|ref|NR_035857.1| (86)
               Pan troglodytes microRNA mir-520c (MIR520C), microRNA
         HSPs: ----  --------  ---------  ------  ---------------  ---------------------
                  #   E-value  Bit score    Span      Query range              Hit range
               ----  --------  ---------  ------  ---------------  ---------------------
                  0   8.9e-20     100.47      60           [1:61]                [13:73]

        """
        hsps = filter(func, self.hsps)
        if hsps:
            obj = self.__class__(hsps)
            self._transfer_attrs(obj)
            return obj

    def index(self, hsp):
        """Returns the index of a given HSP object, zero-based.

        Arguments:
        hsp -- HSP object to be looked up.

        """
        return self._items.index(hsp)

    def map(self, func=None):
        """Creates a new Hit object, mapping the given function to its HSPs.

        Arguments:
        func -- Callback function that accepts a HSP object as its parameter and
                also returns a HSP object.

        `map` is analogous to Python's built-in `map` function. It is applied to
        all HSPs contained in the Hit object and returns a new Hit object.

        """
        if func is not None:
            hsps = map(func, self.hsps[:])  # this creates a shallow copy
        else:
            hsps = self.hsps[:]
        if hsps:
            obj = self.__class__(hsps)
            self._transfer_attrs(obj)
            return obj

    def pop(self, index=-1):
        """Removes and returns the HSP object at the specified index.

        Arguments:
        index -- Integer denoting the index of the HSP object to remove.

        """
        return self._items.pop(index)

    def sort(self, key=None, reverse=False, in_place=True):
        """Sorts the HSP objects.

        Arguments:
        key -- Function used to sort the HSP objects.
        reverse -- Boolean, whether to reverse the sorting or not.
        in_place -- Boolean, whether to perform sorting in place (in the same
                    object) or not (creating a new object).

        `sort` defaults to sorting in-place, to mimick Python's `list.sort`
        method. If you set the `in_place` argument to False, it will treat
        return a new, sorted Hit object and keep the initial one unsorted

        """
        if in_place:
            self._items.sort(key=key, reverse=reverse)
        else:
            hsps = self.hsps[:]
            hsps.sort(key=key, reverse=reverse)
            obj = self.__class__(hsps)
            self._transfer_attrs(obj)
            return obj