API reference

Generated from the docstrings in the installed package, so this page cannot drift from the code. The sibling sites hand-write their reference pages; gffbase exports 90+ public symbols whose docstrings are themselves executed as tests, and a hand-written copy of that surface starts going stale the day it is written.

Name-mangled internals are omitted. Everything shown here is public API and is covered by the compatibility guarantees in Migrating from gffutils to gffbase.


The database

FeatureDB is the handle returned by gffbase.create_db() and is where almost every query lives.

class gffbase.interface.FeatureDB(dbfn, default_encoding: str = 'utf-8', keep_order: bool = False, pragmas: dict | None = None, sort_attribute_values: bool = False, text_factory=<class 'str'>, upgrade: str = 'auto', read_only: bool = False, _own_conn: bool | None = None)[source]

Bases: object

Drop-in successor to gffutils.FeatureDB.

warnings: list[dict]

Specification violations tolerated while building this database. Populated under mode="compat"; empty for a reopened database, which has no record of how it was built.

schema() str[source]

The database schema as SQL text.

A METHOD, not a property: gffutils documents db.schema() and callers write it that way. Exposing it as a property meant the documented call raised TypeError: 'str' object is not callable.

property read_only: bool

True if this handle refuses writes.

property closed: bool

True once close() has run.

close() None[source]

Release the DuckDB connection. Idempotent.

The lazily-created segment cursor is closed whether or not the connection is owned – gffbase created it via conn.cursor(), so gffbase closes it. The connection itself is closed only when this handle opened it: a caller who passed their own connection in still holds it afterwards.

validate(level: str = 'fast', **kwargs)[source]

Check this database’s structural invariants.

See gffbase.validate.validate_db(). Run automatically at the end of a strict-mode ingest; worth running by hand after update(), delete() or coalesce_multipart(), which are the operations that can leave the two halves of a discontinuous feature disagreeing.

count_features_of_type(featuretype: str | None = None) int[source]

Count features, optionally of one type.

Parameters:

featuretype – Restrict the count to this GFF column-3 value ("gene", "exon", …). None counts every feature.

Returns:

The number of matching features. A discontinuous feature counts once, however many input lines it was built from.

Example:

db.count_features_of_type()          # 6
db.count_features_of_type("exon")    # 3
featuretypes() Iterator[str][source]

Yield every distinct featuretype in the database, alphabetically.

Yields:

Each distinct GFF column-3 value, once.

Example:

sorted(db.featuretypes())   # ['CDS', 'exon', 'gene', 'mRNA']
seqids() Iterator[str][source]

Yield every distinct sequence id in the database, alphabetically.

Useful for checking naming convention before a region() query – chr1, 1 and NC_000001.11 are three different sequences as far as the database is concerned, and GENCODE, Ensembl and RefSeq each pick a different one.

Yields:

Each distinct GFF column-1 value, once.

all_features(limit: str | tuple | Feature | None = None, strand: str | None = None, featuretype: str | list[str] | None = None, order_by: str | None = None, reverse: bool = False, completely_within: bool = False) Iterator[Feature][source]

Iterate over every feature in the database.

Parameters:
  • limit – Restrict to a genomic region, as "seqid:start-end" or a (seqid, start, end) tuple. None scans everything.

  • strand – Restrict to "+", "-" or ".".

  • featuretype – One featuretype, or a list of them.

  • order_by – Column to sort by. One of id, seqid, source, featuretype, start, end, score, strand, frame, attributes, extra, file_order, length. Anything else raises ValueError – this is a whitelist, not a SQL fragment.

  • reverse – Sort descending.

  • completely_within – With limit, return only features contained entirely inside the region rather than merely overlapping it.

Yields:

Feature objects in file_order unless order_by says otherwise.

Raises:

ValueErrororder_by names something outside the whitelist.

Example:

for feature in db.all_features(featuretype="exon", order_by="start"):
    print(feature.id, feature.start)
method(*args, **kwargs) Iterator[Feature][source]

Alias for all_features, kept because gffutils defines one.

features_of_type(featuretype: str | list[str], limit: str | tuple | Feature | None = None, strand: str | None = None, order_by: str | None = None, reverse: bool = False, completely_within: bool = False) Iterator[Feature][source]

Iterate over every feature of one type (or several).

Equivalent to all_features(featuretype=...); both exist because gffutils has both.

Parameters:
  • featuretype – One featuretype ("exon"), or a list of them.

  • limit – Restrict to a genomic region – see all_features.

  • strand – Restrict to "+", "-" or ".".

  • order_by – Column to sort by – see all_features for the permitted names.

  • reverse – Sort descending.

  • completely_within – With limit, require full containment.

Yields:

Matching Feature objects.

Example:

genes = list(db.features_of_type("gene"))
both = list(db.features_of_type(["exon", "CDS"]))
region(region=None, seqid: str | None = None, start: int | None = None, end: int | None = None, strand: str | None = None, featuretype: str | list[str] | None = None, completely_within: bool = False) Iterator[Feature][source]

Features overlapping a genomic interval.

The interval can be given as a string, a tuple, or the three keyword arguments:

db.region("chr1:1000-2000")
db.region(("chr1", 1000, 2000))
db.region(seqid="chr1", start=1000, end=2000)
Parameters:
  • region (str or tuple) – "chrom:start-stop", or (chrom, start, stop).

  • seqid (optional) – The same interval, spelled out. Mutually exclusive with region.

  • start (optional) – The same interval, spelled out. Mutually exclusive with region.

  • end (optional) – The same interval, spelled out. Mutually exclusive with region.

  • strand ({"+", "-", "."}, optional) – Restrict to one orientation.

  • featuretype (str or list of str, optional) – Restrict to one or several types.

  • completely_within (bool) – False (default) returns anything that OVERLAPS the interval; True returns only features contained entirely within it.

  • here (Which index answers the query is chosen)

  • caller (not by the)

  • specified (R-tree when one was built and the interval is fully)

  • the

  • -- (tests/test_spatial_parity.py asserts they return the same features)

  • --

  • decision (so this is a planner)

  • one. (not a behavioural)

  • and (A feature with no coordinates (a GFF row carrying . in columns 4)

  • returned. (5) is not in coordinate space and is never)

to_table(featuretype: str | list[str] | None = None, *, format: str = 'arrow', limit: str | tuple | Feature | None = None, strand: str | None = None, order_by: str | None = None, reverse: bool = False, completely_within: bool = False) Any[source]

Return the whole database (or a slice of it) as one table.

The columnar counterpart to all_features(): same filters, but the result is a pyarrow.Table / pandas.DataFrame / polars.DataFrame instead of a stream of Feature objects, and no Feature is constructed at any layer.

For “give me every exon as a dataframe”, the row-by-row API pays a Python object per row – millions of them on a whole-genome corpus – and that allocation dominates everything else. This is one query and one hand-off.

Parameters:
  • featuretype – One featuretype, or a list of them. None is everything.

  • format"arrow" (default, zero-copy), "df" for pandas, or "polars".

  • limit – Restrict to a genomic region, as "seqid:start-end" or a (seqid, start, end) tuple.

  • strand – Restrict to "+", "-" or ".".

  • order_by – Column to sort by – the same whitelist all_features accepts.

  • reverse – Sort descending.

  • completely_within – With limit, require full containment.

