cdhit-reader 0.2.0__tar.gz → 0.3.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.
Files changed (31) hide show
  1. {cdhit-reader-0.2.0/cdhit_reader.egg-info → cdhit_reader-0.3.0}/PKG-INFO +11 -3
  2. cdhit_reader-0.3.0/cdhit_reader/__init__.py +41 -0
  3. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/_fasta.py +51 -19
  4. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/_reader.py +34 -27
  5. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/_version.py +1 -1
  6. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/test/test_clstr.py +9 -1
  7. cdhit_reader-0.3.0/cdhit_reader/test/test_fasta.py +34 -0
  8. cdhit_reader-0.3.0/cdhit_reader/test/test_package_import.py +55 -0
  9. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0/cdhit_reader.egg-info}/PKG-INFO +11 -3
  10. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader.egg-info/SOURCES.txt +3 -1
  11. cdhit_reader-0.3.0/cdhit_reader.egg-info/entry_points.txt +3 -0
  12. cdhit_reader-0.3.0/setup.py +8 -0
  13. cdhit-reader-0.2.0/cdhit_reader/__init__.py +0 -26
  14. cdhit-reader-0.2.0/cdhit_reader.egg-info/entry_points.txt +0 -3
  15. cdhit-reader-0.2.0/setup.py +0 -5
  16. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/LICENSE.md +0 -0
  17. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/MANIFEST.in +0 -0
  18. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/README.md +0 -0
  19. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/_cli.py +0 -0
  20. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/_compare.py +0 -0
  21. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/_testit.py +0 -0
  22. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/test/__init__.py +0 -0
  23. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/test/aa.clstr +0 -0
  24. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader/test/nt.clstr +0 -0
  25. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader.egg-info/dependency_links.txt +0 -0
  26. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader.egg-info/requires.txt +0 -0
  27. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader.egg-info/top_level.txt +0 -0
  28. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/cdhit_reader.egg-info/zip-safe +0 -0
  29. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/pyproject.toml +0 -0
  30. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/setup.cfg +0 -0
  31. {cdhit-reader-0.2.0 → cdhit_reader-0.3.0}/version.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cdhit-reader
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CD-HIT cluster parser
5
5
  Home-page: https://github.com/telatin/cdhit-parser
6
6
  Download-URL: https://github.com/telatin/cdhit-parser
@@ -18,8 +18,16 @@ Classifier: License :: OSI Approved :: MIT License
18
18
  Classifier: Operating System :: OS Independent
19
19
  Classifier: Programming Language :: Python
20
20
  Description-Content-Type: text/markdown
21
- Provides-Extra: cli
22
21
  License-File: LICENSE.md
22
+ Requires-Dist: click>=7.1.2
23
+ Requires-Dist: importlib-resources>=1.4.0
24
+ Requires-Dist: more-itertools>=8.4.0
25
+ Requires-Dist: pytest>=5.4.3
26
+ Requires-Dist: xopen>=1.0.1
27
+ Provides-Extra: cli
28
+ Requires-Dist: plotille; extra == "cli"
29
+ Dynamic: download-url
30
+ Dynamic: license-file
23
31
 
24
32
  # cdhit-parser
25
33
 
@@ -0,0 +1,41 @@
1
+ from importlib import import_module
2
+
3
+ from ._reader import ParsingError, ClusterSequence, Cluster, ClstrReader, read_cdhit, SeqType, Strand
4
+ from ._fasta import FastaParsingError, Sequence, FastaReader, read_fasta
5
+ from ._version import __version__
6
+
7
+ _LAZY_EXPORTS = {
8
+ "cli": ("._cli", "cli"),
9
+ "compare": ("._compare", "compare"),
10
+ "test": ("._testit", "test"),
11
+ }
12
+
13
+ __all__ = [
14
+ "ParsingError",
15
+ "FastaParsingError",
16
+ "ClusterSequence",
17
+ "Cluster",
18
+ "ClstrReader",
19
+ "read_cdhit",
20
+ "FastaReader",
21
+ "read_fasta",
22
+ "SeqType",
23
+ "Strand",
24
+ "Sequence",
25
+ "__version__",
26
+ "cli",
27
+ "compare",
28
+ "test",
29
+ ]
30
+
31
+
32
+ def __getattr__(name):
33
+ if name in _LAZY_EXPORTS:
34
+ module_name, attribute_name = _LAZY_EXPORTS[name]
35
+ module = import_module(module_name, __name__)
36
+ return getattr(module, attribute_name)
37
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
38
+
39
+
40
+ def __dir__():
41
+ return sorted(set(globals()) | set(__all__))
@@ -1,12 +1,31 @@
1
1
  from __future__ import annotations
