From d54e87794b2cfb66828d63e38f61cf3599f05de7 Mon Sep 17 00:00:00 2001 From: Mohammad Alisafaee <50210674+mohammad-sdsc@users.noreply.github.com> Date: Mon, 4 Nov 2019 14:31:07 +0100 Subject: [PATCH] 730 local project configuration (#777) * feat: allow not committing if empty * feat: local project configuration * feat: commit when there is a change * refactor: move cli config command out of core * refactor: remove renku --config option --- conftest.py | 15 ++-- renku/cli/__init__.py | 41 +++------ renku/cli/config.py | 65 ++++++++++++-- renku/core/commands/client.py | 9 +- renku/core/commands/config.py | 54 ++++++++---- renku/core/commands/dataset.py | 8 +- renku/core/commands/migrate.py | 4 +- renku/core/management/config.py | 146 +++++++++++++++++++------------- renku/core/management/git.py | 25 ++++-- tests/cli/test_config.py | 142 +++++++++++++++++++++++++++++++ tests/core/commands/test_cli.py | 82 +++++++++--------- 11 files changed, 417 insertions(+), 174 deletions(-) create mode 100644 tests/cli/test_config.py diff --git a/conftest.py b/conftest.py index 7e1c73180b..117d26cc90 100644 --- a/conftest.py +++ b/conftest.py @@ -48,22 +48,19 @@ def instance_path(renku_path, monkeypatch): @pytest.fixture() -def runner(monkeypatch): +def runner(): """Create a runner on isolated filesystem.""" - from renku.core.management.config import RENKU_HOME - monkeypatch.setenv('RENKU_CONFIG', RENKU_HOME) return CliRunner() @pytest.fixture -def config_dir(monkeypatch, tmpdir_factory): +def global_config_dir(monkeypatch, tmpdir_factory): """Create a temporary renku config directory.""" from renku.core.management.config import ConfigManagerMixin with monkeypatch.context() as m: - home_dir = tmpdir_factory.mktemp('fake_home') - conf_path = home_dir / 'renku.ini' - m.setattr(ConfigManagerMixin, 'config_path', conf_path) + home_dir = tmpdir_factory.mktemp('fake_home').strpath + m.setattr(ConfigManagerMixin, 'global_config_dir', home_dir) yield m @@ -124,10 +121,8 @@ def generate(args=('update', ), cwd=None, **streams): @pytest.fixture() -def isolated_runner(monkeypatch): +def isolated_runner(): """Create a runner on isolated filesystem.""" - from renku.core.management.config import RENKU_HOME - monkeypatch.setenv('RENKU_CONFIG', RENKU_HOME) runner_ = CliRunner() with runner_.isolated_filesystem(): yield runner_ diff --git a/renku/cli/__init__.py b/renku/cli/__init__.py index abe32691a4..3a33d746d6 100644 --- a/renku/cli/__init__.py +++ b/renku/cli/__init__.py @@ -33,8 +33,7 @@ Options: --version Print version number. - --config PATH Location of client config files. - --config-path Print application config path. + --global-config-path Print global application's config path. --install-completion Install completion for the current shell. --path Location of a Renku repository. [default: (dynamic)] @@ -61,18 +60,7 @@ ``C:\Users\\AppData\Roaming\Renku`` If in doubt where to look for the configuration file, you can display its path -by running ``renku --config-path``. - -You can specify a different location via the ``RENKU_CONFIG`` environment -variable or the ``--config`` command line option. If both are specified, then -the ``--config`` option value is used. For example: - -.. code-block:: console - - $ renku --config ~/renku/config/ init - -instructs Renku to store the configuration files in your ``~/renku/config/`` -directory when running the ``init`` command. +by running ``renku --global-config-path``. """ import uuid @@ -102,8 +90,7 @@ option_use_external_storage from renku.core.commands.version import check_version, print_version from renku.core.management.client import LocalClient -from renku.core.management.config import RENKU_HOME, default_config_dir, \ - print_app_config_path +from renku.core.management.config import ConfigManagerMixin, RENKU_HOME from renku.core.management.repository import default_path #: Monkeypatch Click application. @@ -118,6 +105,14 @@ def _uuid_representer(dumper, data): yaml.add_representer(uuid.UUID, _uuid_representer) +def print_global_config_path(ctx, param, value): + """Print global application's config path.""" + if not value or ctx.resilient_parsing: + return + click.echo(ConfigManagerMixin().global_config_path) + ctx.exit() + + @click.group( cls=IssueFromTraceback, context_settings={ @@ -134,20 +129,12 @@ def _uuid_representer(dumper, data): help=print_version.__doc__ ) @click.option( - '--config', - envvar='RENKU_CONFIG', - default=default_config_dir, - type=click.Path(), - expose_value=False, - help='Location of client config files.' -) -@click.option( - '--config-path', + '--global-config-path', is_flag=True, - callback=print_app_config_path, + callback=print_global_config_path, expose_value=False, is_eager=True, - help=print_app_config_path.__doc__ + help=print_global_config_path.__doc__ ) @click.option( '--install-completion', diff --git a/renku/cli/config.py b/renku/cli/config.py index eac6d571b0..ab5668a9d9 100644 --- a/renku/cli/config.py +++ b/renku/cli/config.py @@ -27,32 +27,79 @@ $ renku config registry https://registry.gitlab.com/demo/demo +By default, configuration is stored locally in the project's directory. Use +``--global`` option to store configuration for all projects in your home +directory. + +Remove values +~~~~~~~~~~~~~ + +To remove a specific key from configuration use: + +.. code-block:: console + + $ renku config --remove registry + +By default, only local configuration is searched for removal. Use ``--global`` +option to remove a global configuration value. + Query values ~~~~~~~~~~~~ -You display a previously set value with: +You can display all configuration values with: + +.. code-block:: console + + $ renku config + +Both local and global configuration files are read. Values in local +configuration take precedence over global values. Use ``--local`` or +``--global`` flag to read corresponding configuration only. + +You can provide a KEY to display only its value: .. code-block:: console $ renku config registry https://registry.gitlab.com/demo/demo - """ import click -from renku.core.commands.config import update_config +from renku.core import errors +from renku.core.commands.config import read_config, update_config @click.command() -@click.argument('key', required=True) +@click.argument('key', required=False, default=None) @click.argument('value', required=False, default=None) +@click.option('--remove', is_flag=True, help='Remove specified key.') +@click.option( + '--local', + 'local_only', + is_flag=True, + help='Read/store from/to local configuration only.' +) @click.option( '--global', - 'is_global', + 'global_only', is_flag=True, - help='Store to global configuration.' + help='Read/store from/to global configuration only.' ) -def config(key, value, is_global): +def config(key, value, remove, local_only, global_only): """Manage configuration options.""" - updated = update_config(key, value, is_global) - click.secho(updated) + is_write = value is not None + + if is_write and remove: + raise errors.UsageError('Cannot remove and set at the same time.') + if remove and not key: + raise errors.UsageError('KEY is missing.') + if local_only and global_only: + raise errors.UsageError('Cannot use --local and --global together.') + + if remove: + update_config(key, remove=remove, global_only=global_only) + elif is_write: + update_config(key, value=value, global_only=global_only) + else: + value = read_config(key, local_only, global_only) + click.secho(value) diff --git a/renku/core/commands/client.py b/renku/core/commands/client.py index a273f30160..12d103fae0 100644 --- a/renku/core/commands/client.py +++ b/renku/core/commands/client.py @@ -44,7 +44,8 @@ def pass_local_client( commit=None, commit_only=None, ignore_std_streams=True, - allow_empty=True, + commit_empty=True, + raise_if_empty=False, lock=None, ): """Pass client from the current context to the decorated command.""" @@ -56,7 +57,8 @@ def pass_local_client( commit=commit, commit_only=commit_only, ignore_std_streams=ignore_std_streams, - allow_empty=allow_empty, + commit_empty=commit_empty, + raise_if_empty=raise_if_empty, lock=lock, ) @@ -75,7 +77,8 @@ def new_func(*args, **kwargs): commit=commit, commit_only=commit_only, ignore_std_streams=ignore_std_streams, - allow_empty=allow_empty, + commit_empty=commit_empty, + raise_if_empty=raise_if_empty, ) stack.enter_context(transaction) diff --git a/renku/core/commands/config.py b/renku/core/commands/config.py index 1cb102f855..9121989b6f 100644 --- a/renku/core/commands/config.py +++ b/renku/core/commands/config.py @@ -16,9 +16,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Get and set Renku repository or global options.""" -import configparser - -from renku.core.management.config import get_config +from renku.core import errors +from renku.core.management.config import CONFIG_LOCAL_PATH from .client import pass_local_client @@ -31,18 +30,39 @@ def _split_section_and_key(key): return 'renku', key -@pass_local_client -def update_config(client, key, value, is_global): - """Manage configuration options.""" - write_op = value is not None - config_ = get_config(client, write_op, is_global) - if write_op: - with config_: - section, config_key = _split_section_and_key(key) - config_.set_value(section, config_key, value) - return value +@pass_local_client( + clean=False, + commit=True, + commit_only=CONFIG_LOCAL_PATH, + commit_empty=False +) +def update_config(client, key, *, value=None, remove=False, global_only=False): + """Add, update, or remove configuration values.""" + section, section_key = _split_section_and_key(key) + if remove: + value = client.remove_value( + section, section_key, global_only=global_only + ) + if value is None: + raise errors.ParameterError('Key "{}" not found.'.format(key)) else: - try: - return config_.get_value(*_split_section_and_key(key)) - except configparser.NoSectionError: - raise KeyError('Requested configuration not found') + client.set_value(section, section_key, value, global_only=global_only) + return value + + +@pass_local_client +def read_config(client, key, local_only, global_only): + """Read configuration.""" + if key: + section, section_key = _split_section_and_key(key) + value = client.get_value( + section, + section_key, + local_only=local_only, + global_only=global_only + ) + if value is None: + raise errors.ParameterError('Key "{}" not found.'.format(key)) + return value + + return client.get_config(local_only=local_only, global_only=global_only) diff --git a/renku/core/commands/dataset.py b/renku/core/commands/dataset.py index 186363bd65..318ae7ae1c 100644 --- a/renku/core/commands/dataset.py +++ b/renku/core/commands/dataset.py @@ -363,14 +363,18 @@ def export_dataset( if access_token is None or len(access_token) == 0: raise InvalidAccessToken() - client.set_value(provider_id, config_key_secret, access_token) + client.set_value( + provider_id, config_key_secret, access_token, global_only=True + ) exporter.set_access_token(access_token) try: destination = exporter.export(publish, selected_tag) except HTTPError as e: if 'unauthorized' in str(e): - client.remove_value(provider_id, config_key_secret) + client.remove_value( + provider_id, config_key_secret, global_only=True + ) raise diff --git a/renku/core/commands/migrate.py b/renku/core/commands/migrate.py index 933c3cdfdf..7f5e419569 100644 --- a/renku/core/commands/migrate.py +++ b/renku/core/commands/migrate.py @@ -22,9 +22,7 @@ @pass_local_client( - clean=True, - commit=True, - allow_empty=False, + clean=True, commit=True, commit_empty=False, raise_if_empty=True ) def migrate_datasets(client): """Migrate dataset metadata.""" diff --git a/renku/core/management/config.py b/renku/core/management/config.py index 882f9a8503..14c1236b8b 100644 --- a/renku/core/management/config.py +++ b/renku/core/management/config.py @@ -17,12 +17,13 @@ # limitations under the License. """Client for handling a configuration.""" import configparser -import fcntl import os +from io import StringIO from pathlib import Path import attr import click +import filelock APP_NAME = 'Renku' """Application name for storing configuration.""" @@ -31,16 +32,8 @@ """Project directory name.""" -def print_app_config_path(ctx, param, value): - """Print application config path.""" - if not value or ctx.resilient_parsing: - return - click.echo(ConfigManagerMixin().config_path) - ctx.exit() - - -def default_config_dir(): - """Return default config directory.""" +def _get_global_config_dir(): + """Return user's config directory.""" return click.get_app_dir(APP_NAME, force_posix=True) @@ -48,84 +41,119 @@ def default_config_dir(): class ConfigManagerMixin: """Client for handling global configuration.""" - config_dir = attr.ib(default=default_config_dir(), converter=str) - config_name = attr.ib(default='renku.ini', converter=str) - - _lock = attr.ib(default=None) + CONFIG_NAME = 'renku.ini' - def __enter__(self): - """Acquire a lock file.""" - lock_name = '{0}/{1}.lock'.format(self.config_dir, self.config_name) - locked_file_descriptor = open(lock_name, 'w+') - fcntl.lockf(locked_file_descriptor, fcntl.LOCK_EX) - self._lock = locked_file_descriptor + _global_config_dir = _get_global_config_dir() - def __exit__(self, type, value, traceback): - """Release lock file.""" - self._lock.close() + @property + def global_config_dir(self): + """Return user's config directory.""" + return self._global_config_dir @property - def config_path(self): - """Renku config path.""" - config = Path(self.config_dir) + def global_config_path(self): + """Renku global (user's) config path.""" + config = Path(self.global_config_dir) if not config.exists(): config.mkdir() - return config / Path(self.config_name) + return str(config / Path(self.CONFIG_NAME)) + + @property + def local_config_path(self): + """Renku local (project) config path.""" + return str(self.renku_path / self.CONFIG_NAME) - def load_config(self): - """Loads global configuration object.""" + @property + def global_config_lock(self): + """Create a user-level config lock.""" + lock_file = '{0}/{1}.lock'.format( + self.global_config_dir, self.CONFIG_NAME + ) + return filelock.FileLock(lock_file, timeout=0) + + def load_config(self, local_only, global_only): + """Loads local, global or both configuration object.""" config = configparser.ConfigParser() - config.read(str(self.config_path)) + if local_only: + config_files = [self.local_config_path] + elif global_only: + config_files = [self.global_config_path] + else: + config_files = [self.global_config_path, self.local_config_path] + + if not local_only: + with self.global_config_lock: + config.read(config_files) + else: + config.read(config_files) return config - def store_config(self, config): - """Persists global configuration object.""" - os.umask(0) - fd = os.open( - str(self.config_path), os.O_CREAT | os.O_RDWR | os.O_TRUNC, 0o600 - ) + def store_config(self, config, global_only): + """Persists locally or globally configuration object. + + Global configuration is updated only when :global_only: is True, + otherwise, updates are written to local project configuration + """ + filepath = self.global_config_path if global_only else \ + self.local_config_path + + if global_only: + os.umask(0) + fd = os.open(filepath, os.O_CREAT | os.O_RDWR | os.O_TRUNC, 0o600) + with self.global_config_lock: + with open(fd, 'w+') as file: + config.write(file) + else: + with open(filepath, 'w+') as file: + config.write(file) - with open(fd, 'w+') as file: - config.write(file) + return self.load_config(local_only=True, global_only=True) - return self.load_config() + def get_config(self, local_only=False, global_only=False): + """Read all configurations.""" + config = self.load_config( + local_only=local_only, global_only=global_only + ) + with StringIO() as output: + config.write(output) + return output.getvalue() - def get_value(self, section, key): + def get_value(self, section, key, local_only=False, global_only=False): """Get value from specified section and key.""" - config = self.load_config() + config = self.load_config( + local_only=local_only, global_only=global_only + ) return config.get(section, key, fallback=None) - def set_value(self, section, key, value): + def set_value(self, section, key, value, global_only=False): """Set value to specified section and key.""" - config = self.load_config() + local_only = not global_only + config = self.load_config( + local_only=local_only, global_only=global_only + ) if section in config: config[section][key] = value else: config[section] = {key: value} - config = self.store_config(config) - return config + self.store_config(config, global_only=global_only) - def remove_value(self, section, key): + def remove_value(self, section, key, global_only=False): """Remove key from specified section.""" - config = self.load_config() + local_only = not global_only + config = self.load_config( + local_only=local_only, global_only=global_only + ) if section in config: - config[section].pop(key) + value = config[section].pop(key, None) if not config[section].keys(): config.pop(section) - config = self.store_config(config) - return config - + self.store_config(config, global_only=global_only) + return value -def get_config(client, write_op, is_global): - """Get configuration object.""" - if is_global: - return client - if write_op: - return client.repo.config_writer() - return client.repo.config_reader() +CONFIG_LOCAL_PATH = [Path(RENKU_HOME) / ConfigManagerMixin.CONFIG_NAME] diff --git a/renku/core/management/git.py b/renku/core/management/git.py index 84f44f74d1..3ce99e6218 100644 --- a/renku/core/management/git.py +++ b/renku/core/management/git.py @@ -229,7 +229,13 @@ def ensure_unstaged(self, path): pass @contextmanager - def commit(self, author_date=None, commit_only=None, allow_empty=True): + def commit( + self, + author_date=None, + commit_only=None, + commit_empty=True, + raise_if_empty=False + ): """Automatic commit.""" from git import Actor from renku.version import __version__, version_url @@ -261,6 +267,8 @@ def commit(self, author_date=None, commit_only=None, allow_empty=True): committer = Actor('renku {0}'.format(__version__), version_url) + change_types = {} + if commit_only == COMMIT_DIFF_STRATEGY: # Get diff generated in command. change_types = { @@ -294,8 +302,10 @@ def commit(self, author_date=None, commit_only=None, allow_empty=True): if not commit_only: self.repo.git.add('--all') - if not allow_empty and not self.repo.index.diff('HEAD'): - raise errors.NothingToCommit() + if not commit_empty and not self.repo.index.diff('HEAD'): + if raise_if_empty: + raise errors.NothingToCommit() + return argv = [os.path.basename(sys.argv[0])] + sys.argv[1:] @@ -315,7 +325,8 @@ def transaction( commit=True, commit_only=None, ignore_std_streams=False, - allow_empty=True, + commit_empty=True, + raise_if_empty=False ): """Perform Git checks and operations.""" if clean: @@ -328,7 +339,11 @@ def transaction( pass if commit: - with self.commit(commit_only=commit_only, allow_empty=allow_empty): + with self.commit( + commit_only=commit_only, + commit_empty=commit_empty, + raise_if_empty=raise_if_empty + ): yield self else: yield self diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py new file mode 100644 index 0000000000..08f5b076f7 --- /dev/null +++ b/tests/cli/test_config.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017-2019 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test ``config`` command.""" +import pytest + +from renku.cli import cli + + +def test_config_value_locally(client, runner, project, global_config_dir): + """Check setting/getting from local configuration.""" + result = runner.invoke(cli, ['config', 'key', 'local-value']) + assert result.exit_code == 0 + + result = runner.invoke(cli, ['config', 'key']) + assert result.exit_code == 0 + assert result.output == 'local-value\n' + # Value set locally is not visible globally + result = runner.invoke(cli, ['config', 'key', '--global']) + assert result.exit_code == 2 + + # Reading non-existing values is an error + result = runner.invoke(cli, ['config', 'non-existing']) + assert result.exit_code == 2 + + +def test_config_value_globally(client, runner, project, global_config_dir): + """Check setting/getting from global configuration.""" + result = runner.invoke(cli, ['config', 'key', 'global-value', '--global']) + assert result.exit_code == 0 + + result = runner.invoke(cli, ['config', 'key']) + assert result.exit_code == 0 + assert result.output == 'global-value\n' + result = runner.invoke(cli, ['config', 'key', '--global']) + assert result.exit_code == 0 + assert result.output == 'global-value\n' + # Value set globally is not visible in local config + result = runner.invoke(cli, ['config', 'key', '--local']) + assert result.exit_code == 2 + + +def test_config_get_non_existing_value( + client, runner, project, global_config_dir +): + """Check getting non-existing value is an error.""" + result = runner.invoke(cli, ['config', 'non-existing']) + assert result.exit_code == 2 + + +def test_local_overrides_global_config( + client, runner, project, global_config_dir +): + """Test setting config both global and locally.""" + result = runner.invoke(cli, ['config', 'key', 'global-value', '--global']) + assert result.exit_code == 0 + + result = runner.invoke(cli, ['config', 'key']) + assert result.exit_code == 0 + assert result.output == 'global-value\n' + + result = runner.invoke(cli, ['config', 'key', 'local-value']) + assert result.exit_code == 0 + + result = runner.invoke(cli, ['config', 'key']) + assert result.exit_code == 0 + assert result.output == 'local-value\n' + + +@pytest.mark.parametrize('global_only', (False, True)) +def test_config_remove_value_locally( + client, runner, project, global_config_dir, global_only +): + """Check removing value from local configuration.""" + param = ['--global'] if global_only else ['--local'] + result = runner.invoke(cli, ['config', 'key', 'some-value'] + param) + assert result.exit_code == 0 + + result = runner.invoke(cli, ['config', 'key'] + param) + assert 'some-value\n' == result.output + + result = runner.invoke(cli, ['config', '--remove', 'key'] + param) + assert result.exit_code == 0 + + result = runner.invoke(cli, ['config', 'key'] + param) + assert 'some-value' not in result.output + + +def test_local_config_committed( + client, runner, data_repository, directory_tree, global_config_dir +): + """Test local configuration update is committed only when it is changed.""" + commit_sha_before = client.repo.head.object.hexsha + + result = runner.invoke(cli, ['config', 'local-key', 'value']) + assert result.exit_code == 0 + commit_sha_after = client.repo.head.object.hexsha + assert commit_sha_after != commit_sha_before + + # Adding the same config should not create a new commit + commit_sha_before = client.repo.head.object.hexsha + + result = runner.invoke(cli, ['config', 'local-key', 'value']) + assert result.exit_code == 0 + commit_sha_after = client.repo.head.object.hexsha + assert commit_sha_after == commit_sha_before + + # Adding a global config should not create a new commit + result = runner.invoke(cli, ['config', 'global-key', 'value', '--global']) + assert result.exit_code == 0 + commit_sha_after = client.repo.head.object.hexsha + assert commit_sha_after == commit_sha_before + + +@pytest.mark.parametrize( + 'args,message', + [(['--remove', 'key', 'value'], 'Cannot remove and set at the same time.'), + (['--remove'], 'KEY is missing'), + (['--local', '--global', 'key', 'value' + ], 'Cannot use --local and --global together.')] +) +def test_invalid_command_args( + client, runner, project, global_config_dir, args, message +): + """Test invalid combination of command-line arguments.""" + result = runner.invoke(cli, ['config'] + args) + assert result.exit_code == 2 + assert message in result.output diff --git a/tests/core/commands/test_cli.py b/tests/core/commands/test_cli.py index 9fede52853..bffc9325ca 100644 --- a/tests/core/commands/test_cli.py +++ b/tests/core/commands/test_cli.py @@ -52,7 +52,7 @@ def test_help(arg, runner): def test_config_path(runner): """Test config path.""" - result = runner.invoke(cli, ['--config-path']) + result = runner.invoke(cli, ['--global-config-path']) output = result.output.split('\n')[0] assert 'renku.ini' in output @@ -755,52 +755,56 @@ def test_input_directory(runner, project, run): ) == set(result.output.strip().split('\n')) -def test_config_manager_creation(client): +@pytest.mark.parametrize( + 'global_only,config_path_attr', + ((False, 'local_config_path'), (True, 'global_config_path')) +) +def test_config_manager_creation( + client, global_config_dir, global_only, config_path_attr +): """Check creation of configuration file.""" - path_ = client.config_path - assert str(path_).endswith('renku.ini') - config = client.load_config() - client.store_config(config) - assert path_.exists() + path = getattr(client, config_path_attr) + assert path.endswith('renku.ini') + config = client.load_config(local_only=False, global_only=False) + client.store_config(config, global_only=global_only) + assert Path(path).exists() -def test_config_manager_set_value(client): +@pytest.mark.parametrize('global_only', (False, True)) +def test_config_manager_set_value(client, global_config_dir, global_only): """Check writing to configuration.""" - client.set_value('zenodo', 'access_token', 'my-secret') + local_only = not global_only + + client.set_value( + 'zenodo', 'access_token', 'my-secret', global_only=global_only + ) - config = client.load_config() + config = client.load_config(local_only=local_only, global_only=global_only) assert config.get('zenodo', 'access_token') == 'my-secret' - config = client.remove_value('zenodo', 'access_token') + client.remove_value('zenodo', 'access_token', global_only=global_only) + config = client.load_config(local_only=local_only, global_only=global_only) assert 'zenodo' not in config.sections() -def test_config_load_get_value(client): +def test_config_get_value(client): """Check reading from configuration.""" - client.set_value('zenodo', 'access_token', 'my-secret') - secret = client.get_value('zenodo', 'access_token') - assert secret == 'my-secret' - - secret = client.get_value('zenodo2', 'access_token') - assert secret is None - - secret = client.get_value('zenodo', 'not-secret') - assert secret is None - - config = client.remove_value('zenodo', 'access_token') - assert 'zenodo' not in config.sections() - - -def test_config_manager_cli(client, runner, project, config_dir): - """Check config command for global cfg.""" - result = runner.invoke( - cli, [ - 'config', 'registry', 'http://demo:demo@global.example.com', - '--global' - ] - ) - assert 'http://demo:demo@global.example.com\n' == result.output - assert 0 == result.exit_code - - value = client.get_value('renku', 'registry') - assert 'http://demo:demo@global.example.com' == value + # Value set locally is not visible globally + client.set_value('local', 'key', 'local-value') + value = client.get_value('local', 'key') + assert value == 'local-value' + value = client.get_value('local', 'key', global_only=True) + assert value is None + + # Value set globally is stored globally + client.set_value('global', 'key', 'global-value', global_only=True) + value = client.get_value('global', 'key', local_only=True) + assert value is None + value = client.get_value('global', 'key', global_only=True) + assert value == 'global-value' + value = client.get_value('global', 'key') + assert value == 'global-value' + + # Reading non-existing values returns None + value = client.get_value('non-existing', 'key') + assert value is None