Repository: microsoft/Qcodes

Issue:
Instrument.close()
The Instrument.close() removes the `.name` field from the `Instrument`. This gives problems when printing the instrument. The removal happens in `strip_attrs`. 

I am not sure why _all_ attributes are stripped, but maybe we could make an exception for the `.name` (and other variables) such that the `.__repr__`  and other basic functions still work.
### Steps to reproduce
1. Create an instrument
2. Call instrument.close()
3. Type print(instrument)
### Expected behaviour

The name of the instrument should be printed, perhaps in combination with a notifier that the instrument is closed
### Actual behaviour

An error is generated

@alexcjohnson @giulioungaretti 



Discussion:
@alexcjohnson Could you give me an idea why the stripping takes place and it is not sufficient to close the connection and remote the instance ( see https://github.com/qdev-dk/Qcodes/blob/6375f613bb0e80f5ec133c7b83ac7ebb006a7ab6/qcodes/instrument/base.py#L244)? 
I can make a PR to keep the name intact when stripping

I am not sure actually how necessary or effective it is, but the goal was to delete references to persistent objects or circular references so that the instrument and its associated resources could be garbage collected. We've had, and I believe continue to have, some difficulty with instruments not releasing their resources when they should.

Anyway I like the idea of whitelisting a few attributes and marking it as closed for repr, thanks!

I would remove it, if you are neither sure it's necessary nor effective: that is, unless there is a test where the difference it's obvious.

> I would remove it

I wouldn't want to remove it entirely without (manually) testing for example memory usage and resource availability as you create and recreate instruments, restart the notebook kernel... but you're right, it is going to be a little bit tricky to come up with a test case showing that we need this.


Resolving patch:
diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py
--- a/qcodes/instrument/base.py
+++ b/qcodes/instrument/base.py
@@ -199,7 +199,7 @@ def close(self):
         if hasattr(self, 'connection') and hasattr(self.connection, 'close'):
             self.connection.close()
 
-        strip_attrs(self)
+        strip_attrs(self, whitelist=['name'])
         self.remove_instance(self)
 
     @classmethod
@@ -350,10 +350,10 @@ def snapshot_base(self, update=False):
                 snap[attr] = getattr(self, attr)
         return snap
 
-    ##########################################################################
+    #
     # `write_raw` and `ask_raw` are the interface to hardware                #
     # `write` and `ask` are standard wrappers to help with error reporting   #
-    ##########################################################################
+    #
 
     def write(self, cmd):
         """
@@ -430,14 +430,14 @@ def ask_raw(self, cmd):
             'Instrument {} has not defined an ask method'.format(
                 type(self).__name__))
 
-    ##########################################################################
+    #
     # shortcuts to parameters & setters & getters                            #
-    #                                                                        #
-    #  instrument['someparam'] === instrument.parameters['someparam']        #
-    #  instrument.someparam === instrument.parameters['someparam']           #
-    #  instrument.get('someparam') === instrument['someparam'].get()         #
-    #  etc...                                                                #
-    ##########################################################################
+    #
+    # instrument['someparam'] === instrument.parameters['someparam']        #
+    # instrument.someparam === instrument.parameters['someparam']           #
+    # instrument.get('someparam') === instrument['someparam'].get()         #
+    # etc...                                                                #
+    #
 
     delegate_attr_dicts = ['parameters', 'functions']
 
@@ -483,9 +483,9 @@ def call(self, func_name, *args):
         """
         return self.functions[func_name].call(*args)
 
-    ##########################################################################
+    #
     # info about what's in this instrument, to help construct the remote     #
-    ##########################################################################
+    #
 
     def connection_attrs(self, new_id):
         """
diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py
--- a/qcodes/utils/helpers.py
+++ b/qcodes/utils/helpers.py
@@ -13,9 +13,11 @@
 
 
 class NumpyJSONEncoder(json.JSONEncoder):
+
     """Return numpy types as standard types."""
     # http://stackoverflow.com/questions/27050108/convert-numpy-type-to-python
     # http://stackoverflow.com/questions/9452775/converting-numpy-dtypes-to-native-python-types/11389998#11389998
+
     def default(self, obj):
         if isinstance(obj, np.integer):
             return int(obj)
@@ -28,6 +30,7 @@ def default(self, obj):
         else:
             return super(NumpyJSONEncoder, self).default(obj)
 
+
 def tprint(string, dt=1, tag='default'):
     """ Print progress of a loop every dt seconds """
     ptime = _tprint_times.get(tag, 0)
@@ -199,6 +202,7 @@ def wait_secs(finish_clock):
 
 
 class LogCapture():
+
     '''
     context manager to grab all log messages, optionally
     from a specific logger
@@ -209,6 +213,7 @@ class LogCapture():
         code_that_makes_logs(...)
     log_str = logs.value
     '''
+
     def __init__(self, logger=logging.getLogger()):
         self.logger = logger
 
@@ -242,6 +247,7 @@ def make_unique(s, existing):
 
 
 class DelegateAttributes:
+
     """
     Mixin class to create attributes of this object by
     delegating them to one or more dicts and/or objects
@@ -322,16 +328,18 @@ def __dir__(self):
         return sorted(set(names))
 
 
-def strip_attrs(obj):
+def strip_attrs(obj, whitelist=()):
     """
     Irreversibly remove all direct instance attributes of obj, to help with
     disposal, breaking circular references.
 
     Args:
         obj:  object to be stripped
+        whitelist (list): list of names that are not stripped from the object
     """
     try:
-        for key in list(obj.__dict__.keys()):
+        lst = set(list(obj.__dict__.keys())) - set(whitelist)
+        for key in lst:
             try:
                 del obj.__dict__[key]
             except:


Test patch:
diff --git a/qcodes/tests/instrument_mocks.py b/qcodes/tests/instrument_mocks.py
--- a/qcodes/tests/instrument_mocks.py
+++ b/qcodes/tests/instrument_mocks.py
@@ -1,10 +1,15 @@
 import time
+import logging
+from functools import partial
 
+from qcodes.instrument.base import Instrument
 from qcodes.instrument.mock import MockInstrument, MockModel
 from qcodes.utils.validators import Numbers
+from qcodes.instrument.parameter import ManualParameter
 
 
 class AMockModel(MockModel):
+
     def __init__(self):
         self._memory = {}
         self._reset()
@@ -71,6 +76,7 @@ def meter_get(self, parameter):
 
 
 class ParamNoDoc:
+
     def __init__(self, name, *args, **kwargs):
         self.name = name
 
@@ -79,6 +85,7 @@ def get_attrs(self):
 
 
 class MockInstTester(MockInstrument):
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.attach_adder()
@@ -107,6 +114,7 @@ def add5(self, b):
 
 
 class MockGates(MockInstTester):
+
     def __init__(self, model=None, **kwargs):
         super().__init__('gates', model=model, delay=0.001, **kwargs)
 
@@ -155,6 +163,7 @@ def slow_neg_set(self, val):
 
 
 class MockSource(MockInstTester):
+
     def __init__(self, model=None, **kwargs):
         super().__init__('source', model=model, delay=0.001, **kwargs)
 
@@ -165,9 +174,31 @@ def __init__(self, model=None, **kwargs):
 
 
 class MockMeter(MockInstTester):
+
     def __init__(self, model=None, **kwargs):
         super().__init__('meter', model=model, delay=0.001, **kwargs)
 
         self.add_parameter('amplitude', get_cmd='ampl?', get_parser=float)
         self.add_function('echo', call_cmd='echo {:.2f}?',
                           args=[Numbers(0, 1000)], return_parser=float)
+
+
+class DummyInstrument(Instrument):
+
+    def __init__(self, name='dummy', gates=['dac1', 'dac2', 'dac3'], **kwargs):
+        ''' Create a dummy instrument that can be used for testing
+        
+        Args:
+            name (string): name for the instrument
+            gates (list): list of names that is used to create parameters for
+                            the instrument
+        '''
+        super().__init__(name, **kwargs)
+
+        # make gates
+        for i, g in enumerate(gates):
+            self.add_parameter(g,
+                               parameter_class=ManualParameter,
+                               initial_value=0,
+                               label='Gate {} (arb. units)'.format(g),
+                               vals=Numbers(-800, 400))
diff --git a/qcodes/tests/test_instrument.py b/qcodes/tests/test_instrument.py
--- a/qcodes/tests/test_instrument.py
+++ b/qcodes/tests/test_instrument.py
@@ -16,11 +16,12 @@
 from qcodes.process.helpers import kill_processes
 
 from .instrument_mocks import (AMockModel, MockInstTester,
-                               MockGates, MockSource, MockMeter)
+                               MockGates, MockSource, MockMeter, DummyInstrument)
 from .common import strip_qc
 
 
 class TestParamConstructor(TestCase):
