Repository: dlce-eva/python-nexus

Issue:
Round tripping a nexus with a characters block leads to two character blocks
...which means the resulting nexus can't be read. Example [here](https://github.com/phlorest/walker_and_ribeiro2011/blob/main/cldf/data.nex). 

...which means:

```python
>>> from nexus import NexusReader
>>> NexusReader("walker_and_riberio2015/cldf/data.nex")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/simon/.pyenv/versions/3.9.6/lib/python3.9/site-packages/nexus/reader.py", line 42, in __init__
    self._set_blocks(NexusReader._blocks_from_file(filename))
  File "/Users/simon/.pyenv/versions/3.9.6/lib/python3.9/site-packages/nexus/reader.py", line 77, in _set_blocks
    raise NexusFormatException("Duplicate Block %s" % block)
nexus.exceptions.NexusFormatException: Duplicate Block characters
```

This is because of this:

https://github.com/dlce-eva/python-nexus/blob/071aaca665cc9bfde15201a31c836c6587d04483/src/nexus/reader.py#L80-L81

(which, I admit, I implemented as cheap way to always have a `data` block rather than looking for `data` or `characters`).

What's the best solution here @xrotwang ? 

1. remove these two lines so this change doesn't happen (simplest solution, but means we need to keep a closer eye in e.g. phlorest, and add standardisation there (nex.blocks['data'] = nex.blocks['characters'], del(nex.blocks['characters'])).
2. delete blocks['characters'] once it's been copied to blocks['data']. This keeps the status quo but adds a magic invisible change so seems like a bad idea.
3. fix the handlers to create a data and a characters block. Round-tripping will duplicate data and characters but they'll be named differently at least. 
4. revise `NexusWriter` to check for duplicate blocks, and perhaps allow writing of specific blocks only (nex.write(filename, blocks=['data'])


The fact that the moved block still thinks of itself as a characters block rather than a data block is a bug.


Comments:

Patch:
diff --git a/src/nexus/reader.py b/src/nexus/reader.py
index e42d8a8..8df69cd 100644
--- a/src/nexus/reader.py
+++ b/src/nexus/reader.py
@@ -161,10 +161,18 @@ class NexusReader(object):
         :return: String
         """
         out = ["#NEXUS\n"]
-        for block in self.blocks:
-            out.append(self.blocks[block].write())
+        blocks = []
+        for block in self.blocks.values():
+            if block in blocks:
+                # We skip copies of blocks - which might have happened to provide shortcuts for
+                # semantically identical but differently named blocks, such as "data" and
+                # "characters".
+                continue
+            blocks.append(block)
+        for block in blocks:
+            out.append(block.write())
             # empty line after block if needed
-            if len(self.blocks) > 1:
+            if len(blocks) > 1:
                 out.append("\n")
         return "\n".join(out)
 


Test patch:
diff --git a/tests/test_reader.py b/tests/test_reader.py
index 41e72e4..df9bd01 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -19,6 +19,11 @@ def test_reader_from_blocks():
     assert hasattr(n, 'custom')
 
 
+def test_roundtrip(examples):
+    nex = NexusReader(examples.joinpath('example2.nex'))
+    _ = NexusReader.from_string(nex.write())
+
+
 def test_read_file(nex, examples):
     """Test the Core functionality of NexusReader"""
     assert 'data' in nex.blocks