Repository: biopython/biopython

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)


Discussion:
@gouttegd are you able to take a look at this? Is the feature labelling in the file, or might SnapGene Server being doing some inference?
Sure, I’ll have a look at it.
The feature name is in the `example.dna` file: 
```
<Feature recentID="5" name="lac repressor encoded by lacI lac operator" type="protein_bind" allowSegmentOverlaps="0" consecutiveTranslationNumbering="1">
```
The problem is possibly here, where the parser reads the name of the _feature_ but assigns it as label to the _qualifiers_, where it is later overwritten by the _label_ qualifier:

https://github.com/biopython/biopython/blob/3df702f08c4325b363ee0906c14f55fd18cf08b3/Bio/SeqIO/SnapGeneIO.py#L134-L140

So, feature should have its own `name` attribute?
Using ``.qualifiers["name"]`` seems natural to me here.
I agree with @MarkusPiotrowski regarding the cause of the issue. But I am unsure about using `.qualifiers["name"]`, given that there is no *name* qualifier in the GenBank format (at least according to the list of valid qualifiers [here](http://www.insdc.org/files/feature_table.html#7.3.1)).

What is the project’s policy regarding qualifiers? Shall we stick to the list of standardized qualifiers, or are parsers allowed to add non-standard qualifiers as needed?
Adding a qualifier dictionary entry is less distruptive than adding a new attribute.

The Uniprot XML parser already uses ``.qualifiers["name"]`` as seen here:

https://github.com/biopython/biopython/blob/master/Bio/SeqIO/UniprotIO.py#L342
OK then, that’ll make the fix straightforward. I’ll submit the patch shortly.
@nicolazilio How often does this happen in your >4000 plasmid maps? When looking in your example file, usually the SnapGene "titel" of a feature is identical to the feature's label. In your example the "titel" is quite strange (a combination of it's label and it's bound_moiety) and the content of the title just seems to be wrong. Also, if SnapGene exports the file as Genbank format, this "title" is lost. 

> usually the SnapGene "titel" of a feature is identical to the feature's label

That has been the case in all SnapGene files I have manipulated (when features have a "label" qualifier at all). This is the first time I come across a file with a different value for the "name" attribute.

I suggest the following behavior to deal with that _name_ attribute:
* if the feature has no label, use that _name_ attribute as a label;
* if the feature has a label and that label is the same as the _name_ attribute, ignore it;
* otherwise, if the label and the _name_ attribute are different, store the _name_ into an extra _name_ qualifier.

The rationale is to avoid using a non-standard qualifier when possible, while still giving access to all the information stored in the SnapGene file. The downside is that caller code would then have to look first for a _name_ qualifier, and if that qualifier is absent then look for a _label_.

The other, simpler option would be to systematically add a _name_ qualifier independently of the presence of a label. But then in most cases the _name_ and _label_ qualifiers will contain the same string.

@nicolazilio What do you think?

@gouttegd I think the 3-step behaviour you suggest looks very good to me. Having name and label qualifiers that contain the same string would also work. I would let you guys decide what the best solution is. As long as I can get access to all the information in the SnapGene file, I am happy :).

In our collection of SnapGene plasmid maps, the title of a feature is also, in >99.9% of the cases, identical to its label, if the latter exists. However, probably due to 'historical' reasons, some are not. These may be plasmids that we bulk converted from DNASTAR format, but I am not sure.
Out of 2738 plasmids that I quickly checked, 239 contain 1 instance of these unusual features, 129 contain 2 instances, 37 contain 3 instances, and 5 contain 4 instances. Therefore, combined, ~10% of our plasmids contain these unusual features.

Resolving patch:
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 @@ def _parse_features_packet(length, data, record):
         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 @@ def _parse_features_packet(length, data, record):
                     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)
 


Test patch:
diff --git a/Tests/SnapGene/sample-d.dna b/Tests/SnapGene/sample-d.dna
Binary files a/Tests/SnapGene/sample-d.dna and b/Tests/SnapGene/sample-d.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
@@ -23,7 +23,7 @@ class TestSnapGene(unittest.TestCase):
             "description": "Sample Sequence D",
             "length": 1000,
             "topology": "linear",
-            "date": datetime.datetime(2019, 8, 3, 0, 0),
+            "date": datetime.datetime(2020, 9, 23, 0, 0),
             "features": [
                 {
                     "type": "misc_binding",
@@ -39,6 +39,21 @@ class TestSnapGene(unittest.TestCase):
                     "strand": 1,
                     "label": "FeatureA",
                 },
+                {
+                    "type": "misc_feature",
+                    "start": 700,
+                    "end": 720,
+                    "strand": 1,
+                    "label": "FeatureC",
+                },
+                {
+                    "type": "misc_feature",
+                    "start": 720,
+                    "end": 740,
+                    "strand": 1,
+                    "label": "SampleFeatureD",
+                    "name": "FeatureD",
+                },
             ],
         },
         "sample-e": {
@@ -162,6 +177,10 @@ def test_read(self):
                 self.assertEqual(exp_feat["end"], read_feat.location.end)
                 self.assertEqual(exp_feat["strand"], read_feat.location.strand)
                 self.assertEqual(exp_feat["label"], read_feat.qualifiers["label"][0])
+                if "name" in exp_feat:
+                    self.assertEqual(exp_feat["name"], read_feat.qualifiers["name"][0])
+                else:
+                    self.assertTrue("name" not in read_feat.qualifiers)
 
 
 class TestCorruptedSnapGene(unittest.TestCase):