Repository: pymeasure/pymeasure

Issue:
The `preprocess_reply` argument in `Instrument.__init__()` is no longer used in a `Instrument.values()` call
Previous to the PR https://github.com/pymeasure/pymeasure/pull/718 rework , `Instrument.values()`  would return `Adapter.values()`.  After the rework `Instrument.values()` is implemented in `CommonBase.values()` and no longer calls `Adapter.values()`.

Previously as part of an Instrument's initialization the argument `preprocess_reply` argument would be passed to the `Adapter` which in turn would be used as `self.preprocess_reply`  in the `Adapter.values()` function:
https://github.com/pymeasure/pymeasure/blob/8bf3c295fa2f977145ea42d633da8f16a7cc4151/pymeasure/adapters/adapter.py#L184

Now `CommonBase` doesn't ingest the `preprocess_reply` argument and so `CommonBase.values()` doesn't create or assign `self.preprocess_reply`:
https://github.com/pymeasure/pymeasure/blob/8bf3c295fa2f977145ea42d633da8f16a7cc4151/pymeasure/instruments/common_base.py#L312

That means when an Instrument driver passes along a `preprocess_reply` argument as part of Instrument initialization, that argument has no effect when `Instrument.values()` is called.

Two instrument drivers in the code base use that functionality:

https://github.com/pymeasure/pymeasure/blob/8bf3c295fa2f977145ea42d633da8f16a7cc4151/pymeasure/instruments/fluke/fluke7341.py#L67

https://github.com/pymeasure/pymeasure/blob/8bf3c295fa2f977145ea42d633da8f16a7cc4151/pymeasure/instruments/signalrecovery/dsp7265.py#L72





Discussion:
Thanks for raising the issue.
@bilderbuchi shall we add a (deprecated) global `preprocess_reply` to `CommonBase` for backward compatibility for instruments not in pymeasure?

Right now, if an external instrument hands `preprocess_reply=something` to the Instrument's constructor, a deprecation warning is raised in the VISAAdapter (if they do not provide their own one), but the Adapter's `values` method is never called, so this parameter is useless.

The best transition process would be to add the global preprocess to the `CommonBase` (with a deprecation warning) and then remove it again, whenever we cut those old ties.

Resolving patch:
diff --git a/pymeasure/instruments/common_base.py b/pymeasure/instruments/common_base.py
--- a/pymeasure/instruments/common_base.py
+++ b/pymeasure/instruments/common_base.py
@@ -97,6 +97,13 @@ class CommonBase:
 
     This class contains everything needed for pymeasure's property creator
     :meth:`control` and its derivatives :meth:`measurement` and :meth:`setting`.
