Repository: pymeasure/pymeasure

Issue:
hp8116a should have staticmethods
The methods `_generate_1_2_5_sequence` and `_boolean_control` should be static methods.

For that, they have to be used as `HP8116A._boolean_control`.


Discussion:

Resolving patch:
diff --git a/pymeasure/instruments/hp/hp8116a.py b/pymeasure/instruments/hp/hp8116a.py
--- a/pymeasure/instruments/hp/hp8116a.py
+++ b/pymeasure/instruments/hp/hp8116a.py
@@ -35,6 +35,31 @@
 log.addHandler(logging.NullHandler())
 
 
+# for Python>3.9, these two methods can be static methods in the class
+def _generate_1_2_5_sequence(min, max):
+    """ Generate a list of a 1-2-5 sequence between min and max. """
+    exp_min = int(np.log10(min))
+    exp_max = int(np.log10(max))
+
+    seq_1_2_5 = np.array([1, 2, 5])
+    sequence = np.array([seq_1_2_5 * (10 ** exp) for exp in range(exp_min - 1, exp_max + 1)])
+    sequence = sequence.flatten()
+    sequence = sequence[(sequence >= min) & (sequence <= max)]
+
+    return list(sequence)
+
+
+def _boolean_control(identifier, state_index, docs, inverted=False, **kwargs):
+    return Instrument.control(
+        'CST', identifier + '%d', docs,
+        validator=strict_discrete_set,
+        values=[True, False],
+        get_process=lambda x: inverted ^ bool(int(x[state_index][1])),
+        set_process=lambda x: int(inverted ^ x),
+        **kwargs
+    )
+
+
 class Status(IntFlag):
     """ IntFlag type for the GPIB status byte which is returned by the :py:attr:`status` property.
     When the timing_error or programming_error flag is set, a more detailed error description
@@ -197,28 +222,6 @@ def _parse_value_with_unit(value_str, units):
 
         return value
 
-    def _generate_1_2_5_sequence(min, max):
-        """ Generate a list of a 1-2-5 sequence between min and max. """
-        exp_min = int(np.log10(min))
-        exp_max = int(np.log10(max))
-
-        seq_1_2_5 = np.array([1, 2, 5])
-        sequence = np.array([seq_1_2_5 * (10 ** exp) for exp in range(exp_min - 1, exp_max + 1)])
-        sequence = sequence.flatten()
-        sequence = sequence[(sequence >= min) & (sequence <= max)]
-
-        return list(sequence)
-
-    def _boolean_control(identifier, state_index, docs, inverted=False, **kwargs):
-        return Instrument.control(
-            'CST', identifier + '%d', docs,
-            validator=strict_discrete_set,
-            values=[True, False],
-            get_process=lambda x: inverted ^ bool(int(x[state_index][1])),
-            set_process=lambda x: int(inverted ^ x),
-            **kwargs
-        )
-
     # Instrument communication #
 
     def write(self, command):
@@ -254,7 +257,7 @@ def ask(self, command, num_bytes=None):
 
     operating_mode = Instrument.control(
         'CST', '%s',
-        """ A string property that controls the operating mode of the instrument.
+        """Control the operating mode of the instrument.
         Possible values (without Option 001) are: 'normal', 'triggered', 'gate', 'external_width'.
         With Option 001, 'internal_sweep', 'external_sweep', 'external_width', 'external_pulse'
         are also available.
@@ -267,7 +270,7 @@ def ask(self, command, num_bytes=None):
 
     control_mode = Instrument.control(
         'CST', '%s',
-        """ A string property that controls the control mode of the instrument.
+        """Control the control mode of the instrument.
         Possible values are 'off', 'FM', 'AM', 'PWM', 'VCO'.
         """,
         validator=strict_discrete_set,
