Skip to content

Feature

gffbase.feature.Feature

Feature(seqid: str = '.', source: str = '.', featuretype: str = '.', start='.', end='.', score: str = '.', strand: str = '.', frame: str = '.', attributes=None, extra=None, bin: int | None = None, id: str | None = None, dialect: dict | None = None, file_order: int | None = None, keep_order: bool = False, sort_attribute_values: bool = False)

Backward-compatible public Feature object.

Mirrors the legacy gffutils.Feature constructor and observable behavior: 1-based inclusive coordinates, list-wrapped multi-value attributes, dialect-faithful __str__ round-trip.

Source code in python/gffbase/feature.py
def __init__(
    self,
    seqid: str = ".",
    source: str = ".",
    featuretype: str = ".",
    start=".",
    end=".",
    score: str = ".",
    strand: str = ".",
    frame: str = ".",
    attributes=None,
    extra=None,
    bin: int | None = None,
    id: str | None = None,
    dialect: dict | None = None,
    file_order: int | None = None,
    keep_order: bool = False,
    sort_attribute_values: bool = False,
):
    self.seqid = seqid
    self.source = source
    self.featuretype = featuretype
    self.start = _coord_to_int(start)
    self.end = _coord_to_int(end)
    self.score = score if score is not None else "."
    self.strand = strand if strand is not None else "."
    self.frame = frame if frame is not None else "."
    self.bin = bin
    self.id = id
    self.dialect = dialect or {}
    self.file_order = file_order
    self.keep_order = keep_order
    self.sort_attribute_values = sort_attribute_values
    self._attributes_blob = None
    # Transient: populated by FeatureDB.merge() to expose the component
    # features that were merged into this one. Not persisted.
    self.children: list[Feature] | None = None

    fmt = (self.dialect or {}).get("fmt", "gff3")
    if isinstance(attributes, _LazyAttributes):
        self.attributes = attributes
    elif isinstance(attributes, (bytes, bytearray)):
        self._attributes_blob = bytes(attributes)
        self.attributes = _LazyAttributes(blob=self._attributes_blob, dialect_fmt=fmt)
    elif attributes is None:
        self.attributes = _LazyAttributes(initial={}, dialect_fmt=fmt)
    else:
        self.attributes = _LazyAttributes(initial=attributes, dialect_fmt=fmt)

    if extra is None:
        self.extra = []
    elif isinstance(extra, (bytes, bytearray)):
        text = extra.decode("utf-8", errors="replace")
        self.extra = text.split("\t") if text else []
    elif isinstance(extra, str):
        self.extra = extra.split("\t") if extra else []
    else:
        self.extra = list(extra)

chrom property writable

chrom: str

Alias for seqid (GFF column 1), the name gffutils uses.

Reading and writing either name affects the same underlying value.

stop property writable

stop: int | None

Alias for end (GFF column 5), the name gffutils uses.

None when the source line carried . -- such a feature has no coordinates and is skipped by region().

segments property

segments: tuple[FeatureSegment, ...]

This feature's physical input lines.

An ordinary feature is its own sole segment, so callers can write for seg in feature.segments without first asking whether the feature is discontinuous.

to_line

to_line(normalized: bool = False) -> str

Render this feature as one GFF line.

By default this is byte-faithful: if the original column 9 was never parsed or mutated, its bytes are re-emitted verbatim, so a file that round-trips through gffbase comes back unchanged. That is what str(feature) does too.

normalized=True instead re-renders column 9 from the parsed attribute mapping, applying the dialect's separators and the sort_attribute_values setting. This is what gffutils always does, so it is the form to use when comparing against the oracle -- at the cost of losing whatever the source file's exact spacing was.

Values are percent-encoded on this path, so a value containing ;, ,, =, & or % re-emits as valid GFF3. Spaces and non-ASCII are left alone, which is what the spec says and what the oracle does.

Source code in python/gffbase/feature.py
def to_line(self, normalized: bool = False) -> str:
    """Render this feature as one GFF line.

    By default this is byte-faithful: if the original column 9 was never
    parsed or mutated, its bytes are re-emitted verbatim, so a file that
    round-trips through gffbase comes back unchanged. That is what
    ``str(feature)`` does too.

    ``normalized=True`` instead re-renders column 9 from the parsed
    attribute mapping, applying the dialect's separators and the
    ``sort_attribute_values`` setting. This is what gffutils always does,
    so it is the form to use when comparing against the oracle -- at the
    cost of losing whatever the source file's exact spacing was.

    Values are percent-encoded on this path, so a value containing `;`,
    `,`, `=`, `&` or `%` re-emits as valid GFF3. Spaces and non-ASCII are
    left alone, which is what the spec says and what the oracle does.
    """
    return self._format_line(normalized)

to_lines

to_lines(normalized: bool = False) -> list[str]

Every physical line of this feature. One, unless it is multipart.

