-
Notifications
You must be signed in to change notification settings - Fork 70
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
Extended YAML #164
Changes from 22 commits
1bf47ce
4eef89d
6507fb3
8a48ace
9022687
b3ec1e2
0ff3186
bf8bcc1
f4d23bf
5f4ac27
86ca2e6
fa0a518
a0aff09
0429a58
92e4547
5b164b4
29ffc57
ccda2a1
8711b96
f73a243
8d96981
5b148b6
3099d07
afca981
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
after_success: | ||
- coveralls | ||
deploy: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. da fuck? really? What version is that? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ################################################################### | ||
|
||
|
@@ -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="/"): | ||
""" | ||
|
@@ -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. | ||
|
||
|
@@ -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 |
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') |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,4 @@ enum34 | |
python-vxi11>=0.8 | ||
pyusb | ||
python-usbtmc | ||
pyyaml | ||
ruamel.yaml |
There was a problem hiding this comment.
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