Repository: ShawHahnLab/igseq

Issue:
Duplicate inferred FASTA paths should only be handled once
`vdj-gather` can take fragments of builtin file paths as input, so you could do, for example:

    igseq vdj-gather sonarramesh/IGK sonarramesh/IGH/IGHD -o igdiscover-db-start

...and get IGK genes plus a placeholder D file.  But something like this results in D sequences being written twice:

    igseq vdj-gather sonarramesh/IGH sonarramesh/IGH/IGHD -o igdiscover-db-start

Instead `vdj.parse_vdj_paths` should condense duplicate FASTA file paths internally.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65385be..ac25fbe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 ## Changelog
 
+# dev
+
+Fixed:
+
+ * Duplicate FASTA paths found in vdj-gather will no longer result in
+   duplicated output sequences ([#2])
+
+[#2]: https://github.com/ShawHahnLab/igseq/pull/2
+
 # 0.1.0 - 2021-11-19
 
 First beta release
diff --git a/igseq/vdj.py b/igseq/vdj.py
index e6ac153..1778e34 100644
--- a/igseq/vdj.py
+++ b/igseq/vdj.py
@@ -67,6 +67,17 @@ def parse_vdj_paths(ref_paths):
                 attrs_list.append(attrs)
         else:
             raise util.IgSeqError("ref path not recognized: %s" % entry)
+    # handle duplicates produced from multiple ref_paths leading to the same
+    # files
+    groups = {}
+    for attrs in attrs_list:
+        if attrs["path"] not in groups:
+            groups[attrs["path"]] = attrs
+        else:
+            new_input = groups[attrs["path"]]["input"] + "; " + str(attrs["input"])
+            groups[attrs["path"]].update(attrs)
+            groups[attrs["path"]]["input"] = new_input
+    attrs_list = list(groups.values())
     return attrs_list
 
 def get_internal_vdj(name):
diff --git a/igseq/vdj_gather.py b/igseq/vdj_gather.py
index 40abbd1..6992dcc 100644
--- a/igseq/vdj_gather.py
+++ b/igseq/vdj_gather.py
@@ -18,5 +18,7 @@ def vdj_gather(ref_paths, dir_path_out, dry_run=False):
     LOGGER.info("given ref path(s): %s", ref_paths)
     LOGGER.info("given output: %s", dir_path_out)
     attrs_list = vdj.parse_vdj_paths(ref_paths)
+    for attrs in attrs_list:
+        LOGGER.info("inferred FASTA: %s (from %s)", attrs["path"], attrs["input"])
     paths = [attrs["path"] for attrs in attrs_list]
     vdj.combine_vdj(paths, dir_path_out, dry_run=dry_run)


Test patch:
diff --git a/test_igseq/test_vdj.py b/test_igseq/test_vdj.py
index 72b81d2..e0fd2ce 100644
--- a/test_igseq/test_vdj.py
+++ b/test_igseq/test_vdj.py
@@ -1,7 +1,6 @@
 """Tests for igseq.vdj."""
 
 from pathlib import Path
-from tempfile import TemporaryDirectory
 from igseq import vdj
 from igseq.util import IgSeqError, DATA
 from .util import TestBase
@@ -10,8 +9,7 @@ class TestParseVDJPaths(TestBase):
     """Basic test of parse_vdj_paths."""
 
     def test_parse_vdj_paths(self):
-        # It should give a list of dictionaries with info parsed from the given
-        # paths.
+        """parse_vdj_paths should give a list of dicts with info parsed from the given paths"""
         shared = {"fasta": True, "input": str(self.path/"input"), "type": "dir"}
         attrs_list_exp = [
                 {"path": self.path/"input/D.fasta", "segment": "D"},
@@ -28,9 +26,26 @@ class TestParseVDJPaths(TestBase):
             attrs_list = vdj.parse_vdj_paths(ref_paths)
             self.assertEqual(attrs_list, attrs_list_exp)
 
+    def test_parse_vdj_paths_duplicates(self):
+        """parse_vdj_paths shouldn't repeat paths that come from multiple input names"""
+        # Instead it'll just squish both input names into the same entries.
+        shared = {"fasta": True, "input": str(self.path/"input"), "type": "dir"}
+        attrs_list_exp = [
+                {"path": self.path/"input/D.fasta", "segment": "D"},
+                {"path": self.path/"input/J.fasta", "segment": "J"},
+                {"path": self.path/"input/V.fasta", "segment": "V"}]
+        for attrs in attrs_list_exp:
+            attrs.update(shared)
+        ref_paths = [self.path / "input", self.path/"input/D.fasta"]
+        attrs_list_exp[0]["type"] = "file"
+        attrs_list_exp[0]["input"] = "; ".join([str(p) for p in ref_paths])
+        attrs_list = vdj.parse_vdj_paths(ref_paths)
+        self.assertEqual(attrs_list, attrs_list_exp)
+
     def test_parse_vdj_paths_with_files(self):
-        # Individual files should work too.  In this case they're sorted as
-        # they're given, since the sorting is by-ref and then by-file.
+        """parse_vdj_paths should work with filenames as inputs"""
+        # In this case they're sorted as they're given, since the sorting is
+        # by-ref and then by-file.
         mkdict = lambda s: {
             "path": self.path/f"input/{s}.fasta",
             "input": str(self.path/f"input/{s}.fasta"),
@@ -43,8 +58,7 @@ class TestParseVDJPaths(TestBase):
         self.assertEqual(attrs_list, attrs_list_exp)
 
     def test_parse_vdj_paths_with_ref(self):
-        # If we also ask for a fragment of the filenames of builtin FASTA
-        # files, it should find those too
+        """parse_vdj_paths should work with builtin filename fragments"""
         shared = {"fasta": True, "input": str(self.path/"input"), "type": "dir"}
         attrs_list_exp = [
                 {"path": self.path/"input/D.fasta", "segment": "D"},