Returns:

A table in the shape named by format, one row per feature.

Columns are those of the features table:

id, seqid, source, featuretype, start, end, score, strand,
frame, file_order
Raises:
  • ValueErrororder_by names something outside the whitelist, or format is not one of the three.

  • ImportErrorformat="df"/"polars" without that package.

Example:

exons = db.to_table("exon", format="arrow")
df = db.to_table(["exon", "CDS"], format="df", limit="chr1:1-10000")

Note

Attributes are not included: they are a long-form table, so flattening them would either invent a column per key or collapse multi-valued keys. Query the attributes table with execute() when you need them.

region_batched(regions, featuretype: str | list[str] | None = None, completely_within: bool = False, format: str = 'arrow', explode_segments: bool = False, on_invalid: str = 'raise')[source]

Bulk overlap query. Performs a SINGLE spatial JOIN between every input region and the features table, returning a column-oriented result that maps each query (query_idx) back to its overlapping features.

query_idx indexes regions as you passed it. That is the whole contract of the column – it is how a caller reassembles per-query groups without re-issuing N queries.

Parameters:
  • regions (iterable of (seqid, start, end) | str | Feature) – Each item is normalized through _normalize_region_args; the same four input shapes accepted by region() are accepted here.

  • featuretype (str | list[str] | None) – Optional features.featuretype filter applied to all regions.

  • completely_within (bool) – If True, only features fully contained in the region are returned (default False — overlap is sufficient).

  • format ("arrow" | "df" | "polars") – Return shape (default "arrow").

  • on_invalid ({"raise", "skip"}) – What to do with an item that does not normalize to a region. "raise" (default) raises ValueError naming the offending position and value. "skip" drops it while leaving every other item’s query_idx at its position in regions, so the gap is visible rather than silently closed up.

  • explode_segments (bool) –

    Emit one row per physical INPUT LINE rather than one per logical feature, adding a seg_idx column. A discontinuous feature then contributes a row per segment, each with its own coordinates, score and phase – which is what a caller writing a coverage track or exporting to a line-oriented format actually needs.

    Offered on the tabular APIs only, never on region() / children() / all_features(): those must keep yielding Feature objects, and letting FeatureSegment rows leak into them would corrupt legacy consumers.

  • columns:: (Result) – query_idx, query_seqid, query_start, query_end, id, seqid, source, featuretype, start, end, score, strand, frame, file_order – plus seg_idx when explode_segments is set.

children(id, level: int | None = None, featuretype: str | list[str] | None = None, order_by=None, reverse: bool = False, limit=None, completely_within: bool = False) Iterator[Feature][source]

Descendants of a feature, in hierarchy order.

