We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
BioPython returns label of SnapGene Feature, not title
### Setup

I am reporting a problem with Biopython version, Python version, and operating
system as follows:

```
[GCC 6.3.0 20170516]
CPython
Linux-4.19.0-10-amd64-x86_64-with-debian-10.5
1.78
```
### Expected behaviour

I would like to get the names of all features found in >4000 SnapGene plasmid maps. I would expect the name of the features returned by BioPython to be the same as those returned by SnapGene Server's reportFeatures function.

### Actual behaviour

For some features, the name returned by BioPython is different from that returned by SnapGene Server's reportFeatures function. 

The problematic features are those that have both a title and a label attribute, for instance:

![image](https://user-images.githubusercontent.com/2800360/93993371-f4a72700-fd8e-11ea-9223-6bb4325f4cd5.png)

In the example above, BioPython reports the label `lac operator` while SnapGene Server returns the 
title `lac repressor encoded by lacI lac operator`. This would not be so much of a problem if I could find `lac repressor encoded by lacI lac operator` anywhere in the relevant SeqFeature object, but I cannot, unless, of course, I missed it.

### Steps to reproduce

I am attaching a relevant example SnapGene file.

[example.zip](https://github.com/biopython/biopython/files/5267092/example.zip)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `Bio/SeqIO/SnapGeneIO.py`
Content:
```
1 # Copyright 2017-2019 Damien Goutte-Gattat.  All rights reserved.
2 #
3 # This file is part of the Biopython distribution and governed by your
4 # choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
5 # Please see the LICENSE file that should have been included as part of this
6 # package.
7 
8 """Bio.SeqIO support for the SnapGene file format.
9 
10 The SnapGene binary format is the native format used by the SnapGene program
11 from GSL Biotech LLC.
12 """
13 
14 from datetime import datetime
15 from re import sub
16 from struct import unpack
17 from xml.dom.minidom import parseString
18 
19 from Bio.Seq import Seq
20 from Bio.SeqFeature import SeqFeature, FeatureLocation
21 from Bio.SeqRecord import SeqRecord
22 from .Interfaces import SequenceIterator
23 
24 
25 def _iterate(handle):
26     """Iterate over the packets of a SnapGene file.
27 
28     A SnapGene file is made of packets, each packet being a TLV-like
29     structure comprising:
30 
31       - 1 single byte indicating the packet's type;
32       - 1 big-endian long integer (4 bytes) indicating the length of the
33         packet's data;
34       - the actual data.
35     """
36     while True:
37         packet_type = handle.read(1)
38         if len(packet_type) < 1:  # No more packet
39             return
40         packet_type = unpack(">B", packet_type)[0]
41 
42         length = handle.read(4)
43         if len(length) < 4:
44             raise ValueError("Unexpected end of packet")
45         length = unpack(">I", length)[0]
46 
47         data = handle.read(length)
48         if len(data) < length:
49             raise ValueError("Unexpected end of packet")
50 
51         yield (packet_type, length, data)
52 
53 
54 def _parse_dna_packet(length, data, record):
55     """Parse a DNA sequence packet.
56 
57     A DNA sequence packet contains a single byte flag followed by the
58     sequence itself.
59     """
60     if record.seq:
61         raise ValueError("The file contains more than one DNA packet")
62 
63     flags, sequence = unpack(">B%ds" % (length - 1), data)
64     record.seq = Seq(sequence.decode("ASCII"))
65     record.annotations["molecule_type"] = "DNA"
66     if flags & 0x01:
67         record.annotations["topology"] = "circular"
68     else:
69         record.annotations["topology"] = "linear"
70 
71 
72 def _parse_notes_packet(length, data, record):
73     """Parse a 'Notes' packet.
74 
75     This type of packet contains some metadata about the sequence. They
76     are stored as a XML string with a 'Notes' root node.
77     """
78     xml = parseString(data.decode("UTF-8"))
79     type = _get_child_value(xml, "Type")
80     if type == "Synthetic":
81         record.annotations["data_file_division"] = "SYN"
82     else:
83         record.annotations["data_file_division"] = "UNC"
84 
85     date = _get_child_value(xml, "LastModified")
86     if date:
87         record.annotations["date"] = datetime.strptime(date, "%Y.%m.%d")
88 
89     acc = _get_child_value(xml, "AccessionNumber")
90     if acc:
91         record.id = acc
92 
93     comment = _get_child_value(xml, "Comments")
94     if comment:
95         record.name = comment.split(" ", 1)[0]
96         record.description = comment
97         if not acc:
98             record.id = record.name
99 
100 
101 def _parse_cookie_packet(length, data, record):
102     """Parse a SnapGene cookie packet.
103 
104     Every SnapGene file starts with a packet of this type. It acts as
105     a magic cookie identifying the file as a SnapGene file.
106     """
107     cookie, seq_type, exp_version, imp_version = unpack(">8sHHH", data)
108     if cookie.decode("ASCII") != "SnapGene":
109         raise ValueError("The file is not a valid SnapGene file")
110 
111 
112 def _parse_location(rangespec, strand, record):
113     start, end = [int(x) for x in rangespec.split("-")]
114     # Account for SnapGene's 1-based coordinates
115     start = start - 1
116     if start > end:
117         # Range wrapping the end of the sequence
118         l1 = FeatureLocation(start, len(record), strand=strand)
119         l2 = FeatureLocation(0, end, strand=strand)
120         location = l1 + l2
121     else:
122         location = FeatureLocation(start, end, strand=strand)
123     return location
124 
125 
126 def _parse_features_packet(length, data, record):
127     """Parse a sequence features packet.
128 
129     This packet stores sequence features (except primer binding sites,
130     which are in a dedicated Primers packet). The data is a XML string
131     starting with a 'Features' root node.
132     """
133     xml = parseString(data.decode("UTF-8"))
134     for feature in xml.getElementsByTagName("Feature"):
135         quals = {}
136 
137         type = _get_attribute_value(feature, "type", default="misc_feature")
138         label = _get_attribute_value(feature, "name")
139         if label:
140             quals["label"] = [label]
141 
142         strand = +1
143         directionality = int(
144             _get_attribute_value(feature, "directionality", default="1")
145         )
146         if directionality == 2:
147             strand = -1
148 
149         location = None
150         for segment in feature.getElementsByTagName("Segment"):
151             rng = _get_attribute_value(segment, "range")
152             if not location:
153                 location = _parse_location(rng, strand, record)
154             else:
155                 location = location + _parse_location(rng, strand, record)
156         if not location:
157             raise ValueError("Missing feature location")
158 
159         for qualifier in feature.getElementsByTagName("Q"):
160             qname = _get_attribute_value(
161                 qualifier, "name", error="Missing qualifier name"
162             )
163             qvalues = []
164             for value in qualifier.getElementsByTagName("V"):
165                 if value.hasAttribute("text"):
166                     qvalues.append(_decode(value.attributes["text"].value))
167                 elif value.hasAttribute("predef"):
168                     qvalues.append(_decode(value.attributes["predef"].value))
169                 elif value.hasAttribute("int"):
170                     qvalues.append(int(value.attributes["int"].value))
171             quals[qname] = qvalues
172 
173         feature = SeqFeature(location, type=type, qualifiers=quals)
174         record.features.append(feature)
175 
176 
177 def _parse_primers_packet(length, data, record):
178     """Parse a Primers packet.
179 
180     A Primers packet is similar to a Features packet but specifically
181     stores primer binding features. The data is a XML string starting
182     with a 'Primers' root node.
183     """
184     xml = parseString(data.decode("UTF-8"))
185     for primer in xml.getElementsByTagName("Primer"):
186         quals = {}
187 
188         name = _get_attribute_value(primer, "name")
189         if name:
190             quals["label"] = [name]
191 
192         for site in primer.getElementsByTagName("BindingSite"):
193             rng = _get_attribute_value(
194                 site, "location", error="Missing binding site location"
195             )
196             strand = int(_get_attribute_value(site, "boundStrand", default="0"))
197             if strand == 1:
198                 strand = -1
199             else:
200                 strand = +1
201 
202             feature = SeqFeature(
203                 _parse_location(rng, strand, record),
204                 type="primer_bind",
205                 qualifiers=quals,
206             )
207             record.features.append(feature)
208 
209 
210 _packet_handlers = {
211     0x00: _parse_dna_packet,
212     0x05: _parse_primers_packet,
213     0x06: _parse_notes_packet,
214     0x0A: _parse_features_packet,
215 }
216 
217 
218 # Helper functions to process the XML data in
219 # some of the segments
220 
221 
222 def _decode(text):
223     # Get rid of HTML tags in some values
224     return sub("<[^>]+>", "", text)
225 
226 
227 def _get_attribute_value(node, name, default=None, error=None):
228     if node.hasAttribute(name):
229         return _decode(node.attributes[name].value)
230     elif error:
231         raise ValueError(error)
232     else:
233         return default
234 
235 
236 def _get_child_value(node, name, default=None, error=None):
237     children = node.getElementsByTagName(name)
238     if (
239         children
240         and children[0].childNodes
241         and children[0].firstChild.nodeType == node.TEXT_NODE
242     ):
243         return _decode(children[0].firstChild.data)
244     elif error:
245         raise ValueError(error)
246     else:
247         return default
248 
249 
250 class SnapGeneIterator(SequenceIterator):
251     """Parser for SnapGene files."""
252 
253     def __init__(self, source):
254         """Parse a SnapGene file and return a SeqRecord object.
255 
256         Argument source is a file-like object or a path to a file.
257 
258         Note that a SnapGene file can only contain one sequence, so this
259         iterator will always return a single record.
260         """
261         super().__init__(source, mode="b", fmt="SnapGene")
262 
263     def parse(self, handle):
264         """Start parsing the file, and return a SeqRecord generator."""
265         records = self.iterate(handle)
266         return records
267 
268     def iterate(self, handle):
269         """Iterate over the records in the SnapGene file."""
270         record = SeqRecord(None)
271         packets = _iterate(handle)
272         try:
273             packet_type, length, data = next(packets)
274         except StopIteration:
275             raise ValueError("Empty file.") from None
276 
277         if packet_type != 0x09:
278             raise ValueError("The file does not start with a SnapGene cookie packet")
279         _parse_cookie_packet(length, data, record)
280 
281         for (packet_type, length, data) in packets:
282             handler = _packet_handlers.get(packet_type)
283             if handler is not None:
284                 handler(length, data, record)
285 
286         if not record.seq:
287             raise ValueError("No DNA packet in file")
288 
289         yield record
290
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/Bio/SeqIO/SnapGeneIO.py b/Bio/SeqIO/SnapGeneIO.py
--- a/Bio/SeqIO/SnapGeneIO.py
+++ b/Bio/SeqIO/SnapGeneIO.py
@@ -135,9 +135,6 @@
         quals = {}
 
         type = _get_attribute_value(feature, "type", default="misc_feature")
-        label = _get_attribute_value(feature, "name")
-        if label:
-            quals["label"] = [label]
 
         strand = +1
         directionality = int(
@@ -170,6 +167,16 @@
                     qvalues.append(int(value.attributes["int"].value))
             quals[qname] = qvalues
 
+        name = _get_attribute_value(feature, "name")
+        if name:
+            if "label" not in quals:
+                # No explicit label attribute, use the SnapGene name
+                quals["label"] = [name]
+            elif name not in quals["label"]:
+                # The SnapGene name is different from the label,
+                # add a specific attribute to represent it
+                quals["name"] = [name]
+
         feature = SeqFeature(location, type=type, qualifiers=quals)
         record.features.append(feature)

{"golden_diff": "diff --git a/Bio/SeqIO/SnapGeneIO.py b/Bio/SeqIO/SnapGeneIO.py\n--- a/Bio/SeqIO/SnapGeneIO.py\n+++ b/Bio/SeqIO/SnapGeneIO.py\n@@ -135,9 +135,6 @@\n         quals = {}\n \n         type = _get_attribute_value(feature, \"type\", default=\"misc_feature\")\n-        label = _get_attribute_value(feature, \"name\")\n-        if label:\n-            quals[\"label\"] = [label]\n \n         strand = +1\n         directionality = int(\n@@ -170,6 +167,16 @@\n                     qvalues.append(int(value.attributes[\"int\"].value))\n             quals[qname] = qvalues\n \n+        name = _get_attribute_value(feature, \"name\")\n+        if name:\n+            if \"label\" not in quals:\n+                # No explicit label attribute, use the SnapGene name\n+                quals[\"label\"] = [name]\n+            elif name not in quals[\"label\"]:\n+                # The SnapGene name is different from the label,\n+                # add a specific attribute to represent it\n+                quals[\"name\"] = [name]\n+\n         feature = SeqFeature(location, type=type, qualifiers=quals)\n         record.features.append(feature)\n", "issue": "BioPython returns label of SnapGene Feature, not title\n### Setup\r\n\r\nI am reporting a problem with Biopython version, Python version, and operating\r\nsystem as follows:\r\n\r\n```\r\n[GCC 6.3.0 20170516]\r\nCPython\r\nLinux-4.19.0-10-amd64-x86_64-with-debian-10.5\r\n1.78\r\n```\r\n### Expected behaviour\r\n\r\nI would like to get the names of all features found in >4000 SnapGene plasmid maps. I would expect the name of the features returned by BioPython to be the same as those returned by SnapGene Server's reportFeatures function.\r\n\r\n### Actual behaviour\r\n\r\nFor some features, the name returned by BioPython is different from that returned by SnapGene Server's reportFeatures function. \r\n\r\nThe problematic features are those that have both a title and a label attribute, for instance:\r\n\r\n![image](https://user-images.githubusercontent.com/2800360/93993371-f4a72700-fd8e-11ea-9223-6bb4325f4cd5.png)\r\n\r\nIn the example above, BioPython reports the label `lac operator` while SnapGene Server returns the \r\ntitle `lac repressor encoded by lacI lac operator`. This would not be so much of a problem if I could find `lac repressor encoded by lacI lac operator` anywhere in the relevant SeqFeature object, but I cannot, unless, of course, I missed it.\r\n\r\n### Steps to reproduce\r\n\r\nI am attaching a relevant example SnapGene file.\r\n\r\n[example.zip](https://github.com/biopython/biopython/files/5267092/example.zip)\n", "before_files": [{"content": "# Copyright 2017-2019 Damien Goutte-Gattat.  All rights reserved.\n#\n# This file is part of the Biopython distribution and governed by your\n# choice of the \"Biopython License Agreement\" or the \"BSD 3-Clause License\".\n# Please see the LICENSE file that should have been included as part of this\n# package.\n\n\"\"\"Bio.SeqIO support for the SnapGene file format.\n\nThe SnapGene binary format is the native format used by the SnapGene program\nfrom GSL Biotech LLC.\n\"\"\"\n\nfrom datetime import datetime\nfrom re import sub\nfrom struct import unpack\nfrom xml.dom.minidom import parseString\n\nfrom Bio.Seq import Seq\nfrom Bio.SeqFeature import SeqFeature, FeatureLocation\nfrom Bio.SeqRecord import SeqRecord\nfrom .Interfaces import SequenceIterator\n\n\ndef _iterate(handle):\n    \"\"\"Iterate over the packets of a SnapGene file.\n\n    A SnapGene file is made of packets, each packet being a TLV-like\n    structure comprising:\n\n      - 1 single byte indicating the packet's type;\n      - 1 big-endian long integer (4 bytes) indicating the length of the\n        packet's data;\n      - the actual data.\n    \"\"\"\n    while True:\n        packet_type = handle.read(1)\n        if len(packet_type) < 1:  # No more packet\n            return\n        packet_type = unpack(\">B\", packet_type)[0]\n\n        length = handle.read(4)\n        if len(length) < 4:\n            raise ValueError(\"Unexpected end of packet\")\n        length = unpack(\">I\", length)[0]\n\n        data = handle.read(length)\n        if len(data) < length:\n            raise ValueError(\"Unexpected end of packet\")\n\n        yield (packet_type, length, data)\n\n\ndef _parse_dna_packet(length, data, record):\n    \"\"\"Parse a DNA sequence packet.\n\n    A DNA sequence packet contains a single byte flag followed by the\n    sequence itself.\n    \"\"\"\n    if record.seq:\n        raise ValueError(\"The file contains more than one DNA packet\")\n\n    flags, sequence = unpack(\">B%ds\" % (length - 1), data)\n    record.seq = Seq(sequence.decode(\"ASCII\"))\n    record.annotations[\"molecule_type\"] = \"DNA\"\n    if flags & 0x01:\n        record.annotations[\"topology\"] = \"circular\"\n    else:\n        record.annotations[\"topology\"] = \"linear\"\n\n\ndef _parse_notes_packet(length, data, record):\n    \"\"\"Parse a 'Notes' packet.\n\n    This type of packet contains some metadata about the sequence. They\n    are stored as a XML string with a 'Notes' root node.\n    \"\"\"\n    xml = parseString(data.decode(\"UTF-8\"))\n    type = _get_child_value(xml, \"Type\")\n    if type == \"Synthetic\":\n        record.annotations[\"data_file_division\"] = \"SYN\"\n    else:\n        record.annotations[\"data_file_division\"] = \"UNC\"\n\n    date = _get_child_value(xml, \"LastModified\")\n    if date:\n        record.annotations[\"date\"] = datetime.strptime(date, \"%Y.%m.%d\")\n\n    acc = _get_child_value(xml, \"AccessionNumber\")\n    if acc:\n        record.id = acc\n\n    comment = _get_child_value(xml, \"Comments\")\n    if comment:\n        record.name = comment.split(\" \", 1)[0]\n        record.description = comment\n        if not acc:\n            record.id = record.name\n\n\ndef _parse_cookie_packet(length, data, record):\n    \"\"\"Parse a SnapGene cookie packet.\n\n    Every SnapGene file starts with a packet of this type. It acts as\n    a magic cookie identifying the file as a SnapGene file.\n    \"\"\"\n    cookie, seq_type, exp_version, imp_version = unpack(\">8sHHH\", data)\n    if cookie.decode(\"ASCII\") != \"SnapGene\":\n        raise ValueError(\"The file is not a valid SnapGene file\")\n\n\ndef _parse_location(rangespec, strand, record):\n    start, end = [int(x) for x in rangespec.split(\"-\")]\n    # Account for SnapGene's 1-based coordinates\n    start = start - 1\n    if start > end:\n        # Range wrapping the end of the sequence\n        l1 = FeatureLocation(start, len(record), strand=strand)\n        l2 = FeatureLocation(0, end, strand=strand)\n        location = l1 + l2\n    else:\n        location = FeatureLocation(start, end, strand=strand)\n    return location\n\n\ndef _parse_features_packet(length, data, record):\n    \"\"\"Parse a sequence features packet.\n\n    This packet stores sequence features (except primer binding sites,\n    which are in a dedicated Primers packet). The data is a XML string\n    starting with a 'Features' root node.\n    \"\"\"\n    xml = parseString(data.decode(\"UTF-8\"))\n    for feature in xml.getElementsByTagName(\"Feature\"):\n        quals = {}\n\n        type = _get_attribute_value(feature, \"type\", default=\"misc_feature\")\n        label = _get_attribute_value(feature, \"name\")\n        if label:\n            quals[\"label\"] = [label]\n\n        strand = +1\n        directionality = int(\n            _get_attribute_value(feature, \"directionality\", default=\"1\")\n        )\n        if directionality == 2:\n            strand = -1\n\n        location = None\n        for segment in feature.getElementsByTagName(\"Segment\"):\n            rng = _get_attribute_value(segment, \"range\")\n            if not location:\n                location = _parse_location(rng, strand, record)\n            else:\n                location = location + _parse_location(rng, strand, record)\n        if not location:\n            raise ValueError(\"Missing feature location\")\n\n        for qualifier in feature.getElementsByTagName(\"Q\"):\n            qname = _get_attribute_value(\n                qualifier, \"name\", error=\"Missing qualifier name\"\n            )\n            qvalues = []\n            for value in qualifier.getElementsByTagName(\"V\"):\n                if value.hasAttribute(\"text\"):\n                    qvalues.append(_decode(value.attributes[\"text\"].value))\n                elif value.hasAttribute(\"predef\"):\n                    qvalues.append(_decode(value.attributes[\"predef\"].value))\n                elif value.hasAttribute(\"int\"):\n                    qvalues.append(int(value.attributes[\"int\"].value))\n            quals[qname] = qvalues\n\n        feature = SeqFeature(location, type=type, qualifiers=quals)\n        record.features.append(feature)\n\n\ndef _parse_primers_packet(length, data, record):\n    \"\"\"Parse a Primers packet.\n\n    A Primers packet is similar to a Features packet but specifically\n    stores primer binding features. The data is a XML string starting\n    with a 'Primers' root node.\n    \"\"\"\n    xml = parseString(data.decode(\"UTF-8\"))\n    for primer in xml.getElementsByTagName(\"Primer\"):\n        quals = {}\n\n        name = _get_attribute_value(primer, \"name\")\n        if name:\n            quals[\"label\"] = [name]\n\n        for site in primer.getElementsByTagName(\"BindingSite\"):\n            rng = _get_attribute_value(\n                site, \"location\", error=\"Missing binding site location\"\n            )\n            strand = int(_get_attribute_value(site, \"boundStrand\", default=\"0\"))\n            if strand == 1:\n                strand = -1\n            else:\n                strand = +1\n\n            feature = SeqFeature(\n                _parse_location(rng, strand, record),\n                type=\"primer_bind\",\n                qualifiers=quals,\n            )\n            record.features.append(feature)\n\n\n_packet_handlers = {\n    0x00: _parse_dna_packet,\n    0x05: _parse_primers_packet,\n    0x06: _parse_notes_packet,\n    0x0A: _parse_features_packet,\n}\n\n\n# Helper functions to process the XML data in\n# some of the segments\n\n\ndef _decode(text):\n    # Get rid of HTML tags in some values\n    return sub(\"<[^>]+>\", \"\", text)\n\n\ndef _get_attribute_value(node, name, default=None, error=None):\n    if node.hasAttribute(name):\n        return _decode(node.attributes[name].value)\n    elif error:\n        raise ValueError(error)\n    else:\n        return default\n\n\ndef _get_child_value(node, name, default=None, error=None):\n    children = node.getElementsByTagName(name)\n    if (\n        children\n        and children[0].childNodes\n        and children[0].firstChild.nodeType == node.TEXT_NODE\n    ):\n        return _decode(children[0].firstChild.data)\n    elif error:\n        raise ValueError(error)\n    else:\n        return default\n\n\nclass SnapGeneIterator(SequenceIterator):\n    \"\"\"Parser for SnapGene files.\"\"\"\n\n    def __init__(self, source):\n        \"\"\"Parse a SnapGene file and return a SeqRecord object.\n\n        Argument source is a file-like object or a path to a file.\n\n        Note that a SnapGene file can only contain one sequence, so this\n        iterator will always return a single record.\n        \"\"\"\n        super().__init__(source, mode=\"b\", fmt=\"SnapGene\")\n\n    def parse(self, handle):\n        \"\"\"Start parsing the file, and return a SeqRecord generator.\"\"\"\n        records = self.iterate(handle)\n        return records\n\n    def iterate(self, handle):\n        \"\"\"Iterate over the records in the SnapGene file.\"\"\"\n        record = SeqRecord(None)\n        packets = _iterate(handle)\n        try:\n            packet_type, length, data = next(packets)\n        except StopIteration:\n            raise ValueError(\"Empty file.\") from None\n\n        if packet_type != 0x09:\n            raise ValueError(\"The file does not start with a SnapGene cookie packet\")\n        _parse_cookie_packet(length, data, record)\n\n        for (packet_type, length, data) in packets:\n            handler = _packet_handlers.get(packet_type)\n            if handler is not None:\n                handler(length, data, record)\n\n        if not record.seq:\n            raise ValueError(\"No DNA packet in file\")\n\n        yield record\n", "path": "Bio/SeqIO/SnapGeneIO.py"}], "after_files": [{"content": "# Copyright 2017-2019 Damien Goutte-Gattat.  All rights reserved.\n#\n# This file is part of the Biopython distribution and governed by your\n# choice of the \"Biopython License Agreement\" or the \"BSD 3-Clause License\".\n# Please see the LICENSE file that should have been included as part of this\n# package.\n\n\"\"\"Bio.SeqIO support for the SnapGene file format.\n\nThe SnapGene binary format is the native format used by the SnapGene program\nfrom GSL Biotech LLC.\n\"\"\"\n\nfrom datetime import datetime\nfrom re import sub\nfrom struct import unpack\nfrom xml.dom.minidom import parseString\n\nfrom Bio.Seq import Seq\nfrom Bio.SeqFeature import SeqFeature, FeatureLocation\nfrom Bio.SeqRecord import SeqRecord\nfrom .Interfaces import SequenceIterator\n\n\ndef _iterate(handle):\n    \"\"\"Iterate over the packets of a SnapGene file.\n\n    A SnapGene file is made of packets, each packet being a TLV-like\n    structure comprising:\n\n      - 1 single byte indicating the packet's type;\n      - 1 big-endian long integer (4 bytes) indicating the length of the\n        packet's data;\n      - the actual data.\n    \"\"\"\n    while True:\n        packet_type = handle.read(1)\n        if len(packet_type) < 1:  # No more packet\n            return\n        packet_type = unpack(\">B\", packet_type)[0]\n\n        length = handle.read(4)\n        if len(length) < 4:\n            raise ValueError(\"Unexpected end of packet\")\n        length = unpack(\">I\", length)[0]\n\n        data = handle.read(length)\n        if len(data) < length:\n            raise ValueError(\"Unexpected end of packet\")\n\n        yield (packet_type, length, data)\n\n\ndef _parse_dna_packet(length, data, record):\n    \"\"\"Parse a DNA sequence packet.\n\n    A DNA sequence packet contains a single byte flag followed by the\n    sequence itself.\n    \"\"\"\n    if record.seq:\n        raise ValueError(\"The file contains more than one DNA packet\")\n\n    flags, sequence = unpack(\">B%ds\" % (length - 1), data)\n    record.seq = Seq(sequence.decode(\"ASCII\"))\n    record.annotations[\"molecule_type\"] = \"DNA\"\n    if flags & 0x01:\n        record.annotations[\"topology\"] = \"circular\"\n    else:\n        record.annotations[\"topology\"] = \"linear\"\n\n\ndef _parse_notes_packet(length, data, record):\n    \"\"\"Parse a 'Notes' packet.\n\n    This type of packet contains some metadata about the sequence. They\n    are stored as a XML string with a 'Notes' root node.\n    \"\"\"\n    xml = parseString(data.decode(\"UTF-8\"))\n    type = _get_child_value(xml, \"Type\")\n    if type == \"Synthetic\":\n        record.annotations[\"data_file_division\"] = \"SYN\"\n    else:\n        record.annotations[\"data_file_division\"] = \"UNC\"\n\n    date = _get_child_value(xml, \"LastModified\")\n    if date:\n        record.annotations[\"date\"] = datetime.strptime(date, \"%Y.%m.%d\")\n\n    acc = _get_child_value(xml, \"AccessionNumber\")\n    if acc:\n        record.id = acc\n\n    comment = _get_child_value(xml, \"Comments\")\n    if comment:\n        record.name = comment.split(\" \", 1)[0]\n        record.description = comment\n        if not acc:\n            record.id = record.name\n\n\ndef _parse_cookie_packet(length, data, record):\n    \"\"\"Parse a SnapGene cookie packet.\n\n    Every SnapGene file starts with a packet of this type. It acts as\n    a magic cookie identifying the file as a SnapGene file.\n    \"\"\"\n    cookie, seq_type, exp_version, imp_version = unpack(\">8sHHH\", data)\n    if cookie.decode(\"ASCII\") != \"SnapGene\":\n        raise ValueError(\"The file is not a valid SnapGene file\")\n\n\ndef _parse_location(rangespec, strand, record):\n    start, end = [int(x) for x in rangespec.split(\"-\")]\n    # Account for SnapGene's 1-based coordinates\n    start = start - 1\n    if start > end:\n        # Range wrapping the end of the sequence\n        l1 = FeatureLocation(start, len(record), strand=strand)\n        l2 = FeatureLocation(0, end, strand=strand)\n        location = l1 + l2\n    else:\n        location = FeatureLocation(start, end, strand=strand)\n    return location\n\n\ndef _parse_features_packet(length, data, record):\n    \"\"\"Parse a sequence features packet.\n\n    This packet stores sequence features (except primer binding sites,\n    which are in a dedicated Primers packet). The data is a XML string\n    starting with a 'Features' root node.\n    \"\"\"\n    xml = parseString(data.decode(\"UTF-8\"))\n    for feature in xml.getElementsByTagName(\"Feature\"):\n        quals = {}\n\n        type = _get_attribute_value(feature, \"type\", default=\"misc_feature\")\n\n        strand = +1\n        directionality = int(\n            _get_attribute_value(feature, \"directionality\", default=\"1\")\n        )\n        if directionality == 2:\n            strand = -1\n\n        location = None\n        for segment in feature.getElementsByTagName(\"Segment\"):\n            rng = _get_attribute_value(segment, \"range\")\n            if not location:\n                location = _parse_location(rng, strand, record)\n            else:\n                location = location + _parse_location(rng, strand, record)\n        if not location:\n            raise ValueError(\"Missing feature location\")\n\n        for qualifier in feature.getElementsByTagName(\"Q\"):\n            qname = _get_attribute_value(\n                qualifier, \"name\", error=\"Missing qualifier name\"\n            )\n            qvalues = []\n            for value in qualifier.getElementsByTagName(\"V\"):\n                if value.hasAttribute(\"text\"):\n                    qvalues.append(_decode(value.attributes[\"text\"].value))\n                elif value.hasAttribute(\"predef\"):\n                    qvalues.append(_decode(value.attributes[\"predef\"].value))\n                elif value.hasAttribute(\"int\"):\n                    qvalues.append(int(value.attributes[\"int\"].value))\n            quals[qname] = qvalues\n\n        name = _get_attribute_value(feature, \"name\")\n        if name:\n            if \"label\" not in quals:\n                # No explicit label attribute, use the SnapGene name\n                quals[\"label\"] = [name]\n            elif name not in quals[\"label\"]:\n                # The SnapGene name is different from the label,\n                # add a specific attribute to represent it\n                quals[\"name\"] = [name]\n\n        feature = SeqFeature(location, type=type, qualifiers=quals)\n        record.features.append(feature)\n\n\ndef _parse_primers_packet(length, data, record):\n    \"\"\"Parse a Primers packet.\n\n    A Primers packet is similar to a Features packet but specifically\n    stores primer binding features. The data is a XML string starting\n    with a 'Primers' root node.\n    \"\"\"\n    xml = parseString(data.decode(\"UTF-8\"))\n    for primer in xml.getElementsByTagName(\"Primer\"):\n        quals = {}\n\n        name = _get_attribute_value(primer, \"name\")\n        if name:\n            quals[\"label\"] = [name]\n\n        for site in primer.getElementsByTagName(\"BindingSite\"):\n            rng = _get_attribute_value(\n                site, \"location\", error=\"Missing binding site location\"\n            )\n            strand = int(_get_attribute_value(site, \"boundStrand\", default=\"0\"))\n            if strand == 1:\n                strand = -1\n            else:\n                strand = +1\n\n            feature = SeqFeature(\n                _parse_location(rng, strand, record),\n                type=\"primer_bind\",\n                qualifiers=quals,\n            )\n            record.features.append(feature)\n\n\n_packet_handlers = {\n    0x00: _parse_dna_packet,\n    0x05: _parse_primers_packet,\n    0x06: _parse_notes_packet,\n    0x0A: _parse_features_packet,\n}\n\n\n# Helper functions to process the XML data in\n# some of the segments\n\n\ndef _decode(text):\n    # Get rid of HTML tags in some values\n    return sub(\"<[^>]+>\", \"\", text)\n\n\ndef _get_attribute_value(node, name, default=None, error=None):\n    if node.hasAttribute(name):\n        return _decode(node.attributes[name].value)\n    elif error:\n        raise ValueError(error)\n    else:\n        return default\n\n\ndef _get_child_value(node, name, default=None, error=None):\n    children = node.getElementsByTagName(name)\n    if (\n        children\n        and children[0].childNodes\n        and children[0].firstChild.nodeType == node.TEXT_NODE\n    ):\n        return _decode(children[0].firstChild.data)\n    elif error:\n        raise ValueError(error)\n    else:\n        return default\n\n\nclass SnapGeneIterator(SequenceIterator):\n    \"\"\"Parser for SnapGene files.\"\"\"\n\n    def __init__(self, source):\n        \"\"\"Parse a SnapGene file and return a SeqRecord object.\n\n        Argument source is a file-like object or a path to a file.\n\n        Note that a SnapGene file can only contain one sequence, so this\n        iterator will always return a single record.\n        \"\"\"\n        super().__init__(source, mode=\"b\", fmt=\"SnapGene\")\n\n    def parse(self, handle):\n        \"\"\"Start parsing the file, and return a SeqRecord generator.\"\"\"\n        records = self.iterate(handle)\n        return records\n\n    def iterate(self, handle):\n        \"\"\"Iterate over the records in the SnapGene file.\"\"\"\n        record = SeqRecord(None)\n        packets = _iterate(handle)\n        try:\n            packet_type, length, data = next(packets)\n        except StopIteration:\n            raise ValueError(\"Empty file.\") from None\n\n        if packet_type != 0x09:\n            raise ValueError(\"The file does not start with a SnapGene cookie packet\")\n        _parse_cookie_packet(length, data, record)\n\n        for (packet_type, length, data) in packets:\n            handler = _packet_handlers.get(packet_type)\n            if handler is not None:\n                handler(length, data, record)\n\n        if not record.seq:\n            raise ValueError(\"No DNA packet in file\")\n\n        yield record\n", "path": "Bio/SeqIO/SnapGeneIO.py"}]}