+
     def test_name_s(self):
         p = Parameter('simple')
         self.assertEqual(p.name, 'simple')
@@ -84,7 +85,7 @@ def test_repr(self):
         None,  # no instrument at all
         namedtuple('noname', '')(),  # no .name
         namedtuple('blank', 'name')('')  # blank .name
-        )
+    )
     named_instrument = namedtuple('yesname', 'name')('astro')
 
     def test_full_name(self):
@@ -126,6 +127,7 @@ def test_full_names(self):
 
 
 class GatesBadDelayType(MockGates):
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.add_parameter('chan0bad', get_cmd='c0?',
@@ -137,6 +139,7 @@ def __init__(self, *args, **kwargs):
 
 
 class GatesBadDelayValue(MockGates):
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.add_parameter('chan0bad', get_cmd='c0?',
@@ -148,6 +151,7 @@ def __init__(self, *args, **kwargs):
 
 
 class TestInstrument(TestCase):
+
     @classmethod
     def setUpClass(cls):
         cls.model = AMockModel()
@@ -933,6 +937,7 @@ def test_add_function(self):
 
 
 class TestLocalMock(TestCase):
+
     def setUp(self):
         self.model = AMockModel()
 
@@ -957,6 +962,7 @@ def test_local(self):
 
 
 class TestModelAttrAccess(TestCase):
+
     def setUp(self):
         self.model = AMockModel()
 
@@ -982,3 +988,30 @@ def test_attr_access(self):
         model.fmt = 'local override of a remote method'
         self.assertEqual(model.callattr('fmt', 42), '42.000')
         self.assertEqual(model.callattr('fmt', value=12.4), '12.400')
+
+
+class TestInstrument2(TestCase):
+
+    def setUp(self):
+        self.instrument = DummyInstrument(
+            name='testdummy', gates=['dac1', 'dac2', 'dac3'], server_name=None)
+
+    def tearDown(self):
+        pass
+
+    def test_attr_access(self):
+        instrument = self.instrument
+
+        # test the instrument works
+        instrument.dac1.set(10)
+        val = instrument.dac1.get()
+        self.assertEqual(val, 10)
+
+        # close the instrument
+        instrument.close()
+
+        # make sure we can still print the instrument
+        s = instrument.__repr__()
+
+        # make sure the gate is removed
+        self.assertEqual(hasattr(instrument, 'dac1'), False)