Parameters:
  • id (str or Feature) – The anchor. A Feature built by hand has id is None and is rejected rather than silently matching nothing.

  • level (int, optional) – Only descendants exactly this many steps down – 1 is direct children. None (default) walks the whole subtree.

  • featuretype (str or list of str, optional)

  • order_by (str or sequence of str, optional) – A whitelisted sort key; anything else raises ValueError naming the accepted set. See docs/source/content/advisory_sql_injection.rst for why this is not a free-text field.

  • reverse (bool) – Applies to every key, not just the last.

  • limit (str or tuple, optional) – Restrict to a genomic interval, as region() accepts it.

  • completely_within (bool) – With limit, require containment rather than overlap.

  • the (Reads from the materialized closure table when level is within)

  • depth (database's recorded)

  • a (and falls back to a recursive CTE when)

  • cycle-safe (deeper walk is asked for. Both are)

  • once (containing a loop is traversed)

  • runs (not until the depth budget)

  • out.

parents(id, level: int | None = None, featuretype: str | list[str] | None = None, order_by=None, reverse: bool = False, completely_within: bool = False, limit=None) Iterator[Feature][source]

Ancestors of a feature, in hierarchy order.

The mirror of children(), taking the same arguments and making the same routing decision. level=1 is direct parents.

GFF3 permits a feature to name several Parent attributes, so the hierarchy is a DAG rather than a tree and one ancestor can be reachable by more than one path. Each is returned once.

children_batched(feature_ids, level: int | None = None, featuretype: str | list[str] | None = None, format: str = 'arrow', explode_segments: bool = False)[source]

Bulk children lookup. Returns the descendants of ALL feature_ids in a single vectorized DuckDB query.

Parameters:
  • feature_ids (iterable of str | Feature) – Anchors. May contain Feature objects or raw ID strings.

  • level (int | None) – None → all descendants (closure cache when materialized, otherwise dynamic CTE). Integer → exact-depth point lookup.

  • featuretype (str | list[str] | None) – Optional filter on features.featuretype.

  • format ("arrow" | "df" | "polars") – Return shape (default "arrow"pyarrow.Table).

  • explode_segments (bool) – Emit one row per physical INPUT LINE rather than one per logical feature, adding a seg_idx column. Tabular APIs only – see region_batched().

  • columns:: (Result) – anchor (the parent ID supplied), descendant_id, seqid, source, featuretype, start, end, score, strand, frame, file_order, depth – plus seg_idx when

  • explode_segments.

parents_batched(feature_ids, level: int | None = None, featuretype: str | list[str] | None = None, format: str = 'arrow', explode_segments: bool = False)[source]

Bulk parents lookup. Mirrors children_batched but walks the closure / edges in the reverse direction.

delete(features: str | Feature | Iterable[str | Feature], make_backup: bool = True, **kwargs) FeatureDB[source]

Delete features, and everything that referenced them.

Removes the rows from features, attributes, segments and edges, then rebuilds the transitive closure so no path through a deleted node survives – deleting a transcript really does remove its exons from its gene’s descendants.

Parameters:
  • features – A feature id, a Feature, or an iterable of either.

  • make_backup – Accepted for gffutils compatibility and currently ignored; no .bak is written.

Returns:

self, so calls can be chained.

Raises:

ReadOnlyError – The database was opened with read_only=True.

Example:

db.delete("transcript_1")
db.delete([f.id for f in db.features_of_type("CDS")])
update(data: Iterable, make_backup: bool = True, **kwargs) FeatureDB[source]

Add features to an existing database.

Appends to features, attributes and edges, then rebuilds the transitive closure and re-reads the corpus statistics the relational dispatcher routes on.

Parameters:
  • data – An iterable of Feature or ParsedFeature objects, or another FeatureDB whose features are copied in.

  • make_backup – Accepted for gffutils compatibility and currently ignored; no .bak is written.

Returns:

self, so calls can be chained.

Raises:
  • ReadOnlyError – The database was opened with read_only=True.

  • TypeError – An item is neither a Feature nor a ParsedFeature.

Example:

introns = list(db.create_introns())
db.update(introns)
add_relation(parent, child, level: int = 1, parent_func=None, child_func=None) FeatureDB[source]

Link one parent to one child.

parent_func / child_func receive (parent, child), and whatever they RETURN is written back to the database. That is upstream’s contract and the reason assign_child returns the child: the callback exists to edit an attribute (Parent=) that then has to be persisted. Previously the return value was discarded and nothing was written, so child_func=assign_child set an attribute on a throwaway object.

A string id is resolved to a Feature first, so callbacks fire whether the caller passed objects or ids – they used to be skipped silently for ids.

add_relations(pairs, level: int = 1, parent_func=None, child_func=None) FeatureDB[source]

add_relation for many pairs, with ONE closure rebuild.

Deriving the closure costs a recursive CTE over every edge, so doing it per pair makes a bulk operation quadratic. merge_all links every component of every merged feature and is the caller that made this necessary.

interfeatures(features, new_featuretype=None, merge_attributes: bool = True, numeric_sort: bool = False, dialect=None, attribute_func=None, update_attributes=None)[source]

The gaps between consecutive features.

attribute_func takes ONE argument – an attribute mapping – and returns one, matching the oracle. It is applied to each neighbour’s attributes before they are merged, not to the merged result. The previous three-argument (prev, cur, attrs) form meant any gffutils caller passing a callback got a TypeError.

property derived_source: str

source for features this database derives rather than reads.

gffutils_derived under compat so ported scripts that filter on it keep working; gffbase_derived under strict, which reports honest provenance.

merge(features, merge_criteria=None, multiline: bool = False)[source]

Collapse runs of features that satisfy every criterion.

Consumes features in the order given. That is the oracle’s contract and it matters: merge_all supplies a specific merge_order, and re-sorting here (which this used to do) silently discarded it.

A feature that merged with nothing is yielded unchanged with children set to no_children, so a caller can tell a real merge from a pass-through by truthiness. merge_all depends on exactly that.

merge_all(merge_order=('seqid', 'featuretype', 'strand', 'start'), merge_criteria=None, featuretypes_groups=(None,), exclude_components: bool = False) list[Feature][source]

Merge everything in the database and write the results back.

Three things were wrong here and all three were silent. The method returned every input feature including ones that merged with nothing, so a caller could not tell what had actually been merged; it persisted nothing, despite documenting that “the resulting records are added to the database”; and it accepted exclude_components and ignored it, so asking for the components to be removed did nothing at all.

create_introns(exon_featuretype: str = 'exon', grandparent_featuretype: str | None = 'gene', parent_featuretype: str | None = None, new_featuretype: str = 'intron', merge_attributes: bool = True, numeric_sort: bool = False) Iterator[Feature][source]

Introns, computed per transcript.

grandparent_featuretype="gene" descends one level first and computes the gaps within each transcript separately. Treating the gene as the direct anchor – which this used to do – pools the exons of every isoform into one sorted list, so the “introns” of a multi-isoform gene were computed across transcript boundaries and were not introns of anything.

create_splice_sites(exon_featuretype: str = 'exon', grandparent_featuretype: str | None = 'gene', parent_featuretype: str | None = None, merge_attributes: bool = True, numeric_sort: bool = False) Iterator[Feature][source]

The two-base splice sites flanking each intron.

A splice site is a dinucleotide – GT at the donor, AG at the acceptor – so these are 2 bp features, not the 1 bp ones this used to emit. They are typed by their position in the transcript rather than in the genome, so the left site of a minus-strand transcript is its 3’ site.

Emission order is every left site, then every right site, matching the oracle; the intron’s merged attributes are carried through with the ID prefixed by the featuretype so the two sites of one intron differ.

children_bp(feature: str | Feature, child_featuretype: str = 'exon', merge: bool = False, merge_criteria: Sequence | None = None, **kwargs) int[source]

Total base pairs covered by a feature’s children.

Parameters:
  • feature – The parent, as an id or a Feature.

  • child_featuretype – Which children to measure.

  • merge – Merge overlapping children first, so shared bases are counted once. Without it, overlapping children double-count.

  • merge_criteria – Predicates controlling what may merge; see gffbase.merge_criteria. Defaults to same seqid, strand and featuretype with inclusive overlap.

Returns:

The summed length in base pairs.

Raises:
  • ValueError – The removed ignore_strand argument was passed.

  • TypeError – Any other unexpected keyword argument.

Example:

db.children_bp("transcript_1", child_featuretype="exon", merge=True)
bed12(feature: str | Feature, block_featuretype: Sequence[str] = ('exon',), thick_featuretype: Sequence[str] = ('CDS',), thin_featuretype: Sequence[str] | None = None, name_field: str = 'ID', color: str | None = None) str[source]

Render a feature and its children as one BED12 line.

Parameters:
  • feature – The parent, as an id or a Feature.

  • block_featuretype – Child types that become BED blocks (exons).

  • thick_featuretype – Child types that define the thick region (coding sequence).

  • thin_featuretype – Child types that define the thin region. When given, it is honoured rather than inferred.

  • name_field – Attribute used for BED column 4. Falls back to the feature id when absent.

  • color – RGB string for column 9, e.g. "255,0,0".

Returns:

A tab-separated BED12 line, without a trailing newline.

Note

A feature with no thick children is emitted as entirely thick. blockSizes and blockStarts carry no trailing comma.

Example:

print(db.bed12("transcript_1"))
iter_by_parent_childs(featuretype: str = 'gene', level: int | None = None, order_by: str | None = None, reverse: bool = False, completely_within: bool = False) Iterator[list[Feature]][source]

Group the database by parent, yielding one list per parent.

Parameters:
  • featuretype – The parent featuretype to group by.

  • level – How deep to collect children. None takes the whole subtree; 1 takes direct children only.

  • order_by – Column to sort parents by – see all_features.

  • reverse – Sort parents descending.

  • completely_within – Passed through to the child query.

Yields:

A list per parent, the parent first followed by its children.

Example:

for group in db.iter_by_parent_childs("gene"):
    gene, children = group[0], group[1:]

Features with an attribute VALUE matching text, case-insensitively.

text is SQL LIKE syntax, so % and _ are wildcards; a bare string matches as a substring, which is what a caller searching for an accession expects.

gffutils’ CLI calls db.attribute_search(...), but no such method exists anywhere in gffutils – only in that call site and in an obsolete test file – so gffutils-cli search raises AttributeError on every invocation. This is a working implementation rather than a port of one.

The search is over the long-form attributes table, not over the raw column-9 blob, so it matches DECODED values: searching for a;b finds a feature whose source said a%3Bb.

execute(query: str)[source]

Execute arbitrary SQL. Returns DuckDB’s relation cursor. SQLite-style queries against features_compat and relations_compat views are supported; see compat_views.sql.

Deliberately NOT guarded against read_only: this is the escape hatch, the SQL is the caller’s, and DuckDB’s own refusal names the statement it rejected, which is more use here than a generic message from us.

analyze() None[source]

Refresh DuckDB’s planner statistics for this database.

Worth running once after a large update(); the planner otherwise keeps costing queries against the shape the database had at ingest.

Raises:

ReadOnlyError – The database was opened with read_only=True.

set_pragmas(pragmas: dict) None[source]

Apply DuckDB settings, ignoring pragmas that only SQLite has.

Legacy callers pass constants.default_pragmassynchronous, journal_mode, main.page_size, main.cache_size – none of which DuckDB has. Those are skipped, which is what makes a gffutils script run here unchanged.

Both the name and the value used to be interpolated straight into the statement, and the whole loop body sat inside except duckdb.Error: continue. DuckDB executes trailing statements, so

db.set_pragmas({“threads”: “1; DROP TABLE attributes”})

dropped the table – and because the exception was swallowed, a payload that failed was silent too. Names are now matched against DuckDB’s own settings catalog and values rendered as SQL literals, so nothing a caller supplies reaches the parser as syntax.

Matching the live catalog rather than a hardcoded list means the check tracks whatever DuckDB build is installed, instead of going stale and rejecting settings a newer version added.


Building a database

gffbase.create_db.create_db(data, dbfn, id_spec=None, force=False, verbose=False, checklines=10, merge_strategy='error', transform=None, gtf_transcript_key='transcript_id', gtf_gene_key='gene_id', gtf_subfeature='exon', force_gff=False, force_dialect_check=False, from_string=False, keep_order=False, text_factory=<class 'str'>, force_merge_fields=None, pragmas=None, sort_attribute_values=False, dialect=None, _keep_tempfiles=False, infer_gene_extent=True, disable_infer_genes=False, disable_infer_transcripts=False, mode='compat', validation=None, on_error=None, on_multipart_conflict='error', **kwargs) FeatureDB[source]

Create a database from a GFF3/GTF source.

Parameters:
  • data – Path to a GFF3/GTF file (optionally gzipped), or the file contents themselves when from_string=True.

  • dbfn – Destination database path, or ":memory:".

  • id_spec – Primary-key policy. None uses the per-dialect default: "ID" for GFF3, {"gene": "gene_id", "transcript": "transcript_id"} for GTF. May also be an attribute name, an ordered list of names (first match wins), a featuretype -> name mapping, or a callable returning an id (or "autoincrement:BASE"). A name of the form ":seqid:" reads that GFF column instead of an attribute.

  • force – Overwrite dbfn if it exists. Without it, an existing file raises.

  • verbose – Progress reporting. "debug" selects DEBUG level.

  • checklines – Lines sampled to infer the dialect.

  • merge_strategy – What to do when two features resolve to the same id: "error", "warning", "merge", "create_unique" or "replace".

  • transform – Callable applied to each feature. Returning anything falsy drops it.

  • gtf_transcript_key – Attribute names used to reconstruct GTF hierarchy.

  • gtf_gene_key – Attribute names used to reconstruct GTF hierarchy.

  • gtf_subfeature – Attribute names used to reconstruct GTF hierarchy.

  • force_gff – Skip format autodetection and treat the input as GFF.

  • force_dialect_check – Infer the dialect from every line rather than the first checklines. Mutually exclusive with dialect.

  • from_string – Treat data as file contents rather than a path.

  • keep_order – Preserve attribute order when features are rendered.

  • text_factory – Text coercion applied to values read back out.

  • force_merge_fields – With merge_strategy="merge", fields allowed to differ and be combined. start/end are rejected: they must stay numeric.

  • pragmas – Database pragmas.

  • sort_attribute_values – Sort attribute values when features are rendered.

  • dialect – Explicit dialect, bypassing inference.

  • infer_gene_extent – Deprecated. False sets both disable_infer_* flags.

  • disable_infer_genes – Skip synthesizing GTF gene/transcript rows from their children.

  • disable_infer_transcripts – Skip synthesizing GTF gene/transcript rows from their children.

  • mode ({"compat", "strict"}) –

    "compat" (default) applies gffutils’ rule set, so files that break the GFF3 specification load exactly as they do under gffutils, with every violation recorded in FeatureDB.warnings. "strict" applies the full NCBI specification and rejects violations.

    "strict" is also the only mode that fuses several lines sharing one ID into a single discontinuous feature. That is deliberate: it is new behaviour rather than gffutils behaviour, since gffutils’ merge_strategy="merge" requires all eight non-attribute columns to match and so never merges a genuine split feature.

  • validation ({"gffutils", "ncbi"} | None) – Which rule set to apply, overriding the one mode implies. None (default) takes the mode’s. Use it to keep gffutils-compatible handling while applying the full NCBI rules – the combination validation="ncbi", on_error="warn" audits a file without stopping on it, leaving every violation in FeatureDB.warnings.

  • on_error ({"raise", "warn"} | None) – What a rejected line does, overriding the one mode implies. "raise" stops at the first violation; "warn" records it and carries on. None (default) takes the mode’s.

  • on_multipart_conflict ({"error", "split"}) – Under mode="strict", what to do when lines sharing an ID disagree on seqid, source, featuretype or strand – which GFF3 requires the segments of a discontinuous feature to share. "error" (default) raises MultipartConstraintError naming the diverging column and both line numbers; "split" partitions the run by those four columns, the lowest file_order keeping the bare id.

Return type:

FeatureDB

gffbase.ingest.from_file(path: str, dbfn: str = ':memory:', **kwargs) tuple[DuckDBPyConnection, IngestStats][source]

Ingest a GFF3 or GTF file into a DuckDB database, atomically.

The destination either does not exist or is a complete database: the ingest writes to a scratch file beside it and renames on success, so a failure part-way leaves nothing behind and an existing database is not destroyed until its replacement is finished.

Before this, a failed ingest left a file that was valid DuckDB but held no data and no meta rows. Retrying then refused with “already exists. Pass force=True”, and opening it silently produced an empty database that reported itself as current – the two ways this could mislead someone were to make them force-overwrite something, or to believe their data had loaded.

See _build_database for the ingest itself and the full argument list.

class gffbase.ingest.IngestStats(n_features_raw: int = 0, n_features_synthetic_transcripts: int = 0, n_features_synthetic_genes: int = 0, n_attributes: int = 0, n_edges: int = 0, n_closure_rows: int = 0, rtree_built: bool = False, fmt: str = 'gff3', dialect: dict = None, directives: list[str] = None, warnings: list[dict] = None, n_skipped: int = 0, n_multipart: int = 0)[source]

Bases: object

Reported back to the caller for benchmarking and tests.

n_features_raw: int = 0
n_features_synthetic_transcripts: int = 0
n_features_synthetic_genes: int = 0
n_attributes: int = 0
n_edges: int = 0
n_closure_rows: int = 0
rtree_built: bool = False
fmt: str = 'gff3'
dialect: dict = None
directives: list[str] = None
warnings: list[dict] = None

Specification violations the parser tolerated. Non-empty only under mode="compat", where a violating record is kept and annotated rather than rejected – so the caller gets gffutils’ data plus a diagnostic gffutils never offered.

n_skipped: int = 0

Records dropped by a transform callback or by merge_strategy.

n_multipart: int = 0

Features assembled from more than one input line. Non-zero only under mode="strict", the only mode that fuses.


Features

class gffbase.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)[source]

