We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Order of coordinates of a multi-part feature that maps to the reverse strand maybe 'wrong' in GenBank file generated from SnapGene file?
### Setup

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

```
3.7.10 | packaged by conda-forge | (default, Feb 19 2021, 16:07:37) 
[GCC 9.3.0]
CPython
Linux-5.4.72-microsoft-standard-WSL2-x86_64-with-debian-bullseye-sid
1.79
```

### Expected behaviour
Cloning programs/webapps such as SnapGene or Benchling should display all features included in a GenBank file that has been converted from a SnapGene file with BioPython.

### Actual behaviour
Cloning programs/webapps such as SnapGene or Benchling do not display all features included in a GenBank file that has been converted from a SnapGene file with BioPython. The problematic features are those that contain multiple elements on the reverse strand, for example `complement(join(3873..3941,3081..3872))`. If I reverse the order of those elements, that is, `complement(join(3081..3872,3873..3941))`, then everything's good, and the feature is shown in my fav cloning programs/webapp. I guess the elements of a feature that maps to the reverse/complement strand should nonetheless be listed in an 'increasing order'? 

Although it's most likely a coincidence, I also noticed that for the feature mentioned above, BioPython fails to include one of the notes that SnapGene instead includes in the exported file (the one at the very bottom of the left panel)