Source code in python/gffbase/feature.py
def to_lines(self, normalized: bool = False) -> list[str]:
    """Every physical line of this feature. One, unless it is multipart."""
    return [self.to_line(normalized)]

astuple

astuple(encoding=None)

Legacy 12-tuple shape used by the SQLite export path: (id, seqid, source, featuretype, start, end, score, strand, frame, attributes_json, extra_json, bin).

Source code in python/gffbase/feature.py
def astuple(self, encoding=None):
    """Legacy 12-tuple shape used by the SQLite export path:
    ``(id, seqid, source, featuretype, start, end, score, strand, frame,
        attributes_json, extra_json, bin)``.
    """
    attrs_dict = {k: list(v) for k, v in self.attributes.items()}
    return (
        self.id,
        self.seqid,
        self.source,
        self.featuretype,
        self.start,
        self.end,
        self.score,
        self.strand,
        self.frame,
        json.dumps(attrs_dict, separators=(",", ":")),
        json.dumps(self.extra, separators=(",", ":")) if self.extra else "[]",
        self.bin if self.bin is not None else self.calc_bin(),
    )

calc_bin

calc_bin(_bin: int | None = None) -> int | None

Compute and store this feature's UCSC bin.

Parameters:

  • _bin (int | None, default: None ) –

    Set the bin directly instead of deriving it.

Returns:

  • int | None

    The bin, or None when the feature has no coordinates.

Source code in python/gffbase/feature.py
def calc_bin(self, _bin: int | None = None) -> int | None:
    """Compute and store this feature's UCSC bin.

    Args:
        _bin: Set the bin directly instead of deriving it.

    Returns:
        The bin, or `None` when the feature has no coordinates.
    """
    if _bin is not None:
        self.bin = _bin
        return _bin
    if self.start is None or self.end is None:
        return None
    from gffbase._bins import bin_from_coords

    self.bin = bin_from_coords(self.start, self.end)
    return self.bin

sequence

sequence(fasta, use_strand: bool = True) -> str

Extract sequence from a FASTA path or a pyfaidx-style mapping.

Source code in python/gffbase/feature.py
def sequence(self, fasta, use_strand: bool = True) -> str:
    """Extract sequence from a FASTA path or a pyfaidx-style mapping."""
    if isinstance(fasta, str):  # pragma: no cover - pyfaidx is optional
        try:
            import pyfaidx
        except ImportError as e:
            raise ImportError(
                "Feature.sequence(path=...) requires the optional `pyfaidx` package"
            ) from e
        fa = pyfaidx.Fasta(fasta)
    else:
        fa = fasta
    if self.start is None or self.end is None:
        raise ValueError(
            f"cannot extract sequence for {self.id!r}: feature has no start/end coordinates"
        )
    seq = str(fa[self.seqid][self.start - 1 : self.end])
    if use_strand and self.strand == "-":
        seq = _revcomp(seq)
    return seq

ParsedFeature (parser-internal record)

The slotted dataclass the Rust+Python parser emits before features land in the database.

gffbase.feature.ParsedFeature dataclass

ParsedFeature(seqid: str, source: str, featuretype: str, start: int | None, end: int | None, score: str, strand: str, frame: str, attributes_blob: bytes, attributes_pairs: list[tuple[str, str, int]] = list(), extra: list[str] = list())

chrom property

chrom: str

Alias for seqid, the name gffutils uses.

stop property

stop: int | None

Alias for end, the name gffutils uses.

attributes_dict

attributes_dict() -> dict

Materialize attributes as {key: [values...]}. Preserves first-seen key order and multi-value ordering. Defers to attributes_pairs so the Rust and Python parsers remain trivially comparable.

Source code in python/gffbase/feature.py
def attributes_dict(self) -> dict:
    """Materialize attributes as `{key: [values...]}`. Preserves first-seen
    key order and multi-value ordering. Defers to `attributes_pairs` so the
    Rust and Python parsers remain trivially comparable."""
    out: dict = {}
    for k, v, _idx in self.attributes_pairs:
        out.setdefault(k, []).append(v)
    _drop_lone_empty_values(out)
    return out

from_tuple classmethod

from_tuple(tup) -> ParsedFeature

Build from the 11-tuple shape that the Rust extension yields.

Source code in python/gffbase/feature.py
@classmethod
def from_tuple(cls, tup) -> ParsedFeature:
    """Build from the 11-tuple shape that the Rust extension yields."""
    (
        seqid,
        source,
        featuretype,
        start,
        end,
        score,
        strand,
        frame,
        blob,
        pairs,
        extra,
    ) = tup
    return cls(
        seqid=seqid,
        source=source,
        featuretype=featuretype,
        start=start,
        end=end,
        score=score,
        strand=strand,
        frame=frame,
        attributes_blob=bytes(blob) if not isinstance(blob, bytes) else blob,
        attributes_pairs=[(k, v, int(i)) for (k, v, i) in pairs],
        extra=list(extra),
    )