Bases: object

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.

is_multipart: ClassVar[bool] = False

False for every ordinary feature. MultipartFeature overrides it. A ClassVar rather than an instance attribute so the singleton case – which is every feature in almost every file – costs nothing per object.

n_segments: int = 1

How many physical input lines this feature was built from.

A class attribute, so an ordinary feature carries no per-instance cost for it – but deliberately NOT a ClassVar, because MultipartFeature shadows it with a real slot and assigns per instance. Declaring it ClassVar would make that assignment a type error.

seqid
source
featuretype
start
end
score
strand
frame
bin
id
dialect
file_order
keep_order
sort_attribute_values
children: list[Feature] | None
attributes
extra
property chrom: str

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

Reading and writing either name affects the same underlying value.

property 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().

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(normalized: bool = False) str[source]

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.

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

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

astuple(encoding=None)[source]

Legacy 12-tuple shape used by the SQLite export path.

The elements, in order:

id, seqid, source, featuretype, start, end, score, strand,
frame, attributes_json, extra_json, bin
calc_bin(_bin: int | None = None) int | None[source]

Compute and store this feature’s UCSC bin.

Parameters:

_bin – Set the bin directly instead of deriving it.

Returns:

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

sequence(fasta, use_strand: bool = True) str[source]

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

class gffbase.feature.MultipartFeature(*args, n_segments: int = 1, segments=None, segment_loader=None, **kwargs)[source]

