Repository: microsoft/Qcodes

Issue:
Failing instrument creation adds instrument to list of instruments 
### Steps to reproduce
1. Create an instrument that fails (e.g. timeout, invalid arguments etc) 
2. The name of the instrument gets added to the list of existing instruments 
3. This prevents creating an instrument with the same name that does not fail. 
4. This is a problem because you want to create an instrument with that name when you fixed e.g. your invalid input argument/address or driver bug. 
### Expected behaviour

The name of the instrument does not get added to the list of current instruments 
### Actual behaviour

The name of the instrument gets added to the list of instruments preventing any other instrument from being created with that name, even though no instrument with the name actually exists. 
### Workaround

As suggested by @alexcjohnson use  `Instrument.find_instrument(name).close()` to close the non-existing instrument. 



Discussion:
this was introduced with #323, correct? 

It would have existed prior to #323 as well, in the instance list that each subclass maintains for itself (to enable finding instruments to test) but since we don't have a lot of driver tests (yet) noone has been impacted by this aspect of it.

I think there's a relatively simple solution to this using a metaclass - the standard metaclass, `type`, is what actually calls `__init__`, so if we make our own that first uses `type` to create the object, then we record the instance from there, it will never get to the record call if `__init__` throws an error. I will try this out...


Resolving patch:
diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py
--- a/qcodes/instrument/base.py
+++ b/qcodes/instrument/base.py
@@ -10,10 +10,11 @@
 from qcodes.utils.validators import Anything
 from .parameter import StandardParameter
 from .function import Function
-from .remote import RemoteInstrument
+from .metaclass import InstrumentMetaclass
 
 
-class Instrument(Metadatable, DelegateAttributes, NestedAttrAccess):
+class Instrument(Metadatable, DelegateAttributes, NestedAttrAccess,
+                 metaclass=InstrumentMetaclass):
 
     """
     Base class for all QCodes instruments.
@@ -40,6 +41,9 @@ class Instrument(Metadatable, DelegateAttributes, NestedAttrAccess):
             process is actually a ``RemoteInstrument`` that proxies all method
             calls, ``Parameters``, and ``Functions`` to the server.
 
+            The metaclass ``InstrumentMetaclass`` handles making either the
+            requested class or its RemoteInstrument proxy.
+
         metadata (Optional[Dict]): additional static metadata to add to this
             instrument's JSON snapshot.
 
@@ -76,15 +80,6 @@ class Instrument(Metadatable, DelegateAttributes, NestedAttrAccess):
 
     _all_instruments = {}
 
-    def __new__(cls, *args, server_name=None, **kwargs):
-        """Figure out whether to create a base instrument or proxy."""
-        if server_name is None:
-            return super().__new__(cls)
-        else:
-            warnings.warn("Multiprocessing is in beta, use at own risk", UserWarning)
-            return RemoteInstrument(*args, instrument_class=cls,
-                                    server_name=server_name, **kwargs)
-
     def __init__(self, name, server_name=None, **kwargs):
         self._t0 = time.time()
         super().__init__(**kwargs)
@@ -100,8 +95,6 @@ def __init__(self, name, server_name=None, **kwargs):
 
         self._no_proxy_methods = {'__getstate__'}
 
-        self.record_instance(self)
-
     def get_idn(self):
         """
         Parse a standard VISA '*IDN?' response into an ID dict.
diff --git a/qcodes/instrument/metaclass.py b/qcodes/instrument/metaclass.py
new file mode 100644
--- /dev/null
+++ b/qcodes/instrument/metaclass.py
@@ -0,0 +1,45 @@
+"""Metaclass to choose between Instrument and RemoteInstrument"""
+
+import warnings
+
+from .remote import RemoteInstrument
+
+
+class InstrumentMetaclass(type):
+    def __call__(cls, *args, server_name=None, **kwargs):
+        """
+        Create either a real Instrument or a RemoteInstrument as requested.
+
+        This (metaclass.__call__) is what is actually executed when you
+        instantiate an instrument, and returns the fully initialized object
+        (unlike class.__new__ which returns before __init__) so we can use this
+        to determine if the object was successfully created and only then
+        record its instance.
+
+        Args:
+            cls (type): the specific instrument class you invoked
+
+            *args (List[Any]): positional args to the instrument constructor
+
+            server_name (Optional[Union[str, None]]): if ``None`` we construct
+                a local instrument (with the class you requested). If a string,
+                we construct this instrument on a server with that name, or the
+                default from the instrument's classmethod
+                ``default_server_name`` if a blank string is used)
+
+            **kwargs (Dict[Any]): the kwargs to the instrument constructor,
+                after omitting server_name
+        """
+        if server_name is None:
+            instrument = super().__call__(*args, **kwargs)
+        else:
+            warnings.warn('Multiprocessing is in beta, use at own risk',
+                          UserWarning)
+            instrument = RemoteInstrument(*args, instrument_class=cls,
+                                          server_name=server_name, **kwargs)
+
+        # for RemoteInstrument, we want to record this instance with the
+        # class that it proxies, not with RemoteInstrument itself
+        cls.record_instance(instrument)
+
+        return instrument
diff --git a/qcodes/instrument/remote.py b/qcodes/instrument/remote.py
--- a/qcodes/instrument/remote.py
+++ b/qcodes/instrument/remote.py
@@ -63,9 +63,6 @@ def __init__(self, *args, instrument_class=None, server_name='',
 
         self.connect()
 
-        # must come after connect() because that sets self.name
-        instrument_class.record_instance(self)
-
     def connect(self):
         """Create the instrument on the server and replicate its API here."""
 


Test patch:
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
@@ -197,6 +197,25 @@ def test_remove_instance(self):
         self.assertEqual(self.gates.instances(), [self.gates])
         self.assertEqual(Instrument.find_instrument('gates'), self.gates)
 
+    def test_creation_failure(self):
+        # this we already know should fail (see test_max_delay_errors)
+        name = 'gatesFailing'
+        with self.assertRaises(ValueError):
+            GatesBadDelayValue(model=self.model, name=name, server_name='')
+
+        # this instrument should not be in the instance list
+        with self.assertRaises(KeyError):
+            Instrument.find_instrument(name)
+
+        # now do the same with a local instrument
+        name = 'gatesFailing2'
+        with self.assertRaises(ValueError):
+            GatesBadDelayValue(model=self.model, name=name, server_name=None)
+
+        # this instrument should not be in the instance list
+        with self.assertRaises(KeyError):
+            Instrument.find_instrument(name)
+
     def test_mock_instrument(self):
         gates, source, meter = self.gates, self.source, self.meter