2
2
  from pathlib import Path
3
3
  from typing import IO, Iterator, List, Union
4
- from enum import Enum
5
4
  from more_itertools import peekable
6
5
  from xopen import xopen
7
- import re
8
6
 
9
- __all__ = ["Sequence", "FastaReader", "read_fasta"]
7
+ __all__ = ["FastaParsingError", "Sequence", "FastaReader", "read_fasta"]
8
+
9
+
10
+ class FastaParsingError(Exception):
11
+ """
12
+ FASTA parsing error.
13
+ """
14
+
15
+ def __init__(self, line_number: int, message: str = "Invalid FASTA file"):
16
+ super().__init__(f"{message} at line {line_number}.")
17
+ self._line_number = line_number
18
+
19
+ @property
20
+ def line_number(self) -> int:
21
+ """
22
+ Line number.
23
+
24
+ Returns
25
+ -------
26
+ Line number.
27
+ """
28
+ return self._line_number
10
29
 
11
30
 
12
31
 
@@ -54,6 +73,7 @@ class FastaReader:
54
73
  self._seq = ""
55
74
  self._lines = peekable(line for line in file)
56
75
  self._line_number = 0
76
+ self._current_defline_number = 0
57
77
 
58
78
  def read_item(self) -> Sequence:
59
79
  """
@@ -87,34 +107,46 @@ class FastaReader:
87
107
 
88
108
  def _next_defline(self) -> str:
89
109
  while True:
90
- line = next(self._lines)
91
- self._line_number += 1
92
- if line == "":
110
+ try:
111
+ line = next(self._lines)
112
+ except StopIteration:
93
113
  raise StopIteration
94
114
 
115
+ self._line_number += 1
116
+
95
117
  line = line.strip()
96
118
  if line.startswith(">"):
119
+ self._current_defline_number = self._line_number
97
120
  return line[1:]
98
121
  if line != "":
99
- raise "Invalid FASTA file"
122
+ raise FastaParsingError(self._line_number, "Expected FASTA header")
100
123
 
101
124
  def _next_sequences(self) -> str:
102
125
  seq = ""
103
126
  while True:
104
- line = next(self._lines).strip()
105
-
127
+ try:
128
+ line = next(self._lines)
129
+ except StopIteration as exc:
130
+ raise FastaParsingError(
131
+ self._current_defline_number,
132
+ "Missing sequence for FASTA record",
133
+ ) from exc
134
+
106
135
  self._line_number += 1
107
- if line == "":
108
- raise
109
136
 
110
137
  line = line.strip()
111
- if not line.startswith(">"):
112
- seq += line
113
- if self._sequence_continues():
114
- continue
115
- return seq
116
- if line != "":
117
- raise
138
+ if line == "":
139
+ raise FastaParsingError(self._line_number, "Blank lines are not allowed in FASTA records")
140
+ if line.startswith(">"):
141
+ raise FastaParsingError(
142
+ self._current_defline_number,
143
+ "Missing sequence for FASTA record",
144
+ )
145
+
146
+ seq += line
147
+ if self._sequence_continues():
148
+ continue
149
+ return seq
118
150
 
119
151
 
120
152
  def _sequence_continues(self):
@@ -157,4 +189,4 @@ def read_fasta(file: Union[str, Path, IO[str]], separator=" ", line_len = 0) ->
157
189
  -------
158
190
  FASTA reader.
159
191
  """
160
- return FastaReader(file, separator=separator, line_len=line_len)
192
+ return FastaReader(file, separator=separator, line_len=line_len)
@@ -8,6 +8,13 @@ import re
8
8
 
9
9
  __all__ = ["ParsingError", "ClusterSequence", "Cluster", "ClstrReader", "read_cdhit", "SeqType", "Strand", "FastaReader", "read_fasta"]
10
10
 
11
+ CLUSTER_SEQUENCE_PATTERN = re.compile(
12
+ r"(?P<id>\d+)\s+(?P<size>\d+)(?P<type>aa|nt), >(?P<name>.+?)\.\.\. (?P<attr>.+)"
13
+ )
14
+ CLUSTER_ATTR_PATTERN = re.compile(
15
+ r"(?P<ref>\*|at) .*?(?P<strand>[+-]?)\/?(?P<percent>\d+\.?\d*)%"
16
+ )
17
+
11
18
  class SeqType(Enum):
12
19
  """
13
20
  Sequence type.
@@ -59,7 +66,7 @@ class ClusterSequence:
59
66
  ----------
60
67
  line: str
61
68
  """
62
- def __init__(self, line: str):
69
+ def __init__(self, line: str, line_number: int | None = None):
63
70
  """
64
71
  Parameters
65
72
  ----------
@@ -67,6 +74,7 @@ class ClusterSequence:
67
74
  Line.
68
75
  """
69
76
  self.line = line
77
+ self.line_number = line_number
70
78
  self.length = 0
71
79
  self.name = ""
72
80
  self.identity = 0
@@ -80,33 +88,32 @@ class ClusterSequence:
80
88
  """
81
89
  3 502nt, >IKXM6KN01CFAFI... at 1:502:1:503/+/97.81%
82
90
  """
83
-
84
-
85
- pattern = re.compile(
86
- r"(?P<id>\d+)\s+(?P<size>\d+)(?P<type>aa|nt), >(?P<name>.+?)\.\.\. (?P<attr>.+)"
87
- )
88
- attrpatt = re.compile(
89
- r"(?P<ref>\*|at) .*?(?P<strand>[+-]?)\/?(?P<percent>\d+\.?\d*)%"
90
- )
91
-
92
-
93
- match = pattern.search(self.line)
91
+ match = CLUSTER_SEQUENCE_PATTERN.fullmatch(self.line)
92
+ if not match:
93
+ self._raise_parsing_error()
94
94
 
95
95
  self.seqtype = SeqType.PROTEIN if match["type"] == "aa" else SeqType.NT
96
96
  self.strand = Strand.NONE if self.seqtype == SeqType.PROTEIN else Strand.PLUS
97
- if match:
98
- self.name = match["name"]
99
- self.id = int(match["id"])
100
- self.length = int(match["size"])
101
- if match["attr"] == "*":
102
- self.is_ref = True
103
- self.identity = 100.0
104
-
105
- else:
106
- attrs = attrpatt.match(match["attr"])
107
- self.is_ref = False
108
- self.identity = float(attrs["percent"])
109
- self.strand = Strand.PLUS if attrs["strand"] == "+" else Strand.REVERSE if attrs["strand"] == "-" else Strand.NONE
97
+ self.name = match["name"]
98
+ self.id = int(match["id"])
99
+ self.length = int(match["size"])
100
+ if match["attr"] == "*":
101
+ self.is_ref = True
102
+ self.identity = 100.0
103
+ return
104
+
105
+ attrs = CLUSTER_ATTR_PATTERN.fullmatch(match["attr"])
106
+ if not attrs:
107
+ self._raise_parsing_error()
108
+
109
+ self.is_ref = False
110
+ self.identity = float(attrs["percent"])
111
+ self.strand = Strand.PLUS if attrs["strand"] == "+" else Strand.REVERSE if attrs["strand"] == "-" else Strand.NONE
112
+
113
+ def _raise_parsing_error(self):
114
+ if self.line_number is not None:
115
+ raise ParsingError(self.line_number)
116
+ raise ValueError(f"Invalid cluster sequence line: {self.line}")
110
117
 
111
118
  def __repr__(self):
112
119
  return f"ClusterSequence(id={self.id}, name={self.name}, length={self.length}, identity={self.identity}, is_ref={self.is_ref}, seqtype={self.seqtype}, strand={self.strand})"
@@ -220,7 +227,7 @@ class FastaReader:
220
227
 
221
228
  line = line.strip()
222
229
  if not line.startswith(">"):