Bases: Feature

A logical feature assembled from more than one input line.

Its own start/end are the ENVELOPE – MIN(segment.start) and MAX(segment.end) – and every inherited method operates on that envelope, so a caller that knows nothing about discontinuous features sees exactly the gffutils behaviour for a feature spanning that range.

Consequently len(f) is the envelope span, matching Feature. covered_length is the different, new quantity.

is_multipart: ClassVar[bool] = True

False for every ordinary feature. MultipartFeature overrides it. A ClassVar rather than an instance attribute so the singleton case – which is every feature in almost every file – costs nothing per object.

n_segments: int

How many physical input lines this feature was built from.

A class attribute, so an ordinary feature carries no per-instance cost for it – but deliberately NOT a ClassVar, because MultipartFeature shadows it with a real slot and assigns per instance. Declaring it ClassVar would make that assignment a type error.

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.

property covered_length: int

Total length actually covered, with the gaps excluded.

Differs from len(self), which is the envelope span. For a CDS split across two 100 bp exons 700 bp apart, len is 900 and this is 200.

Segments of a discontinuous feature must not overlap; if a malformed file provides overlapping ones, the shared bases are counted twice.

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

Every input line of this feature, in file order.

class gffbase.feature.FeatureSegment(*args, seg_idx: int = 0, **kwargs)[source]

Bases: Feature

One physical input line of a discontinuous feature.

Carries its OWN coordinates, score, phase and column 9 – per-segment CDS phase is the main reason the storage exists – while seqid, source, featuretype and strand come from the logical feature, which by definition shares them.

self.id is the LOGICAL id, so db[seg.id] finds the whole feature. The segment’s own ID= is preserved byte-for-byte in the attributes blob, so str(segment) reproduces the input line exactly.

seg_idx

0-based position in FILE order, not coordinate order. GFF3 does not require segments to be sorted, and to_lines() has to reproduce the input.

class gffbase.feature.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]]'=<factory>, extra: 'list[str]' = <factory>)[source]

Bases: object

The immutable parse result the engines hand to the ingest layer. Its fields mirror Feature’s and are not repeated here: documenting both made every bare reference to seqid, start or end ambiguous across the two classes, which is a warning under -W and a coin-flip link for a reader.

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]]
extra: list[str]
property chrom: str

Alias for seqid, the name gffutils uses.

property stop: int | None

Alias for end, the name gffutils uses.

attributes_dict() dict[source]

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.

classmethod from_tuple(tup) ParsedFeature[source]

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

The fields are stored as handed over. rust/src/lib.rs already builds a PyBytes for the blob, a PyList of PyTuple(str, str, int) for the pairs, and a fresh PyList for extra – so the coercions that used to sit here (bytes(blob), int(i) in a rebuilt list comprehension, list(extra)) never converted anything. They allocated a second copy of every attribute list, 6,068,892 times on GENCODE: 20.4 s in the comprehension alone, and 13.5% of the whole parse stage.

Nothing else can reach this method. parser.py calls it only when self._native is true; the pure-Python fallback constructs ParsedFeature directly with values it has already normalized. The coercions were defending against a caller that does not exist.


Parsing

gffbase.parser.parse_gff(path: str, *, checklines: int = 10, force_dialect_check: bool = False, force_gff: bool = False, strict: bool = True, validation: str = 'ncbi', engine: str | None = 'auto') _Iterator[source]

Parse a GFF3/GTF file (plain text or .gz).

Returns an iterator of ParsedFeature plus .dialect(), .directives() and .warnings accessors.

Parameters:
  • validation ({"ncbi", "gffutils"}) – Which rule set to apply. "ncbi" (default here) is the full GFF3 specification. "gffutils" is the compatibility profile used by create_db: every rule still runs, but a violation annotates the record instead of rejecting it, because real annotation files break the spec routinely and gffutils reads them anyway.

  • strict (bool) – What a rejection does. True (default) raises GFFFormatError on the first offending line; False skips it and records it in iterator.warnings. Under validation="gffutils" nothing is rejected, so this only affects lines that cannot be parsed at all.

gffbase.parser.parse_bytes(data: bytes, *, checklines: int = 10, force_dialect_check: bool = False, force_gff: bool = False, strict: bool = True, validation: str = 'ncbi', engine: str | None = 'auto') _Iterator[source]
gffbase.parser.detect_dialect(path: str, *, checklines: int = 10, engine: str | None = 'auto') dict[source]
gffbase.parser.native_available() bool[source]

True if the compiled extension is importable.


Reading and writing

class gffbase.iterators.DataIterator(data, checklines: int = 10, transform=None, force_dialect_check: bool = False, from_string: bool = False, **kwargs)[source]

Bases:

Legacy factory. Returns an iterator yielding Feature.

Dispatches on the input, the way the subclasses below have always described and the way gffutils.DataIterator behaves:

  • from_string=Truedata is the GFF text itself.

  • a URL – fetched to a temporary file first (_UrlIterator).

  • any other path-like – read from disk, gzipped or not (_FileIterator).

  • an iterable of Feature / ParsedFeature – yielded straight back (_FeatureIterator), so a generator can be piped into create_db or FeatureDB.update without being written to a file first.

The dispatch was missing: every input went to _DataIterator, which hands whatever it gets to parse_gff(path). So a URL was opened as a filename and an in-memory feature list raised, while the subclasses that exist to handle both sat unreachable and their docstrings described a behaviour the factory did not have.

class gffbase.gffwriter.GFFWriter(out: str | PathLike | IOBase, with_header: bool = True, in_place: bool = False)[source]

Bases: object

Write Feature records back to a GFF/GTF file.

write_rec(rec: Feature | str) None[source]

Write one record, followed by a newline.

Parameters:

rec – A Feature, or a pre-formatted GFF line as a string. A trailing newline on a string is not doubled.

