Repository: biopython/biopython

Issue:
`Bio.PDB.PDBIO.set_structure()` alters parent of chains, residues and atoms
Calling the `set_structure` method of the `Bio.PDB.PDBIO` class alters the parent of (non-`Structure`) entities in a non-obvious way. For example, this script:
```python
import Bio.PDB
pdbio = Bio.PDB.PDBIO()
parser = Bio.PDB.MMCIFParser()
structure = parser.get_structure("12as", "12as.cif")
for chain in structure[0]:
    old_id = chain.id
    chain.id = " "
    pdbio.set_structure(chain)
    chain.id = old_id
```
Gives this error from line 91 of `Bio/PDB/Entity.py`:
```
ValueError: Cannot change id from `B` to ` `. The id ` ` is already used for a sibling of this entity.
```
Removing the call to `set_structure` acts as I would expect: temporarily renaming the chain name, and then reverting it without problems.

When `set_structure` is called with an `Entity` other than a `Structure` object, it constructs a `Structure` and populates it with the entity using the `add` method, which calls `set_parent` on the entity. To me, this isn't very intuitive: I don't expect writing an object to have side effects that act on that object.

I think a good solution to this would be to call `copy()` in `set_structure` before `add`ing the entity to the new `Structure` object. That is, change `sb.structure.add(pdb_object)` to `sb.structure.add(pdb_object.copy())` in `Bio.PDB.PDBIO.set_structure`. I'm happy to make a pull request if people agree that this is an issue.


Discussion:
Indeed. Copying is the better way. I added this 'feature' to allow writing individual bits of Structure object (chains, residues), did not foresee this indeed. Thanks!

If you want to go on with the pull request, I'd be happy to review it.

Resolving patch:
diff --git a/Bio/PDB/PDBIO.py b/Bio/PDB/PDBIO.py
--- a/Bio/PDB/PDBIO.py
+++ b/Bio/PDB/PDBIO.py
@@ -123,12 +123,12 @@ def set_structure(self, pdb_object):
             sb.init_seg(' ')
             # Build parts as necessary
             if pdb_object.level == "M":
-                sb.structure.add(pdb_object)
+                sb.structure.add(pdb_object.copy())
                 self.structure = sb.structure
             else:
                 sb.init_model(0)
                 if pdb_object.level == "C":
-                    sb.structure[0].add(pdb_object)
+                    sb.structure[0].add(pdb_object.copy())
                 else:
                     sb.init_chain('A')
                     if pdb_object.level == "R":
@@ -137,7 +137,7 @@ def set_structure(self, pdb_object):
                             sb.structure[0]['A'].id = parent_id
                         except Exception:
                             pass
-                        sb.structure[0]['A'].add(pdb_object)
+                        sb.structure[0]['A'].add(pdb_object.copy())
                     else:
                         # Atom
                         sb.init_residue('DUM', ' ', 1, ' ')
@@ -146,7 +146,7 @@ def set_structure(self, pdb_object):
                             sb.structure[0]['A'].id = parent_id
                         except Exception:
                             pass
-                        sb.structure[0]['A'].child_list[0].add(pdb_object)
+                        sb.structure[0]['A'].child_list[0].add(pdb_object.copy())
 
             # Return structure
             structure = sb.structure


Test patch:
diff --git a/Tests/test_PDB.py b/Tests/test_PDB.py
--- a/Tests/test_PDB.py
+++ b/Tests/test_PDB.py
@@ -874,8 +874,11 @@ def test_pdbio_write_structure(self):
         """Write a full structure using PDBIO."""
         io = PDBIO()
         struct1 = self.structure
+        # Ensure that set_structure doesn't alter parent
+        parent = struct1.parent
         # Write full model to temp file
         io.set_structure(struct1)
+        self.assertIs(parent, struct1.parent)
         filenumber, filename = tempfile.mkstemp()
         os.close(filenumber)
         try:
@@ -892,8 +895,11 @@ def test_pdbio_write_residue(self):
         io = PDBIO()
         struct1 = self.structure
         residue1 = list(struct1.get_residues())[0]
+        # Ensure that set_structure doesn't alter parent
+        parent = residue1.parent
         # Write full model to temp file
         io.set_structure(residue1)
+        self.assertIs(parent, residue1.parent)
         filenumber, filename = tempfile.mkstemp()
         os.close(filenumber)
         try:
@@ -912,8 +918,11 @@ def test_pdbio_write_custom_residue(self):
         atm = Atom.Atom('CA', [0.1, 0.1, 0.1], 1.0, 1.0, ' ', 'CA', 1, 'C')
         res.add(atm)
 
+        # Ensure that set_structure doesn't alter parent
+        parent = res.parent
         # Write full model to temp file
         io.set_structure(res)
+        self.assertIs(parent, res.parent)
         filenumber, filename = tempfile.mkstemp()
         os.close(filenumber)
         try:
@@ -939,8 +948,11 @@ def accept_atom(self, atom):
 
         io = PDBIO()
         struct1 = self.structure
+        # Ensure that set_structure doesn't alter parent
+        parent = struct1.parent
         # Write to temp file
         io.set_structure(struct1)
+        self.assertIs(parent, struct1.parent)
         filenumber, filename = tempfile.mkstemp()
         os.close(filenumber)
         try: