Repository: microsoft/Qcodes

Issue:
Parsing version string error with Keysight_34465A
When I try to connect a Keysight_34465A digital multimeter with 

```
from qcodes.instrument_drivers.Keysight.Keysight_34465A_submodules import Keysight_34465A
dmm = Keysight_34465A('dmm', 'TCPIP0::10.21.64.130::inst0::INSTR')
```

I get "InvalidVersion: Invalid version '65.02.17-02.40-02.17-00.52-04-01'", whereas the actual firmware version is 'A.02.17-02.40-02.17-00.52-04-01':

```
---------------------------------------------------------------------------
InvalidVersion                            Traceback (most recent call last)
Cell In[1], line 2
      1 from qcodes.instrument_drivers.Keysight.Keysight_34465A_submodules import Keysight_34465A
----> 2 dmm = Keysight_34465A('dmm', 'TCPIP0::10.21.64.130::inst0::INSTR')

File ~\.conda\envs\qcodes\lib\site-packages\qcodes\instrument\instrument_meta.py:36, in InstrumentMeta.__call__(cls, *args, **kwargs)
     31 def __call__(cls, *args: Any, **kwargs: Any) -> Any:
     32     """
     33     Overloads `type.__call__` to add code that runs only if __init__ completes
     34     successfully.
     35     """
---> 36     new_inst = super().__call__(*args, **kwargs)
     37     is_abstract = new_inst._is_abstract()
     38     if is_abstract:

File ~\.conda\envs\qcodes\lib\site-packages\qcodes\instrument_drivers\Keysight\Keysight_34465A_submodules.py:12, in Keysight34465A.__init__(self, name, address, silent, **kwargs)
     10 def __init__(self, name: str, address: str, silent: bool = False,
     11              **kwargs: Any):
---> 12     super().__init__(name, address, silent, **kwargs)

File ~\.conda\envs\qcodes\lib\site-packages\qcodes\instrument_drivers\Keysight\private\Keysight_344xxA_submodules.py:470, in _Keysight_344xxA.__init__(self, name, address, silent, **kwargs)
    463 ####################################
    464 # Instrument specifications
    466 options = self._options()
    467 self.has_DIG = self.is_34465A_34470A and (
    468     "DIG" in options
    469     or version.parse(convert_legacy_version_to_supported_version("A.03"))
--> 470     <= version.parse(
    471         convert_legacy_version_to_supported_version(idn["firmware"])
    472     )
    473 )
    474 # Note that the firmware version check is still needed because
    475 # ``_options`` (the ``*OPT?`` command) returns 'DIG' option for
    476 # firmware 3.0 only if it has been purchased before
    477 self.has_MEM = self.is_34465A_34470A and 'MEM' in options

File ~\.conda\envs\qcodes\lib\site-packages\packaging\version.py:52, in parse(version)
     43 def parse(version: str) -> "Version":
     44     """Parse the given version string.
     45 
     46     >>> parse('1.0.dev1')
   (...)
     50     :raises InvalidVersion: When the version string is not a valid version.
     51     """
---> 52     return Version(version)

File ~\.conda\envs\qcodes\lib\site-packages\packaging\version.py:197, in Version.__init__(self, version)
    195 match = self._regex.search(version)
    196 if not match:
--> 197     raise InvalidVersion(f"Invalid version: '{version}'")
    199 # Store the parsed out pieces of the version
    200 self._version = _Version(
    201     epoch=int(match.group("epoch")) if match.group("epoch") else 0,
    202     release=tuple(int(i) for i in match.group("release").split(".")),
   (...)
    208     local=_parse_local_version(match.group("local")),
    209 )
```

OS Windows 11
qcodes 0.37.0
Pyvisa 1.13.0
pyserial 3.5
Parsing version string error with Keysight_34465A
When I try to connect a Keysight_34465A digital multimeter with 

```
from qcodes.instrument_drivers.Keysight.Keysight_34465A_submodules import Keysight_34465A
dmm = Keysight_34465A('dmm', 'TCPIP0::10.21.64.130::inst0::INSTR')
```

I get "InvalidVersion: Invalid version '65.02.17-02.40-02.17-00.52-04-01'", whereas the actual firmware version is 'A.02.17-02.40-02.17-00.52-04-01':

```
---------------------------------------------------------------------------
InvalidVersion                            Traceback (most recent call last)
Cell In[1], line 2
      1 from qcodes.instrument_drivers.Keysight.Keysight_34465A_submodules import Keysight_34465A
----> 2 dmm = Keysight_34465A('dmm', 'TCPIP0::10.21.64.130::inst0::INSTR')

File ~\.conda\envs\qcodes\lib\site-packages\qcodes\instrument\instrument_meta.py:36, in InstrumentMeta.__call__(cls, *args, **kwargs)
     31 def __call__(cls, *args: Any, **kwargs: Any) -> Any:
     32     """
     33     Overloads `type.__call__` to add code that runs only if __init__ completes
     34     successfully.
     35     """
---> 36     new_inst = super().__call__(*args, **kwargs)
     37     is_abstract = new_inst._is_abstract()
     38     if is_abstract:

File ~\.conda\envs\qcodes\lib\site-packages\qcodes\instrument_drivers\Keysight\Keysight_34465A_submodules.py:12, in Keysight34465A.__init__(self, name, address, silent, **kwargs)
     10 def __init__(self, name: str, address: str, silent: bool = False,
     11              **kwargs: Any):
---> 12     super().__init__(name, address, silent, **kwargs)

File ~\.conda\envs\qcodes\lib\site-packages\qcodes\instrument_drivers\Keysight\private\Keysight_344xxA_submodules.py:470, in _Keysight_344xxA.__init__(self, name, address, silent, **kwargs)
    463 ####################################
    464 # Instrument specifications
    466 options = self._options()
    467 self.has_DIG = self.is_34465A_34470A and (
    468     "DIG" in options
    469     or version.parse(convert_legacy_version_to_supported_version("A.03"))
--> 470     <= version.parse(
    471         convert_legacy_version_to_supported_version(idn["firmware"])
    472     )
    473 )
    474 # Note that the firmware version check is still needed because
    475 # ``_options`` (the ``*OPT?`` command) returns 'DIG' option for
    476 # firmware 3.0 only if it has been purchased before
    477 self.has_MEM = self.is_34465A_34470A and 'MEM' in options

File ~\.conda\envs\qcodes\lib\site-packages\packaging\version.py:52, in parse(version)
     43 def parse(version: str) -> "Version":
     44     """Parse the given version string.
     45 
     46     >>> parse('1.0.dev1')
   (...)
     50     :raises InvalidVersion: When the version string is not a valid version.
     51     """
---> 52     return Version(version)

File ~\.conda\envs\qcodes\lib\site-packages\packaging\version.py:197, in Version.__init__(self, version)
    195 match = self._regex.search(version)
    196 if not match:
--> 197     raise InvalidVersion(f"Invalid version: '{version}'")
    199 # Store the parsed out pieces of the version
    200 self._version = _Version(
    201     epoch=int(match.group("epoch")) if match.group("epoch") else 0,
    202     release=tuple(int(i) for i in match.group("release").split(".")),
   (...)
    208     local=_parse_local_version(match.group("local")),
    209 )
```

OS Windows 11
qcodes 0.37.0
Pyvisa 1.13.0
pyserial 3.5


Discussion:
@MaJaISTA Thanks for the report. This is due to the version parsing code in packaging having become more strict. 

As a work around you should be able to downgrade to packaging 21.3

I should have a fix soon

Solved, thanks!
@MaJaISTA Thanks for the report. This is due to the version parsing code in packaging having become more strict. 

As a work around you should be able to downgrade to packaging 21.3

I should have a fix soon

Solved, thanks!

Resolving patch:
diff --git a/qcodes/utils/installation_info.py b/qcodes/utils/installation_info.py
--- a/qcodes/utils/installation_info.py
+++ b/qcodes/utils/installation_info.py
@@ -67,6 +67,11 @@ def convert_legacy_version_to_supported_version(ver: str) -> str:
     numbers to a regular version string. This is done by replacing a char
     by its ASCII code (using ``ord``). This assumes that the version number
     only uses at most a single char per level and only ASCII chars.
+
+    It also splits off anything that comes after the first ``-`` in the version str.
+
+    This is meant to pass versions like ``'A.02.17-02.40-02.17-00.52-04-01'``
+    primarily used by Keysight instruments.
     """
 
     temp_list = []
@@ -75,4 +80,5 @@ def convert_legacy_version_to_supported_version(ver: str) -> str:
             temp_list.append(str(ord(v.upper())))
         else:
             temp_list.append(v)
-    return "".join(temp_list)
+    temp_str = "".join(temp_list)
+    return temp_str.split("-")[0]


Test patch:
diff --git a/qcodes/tests/test_installation_info.py b/qcodes/tests/test_installation_info.py
--- a/qcodes/tests/test_installation_info.py
+++ b/qcodes/tests/test_installation_info.py
@@ -1,4 +1,5 @@
 import pytest
+from packaging import version
 
 import qcodes as qc
 import qcodes.utils.installation_info as ii
@@ -13,20 +14,20 @@
 # but we can at least test that they execute without errors
 
 
-def test_get_qcodes_version():
+def test_get_qcodes_version() -> None:
     with pytest.warns(QCoDeSDeprecationWarning):
         version = ii.get_qcodes_version()
 
     assert version == qc.__version__
 
 
-def test_is_qcodes_installed_editably():
+def test_is_qcodes_installed_editably() -> None:
     answer = is_qcodes_installed_editably()
 
     assert isinstance(answer, bool)
 
 
-def test_get_all_installed_package_versions():
+def test_get_all_installed_package_versions() -> None:
     ipvs = get_all_installed_package_versions()
 
     assert isinstance(ipvs, dict)
@@ -37,12 +38,29 @@ def test_get_all_installed_package_versions():
         assert isinstance(v, str)
 
 
-def test_convert_legacy_version_to_supported_version():
+def test_convert_legacy_version_to_supported_version() -> None:
+    def assert_version_str(legacy_verstr: str, expected_converted_ver_str: str) -> None:
+        converted_version_str = convert_legacy_version_to_supported_version(
+            legacy_verstr
+        )
+        assert converted_version_str == expected_converted_ver_str
+        assert version.parse(converted_version_str) == version.parse(
+            expected_converted_ver_str
+        )
+
     legacy_verstr = "a.1.4"
-    assert convert_legacy_version_to_supported_version(legacy_verstr) == "65.1.4"
+    expected_converted_ver_str = "65.1.4"
+    assert_version_str(legacy_verstr, expected_converted_ver_str)
+
 
     legacy_verstr = "10.4.7"
-    assert convert_legacy_version_to_supported_version(legacy_verstr) == "10.4.7"
+    expected_converted_ver_str = "10.4.7"
+    assert_version_str(legacy_verstr, expected_converted_ver_str)
 
     legacy_verstr = "C.2.1"
-    assert convert_legacy_version_to_supported_version(legacy_verstr) == "67.2.1"
+    expected_converted_ver_str = "67.2.1"
+    assert_version_str(legacy_verstr, expected_converted_ver_str)
+
+    legacy_verstr = "A.02.17-02.40-02.17-00.52-04-01"
+    expected_converted_ver_str = "65.02.17"
+    assert_version_str(legacy_verstr, expected_converted_ver_str)