diff --git a/qiskit/user_config.py b/qiskit/user_config.py index 12105b86c5cd..170b6dff3b69 100644 --- a/qiskit/user_config.py +++ b/qiskit/user_config.py @@ -143,6 +143,70 @@ def read_config_file(self): self.settings["num_processes"] = num_processes +def set_config(key, value, section=None, file_path=None): + """Adds or modifies a user configuration + + It will add configuration to the currently configured location + or the value of file argument. + + Only valid user config can be set in 'default' section. Custom + user config can be added in any other sections. + + Changes to the existing config file will not be reflected in + the current session since the config file is parsed at import time. + + Args: + key (str): name of the config + value (obj): value of the config + section (str, optional): if not specified, adds it to the + `default` section of the config file. + file_path (str, optional): the file to which config is added. + If not specified, adds it to the default config file or + if set, the value of `QISKIT_SETTINGS` env variable. + + Raises: + QiskitUserConfigError: if the config is invalid + """ + filename = file_path or os.getenv("QISKIT_SETTINGS", DEFAULT_FILENAME) + section = "default" if section is None else section + + if not isinstance(key, str): + raise exceptions.QiskitUserConfigError("Key must be string type") + + valid_config = { + "circuit_drawer", + "circuit_mpl_style", + "circuit_mpl_style_path", + "transpile_optimization_level", + "parallel", + "num_processes", + } + + if section in [None, "default"]: + if key not in valid_config: + raise exceptions.QiskitUserConfigError("{} is not a valid user config.".format(key)) + + config = configparser.ConfigParser() + config.read(filename) + + if section not in config.sections(): + config.add_section(section) + + config.set(section, key, str(value)) + + try: + with open(filename, "w") as cfgfile: + config.write(cfgfile) + except OSError as ex: + raise exceptions.QiskitUserConfigError( + "Unable to load the config file {}. Error: '{}'".format(filename, str(ex)) + ) + + # validates config + user_config = UserConfig(filename) + user_config.read_config_file() + + def get_config(): """Read the config file from the default location or env var diff --git a/releasenotes/notes/set_config-c96a56d1b860386c.yaml b/releasenotes/notes/set_config-c96a56d1b860386c.yaml new file mode 100644 index 000000000000..aeb59a20f4ab --- /dev/null +++ b/releasenotes/notes/set_config-c96a56d1b860386c.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Adds :func:`qiskit.user_config.set_config` function which allows users to set + the user config from Qiskit API. For Example:: + + from qiskit.user_config import set_config + set_config("circuit_drawer", "mpl", section="default", file="settings.conf") + + Section and file_path are optional arguments. If no file_path argument is + specified, config is added to the configured location (using `QISKIT_SETTINGS` + env variable) or the default location `~/.qiskit/settings.conf`. Set the + QISKIT_SETTINGS env variable to use the config files, otherwise the config + in the 'default config file' will be used. Changes to the existing config file + will not be reflected in the current session since the config file is parsed + at import time. diff --git a/test/python/test_user_config.py b/test/python/test_user_config.py index d37bc52d0d5c..4bb272d27140 100644 --- a/test/python/test_user_config.py +++ b/test/python/test_user_config.py @@ -13,8 +13,10 @@ # pylint: disable=missing-docstring import os +import configparser as cp from uuid import uuid4 +from unittest import mock from qiskit import exceptions from qiskit.test import QiskitTestCase from qiskit import user_config @@ -135,14 +137,66 @@ def test_all_options_valid(self): file.flush() config = user_config.UserConfig(self.file_path) config.read_config_file() - self.assertEqual( - { - "circuit_drawer": "latex", - "circuit_mpl_style": "default", - "circuit_mpl_style_path": ["~", "~/.qiskit"], - "transpile_optimization_level": 3, - "num_processes": 15, - "parallel_enabled": False, - }, - config.settings, - ) + + self.assertEqual( + { + "circuit_drawer": "latex", + "circuit_mpl_style": "default", + "circuit_mpl_style_path": ["~", "~/.qiskit"], + "transpile_optimization_level": 3, + "num_processes": 15, + "parallel_enabled": False, + }, + config.settings, + ) + + def test_set_config_all_options_valid(self): + self.addCleanup(os.remove, self.file_path) + + user_config.set_config("circuit_drawer", "latex", file_path=self.file_path) + user_config.set_config("circuit_mpl_style", "default", file_path=self.file_path) + user_config.set_config("circuit_mpl_style_path", "~:~/.qiskit", file_path=self.file_path) + user_config.set_config("transpile_optimization_level", "3", file_path=self.file_path) + user_config.set_config("parallel", "false", file_path=self.file_path) + user_config.set_config("num_processes", "15", file_path=self.file_path) + + config_settings = None + with mock.patch.dict(os.environ, {"QISKIT_SETTINGS": self.file_path}, clear=True): + config_settings = user_config.get_config() + + self.assertEqual( + { + "circuit_drawer": "latex", + "circuit_mpl_style": "default", + "circuit_mpl_style_path": ["~", "~/.qiskit"], + "transpile_optimization_level": 3, + "num_processes": 15, + "parallel_enabled": False, + }, + config_settings, + ) + + def test_set_config_multiple_sections(self): + self.addCleanup(os.remove, self.file_path) + + user_config.set_config("circuit_drawer", "latex", file_path=self.file_path) + user_config.set_config("circuit_mpl_style", "default", file_path=self.file_path) + user_config.set_config("transpile_optimization_level", "3", file_path=self.file_path) + + user_config.set_config("circuit_drawer", "latex", section="test", file_path=self.file_path) + user_config.set_config("parallel", "false", section="test", file_path=self.file_path) + user_config.set_config("num_processes", "15", section="test", file_path=self.file_path) + + config = cp.ConfigParser() + config.read(self.file_path) + + self.assertEqual(config.sections(), ["default", "test"]) + + self.assertEqual( + { + "circuit_drawer": "latex", + "circuit_mpl_style": "default", + "transpile_optimization_level": "3", + }, + dict(config.items("default")), + )