Repository: microsoft/Qcodes

Issue:
Check that instrument name is a valid identifier 
> The doNd functions break when the instrument name is not isidentifier()

See https://github.com/QCoDeS/Qcodes_contrib_drivers/pull/127 for more background. 

For naming parameters etc it makes sense to enforce that instrument names are always valid identifiers. e.g. check `isidentifier(name)` and raise a suitable error if not


Discussion:

Resolving patch:
diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py
--- a/qcodes/instrument/base.py
+++ b/qcodes/instrument/base.py
@@ -52,6 +52,7 @@ class InstrumentBase(Metadatable, DelegateAttributes):
     """
 
     def __init__(self, name: str, metadata: Optional[Mapping[Any, Any]] = None) -> None:
+        name = self._is_valid_identifier(name)
         self._short_name = str(name)
 
         self.parameters: Dict[str, _BaseParameter] = {}
@@ -407,6 +408,17 @@ def short_name(self) -> str:
         """Short name of the instrument"""
         return self._short_name
 
+    @staticmethod
+    def _is_valid_identifier(name: str) -> str:
+        """Check whether given name is a valid instrument identifier."""
+        new_name = name.replace("-", "_")
+        if name != new_name:
+            warnings.warn(f"Changed {name} to {new_name} for instrument identifier")
+        if not new_name.isidentifier():
+            raise ValueError(f"{new_name} invalid instrument identifier")
+
+        return new_name
+
     def _is_abstract(self) -> bool:
         """
         This method is run after the initialization of an instrument but


Test patch:
diff --git a/qcodes/tests/drivers/test_ami430.py b/qcodes/tests/drivers/test_ami430.py
--- a/qcodes/tests/drivers/test_ami430.py
+++ b/qcodes/tests/drivers/test_ami430.py
@@ -68,7 +68,7 @@ def _make_current_driver(magnet_axes_instances):
     """
     mag_x, mag_y, mag_z = magnet_axes_instances
 
-    driver = AMI430_3D("AMI430-3D", mag_x, mag_y, mag_z, field_limit)
+    driver = AMI430_3D("AMI430_3D", mag_x, mag_y, mag_z, field_limit)
 
     yield driver
 
@@ -124,8 +124,7 @@ def test_instantiation_from_names(magnet_axes_instances, request):
     mag_x, mag_y, mag_z = magnet_axes_instances
     request.addfinalizer(AMI430_3D.close_all)
 
-    driver = AMI430_3D("AMI430-3D", mag_x.name, mag_y.name, mag_z.name,
-                       field_limit)
+    driver = AMI430_3D("AMI430_3D", mag_x.name, mag_y.name, mag_z.name, field_limit)
 
     assert driver._instrument_x is mag_x
     assert driver._instrument_y is mag_y
@@ -145,9 +144,7 @@ def test_instantiation_from_name_of_nonexistent_ami_instrument(
             match=f"with name {non_existent_instrument} does not exist"
     ):
         AMI430_3D(
-            "AMI430-3D",
-            mag_x.name, non_existent_instrument, mag_z.name,
-            field_limit
+            "AMI430_3D", mag_x.name, non_existent_instrument, mag_z.name, field_limit
         )
 
 
@@ -168,7 +165,7 @@ def test_instantiation_from_name_of_existing_non_ami_instrument(
             )
     ):
         AMI430_3D(
-            "AMI430-3D",
+            "AMI430_3D",
             mag_x.name, non_ami_existing_instrument.name, mag_z.name,
             field_limit
         )
@@ -186,8 +183,10 @@ def test_instantiation_from_badly_typed_argument(
             ValueError, match="instrument_z argument is neither of those"
     ):
         AMI430_3D(
-            "AMI430-3D",
-            mag_x.name, mag_y, badly_typed_instrument_z_argument,
+            "AMI430_3D",
+            mag_x.name,
+            mag_y,
+            badly_typed_instrument_z_argument,
             field_limit
         )
 
@@ -1054,7 +1053,7 @@ def _parametrization_kwargs():
 @pytest.mark.parametrize('field_limit', **_parametrization_kwargs())
 def test_numeric_field_limit(magnet_axes_instances, field_limit, request):
     mag_x, mag_y, mag_z = magnet_axes_instances
-    ami = AMI430_3D("AMI430-3D", mag_x, mag_y, mag_z, field_limit)
+    ami = AMI430_3D("AMI430_3D", mag_x, mag_y, mag_z, field_limit)
     request.addfinalizer(ami.close)
 
     assert isinstance(ami._field_limit, float)
diff --git a/qcodes/tests/drivers/test_ami430_visa.py b/qcodes/tests/drivers/test_ami430_visa.py
--- a/qcodes/tests/drivers/test_ami430_visa.py
+++ b/qcodes/tests/drivers/test_ami430_visa.py
@@ -64,7 +64,7 @@ def _make_current_driver(magnet_axes_instances):
     """
     mag_x, mag_y, mag_z = magnet_axes_instances
 
