Repository: pymeasure/pymeasure

Issue:
Instrument.setting does not have set_process
`Instrument.control` has `get_process` and `set_process`. `Instrument.measurement` has `get_process`. Why does `Instrument.setting` not have `set_process`? 

I think it would be useful, to e.g. cast truthy/falsy values to `1`or `0`, respectively. I know that I can (mis)use `validator` by passing an appropriate lambda, but it feels kludgy.
Instrument.setting does not have set_process
`Instrument.control` has `get_process` and `set_process`. `Instrument.measurement` has `get_process`. Why does `Instrument.setting` not have `set_process`? 

I think it would be useful, to e.g. cast truthy/falsy values to `1`or `0`, respectively. I know that I can (mis)use `validator` by passing an appropriate lambda, but it feels kludgy.


Discussion:
@bilderbuchi, the idea with these different function generators is that they each serve a specific case.

`Instrument.control` - a value that is set and read
`Instrument.measurement` - only read
`Instrument.setting` - only set

Does that clarify the difference?

Can you elaborate on the casting of boolean values? Where in the code are you suggesting?

I know the difference, but found the absence of `set_process` in the function generator that *sets* values inconsistent. To expand your list:

`Instrument.control` - a value that is set and read -> has `set_process` and `get_process`
`Instrument.measurement` - only read -> has `get_process`
`Instrument.setting` - only set -> does NOT have `set_process` - why?

My example: I have a power supply whose output is switched on/off by sending `OUT 1` and `OUT 0` (don't know exact command name right now). there is no output status query capability, so `Instrument.setting` is the appropriate function generator to use. Now, since this is in effect a boolean switch, and to keep a pythonic interface, I would like it to be able to supply "truthy" and "falsy" values, i.e. things that evaluate to `True` and `False`, respectively, to the property. All truthy values should become `1`, and all falsy values `0` in the command sent. 

Unfortunately, if I supply, e.g. `2` (which evaluates to boolean True) to my function, this gets turned into `OUT 2` (as it just passes the integer along), which is invalid. Therefore, I wanted to use `set_process` of `Instrument.setting`, which I expected to exist, to turn the supplied value into one that is valid for the device. If `set_process` existed, this would be, to my humble understanding from the other function generators and from the examples, the place to do such adaptation.

As a workaround, I have (mis)used `validator` and a lambda which basically casts all supplied values to bool, then to int - `lambda x, y: int(bool(x))` off the top of my hat (but don't have the code with me right now).

@bilderbuchi, thanks for the further explanation. I agree with your suggestion to add `set_process` to the arguments of `Instrument.setting`. Its absence was an oversight when I introduced the `get_process` and `set_process` to the other 2 functions. Do you want to start a PR?
It will take a couple of weeks until I can start on this, but yeah, I'll try to get a PR together. :-)
@bilderbuchi, the idea with these different function generators is that they each serve a specific case.

`Instrument.control` - a value that is set and read
`Instrument.measurement` - only read
`Instrument.setting` - only set

Does that clarify the difference?

Can you elaborate on the casting of boolean values? Where in the code are you suggesting?

I know the difference, but found the absence of `set_process` in the function generator that *sets* values inconsistent. To expand your list:

`Instrument.control` - a value that is set and read -> has `set_process` and `get_process`
`Instrument.measurement` - only read -> has `get_process`
`Instrument.setting` - only set -> does NOT have `set_process` - why?

My example: I have a power supply whose output is switched on/off by sending `OUT 1` and `OUT 0` (don't know exact command name right now). there is no output status query capability, so `Instrument.setting` is the appropriate function generator to use. Now, since this is in effect a boolean switch, and to keep a pythonic interface, I would like it to be able to supply "truthy" and "falsy" values, i.e. things that evaluate to `True` and `False`, respectively, to the property. All truthy values should become `1`, and all falsy values `0` in the command sent. 

Unfortunately, if I supply, e.g. `2` (which evaluates to boolean True) to my function, this gets turned into `OUT 2` (as it just passes the integer along), which is invalid. Therefore, I wanted to use `set_process` of `Instrument.setting`, which I expected to exist, to turn the supplied value into one that is valid for the device. If `set_process` existed, this would be, to my humble understanding from the other function generators and from the examples, the place to do such adaptation.

As a workaround, I have (mis)used `validator` and a lambda which basically casts all supplied values to bool, then to int - `lambda x, y: int(bool(x))` off the top of my hat (but don't have the code with me right now).

@bilderbuchi, thanks for the further explanation. I agree with your suggestion to add `set_process` to the arguments of `Instrument.setting`. Its absence was an oversight when I introduced the `get_process` and `set_process` to the other 2 functions. Do you want to start a PR?
It will take a couple of weeks until I can start on this, but yeah, I'll try to get a PR together. :-)

Resolving patch:
diff --git a/pymeasure/instruments/instrument.py b/pymeasure/instruments/instrument.py
--- a/pymeasure/instruments/instrument.py
+++ b/pymeasure/instruments/instrument.py
@@ -240,6 +240,7 @@ def fget(self):
     @staticmethod
     def setting(set_command, docs,
                 validator=lambda x, y: x, values=(), map_values=False,
+                set_process=lambda v: v,
                 check_set_errors=False,
                 **kwargs):
         """Returns a property for the class based on the supplied
@@ -254,7 +255,9 @@ def setting(set_command, docs,
                        as to map values if :code:`map_values` is True.
         :param map_values: A boolean flag that determines if the values should be 
                           interpreted as a map
-        :param check_set_errors: Toggles checking errors after setting     
+        :param set_process: A function that takes a value and allows processing
+                            before value mapping, returning the processed value
+        :param check_set_errors: Toggles checking errors after setting
         """
 
         if map_values and isinstance(values, dict):
@@ -265,7 +268,7 @@ def fget(self):
             raise LookupError("Instrument.setting properties can not be read.")
 
         def fset(self, value):
-            value = validator(value, values)
+            value = set_process(validator(value, values))
             if not map_values:
                 pass
             elif isinstance(values, (list, tuple, range)):


Test patch:
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
@@ -165,3 +165,17 @@ class Fake(FakeInstrument):
     assert fake.x == 'Y'
     fake.write('3')
     assert fake.x == 'Z'
+
+
+def test_setting_process():
+    class Fake(FakeInstrument):
+        x = Instrument.setting(
+            "OUT %d", "",
+            set_process=lambda v: int(bool(v)),
+        )
+
+    fake = Fake()
+    fake.x = False
+    assert fake.read() == 'OUT 0'
+    fake.x = 2
+    assert fake.read() == 'OUT 1'