gtfreader 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtfreader
3
+ Version: 0.2.0
4
+ Summary: Fast Cython-backed parsing for GTF attribute columns.
5
+ Author: Endre Bakken Stovner
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Cython
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pandas>=2.0
16
+
17
+ # gtfreader
18
+
19
+ Fast GTF reading into pandas DataFrames.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ python -m pip install -e .
25
+ ```
26
+
27
+ ## Example
28
+
29
+ ```python
30
+ from gtfreader import read_gtf
31
+
32
+ df = read_gtf("annotation.gtf")
33
+ print(df.columns)
34
+ print(df.head())
35
+ ```
@@ -0,0 +1,19 @@
1
+ # gtfreader
2
+
3
+ Fast GTF reading into pandas DataFrames.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ python -m pip install -e .
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```python
14
+ from gtfreader import read_gtf
15
+
16
+ df = read_gtf("annotation.gtf")
17
+ print(df.columns)
18
+ print(df.head())
19
+ ```
@@ -14,8 +14,6 @@ from .readers import (
14
14
  read_gtf_full,
15
15
  read_gtf_full_python,
16
16
  read_gtf_python,
17
- read_gtf_restricted,
18
- read_gtf_restricted_python,
19
17
  to_rows,
20
18
  to_rows_keep_duplicates,
21
19
  )
@@ -28,8 +26,6 @@ __all__ = [
28
26
  "read_gtf_full",
29
27
  "read_gtf_full_python",
30
28
  "read_gtf_python",
31
- "read_gtf_restricted",
32
- "read_gtf_restricted_python",
33
29
  "to_rows",
34
30
  "to_rows_keep_duplicates",
35
31
  ]
@@ -0,0 +1,197 @@
1
+ # cython: language_level=3
2
+ # cython: boundscheck=False
3
+ # cython: wraparound=False
4
+ # cython: initializedcheck=False
5
+ # cython: infer_types=True
6
+ # cython: nonecheck=False
7
+
8
+ cdef inline int _key_code(str s, Py_ssize_t a, Py_ssize_t b):
9
+ cdef Py_ssize_t n
10
+ n = b - a
11
+
12
+ if n == 7:
13
+ if (
14
+ s[a] == 'g' and s[a + 1] == 'e' and s[a + 2] == 'n' and
15
+ s[a + 3] == 'e' and s[a + 4] == '_' and s[a + 5] == 'i' and
16
+ s[a + 6] == 'd'
17
+ ):
18
+ return 1
19
+ if (
20
+ s[a] == 'e' and s[a + 1] == 'x' and s[a + 2] == 'o' and
21
+ s[a + 3] == 'n' and s[a + 4] == '_' and s[a + 5] == 'i' and
22
+ s[a + 6] == 'd'
23
+ ):
24
+ return 6
25
+
26
+ elif n == 9:
27
+ if (
28
+ s[a] == 'g' and s[a + 1] == 'e' and s[a + 2] == 'n' and
29
+ s[a + 3] == 'e' and s[a + 4] == '_' and s[a + 5] == 'n' and
30
+ s[a + 6] == 'a' and s[a + 7] == 'm' and s[a + 8] == 'e'
31
+ ):
32
+ return 3
33
+
34
+ elif n == 11:
35
+ if (
36
+ s[a] == 'e' and s[a + 1] == 'x' and s[a + 2] == 'o' and
37
+ s[a + 3] == 'n' and s[a + 4] == '_' and s[a + 5] == 'n' and
38
+ s[a + 6] == 'u' and s[a + 7] == 'm' and s[a + 8] == 'b' and
39
+ s[a + 9] == 'e' and s[a + 10] == 'r'
40
+ ):
41
+ return 5
42
+
43
+ elif n == 13:
44
+ if (
45
+ s[a] == 't' and s[a + 1] == 'r' and s[a + 2] == 'a' and
46
+ s[a + 3] == 'n' and s[a + 4] == 's' and s[a + 5] == 'c' and
47
+ s[a + 6] == 'r' and s[a + 7] == 'i' and s[a + 8] == 'p' and
48
+ s[a + 9] == 't' and s[a + 10] == '_' and s[a + 11] == 'i' and
49
+ s[a + 12] == 'd'
50
+ ):
51
+ return 2
52
+
53
+ elif n == 15:
54
+ if (
55
+ s[a] == 't' and s[a + 1] == 'r' and s[a + 2] == 'a' and
56
+ s[a + 3] == 'n' and s[a + 4] == 's' and s[a + 5] == 'c' and
57
+ s[a + 6] == 'r' and s[a + 7] == 'i' and s[a + 8] == 'p' and
58
+ s[a + 9] == 't' and s[a + 10] == '_' and s[a + 11] == 'n' and
59
+ s[a + 12] == 'a' and s[a + 13] == 'm' and s[a + 14] == 'e'
60
+ ):
61
+ return 4
62
+
63
+ return 0
64
+
65
+
66
+ cdef inline str _cached_str(dict cache, str value):
67
+ cdef object obj
68
+ obj = cache.get(value)
69
+ if obj is None:
70
+ cache[value] = value
71
+ return value
72
+ return <str> obj
73
+
74
+
75
+ def parse_chunk_columns(lines):
76
+ cdef Py_ssize_t n, row_i
77
+ cdef Py_ssize_t pos, m, key_start, key_end, val_start, val_end
78
+ cdef dict columns
79
+ cdef dict gene_name_cache
80
+
81
+ cdef str s, key, value
82
+ cdef list col
83
+ cdef int code
84
+
85
+ cdef object obj_col
86
+
87
+ cdef list gene_id_col
88
+ cdef list transcript_id_col
89
+ cdef list gene_name_col
90
+ cdef list transcript_name_col
91
+ cdef list exon_number_col
92
+ cdef list exon_id_col
93
+
94
+ n = len(lines)
95
+ columns = {}
96
+ gene_name_cache = {}
97
+
98
+ gene_id_col = None
99
+ transcript_id_col = None
100
+ gene_name_col = None
101
+ transcript_name_col = None
102
+ exon_number_col = None
103
+ exon_id_col = None
104
+
105
+ for row_i in range(n):
106
+ s = <str> lines[row_i]
107
+ m = len(s)
108
+ pos = 0
109
+
110
+ while pos < m:
111
+ while pos < m and (s[pos] == ' ' or s[pos] == '\t' or s[pos] == ';'):
112
+ pos += 1
113
+ if pos >= m:
114
+ break
115
+
116
+ key_start = pos
117
+
118
+ while pos < m and s[pos] != ' ' and s[pos] != '\t':
119
+ pos += 1
120
+ key_end = pos
121
+
122
+ while pos < m and (s[pos] == ' ' or s[pos] == '\t'):
123
+ pos += 1
124
+ if pos >= m:
125
+ break
126
+
127
+ if s[pos] == '"':
128
+ val_start = pos + 1
129
+ pos += 1
130
+
131
+ while pos < m and s[pos] != '"':
132
+ pos += 1
133
+ if pos >= m:
134
+ break
135
+
136
+ val_end = pos
137
+ else:
138
+ val_start = pos
139
+ while pos < m and s[pos] != ';':
140
+ pos += 1
141
+ val_end = pos
142
+ while val_end > val_start and (s[val_end - 1] == ' ' or s[val_end - 1] == '\t'):
143
+ val_end -= 1
144
+
145
+ value = s[val_start:val_end]
146
+
147
+ code = _key_code(s, key_start, key_end)
148
+
149
+ if code == 1:
150
+ if gene_id_col is None:
151
+ gene_id_col = [None] * n
152
+ columns["gene_id"] = gene_id_col
153
+ gene_id_col[row_i] = value
154
+
155
+ elif code == 2:
156
+ if transcript_id_col is None:
157
+ transcript_id_col = [None] * n
158
+ columns["transcript_id"] = transcript_id_col
159
+ transcript_id_col[row_i] = value
160
+
161
+ elif code == 3:
162
+ if gene_name_col is None:
163
+ gene_name_col = [None] * n
164
+ columns["gene_name"] = gene_name_col
165
+ gene_name_col[row_i] = _cached_str(gene_name_cache, value)
166
+
167
+ elif code == 4:
168
+ if transcript_name_col is None:
169
+ transcript_name_col = [None] * n
170
+ columns["transcript_name"] = transcript_name_col
171
+ transcript_name_col[row_i] = value
172
+
173
+ elif code == 5:
174
+ if exon_number_col is None:
175
+ exon_number_col = [None] * n
176
+ columns["exon_number"] = exon_number_col
177
+ exon_number_col[row_i] = value
178
+
179
+ elif code == 6:
180
+ if exon_id_col is None:
181
+ exon_id_col = [None] * n
182
+ columns["exon_id"] = exon_id_col
183
+ exon_id_col[row_i] = value
184
+
185
+ else:
186
+ key = s[key_start:key_end]
187
+ obj_col = columns.get(key)
188
+ if obj_col is None:
189
+ col = [None] * n
190
+ columns[key] = col
191
+ else:
192
+ col = <list> obj_col
193
+ col[row_i] = value
194
+
195
+ pos += 1
196
+
197
+ return columns
@@ -23,7 +23,6 @@ GTF_DTYPES = {
23
23
  "Frame": "category",
24
24
  }
25
25
  GTF_NAMES = ["Chromosome", "Source", "Feature", "Start", "End", "Score", "Strand", "Frame", "Attribute"]
26
- RESTRICTED_ATTRIBUTE_COLUMNS = ["gene_id", "transcript_id", "exon_number", "exon_id"]
27
26
 
28
27
 
29
28
  def find_first_data_line_index(file_path: str | Path) -> int:
@@ -162,6 +161,7 @@ def _open_gtf_reader(
162
161
  header=None,
163
162
  names=GTF_NAMES,
164
163
  dtype=GTF_DTYPES,
164
+ comment="#",
165
165
  chunksize=chunksize,
166
166
  skiprows=skiprows,
167
167
  nrows=nrows,
@@ -204,22 +204,6 @@ def _parse_attributes_python(
204
204
  return to_rows(attribute_column, ignore_bad=ignore_bad)
205
205
 
206
206
 
207
- def _parse_restricted_attributes_compiled(attribute_column: pd.Series) -> pd.DataFrame:
208
- if _parse_chunk_columns_compiled is None:
209
- return _parse_restricted_attributes_python(attribute_column)
210
-
211
- attribute_column = _normalize_attribute_series(attribute_column)
212
- columns = _parse_chunk_columns_compiled(attribute_column.to_numpy(copy=False))
213
- return pd.DataFrame(
214
- {name: columns[name] for name in RESTRICTED_ATTRIBUTE_COLUMNS},
215
- index=attribute_column.index,
216
- )
217
-
218
-
219
- def _parse_restricted_attributes_python(attribute_column: pd.Series) -> pd.DataFrame:
220
- return to_rows(_normalize_attribute_series(attribute_column)).reindex(columns=RESTRICTED_ATTRIBUTE_COLUMNS)
221
-
222
-
223
207
  def _read_gtf_full(
224
208
  path: Path,
225
209
  *,
@@ -242,55 +226,24 @@ def _read_gtf_full(
242
226
  return _finalize_gtf_frame(pd.concat(dfs, sort=False))
243
227
 
244
228
 
245
- def _read_gtf_restricted(
246
- path: Path,
247
- *,
248
- skiprows: int,
249
- nrows: int | None,
250
- chunksize: int,
251
- parse_attributes,
252
- ) -> pd.DataFrame:
253
- dfs: list[pd.DataFrame] = []
254
- with _open_gtf_reader(path, chunksize=chunksize, skiprows=skiprows, nrows=nrows) as df_iter:
255
- for df in df_iter:
256
- subset = parse_attributes(df["Attribute"])
257
- dfs.append(
258
- pd.concat(
259
- [df[["Chromosome", "Source", "Feature", "Start", "End", "Score", "Strand", "Frame"]], subset],
260
- axis=1,
261
- sort=False,
262
- )
263
- )
264
-
265
- if not dfs:
266
- return pd.DataFrame(columns=GTF_NAMES[:-1] + RESTRICTED_ATTRIBUTE_COLUMNS)
267
-
268
- return _finalize_gtf_frame(pd.concat(dfs, sort=False))
269
-
270
-
271
229
  def read_gtf(
272
230
  f: str | Path,
273
231
  /,
274
232
  *,
275
233
  nrows: int | None = None,
276
- full: bool = True,
277
234
  duplicate_attr: bool = False,
278
235
  ignore_bad: bool = False,
279
236
  ) -> pd.DataFrame:
280
237
  """Read a GTF file using the compiled parser path when available."""
281
238
  path = Path(f)
282
239
  skiprows = find_first_data_line_index(path)
283
-
284
- if full:
285
- return read_gtf_full(
286
- path,
287
- nrows=nrows,
288
- skiprows=skiprows,
289
- duplicate_attr=duplicate_attr,
290
- ignore_bad=ignore_bad,
291
- )
292
-
293
- return read_gtf_restricted(path, skiprows=skiprows, nrows=nrows)
240
+ return read_gtf_full(
241
+ path,
242
+ nrows=nrows,
243
+ skiprows=skiprows,
244
+ duplicate_attr=duplicate_attr,
245
+ ignore_bad=ignore_bad,
246
+ )
294
247
 
295
248
 
296
249
  def read_gtf_python(
@@ -298,24 +251,19 @@ def read_gtf_python(
298
251
  /,
299
252
  *,
300
253
  nrows: int | None = None,
301
- full: bool = True,
302
254
  duplicate_attr: bool = False,
303
255
  ignore_bad: bool = False,
304
256
  ) -> pd.DataFrame:
305
257
  """Read a GTF file using the pure Python attribute parser."""
306
258
  path = Path(f)
307
259
  skiprows = find_first_data_line_index(path)
308
-
309
- if full:
310
- return read_gtf_full_python(
311
- path,
312
- nrows=nrows,
313
- skiprows=skiprows,
314
- duplicate_attr=duplicate_attr,
315
- ignore_bad=ignore_bad,
316
- )
317
-
318
- return read_gtf_restricted_python(path, skiprows=skiprows, nrows=nrows)
260
+ return read_gtf_full_python(
261
+ path,
262
+ nrows=nrows,
263
+ skiprows=skiprows,
264
+ duplicate_attr=duplicate_attr,
265
+ ignore_bad=ignore_bad,
266
+ )
319
267
 
320
268
 
321
269
  def read_gtf_full(
@@ -368,48 +316,6 @@ def read_gtf_full_python(
368
316
  )
369
317
 
370
318
 
371
- def read_gtf_restricted(
372
- f: str | Path,
373
- /,
374
- skiprows: int = 0,
375
- nrows: int | None = None,
376
- chunksize: int = int(1e5),
377
- *,
378
- chunk_size: int | None = None,
379
- ) -> pd.DataFrame:
380
- """Read core GTF columns plus a small compiled-parser attribute subset."""
381
- path = Path(f)
382
- chunksize = _resolve_chunksize(chunksize, chunk_size)
383
- return _read_gtf_restricted(
384
- path,
385
- skiprows=skiprows,
386
- nrows=nrows,
387
- chunksize=chunksize,
388
- parse_attributes=_parse_restricted_attributes_compiled,
389
- )
390
-
391
-
392
- def read_gtf_restricted_python(
393
- f: str | Path,
394
- /,
395
- skiprows: int = 0,
396
- nrows: int | None = None,
397
- chunksize: int = int(1e5),
398
- *,
399
- chunk_size: int | None = None,
400
- ) -> pd.DataFrame:
401
- """Read core GTF columns plus a small pure-Python attribute subset."""
402
- path = Path(f)
403
- chunksize = _resolve_chunksize(chunksize, chunk_size)
404
- return _read_gtf_restricted(
405
- path,
406
- skiprows=skiprows,
407
- nrows=nrows,
408
- chunksize=chunksize,
409
- parse_attributes=_parse_restricted_attributes_python,
410
- )
411
-
412
-
413
319
  __all__ = [
414
320
  "find_first_data_line_index",
415
321
  "parse_kv_fields",
@@ -417,8 +323,6 @@ __all__ = [
417
323
  "read_gtf_full",
418
324
  "read_gtf_full_python",
419
325
  "read_gtf_python",
420
- "read_gtf_restricted",
421
- "read_gtf_restricted_python",
422
326
  "to_rows",
423
327
  "to_rows_keep_duplicates",
424
328
  ]
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtfreader
3
+ Version: 0.2.0
4
+ Summary: Fast Cython-backed parsing for GTF attribute columns.
5
+ Author: Endre Bakken Stovner
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Cython
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pandas>=2.0
16
+
17
+ # gtfreader
18
+
19
+ Fast GTF reading into pandas DataFrames.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ python -m pip install -e .
25
+ ```
26
+
27
+ ## Example
28
+
29
+ ```python
30
+ from gtfreader import read_gtf
31
+
32
+ df = read_gtf("annotation.gtf")
33
+ print(df.columns)
34
+ print(df.head())
35
+ ```
@@ -8,10 +8,10 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "gtfreader"
11
- version = "0.1.1"
11
+ version = "0.2.0"
12
12
  description = "Fast Cython-backed parsing for GTF attribute columns."
13
13
  readme = "README.md"
14
- requires-python = ">=3.9"
14
+ requires-python = ">=3.12"
15
15
  authors = [
16
16
  { name = "Endre Bakken Stovner" },
17
17
  ]
@@ -22,6 +22,8 @@ classifiers = [
22
22
  "Programming Language :: Python :: 3",
23
23
  "Programming Language :: Cython",
24
24
  "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
25
27
  "Operating System :: OS Independent",
26
28
  "Topic :: Scientific/Engineering :: Bio-Informatics",
27
29
  ]
@@ -39,7 +41,7 @@ include = ["gtfreader*"]
39
41
  testpaths = ["tests"]
40
42
 
41
43
  [tool.cibuildwheel]
42
- build = "cp39-* cp310-* cp311-* cp312-* cp313-*"
44
+ build = "cp312-* cp313-*"
43
45
  skip = "pp* *-musllinux_* *-manylinux_i686"
44
46
  test-command = "pytest -q {project}/tests"
45
47
  test-requires = ["pytest"]
@@ -25,8 +25,14 @@ def test_known_and_dynamic_columns_are_parsed():
25
25
  assert columns["exon_number"] == ["1", None]
26
26
  assert columns["custom_key"] == ["custom", None]
27
27
  assert columns["gene_name"][0] is columns["gene_name"][1]
