-
Notifications
You must be signed in to change notification settings - Fork 85
[WIP] Extended YAML support #162
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
Changes from all commits
1bf47ce
4eef89d
6507fb3
e878a21
8669ae7
1ce0320
b0c6809
eaec515
c92cd6f
fc853c2
b0f7483
10915e6
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 |
|---|---|---|
|
|
@@ -16,6 +16,12 @@ | |
| except ImportError: | ||
| yaml = None | ||
|
|
||
| import quantities as pq | ||
|
|
||
| from future.builtins import str | ||
|
|
||
| from instruments.util_fns import setattr_expression, split_unit_str | ||
|
|
||
| # FUNCTIONS ################################################################### | ||
|
|
||
|
|
||
|
|
@@ -37,15 +43,29 @@ 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. | ||
| if yaml: | ||
| yaml.add_constructor(u'!Q', quantity_constructor) | ||
|
|
||
| def load_instruments(conf_file_name, conf_path="/"): | ||
| """ | ||
|
|
@@ -63,6 +83,27 @@ def load_instruments(conf_file_name, conf_path="/"): | |
| the form | ||
| ``{'ddg': instruments.srs.SRSDG645.open_from_uri('gpib+usb://COM7/15')}``. | ||
|
|
||
| Optionally, each the value of each instrument key can also contain a dictionary | ||
| of attributes to set on the newly-created instrument. 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: | ||
|
Contributor
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. I LOVE IT |
||
| 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 +119,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 +139,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(): | ||
|
Contributor
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 catch |
||
| 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,75 @@ | ||
| #!/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 | ||
|
|
||
| from unittest import skipIf | ||
|
|
||
| import quantities as pq | ||
|
|
||
| try: | ||
| import yaml | ||
| except ImportError: | ||
| yaml = None | ||
|
|
||
| from instruments import Instrument | ||
| from instruments.config import ( | ||
| load_instruments | ||
| ) | ||
|
|
||
| # TEST CASES ################################################################# | ||
|
|
||
| # pylint: disable=protected-access,missing-docstring | ||
|
|
||
| @skipIf(yaml is None, "PyYAML is not installed.") | ||
|
Contributor
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. I don't think we should be skipping tests if pyyaml is missing. I can see the situation where it fails to install in CI (for whatever reason) and simultaneously there is a bug that these tests would reveal. Build would be green, even though some tests technically fail.
Contributor
Author
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. Fair point, I'll make sure YAML (whichever once we go with) is appropriately a dependency for the test environment, then. I think now that the |
||
| 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) | ||
|
|
||
| @skipIf(yaml is None, "PyYAML is not installed.") | ||
| 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) | ||
|
|
||
| @skipIf(yaml is None, "PyYAML is not installed.") | ||
| 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 | ||
|
|
||
| @skipIf(yaml is None, "PyYAML is not installed.") | ||
| 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') | ||
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.
awkward sentence
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.
Good point, I'll improve that, then.