Repository: obspy/obspy

Issue:
Inventory __add__ mixes deep and shallow copies
<!--

**GitHub should mainly be used for bug reports and code developments/discussions. For generic questions, please use either the [[obspy-users] mailing list](http://lists.swapbytes.de/mailman/listinfo/obspy-users) (for scientific questions to the general ObsPy community) or our [gitter chat](https://gitter.im/obspy/obspy) (for technical questions and direct contact to the developers).**

Before submitting an Issue, please review the [Issue Guidelines](https://github.com/obspy/obspy/blob/master/CONTRIBUTING.md#submitting-an-issue).

-->

Adding two inventories using the `__add__` operator leads to an inventory consting of a deep copy of the first inventory, but a shallow copy of the second inventory. For example `inv1 + inv2` returns a deep copy of `inv1`, but a shallow copy of `inv2`. This also implies that runtime of inventory addition can change substantially by swapping the summands, if the two inventories differ strongly in size. The function in question is implemented here: https://github.com/obspy/obspy/blob/95b3adbe138bbb9460d3d5dc779660df35daadbb/obspy/core/inventory/inventory.py#L168-L184

I'd suggest either using deep or shallow copies of both inventories. Personally I'd prefer shallow copies for performance reasons and because they leave the option of making a deep copy to the user. I could write a PR, if this should be changed.
Inventory __add__ mixes deep and shallow copies
<!--

**GitHub should mainly be used for bug reports and code developments/discussions. For generic questions, please use either the [[obspy-users] mailing list](http://lists.swapbytes.de/mailman/listinfo/obspy-users) (for scientific questions to the general ObsPy community) or our [gitter chat](https://gitter.im/obspy/obspy) (for technical questions and direct contact to the developers).**

Before submitting an Issue, please review the [Issue Guidelines](https://github.com/obspy/obspy/blob/master/CONTRIBUTING.md#submitting-an-issue).

-->

Adding two inventories using the `__add__` operator leads to an inventory consting of a deep copy of the first inventory, but a shallow copy of the second inventory. For example `inv1 + inv2` returns a deep copy of `inv1`, but a shallow copy of `inv2`. This also implies that runtime of inventory addition can change substantially by swapping the summands, if the two inventories differ strongly in size. The function in question is implemented here: https://github.com/obspy/obspy/blob/95b3adbe138bbb9460d3d5dc779660df35daadbb/obspy/core/inventory/inventory.py#L168-L184

I'd suggest either using deep or shallow copies of both inventories. Personally I'd prefer shallow copies for performance reasons and because they leave the option of making a deep copy to the user. I could write a PR, if this should be changed.


Discussion:
Good catch, I agree this should be changed. `__add__` should make a deepcopy of both items. `__iadd__` should add a deppcopy of `other`, imo.

CC @krischer 
For merging two inventories without a deepcopy, `Inventory.extend` can be used. Although this does not call `__copy_inventory_metadata` when other is an inventory. Maybe this could be added? By the way, the docs of extend still talk about Catalog.
Good catch, I agree this should be changed. `__add__` should make a deepcopy of both items. `__iadd__` should add a deppcopy of `other`, imo.

CC @krischer 
For merging two inventories without a deepcopy, `Inventory.extend` can be used. Although this does not call `__copy_inventory_metadata` when other is an inventory. Maybe this could be added? By the way, the docs of extend still talk about Catalog.

Resolving patch:
diff --git a/obspy/core/inventory/inventory.py b/obspy/core/inventory/inventory.py
--- a/obspy/core/inventory/inventory.py
+++ b/obspy/core/inventory/inventory.py
@@ -172,7 +172,7 @@ def __ne__(self, other):
         return not self.__eq__(other)
 
     def __add__(self, other):
-        new = copy.deepcopy(self)
+        new = copy.copy(self)
         new += other
         return new
 
@@ -302,17 +302,19 @@ def _repr_pretty_(self, p, cycle):
 
     def extend(self, network_list):
         """
-        Extends the current Catalog object with a list of Network objects.
+        Extends the current Inventory object with another Inventory or a list
+        of Network objects.
         """
         if isinstance(network_list, list):
             for _i in network_list:
-                # Make sure each item in the list is a event.
+                # Make sure each item in the list is a Network.
                 if not isinstance(_i, Network):
                     msg = 'Extend only accepts a list of Network objects.'
                     raise TypeError(msg)
             self.networks.extend(network_list)
         elif isinstance(network_list, Inventory):
             self.networks.extend(network_list.networks)
+            self.__copy_inventory_metadata(network_list)
         else:
             msg = 'Extend only supports a list of Network objects as argument.'
             raise TypeError(msg)


Test patch:
diff --git a/obspy/core/tests/test_inventory.py b/obspy/core/tests/test_inventory.py
--- a/obspy/core/tests/test_inventory.py
+++ b/obspy/core/tests/test_inventory.py
@@ -661,6 +661,49 @@ def test_copy(self):
         self.assertEqual(inv2[0][0][0].latitude, original_latitude + 1)
         self.assertNotEqual(inv[0][0][0].latitude, inv2[0][0][0].latitude)
 
+    def test_add(self):
+        """
+        Test shallow copies for inventory addition
+        """
+        inv1 = read_inventory()
+        inv2 = read_inventory()
+
+        # __add__ creates two shallow copies
+        inv_sum = inv1 + inv2
+        self.assertEqual({id(net) for net in inv_sum},
+                         {id(net) for net in inv1} | {id(net) for net in inv2})
+
+        # __iadd__ creates a shallow copy of other and keeps self
+        ids1 = {id(net) for net in inv1}
+        inv1 += inv2
+        self.assertEqual({id(net) for net in inv1},
+                         ids1 | {id(net) for net in inv2})
+
+        # __add__ with a network appends the network to a shallow copy of
+        # the inventory
+        net1 = Network('N1')
+        inv_sum = inv1 + net1
+        self.assertEqual({id(net) for net in inv_sum},
+                         {id(net) for net in inv1} | {id(net1)})
+
+        # __iadd__ with a network appends the network to the inventory
+        net1 = Network('N1')
+        ids1 = {id(net) for net in inv1}
+        inv1 += net1
+        self.assertEqual({id(net) for net in inv1}, ids1 | {id(net1)})
+
+    def test_extend_metadata(self):
+        """
+        Test that extend merges the metadata of the Inventories
+        """
+        inv1 = Inventory([], source='S1', sender='T1')
+        inv2 = Inventory([], source='S2', sender='T2')
+
+        inv1.extend(inv2)
+
+        self.assertEqual(inv1.source, 'S1,S2')
+        self.assertEqual(inv1.sender, 'T1,T2')
+
     def test_read_inventory_with_wildcard(self):
         """
         Tests the read_inventory() function with a filename wild card.