+
+    :param preprocess_reply: An optional callable used to preprocess
+        strings received from the instrument. The callable returns the
+        processed string.
+
+        .. deprecated:: 0.11
+            Implement it in the instrument's `read` method instead.
     """
 
     # Variable holding the list of DynamicProperty parameters that are configurable
@@ -119,9 +126,14 @@ class CommonBase:
     # Prefix used to store reserved variables
     __reserved_prefix = "___"
 
-    def __init__(self, **kwargs):
+    def __init__(self, preprocess_reply=None, **kwargs):
         self._special_names = self._setup_special_names()
         self._create_channels()
+        if preprocess_reply is not None:
+            warn(("Parameter `preprocess_reply` is deprecated. "
+                 "Implement it in the instrument, e.g. in `read`, instead."),
+                 FutureWarning)
+        self.preprocess_reply = preprocess_reply
         super().__init__(**kwargs)
 
     class ChannelCreator:
@@ -311,6 +323,8 @@ def values(self, command, separator=',', cast=float, preprocess_reply=None, maxs
         results = self.ask(command, **kwargs).strip()
         if callable(preprocess_reply):
             results = preprocess_reply(results)
+        elif callable(self.preprocess_reply):
+            results = self.preprocess_reply(results)
         results = results.split(separator, maxsplit=maxsplit)
         for i, result in enumerate(results):
             try:
diff --git a/pymeasure/instruments/instrument.py b/pymeasure/instruments/instrument.py
--- a/pymeasure/instruments/instrument.py
+++ b/pymeasure/instruments/instrument.py
@@ -59,6 +59,12 @@ class Instrument(CommonBase):
     :param adapter: A string, integer, or :py:class:`~pymeasure.adapters.Adapter` subclass object
     :param string name: The name of the instrument. Often the model designation by default.
     :param includeSCPI: A boolean, which toggles the inclusion of standard SCPI commands
+    :param preprocess_reply: An optional callable used to preprocess
+        strings received from the instrument. The callable returns the
+        processed string.
+
+        .. deprecated:: 0.11
+            Implement it in the instrument's `read` method instead.
     :param \\**kwargs: In case ``adapter`` is a string or integer, additional arguments passed on
         to :py:class:`~pymeasure.adapters.VISAAdapter` (check there for details).
         Discarded otherwise.
@@ -66,6 +72,7 @@ class Instrument(CommonBase):
 
     # noinspection PyPep8Naming
     def __init__(self, adapter, name, includeSCPI=True,
+                 preprocess_reply=None,
                  **kwargs):
         # Setup communication before possible children require the adapter.
         if isinstance(adapter, (int, str)):
@@ -79,7 +86,7 @@ def __init__(self, adapter, name, includeSCPI=True,
         self.isShutdown = False
         self.name = name
 
-        super().__init__()
+        super().__init__(preprocess_reply=preprocess_reply)
 
         log.info("Initializing %s." % self.name)
 


Test patch:
diff --git a/tests/instruments/test_common_base.py b/tests/instruments/test_common_base.py
--- a/tests/instruments/test_common_base.py
+++ b/tests/instruments/test_common_base.py
@@ -35,7 +35,10 @@ class CommonBaseTesting(CommonBase):
     """Add read/write methods in order to use the ProtocolAdapter."""
 
     def __init__(self, parent, id=None, *args, **kwargs):
-        super().__init__()
+        print(args, kwargs)
+        if "test" in kwargs:
+            self.test = kwargs.pop("test")
+        super().__init__(*args, **kwargs)
         self.parent = parent
         self.id = id
         self.args = args
@@ -85,7 +88,7 @@ def generic():
 
 class FakeBase(CommonBaseTesting):
     def __init__(self, *args, **kwargs):
-        super().__init__(FakeAdapter())
+        super().__init__(FakeAdapter(), *args, **kwargs)
 
     fake_ctrl = CommonBase.control(
         "", "%d", "docs",
@@ -217,7 +220,7 @@ def test_correct_class(self, parent):
 
     def test_arguments(self, parent):
         assert parent.channels["A"].id == "A"
-        assert parent.channels["A"].kwargs == {'test': 5}
+        assert parent.channels["A"].test == 5
 
     def test_attribute_access(self, parent):
         assert parent.ch_B == parent.channels["B"]
@@ -336,6 +339,18 @@ def test_values(value, kwargs, result):
     assert cb.values(value, **kwargs) == result
 
 
+def test_global_preprocess_reply():
+    with pytest.warns(FutureWarning, match="deprecated"):
+        cb = CommonBaseTesting(FakeAdapter(), preprocess_reply=lambda v: v.strip("x"))
+        assert cb.values("x5x") == [5]
+
+
+def test_values_global_preprocess_reply():
+    cb = CommonBaseTesting(FakeAdapter())
+    cb.preprocess_reply = lambda v: v.strip("x")
+    assert cb.values("x5x") == [5]
+
+
 def test_binary_values(fake):
     fake.read_binary_values = fake.read
     assert fake.binary_values("123") == "123"
@@ -626,7 +641,7 @@ def test_measurement_cast(cast, expected):
     class Fake(CommonBaseTesting):
         x = CommonBase.measurement(
             "x", "doc", cast=cast)
-    with expected_protocol(Fake, [("x", "5.5")], name="test") as instr:
+    with expected_protocol(Fake, [("x", "5.5")]) as instr:
         assert instr.x == expected
 
 
diff --git a/tests/instruments/test_instrument.py b/tests/instruments/test_instrument.py
--- a/tests/instruments/test_instrument.py
+++ b/tests/instruments/test_instrument.py
@@ -116,6 +116,12 @@ def test_init_visa_fail():
         Instrument("abc", "def", visa_library="@xyz")
 
 
+def test_global_preprocess_reply():
+    with pytest.warns(FutureWarning, match="deprecated"):
+        inst = Instrument(FakeAdapter(), "name", preprocess_reply=lambda v: v.strip("x"))
+        assert inst.values("x5x") == [5]
+
+
 def test_instrument_in_context():
     with Instrument("abc", "def", visa_library="@sim") as instr:
         pass