28
- assert columns["gene_type"][0] is columns["gene_type"][1]
29
- assert columns["level"][0] is columns["level"][1]
28
+ assert columns["gene_type"] == ["protein_coding", "protein_coding"]
29
+ assert columns["level"] == ["2", "2"]
30
+
31
+
32
+ def test_compiled_parser_omits_unseen_fast_path_columns():
33
+ columns = parse_chunk_columns(['gene_id "GENE1"; transcript_id "TX1";'])
34
+
35
+ assert set(columns) == {"gene_id", "transcript_id"}
30
36
 
31
37
 
32
38
  def test_compiled_parser_supports_semicolons_in_quotes_and_unquoted_values():
@@ -11,8 +11,6 @@ from gtfreader import (
11
11
  read_gtf_full,
12
12
  read_gtf_full_python,
13
13
  read_gtf_python,
14
- read_gtf_restricted,
15
- read_gtf_restricted_python,
16
14
  )
17
15
 
18
16
 
@@ -120,6 +118,23 @@ def test_full_readers_support_mixed_attribute_formats(tmp_path: Path, reader):
120
118
  assert result.iloc[1]["exon_id"] == "EX2"
121
119
 
122
120
 
121
+ @pytest.mark.parametrize("reader", [read_gtf, read_gtf_python], ids=["default", "python"])
122
+ def test_readers_ignore_comment_lines_inside_body(tmp_path: Path, reader):
123
+ path = _write_temp_gtf(
124
+ tmp_path,
125
+ "# header\n"
126
+ 'chr1\thavana\tgene\t11869\t14409\t.\t+\t.\tgene_id "G1";\n'
127
+ "# another comment\n"
128
+ 'chr1\thavana\ttranscript\t11869\t14409\t.\t+\t.\tgene_id "G1"; transcript_id "T1";\n',
129
+ )
130
+
131
+ result = _normalize_frame_for_compare(reader(path))
132
+
133
+ assert list(result["Feature"]) == ["gene", "transcript"]
134
+ assert list(result["gene_id"]) == ["G1", "G1"]
135
+ assert list(result["transcript_id"]) == [None, "T1"]
136
+
137
+
123
138
  def test_read_gtf_duplicate_attr_keeps_all_values(tmp_path: Path):
