diff --git a/qcodes/__init__.py b/qcodes/__init__.py index f7a65d3adea..1fe3b2a0728 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -7,6 +7,35 @@ from multiprocessing import active_children from qcodes.version import __version__ + +# load config system +import qcodes.config +from qcodes.config import (is_int, is_bool, is_text, is_float, + is_instance_factory, is_one_of_factory, + get_default_val) +from qcodes.config import (get_option, set_option, reset_option, + describe_option, option_context, options) + +# create various options +usezmq_doc = """ +: bool + If set to True the framework will install a hook to send logging data + to a ZMQ socket. +""" + +with config.config_prefix('display'): + config.register_option( + 'frontend', 1, 'frontend that is used (1: notebook, 0: unknown, 2: spyder', validator=is_int) +with config.config_prefix('logging'): + config.register_option('usezmq', 1, usezmq_doc, validator=is_int) + + +# load config file +path = qcodes.config.qcodes_fname() +if path is not None: + qcodes.config.from_file(path) + + from qcodes.process.helpers import set_mp_method from qcodes.utils.helpers import in_notebook diff --git a/qcodes/config.py b/qcodes/config.py new file mode 100644 index 00000000000..740b89fdcc7 --- /dev/null +++ b/qcodes/config.py @@ -0,0 +1,875 @@ +""" + +The code in this file is from: + +https://github.com/pydata/pandas + +The config module holds package-wide configurables and provides +a uniform API for working with them. + +Overview +======== + +This module supports the following requirements: +- options are referenced using keys in dot.notation, e.g. "x.y.option - z". +- keys are case-insensitive. +- functions should accept partial/regex keys, when unambiguous. +- options can be registered by modules at import time. +- options can be registered at init-time (via core.config_init) +- options have a default value, and (optionally) a description and + validation function associated with them. +- options can be deprecated, in which case referencing them + should produce a warning. +- deprecated options can optionally be rerouted to a replacement + so that accessing a deprecated option reroutes to a differently + named option. +- options can be reset to their default value. +- all option can be reset to their default value at once. +- all options in a certain sub - namespace can be reset at once. +- the user can set / get / reset or ask for the description of an option. +- a developer can register and mark an option as deprecated. +- you can register a callback to be invoked when the the option value + is set or reset. Changing the stored value is considered misuse, but + is not verboten. + +Implementation +============== + +- Data is stored using nested dictionaries, and should be accessed + through the provided API. + +- "Registered options" and "Deprecated options" have metadata associcated + with them, which are stored in auxilary dictionaries keyed on the + fully-qualified key, e.g. "x.y.z.option". + +- the config_init module is imported by the package's __init__.py file. + placing any register_option() calls there will ensure those options + are available as soon as pandas is loaded. If you use register_option + in a module, it will only be available after that module is imported, + which you should be aware of. + +- `config_prefix` is a context_manager (for use with the `with` keyword) + which can save developers some typing, see the docstring. + +""" + +import os +import six +import re + +from collections import namedtuple +from contextlib import contextmanager +import warnings +from pandas.compat import lmap, u + +DeprecatedOption = namedtuple('DeprecatedOption', 'key msg rkey removal_ver') +RegisteredOption = namedtuple( + 'RegisteredOption', 'key defval doc validator cb') + +_deprecated_options = {} # holds deprecated option metdata +_registered_options = {} # holds registered option metdata +_global_config = {} # holds the current values for registered options +_reserved_keys = ['all'] # keys which have a special meaning + + +class OptionError(AttributeError, KeyError): + + """Exception for pandas.options, backwards compatible with KeyError + checks""" + + +# +# User API + +def _get_single_key(pat, silent): + keys = _select_options(pat) + if len(keys) == 0: + if not silent: + _warn_if_deprecated(pat) + raise OptionError('No such keys(s): %r' % pat) + if len(keys) > 1: + raise OptionError('Pattern matched multiple keys') + key = keys[0] + + if not silent: + _warn_if_deprecated(key) + + key = _translate_key(key) + + return key + + +def _get_option(pat, silent=False): + key = _get_single_key(pat, silent) + + # walk the nested dict + root, k = _get_root(key) + return root[k] + + +def _set_option(*args, **kwargs): + # must at least 1 arg deal with constraints later + nargs = len(args) + if not nargs or nargs % 2 != 0: + raise ValueError("Must provide an even number of non-keyword " + "arguments") + + # default to false + silent = kwargs.pop('silent', False) + + if kwargs: + raise TypeError('_set_option() got an unexpected keyword ' + 'argument "{0}"'.format(list(kwargs.keys())[0])) + + for k, v in zip(args[::2], args[1::2]): + key = _get_single_key(k, silent) + + o = _get_registered_option(key) + if o and o.validator: + o.validator(v) + + # walk the nested dict + root, k = _get_root(key) + root[k] = v + + if o.cb: + o.cb(key) + + +def _describe_option(pat='', _print_desc=True): + + keys = _select_options(pat) + if len(keys) == 0: + raise OptionError('No such keys(s)') + + s = u('') + for k in keys: # filter by pat + s += _build_option_description(k) + + if _print_desc: + print(s) + else: + return s + + +def _reset_option(pat, silent=False): + + keys = _select_options(pat) + + if len(keys) == 0: + raise OptionError('No such keys(s)') + + if len(keys) > 1 and len(pat) < 4 and pat != 'all': + raise ValueError('You must specify at least 4 characters when ' + 'resetting multiple keys, use the special keyword ' + '"all" to reset all the options to their default ' + 'value') + + for k in keys: + _set_option(k, _registered_options[k].defval, silent=silent) + + +def get_default_val(pat): + key = _get_single_key(pat, silent=True) + return _get_registered_option(key).defval + + +class DictWrapper(object): + + """ provide attribute-style access to a nested dict + """ + + def __init__(self, d, prefix=""): + object.__setattr__(self, "d", d) + object.__setattr__(self, "prefix", prefix) + + def __setattr__(self, key, val): + prefix = object.__getattribute__(self, "prefix") + if prefix: + prefix += "." + prefix += key + # you can't set new keys + # can you can't overwrite subtrees + if key in self.d and not isinstance(self.d[key], dict): + _set_option(prefix, val) + else: + raise OptionError("You can only set the value of existing options") + + def __getattr__(self, key): + prefix = object.__getattribute__(self, "prefix") + if prefix: + prefix += "." + prefix += key + v = object.__getattribute__(self, "d")[key] + if isinstance(v, dict): + return DictWrapper(v, prefix) + else: + return _get_option(prefix) + + def __dir__(self): + return list(self.d.keys()) + + +# For user convenience, we'd like to have the available options described +# in the docstring. For dev convenience we'd like to generate the docstrings +# dynamically instead of maintaining them by hand. To this, we use the +# class below which wraps functions inside a callable, and converts +# __doc__ into a propery function. The doctsrings below are templates +# using the py2.6+ advanced formatting syntax to plug in a concise list +# of options, and option descriptions. + + +class CallableDynamicDoc(object): + + def __init__(self, func, doc_tmpl): + self.__doc_tmpl__ = doc_tmpl + self.__func__ = func + + def __call__(self, *args, **kwds): + return self.__func__(*args, **kwds) + + @property + def __doc__(self): + opts_desc = _describe_option('all', _print_desc=False) + opts_list = pp_options_list(list(_registered_options.keys())) + return self.__doc_tmpl__.format(opts_desc=opts_desc, + opts_list=opts_list) + +_get_option_tmpl = """ +get_option(pat) + +Retrieves the value of the specified option. + +Available options: + +{opts_list} + +Parameters +---------- +pat : str + Regexp which should match a single option. + Note: partial matches are supported for convenience, but unless you use the + full option name (e.g. x.y.z.option_name), your code may break in future + versions if new options with similar names are introduced. + +Returns +------- +result : the value of the option + +Raises +------ +OptionError : if no such option exists + +Notes +----- +The available options with its descriptions: + +{opts_desc} +""" + +_set_option_tmpl = """ +set_option(pat, value) + +Sets the value of the specified option. + +Available options: + +{opts_list} + +Parameters +---------- +pat : str + Regexp which should match a single option. + Note: partial matches are supported for convenience, but unless you use the + full option name (e.g. x.y.z.option_name), your code may break in future + versions if new options with similar names are introduced. +value : + new value of option. + +Returns +------- +None + +Raises +------ +OptionError if no such option exists + +Notes +----- +The available options with its descriptions: + +{opts_desc} +""" + +_describe_option_tmpl = """ +describe_option(pat, _print_desc=False) + +Prints the description for one or more registered options. + +Call with not arguments to get a listing for all registered options. + +Available options: + +{opts_list} + +Parameters +---------- +pat : str + Regexp pattern. All matching keys will have their description displayed. +_print_desc : bool, default True + If True (default) the description(s) will be printed to stdout. + Otherwise, the description(s) will be returned as a unicode string + (for testing). + +Returns +------- +None by default, the description(s) as a unicode string if _print_desc +is False + +Notes +----- +The available options with its descriptions: + +{opts_desc} +""" + +_reset_option_tmpl = """ +reset_option(pat) + +Reset one or more options to their default value. + +Pass "all" as argument to reset all options. + +Available options: + +{opts_list} + +Parameters +---------- +pat : str/regex + If specified only options matching `prefix*` will be reset. + Note: partial matches are supported for convenience, but unless you + use the full option name (e.g. x.y.z.option_name), your code may break + in future versions if new options with similar names are introduced. + +Returns +------- +None + +Notes +----- +The available options with its descriptions: + +{opts_desc} +""" + +# bind the functions with their docstrings into a Callable +# and use that as the functions exposed in pd.api +get_option = CallableDynamicDoc(_get_option, _get_option_tmpl) +set_option = CallableDynamicDoc(_set_option, _set_option_tmpl) +reset_option = CallableDynamicDoc(_reset_option, _reset_option_tmpl) +describe_option = CallableDynamicDoc(_describe_option, _describe_option_tmpl) +options = DictWrapper(_global_config) + +# +# Functions for use by pandas developers, in addition to User - api + + +class option_context(object): + + """ + Context manager to temporarily set options in the `with` statement context. + + You need to invoke as ``option_context(pat, val, [(pat, val), ...])``. + + Examples + -------- + + >>> with option_context('display.max_rows', 10, 'display.max_columns', 5): + ... + + """ + + def __init__(self, *args): + if not (len(args) % 2 == 0 and len(args) >= 2): + raise ValueError( + 'Need to invoke as' + 'option_context(pat, val, [(pat, val), ...)).' + ) + + self.ops = list(zip(args[::2], args[1::2])) + + def __enter__(self): + undo = [] + for pat, val in self.ops: + undo.append((pat, _get_option(pat, silent=True))) + + self.undo = undo + + for pat, val in self.ops: + _set_option(pat, val, silent=True) + + def __exit__(self, *args): + if self.undo: + for pat, val in self.undo: + _set_option(pat, val, silent=True) + + +def register_option(key, defval, doc='', validator=None, cb=None): + """Register an option in the package-wide pandas config object + + Parameters + ---------- + key - a fully-qualified key, e.g. "x.y.option - z". + defval - the default value of the option + doc - a string description of the option + validator - a function of a single argument, should raise `ValueError` if + called with a value which is not a legal value for the option. + cb - a function of a single argument "key", which is called + immediately after an option value is set/reset. key is + the full name of the option. + + Returns + ------- + Nothing. + + Raises + ------ + ValueError if `validator` is specified and `defval` is not a valid value. + + """ + import tokenize + import keyword + key = key.lower() + + if key in _registered_options: + raise OptionError("Option '%s' has already been registered" % key) + if key in _reserved_keys: + raise OptionError("Option '%s' is a reserved key" % key) + + # the default value should be legal + if validator: + validator(defval) + + # walk the nested dict, creating dicts as needed along the path + path = key.split('.') + + for k in path: + if not bool(re.match('^' + tokenize.Name + '$', k)): + raise ValueError("%s is not a valid identifier" % k) + if keyword.iskeyword(k): + raise ValueError("%s is a python keyword" % k) + + cursor = _global_config + for i, p in enumerate(path[:-1]): + if not isinstance(cursor, dict): + raise OptionError("Path prefix to option '%s' is already an option" + % '.'.join(path[:i])) + if p not in cursor: + cursor[p] = {} + cursor = cursor[p] + + if not isinstance(cursor, dict): + raise OptionError("Path prefix to option '%s' is already an option" + % '.'.join(path[:-1])) + + cursor[path[-1]] = defval # initialize + + # save the option metadata + _registered_options[key] = RegisteredOption(key=key, defval=defval, + doc=doc, validator=validator, + cb=cb) + + +def deprecate_option(key, msg=None, rkey=None, removal_ver=None): + """ + Mark option `key` as deprecated, if code attempts to access this option, + a warning will be produced, using `msg` if given, or a default message + if not. + if `rkey` is given, any access to the key will be re-routed to `rkey`. + + Neither the existence of `key` nor that if `rkey` is checked. If they + do not exist, any subsequence access will fail as usual, after the + deprecation warning is given. + + Parameters + ---------- + key - the name of the option to be deprecated. must be a fully-qualified + option name (e.g "x.y.z.rkey"). + + msg - (Optional) a warning message to output when the key is referenced. + if no message is given a default message will be emitted. + + rkey - (Optional) the name of an option to reroute access to. + If specified, any referenced `key` will be re-routed to `rkey` + including set/get/reset. + rkey must be a fully-qualified option name (e.g "x.y.z.rkey"). + used by the default message if no `msg` is specified. + + removal_ver - (Optional) specifies the version in which this option will + be removed. used by the default message if no `msg` + is specified. + + Returns + ------- + Nothing + + Raises + ------ + OptionError - if key has already been deprecated. + + """ + + key = key.lower() + + if key in _deprecated_options: + raise OptionError("Option '%s' has already been defined as deprecated." + % key) + + _deprecated_options[key] = DeprecatedOption(key, msg, rkey, removal_ver) + + +# +# functions internal to the module + +def _select_options(pat): + """returns a list of keys matching `pat` + + if pat=="all", returns all registered options + """ + + # short-circuit for exact key + if pat in _registered_options: + return [pat] + + # else look through all of them + keys = sorted(_registered_options.keys()) + if pat == 'all': # reserved key + return keys + + return [k for k in keys if re.search(pat, k, re.I)] + + +def _get_root(key): + path = key.split('.') + cursor = _global_config + for p in path[:-1]: + cursor = cursor[p] + return cursor, path[-1] + + +def _is_deprecated(key): + """ Returns True if the given option has been deprecated """ + + key = key.lower() + return key in _deprecated_options + + +def _get_deprecated_option(key): + """ + Retrieves the metadata for a deprecated option, if `key` is deprecated. + + Returns + ------- + DeprecatedOption (namedtuple) if key is deprecated, None otherwise + """ + + try: + d = _deprecated_options[key] + except KeyError: + return None + else: + return d + + +def _get_registered_option(key): + """ + Retrieves the option metadata if `key` is a registered option. + + Returns + ------- + RegisteredOption (namedtuple) if key is deprecated, None otherwise + """ + return _registered_options.get(key) + + +def _translate_key(key): + """ + if key id deprecated and a replacement key defined, will return the + replacement key, otherwise returns `key` as - is + """ + + d = _get_deprecated_option(key) + if d: + return d.rkey or key + else: + return key + + +def _warn_if_deprecated(key): + """ + Checks if `key` is a deprecated option and if so, prints a warning. + + Returns + ------- + bool - True if `key` is deprecated, False otherwise. + """ + + d = _get_deprecated_option(key) + if d: + if d.msg: + print(d.msg) + warnings.warn(d.msg, DeprecationWarning) + else: + msg = "'%s' is deprecated" % key + if d.removal_ver: + msg += ' and will be removed in %s' % d.removal_ver + if d.rkey: + msg += ", please use '%s' instead." % d.rkey + else: + msg += ', please refrain from using it.' + + warnings.warn(msg, DeprecationWarning) + return True + return False + + +def _build_option_description(k): + """ Builds a formatted description of a registered option and prints it """ + + o = _get_registered_option(k) + d = _get_deprecated_option(k) + + s = u('%s ') % k + + if o.doc: + s += '\n'.join(o.doc.strip().split('\n')) + else: + s += 'No description available.' + + if o: + s += u('\n [default: %s] [currently: %s]') % (o.defval, + _get_option(k, True)) + + if d: + s += u('\n (Deprecated') + s += (u(', use `%s` instead.') % d.rkey if d.rkey else '') + s += u(')') + + s += '\n\n' + return s + + +def pp_options_list(keys, width=80, _print=False): + """ Builds a concise listing of available options, grouped by prefix """ + + from textwrap import wrap + from itertools import groupby + + def pp(name, ks): + pfx = ('- ' + name + '.[' if name else '') + ls = wrap(', '.join(ks), width, initial_indent=pfx, + subsequent_indent=' ', break_long_words=False) + if ls and ls[-1] and name: + ls[-1] = ls[-1] + ']' + return ls + + ls = [] + singles = [x for x in sorted(keys) if x.find('.') < 0] + if singles: + ls += pp('', singles) + keys = [x for x in keys if x.find('.') >= 0] + + for k, g in groupby(sorted(keys), lambda x: x[:x.rfind('.')]): + ks = [x[len(k) + 1:] for x in list(g)] + ls += pp(k, ks) + s = '\n'.join(ls) + if _print: + print(s) + else: + return s + + +# +# helpers + + +@contextmanager +def config_prefix(prefix): + """contextmanager for multiple invocations of API with a common prefix + + supported API functions: (register / get / set )__option + + Warning: This is not thread - safe, and won't work properly if you import + the API functions into your module using the "from x import y" construct. + + Example: + + import pandas.core.config as cf + with cf.config_prefix("display.font"): + cf.register_option("color", "red") + cf.register_option("size", " 5 pt") + cf.set_option(size, " 6 pt") + cf.get_option(size) + ... + + etc' + + will register options "display.font.color", "display.font.size", set the + value of "display.font.size"... and so on. + """ + + # Note: reset_option relies on set_option, and on key directly + # it does not fit in to this monkey-patching scheme + + global register_option, get_option, set_option, reset_option + + def wrap(func): + + def inner(key, *args, **kwds): + pkey = '%s.%s' % (prefix, key) + return func(pkey, *args, **kwds) + + return inner + + _register_option = register_option + _get_option = get_option + _set_option = set_option + set_option = wrap(set_option) + get_option = wrap(get_option) + register_option = wrap(register_option) + yield None + set_option = _set_option + get_option = _get_option + register_option = _register_option + + +# These factories and methods are handy for use as the validator +# arg in register_option + +def is_type_factory(_type): + """ + + Parameters + ---------- + `_type` - a type to be compared against (e.g. type(x) == `_type`) + + Returns + ------- + validator - a function of a single argument x , which returns the + True if type(x) is equal to `_type` + + """ + + def inner(x): + if type(x) != _type: + raise ValueError("Value must have type '%s'" % str(_type)) + + return inner + + +def is_instance_factory(_type): + """ + + Parameters + ---------- + `_type` - the type to be checked against + + Returns + ------- + validator - a function of a single argument x , which returns the + True if x is an instance of `_type` + + """ + if isinstance(_type, (tuple, list)): + _type = tuple(_type) + from pandas.core.common import pprint_thing + type_repr = "|".join(map(pprint_thing, _type)) + else: + type_repr = "'%s'" % _type + + def inner(x): + if not isinstance(x, _type): + raise ValueError("Value must be an instance of %s" % type_repr) + + return inner + + +def is_one_of_factory(legal_values): + def inner(x): + from pandas.core.common import pprint_thing as pp + if not x in legal_values: + pp_values = lmap(pp, legal_values) + raise ValueError("Value must be one of %s" + % pp("|".join(pp_values))) + + return inner + +# common type validators, for convenience +# usage: register_option(... , validator = is_int) +is_int = is_type_factory(int) +is_bool = is_type_factory(bool) +is_float = is_type_factory(float) +is_str = is_type_factory(str) +is_text = is_instance_factory((str, bytes)) + +#%% Qcodes custom + + +def from_file(path: str, verbose=1): + ''' Load options from specified configuration file ''' + option_splitter = re.compile('\s*[:=]\s*').split + f = open(path, 'r') + errors = [] + for i, line in enumerate(f): + # allow for comments + line = line.split('#')[0].strip() + if line: + try: + split = option_splitter(line) + if len(split) == 2: + option, value = split + if verbose >= 2: + print('option %s: %s' % (option, value)) + if isinstance(get_default_val(option), int): + set_option(option, int(value)) + else: + set_option(option, value) + else: + raise ValueError("Malformed option") + except (KeyError, ValueError) as e: + errors.append("%d: %s" % (i, e)) + if verbose: + if len(errors) > 0: + print(errors) + + +def qcodes_fname(): + """ + Get the location of the config file. + + The file location is determined in the following order + + - `$PWD/qcodesrc` + + - `$QCODESRC/qcodesrc` + + If no file is found, None is returned + """ + qcodesrc = 'qcodesrc' + if six.PY2: + cwd = os.getcwdu() + else: + cwd = os.getcwd() + fname = os.path.join(cwd, qcodesrc) + if os.path.exists(fname): + return fname + + if 'QCODESRC' in os.environ: + path = os.environ['QCODESRC'] + if os.path.exists(path): + fname = os.path.join(path, qcodesrc) + if os.path.exists(fname): + return fname + + return None