Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended YAML #164

Merged
merged 24 commits into from
Apr 20, 2017
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1bf47ce
Added test:// URI schema.
cgranade Apr 12, 2017
4eef89d
Config: Py3 fix and support for setting attrs.
cgranade Apr 12, 2017
6507fb3
Added sub/indexing to attrs config.
cgranade Apr 12, 2017
8a48ace
Added to docstring.
cgranade Apr 12, 2017
9022687
Added support for physical quantities to config.
cgranade Apr 12, 2017
b3ec1e2
pylint fixes
cgranade Apr 12, 2017
0ff3186
Tests for new setattr_expression
cgranade Apr 12, 2017
bf8bcc1
Added tests for new config, YAML.
cgranade Apr 12, 2017
f4d23bf
pylint fix for new tests
cgranade Apr 12, 2017
5f4ac27
Very minor pylint fix
cgranade Apr 12, 2017
86ca2e6
Added ruamel.yaml as testing dep.
cgranade Apr 13, 2017
fa0a518
Reworked awkward sentence.
cgranade Apr 13, 2017
a0aff09
Updated docs for ruamel.yaml dep.
cgranade Apr 13, 2017
0429a58
Merge branch 'develop' into feature-extended-yaml-no-apt
cgranade Apr 19, 2017
92e4547
Merge branch 'master' into feature-extended-yaml-no-apt
cgranade Apr 19, 2017
5b164b4
Merge branch 'develop' into feature-extended-yaml-no-apt
scasagrande Apr 19, 2017
29ffc57
Remove pyyaml, only use ruamel.yaml
scasagrande Apr 19, 2017
ccda2a1
Disabled info category messages from mylint.
cgranade Apr 20, 2017
8711b96
Added version dump to travis config.
cgranade Apr 20, 2017
f73a243
Trying ot use python -m to work around venv issues.
cgranade Apr 20, 2017
8d96981
Revert "Trying ot use python -m to work around venv issues."
cgranade Apr 20, 2017
5b148b6
ruamel.yaml vs ruamel_yaml and fixing pylint false +ve.
cgranade Apr 20, 2017
3099d07
Explicitly use unsafe loader as suggested by ruamel.yaml warnings.
cgranade Apr 20, 2017
afca981
Marked test as explicitly unsafe as well.
cgranade Apr 20, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugly, but yeah can help with debugging

- 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, should help a lot. I wonder if there's a way to put that in the .pylintrc file to make it the default. I'm good with this though

after_success:
- coveralls
deploy:
Expand Down
6 changes: 3 additions & 3 deletions doc/source/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion instruments/abstract_instruments/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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.")
Expand Down
74 changes: 66 additions & 8 deletions instruments/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

da fuck? really? What version is that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anaconda, at least, seems to rename it to avoid a conflict somewhere. Ran into that trying to locally reproduce the pylint failure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well that's a thing.

#
# 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 ###################################################################

Expand All @@ -37,15 +50,28 @@ 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:])
else:
# 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="/"):
"""
Expand All @@ -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::

Expand All @@ -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.

Expand All @@ -98,20 +146,30 @@ def load_instruments(conf_file_name, conf_path="/"):
raise ImportError("Could not import PyYAML, 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)
else:
conf_dict = yaml.load(conf_file_name)

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
64 changes: 64 additions & 0 deletions instruments/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -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)
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')
45 changes: 44 additions & 1 deletion instruments/tests/test_util_fns.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from instruments.util_fns import (
ProxyList,
assume_units, convert_temperature
assume_units, convert_temperature,
setattr_expression
)

# TEST CASES #################################################################
Expand Down Expand Up @@ -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'
34 changes: 32 additions & 2 deletions instruments/util_fns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
"""
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ enum34
python-vxi11>=0.8
pyusb
python-usbtmc
pyyaml
ruamel.yaml
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"python-vxi11",
"python-usbtmc",
"pyusb",
"pyyaml"
"ruamel.yaml"
]
EXTRAS_REQUIRE = {
'VISA': ["pyvisa"]
Expand Down