124
139
  path = _write_temp_gtf(
125
140
  tmp_path,
@@ -153,15 +168,14 @@ def test_read_gtf_full_multiple_chunks_match(tmp_path: Path):
153
168
  assert_frame_equal(chunked, unchunked, check_dtype=False)
154
169
 
155
170
 
156
- def test_read_gtf_restricted_returns_core_columns(tmp_path: Path):
171
+ def test_read_gtf_omits_unseen_fast_path_columns(tmp_path: Path):
157
172
  path = _write_temp_gtf(
158
173
  tmp_path,
159
174
  "# header\n"
160
- 'chr1\thavana\texon\t12010\t12057\t.\t+\t.\tgene_id "ENSG1"; transcript_id "ENST1"; exon_number "1"; exon_id "ENSE1";\n',
175
+ 'chr1\thavana\tgene\t12010\t12057\t.\t+\t.\tgene_id "ENSG1"; gene_name "DDX11L1";\n',
161
176
  )
162
177
 
163
- skiprows = find_first_data_line_index(path)
164
- result = read_gtf_restricted(path, skiprows=skiprows)
178
+ result = read_gtf(path)
165
179
 
166
180
  assert list(result.columns) == [
167
181
  "Chromosome",
@@ -173,33 +187,10 @@ def test_read_gtf_restricted_returns_core_columns(tmp_path: Path):
173
187
  "Strand",
174
188
  "Frame",
175
189
  "gene_id",
176
- "transcript_id",
177
- "exon_number",
178
- "exon_id",
190
+ "gene_name",
179
191
  ]
180
192
  assert result.iloc[0]["Start"] == 12009
181
- assert result.iloc[0]["transcript_id"] == "ENST1"
182
-
183
-
184
- @pytest.mark.parametrize(
185
- "reader",
186
- [read_gtf_restricted, read_gtf_restricted_python],
187
- ids=["default", "python"],
188
- )
189
- def test_restricted_readers_support_unquoted_values(tmp_path: Path, reader):
190
- path = _write_temp_gtf(
191
- tmp_path,
192
- "# header\n"
193
- 'chr1\tensembl_havana\texon\t3069203\t3069296\t.\t+\t.\tgene_id "G1"; transcript_id "T1"; exon_number 4; exon_id "EX4";\n',
194
- )
195
-
196
- skiprows = find_first_data_line_index(path)
197
- result = _normalize_frame_for_compare(reader(path, skiprows=skiprows))
198
-
199
- assert result.iloc[0]["gene_id"] == "G1"
200
- assert result.iloc[0]["transcript_id"] == "T1"
201
- assert result.iloc[0]["exon_number"] == "4"
202
- assert result.iloc[0]["exon_id"] == "EX4"
193
+ assert result.iloc[0]["gene_name"] == "DDX11L1"
203
194
 
204
195
 
205
196
  def test_read_gtf_python_matches_compiled_reader(tmp_path: Path):
@@ -217,12 +208,7 @@ def test_read_gtf_python_matches_compiled_reader(tmp_path: Path):
217
208
  compiled = _normalize_frame_for_compare(compiled)
218
209
  python = _normalize_frame_for_compare(python)
219
210
 
220
- assert list(python["gene_id"]) == list(compiled["gene_id"])
221
- assert list(python["gene_name"]) == ["DDX11L1", None]
222
- assert list(python["transcript_id"]) == [None, "ENST1"]
223
- assert list(python["transcript_name"]) == [None, "DDX11L1-201"]
224
- assert list(python["tag"]) == [None, "basic"]
225
- assert list(python["Start"]) == list(compiled["Start"])
211
+ assert_frame_equal(compiled, python, check_dtype=False)
226
212
 
227
213
 
228
214
  def test_read_gtf_python_duplicate_attr_keeps_all_values(tmp_path: Path):
@@ -237,20 +223,3 @@ def test_read_gtf_python_duplicate_attr_keeps_all_values(tmp_path: Path):
237
223
 
238
224
  assert compiled.iloc[0]["tag"] == "CCDS,basic"
239
225
  assert python.iloc[0]["tag"] == "CCDS,basic"
240
-
241
-
242
- def test_read_gtf_restricted_python_matches_compiled(tmp_path: Path):
243
- path = _write_temp_gtf(
244
- tmp_path,
245
- "# header\n"
246
- 'chr1\thavana\texon\t12010\t12057\t.\t+\t.\tgene_id "ENSG1"; transcript_id "ENST1"; exon_number "1"; exon_id "ENSE1";\n',
247
- )
248
-
249
- skiprows = find_first_data_line_index(path)
250
- compiled = read_gtf_restricted(path, skiprows=skiprows)
251
- python = read_gtf_restricted_python(path, skiprows=skiprows)
252
-
253
- compiled = _normalize_frame_for_compare(compiled)
254
- python = _normalize_frame_for_compare(python)
255
-
256
- assert_frame_equal(compiled, python, check_dtype=False)
gtfreader-0.1.1/PKG-INFO DELETED
@@ -1,66 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: gtfreader
3
- Version: 0.1.1
4
- Summary: Fast Cython-backed parsing for GTF attribute columns.
5
- Author: Endre Bakken Stovner
6
- Classifier: Programming Language :: Python :: 3
7
- Classifier: Programming Language :: Cython
8
- Classifier: Programming Language :: Python :: 3 :: Only
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
11
- Requires-Python: >=3.9
12
- Description-Content-Type: text/markdown
13
- Requires-Dist: pandas>=2.0
14
-
15
- # gtfreader
16
-
17
- `gtfreader` is a small package for parsing and reading GTF files into pandas dataframes.
18
-
19
- ## Install
20
-
21
- ```bash
22
- python -m pip install -e .
23
- ```
24
-
25
- ## Usage
26
-
27
- ```python
28
- from gtfreader import read_gtf, read_gtf_python
29
-
30
- df = read_gtf("annotation.gtf")
31
- df_python = read_gtf_python("annotation.gtf")
32
- ```
33
-
34
- `read_gtf(...)` uses the compiled parser path when available. `read_gtf_python(...)` uses the high-level pure Python parser path used by the current `pyrunges` reader style.
35
-
36
- If you want to use the compiled low-level parser directly, pass it raw attribute strings from column 9 of the GTF before they have been expanded:
37
-
38
- ```python
39
- import pandas as pd
40
-
41
- from gtfreader import find_first_data_line_index, parse_chunk_columns
42
-
43
- skiprows = find_first_data_line_index("annotation.gtf")
44
- attribute_lines = pd.read_csv(
45
- "annotation.gtf",
46
- sep="\t",
47
- header=None,
48
- usecols=[8],
49
- names=["Attribute"],
50
- skiprows=skiprows,
51
- )["Attribute"].tolist()
52
-
53
- compiled_columns = parse_chunk_columns(attribute_lines)
54
- ```
55
-
56
- ## Build
57
-
58
- ```bash
59
- python -m build
60
- ```
61
-
62
- ## Test
63
-
64
- ```bash
65
- python -m pytest -q
66
- ```
gtfreader-0.1.1/README.md DELETED
@@ -1,52 +0,0 @@
1
- # gtfreader
2
-
3
- `gtfreader` is a small package for parsing and reading GTF files into pandas dataframes.
4
-
5
- ## Install
6
-
7
- ```bash
8
- python -m pip install -e .
9
- ```
10
-
11
- ## Usage
12
-
13
- ```python
14
- from gtfreader import read_gtf, read_gtf_python
15
-
16
- df = read_gtf("annotation.gtf")
17
- df_python = read_gtf_python("annotation.gtf")
18
- ```
19
-
20
- `read_gtf(...)` uses the compiled parser path when available. `read_gtf_python(...)` uses the high-level pure Python parser path used by the current `pyrunges` reader style.
21
-
22
- If you want to use the compiled low-level parser directly, pass it raw attribute strings from column 9 of the GTF before they have been expanded:
23
-
24
- ```python
25
- import pandas as pd
26
-
27
- from gtfreader import find_first_data_line_index, parse_chunk_columns
28
-
29
- skiprows = find_first_data_line_index("annotation.gtf")
30
- attribute_lines = pd.read_csv(
31
- "annotation.gtf",
32
- sep="\t",
33
- header=None,
34
- usecols=[8],
35
- names=["Attribute"],
36
- skiprows=skiprows,
37
- )["Attribute"].tolist()
38
-
39
- compiled_columns = parse_chunk_columns(attribute_lines)
40
- ```
41
-
42
- ## Build
43
-
44
- ```bash
45
- python -m build
46
- ```
47
-
48
- ## Test
49
-
50
- ```bash
51
- python -m pytest -q
52
- ```
@@ -1,352 +0,0 @@
1
- # cython: language_level=3
2
- # cython: boundscheck=False
3
- # cython: wraparound=False
4
- # cython: initializedcheck=False
5
- # cython: infer_types=True
6
- # cython: nonecheck=False
7
-
8
- cdef inline int _key_code(str s, Py_ssize_t a, Py_ssize_t b):
9
- cdef Py_ssize_t n
10
- n = b - a
11
-
12
- if n == 3:
13
- if s[a] == 't' and s[a + 1] == 'a' and s[a + 2] == 'g':
14
- return 9
15
- if s[a] == 'o' and s[a + 1] == 'n' and s[a + 2] == 't':
16
- return 17
17
-
18
- elif n == 5:
19
- if (
20
- s[a] == 'l' and s[a + 1] == 'e' and s[a + 2] == 'v' and
21
- s[a + 3] == 'e' and s[a + 4] == 'l'
22
- ):
23
- return 16
24
-
25
- elif n == 6:
26
- if (
27
- s[a] == 'c' and s[a + 1] == 'c' and s[a + 2] == 'd' and
28
- s[a + 3] == 's' and s[a + 4] == 'i' and s[a + 5] == 'd'
29
- ):
30
- return 14
31
-
32
- elif n == 7:
33
- if (
34
- s[a] == 'g' and s[a + 1] == 'e' and s[a + 2] == 'n' and
35
- s[a + 3] == 'e' and s[a + 4] == '_' and s[a + 5] == 'i' and
36
- s[a + 6] == 'd'
37
- ):
38
- return 1
39
- if (
40
- s[a] == 'e' and s[a + 1] == 'x' and s[a + 2] == 'o' and
41
- s[a + 3] == 'n' and s[a + 4] == '_' and s[a + 5] == 'i' and
42
- s[a + 6] == 'd'
43
- ):
44
- return 8
45
- if (
46
- s[a] == 'h' and s[a + 1] == 'g' and s[a + 2] == 'n' and
47
- s[a + 3] == 'c' and s[a + 4] == '_' and s[a + 5] == 'i' and
48
- s[a + 6] == 'd'
49
- ):
50
- return 13
51
-
52
- elif n == 9:
53
- if (
54
- s[a] == 'g' and s[a + 1] == 'e' and s[a + 2] == 'n' and
55
- s[a + 3] == 'e' and s[a + 4] == '_'
56
- ):
57
- if (
58
- s[a + 5] == 'n' and s[a + 6] == 'a' and
59
- s[a + 7] == 'm' and s[a + 8] == 'e'
60
- ):
61
- return 3
62
- if (
63
- s[a + 5] == 't' and s[a + 6] == 'y' and
64
- s[a + 7] == 'p' and s[a + 8] == 'e'
65
- ):
66
- return 4
67
-
68
- elif n == 10:
69
- if (
70
- s[a] == 'a' and s[a + 1] == 'r' and s[a + 2] == 't' and
71
- s[a + 3] == 'i' and s[a + 4] == 'f' and s[a + 5] == '_' and
72
- s[a + 6] == 'd' and s[a + 7] == 'u' and s[a + 8] == 'p' and
73
- s[a + 9] == 'l'
74
- ):
75
- return 15
76
-
77
- elif n == 11:
78
- if (
79
- s[a] == 'e' and s[a + 1] == 'x' and s[a + 2] == 'o' and
80
- s[a + 3] == 'n' and s[a + 4] == '_' and s[a + 5] == 'n' and
81
- s[a + 6] == 'u' and s[a + 7] == 'm' and s[a + 8] == 'b' and
82
- s[a + 9] == 'e' and s[a + 10] == 'r'
83
- ):
84
- return 7
85
- if (
86
- s[a] == 'h' and s[a + 1] == 'a' and s[a + 2] == 'v' and
87
- s[a + 3] == 'a' and s[a + 4] == 'n' and s[a + 5] == 'a' and
88
- s[a + 6] == '_' and s[a + 7] == 'g' and s[a + 8] == 'e' and
89
- s[a + 9] == 'n' and s[a + 10] == 'e'
90
- ):
91
- return 11
92
-
93
- elif n == 13:
94
- if (
95
- s[a] == 't' and s[a + 1] == 'r' and s[a + 2] == 'a' and
96
- s[a + 3] == 'n' and s[a + 4] == 's' and s[a + 5] == 'c' and
97
- s[a + 6] == 'r' and s[a + 7] == 'i' and s[a + 8] == 'p' and
98
- s[a + 9] == 't' and s[a + 10] == '_' and s[a + 11] == 'i' and
99
- s[a + 12] == 'd'
100
- ):
101
- return 2
102
-
103
- elif n == 15:
104
- if (
105
- s[a] == 't' and s[a + 1] == 'r' and s[a + 2] == 'a' and
106
- s[a + 3] == 'n' and s[a + 4] == 's' and s[a + 5] == 'c' and
107
- s[a + 6] == 'r' and s[a + 7] == 'i' and s[a + 8] == 'p' and
108
- s[a + 9] == 't' and s[a + 10] == '_'
109
- ):
110
- if (
111
- s[a + 11] == 'n' and s[a + 12] == 'a' and
112
- s[a + 13] == 'm' and s[a + 14] == 'e'
113
- ):
114
- return 5
115
- if (
116
- s[a + 11] == 't' and s[a + 12] == 'y' and
117
- s[a + 13] == 'p' and s[a + 14] == 'e'
118
- ):
119
- return 6
120
-
121
- elif n == 17:
122
- if (
123
- s[a] == 'h' and s[a + 1] == 'a' and s[a + 2] == 'v' and
124
- s[a + 3] == 'a' and s[a + 4] == 'n' and s[a + 5] == 'a' and
125
- s[a + 6] == '_' and s[a + 7] == 't' and s[a + 8] == 'r' and
126
- s[a + 9] == 'a' and s[a + 10] == 'n' and s[a + 11] == 's' and
127
- s[a + 12] == 'c' and s[a + 13] == 'r' and s[a + 14] == 'i' and
128
- s[a + 15] == 'p' and s[a + 16] == 't'
129
- ):
130
- return 10
131
-
132
- elif n == 24:
133
- if (
134
- s[a] == 't' and s[a + 1] == 'r' and s[a + 2] == 'a' and
135
- s[a + 3] == 'n' and s[a + 4] == 's' and s[a + 5] == 'c' and
136
- s[a + 6] == 'r' and s[a + 7] == 'i' and s[a + 8] == 'p' and
137
- s[a + 9] == 't' and s[a + 10] == '_' and s[a + 11] == 's' and
138
- s[a + 12] == 'u' and s[a + 13] == 'p' and s[a + 14] == 'p' and
139
- s[a + 15] == 'o' and s[a + 16] == 'r' and s[a + 17] == 't' and
140
- s[a + 18] == '_' and s[a + 19] == 'l' and s[a + 20] == 'e' and
141
- s[a + 21] == 'v' and s[a + 22] == 'e' and s[a + 23] == 'l'
142
- ):
143
- return 12
144
-
145
- return 0
146
-
147
-
148
- cdef inline str _cached_str(dict cache, str value):
149
- cdef object obj
150
- obj = cache.get(value)
151
- if obj is None:
152
- cache[value] = value
153
- return value
154
- return <str> obj
155
-
156
-
157
- def parse_chunk_columns(lines):
158
- cdef Py_ssize_t n, row_i
159
- cdef Py_ssize_t pos, m, key_start, key_end, val_start, val_end
160
- cdef dict columns
161
-
162
- cdef dict gene_name_cache
163
- cdef dict gene_type_cache
164
- cdef dict transcript_type_cache
165
- cdef dict tag_cache
166
- cdef dict level_cache
167
- cdef dict ont_cache
168
- cdef dict transcript_support_level_cache
169
- cdef dict artif_dupl_cache
170
-
171
- cdef str s, key, value
172
- cdef list col
173
- cdef int code
174
-
175
- cdef object obj_col
176
-
177
- cdef list gene_id_col
178
- cdef list transcript_id_col
179
- cdef list gene_name_col
180
- cdef list gene_type_col
181
- cdef list transcript_name_col
182
- cdef list transcript_type_col
183
- cdef list exon_number_col
184
- cdef list exon_id_col
185
- cdef list tag_col
186
- cdef list havana_transcript_col
187
- cdef list havana_gene_col
188
- cdef list transcript_support_level_col
189
- cdef list hgnc_id_col
190
- cdef list ccdsid_col
191
- cdef list artif_dupl_col
192
- cdef list level_col
193
- cdef list ont_col
194
-
195
- n = len(lines)
196
- columns = {}
197
-
198
- gene_name_cache = {}
199
- gene_type_cache = {}
200
- transcript_type_cache = {}
201
- tag_cache = {}
202
- level_cache = {}
203
- ont_cache = {}
204
- transcript_support_level_cache = {}
205
- artif_dupl_cache = {}
206
-
207
- gene_id_col = [None] * n
208
- transcript_id_col = [None] * n
209
- gene_name_col = [None] * n
210
- gene_type_col = [None] * n
211
- transcript_name_col = [None] * n
212
- transcript_type_col = [None] * n
213
- exon_number_col = [None] * n
214
- exon_id_col = [None] * n
215
- tag_col = [None] * n
216
- havana_transcript_col = [None] * n
217
- havana_gene_col = [None] * n
218
- transcript_support_level_col = [None] * n
219
- hgnc_id_col = [None] * n
220
- ccdsid_col = [None] * n
221
- artif_dupl_col = [None] * n
222
- level_col = [None] * n
223
- ont_col = [None] * n
224
-
225
- columns["gene_id"] = gene_id_col
226
- columns["transcript_id"] = transcript_id_col
227
- columns["gene_name"] = gene_name_col
228
- columns["gene_type"] = gene_type_col
229
- columns["transcript_name"] = transcript_name_col
230
- columns["transcript_type"] = transcript_type_col
231
- columns["exon_number"] = exon_number_col
232
- columns["exon_id"] = exon_id_col
233
- columns["tag"] = tag_col
234
- columns["havana_transcript"] = havana_transcript_col
235
- columns["havana_gene"] = havana_gene_col
236
- columns["transcript_support_level"] = transcript_support_level_col
237
- columns["hgnc_id"] = hgnc_id_col
238
- columns["ccdsid"] = ccdsid_col
239
- columns["artif_dupl"] = artif_dupl_col
240
- columns["level"] = level_col
241
- columns["ont"] = ont_col
242
-
243
- for row_i in range(n):
244
- s = <str> lines[row_i]
245
- m = len(s)
246
- pos = 0
247
-
248
- while pos < m:
249
- while pos < m and (s[pos] == ' ' or s[pos] == '\t' or s[pos] == ';'):
250
- pos += 1
251
- if pos >= m:
252
- break
253
-
254
- key_start = pos
255
-
256
- while pos < m and s[pos] != ' ' and s[pos] != '\t':
257
- pos += 1
258
- key_end = pos
259
-
260
- while pos < m and (s[pos] == ' ' or s[pos] == '\t'):
261
- pos += 1
262
- if pos >= m:
263
- break
264
-
265
- if s[pos] == '"':
266
- val_start = pos + 1
267
- pos += 1
268
-
269
- while pos < m and s[pos] != '"':
270
- pos += 1
271
- if pos >= m:
272
- break
273
-
274
- val_end = pos
275
- else:
276
- val_start = pos
277
- while pos < m and s[pos] != ';':
278
- pos += 1
279
- val_end = pos
280
- while val_end > val_start and (s[val_end - 1] == ' ' or s[val_end - 1] == '\t'):
281
- val_end -= 1
282
-
283
- value = s[val_start:val_end]
284
-
285
- code = _key_code(s, key_start, key_end)
286
-
287
- if code == 1:
288
- gene_id_col[row_i] = value
289
-
290
- elif code == 2:
291
- transcript_id_col[row_i] = value
292
-
293
- elif code == 3:
294
- gene_name_col[row_i] = _cached_str(gene_name_cache, value)
295
-
296
- elif code == 4:
297
- gene_type_col[row_i] = _cached_str(gene_type_cache, value)
298
-
299
- elif code == 5:
300
- transcript_name_col[row_i] = value
301
-
302
- elif code == 6:
303
- transcript_type_col[row_i] = _cached_str(transcript_type_cache, value)
304
-
305
- elif code == 7:
306
- exon_number_col[row_i] = value
307
-
308
- elif code == 8:
309
- exon_id_col[row_i] = value
310
-
311
- elif code == 9:
312
- tag_col[row_i] = _cached_str(tag_cache, value)
313
-
314
- elif code == 10:
315
- havana_transcript_col[row_i] = value
316
-
317
- elif code == 11:
318
- havana_gene_col[row_i] = value
319
-
320
- elif code == 12:
321
- transcript_support_level_col[row_i] = _cached_str(
322
- transcript_support_level_cache, value
323
- )
324
-
325
- elif code == 13:
326
- hgnc_id_col[row_i] = value
327
-
328
- elif code == 14:
329
- ccdsid_col[row_i] = value
330
-
331
- elif code == 15:
332
- artif_dupl_col[row_i] = _cached_str(artif_dupl_cache, value)
333
-
334
- elif code == 16:
335
- level_col[row_i] = _cached_str(level_cache, value)
336
-
337
- elif code == 17:
338
- ont_col[row_i] = _cached_str(ont_cache, value)
339
-
340
- else:
341
- key = s[key_start:key_end]
342
- obj_col = columns.get(key)
343
- if obj_col is None:
344
- col = [None] * n
345
- columns[key] = col
346
- else:
347
- col = <list> obj_col
348
- col[row_i] = value
349
-
350
- pos += 1
351
-
352
- return columns
@@ -1,66 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: gtfreader
3
- Version: 0.1.1
4
- Summary: Fast Cython-backed parsing for GTF attribute columns.
5
- Author: Endre Bakken Stovner
6
- Classifier: Programming Language :: Python :: 3
7
- Classifier: Programming Language :: Cython
8
- Classifier: Programming Language :: Python :: 3 :: Only
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
11
- Requires-Python: >=3.9
12
- Description-Content-Type: text/markdown
13
- Requires-Dist: pandas>=2.0
14
-
15
- # gtfreader
16
-
17
- `gtfreader` is a small package for parsing and reading GTF files into pandas dataframes.
18
-
19
- ## Install
20
-
21
- ```bash
22
- python -m pip install -e .
23
- ```
24
-
25
- ## Usage
26
-
27
- ```python
28
- from gtfreader import read_gtf, read_gtf_python
29
-
30
- df = read_gtf("annotation.gtf")
31
- df_python = read_gtf_python("annotation.gtf")
32
- ```
33
-
34
- `read_gtf(...)` uses the compiled parser path when available. `read_gtf_python(...)` uses the high-level pure Python parser path used by the current `pyrunges` reader style.
35
-
36
- If you want to use the compiled low-level parser directly, pass it raw attribute strings from column 9 of the GTF before they have been expanded:
37
-
38
- ```python
39
- import pandas as pd
40
-
41
- from gtfreader import find_first_data_line_index, parse_chunk_columns
42
-
43
- skiprows = find_first_data_line_index("annotation.gtf")
44
- attribute_lines = pd.read_csv(
45
- "annotation.gtf",
46
- sep="\t",
47
- header=None,
48
- usecols=[8],
49
- names=["Attribute"],
50
- skiprows=skiprows,
51
- )["Attribute"].tolist()
52
-
53
- compiled_columns = parse_chunk_columns(attribute_lines)
54
- ```
55
-
56
- ## Build
57
-
58
- ```bash
59
- python -m build
60
- ```
61
-
62
- ## Test
63
-
64
- ```bash
65
- python -m pytest -q
66
- ```
File without changes
File without changes
File without changes