write_recs(recs: Iterable) None[source]

Write many records, in the order given.

Parameters:

recs – An iterable of Feature objects or GFF line strings.

write_gene_recs(db: FeatureDB, gene_id: str | Feature) None[source]

Write a gene and its ENTIRE subtree, sorted by start.

Parameters:
  • db – The FeatureDB to read from.

  • gene_id – The gene, as an id or a Feature.

write_mRNA_children(db: FeatureDB, mrna_id: str | Feature) None[source]

Write a transcript and its DIRECT children, sorted by start.

Parameters:
  • db – The FeatureDB to read from.

  • mrna_id – The transcript, as an id or a Feature.

write_exon_children(db: FeatureDB, exon_id: str | Feature) None[source]

Write an exon and its direct children, sorted by start.

Parameters:
  • db – The FeatureDB to read from.

  • exon_id – The exon, as an id or a Feature.

close() None[source]

Flush, and close only a handle this writer opened.

A stream the caller passed in belongs to the caller. Closing it – as this used to, and as gffutils still does – means GFFWriter(sys.stdout) shuts stdout down for the whole process, so anything written afterwards raises ValueError: I/O operation on closed file. It is flushed instead, which is the part that actually matters for the output being complete.

gffbase.sqlite_export.export_sqlite(con: DuckDBPyConnection, path: str, force: bool = False) str[source]

Write a legacy SQLite .db from the given DuckDB connection.

The result is openable by real gffutils. Because gffutils has no way to represent a discontinuous feature, one is flattened back into the N features gffutils itself would have made – see _legacy_ids. The grouping is not lost: duplicates records (logical_id, legacy_id) for every segment past the first, which is both what that table means and how a re-import can rediscover it.

Returns the absolute path on success.


Merge criteria

Predicates consumed by FeatureDB.merge and merge_all.

Merge predicates — pure functions consumed by FeatureDB.merge.

Signature: (acc: Feature, cur: Feature, components: list[Feature]) -> bool. acc is the running accumulator; cur is the candidate to fold in; components is the list of already-folded features. All callables return True if the candidate should be merged into the accumulator.

Mirrors the legacy gffutils.merge_criteria module.

gffbase.merge_criteria.seqid(acc, cur, components)[source]

Same sequence. Almost always wanted – omitting it merges features on different chromosomes into one.

gffbase.merge_criteria.strand(acc, cur, components)[source]

Same orientation. Leave this out to merge regardless of strand.

gffbase.merge_criteria.feature_type(acc, cur, components)[source]

Same featuretype, so exons do not merge with CDSs.

gffbase.merge_criteria.exact_coordinates_only(acc, cur, components)[source]

Identical span. Merges duplicates, nothing else.

gffbase.merge_criteria.overlap_end_inclusive(acc, cur, components)[source]

cur starts within acc, or immediately after it.

“Immediately after” is the + 1: two features that abut with no gap are adjacent, not overlapping, and merging them is normally what a caller wants when collapsing exon runs.

gffbase.merge_criteria.overlap_start_inclusive(acc, cur, components)[source]

cur ends within acc, or immediately before it.

gffbase.merge_criteria.overlap_any_inclusive(acc, cur, components)[source]

Either end qualifies.

gffbase.merge_criteria.overlap_end_threshold(threshold: int)[source]

cur starts within the accumulator, allowing a gap of threshold.

Changed in 0.2.0. This and the two factories below are RANGE tests, not distance tests. They used to compute abs(acc.end - cur.start) <= threshold, which reads naturally but answers a different question: it asks how far apart two boundaries are, and so rejects a feature lying entirely inside the accumulator – the most unambiguous overlap there is.

Concretely, with acc = (1, 100), cur = (50, 200), threshold = 5, the old form gave abs(100 - 50) = 50 <= 5 -> False, and these two plainly overlapping features did not merge. This form gives 1 <= 50 <= 105 -> True.

If you call merge or merge_all with one of these, the set of features that merge has changed. Nothing else in the merge machinery did.

gffbase.merge_criteria.overlap_start_threshold(threshold: int)[source]

cur ends within the accumulator, allowing a gap of threshold.

gffbase.merge_criteria.overlap_any_threshold(threshold: int)[source]

Either end qualifies.


Validation and migration

Post-ingest invariants.

Schema v2 spreads one logical feature across two tables, and the interesting failure mode is not a crash – it is a database that answers every query plausibly and wrongly. The worst case is INV-5: if a fused feature’s envelope is narrower than its segments, region() silently stops returning it. Nothing raises, no count looks odd, and the feature is simply gone from results.

So these run automatically at the end of a strict-mode ingest, and can be run by hand at any time. Every check is a single set-based query returning a count plus a few examples, so validating a GENCODE-scale database is a handful of aggregate scans rather than a row-by-row walk.

Severity is not decoration. An error means the database will give wrong answers; a warning means something is unusual but defensible in real data – abutting segments, for instance, occur in files people actually ship.

gffbase.validate.LEVELS = ('fast', 'full')

full additionally re-parses stored attribute blobs, which is the only check that scales with feature count rather than with table count.

class gffbase.validate.Violation(invariant: str, name: str, severity: str, count: int, detail: str, examples: tuple = ())[source]

Bases: object

One invariant that did not hold.

invariant: str
name: str
severity: str
count: int
detail: str
examples: tuple = ()
class gffbase.validate.ValidationReport(level: str, checked: list[str] = <factory>, checked_ids: list[str] = <factory>, violations: list[Violation] = <factory>, skipped: list[str] = <factory>, attribute_eligible: int | None = None, attribute_checked: int | None = None)[source]

Bases: object

What validation found. Falsy when anything failed at error severity.

level: str
checked: list[str]
checked_ids: list[str]

Machine-stable invariant identifiers corresponding to checked.

violations: list[Violation]
skipped: list[str]

Checks skipped because the database predates the structure they test.

attribute_eligible: int | None = None

INV-12 candidates with a stored, non-synthetic attribute blob.

attribute_checked: int | None = None

INV-12 candidates actually re-parsed after applying sample.

property errors: list[Violation]
property warnings: list[Violation]
property ok: bool

True when nothing failed at error severity. Warnings do not count: they flag data that is unusual, not a database that answers wrongly.

raise_for_status() None[source]

Raise if any invariant failed at error severity.

exception gffbase.validate.ValidationError[source]

Bases: AssertionError

A database violates an invariant that makes its answers wrong.

AssertionError rather than ValueError: this is never bad input from the caller, it is gffbase having produced or been handed a database whose internal structure contradicts itself.

gffbase.validate.validate_db(db, level: str = 'fast', *, raise_on_error: bool = False, sample: int | None = 200) ValidationReport[source]

Check a database’s structural invariants.

level="fast" runs every check that is a fixed number of aggregate scans. level="full" adds INV-12, which re-parses stored attribute blobs for sample features and is the only check whose cost grows with the corpus.

Checks that depend on structures a database does not have – a v1 shim has no segments – are recorded as skipped rather than silently passing, so a green report cannot mean “nothing was looked at”.