223
- clusterSequences.append( ClusterSequence(line.strip()) )
230
+ clusterSequences.append(ClusterSequence(line.strip(), line_number=self._line_number))
224
231
  if self._sequence_continues():
225
232
  continue
226
233
  return clusterSequences
@@ -330,7 +337,7 @@ class ClstrReader:
330
337
 
331
338
  line = line.strip()
332
339
  if not line.startswith(">"):
333
- clusterSequences.append( ClusterSequence(line.strip()) )
340
+ clusterSequences.append(ClusterSequence(line.strip(), line_number=self._line_number))
334
341
  if self._sequence_continues():
335
342
  continue
336
343
  return clusterSequences
@@ -1,4 +1,4 @@
1
1
  # Version file
2
- __version__ = "0.2.0"
2
+ __version__ = "0.3.0"
3
3
 
4
4
  __all__ = ["__version__"]
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from pathlib import Path
3
+ from io import StringIO
3
4
 
4
5
  import pytest
5
6
  import sys
@@ -60,6 +61,13 @@ def test_aa():
60
61
  assert [s.name for s in cluster.sequences] == cluster2
61
62
  assert cluster.refname == cluster2[0]
62
63
  n += 1
64
+
65
+
66
+ def test_invalid_member_line_raises_parsing_error():
67
+ with pytest.raises(ParsingError) as excinfo:
68
+ list(read_cdhit(StringIO(">Cluster 0\n0 492nt, >seq1.A...\n")))
69
+
70
+ assert excinfo.value.line_number == 2
63
71
 
64
72
  def _test_cluster_structure(cluster):
65
73
  # name
@@ -85,4 +93,4 @@ def _test_seq(s):
85
93
 
86
94
  assert s.strand is not None
87
95
  assert s.strand == Strand.PLUS or s.strand == Strand.REVERSE
88
- return True
96
+ return True
@@ -0,0 +1,34 @@
1
+ from io import StringIO
2
+
3
+ import pytest
4
+
5
+ from cdhit_reader._fasta import FastaParsingError, read_fasta
6
+
7
+
8
+ def test_fasta_reads_last_record_without_trailing_newline():
9
+ records = list(read_fasta(StringIO(">seq1 comment\nACG\n>seq2\nTT")))
10
+
11
+ assert [record.name for record in records] == ["seq1", "seq2"]
12
+ assert [record.comment for record in records] == ["comment", None]
13
+ assert [record.sequence for record in records] == ["ACG", "TT"]
14
+
15
+
16
+ def test_fasta_rejects_non_header_first_line():
17
+ with pytest.raises(FastaParsingError) as excinfo:
18
+ list(read_fasta(StringIO("seq1\nACG\n")))
19
+
20
+ assert excinfo.value.line_number == 1
21
+
22
+
23
+ def test_fasta_rejects_header_without_sequence():
24
+ with pytest.raises(FastaParsingError) as excinfo:
25
+ list(read_fasta(StringIO(">seq1\n")))
26
+
27
+ assert excinfo.value.line_number == 1
28
+
29
+
30
+ def test_fasta_rejects_header_without_sequence_before_next_record():
31
+ with pytest.raises(FastaParsingError) as excinfo:
32
+ list(read_fasta(StringIO(">seq1\n>seq2\nTT\n")))
33
+
34
+ assert excinfo.value.line_number == 1
@@ -0,0 +1,55 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+
5
+
6
+ def test_package_import_does_not_eagerly_import_cli_modules():
7
+ code = """
8
+ import json
9
+ import sys
10
+ import cdhit_reader
11
+
12
+ print(json.dumps({
13
+ "has_read_cdhit": hasattr(cdhit_reader, "read_cdhit"),
14
+ "cli_loaded": "cdhit_reader._cli" in sys.modules,
15
+ "compare_loaded": "cdhit_reader._compare" in sys.modules,
16
+ "test_loaded": "cdhit_reader._testit" in sys.modules,
17
+ }))
18
+ """
19
+ result = subprocess.run(
20
+ [sys.executable, "-c", code],
21
+ capture_output=True,
22
+ text=True,
23
+ check=True,
24
+ )
25
+
26
+ payload = json.loads(result.stdout)
27
+ assert payload == {
28
+ "has_read_cdhit": True,
29
+ "cli_loaded": False,
30
+ "compare_loaded": False,
31
+ "test_loaded": False,
32
+ }
33
+
34
+
35
+ def test_lazy_cli_export_imports_on_demand():
36
+ code = """
37
+ import json
38
+ import sys
39
+ import cdhit_reader
40
+
41
+ before = "cdhit_reader._cli" in sys.modules
42
+ _ = cdhit_reader.cli
43
+ after = "cdhit_reader._cli" in sys.modules
44
+
45
+ print(json.dumps({"before": before, "after": after}))
46
+ """
47
+ result = subprocess.run(
48
+ [sys.executable, "-c", code],
49
+ capture_output=True,
50
+ text=True,
51
+ check=True,
52
+ )
53
+
54
+ payload = json.loads(result.stdout)
55
+ assert payload == {"before": False, "after": True}
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cdhit-reader
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CD-HIT cluster parser
5
5
  Home-page: https://github.com/telatin/cdhit-parser