![Screenshot 2021-07-05 170536](https://user-images.githubusercontent.com/2800360/124495090-f428cb80-ddb7-11eb-957e-f8becf652083.png)

### Steps to reproduce

I am attaching the original SnapGene map and those converted to GenBank with SnapGene and BioPython to this message.

[test_plasmid.zip](https://github.com/biopython/biopython/files/6765312/test_plasmid.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 """Bio.SeqIO support for the SnapGene file format.
8 
9 The SnapGene binary format is the native format used by the SnapGene program
10 from GSL Biotech LLC.
11 """
12 from datetime import datetime
13 from re import sub
14 from struct import unpack
15 from xml.dom.minidom import parseString
16 
17 from Bio.Seq import Seq
18 from Bio.SeqFeature import FeatureLocation
19 from Bio.SeqFeature import SeqFeature
20 from Bio.SeqRecord import SeqRecord
21 
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 
139         strand = +1
140         directionality = int(
141             _get_attribute_value(feature, "directionality", default="1")
142         )
143         if directionality == 2:
144             strand = -1
145 
146         location = None
147         for segment in feature.getElementsByTagName("Segment"):
148             rng = _get_attribute_value(segment, "range")
149             if not location:
150                 location = _parse_location(rng, strand, record)
151             else:
152                 location = location + _parse_location(rng, strand, record)
153         if not location:
154             raise ValueError("Missing feature location")
155 
156         for qualifier in feature.getElementsByTagName("Q"):
157             qname = _get_attribute_value(
158                 qualifier, "name", error="Missing qualifier name"
159             )
160             qvalues = []
161             for value in qualifier.getElementsByTagName("V"):
162                 if value.hasAttribute("text"):
163                     qvalues.append(_decode(value.attributes["text"].value))
164                 elif value.hasAttribute("predef"):
165                     qvalues.append(_decode(value.attributes["predef"].value))
166                 elif value.hasAttribute("int"):
167                     qvalues.append(int(value.attributes["int"].value))
168             quals[qname] = qvalues
169 
170         name = _get_attribute_value(feature, "name")
171         if name:
172             if "label" not in quals:
173                 # No explicit label attribute, use the SnapGene name
174                 quals["label"] = [name]
175             elif name not in quals["label"]:
176                 # The SnapGene name is different from the label,
177                 # add a specific attribute to represent it
178                 quals["name"] = [name]
179 
180         feature = SeqFeature(location, type=type, qualifiers=quals)
181         record.features.append(feature)
182 
183 
184 def _parse_primers_packet(length, data, record):
185     """Parse a Primers packet.
186 
187     A Primers packet is similar to a Features packet but specifically
188     stores primer binding features. The data is a XML string starting
189     with a 'Primers' root node.
190     """
191     xml = parseString(data.decode("UTF-8"))
192     for primer in xml.getElementsByTagName("Primer"):
193         quals = {}
194 
195         name = _get_attribute_value(primer, "name")
196         if name:
197             quals["label"] = [name]
198 
199         for site in primer.getElementsByTagName("BindingSite"):
200             rng = _get_attribute_value(
201                 site, "location", error="Missing binding site location"
202             )
203             strand = int(_get_attribute_value(site, "boundStrand", default="0"))
204             if strand == 1:
205                 strand = -1
206             else:
207                 strand = +1
208 
209             feature = SeqFeature(
210                 _parse_location(rng, strand, record),
211                 type="primer_bind",
212                 qualifiers=quals,
213             )
214             record.features.append(feature)
215 
216 
217 _packet_handlers = {
218     0x00: _parse_dna_packet,
219     0x05: _parse_primers_packet,
220     0x06: _parse_notes_packet,
221     0x0A: _parse_features_packet,
222 }
223 
224 
225 # Helper functions to process the XML data in
226 # some of the segments
227 
228 
229 def _decode(text):
230     # Get rid of HTML tags in some values
231     return sub("<[^>]+>", "", text)
232 
233 
234 def _get_attribute_value(node, name, default=None, error=None):
235     if node.hasAttribute(name):
236         return _decode(node.attributes[name].value)
237     elif error:
238         raise ValueError(error)
239     else:
240         return default
241 
242 
243 def _get_child_value(node, name, default=None, error=None):
244     children = node.getElementsByTagName(name)
245     if (
246         children
247         and children[0].childNodes
248         and children[0].firstChild.nodeType == node.TEXT_NODE
249     ):
250         return _decode(children[0].firstChild.data)
251     elif error:
252         raise ValueError(error)
253     else:
254         return default
255 
256 
257 class SnapGeneIterator(SequenceIterator):
258     """Parser for SnapGene files."""
259 
260     def __init__(self, source):
261         """Parse a SnapGene file and return a SeqRecord object.
262 
263         Argument source is a file-like object or a path to a file.
264 
265         Note that a SnapGene file can only contain one sequence, so this
266         iterator will always return a single record.
267         """
268         super().__init__(source, mode="b", fmt="SnapGene")
269 
270     def parse(self, handle):
271         """Start parsing the file, and return a SeqRecord generator."""
272         records = self.iterate(handle)
273         return records
274 
275     def iterate(self, handle):
276         """Iterate over the records in the SnapGene file."""
277         record = SeqRecord(None)
278         packets = _iterate(handle)
279         try:
280             packet_type, length, data = next(packets)
281         except StopIteration:
282             raise ValueError("Empty file.") from None
283 
284         if packet_type != 0x09:
285             raise ValueError("The file does not start with a SnapGene cookie packet")
286         _parse_cookie_packet(length, data, record)
287 
288         for (packet_type, length, data) in packets:
289             handler = _packet_handlers.get(packet_type)
290             if handler is not None:
291                 handler(length, data, record)
292 
293         if not record.seq:
294             raise ValueError("No DNA packet in file")
295 
296         yield record
297
```
--- 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
@@ -144,12 +144,33 @@
             strand = -1
 
         location = None
+        subparts = []
+        n_parts = 0
         for segment in feature.getElementsByTagName("Segment"):
+            if _get_attribute_value(segment, "type", "standard") == "gap":
+                continue
             rng = _get_attribute_value(segment, "range")
+            n_parts += 1
+            next_location = _parse_location(rng, strand, record)
             if not location:
-                location = _parse_location(rng, strand, record)
+                location = next_location
+            elif strand == -1:
+                # Reverse segments order for reverse-strand features
+                location = next_location + location
             else:
-                location = location + _parse_location(rng, strand, record)
+                location = location + next_location
+
+            name = _get_attribute_value(segment, "name")
+            if name:
+                subparts.append([n_parts, name])
+
+        if len(subparts) > 0:
+            # Add a "parts" qualifiers to represent "named subfeatures"
+            if strand == -1:
+                # Reverse segment indexes and order for reverse-strand features
+                subparts = reversed([[n_parts - i + 1, name] for i, name in subparts])
+            quals["parts"] = [";".join("{}:{}".format(i, name) for i, name in subparts)]
+
         if not location:
             raise ValueError("Missing feature location")
 
@@ -221,7 +242,6 @@
     0x0A: _parse_features_packet,
 }
 
-
 # Helper functions to process the XML data in
 # some of the segments

{"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@@ -144,12 +144,33 @@\n             strand = -1\n \n         location = None\n+        subparts = []\n+        n_parts = 0\n         for segment in feature.getElementsByTagName(\"Segment\"):\n+            if _get_attribute_value(segment, \"type\", \"standard\") == \"gap\":\n+                continue\n             rng = _get_attribute_value(segment, \"range\")\n+            n_parts += 1\n+            next_location = _parse_location(rng, strand, record)\n             if not location:\n-                location = _parse_location(rng, strand, record)\n+                location = next_location\n+            elif strand == -1:\n+                # Reverse segments order for reverse-strand features\n+                location = next_location + location\n             else:\n-                location = location + _parse_location(rng, strand, record)\n+                location = location + next_location\n+\n+            name = _get_attribute_value(segment, \"name\")\n+            if name:\n+                subparts.append([n_parts, name])\n+\n+        if len(subparts) > 0:\n+            # Add a \"parts\" qualifiers to represent \"named subfeatures\"\n+            if strand == -1:\n+                # Reverse segment indexes and order for reverse-strand features\n+                subparts = reversed([[n_parts - i + 1, name] for i, name in subparts])\n+            quals[\"parts\"] = [\";\".join(\"{}:{}\".format(i, name) for i, name in subparts)]\n+\n         if not location:\n             raise ValueError(\"Missing feature location\")\n \n@@ -221,7 +242,6 @@\n     0x0A: _parse_features_packet,\n }\n \n-\n # Helper functions to process the XML data in\n # some of the segments\n", "issue": "Order of coordinates of a multi-part feature that maps to the reverse strand maybe 'wrong' in GenBank file generated from SnapGene file?\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\n3.7.10 | packaged by conda-forge | (default, Feb 19 2021, 16:07:37) \r\n[GCC 9.3.0]\r\nCPython\r\nLinux-5.4.72-microsoft-standard-WSL2-x86_64-with-debian-bullseye-sid\r\n1.79\r\n```\r\n\r\n### Expected behaviour\r\nCloning programs/webapps such as SnapGene or Benchling should display all features included in a GenBank file that has been converted from a SnapGene file with BioPython.\r\n\r\n### Actual behaviour\r\nCloning programs/webapps such as SnapGene or Benchling do not display all features included in a GenBank file that has been converted from a SnapGene file with BioPython. The problematic features are those that contain multiple elements on the reverse strand, for example `complement(join(3873..3941,3081..3872))`. If I reverse the order of those elements, that is, `complement(join(3081..3872,3873..3941))`, then everything's good, and the feature is shown in my fav cloning programs/webapp. I guess the elements of a feature that maps to the reverse/complement strand should nonetheless be listed in an 'increasing order'? \r\n\r\nAlthough it's most likely a coincidence, I also noticed that for the feature mentioned above, BioPython fails to include one of the notes that SnapGene instead includes in the exported file (the one at the very bottom of the left panel)\r\n\r\n![Screenshot 2021-07-05 170536](https://user-images.githubusercontent.com/2800360/124495090-f428cb80-ddb7-11eb-957e-f8becf652083.png)\r\n\r\n### Steps to reproduce\r\n\r\nI am attaching the original SnapGene map and those converted to GenBank with SnapGene and BioPython to this message.\r\n\r\n[test_plasmid.zip](https://github.com/biopython/biopython/files/6765312/test_plasmid.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\"\"\"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\"\"\"\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 FeatureLocation\nfrom Bio.SeqFeature import SeqFeature\nfrom Bio.SeqRecord import SeqRecord\n\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"}], "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\"\"\"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\"\"\"\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 FeatureLocation\nfrom Bio.SeqFeature import SeqFeature\nfrom Bio.SeqRecord import SeqRecord\n\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        subparts = []\n        n_parts = 0\n        for segment in feature.getElementsByTagName(\"Segment\"):\n            if _get_attribute_value(segment, \"type\", \"standard\") == \"gap\":\n                continue\n            rng = _get_attribute_value(segment, \"range\")\n            n_parts += 1\n            next_location = _parse_location(rng, strand, record)\n            if not location:\n                location = next_location\n            elif strand == -1:\n                # Reverse segments order for reverse-strand features\n                location = next_location + location\n            else:\n                location = location + next_location\n\n            name = _get_attribute_value(segment, \"name\")\n            if name:\n                subparts.append([n_parts, name])\n\n        if len(subparts) > 0:\n            # Add a \"parts\" qualifiers to represent \"named subfeatures\"\n            if strand == -1:\n                # Reverse segment indexes and order for reverse-strand features\n                subparts = reversed([[n_parts - i + 1, name] for i, name in subparts])\n            quals[\"parts\"] = [\";\".join(\"{}:{}\".format(i, name) for i, name in subparts)]\n\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# 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"}]}