Upgrading a schema v1 database in place.

Two separate operations, deliberately not combined:

migrate_v1_to_v2()

Structural only – ADD COLUMN, CREATE TABLE, CREATE VIEW. It must never change what any query returns, so it does not attempt to recognise v1’s x_1 / x_2 rows as segments of one discontinuous feature. An in-place upgrade that silently merged 18 features into 17 would be a data change wearing a migration’s clothes.

coalesce_multipart()

The opt-in second step that does exactly that, reusing the ingest resolve pass. It changes results, which is why the caller has to ask for it.

What the migration deliberately does NOT do is backfill raw_id. NULL there means “this database predates the column, and the id column 9 originally carried is unknown” – which is the truth. Setting raw_id = id would be a lie precisely for the rows that matter: the ones create_unique renamed. meta.raw_id_valid records it, and the resolve pass skips NULL raw_id, so a migrated database is simply inert for fusion until coalesce_multipart runs.

class gffbase.migrate.MigrationResult(from_version: str, to_version: str, changed: bool = False, applied: list[str] = <factory>)[source]

Bases: object

What a migration did, for logging and for tests to assert on.

from_version: str

Version found before the migration ran.

to_version: str

Version now recorded.

changed: bool = False

False when the database was already current – the idempotent re-run.

applied: list[str]

Structures actually created, in the order they were applied.

gffbase.migrate.migrate_v1_to_v2(target, *, con: DuckDBPyConnection | None = None) MigrationResult[source]

Upgrade a schema v1 database to v2, in place.

target may be a path or an open connection. The whole upgrade runs in one transaction, so a database is never left half-migrated; and it is idempotent, so running it against a database that is already v2 is a no-op that reports changed=False rather than an error.

Every statement is additive. No feature row’s data is altered, and a caller’s queries return exactly what they returned before – which is the property that makes this safe to run automatically at open.

gffbase.migrate.coalesce_multipart(db, *, on_multipart_conflict: str = 'error') int[source]

Re-fuse rows a v1 create_unique split apart. Returns the count fused.

Deliberately NOT part of migrate_v1_to_v2(): it changes query results, merging what were N features into one, and an in-place upgrade must never do that behind a caller’s back.

Reuses the ingest resolve pass, so the GFF3 predicate applies unchanged – segments must share seqid, source, featuretype and strand, and a run that does not is reported rather than fused.


Exceptions

Exception classes — verbatim port of legacy gffutils.exceptions.

Same names, same constructors, same attributes. Downstream code that catches gffutils.FeatureNotFoundError works against gffbase.FeatureNotFoundError.

exception gffbase.exceptions.GFFFormatError(message: str = '', *, line_no: int = 0, kind: str = '')[source]

Bases: ValueError

Raised when a GFF3 line violates the spec.

Inherits from ValueError so legacy code that catches ValueError keeps working — but the dedicated subclass carries structured fields line_no, kind, and message that point callers straight to the offender in the input.

When the Rust extension is loaded, the canonical class is gffbase._native.GFFFormatError (a PyO3 create_exception! type). At import time, gffbase.__init__ rebinds the public name to whichever class is actually live so isinstance and except clauses keep working regardless of which path raised.

exception gffbase.exceptions.FeatureNotFoundError(feature_id: str)[source]

Bases: Exception

Raised by FeatureDB.__getitem__ when the requested ID is absent.

exception gffbase.exceptions.DuplicateIDError[source]

Bases: ValueError

Two features resolved to the same primary key.

Raised during ingestion under merge_strategy="error" (the default).

exception gffbase.exceptions.SynthesisConflictError[source]

Bases: DuplicateIDError

One inferred GTF parent ID spans incompatible genomic groups.

A gene or transcript cannot be synthesized safely when the same raw gene_id or transcript_id occurs on more than one (seqid, strand) pair. The default is to raise rather than silently choose one location and build an envelope across chromosomes. Callers that deliberately want one parent per group can opt into merge_strategy="create_unique".

Subclassing DuplicateIDError preserves compatibility with code that already catches duplicate identifiers during ingestion.

exception gffbase.exceptions.AttributeStringError[source]

Bases: ValueError

Raised on malformed col-9 attributes.

exception gffbase.exceptions.EmptyInputError[source]

Bases: ValueError

Raised when an input file or iterable yields no features.

exception gffbase.exceptions.SchemaVersionError[source]

Bases: ValueError

The database’s schema version is one this build cannot read.

Raised when opening a database written by a newer gffbase, or one whose meta.schema_version is unintelligible. An older version is not an error: it degrades to a read-only compatibility mode instead.

This exists because the version was previously written and never read, so a schema mismatch surfaced as a missing-column error from whichever query happened to run first – or, worse, as a silently wrong answer from one that did not touch the new columns.

exception gffbase.exceptions.MultipartConstraintError[source]

Bases: ValueError

Lines sharing one ID cannot be one discontinuous feature.

GFF3 requires the segments of a discontinuous feature to agree on seqid, source, featuretype and strand. Raised in mode="strict" when they do not; pass on_multipart_conflict="split" to partition them instead.

exception gffbase.exceptions.ReadOnlyError[source]

Bases: ValueError

A write was attempted on a database opened with read_only=True.

exception gffbase.exceptions.ClosedDatabaseError[source]

Bases: ValueError

A FeatureDB was used after close().

Raised in place of DuckDB’s own ConnectionException, which reports that a connection is closed without saying which object or which call.


Compatibility modules

Ported surface-for-surface from gffutils so that a script that imported them keeps working. See Migrating from gffutils to gffbase for what is guaranteed.

Compatibility helpers.

gffbase.helpers.merge_attributes(attr1, attr2, numeric_sort: bool = False) dict[source]

Union two attribute mappings, key by key.

Values are deduplicated and sorted, not kept in insertion order: the result is a set union with a deterministic rendering, which is what makes two features merge to the same attributes regardless of which was seen first. numeric_sort sorts numerically where every value of a key parses as a number, so ["2", "10"] does not come back as ["10", "2"].

Used by every derived-feature method; ported from gffutils.helpers so that a caller comparing derived output against the oracle sees the same ordering.

gffbase.helpers.example_filename(fn: str) str[source]

Return the absolute path to a packaged example file under tests/data.

Mirrors the legacy gffutils.example_filename API. The legacy package shipped fixtures inside the package itself; we point at the test fixtures directory bundled with the source distribution.

gffbase.helpers.HERE = '/home/runner/work/gffbase/gffbase/python/gffbase'

Directory this package lives in. Upstream locates bundled example data relative to it; example_filename below does the same, and this is exported because callers use it to find their own files next to the package.

gffbase.helpers.infer_dialect(attributes: str) dict[source]

Infer a dialect from one attribute string.

Returns the full dialect dict, with everything the single string does not speak to left at its default.

gffbase.helpers.dialect_compare(dialect1: dict, dialect2: dict) dict[source]

What changed between two dialects, as {"added": ..., "removed": ...}.

Unhashable values (order is a list) are compared by key rather than through a set of items, so this works on real dialects. The oracle’s version raises TypeError: unhashable type: 'list' on any dialect that carries an order, which is all of them.