6
6
  Download-URL: https://github.com/telatin/cdhit-parser
@@ -18,8 +18,16 @@ Classifier: License :: OSI Approved :: MIT License
18
18
  Classifier: Operating System :: OS Independent
19
19
  Classifier: Programming Language :: Python
20
20
  Description-Content-Type: text/markdown
21
- Provides-Extra: cli
22
21
  License-File: LICENSE.md
22
+ Requires-Dist: click>=7.1.2
23
+ Requires-Dist: importlib-resources>=1.4.0
24
+ Requires-Dist: more-itertools>=8.4.0
25
+ Requires-Dist: pytest>=5.4.3
26
+ Requires-Dist: xopen>=1.0.1
27
+ Provides-Extra: cli
28
+ Requires-Dist: plotille; extra == "cli"
29
+ Dynamic: download-url
30
+ Dynamic: license-file
23
31
 
24
32
  # cdhit-parser
25
33
 
@@ -22,4 +22,6 @@ cdhit_reader.egg-info/zip-safe
22
22
  cdhit_reader/test/__init__.py
23
23
  cdhit_reader/test/aa.clstr
24
24
  cdhit_reader/test/nt.clstr
25
- cdhit_reader/test/test_clstr.py
25
+ cdhit_reader/test/test_clstr.py
26
+ cdhit_reader/test/test_fasta.py
27
+ cdhit_reader/test/test_package_import.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cdhit-compare = cdhit_reader._compare:compare
3
+ cdhit-parser = cdhit_reader._cli:cli
@@ -0,0 +1,8 @@
1
+ from setuptools import setup
2
+
3
+ if __name__ == "__main__":
4
+ console_scripts = [
5
+ "cdhit-parser = cdhit_reader._cli:cli",
6
+ "cdhit-compare = cdhit_reader._compare:compare",
7
+ ]
8
+ setup(entry_points=dict(console_scripts=console_scripts))
@@ -1,26 +0,0 @@
1
- from ._cli import cli
2
- from ._compare import compare
3
- from ._reader import ParsingError, ClusterSequence, Cluster, ClstrReader, read_cdhit, SeqType, Strand
4
- from ._fasta import Sequence, FastaReader, read_fasta
5
- from ._testit import test
6
- from ._version import __version__
7
- #from ._writer import FASTAWriter, write_fasta
8
-
9
- __all__ = [
10
- "ParsingError",
11
- "ClusterSequence",
12
- "Cluster",
13
- "ClstrReader",
14
- "read_cdhit",
15
- "FastaReader",
16
- "read_fasta",
17
- "SeqType",
18
- "Strand",
19
- "Sequence",
20
- "FastaReader",
21
- "read_fasta",
22
- "__version__",
23
- "cli",
24
- "compare",
25
- "test",
26
- ]
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- cdhit-compare = cdhit_reader:compare
3
- cdhit-parser = cdhit_reader:cli
@@ -1,5 +0,0 @@
1
- from setuptools import setup
2
-
3
- if __name__ == "__main__":
4
- console_scripts = ["cdhit-parser = cdhit_reader:cli", "cdhit-compare = cdhit_reader:compare"]
5
- setup(entry_points=dict(console_scripts=console_scripts))
File without changes
File without changes
File without changes
File without changes
File without changes