From 065d3d23d78ae2a91ed452d4610fb7607a5471fb Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Thu, 20 Apr 2017 23:24:15 +1000 Subject: [PATCH 1/8] Extended YAML (#164) * Added test:// URI schema. * Config: Py3 fix and support for setting attrs. * Added sub/indexing to attrs config. * Added to docstring. * Added support for physical quantities to config. * pylint fixes * Tests for new setattr_expression * Added tests for new config, YAML. * pylint fix for new tests * Very minor pylint fix * Added ruamel.yaml as testing dep. * Reworked awkward sentence. * Updated docs for ruamel.yaml dep. * Remove pyyaml, only use ruamel.yaml * Disabled info category messages from mylint. * Added version dump to travis config. * Trying ot use python -m to work around venv issues. * Revert "Trying ot use python -m to work around venv issues." This reverts commit f73a2439116ba89b867893dbcfe00f1982e03694. * ruamel.yaml vs ruamel_yaml and fixing pylint false +ve. * Explicitly use unsafe loader as suggested by ruamel.yaml warnings. * Marked test as explicitly unsafe as well. --- .travis.yml | 15 +++- doc/source/intro.rst | 6 +- .../abstract_instruments/instrument.py | 6 +- instruments/config.py | 76 ++++++++++++++++--- instruments/tests/test_config.py | 64 ++++++++++++++++ instruments/tests/test_util_fns.py | 45 ++++++++++- instruments/util_fns.py | 34 ++++++++- requirements.txt | 2 +- setup.py | 2 +- 9 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 instruments/tests/test_config.py diff --git a/.travis.yml b/.travis.yml index 7cbb7d02b..ed6d2479f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,10 +10,21 @@ install: - "pip install -r dev-requirements.txt" - pip install python-coveralls - pip install coverage +before_script: + # We use before_script to report version and path information in a way + # that can be easily hidden by Travis' log folding. Moreover, a nonzero + # exit code from this block kills the entire job, meaning that if we can't + # even sensibly get version information, we correctly abort. + - which python + - python --version + - which nosetests + - nosetests --version + - which pylint + - pylint --version script: - nosetests --with-coverage -w instruments - - pylint --py3k instruments/ - - pylint instruments/ + - pylint --py3k instruments + - pylint --disable=I instruments after_success: - coveralls deploy: diff --git a/doc/source/intro.rst b/doc/source/intro.rst index bc8c18ac8..733a33c44 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -33,9 +33,9 @@ $ pip install -r requirements.txt - `enum34`_ - `future`_ - `python-vxi11`_ -- `PyUSB`_ +- `PyUSB`_ (version 1.0a or higher, required for raw USB support) - `python-usbtmc`_ -- `PyYAML`_ +- `ruamel.yaml`_ (required for configuration file support) Optional Dependencies ~~~~~~~~~~~~~~~~~~~~~ @@ -46,7 +46,7 @@ Optional Dependencies .. _quantities: http://pythonhosted.org/quantities/ .. _enum34: https://pypi.python.org/pypi/enum34 .. _future: https://pypi.python.org/pypi/future -.. _PyYAML: https://bitbucket.org/xi/pyyaml +.. _ruamel.yaml: http://yaml.readthedocs.io .. _PyUSB: http://sourceforge.net/apps/trac/pyusb/ .. _PyVISA: http://pyvisa.sourceforge.net/ .. _python-usbtmc: https://pypi.python.org/pypi/python-usbtmc diff --git a/instruments/abstract_instruments/instrument.py b/instruments/abstract_instruments/instrument.py index 0e791142f..7163f40f0 100644 --- a/instruments/abstract_instruments/instrument.py +++ b/instruments/abstract_instruments/instrument.py @@ -310,7 +310,8 @@ def binblockread(self, data_width, fmt=None): # CLASS METHODS # URI_SCHEMES = ["serial", "tcpip", "gpib+usb", - "gpib+serial", "visa", "file", "usbtmc", "vxi11"] + "gpib+serial", "visa", "file", "usbtmc", "vxi11", + "test"] @classmethod def open_from_uri(cls, uri): @@ -330,6 +331,7 @@ def open_from_uri(cls, uri): gpib+serial:///dev/ttyACM0/15 # Currently non-functional. visa://USB::0x0699::0x0401::C0000001::0::INSTR usbtmc://USB::0x0699::0x0401::C0000001::0::INSTR + test:// For the ``serial`` URI scheme, baud rates may be explicitly specified using the query parameter ``baud=``, as in the example @@ -415,6 +417,8 @@ def open_from_uri(cls, uri): # vxi11://192.168.1.104 # vxi11://TCPIP::192.168.1.105::gpib,5::INSTR return cls.open_vxi11(parsed_uri.netloc, **kwargs) + elif parsed_uri.scheme == "test": + return cls.open_test(**kwargs) else: raise NotImplementedError("Invalid scheme or not yet " "implemented.") diff --git a/instruments/config.py b/instruments/config.py index 74d09499b..107daa157 100644 --- a/instruments/config.py +++ b/instruments/config.py @@ -12,9 +12,22 @@ import warnings try: - import yaml + import ruamel.yaml as yaml except ImportError: - yaml = None + # Some versions of ruamel.yaml are named ruamel_yaml, so try that + # too. + # + # In either case, we've observed issues with pylint where it will raise + # a false positive from its import-error checker, so we locally disable + # it here. Once the cause for the false positive has been identified, + # the import-error check should be re-enabled. + import ruamel_yaml as yaml # pylint: disable=import-error + +import quantities as pq + +from future.builtins import str + +from instruments.util_fns import setattr_expression, split_unit_str # FUNCTIONS ################################################################### @@ -37,8 +50,9 @@ def walk_dict(d, path): # Treat as a base case that the path is empty. if not path: return d - if isinstance(path, str): + if not isinstance(path, list): path = path.split("/") + if not path[0]: # If the first part of the path is empty, do nothing. return walk_dict(d, path[1:]) @@ -46,6 +60,18 @@ def walk_dict(d, path): # Otherwise, resolve that segment and recurse. return walk_dict(d[path[0]], path[1:]) +def quantity_constructor(loader, node): + """ + Constructs a `pq.Quantity` instance from a PyYAML + node tagged as ``!Q``. + """ + # Follows the example of http://stackoverflow.com/a/43081967/267841. + value = loader.construct_scalar(node) + return pq.Quantity(*split_unit_str(value)) + +# We avoid having to register !Q every time by doing as soon as the +# relevant constructor is defined. +yaml.add_constructor(u'!Q', quantity_constructor) def load_instruments(conf_file_name, conf_path="/"): """ @@ -63,6 +89,28 @@ def load_instruments(conf_file_name, conf_path="/"): the form ``{'ddg': instruments.srs.SRSDG645.open_from_uri('gpib+usb://COM7/15')}``. + Each instrument configuration section can also specify one or more attributes + to set. These attributes are specified using a ``attrs`` section as well as the + required ``class`` and ``uri`` sections. For instance, the following + dictionary creates a ThorLabs APT motor controller instrument with a single motor + model configured:: + + rot_stage: + class: !!python/name:instruments.thorabsapt.APTMotorController + uri: serial:///dev/ttyUSB0?baud=115200 + attrs: + channel[0].motor_model: PRM1-Z8 + + Unitful attributes can be specified by using the ``!Q`` tag to quickly create + instances of `pq.Quantity`. In the example above, for instance, we can set a motion + timeout as a unitful quantity:: + + attrs: + motion_timeout: !Q 1 minute + + When using the ``!Q`` tag, any text before a space is taken to be the magnitude + of the quantity, and text following is taken to be the unit specification. + By specifying a path within the configuration file, one can load only a part of the given file. For instance, consider the configuration:: @@ -78,7 +126,7 @@ def load_instruments(conf_file_name, conf_path="/"): all other keys in the YAML file. :param str conf_file_name: Name of the configuration file to load - instruments from. + instruments from. Alternatively, a file-like object may be provided. :param str conf_path: ``"/"`` separated path to the section in the configuration file to load. @@ -95,23 +143,33 @@ def load_instruments(conf_file_name, conf_path="/"): """ if yaml is None: - raise ImportError("Could not import PyYAML, which is required " + raise ImportError("Could not import ruamel.yaml, which is required " "for this function.") - with open(conf_file_name, 'r') as f: - conf_dict = yaml.load(f) + if isinstance(conf_file_name, str): + with open(conf_file_name, 'r') as f: + conf_dict = yaml.load(f, Loader=yaml.Loader) + else: + conf_dict = yaml.load(conf_file_name, Loader=yaml.Loader) conf_dict = walk_dict(conf_dict, conf_path) inst_dict = {} - for name, value in conf_dict.iteritems(): + for name, value in conf_dict.items(): try: inst_dict[name] = value["class"].open_from_uri(value["uri"]) + + if 'attrs' in value: + # We have some attrs we can set on the newly created instrument. + for attr_name, attr_value in value['attrs'].items(): + setattr_expression(inst_dict[name], attr_name, attr_value) + except IOError as ex: # FIXME: need to subclass Warning so that repeated warnings # aren't ignored. - warnings.warn("Exception occured loading device URI " + warnings.warn("Exception occured loading device with URI " "{}:\n\t{}.".format(value["uri"], ex), RuntimeWarning) inst_dict[name] = None + return inst_dict diff --git a/instruments/tests/test_config.py b/instruments/tests/test_config.py new file mode 100644 index 000000000..6a2119fa5 --- /dev/null +++ b/instruments/tests/test_config.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for util_fns.py +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import, unicode_literals + +from io import StringIO + +import quantities as pq + +from instruments import Instrument +from instruments.config import ( + load_instruments, yaml +) + +# TEST CASES ################################################################# + +# pylint: disable=protected-access,missing-docstring + +def test_load_test_instrument(): + config_data = StringIO(u""" +test: + class: !!python/name:instruments.Instrument + uri: test:// +""") + insts = load_instruments(config_data) + assert isinstance(insts['test'], Instrument) + +def test_load_test_instrument_subtree(): + config_data = StringIO(u""" +instruments: + test: + class: !!python/name:instruments.Instrument + uri: test:// +""") + insts = load_instruments(config_data, conf_path="/instruments") + assert isinstance(insts['test'], Instrument) + +def test_yaml_quantity_tag(): + yaml_data = StringIO(u""" +a: + b: !Q 37 tesla + c: !Q 41.2 inches + d: !Q 98 +""") + data = yaml.load(yaml_data, Loader=yaml.Loader) + assert data['a']['b'] == pq.Quantity(37, 'tesla') + assert data['a']['c'] == pq.Quantity(41.2, 'inches') + assert data['a']['d'] == 98 + +def test_load_test_instrument_setattr(): + config_data = StringIO(u""" +test: + class: !!python/name:instruments.Instrument + uri: test:// + attrs: + foo: !Q 111 GHz +""") + insts = load_instruments(config_data) + assert insts['test'].foo == pq.Quantity(111, 'GHz') diff --git a/instruments/tests/test_util_fns.py b/instruments/tests/test_util_fns.py index 914372404..ecf8e6ec5 100644 --- a/instruments/tests/test_util_fns.py +++ b/instruments/tests/test_util_fns.py @@ -16,7 +16,8 @@ from instruments.util_fns import ( ProxyList, - assume_units, convert_temperature + assume_units, convert_temperature, + setattr_expression ) # TEST CASES ################################################################# @@ -169,3 +170,45 @@ def test_temperater_conversion_failure(): @raises(ValueError) def test_assume_units_failures(): assume_units(1, 'm').rescale('s') + +def test_setattr_expression_simple(): + class A(object): + x = 'x' + y = 'y' + z = 'z' + + a = A() + setattr_expression(a, 'x', 'foo') + assert a.x == 'foo' + +def test_setattr_expression_index(): + class A(object): + x = ['x', 'y', 'z'] + + a = A() + setattr_expression(a, 'x[1]', 'foo') + assert a.x[1] == 'foo' + +def test_setattr_expression_nested(): + class B(object): + x = 'x' + class A(object): + b = None + def __init__(self): + self.b = B() + + a = A() + setattr_expression(a, 'b.x', 'foo') + assert a.b.x == 'foo' + +def test_setattr_expression_both(): + class B(object): + x = 'x' + class A(object): + b = None + def __init__(self): + self.b = [B()] + + a = A() + setattr_expression(a, 'b[0].x', 'foo') + assert a.b[0].x == 'foo' diff --git a/instruments/util_fns.py b/instruments/util_fns.py index 316343fc9..977bfdcf3 100644 --- a/instruments/util_fns.py +++ b/instruments/util_fns.py @@ -14,11 +14,13 @@ from enum import Enum, IntEnum import quantities as pq -# FUNCTIONS ################################################################### +# CONSTANTS ################################################################### -# pylint: disable=too-many-arguments +_IDX_REGEX = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)\[(-?[0-9]*)\]') +# FUNCTIONS ################################################################### +# pylint: disable=too-many-arguments def assume_units(value, units): """ If units are not provided for ``value`` (that is, if it is a raw @@ -37,6 +39,34 @@ def assume_units(value, units): value = pq.Quantity(value, units) return value +def setattr_expression(target, name_expr, value): + """ + Recursively calls getattr/setattr for attribute + names that are miniature expressions with subscripting. + For instance, of the form ``a[0].b``. + """ + # Allow "." in attribute names so that we can set attributes + # recursively. + if '.' in name_expr: + # Recursion: We have to strip off a level of getattr. + head, name_expr = name_expr.split('.', 1) + match = _IDX_REGEX.match(head) + if match: + head_name, head_idx = match.groups() + target = getattr(target, head_name)[int(head_idx)] + else: + target = getattr(target, head) + + setattr_expression(target, name_expr, value) + else: + # Base case: We're in the last part of a dot-expression. + match = _IDX_REGEX.match(name_expr) + if match: + name, idx = match.groups() + getattr(target, name)[int(idx)] = value + else: + setattr(target, name_expr, value) + def convert_temperature(temperature, base): """ diff --git a/requirements.txt b/requirements.txt index 113c7a6a3..622bb14d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ enum34 python-vxi11>=0.8 pyusb python-usbtmc -pyyaml +ruamel.yaml diff --git a/setup.py b/setup.py index c928c9608..b422efd4b 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ "python-vxi11", "python-usbtmc", "pyusb", - "pyyaml" + "ruamel.yaml" ] EXTRAS_REQUIRE = { 'VISA': ["pyvisa"] From 6c6dbb498ad38abd0310f7e4a894d329f6e60280 Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Wed, 7 Jun 2017 07:34:05 +1000 Subject: [PATCH 2/8] Fixes for pylint version 1.7 (#166) * Started fixing new pylint 1.7 checks. * More pylint17 fixes. * Removed flags that are now disabled by default. * Still more misc pylint17 fixes. * Updated pylint freeze to 1.7. * Guess what? Still more misc fixes. * More fixes. * pylint17 fixes nearly done * Should be the last fixes for 1.7. * pylint 1.7 fix * Fix parameter names * Update pylint 1.7->1.7.1 * Cleanup thorlabs_utils.py --- dev-requirements.txt | 2 +- .../comm/file_communicator.py | 4 +-- .../comm/gi_gpib_communicator.py | 7 ++--- .../comm/usbtmc_communicator.py | 4 +-- .../function_generator.py | 5 ++-- .../abstract_instruments/oscilloscope.py | 6 ++-- instruments/agilent/agilent33220a.py | 3 -- instruments/agilent/agilent34410a.py | 3 -- instruments/config.py | 6 ++-- .../generic_scpi/scpi_function_generator.py | 3 -- instruments/generic_scpi/scpi_instrument.py | 3 -- instruments/generic_scpi/scpi_multimeter.py | 5 +--- instruments/hp/hp6632b.py | 3 -- instruments/hp/hp6652a.py | 3 -- instruments/keithley/keithley195.py | 10 +++---- instruments/keithley/keithley2182.py | 3 -- instruments/keithley/keithley580.py | 8 ++--- instruments/ondax/lm.py | 4 +-- instruments/qubitekk/cc1.py | 4 +-- instruments/rigol/rigolds1000.py | 3 -- instruments/srs/srs830.py | 4 +-- instruments/srs/srsctc100.py | 2 +- instruments/tektronix/tekdpo4104.py | 10 ++++--- instruments/tektronix/tekdpo70000.py | 3 -- instruments/tektronix/tektds224.py | 4 +-- instruments/tektronix/tektds5xx.py | 11 +++---- instruments/tests/test_comm/test_file.py | 2 +- .../tests/test_comm/test_gi_gpibusb.py | 2 +- instruments/tests/test_comm/test_loopback.py | 2 +- instruments/tests/test_comm/test_socket.py | 2 +- instruments/tests/test_comm/test_usbtmc.py | 2 +- .../tests/test_qubitekk/test_qubitekk_cc1.py | 4 +-- instruments/tests/test_srs/test_srs830.py | 2 +- instruments/thorlabs/_packets.py | 10 +++---- instruments/thorlabs/tc200.py | 4 +-- instruments/thorlabs/thorlabs_utils.py | 30 ++++--------------- instruments/toptica/topmode.py | 6 ++-- instruments/toptica/toptica_utils.py | 4 +-- instruments/util_fns.py | 17 ++++++----- instruments/yokogawa/yokogawa7651.py | 3 -- 40 files changed, 80 insertions(+), 133 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index c42f8689e..419748e1c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,3 @@ mock nose -pylint==1.6.5 +pylint==1.7.1 diff --git a/instruments/abstract_instruments/comm/file_communicator.py b/instruments/abstract_instruments/comm/file_communicator.py index b6022e7ce..46245660e 100644 --- a/instruments/abstract_instruments/comm/file_communicator.py +++ b/instruments/abstract_instruments/comm/file_communicator.py @@ -60,8 +60,8 @@ def address(self): """ if hasattr(self._filelike, 'name'): return self._filelike.name - else: - return None + + return None @address.setter def address(self, newval): diff --git a/instruments/abstract_instruments/comm/gi_gpib_communicator.py b/instruments/abstract_instruments/comm/gi_gpib_communicator.py index ab686bb15..3123059d7 100644 --- a/instruments/abstract_instruments/comm/gi_gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gi_gpib_communicator.py @@ -48,7 +48,7 @@ def __init__(self, filelike, gpib_address): if self._version <= 4: self._eos = 10 else: - self._eos = "\n" # pylint: disable=redefined-variable-type + self._eos = "\n" # PROPERTIES # @@ -117,8 +117,8 @@ def terminator(self): """ if not self._eoi: return self._terminator - else: - return 'eoi' + + return 'eoi' @terminator.setter def terminator(self, newval): @@ -203,7 +203,6 @@ def eos(self): @eos.setter def eos(self, newval): - # pylint: disable=redefined-variable-type if self._version <= 4: if isinstance(newval, (str, bytes)): newval = ord(newval) diff --git a/instruments/abstract_instruments/comm/usbtmc_communicator.py b/instruments/abstract_instruments/comm/usbtmc_communicator.py index 2f96194f6..f465abba7 100644 --- a/instruments/abstract_instruments/comm/usbtmc_communicator.py +++ b/instruments/abstract_instruments/comm/usbtmc_communicator.py @@ -43,8 +43,8 @@ def __init__(self, *args, **kwargs): def address(self): if hasattr(self._filelike, "name"): return id(self._filelike) # TODO: replace with something more useful. - else: - return None + + return None @property def terminator(self): diff --git a/instruments/abstract_instruments/function_generator.py b/instruments/abstract_instruments/function_generator.py index ffe73f3aa..e9baa64be 100644 --- a/instruments/abstract_instruments/function_generator.py +++ b/instruments/abstract_instruments/function_generator.py @@ -153,8 +153,8 @@ def amplitude(self): if units == self.VoltageMode.dBm: return pq.Quantity(mag, u.dBm) - else: - return pq.Quantity(mag, pq.V), units + + return pq.Quantity(mag, pq.V), units @amplitude.setter def amplitude(self, newval): @@ -168,7 +168,6 @@ def amplitude(self, newval): # OK, we have volts. Now, do we have a tuple? If not, assume Vpp. if not isinstance(newval, tuple): mag = newval - # pylint: disable=redefined-variable-type units = self.VoltageMode.peak_to_peak else: mag, units = newval diff --git a/instruments/abstract_instruments/oscilloscope.py b/instruments/abstract_instruments/oscilloscope.py index 960f28b28..16fa662ee 100644 --- a/instruments/abstract_instruments/oscilloscope.py +++ b/instruments/abstract_instruments/oscilloscope.py @@ -49,8 +49,10 @@ def __exit__(self, type, value, traceback): def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented - else: - return other.name == self.name + + return other.name == self.name + + __hash__ = None # PROPERTIES # diff --git a/instruments/agilent/agilent33220a.py b/instruments/agilent/agilent33220a.py index 7a2c56b7c..5a24ea9ad 100644 --- a/instruments/agilent/agilent33220a.py +++ b/instruments/agilent/agilent33220a.py @@ -43,9 +43,6 @@ class Agilent33220a(SCPIFunctionGenerator): """ - def __init__(self, filelike): - super(Agilent33220a, self).__init__(filelike) - # ENUMS # class Function(Enum): diff --git a/instruments/agilent/agilent34410a.py b/instruments/agilent/agilent34410a.py index 329ad8abf..8c444f552 100644 --- a/instruments/agilent/agilent34410a.py +++ b/instruments/agilent/agilent34410a.py @@ -35,9 +35,6 @@ class Agilent34410a(SCPIMultimeter): # pylint: disable=abstract-method .. _Keysight website: http://www.keysight.com/ """ - def __init__(self, filelike): - super(Agilent34410a, self).__init__(filelike) - # PROPERTIES # @property diff --git a/instruments/config.py b/instruments/config.py index 107daa157..2e6d7b6eb 100644 --- a/instruments/config.py +++ b/instruments/config.py @@ -56,9 +56,9 @@ def walk_dict(d, path): if not path[0]: # If the first part of the path is empty, do nothing. return walk_dict(d, path[1:]) - else: - # Otherwise, resolve that segment and recurse. - return walk_dict(d[path[0]], path[1:]) + + # Otherwise, resolve that segment and recurse. + return walk_dict(d[path[0]], path[1:]) def quantity_constructor(loader, node): """ diff --git a/instruments/generic_scpi/scpi_function_generator.py b/instruments/generic_scpi/scpi_function_generator.py index fcad54c04..66630006a 100644 --- a/instruments/generic_scpi/scpi_function_generator.py +++ b/instruments/generic_scpi/scpi_function_generator.py @@ -32,9 +32,6 @@ class SCPIFunctionGenerator(FunctionGenerator, SCPIInstrument): >>> inst.frequency = 1 * pq.kHz """ - def __init__(self, filelike): - super(SCPIFunctionGenerator, self).__init__(filelike) - # CONSTANTS # _UNIT_MNEMONICS = { diff --git a/instruments/generic_scpi/scpi_instrument.py b/instruments/generic_scpi/scpi_instrument.py index 30d16f670..5f4f0d93b 100644 --- a/instruments/generic_scpi/scpi_instrument.py +++ b/instruments/generic_scpi/scpi_instrument.py @@ -37,9 +37,6 @@ class SCPIInstrument(Instrument): >>> print(inst.name) """ - def __init__(self, filelike): - super(SCPIInstrument, self).__init__(filelike) - # PROPERTIES # @property diff --git a/instruments/generic_scpi/scpi_multimeter.py b/instruments/generic_scpi/scpi_multimeter.py index 213d9b3d7..b6cc062fd 100644 --- a/instruments/generic_scpi/scpi_multimeter.py +++ b/instruments/generic_scpi/scpi_multimeter.py @@ -45,9 +45,6 @@ class SCPIMultimeter(SCPIInstrument, Multimeter): >>> print(inst.measure(inst.Mode.resistance)) """ - def __init__(self, filelike): - super(SCPIMultimeter, self).__init__(filelike) - # ENUMS ## class Mode(Enum): @@ -142,7 +139,7 @@ class SampleSource(Enum): # PROPERTIES ## - # pylint: disable=unnecessary-lambda + # pylint: disable=unnecessary-lambda,undefined-variable mode = enum_property( name="CONF", enum=Mode, diff --git a/instruments/hp/hp6632b.py b/instruments/hp/hp6632b.py index 635ca53cd..b07407293 100644 --- a/instruments/hp/hp6632b.py +++ b/instruments/hp/hp6632b.py @@ -77,9 +77,6 @@ class HP6632b(SCPIInstrument, HP6652a): array(10.0) * V """ - def __init__(self, filelike): - super(HP6632b, self).__init__(filelike) - # ENUMS ## class ALCBandwidth(IntEnum): diff --git a/instruments/hp/hp6652a.py b/instruments/hp/hp6652a.py index 38e6bac40..f5d88ce03 100644 --- a/instruments/hp/hp6652a.py +++ b/instruments/hp/hp6652a.py @@ -55,9 +55,6 @@ class HP6652a(PowerSupply, PowerSupplyChannel): >>> psu.display_textmode=False """ - def __init__(self, filelike): - super(HP6652a, self).__init__(filelike) - # ENUMS ## # I don't know of any possible enumerations supported diff --git a/instruments/keithley/keithley195.py b/instruments/keithley/keithley195.py index 5bf00b06f..620be342b 100644 --- a/instruments/keithley/keithley195.py +++ b/instruments/keithley/keithley195.py @@ -195,11 +195,11 @@ def input_range(self): index = self.parse_status_word(self.get_status_word())['range'] if index == 0: return 'auto' - else: - mode = self.mode - value = Keithley195.ValidRange[mode.name].value[index - 1] - units = UNITS2[mode] - return value * units + + mode = self.mode + value = Keithley195.ValidRange[mode.name].value[index - 1] + units = UNITS2[mode] + return value * units @input_range.setter def input_range(self, newval): diff --git a/instruments/keithley/keithley2182.py b/instruments/keithley/keithley2182.py index 6a4873b06..8e3e245b1 100644 --- a/instruments/keithley/keithley2182.py +++ b/instruments/keithley/keithley2182.py @@ -35,9 +35,6 @@ class Keithley2182(SCPIMultimeter): """ - def __init__(self, filelike): - super(Keithley2182, self).__init__(filelike) - # INNER CLASSES # class Channel(Multimeter): diff --git a/instruments/keithley/keithley580.py b/instruments/keithley/keithley580.py index d8336ebbd..5ed6bbd26 100644 --- a/instruments/keithley/keithley580.py +++ b/instruments/keithley/keithley580.py @@ -463,8 +463,8 @@ def parse_measurement(measurement): # COMMUNICATOR METHODS # - def sendcmd(self, msg): - super(Keithley580, self).sendcmd(msg + ':') + def sendcmd(self, cmd): + super(Keithley580, self).sendcmd(cmd + ':') - def query(self, msg, size=-1): - return super(Keithley580, self).query(msg + ':', size)[:-1] + def query(self, cmd, size=-1): + return super(Keithley580, self).query(cmd + ':', size)[:-1] diff --git a/instruments/ondax/lm.py b/instruments/ondax/lm.py index 11070253a..0f4dfbfa3 100644 --- a/instruments/ondax/lm.py +++ b/instruments/ondax/lm.py @@ -411,8 +411,8 @@ def enabled(self, newval): def _ack_expected(self, msg=""): if msg.find("?") > 0: return None - else: - return "OK" + + return "OK" @property def firmware(self): diff --git a/instruments/qubitekk/cc1.py b/instruments/qubitekk/cc1.py index b6074755a..e2c785b31 100644 --- a/instruments/qubitekk/cc1.py +++ b/instruments/qubitekk/cc1.py @@ -275,8 +275,8 @@ def dwell_time(self): dwell_time = pq.Quantity(*split_unit_str(self.query("DWEL?"), "s")) if self.firmware[0] <= 2 and self.firmware[1] <= 1: return dwell_time/1000.0 - else: - return dwell_time + + return dwell_time @dwell_time.setter def dwell_time(self, new_val): diff --git a/instruments/rigol/rigolds1000.py b/instruments/rigol/rigolds1000.py index 4f23ae5e5..91acc4777 100644 --- a/instruments/rigol/rigolds1000.py +++ b/instruments/rigol/rigolds1000.py @@ -60,9 +60,6 @@ class DataSource(OscilloscopeDataSource): is designed to be initialized by the `RigolDS1000Series` class. """ - def __init__(self, parent, name): - super(RigolDS1000Series.DataSource, self).__init__(parent, name) - @property def name(self): return self._name diff --git a/instruments/srs/srs830.py b/instruments/srs/srs830.py index f533d0b9e..93f195c43 100644 --- a/instruments/srs/srs830.py +++ b/instruments/srs/srs830.py @@ -61,9 +61,9 @@ def __init__(self, filelike, outx_mode=None): # pragma: no cover will be sent depending on what type of communicator self._file is. """ super(SRS830, self).__init__(filelike) - if outx_mode is 1: + if outx_mode == 1: self.sendcmd("OUTX 1") - elif outx_mode is 2: + elif outx_mode == 2: self.sendcmd("OUTX 2") else: if isinstance(self._file, GPIBCommunicator): diff --git a/instruments/srs/srsctc100.py b/instruments/srs/srsctc100.py index 827b3ee90..3042ebc5b 100644 --- a/instruments/srs/srsctc100.py +++ b/instruments/srs/srsctc100.py @@ -168,7 +168,7 @@ def stats_enabled(self): :type: `bool` """ - return True if self._get('stats') is 'On' else False + return True if self._get('stats') == 'On' else False @stats_enabled.setter def stats_enabled(self, newval): diff --git a/instruments/tektronix/tekdpo4104.py b/instruments/tektronix/tekdpo4104.py index e770e70b6..0baea2240 100644 --- a/instruments/tektronix/tekdpo4104.py +++ b/instruments/tektronix/tekdpo4104.py @@ -83,8 +83,10 @@ def __exit__(self, type, value, traceback): def __eq__(self, other): if not isinstance(other, type(self)): return NotImplemented - else: - return other.name == self.name + + return other.name == self.name + + __hash__ = None def read_waveform(self, bin_format=True): """ @@ -259,8 +261,8 @@ def data_source(self): name = self.query("DAT:SOU?") if name.startswith("CH"): return _TekDPO4104Channel(self, int(name[2:]) - 1) - else: - return _TekDPO4104DataSource(self, name) + + return _TekDPO4104DataSource(self, name) @data_source.setter def data_source(self, newval): diff --git a/instruments/tektronix/tekdpo70000.py b/instruments/tektronix/tekdpo70000.py index 3c4c7664a..a70767b7c 100644 --- a/instruments/tektronix/tekdpo70000.py +++ b/instruments/tektronix/tekdpo70000.py @@ -172,9 +172,6 @@ class DataSource(OscilloscopeDataSource): is designed to be initialized by the `TekDPO70000` class. """ - def __init__(self, parent, name): - super(TekDPO70000.DataSource, self).__init__(parent, name) - @property def name(self): return self._name diff --git a/instruments/tektronix/tektds224.py b/instruments/tektronix/tektds224.py index ba7238d50..e5574da80 100644 --- a/instruments/tektronix/tektds224.py +++ b/instruments/tektronix/tektds224.py @@ -227,8 +227,8 @@ def data_source(self): name = self.query("DAT:SOU?") if name.startswith("CH"): return _TekTDS224Channel(self, int(name[2:]) - 1) - else: - return _TekTDS224DataSource(self, name) + + return _TekTDS224DataSource(self, name) @data_source.setter def data_source(self, newval): diff --git a/instruments/tektronix/tektds5xx.py b/instruments/tektronix/tektds5xx.py index d41c930c3..91bf28cfa 100644 --- a/instruments/tektronix/tektds5xx.py +++ b/instruments/tektronix/tektds5xx.py @@ -83,8 +83,8 @@ def read(self): resp = self._tek.query('MEASU:MEAS{}:VAL?'.format(self._id)) self._data['value'] = float(resp) return self._data - else: - return self._data + + return self._data class _TekTDS5xxDataSource(OscilloscopeDataSource): @@ -97,9 +97,6 @@ class _TekTDS5xxDataSource(OscilloscopeDataSource): designed to be initialized by the `TekTDS5xx` class. """ - def __init__(self, parent, name): - super(_TekTDS5xxDataSource, self).__init__(parent, name) - @property def name(self): """ @@ -434,8 +431,8 @@ def data_source(self): name = self.query("DAT:SOU?") if name.startswith("CH"): return _TekTDS5xxChannel(self, int(name[2:]) - 1) - else: - return _TekTDS5xxDataSource(self, name) + + return _TekTDS5xxDataSource(self, name) @data_source.setter def data_source(self, newval): diff --git a/instruments/tests/test_comm/test_file.py b/instruments/tests/test_comm/test_file.py index fcd242077..fde974caf 100644 --- a/instruments/tests/test_comm/test_file.py +++ b/instruments/tests/test_comm/test_file.py @@ -60,7 +60,7 @@ def test_filecomm_terminator(): comm.terminator = "*" eq_(comm._terminator, "*") - comm.terminator = b"*" # pylint: disable=redefined-variable-type + comm.terminator = b"*" eq_(comm._terminator, "*") diff --git a/instruments/tests/test_comm/test_gi_gpibusb.py b/instruments/tests/test_comm/test_gi_gpibusb.py index bee107953..263844e3e 100644 --- a/instruments/tests/test_comm/test_gi_gpibusb.py +++ b/instruments/tests/test_comm/test_gi_gpibusb.py @@ -66,7 +66,7 @@ def test_gpibusbcomm_address(): eq_(comm._gpib_address, 5) # Able to set address with a list - comm.address = [6, "/dev/foobar"] # pylint: disable=redefined-variable-type + comm.address = [6, "/dev/foobar"] eq_(comm._gpib_address, 6) port_name.assert_called_with("/dev/foobar") diff --git a/instruments/tests/test_comm/test_loopback.py b/instruments/tests/test_comm/test_loopback.py index 66e5aae5d..eaa9e796c 100644 --- a/instruments/tests/test_comm/test_loopback.py +++ b/instruments/tests/test_comm/test_loopback.py @@ -48,7 +48,7 @@ def test_loopbackcomm_terminator(): eq_(comm.terminator, "*") eq_(comm._terminator, "*") - comm.terminator = u"\r" # pylint: disable=redefined-variable-type + comm.terminator = u"\r" eq_(comm.terminator, u"\r") eq_(comm._terminator, u"\r") diff --git a/instruments/tests/test_comm/test_socket.py b/instruments/tests/test_comm/test_socket.py index a08c39660..05307f946 100644 --- a/instruments/tests/test_comm/test_socket.py +++ b/instruments/tests/test_comm/test_socket.py @@ -59,7 +59,7 @@ def test_socketcomm_terminator(): eq_(comm.terminator, "*") eq_(comm._terminator, "*") - comm.terminator = u"\r" # pylint: disable=redefined-variable-type + comm.terminator = u"\r" eq_(comm.terminator, u"\r") eq_(comm._terminator, u"\r") diff --git a/instruments/tests/test_comm/test_usbtmc.py b/instruments/tests/test_comm/test_usbtmc.py index 826ccecec..18e2ebcf7 100644 --- a/instruments/tests/test_comm/test_usbtmc.py +++ b/instruments/tests/test_comm/test_usbtmc.py @@ -58,7 +58,7 @@ def test_usbtmccomm_terminator_setter(mock_usbtmc): eq_(comm._terminator, "*") term_char.assert_called_with(42) - comm.terminator = b"*" # pylint: disable=redefined-variable-type + comm.terminator = b"*" eq_(comm._terminator, "*") term_char.assert_called_with(42) diff --git a/instruments/tests/test_qubitekk/test_qubitekk_cc1.py b/instruments/tests/test_qubitekk/test_qubitekk_cc1.py index e5d366fbc..cdbe9b499 100644 --- a/instruments/tests/test_qubitekk/test_qubitekk_cc1.py +++ b/instruments/tests/test_qubitekk/test_qubitekk_cc1.py @@ -364,7 +364,7 @@ def test_cc1_subtract_error(): cc.subtract = "blo" -def test_cc1_trigger_mode(): # pylint: disable=redefined-variable-type +def test_cc1_trigger_mode(): with expected_protocol( ik.qubitekk.CC1, [ @@ -386,7 +386,7 @@ def test_cc1_trigger_mode(): # pylint: disable=redefined-variable-type cc.trigger_mode = cc.TriggerMode.start_stop -def test_cc1_trigger_mode_old_firmware(): # pylint: disable=redefined-variable-type +def test_cc1_trigger_mode_old_firmware(): with expected_protocol( ik.qubitekk.CC1, [ diff --git a/instruments/tests/test_srs/test_srs830.py b/instruments/tests/test_srs/test_srs830.py index d94128726..57a612c08 100644 --- a/instruments/tests/test_srs/test_srs830.py +++ b/instruments/tests/test_srs/test_srs830.py @@ -125,7 +125,7 @@ def test_sample_rate(): # sends index of VALID_SAMPLE_RATES assert inst.sample_rate == 16 * pq.Hz assert inst.sample_rate == "trigger" inst.sample_rate = 2 - inst.sample_rate = "trigger" # pylint: disable=redefined-variable-type + inst.sample_rate = "trigger" @raises(ValueError) diff --git a/instruments/thorlabs/_packets.py b/instruments/thorlabs/_packets.py index 83c6decb9..7a9ef8ded 100644 --- a/instruments/thorlabs/_packets.py +++ b/instruments/thorlabs/_packets.py @@ -138,11 +138,11 @@ def pack(self): self._message_id, len( self._data), 0x80 | self._dest, self._source ) + self._data - else: - return message_header_nopacket.pack( - self._message_id, self._param1, self._param2, self._dest, - self._source - ) + + return message_header_nopacket.pack( + self._message_id, self._param1, self._param2, self._dest, + self._source + ) @classmethod def unpack(cls, bytes): diff --git a/instruments/thorlabs/tc200.py b/instruments/thorlabs/tc200.py index 0589de9ee..4478c80d8 100644 --- a/instruments/thorlabs/tc200.py +++ b/instruments/thorlabs/tc200.py @@ -292,8 +292,8 @@ def degrees(self): return pq.degC elif (response >> 5) % 2: return pq.degK - else: - return pq.degF + + return pq.degF @degrees.setter def degrees(self, newval): diff --git a/instruments/thorlabs/thorlabs_utils.py b/instruments/thorlabs/thorlabs_utils.py index d5514d0a6..5969afa1d 100644 --- a/instruments/thorlabs/thorlabs_utils.py +++ b/instruments/thorlabs/thorlabs_utils.py @@ -1,26 +1,5 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# -# thorlabs_utils.py: Utility functions for Thorlabs-brand instruments. -# -# © 2016 Steven Casagrande (scasagrande@galvant.ca). -# -# This file is a part of the InstrumentKit project. -# Licensed under the AGPL version 3. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# """ Contains common utility functions for Thorlabs-brand instruments """ @@ -35,7 +14,8 @@ def check_cmd(response): :return: 1 if not found, 0 otherwise :rtype: int """ - if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID": - return 1 - else: - return 0 + return ( + 1 + if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID" + else 0 + ) diff --git a/instruments/toptica/topmode.py b/instruments/toptica/topmode.py index e50607b84..b12978a40 100644 --- a/instruments/toptica/topmode.py +++ b/instruments/toptica/topmode.py @@ -48,8 +48,8 @@ def _ack_expected(self, msg=""): return [msg, "reboot process started."] elif "start-correction" in msg: return [msg, "()"] - else: - return msg + + return msg # ENUMS # @@ -326,7 +326,7 @@ def set(self, param, value): if isinstance(value, str): self.query("(param-set! '{} \"{}\")".format(param, value)) - elif isinstance(value, tuple) or isinstance(value, list): + elif isinstance(value, (tuple, list)): self.query("(param-set! '{} '({}))".format(param, " ".join(value))) elif isinstance(value, bool): value = "t" if value else "f" diff --git a/instruments/toptica/toptica_utils.py b/instruments/toptica/toptica_utils.py index ead664049..0ffb424e5 100644 --- a/instruments/toptica/toptica_utils.py +++ b/instruments/toptica/toptica_utils.py @@ -37,5 +37,5 @@ def convert_toptica_datetime(response): """ if response.find('""') >= 0: return None - else: - return datetime.strptime(response, "%Y-%m-%d %H:%M:%S") + + return datetime.strptime(response, "%Y-%m-%d %H:%M:%S") diff --git a/instruments/util_fns.py b/instruments/util_fns.py index 977bfdcf3..fad59455e 100644 --- a/instruments/util_fns.py +++ b/instruments/util_fns.py @@ -151,8 +151,9 @@ def split_unit_str(s, default_units=pq.dimensionless, lookup=None): if units is None: return float(val), default_units - else: - return float(val), lookup(units) + + return float(val), lookup(units) + else: try: return float(s), default_units @@ -179,8 +180,8 @@ def rproperty(fget=None, fset=None, doc=None, readonly=False, writeonly=False): return property(fget=fget, fset=None, doc=doc) elif writeonly: return property(fget=None, fset=fset, doc=doc) - else: - return property(fget=fget, fset=fset, doc=doc) + + return property(fget=fget, fset=fset, doc=doc) def bool_property(name, inst_true, inst_false, doc=None, readonly=False, @@ -480,14 +481,14 @@ def bounded_unitful_property(name, units, min_fmt_str="{}:MIN?", def _min_getter(self): if valid_range[0] == "query": return pq.Quantity(*split_unit_str(self.query(min_fmt_str.format(name)), units)) - else: - return assume_units(valid_range[0], units).rescale(units) + + return assume_units(valid_range[0], units).rescale(units) def _max_getter(self): if valid_range[1] == "query": return pq.Quantity(*split_unit_str(self.query(max_fmt_str.format(name)), units)) - else: - return assume_units(valid_range[1], units).rescale(units) + + return assume_units(valid_range[1], units).rescale(units) new_range = ( None if valid_range[0] is None else _min_getter, diff --git a/instruments/yokogawa/yokogawa7651.py b/instruments/yokogawa/yokogawa7651.py index c5779ab06..3d7d03e08 100644 --- a/instruments/yokogawa/yokogawa7651.py +++ b/instruments/yokogawa/yokogawa7651.py @@ -36,9 +36,6 @@ class Yokogawa7651(PowerSupply, Instrument): >>> inst.voltage = 10 * pq.V """ - def __init__(self, filelike): - super(Yokogawa7651, self).__init__(filelike) - # INNER CLASSES # class Channel(PowerSupplyChannel): From 3316327723a58f29ad9c21d1524358df3e49d29a Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Tue, 6 Jun 2017 23:16:44 -0400 Subject: [PATCH 3/8] Update supported Python versions (#170) * Add Py36 and drop Py33 * Update readme * Revert adding version constraint to numpy --- .travis.yml | 2 +- README.rst | 6 +++--- setup.py | 2 +- tox.ini | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed6d2479f..a076dfaaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,9 @@ sudo: false language: python python: - "2.7" - - "3.3" - "3.4" - "3.5" + - "3.6" install: - "pip install -r requirements.txt" - "pip install -r dev-requirements.txt" diff --git a/README.rst b/README.rst index 67bbd6c3c..d9e318113 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ InstrumentKit ============= -.. image:: https://img.shields.io/travis/Galvant/InstrumentKit.svg?maxAge=2592000 +.. image:: https://img.shields.io/travis/Galvant/InstrumentKit/master.svg?maxAge=2592000 :target: https://travis-ci.org/Galvant/InstrumentKit :alt: Travis-CI build status @@ -111,7 +111,7 @@ send, one can use the following functions to do so: Python Version Compatibility ---------------------------- -At this time, Python 2.7, 3.3, 3.4, and 3.5 are supported. Should you encounter +At this time, Python 2.7, 3.4, 3.5, and 3.6 are supported. Should you encounter any problems with this library that occur in one version or another, please do not hesitate to let us know. @@ -134,7 +134,7 @@ To run the tests against all supported version of Python, you will need to have the binary for each installed, as well as any requirements needed to install ``numpy`` under each Python version. On Debian/Ubuntu systems this means you will need to install the ``python-dev`` package for each version of Python -supported (``python2.7-dev``, ``python3.3-dev``, etc). +supported (``python2.7-dev``, ``python3.4-dev``, etc). With the required system packages installed, all tests can be run with ``tox``: diff --git a/setup.py b/setup.py index b422efd4b..e658c45b8 100644 --- a/setup.py +++ b/setup.py @@ -23,9 +23,9 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Operating System :: OS Independent", "License :: OSI Approved :: GNU Affero General Public License v3", "Intended Audience :: Science/Research", diff --git a/tox.ini b/tox.ini index ae996789c..a6bf9ea2e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py33,py34,py35 +envlist = py27,py34,py35,py36 [testenv] deps = -rdev-requirements.txt commands = nosetests From cf711163fa11b620fa5d65b910079dc0362724b3 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Fri, 9 Jun 2017 18:46:44 -0400 Subject: [PATCH 4/8] Pin ruamel version (#171) * Use ruamel.yaml 0.14 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 622bb14d0..30a1a88d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ enum34 python-vxi11>=0.8 pyusb python-usbtmc -ruamel.yaml +ruamel.yaml~=0.14.12 From 363de47d7941cd83953728b3c4fd4abd6511f81d Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Fri, 9 Jun 2017 19:31:53 -0400 Subject: [PATCH 5/8] Add set_cmd to property factories (#172) --- instruments/agilent/agilent33220a.py | 12 +- .../generic_scpi/scpi_function_generator.py | 6 +- instruments/generic_scpi/scpi_multimeter.py | 10 +- instruments/lakeshore/lakeshore475.py | 2 +- instruments/picowatt/picowattavs47.py | 10 +- instruments/qubitekk/mc1.py | 10 +- instruments/srs/srs345.py | 8 +- .../test_bool_property.py | 26 ++- .../test_bounded_unitful_property.py | 4 +- .../test_enum_property.py | 15 ++ .../test_int_property.py | 12 ++ .../test_string_property.py | 12 ++ .../test_unitful_property.py | 15 ++ .../test_unitless_property.py | 12 ++ instruments/thorlabs/lcc25.py | 12 +- instruments/thorlabs/sc10.py | 12 +- instruments/util_fns.py | 150 +++++++++++++----- 17 files changed, 241 insertions(+), 87 deletions(-) diff --git a/instruments/agilent/agilent33220a.py b/instruments/agilent/agilent33220a.py index 5a24ea9ad..8afb868cf 100644 --- a/instruments/agilent/agilent33220a.py +++ b/instruments/agilent/agilent33220a.py @@ -87,7 +87,7 @@ def frequency(self, newval): super(Agilent33220a, self).frequency = newval function = enum_property( - name="FUNC", + command="FUNC", enum=Function, doc=""" Gets/sets the output function of the function generator @@ -98,7 +98,7 @@ def frequency(self, newval): ) duty_cycle = int_property( - name="FUNC:SQU:DCYC", + command="FUNC:SQU:DCYC", doc=""" Gets/sets the duty cycle of a square wave. @@ -111,7 +111,7 @@ def frequency(self, newval): ) ramp_symmetry = int_property( - name="FUNC:RAMP:SYMM", + command="FUNC:RAMP:SYMM", doc=""" Gets/sets the ramp symmetry for ramp waves. @@ -124,7 +124,7 @@ def frequency(self, newval): ) output = bool_property( - name="OUTP", + command="OUTP", inst_true="ON", inst_false="OFF", doc=""" @@ -138,7 +138,7 @@ def frequency(self, newval): ) output_sync = bool_property( - name="OUTP:SYNC", + command="OUTP:SYNC", inst_true="ON", inst_false="OFF", doc=""" @@ -149,7 +149,7 @@ def frequency(self, newval): ) output_polarity = enum_property( - name="OUTP:POL", + command="OUTP:POL", enum=OutputPolarity, doc=""" Gets/sets the polarity of the waveform relative to the offset voltage. diff --git a/instruments/generic_scpi/scpi_function_generator.py b/instruments/generic_scpi/scpi_function_generator.py index 66630006a..4877168ae 100644 --- a/instruments/generic_scpi/scpi_function_generator.py +++ b/instruments/generic_scpi/scpi_function_generator.py @@ -75,7 +75,7 @@ def _set_amplitude_(self, magnitude, units): # PROPERTIES # frequency = unitful_property( - name="FREQ", + command="FREQ", units=pq.Hz, doc=""" Gets/sets the output frequency. @@ -86,7 +86,7 @@ def _set_amplitude_(self, magnitude, units): ) function = enum_property( - name="FUNC", + command="FUNC", enum=lambda: Function, # pylint: disable=undefined-variable doc=""" Gets/sets the output function of the function generator @@ -96,7 +96,7 @@ def _set_amplitude_(self, magnitude, units): ) offset = unitful_property( - name="VOLT:OFFS", + command="VOLT:OFFS", units=pq.volt, doc=""" Gets/sets the offset voltage of the function generator. diff --git a/instruments/generic_scpi/scpi_multimeter.py b/instruments/generic_scpi/scpi_multimeter.py index b6cc062fd..1a273f7de 100644 --- a/instruments/generic_scpi/scpi_multimeter.py +++ b/instruments/generic_scpi/scpi_multimeter.py @@ -141,7 +141,7 @@ class SampleSource(Enum): # pylint: disable=unnecessary-lambda,undefined-variable mode = enum_property( - name="CONF", + command="CONF", enum=Mode, doc=""" Gets/sets the current measurement mode for the multimeter. @@ -157,7 +157,7 @@ class SampleSource(Enum): ) trigger_mode = enum_property( - name="TRIG:SOUR", + command="TRIG:SOUR", enum=TriggerMode, doc=""" Gets/sets the SCPI Multimeter trigger mode. @@ -317,7 +317,7 @@ def sample_count(self, newval): self.sendcmd("SAMP:COUN {}".format(newval)) trigger_delay = unitful_property( - name="TRIG:DEL", + command="TRIG:DEL", units=pq.second, doc=""" Gets/sets the time delay which the multimeter will use following @@ -329,7 +329,7 @@ def sample_count(self, newval): ) sample_source = enum_property( - name="SAMP:SOUR", + command="SAMP:SOUR", enum=SampleSource, doc=""" Gets/sets the multimeter sample source. This determines whether the @@ -344,7 +344,7 @@ def sample_count(self, newval): ) sample_timer = unitful_property( - name="SAMP:TIM", + command="SAMP:TIM", units=pq.second, doc=""" Gets/sets the sample interval when the sample counter is greater than diff --git a/instruments/lakeshore/lakeshore475.py b/instruments/lakeshore/lakeshore475.py index 86d02fb59..0db502339 100644 --- a/instruments/lakeshore/lakeshore475.py +++ b/instruments/lakeshore/lakeshore475.py @@ -269,7 +269,7 @@ def control_slope_limit(self, newval): self.field_control_params = tuple(values) control_mode = bool_property( - name="CMODE", + command="CMODE", inst_true="1", inst_false="0", doc=""" diff --git a/instruments/picowatt/picowattavs47.py b/instruments/picowatt/picowattavs47.py index c255b8d28..acf1d8d0f 100644 --- a/instruments/picowatt/picowattavs47.py +++ b/instruments/picowatt/picowattavs47.py @@ -97,7 +97,7 @@ def sensor(self): return ProxyList(self, PicowattAVS47.Sensor, range(8)) remote = bool_property( - name="REM", + command="REM", inst_true="1", inst_false="0", doc=""" @@ -111,7 +111,7 @@ def sensor(self): ) input_source = enum_property( - name="INP", + command="INP", enum=InputSource, input_decoration=int, doc=""" @@ -122,7 +122,7 @@ def sensor(self): ) mux_channel = int_property( - name="MUX", + command="MUX", doc=""" Gets/sets the multiplexer sensor number. It is recommended that you ground the input before switching the @@ -136,7 +136,7 @@ def sensor(self): ) excitation = int_property( - name="EXC", + command="EXC", doc=""" Gets/sets the excitation sensor number. @@ -148,7 +148,7 @@ def sensor(self): ) display = int_property( - name="DIS", + command="DIS", doc=""" Gets/sets the sensor that is displayed on the front panel. diff --git a/instruments/qubitekk/mc1.py b/instruments/qubitekk/mc1.py index 98b353962..facc54eab 100644 --- a/instruments/qubitekk/mc1.py +++ b/instruments/qubitekk/mc1.py @@ -91,7 +91,7 @@ def upper_limit(self, newval): self._upper_limit = assume_units(newval, pq.ms).rescale(pq.ms) direction = unitful_property( - name="DIRE", + command="DIRE", doc=""" Get the internal direction variable, which is a function of how far the motor needs to go. @@ -104,7 +104,7 @@ def upper_limit(self, newval): ) inertia = unitful_property( - name="INER", + command="INER", doc=""" Gets/Sets the amount of force required to overcome static inertia. Must be between 0 and 100 milliseconds. @@ -133,7 +133,7 @@ def internal_position(self): return response metric_position = unitful_property( - name="METR", + command="METR", doc=""" Get the estimated motor position, in millimeters. @@ -145,7 +145,7 @@ def internal_position(self): ) setting = int_property( - name="OUTP", + command="OUTP", doc=""" Gets/sets the output port of the optical switch. 0 means input 1 is directed to output 1, and input 2 is directed to output 2. 1 means that @@ -158,7 +158,7 @@ def internal_position(self): ) step_size = unitful_property( - name="STEP", + command="STEP", doc=""" Gets/Sets the number of milliseconds per step. Must be between 1 and 100 milliseconds. diff --git a/instruments/srs/srs345.py b/instruments/srs/srs345.py index a344a4b5d..c86de3c63 100644 --- a/instruments/srs/srs345.py +++ b/instruments/srs/srs345.py @@ -78,7 +78,7 @@ class Function(IntEnum): # PROPERTIES ## frequency = unitful_property( - name="FREQ", + command="FREQ", units=pq.Hz, doc=""" Gets/sets the output frequency. @@ -89,7 +89,7 @@ class Function(IntEnum): ) function = enum_property( - name="FUNC", + command="FUNC", enum=Function, input_decoration=int, doc=""" @@ -100,7 +100,7 @@ class Function(IntEnum): ) offset = unitful_property( - name="OFFS", + command="OFFS", units=pq.volt, doc=""" Gets/sets the offset voltage for the output waveform. @@ -111,7 +111,7 @@ class Function(IntEnum): ) phase = unitful_property( - name="PHSE", + command="PHSE", units=pq.degree, doc=""" Gets/sets the phase for the output waveform. diff --git a/instruments/tests/test_property_factories/test_bool_property.py b/instruments/tests/test_property_factories/test_bool_property.py index 9fc36fc19..01995485e 100644 --- a/instruments/tests/test_property_factories/test_bool_property.py +++ b/instruments/tests/test_property_factories/test_bool_property.py @@ -20,8 +20,8 @@ def test_bool_property_basics(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF') - mock2 = bool_property('MOCK2', 'YES', 'NO') + mock1 = bool_property('MOCK1') + mock2 = bool_property('MOCK2', inst_true='YES', inst_false='NO') mock_inst = BoolMock({'MOCK1?': 'OFF', 'MOCK2?': 'YES'}) @@ -36,7 +36,7 @@ class BoolMock(MockInstrument): def test_bool_property_set_fmt(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', set_fmt="{}={}") + mock1 = bool_property('MOCK1', set_fmt="{}={}") mock_instrument = BoolMock({'MOCK1?': 'OFF'}) @@ -48,7 +48,7 @@ class BoolMock(MockInstrument): @raises(AttributeError) def test_bool_property_readonly_writing_fails(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', readonly=True) + mock1 = bool_property('MOCK1', readonly=True) mock_instrument = BoolMock({'MOCK1?': 'OFF'}) @@ -57,7 +57,7 @@ class BoolMock(MockInstrument): def test_bool_property_readonly_reading_passes(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', readonly=True) + mock1 = bool_property('MOCK1', readonly=True) mock_instrument = BoolMock({'MOCK1?': 'OFF'}) @@ -67,7 +67,7 @@ class BoolMock(MockInstrument): @raises(AttributeError) def test_bool_property_writeonly_reading_fails(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', writeonly=True) + mock1 = bool_property('MOCK1', writeonly=True) mock_instrument = BoolMock({'MOCK1?': 'OFF'}) @@ -76,8 +76,20 @@ class BoolMock(MockInstrument): def test_bool_property_writeonly_writing_passes(): class BoolMock(MockInstrument): - mock1 = bool_property('MOCK1', 'ON', 'OFF', writeonly=True) + mock1 = bool_property('MOCK1', writeonly=True) mock_instrument = BoolMock({'MOCK1?': 'OFF'}) mock_instrument.mock1 = False + + +def test_bool_property_set_cmd(): + class BoolMock(MockInstrument): + mock1 = bool_property('MOCK1', set_cmd='FOOBAR') + + mock_inst = BoolMock({'MOCK1?': 'OFF'}) + + eq_(mock_inst.mock1, False) + mock_inst.mock1 = True + + eq_(mock_inst.value, 'MOCK1?\nFOOBAR ON\n') diff --git a/instruments/tests/test_property_factories/test_bounded_unitful_property.py b/instruments/tests/test_property_factories/test_bounded_unitful_property.py index 543320c5c..d982dc989 100644 --- a/instruments/tests/test_property_factories/test_bounded_unitful_property.py +++ b/instruments/tests/test_property_factories/test_bounded_unitful_property.py @@ -124,7 +124,7 @@ class BoundedUnitfulMock(MockInstrument): @mock.patch("instruments.util_fns.unitful_property") def test_bounded_unitful_property_passes_kwargs(mock_unitful_property): bounded_unitful_property( - name='MOCK', + command='MOCK', units=pq.Hz, derp="foobar" ) @@ -139,7 +139,7 @@ def test_bounded_unitful_property_passes_kwargs(mock_unitful_property): @mock.patch("instruments.util_fns.unitful_property") def test_bounded_unitful_property_valid_range_none(mock_unitful_property): bounded_unitful_property( - name='MOCK', + command='MOCK', units=pq.Hz, valid_range=(None, None) ) diff --git a/instruments/tests/test_property_factories/test_enum_property.py b/instruments/tests/test_property_factories/test_enum_property.py index 6384e7305..862a55df2 100644 --- a/instruments/tests/test_property_factories/test_enum_property.py +++ b/instruments/tests/test_property_factories/test_enum_property.py @@ -196,3 +196,18 @@ class EnumMock(MockInstrument): eq_(mock_instrument.a, SillyEnum.a) eq_(mock_instrument.value, 'MOCK:A?\n') + + +def test_enum_property_set_cmd(): + class SillyEnum(Enum): + a = 'aa' + + class EnumMock(MockInstrument): + a = enum_property('MOCK:A', SillyEnum, set_cmd='FOOBAR:A') + + mock_inst = EnumMock({'MOCK:A?': 'aa'}) + + eq_(mock_inst.a, SillyEnum.a) + mock_inst.a = SillyEnum.a + + eq_(mock_inst.value, 'MOCK:A?\nFOOBAR:A aa\n') diff --git a/instruments/tests/test_property_factories/test_int_property.py b/instruments/tests/test_property_factories/test_int_property.py index b0bec2064..1a9ab2340 100644 --- a/instruments/tests/test_property_factories/test_int_property.py +++ b/instruments/tests/test_property_factories/test_int_property.py @@ -97,3 +97,15 @@ class IntMock(MockInstrument): mock_inst.int_property = 1 eq_(mock_inst.value, 'MOCK {:e}\n'.format(1)) + + +def test_int_property_set_cmd(): + class IntMock(MockInstrument): + int_property = int_property('MOCK', set_cmd='FOOBAR') + + mock_inst = IntMock({'MOCK?': '1'}) + + eq_(mock_inst.int_property, 1) + mock_inst.int_property = 1 + + eq_(mock_inst.value, 'MOCK?\nFOOBAR 1\n') diff --git a/instruments/tests/test_property_factories/test_string_property.py b/instruments/tests/test_property_factories/test_string_property.py index d1c94554d..e30e5998f 100644 --- a/instruments/tests/test_property_factories/test_string_property.py +++ b/instruments/tests/test_property_factories/test_string_property.py @@ -52,3 +52,15 @@ class StringMock(MockInstrument): mock_inst.mock_property = 'foo' eq_(mock_inst.value, 'MOCK?\nMOCK foo\n') + + +def test_string_property_set_cmd(): + class StringMock(MockInstrument): + mock_property = string_property('MOCK', set_cmd='FOOBAR') + + mock_inst = StringMock({'MOCK?': '"derp"'}) + + eq_(mock_inst.mock_property, 'derp') + + mock_inst.mock_property = 'qwerty' + eq_(mock_inst.value, 'MOCK?\nFOOBAR "qwerty"\n') diff --git a/instruments/tests/test_property_factories/test_unitful_property.py b/instruments/tests/test_property_factories/test_unitful_property.py index 551d28d42..9d146236b 100644 --- a/instruments/tests/test_property_factories/test_unitful_property.py +++ b/instruments/tests/test_property_factories/test_unitful_property.py @@ -242,3 +242,18 @@ class UnitfulMock(MockInstrument): value = mock_inst.unitful_property assert value.magnitude == 1000 assert value.units == pq.hertz + + +def test_unitful_property_name_read_not_none(): + class UnitfulMock(MockInstrument): + a = unitful_property( + 'MOCK', + units=pq.hertz, + set_cmd='FOOBAR' + ) + + mock_inst = UnitfulMock({'MOCK?': '1000'}) + eq_(mock_inst.a, 1000 * pq.hertz) + mock_inst.a = 1000 * pq.hertz + + eq_(mock_inst.value, 'MOCK?\nFOOBAR {:e}\n'.format(1000)) diff --git a/instruments/tests/test_property_factories/test_unitless_property.py b/instruments/tests/test_property_factories/test_unitless_property.py index 16654d3b5..7698f2aaf 100644 --- a/instruments/tests/test_property_factories/test_unitless_property.py +++ b/instruments/tests/test_property_factories/test_unitless_property.py @@ -88,3 +88,15 @@ class UnitlessMock(MockInstrument): mock_inst = UnitlessMock({'MOCK?': '1'}) eq_(mock_inst.mock_property, 1) + + +def test_unitless_property_set_cmd(): + class UnitlessMock(MockInstrument): + mock_property = unitless_property('MOCK', set_cmd='FOOBAR') + + mock_inst = UnitlessMock({'MOCK?': '1'}) + + eq_(mock_inst.mock_property, 1) + mock_inst.mock_property = 1 + + eq_(mock_inst.value, 'MOCK?\nFOOBAR {:e}\n'.format(1)) diff --git a/instruments/thorlabs/lcc25.py b/instruments/thorlabs/lcc25.py index a44211507..2989283d9 100644 --- a/instruments/thorlabs/lcc25.py +++ b/instruments/thorlabs/lcc25.py @@ -95,8 +95,8 @@ def name(self): enable = bool_property( "enable", - "1", - "0", + inst_true="1", + inst_false="0", set_fmt="{}={}", doc=""" Gets/sets the output enable status. @@ -109,8 +109,8 @@ def name(self): extern = bool_property( "extern", - "1", - "0", + inst_true="1", + inst_false="0", set_fmt="{}={}", doc=""" Gets/sets the use of the external TTL modulation. @@ -124,8 +124,8 @@ def name(self): remote = bool_property( "remote", - "1", - "0", + inst_true="1", + inst_false="0", set_fmt="{}={}", doc=""" Gets/sets front panel lockout status for remote instrument operation. diff --git a/instruments/thorlabs/sc10.py b/instruments/thorlabs/sc10.py index 135d65b3c..cfce252b4 100644 --- a/instruments/thorlabs/sc10.py +++ b/instruments/thorlabs/sc10.py @@ -67,8 +67,8 @@ def name(self): enable = bool_property( "ens", - "1", - "0", + inst_true="1", + inst_false="0", set_fmt="{}={}", doc=""" Gets/sets the shutter enable status, False for disabled, True if @@ -182,8 +182,8 @@ def baud_rate(self, newval): closed = bool_property( "closed", - "1", - "0", + inst_true="1", + inst_false="0", readonly=True, doc=""" Gets the shutter closed status. @@ -197,8 +197,8 @@ def baud_rate(self, newval): interlock = bool_property( "interlock", - "1", - "0", + inst_true="1", + inst_false="0", readonly=True, doc=""" Gets the interlock tripped status. diff --git a/instruments/util_fns.py b/instruments/util_fns.py index fad59455e..d0430170a 100644 --- a/instruments/util_fns.py +++ b/instruments/util_fns.py @@ -20,6 +20,7 @@ # FUNCTIONS ################################################################### + # pylint: disable=too-many-arguments def assume_units(value, units): """ @@ -39,6 +40,7 @@ def assume_units(value, units): value = pq.Quantity(value, units) return value + def setattr_expression(target, name_expr, value): """ Recursively calls getattr/setattr for attribute @@ -184,16 +186,29 @@ def rproperty(fget=None, fset=None, doc=None, readonly=False, writeonly=False): return property(fget=fget, fset=fset, doc=doc) -def bool_property(name, inst_true, inst_false, doc=None, readonly=False, - writeonly=False, set_fmt="{} {}"): +def bool_property(command, set_cmd=None, inst_true="ON", inst_false="OFF", + doc=None, readonly=False, writeonly=False, set_fmt="{} {}"): """ Called inside of SCPI classes to instantiate boolean properties of the device cleanly. For example: - >>> my_property = bool_property("BEST:PROPERTY", "ON", "OFF") # doctest: +SKIP - - :param str name: Name of the SCPI command corresponding to this property. + >>> my_property = bool_property( + ... "BEST:PROPERTY", + ... inst_true="ON", + ... inst_false="OFF" + ... ) # doctest: +SKIP + + This will result in "BEST:PROPERTY ON" or "BEST:PROPERTY OFF" being sent + when setting, and "BEST:PROPERTY?" being sent when getting. + + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str inst_true: String returned and accepted by the instrument for `True` values. :param str inst_false: String returned and accepted by the instrument for @@ -210,19 +225,22 @@ def bool_property(name, inst_true, inst_false, doc=None, readonly=False, """ def _getter(self): - return self.query(name + "?").strip() == inst_true + return self.query(command + "?").strip() == inst_true def _setter(self, newval): if not isinstance(newval, bool): raise TypeError("Bool properties must be specified with a " "boolean value") - self.sendcmd(set_fmt.format(name, inst_true if newval else inst_false)) + self.sendcmd(set_fmt.format( + command if set_cmd is None else set_cmd, + inst_true if newval else inst_false + )) return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly) -def enum_property(name, enum, doc=None, input_decoration=None, +def enum_property(command, enum, set_cmd=None, doc=None, input_decoration=None, output_decoration=None, readonly=False, writeonly=False, set_fmt="{} {}"): """ @@ -234,7 +252,13 @@ def enum_property(name, enum, doc=None, input_decoration=None, Example: my_property = bool_property("BEST:PROPERTY", enum_class) - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param type enum: Class derived from `Enum` representing valid values. :param callable input_decoration: Function called on responses from the instrument before passing to user code. @@ -249,6 +273,10 @@ def enum_property(name, enum, doc=None, input_decoration=None, non-query to the instrument. The default is "{} {}" which places a space between the SCPI command the associated parameter. By switching to "{}={}" an equals sign would instead be used as the separator. + :param str get_cmd: If not `None`, this parameter sets the command string + to be used when reading/querying from the instrument. If used, the name + parameter is still used to set the command for pure-write commands to + the instrument. """ def _in_decor_fcn(val): if input_decoration is None: @@ -265,7 +293,7 @@ def _out_decor_fcn(val): return output_decoration(val) def _getter(self): - return enum(_in_decor_fcn(self.query("{}?".format(name)).strip())) + return enum(_in_decor_fcn(self.query("{}?".format(command)).strip())) def _setter(self, newval): try: # First assume newval is Enum.value @@ -275,19 +303,28 @@ def _setter(self, newval): newval = enum(newval) except ValueError: raise ValueError("Enum property new value not in enum.") - self.sendcmd(set_fmt.format(name, _out_decor_fcn(enum(newval).value))) + self.sendcmd(set_fmt.format( + command if set_cmd is None else set_cmd, + _out_decor_fcn(enum(newval).value) + )) return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly) -def unitless_property(name, format_code='{:e}', doc=None, readonly=False, - writeonly=False, set_fmt="{} {}"): +def unitless_property(command, set_cmd=None, format_code='{:e}', doc=None, + readonly=False, writeonly=False, set_fmt="{} {}"): """ Called inside of SCPI classes to instantiate properties with unitless numeric values. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str format_code: Argument to `str.format` used in sending values to the instrument. :param str doc: Docstring to be associated with the new property. @@ -302,7 +339,7 @@ def unitless_property(name, format_code='{:e}', doc=None, readonly=False, """ def _getter(self): - raw = self.query("{}?".format(name)) + raw = self.query("{}?".format(command)) return float(raw) def _setter(self, newval): @@ -312,19 +349,29 @@ def _setter(self, newval): else: raise ValueError strval = format_code.format(newval) - self.sendcmd(set_fmt.format(name, strval)) + self.sendcmd(set_fmt.format( + command if set_cmd is None else set_cmd, + strval + )) return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly) -def int_property(name, format_code='{:d}', doc=None, readonly=False, - writeonly=False, valid_set=None, set_fmt="{} {}"): +def int_property(command, set_cmd=None, format_code='{:d}', doc=None, + readonly=False, writeonly=False, valid_set=None, + set_fmt="{} {}"): """ Called inside of SCPI classes to instantiate properties with unitless numeric values. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str format_code: Argument to `str.format` used in sending values to the instrument. :param str doc: Docstring to be associated with the new property. @@ -341,12 +388,15 @@ def int_property(name, format_code='{:d}', doc=None, readonly=False, """ def _getter(self): - raw = self.query("{}?".format(name)) + raw = self.query("{}?".format(command)) return int(raw) if valid_set is None: def _setter(self, newval): strval = format_code.format(newval) - self.sendcmd(set_fmt.format(name, strval)) + self.sendcmd(set_fmt.format( + command if set_cmd is None else set_cmd, + strval + )) else: def _setter(self, newval): if newval not in valid_set: @@ -355,13 +405,16 @@ def _setter(self, newval): "must be one of {}.".format(newval, valid_set) ) strval = format_code.format(newval) - self.sendcmd(set_fmt.format(name, strval)) + self.sendcmd(set_fmt.format( + command if set_cmd is None else set_cmd, + strval + )) return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly) -def unitful_property(name, units, format_code='{:e}', doc=None, +def unitful_property(command, units, set_cmd=None, format_code='{:e}', doc=None, input_decoration=None, output_decoration=None, readonly=False, writeonly=False, set_fmt="{} {}", valid_range=(None, None)): @@ -373,7 +426,13 @@ def unitful_property(name, units, format_code='{:e}', doc=None, for instruments where the units can change dynamically due to front-panel interaction or due to remote commands. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param units: Units to assume in sending and receiving magnitudes to and from the instrument. :param str format_code: Argument to `str.format` used in sending the @@ -413,7 +472,7 @@ def _out_decor_fcn(val): return output_decoration(val) def _getter(self): - raw = _in_decor_fcn(self.query("{}?".format(name))) + raw = _in_decor_fcn(self.query("{}?".format(command))) return pq.Quantity(*split_unit_str(raw, units)).rescale(units) def _setter(self, newval): @@ -434,13 +493,16 @@ def _setter(self, newval): # catch bad units. strval = format_code.format( assume_units(newval, units).rescale(units).item()) - self.sendcmd(set_fmt.format(name, _out_decor_fcn(strval))) + self.sendcmd(set_fmt.format( + command if set_cmd is None else set_cmd, + _out_decor_fcn(strval) + )) return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly) -def bounded_unitful_property(name, units, min_fmt_str="{}:MIN?", +def bounded_unitful_property(command, units, min_fmt_str="{}:MIN?", max_fmt_str="{}:MAX?", valid_range=("query", "query"), **kwargs): """ @@ -454,7 +516,13 @@ def bounded_unitful_property(name, units, min_fmt_str="{}:MIN?", the one created by `unitful_property`, one for the minimum value, and one for the maximum value. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param units: Units to assume in sending and receiving magnitudes to and from the instrument. :param str min_fmt_str: Specify the string format to use when sending a @@ -480,13 +548,13 @@ def bounded_unitful_property(name, units, min_fmt_str="{}:MIN?", def _min_getter(self): if valid_range[0] == "query": - return pq.Quantity(*split_unit_str(self.query(min_fmt_str.format(name)), units)) + return pq.Quantity(*split_unit_str(self.query(min_fmt_str.format(command)), units)) return assume_units(valid_range[0], units).rescale(units) def _max_getter(self): if valid_range[1] == "query": - return pq.Quantity(*split_unit_str(self.query(max_fmt_str.format(name)), units)) + return pq.Quantity(*split_unit_str(self.query(max_fmt_str.format(command)), units)) return assume_units(valid_range[1], units).rescale(units) @@ -496,18 +564,24 @@ def _max_getter(self): ) return ( - unitful_property(name, units, valid_range=new_range, **kwargs), + unitful_property(command, units, valid_range=new_range, **kwargs), property(_min_getter) if valid_range[0] is not None else None, property(_max_getter) if valid_range[1] is not None else None ) -def string_property(name, bookmark_symbol='"', doc=None, readonly=False, - writeonly=False, set_fmt="{} {}{}{}"): +def string_property(command, set_cmd=None, bookmark_symbol='"', doc=None, + readonly=False, writeonly=False, set_fmt="{} {}{}{}"): """ Called inside of SCPI classes to instantiate properties with a string value. - :param str name: Name of the SCPI command corresponding to this property. + :param str command: Name of the SCPI command corresponding to this property. + If parameter set_cmd is not specified, then this parameter is also used + for both getting and setting. + :param str set_cmd: If not `None`, this parameter sets the command string + to be used when sending commands with no return values to the + instrument. This allows for non-symmetric properties that have different + strings for getting vs setting a property. :param str doc: Docstring to be associated with the new property. :param bool readonly: If `True`, the returned property does not have a setter. @@ -523,14 +597,16 @@ def string_property(name, bookmark_symbol='"', doc=None, readonly=False, bookmark_length = len(bookmark_symbol) def _getter(self): - string = self.query("{}?".format(name)) + string = self.query("{}?".format(command)) string = string[ bookmark_length:-bookmark_length] if bookmark_length > 0 else string return string def _setter(self, newval): - self.sendcmd( - set_fmt.format(name, bookmark_symbol, newval, bookmark_symbol)) + self.sendcmd(set_fmt.format( + command if set_cmd is None else set_cmd, + bookmark_symbol, newval, bookmark_symbol + )) return rproperty(fget=_getter, fset=_setter, doc=doc, readonly=readonly, writeonly=writeonly) From 215a6d0adb13eaadcb330e5b14d37db604506274 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Sat, 10 Jun 2017 20:29:24 -0400 Subject: [PATCH 6/8] Fix timeout and terminator for USBTMC (#173) --- .../abstract_instruments/comm/usbtmc_communicator.py | 4 ++-- instruments/tests/test_comm/test_usbtmc.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/instruments/abstract_instruments/comm/usbtmc_communicator.py b/instruments/abstract_instruments/comm/usbtmc_communicator.py index f465abba7..88855d4e5 100644 --- a/instruments/abstract_instruments/comm/usbtmc_communicator.py +++ b/instruments/abstract_instruments/comm/usbtmc_communicator.py @@ -54,7 +54,7 @@ def terminator(self): :type: `str` """ - return self._filelike.term_char + return chr(self._filelike.term_char) @terminator.setter def terminator(self, newval): @@ -78,7 +78,7 @@ def timeout(self): @timeout.setter def timeout(self, newval): - newval = assume_units(newval, pq.second).rescale(pq.ms).magnitude + newval = assume_units(newval, pq.second).rescale(pq.s).magnitude self._filelike.timeout = newval # FILE-LIKE METHODS # diff --git a/instruments/tests/test_comm/test_usbtmc.py b/instruments/tests/test_comm/test_usbtmc.py index 18e2ebcf7..da56749af 100644 --- a/instruments/tests/test_comm/test_usbtmc.py +++ b/instruments/tests/test_comm/test_usbtmc.py @@ -40,7 +40,7 @@ def test_usbtmccomm_init_missing_module(): def test_usbtmccomm_terminator_getter(mock_usbtmc): comm = USBTMCCommunicator() - term_char = mock.PropertyMock(return_value="\n") + term_char = mock.PropertyMock(return_value=10) type(comm._filelike).term_char = term_char eq_(comm.terminator, "\n") @@ -74,10 +74,10 @@ def test_usbtmccomm_timeout(mock_usbtmc): timeout.assert_called_with() comm.timeout = 10 - timeout.assert_called_with(array(10000.0)) + timeout.assert_called_with(array(10.0)) comm.timeout = 1000 * pq.millisecond - timeout.assert_called_with(array(1000.0)) + timeout.assert_called_with(array(1.0)) @mock.patch(patch_path) From 12e9a157ac17e2a2d536dd567c70330e8f824dcf Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Sat, 15 Jul 2017 08:00:00 +1000 Subject: [PATCH 7/8] Feature: named structures (#169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial version of named struct class. * Added tests for new class. * docs and a pylint fix. * Py3k fix (filter → list(filter)) * Improvements to string handling. * Ignored additional carp. * Provide fixes for build and start using hypothesis * Locked requirements.txt; see #174. * Fixed py3k compat issue. * Fixed negative length checks. * Fixed bug in StringField.__get__ * Added NamedStruct.__eq__ test. * Fixing pylint warnings. * Moved pylint warning comment. * Changed numpy version in reqs as suggested by @scasagrande --- .gitignore | 9 + dev-requirements.txt | 1 + doc/source/devguide/util_fns.rst | 12 + instruments/named_struct.py | 318 +++++++++++++++++++++++++ instruments/tests/test_named_struct.py | 70 ++++++ requirements.txt | 2 +- 6 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 instruments/named_struct.py create mode 100644 instruments/tests/test_named_struct.py diff --git a/.gitignore b/.gitignore index 3f7a79c94..3d527fd94 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,12 @@ nosetests.xml #pycharm generated .idea + +# VS Code IDE internals +.vscode/ + +# nosetests metadata +.noseids + +# Hypothesis files +.hypothesis/ diff --git a/dev-requirements.txt b/dev-requirements.txt index 419748e1c..825550120 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ mock nose +hypothesis pylint==1.7.1 diff --git a/doc/source/devguide/util_fns.rst b/doc/source/devguide/util_fns.rst index 2d2719a91..9fa06769b 100644 --- a/doc/source/devguide/util_fns.rst +++ b/doc/source/devguide/util_fns.rst @@ -183,3 +183,15 @@ String Property .. autofunction:: string_property +Named Structures +================ + +The :class:`~instruments.named_struct.NamedStruct` class can be used to represent +C-style structures for serializing and deserializing data. + +.. autoclass:: instruments.named_struct.NamedStruct + +.. autoclass:: instruments.named_struct.Field + +.. autoclass:: instruments.named_struct.Padding + diff --git a/instruments/named_struct.py b/instruments/named_struct.py new file mode 100644 index 000000000..ef8522ab7 --- /dev/null +++ b/instruments/named_struct.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Class for quickly defining C-like structures with named fields. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division + +import struct +from collections import OrderedDict + +from future.utils import with_metaclass + +# DESIGN NOTES ################################################################ + +# This class uses the Django-like strategy described at +# http://stackoverflow.com/a/3288988/267841 +# to assign a "birthday" to each Field as it's instantiated. We can thus sort +# each Field in a NamedStruct by its birthday. + +# Notably, this hack is not at all required on Python 3.6: +# https://www.python.org/dev/peps/pep-0520/ + +# TODO: arrays other than string arrays do not currently work. + +# PYLINT CONFIGURATION ######################################################## + +# All of the classes in this module need to interact with each other rather +# deeply, so we disable the protected-access check within this module. + +# pylint:disable=protected-access + +# CLASSES ##################################################################### + + +class Field(object): + """ + A named field within a C-style structure. + + :param str fmt: Format for the field, corresponding to the + documentation of the :mod:`struct` standard library package. + """ + + __n_fields_created = 0 + _field_birthday = None + + _fmt = '' + _name = None + _owner_type = object + + def __init__(self, fmt, strip_null=False): + super(Field, self).__init__() + + # Record our birthday so that we can sort fields later. + self._field_birthday = Field.__n_fields_created + Field.__n_fields_created += 1 + + self._fmt = fmt.strip() + self._strip_null = strip_null + + # If we're given a length, check that it + # makes sense. + if self._fmt[:-1] and int(self._fmt[:-1]) < 0: + raise TypeError("Field is specified with negative length.") + + + def is_significant(self): + return not self._fmt.endswith('x') + + @property + def fmt_char(self): + return self._fmt[-1] + + def __len__(self): + if self._fmt[:-1]: + # Although we know that length > 0, this abs ensures that static + # code checks are happy with __len__ always returning a positive number + return abs(int(self._fmt[:-1])) + + raise TypeError("Field is scalar and has no len().") + + def __repr__(self): + if self._owner_type: + return "".format( + self._name, self._owner_type, self._fmt + ) + + return "".format( + self._fmt + ) + + def __str__(self): + n, fmt_char = len(self), self.fmt_char + c_type = { + 'x': 'char', + 'c': 'char', + 'b': 'char', + 'B': 'unsigned char', + '?': 'bool', + 'h': 'short', + 'H': 'unsigned short', + 'i': 'int', + 'I': 'unsigned int', + 'l': 'long', + 'L': 'unsigned long', + 'q': 'long long', + 'Q': 'unsigned long long', + 'f': 'float', + 'd': 'double', + # NB: no [], since that will be implied by n. + 's': 'char', + 'p': 'char', + 'P': 'void *' + }[fmt_char] + + if n: + c_type = "{}[{}]".format(c_type, n) + return ( + "{c_type} {self._name}".format(c_type=c_type, self=self) + if self.is_significant() + else c_type + ) + + # DESCRIPTOR PROTOCOL # + + def __get__(self, obj, type=None): + return obj._values[self._name] + + def __set__(self, obj, value): + obj._values[self._name] = value + +class StringField(Field): + """ + Represents a field that is interpreted as a Python string. + + :param int length: Maximum allowed length of the field, as + measured in the number of bytes used by its encoding. + Note that if a shorter string is provided, it will + be padded by null bytes. + :param str encoding: Name of an encoding to use in serialization + and deserialization to Python strings. + :param bool strip_null: If `True`, null bytes (``'\x00'``) will + be removed from the right upon deserialization. + """ + + _strip_null = False + _encoding = 'ascii' + + def __init__(self, length, encoding='ascii', strip_null=False): + super(StringField, self).__init__('{}s'.format(length)) + self._strip_null = strip_null + self._encoding = encoding + + def __set__(self, obj, value): + if isinstance(value, bytes): + value = value.decode(self._encoding) + if self._strip_null: + value = value.rstrip('\x00') + value = value.encode(self._encoding) + + super(StringField, self).__set__(obj, value) + + def __get__(self, obj, type=None): + return super(StringField, self).__get__(obj, type=type).decode(self._encoding) + + +class Padding(Field): + """ + Represents a field whose value is insignificant, and will not + be kept in serialization and deserialization. + + :param int n_bytes: Number of padding bytes occupied by this field. + """ + + def __init__(self, n_bytes=1): + super(Padding, self).__init__('{}x'.format(n_bytes)) + +class HasFields(type): + def __new__(mcs, name, bases, attrs): + # Since this is a metaclass, the __new__ method observes + # creation of new *classes* and not new instances. + # We call the superclass of HasFields, which is another + # metaclass, to do most of the heavy lifting of creating + # the new class. + cls = super(HasFields, mcs).__new__(mcs, name, bases, attrs) + + # We now sort the fields by their birthdays and store them in an + # ordered dict for easier look up later. + cls._fields = OrderedDict([ + (field_name, field) + for field_name, field in sorted( + [ + (field_name, field) + for field_name, field in attrs.items() + if isinstance(field, Field) + ], + key=lambda item: item[1]._field_birthday + ) + ]) + + # Assign names and owner types to each field so that they can follow + # the descriptor protocol. + for field_name, field in cls._fields.items(): + field._name = field_name + field._owner_type = cls + + # Associate a struct.Struct instance with the new class + # that defines how to pack/unpack the new type. + cls._struct = struct.Struct( + # TODO: support alignment char at start. + " ".join([ + field._fmt for field in cls._fields.values() + ]) + ) + + return cls + + +class NamedStruct(with_metaclass(HasFields, object)): + """ + Represents a C-style struct with one or more named fields, + useful for packing and unpacking serialized data documented + in terms of C examples. For instance, consider a struct of the + form:: + + typedef struct { + unsigned long a = 0x1234; + char[12] dummy; + unsigned char b = 0xab; + } Foo; + + This struct can be represented as the following NamedStruct:: + + class Foo(NamedStruct): + a = Field('L') + dummy = Padding(12) + b = Field('B') + + foo = Foo(a=0x1234, b=0xab) + """ + + # Provide reasonable defaults for the lowercase-f-fields + # created by HasFields. This will prevent a few edge cases, + # allow type inference and will prevent pylint false positives. + _fields = {} + _struct = None + + def __init__(self, **kwargs): + super(NamedStruct, self).__init__() + self._values = OrderedDict([ + ( + field._name, None + ) + for field in filter(Field.is_significant, self._fields.values()) + ]) + + for field_name, value in kwargs.items(): + setattr(self, field_name, value) + + def _to_seq(self): + return tuple(self._values.values()) + + @classmethod + def _from_seq(cls, new_values): + return cls(**{ + field._name: new_value + for field, new_value in + zip(list(filter(Field.is_significant, cls._fields.values())), new_values) + }) + + def pack(self): + """ + Packs this instance into bytes, suitable for transmitting over + a network or recording to disc. See :func:`struct.pack` for details. + + :return bytes packed_data: A serialized representation of this + instance. + """ + return self._struct.pack(*self._to_seq()) + + @classmethod + def unpack(cls, buffer): + """ + Given a buffer, unpacks it into an instance of this NamedStruct. + See :func:`struct.unpack` for details. + + :param bytes buffer: Data to use in creating a new instance. + :return: The new instance represented by `buffer`. + """ + return cls._from_seq(cls._struct.unpack(buffer)) + + def __eq__(self, other): + if not isinstance(other, NamedStruct): + return False + + return self._values == other._values + + def __hash__(self): + return hash(self._values) + + def __str__(self): + return "{name} {{\n{fields}\n}}".format( + name=type(self).__name__, + fields="\n".join([ + " {field}{value};".format( + field=field, + value=( + " = {}".format(repr(self._values[field._name])) + if field.is_significant() + else "" + ) + ) + for field in self._fields.values() + ]) + ) diff --git a/instruments/tests/test_named_struct.py b/instruments/tests/test_named_struct.py new file mode 100644 index 000000000..adeef46da --- /dev/null +++ b/instruments/tests/test_named_struct.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for named structures. +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import, unicode_literals + +from unittest import TestCase + +from hypothesis import given +import hypothesis.strategies as st + +from instruments.named_struct import ( + Field, StringField, Padding, NamedStruct +) + +# TESTS ###################################################################### + +# We disable pylint warnings that are not as applicable for unit tests. +# pylint: disable=no-member,protected-access,blacklisted-name,missing-docstring,no-self-use + +class TestNamedStruct(TestCase): + @given(st.integers(min_value=0, max_value=0x7FFF*2+1), st.integers(min_value=0, max_value=0xFF)) + def test_roundtrip(self, var1, var2): + class Foo(NamedStruct): + a = Field('H') + padding = Padding(12) + b = Field('B') + + foo = Foo(a=var1, b=var2) + assert Foo.unpack(foo.pack()) == foo + + + def test_str(self): + class Foo(NamedStruct): + a = StringField(8, strip_null=False) + b = StringField(9, strip_null=True) + c = StringField(2, encoding='utf-8') + + foo = Foo(a="0123456\x00", b='abc', c=u'α') + assert Foo.unpack(foo.pack()) == foo + + # Also check that we can get fields out directly. + self.assertEqual(foo.a, '0123456\x00') + self.assertEqual(foo.b, 'abc') + self.assertEqual(foo.c, u'α') + + + def test_negative_len(self): + """ + Checks whether negative field lengths correctly raise. + """ + with self.assertRaises(TypeError): + class Foo(NamedStruct): # pylint: disable=unused-variable + a = StringField(-1) + + def test_equality(self): + class Foo(NamedStruct): + a = Field('H') + b = Field('B') + c = StringField(5, encoding='utf8', strip_null=True) + + foo1 = Foo(a=0x1234, b=0x56, c=u'ω') + foo2 = Foo(a=0xabcd, b=0xef, c=u'α') + + assert foo1 == foo1 + assert foo1 != foo2 diff --git a/requirements.txt b/requirements.txt index 30a1a88d6..89e22a811 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy +numpy<1.13.0 pyserial quantities future>=0.15 From 5a8e546d056f11d7c8034159accc021aa648c49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Troels=20Kr=C3=B8gh?= Date: Thu, 17 Aug 2017 00:24:09 +0200 Subject: [PATCH 8/8] Change write to write_raw when communicating using visa (#177) --- instruments/abstract_instruments/comm/visa_communicator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/abstract_instruments/comm/visa_communicator.py b/instruments/abstract_instruments/comm/visa_communicator.py index 391487757..7f407298c 100644 --- a/instruments/abstract_instruments/comm/visa_communicator.py +++ b/instruments/abstract_instruments/comm/visa_communicator.py @@ -149,7 +149,7 @@ def write_raw(self, msg): :param bytes msg: Bytes to be sent to the instrument over the VISA connection. """ - self._conn.write(msg) + self._conn.write_raw(msg) def seek(self, offset): # pylint: disable=unused-argument,no-self-use return NotImplemented