qcodes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qcodes/__init__.py +65 -0
- qcodes/actions.py +207 -0
- qcodes/config/__init__.py +2 -0
- qcodes/config/config.py +396 -0
- qcodes/config/qcodesrc.json +11 -0
- qcodes/config/qcodesrc_schema.json +55 -0
- qcodes/data/__init__.py +0 -0
- qcodes/data/data_array.py +520 -0
- qcodes/data/data_set.py +803 -0
- qcodes/data/format.py +309 -0
- qcodes/data/gnuplot_format.py +366 -0
- qcodes/data/hdf5_format.py +385 -0
- qcodes/data/io.py +266 -0
- qcodes/data/location.py +166 -0
- qcodes/data/manager.py +163 -0
- qcodes/instrument/__init__.py +0 -0
- qcodes/instrument/base.py +630 -0
- qcodes/instrument/function.py +134 -0
- qcodes/instrument/ip.py +236 -0
- qcodes/instrument/metaclass.py +45 -0
- qcodes/instrument/mock.py +278 -0
- qcodes/instrument/parameter.py +1001 -0
- qcodes/instrument/remote.py +529 -0
- qcodes/instrument/server.py +189 -0
- qcodes/instrument/sweep_values.py +286 -0
- qcodes/instrument/visa.py +198 -0
- qcodes/instrument_drivers/AlazarTech/ATS.py +1001 -0
- qcodes/instrument_drivers/AlazarTech/ATS9870.py +266 -0
- qcodes/instrument_drivers/AlazarTech/ATS_acquisition_controllers.py +143 -0
- qcodes/instrument_drivers/AlazarTech/__init__.py +0 -0
- qcodes/instrument_drivers/QuTech/IVVI.py +390 -0
- qcodes/instrument_drivers/QuTech/__init__.py +0 -0
- qcodes/instrument_drivers/__init__.py +0 -0
- qcodes/instrument_drivers/agilent/Agilent_34400A.py +155 -0
- qcodes/instrument_drivers/agilent/E8527D.py +79 -0
- qcodes/instrument_drivers/agilent/HP33210A.py +104 -0
- qcodes/instrument_drivers/agilent/__init__.py +0 -0
- qcodes/instrument_drivers/agilent/test_suite.py +96 -0
- qcodes/instrument_drivers/ithaco/Ithaco_1211.py +110 -0
- qcodes/instrument_drivers/ithaco/__init__.py +0 -0
- qcodes/instrument_drivers/oxford/__init__.py +0 -0
- qcodes/instrument_drivers/oxford/mercuryiPS.py +358 -0
- qcodes/instrument_drivers/oxford/triton.py +227 -0
- qcodes/instrument_drivers/rigol/DG4000.py +591 -0
- qcodes/instrument_drivers/rigol/__init__.py +0 -0
- qcodes/instrument_drivers/rohde_schwarz/SGS100A.py +99 -0
- qcodes/instrument_drivers/rohde_schwarz/ZNB20.py +154 -0
- qcodes/instrument_drivers/rohde_schwarz/__init__.py +0 -0
- qcodes/instrument_drivers/signal_hound/USB_SA124B.py +567 -0
- qcodes/instrument_drivers/signal_hound/__init__.py +0 -0
- qcodes/instrument_drivers/stanford_research/SR560.py +116 -0
- qcodes/instrument_drivers/stanford_research/SR830.py +314 -0
- qcodes/instrument_drivers/stanford_research/SR865.py +305 -0
- qcodes/instrument_drivers/stanford_research/__init__.py +0 -0
- qcodes/instrument_drivers/tektronix/AWG5014.py +1228 -0
- qcodes/instrument_drivers/tektronix/AWG520.py +659 -0
- qcodes/instrument_drivers/tektronix/Keithley_2000.py +195 -0
- qcodes/instrument_drivers/tektronix/Keithley_2600.py +83 -0
- qcodes/instrument_drivers/tektronix/Keithley_2700.py +395 -0
- qcodes/instrument_drivers/tektronix/__init__.py +0 -0
- qcodes/instrument_drivers/test.py +94 -0
- qcodes/instrument_drivers/weinschel/Weinschel_8320.py +21 -0
- qcodes/instrument_drivers/weinschel/__init__.py +0 -0
- qcodes/instrument_drivers/weinschel/test_suite.py +27 -0
- qcodes/loops.py +1009 -0
- qcodes/measure.py +155 -0
- qcodes/plots/__init__.py +0 -0
- qcodes/plots/base.py +280 -0
- qcodes/plots/colors.py +135 -0
- qcodes/plots/pyqtgraph.py +384 -0
- qcodes/plots/qcmatplotlib.py +206 -0
- qcodes/process/__init__.py +0 -0
- qcodes/process/helpers.py +60 -0
- qcodes/process/qcodes_process.py +72 -0
- qcodes/process/server.py +395 -0
- qcodes/process/stream_queue.py +152 -0
- qcodes/station.py +178 -0
- qcodes/test.py +119 -0
- qcodes/tests/__init__.py +0 -0
- qcodes/tests/common.py +7 -0
- qcodes/tests/data_mocks.py +173 -0
- qcodes/tests/instrument_mocks.py +327 -0
- qcodes/tests/py35_syntax.py +13 -0
- qcodes/tests/test_combined_par.py +156 -0
- qcodes/tests/test_command.py +120 -0
- qcodes/tests/test_config.py +222 -0
- qcodes/tests/test_data.py +668 -0
- qcodes/tests/test_deferred_operations.py +105 -0
- qcodes/tests/test_driver_testcase.py +58 -0
- qcodes/tests/test_format.py +414 -0
- qcodes/tests/test_hdf5formatter.py +333 -0
- qcodes/tests/test_helpers.py +630 -0
- qcodes/tests/test_instrument.py +981 -0
- qcodes/tests/test_instrument_server.py +156 -0
- qcodes/tests/test_json.py +48 -0
- qcodes/tests/test_location_provider.py +109 -0
- qcodes/tests/test_loop.py +825 -0
- qcodes/tests/test_measure.py +49 -0
- qcodes/tests/test_metadata.py +49 -0
- qcodes/tests/test_multiprocessing.py +471 -0
- qcodes/tests/test_nested_attrs.py +100 -0
- qcodes/tests/test_parameter.py +151 -0
- qcodes/tests/test_plots.py +44 -0
- qcodes/tests/test_sweep_values.py +159 -0
- qcodes/tests/test_validators.py +482 -0
- qcodes/tests/test_visa.py +184 -0
- qcodes/utils/__init__.py +0 -0
- qcodes/utils/command.py +176 -0
- qcodes/utils/deferred_operations.py +276 -0
- qcodes/utils/helpers.py +417 -0
- qcodes/utils/metadata.py +42 -0
- qcodes/utils/nested_attrs.py +177 -0
- qcodes/utils/reload_code.py +98 -0
- qcodes/utils/threading.py +72 -0
- qcodes/utils/timing.py +89 -0
- qcodes/utils/validators.py +393 -0
- qcodes/version.py +1 -0
- qcodes/widgets/__init__.py +0 -0
- qcodes/widgets/display.py +47 -0
- qcodes/widgets/widgets.css +77 -0
- qcodes/widgets/widgets.js +237 -0
- qcodes/widgets/widgets.py +215 -0
- qcodes-0.1.0.dist-info/DESCRIPTION.rst +165 -0
- qcodes-0.1.0.dist-info/METADATA +194 -0
- qcodes-0.1.0.dist-info/RECORD +128 -0
- qcodes-0.1.0.dist-info/WHEEL +5 -0
- qcodes-0.1.0.dist-info/metadata.json +1 -0
- qcodes-0.1.0.dist-info/top_level.txt +1 -0
qcodes/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Set up the main qcodes namespace."""
|
|
2
|
+
|
|
3
|
+
# flake8: noqa (we don't need the "<...> imported but unused" error)
|
|
4
|
+
|
|
5
|
+
# just for convenience in debugging, so we don't have to
|
|
6
|
+
# separately import multiprocessing
|
|
7
|
+
from multiprocessing import active_children
|
|
8
|
+
|
|
9
|
+
# config
|
|
10
|
+
|
|
11
|
+
from qcodes.config import Config
|
|
12
|
+
|
|
13
|
+
config = Config()
|
|
14
|
+
|
|
15
|
+
from qcodes.version import __version__
|
|
16
|
+
from qcodes.process.helpers import set_mp_method
|
|
17
|
+
from qcodes.utils.helpers import in_notebook
|
|
18
|
+
|
|
19
|
+
# code that should only be imported into the main (notebook) thread
|
|
20
|
+
# in particular, importing matplotlib in the side processes takes a long
|
|
21
|
+
# time and spins up other processes in order to try and get a front end
|
|
22
|
+
if in_notebook(): # pragma: no cover
|
|
23
|
+
try:
|
|
24
|
+
from qcodes.plots.qcmatplotlib import MatPlot
|
|
25
|
+
except Exception:
|
|
26
|
+
print('matplotlib plotting not supported, '
|
|
27
|
+
'try "from qcodes.plots.qcmatplotlib import MatPlot" '
|
|
28
|
+
'to see the full error')
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from qcodes.plots.pyqtgraph import QtPlot
|
|
32
|
+
except Exception:
|
|
33
|
+
print('pyqtgraph plotting not supported, '
|
|
34
|
+
'try "from qcodes.plots.pyqtgraph import QtPlot" '
|
|
35
|
+
'to see the full error')
|
|
36
|
+
|
|
37
|
+
from qcodes.widgets.widgets import show_subprocess_widget
|
|
38
|
+
|
|
39
|
+
from qcodes.station import Station
|
|
40
|
+
from qcodes.loops import get_bg, halt_bg, Loop
|
|
41
|
+
from qcodes.measure import Measure
|
|
42
|
+
from qcodes.actions import Task, Wait, BreakIf
|
|
43
|
+
|
|
44
|
+
from qcodes.data.manager import get_data_manager
|
|
45
|
+
from qcodes.data.data_set import DataMode, DataSet, new_data, load_data
|
|
46
|
+
from qcodes.data.location import FormatLocation
|
|
47
|
+
from qcodes.data.data_array import DataArray
|
|
48
|
+
from qcodes.data.format import Formatter
|
|
49
|
+
from qcodes.data.gnuplot_format import GNUPlotFormat
|
|
50
|
+
from qcodes.data.hdf5_format import HDF5Format
|
|
51
|
+
from qcodes.data.io import DiskIO
|
|
52
|
+
|
|
53
|
+
from qcodes.instrument.base import Instrument
|
|
54
|
+
from qcodes.instrument.ip import IPInstrument
|
|
55
|
+
from qcodes.instrument.visa import VisaInstrument
|
|
56
|
+
from qcodes.instrument.mock import MockInstrument, MockModel
|
|
57
|
+
|
|
58
|
+
from qcodes.instrument.function import Function
|
|
59
|
+
from qcodes.instrument.parameter import Parameter, StandardParameter, combine, CombinedParameter
|
|
60
|
+
from qcodes.instrument.sweep_values import SweepFixedValues, SweepValues
|
|
61
|
+
|
|
62
|
+
from qcodes.utils import validators
|
|
63
|
+
|
|
64
|
+
from qcodes.instrument_drivers.test import test_instruments, test_instrument
|
|
65
|
+
from qcodes.test import test_core, test_part
|
qcodes/actions.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Actions, mainly to be executed in measurement Loops."""
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from qcodes.utils.deferred_operations import is_function
|
|
5
|
+
from qcodes.utils.threading import thread_map
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_NO_SNAPSHOT = {'type': None, 'description': 'Action without snapshot'}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _actions_snapshot(actions, update):
|
|
12
|
+
"""Make a list of snapshots from a list of actions."""
|
|
13
|
+
snapshot = []
|
|
14
|
+
for action in actions:
|
|
15
|
+
if hasattr(action, 'snapshot'):
|
|
16
|
+
snapshot.append(action.snapshot(update=update))
|
|
17
|
+
else:
|
|
18
|
+
snapshot.append(_NO_SNAPSHOT)
|
|
19
|
+
return snapshot
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Task:
|
|
23
|
+
"""
|
|
24
|
+
A predefined task to be executed within a measurement Loop.
|
|
25
|
+
|
|
26
|
+
The first argument should be a callable, to which any subsequent
|
|
27
|
+
args and kwargs (which are evaluated before the loop starts) are passed.
|
|
28
|
+
|
|
29
|
+
The args and kwargs are first evaluated if they are found to be callable.
|
|
30
|
+
|
|
31
|
+
Keyword Args passed when the Task is called are ignored,
|
|
32
|
+
but are accepted for compatibility with other things happening in a Loop.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
func (callable): Function to executed
|
|
36
|
+
*args: pass to func, after evaluation if callable
|
|
37
|
+
**kwargs: pass to func, after evaluation if callable
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
def __init__(self, func, *args, **kwargs):
|
|
41
|
+
self.func = func
|
|
42
|
+
self.args = args
|
|
43
|
+
self.kwargs = kwargs
|
|
44
|
+
|
|
45
|
+
def __call__(self, **ignore_kwargs):
|
|
46
|
+
# If any of the arguments are callable, evaluate them first
|
|
47
|
+
eval_args = [arg() if callable(arg) else arg for arg in self.args]
|
|
48
|
+
eval_kwargs = {k: (v() if callable(v) else v) for k, v in self.kwargs.items()}
|
|
49
|
+
|
|
50
|
+
self.func(*eval_args, **eval_kwargs)
|
|
51
|
+
|
|
52
|
+
def snapshot(self, update=False):
|
|
53
|
+
"""
|
|
54
|
+
Snapshots task
|
|
55
|
+
Args:
|
|
56
|
+
update (bool): TODO not in use
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
dict: snapshot
|
|
60
|
+
"""
|
|
61
|
+
return {'type': 'Task', 'func': repr(self.func)}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Wait:
|
|
65
|
+
"""
|
|
66
|
+
A simple class to tell a Loop to wait <delay> seconds.
|
|
67
|
+
|
|
68
|
+
This is transformed into a Task within the Loop, such that
|
|
69
|
+
it can do other things (monitor, check for halt) during the delay.
|
|
70
|
+
|
|
71
|
+
But for use outside of a Loop, it is also callable (then it just sleeps)
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
delay: seconds to delay
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: if delay is negative
|
|
78
|
+
"""
|
|
79
|
+
def __init__(self, delay):
|
|
80
|
+
if not delay >= 0:
|
|
81
|
+
raise ValueError('delay must be > 0, not {}'.format(repr(delay)))
|
|
82
|
+
self.delay = delay
|
|
83
|
+
|
|
84
|
+
def __call__(self):
|
|
85
|
+
if self.delay:
|
|
86
|
+
time.sleep(self.delay)
|
|
87
|
+
|
|
88
|
+
def snapshot(self, update=False):
|
|
89
|
+
"""
|
|
90
|
+
Snapshots delay
|
|
91
|
+
Args:
|
|
92
|
+
update (bool): TODO not in use
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
dict: snapshot
|
|
96
|
+
"""
|
|
97
|
+
return {'type': 'Wait', 'delay': self.delay}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _Measure:
|
|
101
|
+
"""
|
|
102
|
+
A callable collection of parameters to measure.
|
|
103
|
+
|
|
104
|
+
This should not be constructed manually, only by an ActiveLoop.
|
|
105
|
+
"""
|
|
106
|
+
def __init__(self, params_indices, data_set, use_threads):
|
|
107
|
+
self.use_threads = use_threads and len(params_indices) > 1
|
|
108
|
+
# the applicable DataSet.store function
|
|
109
|
+
self.store = data_set.store
|
|
110
|
+
|
|
111
|
+
# for performance, pre-calculate which params return data for
|
|
112
|
+
# multiple arrays, and the name mappings
|
|
113
|
+
self.getters = []
|
|
114
|
+
self.param_ids = []
|
|
115
|
+
self.composite = []
|
|
116
|
+
for param, action_indices in params_indices:
|
|
117
|
+
self.getters.append(param.get)
|
|
118
|
+
|
|
119
|
+
if hasattr(param, 'names'):
|
|
120
|
+
part_ids = []
|
|
121
|
+
for i in range(len(param.names)):
|
|
122
|
+
param_id = data_set.action_id_map[action_indices + (i,)]
|
|
123
|
+
part_ids.append(param_id)
|
|
124
|
+
self.param_ids.append(None)
|
|
125
|
+
self.composite.append(part_ids)
|
|
126
|
+
else:
|
|
127
|
+
param_id = data_set.action_id_map[action_indices]
|
|
128
|
+
self.param_ids.append(param_id)
|
|
129
|
+
self.composite.append(False)
|
|
130
|
+
|
|
131
|
+
def __call__(self, loop_indices, **ignore_kwargs):
|
|
132
|
+
out_dict = {}
|
|
133
|
+
if self.use_threads:
|
|
134
|
+
out = thread_map(self.getters)
|
|
135
|
+
else:
|
|
136
|
+
out = [g() for g in self.getters]
|
|
137
|
+
|
|
138
|
+
for param_out, param_id, composite in zip(out, self.param_ids,
|
|
139
|
+
self.composite):
|
|
140
|
+
if composite:
|
|
141
|
+
for val, part_id in zip(param_out, composite):
|
|
142
|
+
out_dict[part_id] = val
|
|
143
|
+
else:
|
|
144
|
+
out_dict[param_id] = param_out
|
|
145
|
+
|
|
146
|
+
self.store(loop_indices, out_dict)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _Nest:
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
Wrapper to make a callable nested ActiveLoop.
|
|
153
|
+
|
|
154
|
+
This should not be constructed manually, only by an ActiveLoop.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, inner_loop, action_indices):
|
|
158
|
+
self.inner_loop = inner_loop
|
|
159
|
+
self.action_indices = action_indices
|
|
160
|
+
|
|
161
|
+
def __call__(self, **kwargs):
|
|
162
|
+
self.inner_loop._run_loop(action_indices=self.action_indices, **kwargs)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class BreakIf:
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
Loop action that breaks out of the loop if a condition is truthy.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
condition (callable): a callable taking no arguments.
|
|
172
|
+
Can be a simple function that returns truthy when it's time to quit
|
|
173
|
+
May also be constructed by deferred operations on `Parameter`.
|
|
174
|
+
Raises:
|
|
175
|
+
TypeError: if condition is not a callable with no aguments.
|
|
176
|
+
|
|
177
|
+
Examples:
|
|
178
|
+
>>> BreakIf(gates.chan1 >= 3)
|
|
179
|
+
>>> BreakIf(abs(source.I * source.V) >= source.power_limit.get_latest)
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, condition):
|
|
183
|
+
if not is_function(condition, 0):
|
|
184
|
+
raise TypeError('BreakIf condition must be a callable with '
|
|
185
|
+
'no arguments')
|
|
186
|
+
self.condition = condition
|
|
187
|
+
|
|
188
|
+
def __call__(self, **ignore_kwargs):
|
|
189
|
+
if self.condition():
|
|
190
|
+
raise _QcodesBreak
|
|
191
|
+
|
|
192
|
+
def snapshot(self, update=False):
|
|
193
|
+
# TODO: make nice reprs for DeferredOperations
|
|
194
|
+
"""
|
|
195
|
+
Snapshots breakif action
|
|
196
|
+
Args:
|
|
197
|
+
update (bool): TODO not in use
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
dict: snapshot
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
return {'type': 'BreakIf', 'condition': repr(self.condition)}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class _QcodesBreak(Exception):
|
|
207
|
+
pass
|
qcodes/config/config.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import jsonschema
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import pkg_resources as pkgr
|
|
7
|
+
|
|
8
|
+
from os.path import expanduser
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
EMPTY_USER_SCHEMA = "User schema at {} not found." + \
|
|
14
|
+
"User settings won't be validated"
|
|
15
|
+
MISS_DESC = """ Passing a description without a type does not make sense.
|
|
16
|
+
Description is ignored """
|
|
17
|
+
|
|
18
|
+
BASE_SCHEMA = {
|
|
19
|
+
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
20
|
+
"type": "object",
|
|
21
|
+
"description": "schema for a user qcodes config file",
|
|
22
|
+
"properties": {},
|
|
23
|
+
"required": []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Config():
|
|
28
|
+
"""
|
|
29
|
+
QCoDeS config system
|
|
30
|
+
|
|
31
|
+
Start with sane defaults, which you can't change, and
|
|
32
|
+
then customize your experience using files that update the configuration.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
config_file_name(str): Name of config file
|
|
37
|
+
schema_file_name(str): Name of schema file
|
|
38
|
+
|
|
39
|
+
default_file_name(str):Filene name of default config
|
|
40
|
+
schema_default_file_name(str):Filene name of default schema
|
|
41
|
+
|
|
42
|
+
home_file_name(str):Filene name of home config
|
|
43
|
+
schema_home_file_name(str):Filene name of home schema
|
|
44
|
+
|
|
45
|
+
env_file_name(str):Filene name of env config
|
|
46
|
+
schema_env_file_name(str):Filene name of env schema
|
|
47
|
+
|
|
48
|
+
cwd_file_name(str):Filene name of cwd config
|
|
49
|
+
schema_cwd_file_name(str):Filene name of cwd schema
|
|
50
|
+
|
|
51
|
+
current_config(dict): Vaild config values
|
|
52
|
+
current_schema(dict): Validators and desciptions of config values
|
|
53
|
+
current_config_path(path): Path of the currently loaded config
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
config_file_name = "qcodesrc.json"
|
|
58
|
+
schema_file_name = "qcodesrc_schema.json"
|
|
59
|
+
|
|
60
|
+
# get abs path of packge config file
|
|
61
|
+
default_file_name = pkgr.resource_filename(__name__, config_file_name)
|
|
62
|
+
current_config_path = default_file_name
|
|
63
|
+
|
|
64
|
+
# get abs path of schema file
|
|
65
|
+
schema_default_file_name = pkgr.resource_filename(__name__,
|
|
66
|
+
schema_file_name)
|
|
67
|
+
|
|
68
|
+
# home dir, os independent
|
|
69
|
+
home_file_name = expanduser("~/{}".format(config_file_name))
|
|
70
|
+
schema_home_file_name = home_file_name.replace(config_file_name,
|
|
71
|
+
schema_file_name)
|
|
72
|
+
|
|
73
|
+
# this is for *nix people
|
|
74
|
+
env_file_name = os.environ.get("QCODES_CONFIG", "")
|
|
75
|
+
schema_env_file_name = env_file_name.replace(config_file_name,
|
|
76
|
+
schema_file_name)
|
|
77
|
+
# current working dir
|
|
78
|
+
cwd_file_name = "{}/{}".format(Path.cwd(), config_file_name)
|
|
79
|
+
schema_cwd_file_name = cwd_file_name.replace(config_file_name,
|
|
80
|
+
schema_file_name)
|
|
81
|
+
|
|
82
|
+
current_schema = None
|
|
83
|
+
current_config = None
|
|
84
|
+
|
|
85
|
+
defaults = None
|
|
86
|
+
defaults_schema = None
|
|
87
|
+
|
|
88
|
+
_diff_config = {}
|
|
89
|
+
_diff_schema = {}
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.defaults, self.defaults_schema = self.load_default()
|
|
93
|
+
self.current_config = self.update_config()
|
|
94
|
+
|
|
95
|
+
def load_default(self):
|
|
96
|
+
defaults = self.load_config(self.default_file_name)
|
|
97
|
+
defaults_schema = self.load_config(self.schema_default_file_name)
|
|
98
|
+
self.validate(defaults, defaults_schema)
|
|
99
|
+
return defaults, defaults_schema
|
|
100
|
+
|
|
101
|
+
def update_config(self):
|
|
102
|
+
"""
|
|
103
|
+
Load defaults and validates.
|
|
104
|
+
A configuration file must be called qcodesrc.json
|
|
105
|
+
A schema file must be called schema.json
|
|
106
|
+
Configuration files (and their schema) are loaded and updated from the
|
|
107
|
+
default directories in the following order:
|
|
108
|
+
- default json config file from the repository
|
|
109
|
+
- user json config in user home directory
|
|
110
|
+
- user json config in $QCODES_CONFIG
|
|
111
|
+
- user json config in current working directory
|
|
112
|
+
|
|
113
|
+
If a key/value is not specified in the user configuration the default
|
|
114
|
+
is used. Configs are validated after every update.
|
|
115
|
+
Validation is also performed against a user provied schema if it's
|
|
116
|
+
found in the directory.
|
|
117
|
+
"""
|
|
118
|
+
config = copy.deepcopy(self.defaults)
|
|
119
|
+
self.current_schema = copy.deepcopy(self.defaults_schema)
|
|
120
|
+
|
|
121
|
+
if os.path.isfile(self.home_file_name):
|
|
122
|
+
home_config = self.load_config(self.home_file_name)
|
|
123
|
+
config.update(home_config)
|
|
124
|
+
self.validate(config, self.current_schema,
|
|
125
|
+
self.schema_home_file_name)
|
|
126
|
+
|
|
127
|
+
if os.path.isfile(self.env_file_name):
|
|
128
|
+
env_config = self.load_config(self.env_file_name)
|
|
129
|
+
config.update(env_config)
|
|
130
|
+
self.validate(config, self.current_schema,
|
|
131
|
+
self.schema_env_file_name)
|
|
132
|
+
|
|
133
|
+
if os.path.isfile(self.cwd_file_name):
|
|
134
|
+
cwd_config = self.load_config(self.cwd_file_name)
|
|
135
|
+
config.update(cwd_config)
|
|
136
|
+
self.validate(config, self.current_schema,
|
|
137
|
+
self.schema_cwd_file_name)
|
|
138
|
+
|
|
139
|
+
return config
|
|
140
|
+
|
|
141
|
+
def validate(self, json_config=None, schema=None, extra_schema_path=None):
|
|
142
|
+
"""
|
|
143
|
+
Validate configuration, if no arguments are passed, the default
|
|
144
|
+
validators are used.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
json_config (Optiona[string]) : json file to validate
|
|
148
|
+
schema (Optiona[dict]): schema dictionary
|
|
149
|
+
extra_schema_path (Optiona[string]): schema path that contains
|
|
150
|
+
extra validators to be added to schema dictionary
|
|
151
|
+
"""
|
|
152
|
+
if extra_schema_path is not None:
|
|
153
|
+
# add custom validation
|
|
154
|
+
if os.path.isfile(extra_schema_path):
|
|
155
|
+
with open(extra_schema_path) as f:
|
|
156
|
+
# user schema has to be both vaild in itself
|
|
157
|
+
# but then just update the user properties
|
|
158
|
+
# so that default types and values can NEVER
|
|
159
|
+
# be overwritten
|
|
160
|
+
new_user = json.load(f)["properties"]["user"]
|
|
161
|
+
user = schema["properties"]['user']
|
|
162
|
+
user["properties"].update(
|
|
163
|
+
new_user["properties"]
|
|
164
|
+
)
|
|
165
|
+
jsonschema.validate(json_config, schema)
|
|
166
|
+
else:
|
|
167
|
+
logger.warning(EMPTY_USER_SCHEMA.format(extra_schema_path))
|
|
168
|
+
else:
|
|
169
|
+
if json_config is None and schema is None:
|
|
170
|
+
jsonschema.validate(self.current_config, self.current_schema)
|
|
171
|
+
else:
|
|
172
|
+
jsonschema.validate(json_config, schema)
|
|
173
|
+
|
|
174
|
+
def add(self, key, value, value_type=None, description=None, default=None):
|
|
175
|
+
""" Add custom config value in place.
|
|
176
|
+
Add key, value with optional value_type to user cofnig and schema.
|
|
177
|
+
If value_type is specified then the new value is validated.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
key(str): key to be added under user config
|
|
181
|
+
value (any): value to add to config
|
|
182
|
+
value_type(Optional(string)): type of value
|
|
183
|
+
allowed are string, boolean, integer
|
|
184
|
+
default (str): default value, stored only in the schema
|
|
185
|
+
description (str): description of key to add to schema
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
|
|
189
|
+
>>> defaults.add("trace_color", "blue", "string", "description")
|
|
190
|
+
|
|
191
|
+
will update the config:
|
|
192
|
+
`...
|
|
193
|
+
"user": { "trace_color": "blue"}
|
|
194
|
+
...`
|
|
195
|
+
and the schema:
|
|
196
|
+
`...
|
|
197
|
+
"user":{
|
|
198
|
+
"type" : "object",
|
|
199
|
+
"description": "controls user settings of qcodes"
|
|
200
|
+
"properties" : {
|
|
201
|
+
"trace_color": {
|
|
202
|
+
"description" : "description",
|
|
203
|
+
"type": "string"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
...`
|
|
208
|
+
|
|
209
|
+
Todo:
|
|
210
|
+
- Add enum support for value_type
|
|
211
|
+
- finsh _diffing
|
|
212
|
+
"""
|
|
213
|
+
self.current_config["user"].update({key: value})
|
|
214
|
+
|
|
215
|
+
if self._diff_config.get("user", True):
|
|
216
|
+
self._diff_config["user"] = {}
|
|
217
|
+
self._diff_config.get("user").update({key: value})
|
|
218
|
+
|
|
219
|
+
if value_type is None:
|
|
220
|
+
if description is not None:
|
|
221
|
+
logger.warning(MISS_DESC)
|
|
222
|
+
else:
|
|
223
|
+
# update schema!
|
|
224
|
+
schema_entry = {key: {"type": value_type}}
|
|
225
|
+
if description is not None:
|
|
226
|
+
schema_entry = {key: {
|
|
227
|
+
"type": value_type,
|
|
228
|
+
"default": default,
|
|
229
|
+
"description": description}
|
|
230
|
+
}
|
|
231
|
+
# the schema is nested we only update properties of the user object
|
|
232
|
+
user = self.current_schema['properties']["user"]
|
|
233
|
+
user["properties"].update(schema_entry)
|
|
234
|
+
self.validate(self.current_config, self.current_schema)
|
|
235
|
+
|
|
236
|
+
# TODO(giulioungaretti) finish diffing
|
|
237
|
+
# now we update the entire schema
|
|
238
|
+
# and the entire configuration
|
|
239
|
+
# if it's saved then it will always
|
|
240
|
+
# take precedence even if the defaults
|
|
241
|
+
# values are changed upstream, and the local
|
|
242
|
+
# ones were actually left to their default
|
|
243
|
+
# values
|
|
244
|
+
if not self._diff_schema:
|
|
245
|
+
self._diff_schema = BASE_SCHEMA
|
|
246
|
+
|
|
247
|
+
props = self._diff_schema['properties']
|
|
248
|
+
if props.get("user", True):
|
|
249
|
+
props["user"] = {}
|
|
250
|
+
props.get("user").update(schema_entry)
|
|
251
|
+
|
|
252
|
+
def load_config(self, path):
|
|
253
|
+
""" Load a config JSON file
|
|
254
|
+
As a side effect it records which file is loaded
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
path(str): path to the config file
|
|
258
|
+
Raises:
|
|
259
|
+
FileNotFoundError: if config is missing
|
|
260
|
+
Return:
|
|
261
|
+
Union[DotDict, None]: a dot accessible config object
|
|
262
|
+
"""
|
|
263
|
+
with open(path, "r") as fp:
|
|
264
|
+
config = json.load(fp)
|
|
265
|
+
|
|
266
|
+
config = DotDict(config)
|
|
267
|
+
self.current_config_path = path
|
|
268
|
+
return config
|
|
269
|
+
|
|
270
|
+
def save_config(self, path):
|
|
271
|
+
""" Save to file(s)
|
|
272
|
+
Saves current config to path.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
path (string): path of new file(s)
|
|
276
|
+
"""
|
|
277
|
+
with open(path, "w") as fp:
|
|
278
|
+
json.dump(self.current_config, fp, indent=4)
|
|
279
|
+
|
|
280
|
+
def save_schema(self, path):
|
|
281
|
+
""" Save to file(s)
|
|
282
|
+
Saves current schema to path.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
path (string): path of new file(s)
|
|
286
|
+
"""
|
|
287
|
+
with open(path, "w") as fp:
|
|
288
|
+
json.dump(self.current_schema, fp, indent=4)
|
|
289
|
+
|
|
290
|
+
def save_to_home(self):
|
|
291
|
+
""" Save files to home dir
|
|
292
|
+
"""
|
|
293
|
+
self.save_config(self.home_file_name)
|
|
294
|
+
self.save_schema(self.schema_home_file_name)
|
|
295
|
+
|
|
296
|
+
def save_to_env(self):
|
|
297
|
+
""" Save files to env path
|
|
298
|
+
"""
|
|
299
|
+
self.save_config(self.env_file_name)
|
|
300
|
+
self.save_schema(self.schema_env_file_name)
|
|
301
|
+
|
|
302
|
+
def save_to_cwd(self):
|
|
303
|
+
""" Save files to current working dir
|
|
304
|
+
"""
|
|
305
|
+
self.save_config(self.cwd_file_name)
|
|
306
|
+
self.save_schema(self.schema_cwd_file_name)
|
|
307
|
+
|
|
308
|
+
def describe(self, name):
|
|
309
|
+
"""
|
|
310
|
+
Describe a configuration entry
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
name (str): name of entry to describe
|
|
314
|
+
"""
|
|
315
|
+
val = self.current_config
|
|
316
|
+
sch = self.current_schema["properties"]
|
|
317
|
+
for key in name.split('.'):
|
|
318
|
+
val = val[key]
|
|
319
|
+
if sch.get(key):
|
|
320
|
+
sch = sch[key]
|
|
321
|
+
else:
|
|
322
|
+
sch = sch['properties'][key]
|
|
323
|
+
description = sch.get("description", None) or "Generic value"
|
|
324
|
+
_type = str(sch.get("type", None)) or "Not defined"
|
|
325
|
+
default = sch.get("default", None) or "Not defined"
|
|
326
|
+
|
|
327
|
+
# add cool description to docstring
|
|
328
|
+
base_docstring = """{}.\nCurrent value: {}. Type: {}. Default: {}."""
|
|
329
|
+
|
|
330
|
+
doc = base_docstring.format(
|
|
331
|
+
description,
|
|
332
|
+
val,
|
|
333
|
+
_type,
|
|
334
|
+
default
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return doc
|
|
338
|
+
|
|
339
|
+
def __getitem__(self, name):
|
|
340
|
+
val = self.current_config
|
|
341
|
+
for key in name.split('.'):
|
|
342
|
+
val = val[key]
|
|
343
|
+
return val
|
|
344
|
+
|
|
345
|
+
def __getattr__(self, name):
|
|
346
|
+
return getattr(self.current_config, name)
|
|
347
|
+
|
|
348
|
+
def __repr__(self):
|
|
349
|
+
old = super().__repr__()
|
|
350
|
+
base = """Current values: \n {} \n Current path: \n {} \n {}"""
|
|
351
|
+
return base.format(self.current_config,
|
|
352
|
+
self.current_config_path,
|
|
353
|
+
old)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class DotDict(dict):
|
|
357
|
+
"""
|
|
358
|
+
Wrapper dict that allows to get dotted attributes
|
|
359
|
+
"""
|
|
360
|
+
def __init__(self, value=None):
|
|
361
|
+
if value is None:
|
|
362
|
+
pass
|
|
363
|
+
else:
|
|
364
|
+
for key in value:
|
|
365
|
+
self.__setitem__(key, value[key])
|
|
366
|
+
|
|
367
|
+
def __setitem__(self, key, value):
|
|
368
|
+
if '.' in key:
|
|
369
|
+
myKey, restOfKey = key.split('.', 1)
|
|
370
|
+
target = self.setdefault(myKey, DotDict())
|
|
371
|
+
target[restOfKey] = value
|
|
372
|
+
else:
|
|
373
|
+
if isinstance(value, dict) and not isinstance(value, DotDict):
|
|
374
|
+
value = DotDict(value)
|
|
375
|
+
dict.__setitem__(self, key, value)
|
|
376
|
+
|
|
377
|
+
def __getitem__(self, key):
|
|
378
|
+
if '.' not in key:
|
|
379
|
+
return dict.__getitem__(self, key)
|
|
380
|
+
myKey, restOfKey = key.split('.', 1)
|
|
381
|
+
target = dict.__getitem__(self, myKey)
|
|
382
|
+
return target[restOfKey]
|
|
383
|
+
|
|
384
|
+
def __contains__(self, key):
|
|
385
|
+
if '.' not in key:
|
|
386
|
+
return dict.__contains__(self, key)
|
|
387
|
+
myKey, restOfKey = key.split('.', 1)
|
|
388
|
+
target = dict.__getitem__(self, myKey)
|
|
389
|
+
return restOfKey in target
|
|
390
|
+
|
|
391
|
+
def __deepcopy__(self, memo):
|
|
392
|
+
return DotDict(copy.deepcopy(dict(self)))
|
|
393
|
+
|
|
394
|
+
# dot acces baby
|
|
395
|
+
__setattr__ = __setitem__
|
|
396
|
+
__getattr__ = __getitem__
|