gffbase.helpers.to_unicode(obj, encoding: str = 'utf-8')[source]

Decode bytes to str; pass anything else through.

A Python-2-era shim. Upstream’s body is unreachable after 2to3 and is the identity for every input, including bytes; this one actually decodes, because returning bytes from a function named to_unicode is not useful to anybody.

gffbase.helpers.is_gff_db(db_fname) bool[source]

True if the path looks like an existing database file.

Extension-based, as upstream: .db for a gffutils database, plus .duckdb, which is what gffbase writes.

gffbase.helpers.get_gff_db(gff_fname, ext: str = '.db')[source]

Open the database beside a GFF file, or build one.

Always returns a FeatureDB. Upstream returns a path string when a sibling database exists and a FeatureDB when it had to build one, which is why gffutils-cli fetch raises TypeError: string indices must be integers in its common case – it indexes whatever it gets. Returning one type is the fix.

The sibling filename is <gff_fname><ext>; upstream’s "%s.%s" inserts a second dot, so it looks for foo.gff..db.

gffbase.helpers.sanitize_gff_db(db, gid_field: str = 'gid')[source]

Return a copy of db with coordinates ordered and a gene id stamped on.

Two repairs, both aimed at making a file greppable and self-consistent: every record gets start <= end, and every record inherits its gene’s id under gid_field, so a gene’s whole block can be found with one grep.

gffbase.helpers.sanitize_gff_file(gff_fname, in_memory: bool = True, in_place: bool = False) None[source]

Sanitize a GFF file, writing to stdout or over the input.

gffbase.helpers.annotate_gff_db(db)[source]

Cross-reference a GFF database against another.

Not implemented, and not implemented upstream either – gffutils’ body is a bare pass, so it silently returns None. The name exists so an import resolves; calling it raises rather than pretending to work, because a function that quietly does nothing is worse than one that says so.

gffbase.helpers.canonical_transcripts(db, fasta_filename)[source]

Yield (transcript, sequence) for the canonical transcript of each gene.

Canonical means the longest CDS, falling back to the longest transcript when a gene has no CDS at all.

Two upstream defects are not reproduced: its fallback sorts ascending and takes [0], selecting the shortest transcript against its own comment; and it calls print() on an internal tuple for every gene, which corrupts any piped output.

gffbase.helpers.asinterval(feature)[source]

Convert a Feature to a pybedtools.Interval.

gffbase.helpers.make_query(args, other=None, limit=None, strand=None, featuretype=None, extra=None, order_by=None, reverse=False, completely_within=False)[source]

Compose a legacy SQLite query and its arguments.

Provided for code that builds queries against an exported database, or that reads gffutils’ generated SQL. gffbase’s own queries do not go through it – they are built in gffbase.interface against the DuckDB schema.

One deviation, and it is the point of having this here: order_by is validated even when it is a plain string. Upstream checks its whitelist only for the iterable form and interpolates a bare string verbatim, which is the same class of hole that FeatureDB.order_by had. See docs/source/content/advisory_sql_injection.rst.

Danger

other and extra are raw SQL

featuretype, limit and strand become bound parameters, and order_by is checked against a whitelist – but other and extra are interpolated verbatim, because they exist to carry a caller’s own SQL fragment (upstream builds its relation joins through other). Passing untrusted input to either is equivalent to passing it to execute(). No gffbase code path routes caller data into them; the asymmetry is documented here because the surrounding validation makes it easy to assume otherwise.

Summarise a GFF/GTF source without building a database.

The point is to answer “what featuretypes are in this file?” before deciding what to keep, on a file too large to want to ingest twice.

gffbase.inspect.inspect(data, look_for=['featuretype', 'chrom', 'attribute_keys', 'feature_count'], limit=None, verbose: bool = True) dict[source]

Count things in a GFF/GTF source.

Parameters:
  • data – A filename, a FeatureDB (its all_features() is used), or any iterable of features.

  • look_for (list) – What to tally. Any Feature attribute name works (chrom, source, strand, …), plus the special "attribute_keys", which counts column-9 keys rather than a field. "feature_count" is always reported whether or not it is requested.

  • limit (int) – Stop after this many features.

  • verbose (bool) – Report progress to stderr.

Returns:

  • dict – One key per entry in look_for, each mapping value -> count, plus feature_count.

  • The mutable default for look_for is upstream’s and is part of the pinned

  • signature. It is never mutated here, which is what makes it harmless.

Conversions that operate on a FeatureDB.

gffbase.convert.to_bed12(f, db, child_type: str = 'exon', name_field: str = 'ID') str[source]

Build one BED12 line for a top-level feature.

Superseded by FeatureDB.bed12, which this delegates to so the two cannot disagree, but still importable because gffutils exports it.

Two differences from FeatureDB.bed12 are upstream’s, not ours, and are preserved: the line ends with a newline, and the thick span always covers the whole feature (this function predates thick/thin handling and never looks at CDS children).

UCSC genomic binning, the public surface.

gffbase._bins has the arithmetic and is what the SQLite export uses; this module is the compatibility face of it, under the names gffutils.bins exports. The two must not diverge, so everything here delegates rather than reimplements — tests/test_compat_surface.py pins that.

The one thing this adds over _bins is one=False: the set of bins a range overlaps, rather than the single smallest bin containing it. That is what a query needs (WHERE bin IN (...)) as opposed to what a write needs, and it is required by helpers.make_query.

gffbase.bins.COORD_OFFSETS = {'bed': 0, 'gff': 1}

How much to subtract from start to reach 0-based coordinates. GFF is 1-based and BED is 0-based, and getting this wrong shifts every bin at a level boundary.

gffbase.bins.FIRST_SHIFT = 17

The finest bin is 2**17 wide.

gffbase.bins.NEXT_SHIFT = 3

each level’s bins are 8x the width of the one below.

Type:

Shift per level

gffbase.bins.OFFSETS = [4681, 585, 73, 9, 1]

Bin number at the start of each level, finest first.

gffbase.bins.bins(start: int, stop: int, fmt: str = 'gff', one: bool = True)[source]

The UCSC bin(s) for the range [start, stop].

Parameters:
  • start – Range endpoints, inclusive.

  • stop – Range endpoints, inclusive.

  • fmt ({"gff", "bed"}) – Coordinate convention of start; see COORD_OFFSETS.

  • one (bool) – True (default) returns the single smallest bin that fully contains the range – what you store on a row. False returns the set of every bin the range overlaps at any level – what you query with, since a feature in a coarser bin can still overlap your range.

  • MAX_CHROM_SIZE (Ranges at or beyond)

  • ones (and negative)

  • 1 (collapse to bin)

  • chromosome") (("somewhere on this)

  • rather (which is how the scheme degrades)

  • represent. (than raising on a coordinate it cannot)

gffbase.bins.print_bin_sizes() None[source]

Report each level’s bin count and width. A debugging aid, kept because the oracle exports it and upstream examples call it.


Usage Gallery — every public method, copy-pasteable for worked examples of every method · Migrating from gffutils to gffbase for the gffutils mapping