@@ -278,7 +281,7 @@ def ask(self, command, num_bytes=None):
 
     trigger_slope = Instrument.control(
         'CST', '%s',
-        """ A string property that controls the slope the trigger triggers on.
+        """Control the slope the trigger triggers on.
         Possible values are: 'off', 'positive', 'negative'.
         """,
         validator=strict_discrete_set,
@@ -289,7 +292,7 @@ def ask(self, command, num_bytes=None):
 
     shape = Instrument.control(
         'CST', '%s',
-        """ A string property that controls the shape of the output waveform.
+        """Control the shape of the output waveform.
         Possible values are: 'dc', 'sine', 'triangle', 'square', 'pulse'.
         """,
         validator=strict_discrete_set,
@@ -300,39 +303,36 @@ def ask(self, command, num_bytes=None):
 
     haversine_enabled = _boolean_control(
         'H', 4,
-        """ A boolean property that controls whether a haversine/havertriangle signal
+        """Control whether a haversine/havertriangle signal
         is generated when in 'triggered', 'internal_burst' or 'external_burst' operating mode.
         """,
     )
 
     autovernier_enabled = _boolean_control(
         'A', 5,
-        """ A boolean property that controls whether the autovernier is enabled. """,
+        """Control whether the autovernier is enabled (bool).""",
         check_set_errors=True
     )
 
     limit_enabled = _boolean_control(
         'L', 6,
-        """ A boolean property that controls whether parameter limiting is enabled. """,
+        """Control whether parameter limiting is enabled (bool).""",
     )
 
     complement_enabled = _boolean_control(
         'C', 7,
-        """ A boolean property that controls whether the complement
-        of the signal is generated.
-        """,
+        """Control whether the complement of the signal is generated (bool).""",
     )
 
     output_enabled = _boolean_control(
         'D', 8,
-        """ A boolean property that controls whether the output is enabled. """,
+        """Control whether the output is enabled (bool).""",
         inverted=True,  # The actual command is "Disable output"...
     )
 
     frequency = Instrument.control(
         'IFRQ', 'FRQ %s',
-        """ A floating point value that controls the frequency of the
-        output in Hz. The allowed frequency range is 1 mHz to 52.5 MHz.
+        """Control the frequency of the output in Hz (strict float from 1e-3 to 52.5e6).
         """,
         validator=strict_range,
         values=[1e-3, 52.5001e6],
@@ -342,7 +342,7 @@ def ask(self, command, num_bytes=None):
 
     duty_cycle = Instrument.control(
         'IDTY', 'DTY %s %%',
-        """ An integer value that controls the duty cycle of the output in percent.
+        """Control the duty cycle of the output in percent (float).
         The allowed range generally is 10 % to 90 %, but it also depends on the current frequency.
         It is valid for all shapes except 'pulse', where :py:attr:`pulse_width` is used instead.
         """,
@@ -354,8 +354,7 @@ def ask(self, command, num_bytes=None):
 
     pulse_width = Instrument.control(
         'IWID', 'WID %s',
-        """ A floating point value that controls the pulse width.
-        The allowed pulse width range is 8 ns to 999 ms.
+        """Control the pulse width in s (strict float from 8e-9 to 999e-3).
         The pulse width may not be larger than the period.
         """,
         validator=strict_range,
@@ -366,9 +365,8 @@ def ask(self, command, num_bytes=None):
 
     amplitude = Instrument.control(
         'IAMP', 'AMP %s',
-        """ A floating point value that controls the amplitude of the
-        output in V. The allowed amplitude range generally is 10 mV to 16 V,
-        but it is also limited by the current offset.
+        """Control the amplitude of the output in V (strict float from 10e-3 to 16).
+        The allowed amplitude range is also limited by the current offset.
         """,
         validator=strict_range,
         values=[10e-3, 16.001],
@@ -378,9 +376,8 @@ def ask(self, command, num_bytes=None):
 
     offset = Instrument.control(
         'IOFS', 'OFS %s',
-        """ A floating point value that controls the offset of the
-        output in V. The allowed offset range generally is -7.95 V to 7.95 V,
-        but it is also limited by the amplitude.
+        """Control the offset of the output in V (strit float from -7.95 to 7.95).
+        The allowed offset range is also limited by the amplitude.
         """,
         validator=strict_range,
         values=[-7.95, 7.95001],
@@ -390,9 +387,8 @@ def ask(self, command, num_bytes=None):
 
     high_level = Instrument.control(
         'IHIL', 'HIL %s',
-        """ A floating point value that controls the high level of the
-        output in V. The allowed high level range generally is -7.9 V to 8 V,
-        but it must be at least 10 mV greater than the low level.
+        """Control the high level of the output in V (strict float from -7.9 to 8).
+        The allowed high level range must be at least 10 mV greater than the low level.
         """,
         validator=strict_range,
         values=[-7.9, 8.001],
@@ -402,9 +398,8 @@ def ask(self, command, num_bytes=None):
 
     low_level = Instrument.control(
         'ILOL', 'LOL %s',
-        """ A floating point value that controls the low level of the
-        output in V. The allowed low level range generally is -8 V to 7.9 V,
-        but it must be at least 10 mV less than the high level.
+        """Control the low level of the output in V (strict float from -8 to 7.9).
+        The allowed low level range must be at least 10 mV less than the high level.
         """,
         validator=strict_range,
         values=[-8, 7.9001],
@@ -414,9 +409,8 @@ def ask(self, command, num_bytes=None):
 
     burst_number = Instrument.control(
         'IBUR', 'BUR %s #',
-        """ An integer value that controls the number of periods generated in a burst.
-        The allowed range is 1 to 1999. It is only valid for units with Option 001
-        in one of the burst modes.
+        """Control the number of periods generated in a burst (strict int from 1 to 1999).
+        It is only valid for units with Option 001 in one of the burst modes.
         """,
         validator=strict_range,
         values=[1, 1999],
@@ -425,8 +419,8 @@ def ask(self, command, num_bytes=None):
 
     repetition_rate = Instrument.control(
         'IRPT', 'RPT %s',
-        """ A floating point value that controls the repetition rate (= the time between bursts)
-        in 'internal_burst' mode. The allowed range is 20 ns to 999 ms.
+        """Control the repetition rate in s (i.e. the time between bursts)
+        in 'internal_burst' mode (strict float from 20e-9 to 999e-3).
         """,
         validator=strict_range,
         values=[20e-9, 999.001e-3],
@@ -436,8 +430,8 @@ def ask(self, command, num_bytes=None):
 
     sweep_start = Instrument.control(
         'ISTA', 'STA %s',
-        """ A floating point value that controls the start frequency in both sweep modes.
-        The allowed range is 1 mHz to 52.5 MHz.
+        """Control the start frequency in both sweep modes in Hz
+        (strict float from 1e-3 to 52.5e6).
         """,
         validator=strict_range,
         values=[1e-3, 52.5001e6],
@@ -447,8 +441,7 @@ def ask(self, command, num_bytes=None):
 
     sweep_stop = Instrument.control(
         'ISTP', 'STP %s',
-        """ A floating point value that controls the stop frequency in both sweep modes.
-        The allowed range is 1 mHz to 52.5 MHz.
+        """Control the stop frequency in both sweep modes in Hz (strict float from 1e-3 to 52.5e6).
         """,
         validator=strict_range,
         values=[1e-3, 52.5001e6],
@@ -458,9 +451,9 @@ def ask(self, command, num_bytes=None):
 
     sweep_marker_frequency = Instrument.control(
         'IMRK', 'MRK %s',
-        """ A floating point value that controls the frequency marker in both sweep modes.
+        """Control the frequency marker in both sweep modes in Hz
+        (strict float from 1e-3 to 52.5e6).
         At this frequency, the marker output switches from low to high.
-        The allowed range is 1 mHz to 52.5 MHz.
         """,
         validator=strict_range,
         values=[1e-3, 52.5001e6],
@@ -470,7 +463,7 @@ def ask(self, command, num_bytes=None):
 
     sweep_time = Instrument.control(
         'ISWT', 'SWT %s',
-        """ A floating point value that controls the sweep time per decade in both sweep modes.
+        """Control the sweep time per decade in both sweep modes in s (float).
         The sweep time is selectable in a 1-2-5 sequence between 10 ms and 500 s.
         """,
         validator=truncated_discrete_set,
@@ -481,16 +474,17 @@ def ask(self, command, num_bytes=None):
 
     @property
     def status(self):
-        """ Returns the status byte of the 8116A as an IntFlag-type enum. """
+        """Get the status byte of the 8116A as a :class:`Status` IntFlag-type enum."""
         return Status(self.adapter.connection.read_stb())
 
     @property
     def complete(self):
+        """Get whether the measurement is complete (bool)."""
         return not (self.status & Status.buffer_not_empty)
 
     @property
     def options(self):
-        """ Return the device options installed. The only possible option is 001. """
+        """Get the device options installed. The only possible option is 001."""
         if self.has_option_001:
             return ['001']
         else:
@@ -533,7 +527,7 @@ def reset(self):
     def shutdown(self):
         """ Gracefully close the connection to the 8116A. """
         self.adapter.connection.clear()
-        self.adapter.connection.close()
+        self.adapter.close()
         super().shutdown()
 
     def check_errors(self):


Test patch:
diff --git a/tests/instruments/hp/test_hp8116a.py b/tests/instruments/hp/test_hp8116a.py
--- a/tests/instruments/hp/test_hp8116a.py
+++ b/tests/instruments/hp/test_hp8116a.py
@@ -31,10 +31,13 @@
 HP8116A.status = property(fget=lambda self: Status(5))
 
 
+init_comm = [(b"CST", b"x" * 87 + b' ,\r\n')]  # communication during init
+
+
 def test_init():
     with expected_protocol(
             HP8116A,
-            [(b"CST", b"x" * 87 + b' ,\r\n')],
+            init_comm,
     ):
         pass  # Verify the expected communication.
 
@@ -42,7 +45,7 @@ def test_init():
 def test_duty_cycle():
     with expected_protocol(
             HP8116A,
-            [(b"CST", b"x" * 87 + b' ,\r\n'), (b"IDTY", b"00000035")],
+            init_comm + [(b"IDTY", b"00000035")],
     ) as instr:
         assert instr.duty_cycle == 35
 
@@ -50,6 +53,17 @@ def test_duty_cycle():
 def test_duty_cycle_setter():
     with expected_protocol(
             HP8116A,
-            [(b"CST", b"x" * 87 + b' ,\r\n'), (b"DTY 34.5 %", None)],
+            init_comm + [(b"DTY 34.5 %", None)],
     ) as instr:
         instr.duty_cycle = 34.5
+
+
+def test_sweep_time():
+    with expected_protocol(HP8116A, init_comm + [("SWT 5 S", None)]) as inst:
+        # This test tests also the generate_1_2_5_sequence method and truncation.
+        inst.sweep_time = 3
+
+
+def test_limit_enabled():
+    with expected_protocol(HP8116A, init_comm + [("L1", None)]) as inst:
+        inst.limit_enabled = True
diff --git a/tests/instruments/test_all_instruments.py b/tests/instruments/test_all_instruments.py
--- a/tests/instruments/test_all_instruments.py
+++ b/tests/instruments/test_all_instruments.py
@@ -156,7 +156,6 @@ def find_devices_in_module(module):
     "HP6632A",
     "HP6633A",
     "HP6634A",
-    "HP8116A",
     "Keithley2000",
     "Keithley2306",
     "Keithley2306Channel",