-    driver = AMI430_3D("AMI430-3D", mag_x, mag_y, mag_z, field_limit)
+    driver = AMI430_3D("AMI430_3D", mag_x, mag_y, mag_z, field_limit)
 
     yield driver
 
@@ -119,7 +119,7 @@ def test_instantiation_from_names(magnet_axes_instances, request):
     mag_x, mag_y, mag_z = magnet_axes_instances
     request.addfinalizer(AMI430_3D.close_all)
 
-    driver = AMI430_3D("AMI430-3D", mag_x.name, mag_y.name, mag_z.name, field_limit)
+    driver = AMI430_3D("AMI430_3D", mag_x.name, mag_y.name, mag_z.name, field_limit)
 
     assert driver._instrument_x is mag_x
     assert driver._instrument_y is mag_y
@@ -138,7 +138,7 @@ def test_instantiation_from_name_of_nonexistent_ami_instrument(
         KeyError, match=f"with name {non_existent_instrument} does not exist"
     ):
         AMI430_3D(
-            "AMI430-3D", mag_x.name, non_existent_instrument, mag_z.name, field_limit
+            "AMI430_3D", mag_x.name, non_existent_instrument, mag_z.name, field_limit
         )
 
 
@@ -159,7 +159,7 @@ def test_instantiation_from_name_of_existing_non_ami_instrument(
         ),
     ):
         AMI430_3D(
-            "AMI430-3D",
+            "AMI430_3D",
             mag_x.name,
             non_ami_existing_instrument.name,
             mag_z.name,
@@ -175,7 +175,7 @@ def test_instantiation_from_badly_typed_argument(magnet_axes_instances, request)
 
     with pytest.raises(ValueError, match="instrument_z argument is neither of those"):
         AMI430_3D(
-            "AMI430-3D",
+            "AMI430_3D",
             mag_x.name,
             mag_y,
             badly_typed_instrument_z_argument,
@@ -1037,7 +1037,7 @@ def _parametrization_kwargs():
 @pytest.mark.parametrize("field_limit", **_parametrization_kwargs())
 def test_numeric_field_limit(magnet_axes_instances, field_limit, request):
     mag_x, mag_y, mag_z = magnet_axes_instances
-    ami = AMI430_3D("AMI430-3D", mag_x, mag_y, mag_z, field_limit)
+    ami = AMI430_3D("AMI430_3D", mag_x, mag_y, mag_z, field_limit)
     request.addfinalizer(ami.close)
 
     assert isinstance(ami._field_limit, float)
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
@@ -84,6 +84,31 @@ def test_instrument_fail(close_before_and_after):
     assert Instrument._all_instruments == {}
 
 
+def test_instrument_on_invalid_identifier(close_before_and_after):
+    # Check if warning and error raised when invalid identifer name given
+    with pytest.warns(
+        UserWarning, match="Changed !-name to !_name for instrument identifier"
+    ):
+        with pytest.raises(ValueError, match="!_name invalid instrument identifier"):
+            DummyInstrument(name="!-name")
+
+    assert Instrument.instances() == []
+    assert DummyInstrument.instances() == []
+    assert Instrument._all_instruments == {}
+
+    # Check if warning is raised and name is valid
+    # identifier when dashes '-' are converted to underscores '_'
+    with pytest.warns(
+        UserWarning, match="Changed -name to _name for instrument identifier"
+    ):
+        instr = DummyInstrument(name="-name")
+
+    assert instr.name == "_name"
+    assert Instrument.instances() == []
+    assert DummyInstrument.instances() == [instr]
+    assert Instrument._all_instruments != {}
+
+
 def test_instrument_retry_with_same_name(close_before_and_after):
     with pytest.raises(RuntimeError, match="Failed to create instrument"):
         DummyFailingInstrument(name="failinginstrument")
diff --git a/qcodes/tests/test_logger.py b/qcodes/tests/test_logger.py
--- a/qcodes/tests/test_logger.py
+++ b/qcodes/tests/test_logger.py
@@ -94,7 +94,7 @@ def AMI430_3D():
         lambda x, y, z: x == 0 and y == 0 and z < 3,
         lambda x, y, z: np.linalg.norm([x, y, z]) < 2
     ]
-    driver = AMI430_3D("AMI430-3D", mag_x, mag_y, mag_z, field_limit)
+    driver = AMI430_3D("AMI430_3D", mag_x, mag_y, mag_z, field_limit)
     try:
         yield driver, mag_x, mag_y, mag_z
     finally: