Repository: biopython/biopython

Issue:
Duplicate primers when reading SnapGene file
### Setup

I am reporting a problem with Biopython version, Python version, and operating system as follows:

```Python
3.8.13 (default, Jul 24 2022, 18:39:26) 
[GCC 8.3.0]
CPython
Linux-4.19.0-23-amd64-x86_64-with-glibc2.28
1.80
```

### Expected behaviour
Equivalent plasmid maps in .dna and .gbk formats should contain the same features when parsed with the snapgene and genbank parsers.

### Actual behaviour

@gouttegd 

Using SnapGene Server, I save a plasmid map as .dna and .gbk. When I parse those files, which should be identical to each other(?), in Biopython, the SnapGene parser reports duplicated primers, while the GenBank file does not, as one would expect. 

For example

```Python
from Bio import SeqIO
from collection_management.models import Plasmid

for p in Plasmid.objects.filter(created_by__id=2, id=4403):

    print(f"*** {p.id} ***\n")

    sg_map = SeqIO.read(p.map.path, "snapgene")
    print('Snapgene')
    print('\t' + "\t".join(sorted(
        [f"{f.qualifiers['label'][0]} {f.location}\n"
         for f in sg_map.features
         if f.qualifiers.get('label', False)])))

    print('Genbank')
    gb_map = SeqIO.read(p.map_gbk.path, "genbank")
    print('\t' + "\t".join(sorted(
        [f"{f.qualifiers['label'][0]} {f.location}\n"
         for f in gb_map.features
         if f.qualifiers.get('label', False)])))
```

returns 

```
*** 4403 ***

Snapgene
	1468 [767:788](+)
	1468 [767:788](+)
	5076 [4340:4365](-)
	5076 [4340:4365](-)
	5093 [897:957](+)
	5093 [897:957](+)
	5094 [3192:3234](-)
	5094 [3192:3234](-)
	5095 [3194:3238](+)
	5095 [3194:3238](+)
	AmpR join{[7956:8025](-), [7164:7956](-)}
	AmpR promoter [8025:8130](-)
	CAP binding site [6096:6118](+)
	CMV enhancer [234:614](+)
	CMV promoter [614:818](+)
	CTCF [992:3173](+)
	CTCF [992:3173](+)
	Dam methylase [3251:4091](+)
	FLAG [3221:3248](+)
	FLAG [3224:3248](+)
	FRT [4626:4674](+)
	HygR [4681:5704](+)
	M13 rev [6003:6020](-)
	SV40 poly(A) signal [5833:5955](+)
	attB1 [962:987](+)
	attB2 [3177:3202](-)
	bGH poly(A) signal [4118:4343](+)
	lac operator [6027:6044](+)
	lac promoter join{[6076:6082](-), [6058:6076](-), [6051:6058](-)}
	ori [6405:6994](-)
	tet operator [819:838](+)
	tet operator [840:859](+)

Genbank
	1468 [768:789](+)
	5076 [4341:4366](-)
	5093 [898:958](+)
	5094 [3193:3235](-)
	5095 [3195:3239](+)
	AmpR [7164:8025](-)
	AmpR promoter [8025:8130](-)
	CAP binding site [6096:6118](+)
	CMV enhancer [234:614](+)
	CMV promoter [614:818](+)
	CTCF [992:3173](+)
	CTCF [992:3173](+)
	Dam methylase [3251:4091](+)
	FLAG [3221:3248](+)
	FLAG [3224:3248](+)
	FRT [4626:4674](+)
	HygR [4681:5704](+)
	M13 rev [6003:6020](-)
	SV40 poly(A) signal [5833:5955](+)
	attB1 [962:987](+)
	attB2 [3177:3202](-)
	bGH poly(A) signal [4118:4343](+)
	lac operator [6027:6044](+)
	lac promoter [6051:6082](-)
	ori [6405:6994](-)
	tet operator [819:838](+)
	tet operator [840:859](+)

```

The plasmid maps are attached.

Thanks!
[pHU4403 - pDEST_FRT_TO-Cterm-FLAG-Dam - CTCF.zip](https://github.com/biopython/biopython/files/10455588/pHU4403.-.pDEST_FRT_TO-Cterm-FLAG-Dam.-.CTCF.zip)



Discussion:
The lac promoter appears to be fragmented into three contiguous pieces in the snapgene file, that's also odd.

@gouttegd Are you able to take a look at this please?
@peterjc Yes, you may assign to me.
> The lac promoter appears to be fragmented into three contiguous pieces in the snapgene file, that's also odd.
> 
> @gouttegd Are you able to take a look at this please?

The slit feature is actually correct, see https://github.com/biopython/biopython/issues/3655#issuecomment-884945935
It seems the SnapGene folks have come up with a new idea…

All primer specifications in that file have _two_ `BindingSite` nodes instead of one:

```xml
<Primer recentID="4" name="1468" sequence="CGCAAATGGGCGGTAGGCGTG">
  <BindingSite location="768-788" boundStrand="0" annealedBases="CGCAAATGGGCGGTAGGCGTG" meltingTemperature="65">
    <Component hybridizedRange="768-788" bases="CGCAAATGGGCGGTAGGCGTG"/>
  </BindingSite>
  <BindingSite simplified="1" location="768-788" boundStrand="0" annealedBases="CGCAAATGGGCGGTAGGCGTG" meltingTemperature="65">
    <Component hybridizedRange="768-788" bases="CGCAAATGGGCGGTAGGCGTG"/>
  </BindingSite>
</Primer>
```

For each primers, the two `BindingSite` nodes are completely identical except for the `simplified="1"` attribute on the second node. Not sure what they were thinking… Looks like the second node was intended to be a “simplified” representation of the primer’s binding site, but then they forgot to actually simplify it… :D 

I’ll update the parser to simply ignore these so-called “simplified” primers if a “non-simplified” one already exists.
Do either of you have a technical contact at Snapgene to raise this issue with? It would be even nicer to just have them fix their output.
I don’t. All I know is the general “[contact us](https://support.snapgene.com/hc/en-us/requests/new)” form on SnapGene’s website. We can try this channel but I don’t have much hope it would lead to anything.

And in any case, even if they do fix their output, our parser would still have to cope with the already existing files that have been produced with the bogus “simplified” primers.
I can contact them. But I suspect they'll just say that's how it's supposed to be.
Yes please @nicolazilio - and if they say it is deliberate, the @gouttegd's proposal sounds good.

Resolving patch:
diff --git a/Bio/SeqIO/SnapGeneIO.py b/Bio/SeqIO/SnapGeneIO.py
--- a/Bio/SeqIO/SnapGeneIO.py
+++ b/Bio/SeqIO/SnapGeneIO.py
@@ -109,10 +109,15 @@ def _parse_cookie_packet(length, data, record):
         raise ValueError("The file is not a valid SnapGene file")
 
 
-def _parse_location(rangespec, strand, record):
+def _parse_location(rangespec, strand, record, is_primer=False):
     start, end = (int(x) for x in rangespec.split("-"))
     # Account for SnapGene's 1-based coordinates
     start = start - 1
+    if is_primer:
+        # Primers' coordinates in SnapGene files are shifted by -1
+        # for some reasons
+        start += 1
+        end += 1
     if start > end:
         # Range wrapping the end of the sequence
         l1 = SimpleLocation(start, len(record), strand=strand)
@@ -217,6 +222,7 @@ def _parse_primers_packet(length, data, record):
         if name:
             quals["label"] = [name]
 
+        locations = []
         for site in primer.getElementsByTagName("BindingSite"):
             rng = _get_attribute_value(
                 site, "location", error="Missing binding site location"
@@ -227,8 +233,15 @@ def _parse_primers_packet(length, data, record):
             else:
                 strand = +1
 
+            location = _parse_location(rng, strand, record, is_primer=True)
+            simplified = int(_get_attribute_value(site, "simplified", default="0")) == 1
+            if simplified and location in locations:
+                # Duplicate "simplified" binding site, ignore
+                continue
+
+            locations.append(location)
             feature = SeqFeature(
-                _parse_location(rng, strand, record),
+                location,
                 type="primer_bind",
                 qualifiers=quals,
             )


Test patch:
diff --git a/Tests/SnapGene/sample-f.dna b/Tests/SnapGene/sample-f.dna
Binary files a/Tests/SnapGene/sample-f.dna and b/Tests/SnapGene/sample-f.dna differ
diff --git a/Tests/test_SeqIO_SnapGene.py b/Tests/test_SeqIO_SnapGene.py
--- a/Tests/test_SeqIO_SnapGene.py
+++ b/Tests/test_SeqIO_SnapGene.py
@@ -97,7 +97,7 @@ class TestSnapGene(unittest.TestCase):
             "id": "Sample",
             "description": "Sample Sequence F",
             "length": 1000,
-            "date": datetime.datetime(2021, 7, 26, 0, 0),
+            "date": datetime.datetime(2023, 1, 22, 0, 0),
             "topology": "circular",
             "features": [
                 {
@@ -128,6 +128,13 @@ class TestSnapGene(unittest.TestCase):
                         {"start": 214, "end": 241},
                     ],
                 },
+                {
+                    "type": "primer_bind",
+                    "start": 751,
+                    "end": 776,
+                    "strand": 1,
+                    "label": ["Primer 1"],
+                },
             ],
         },
         "pFA-KanMX4": {