Repository: iterative/dvc

Issue:
dvc config: cannot set value if cloud is not configured
```
$ dvc config Global.Cloud AWS
Initialization error: Config file error: can't read google cloud project name. Please set ProjectName in section GC.
```

It should not care about GCP config when I just set a config value.


Discussion:

Resolving patch:
diff --git a/dvc/cli.py b/dvc/cli.py
--- a/dvc/cli.py
+++ b/dvc/cli.py
@@ -123,34 +123,42 @@ def parse_args(argv=None):
                         help='Command or command file to execute')
     run_parser.set_defaults(func=CmdRun)
 
-    # Parent parser used in sync/pull/push
-    parent_sync_parser = argparse.ArgumentParser(
+    # Parent parser used in pull/push/status
+    parent_cache_parser = argparse.ArgumentParser(
                         add_help=False,
                         parents=[parent_parser])
-    parent_sync_parser.add_argument('-j',
+    parent_cache_parser.add_argument('-j',
                         '--jobs',
                         type=int,
                         default=cpu_count(),
                         help='Number of jobs to run simultaneously.')
+    # Cache
+    cache_parser = subparsers.add_parser(
+                        'cache',
+                        parents=[parent_parser],
+                        help='Cache operations')
+    cache_subparsers = cache_parser.add_subparsers(
+                        dest='cmd',
+                        help='Use `dvc cache CMD` --help for command-specific help')
 
     # Pull
-    pull_parser = subparsers.add_parser(
+    pull_parser = cache_subparsers.add_parser(
                         'pull',
-                        parents=[parent_sync_parser],
+                        parents=[parent_cache_parser],
                         help='Pull data files from the cloud')
     pull_parser.set_defaults(func=CmdDataPull)
 
     # Push
-    push_parser = subparsers.add_parser(
+    push_parser = cache_subparsers.add_parser(
                         'push',
-                        parents=[parent_sync_parser],
+                        parents=[parent_cache_parser],
                         help='Push data files to the cloud')
     push_parser.set_defaults(func=CmdDataPush)
 
     # Status
-    status_parser = subparsers.add_parser(
+    status_parser = cache_subparsers.add_parser(
                         'status',
-                        parents=[parent_sync_parser],
+                        parents=[parent_cache_parser],
                         help='Show mismatches between local cache and cloud cache')
     status_parser.set_defaults(func=CmdDataStatus)
 
diff --git a/dvc/command/common/base.py b/dvc/command/common/base.py
--- a/dvc/command/common/base.py
+++ b/dvc/command/common/base.py
@@ -10,7 +10,10 @@ class CmdBase(object):
     def __init__(self, args):
         self.project = Project(self._find_root())
         self.args = args
+        self._set_loglevel(args)
 
+    @staticmethod
+    def _set_loglevel(args):
         if args.quiet and not args.verbose:
             Logger.be_quiet()
         elif not args.quiet and args.verbose:
diff --git a/dvc/command/config.py b/dvc/command/config.py
--- a/dvc/command/config.py
+++ b/dvc/command/config.py
@@ -4,9 +4,19 @@
 from dvc.command.common.base import CmdBase
 from dvc.logger import Logger
 from dvc.config import Config
+from dvc.project import Project
 
 
 class CmdConfig(CmdBase):
+    def __init__(self, args):
+        self.args = args
+        root_dir = self._find_root()
+        self.config_file = os.path.join(root_dir, Project.DVC_DIR, Config.CONFIG)
+        self._set_loglevel(args)
+
+    def run_cmd(self):
+        return self.run()
+
     def _get_key(self, d, name, add=False):
         for k in d.keys():
             if k.lower() == name.lower():
@@ -67,7 +77,7 @@ def check_opt(self):
     def run(self):
         # Using configobj because it doesn't
         # drop comments like configparser does.
-        self.configobj = configobj.ConfigObj(self.project.config.config_file, write_empty_values=True)
+        self.configobj = configobj.ConfigObj(self.config_file, write_empty_values=True)
 
         if self.check_opt() != 0:
             return 1


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -62,19 +62,19 @@ def test(self):
 
 class TestPull(TestDvc):
     def test(self):
-        args = parse_args(['pull'])
+        args = parse_args(['cache', 'pull'])
         self.assertIsInstance(args.func(args), CmdDataPull)
 
 
 class TestPush(TestDvc):
     def test(self):
-        args = parse_args(['push'])
+        args = parse_args(['cache', 'push'])
         self.assertIsInstance(args.func(args), CmdDataPush)
 
 
 class TestStatus(TestDvc):
     def test(self):
-        args = parse_args(['status'])
+        args = parse_args(['cache', 'status'])
         self.assertIsInstance(args.func(args), CmdDataStatus)
 
 
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,5 +1,6 @@
 import os
 import configparser
+import configobj
 from unittest import TestCase
 import mock
 
@@ -17,7 +18,8 @@
     builtin_module_name = 'builtins'
 
 from dvc.cloud.data_cloud import DataCloud
-
+from dvc.main import main
+from tests.basic_env import TestDvc
 
 class TestConfigTest(TestCase):
     def setUp(self):
@@ -141,3 +143,38 @@ def aws_credentials_specified_tes_t_DISABLED(self, isfile_function):
 
         self.assertEqual(aws_creds[0], 'override_access_id')
         self.assertEqual(aws_creds[1], 'override_sekret')
+
+
+class TestConfigCLI(TestDvc):
+    def _contains(self, section, field, value):
+        config = configobj.ConfigObj(self.dvc.config.config_file)
+        if section not in config.keys():
+            return False
+
+        if field not in config[section].keys():
+            return False
+
+        if config[section][field] != value:
+            return False
+
+        return True
+
+    def test(self):
+        section = 'SetSection'
+        field = 'setfield'
+        section_field = '{}.{}'.format(section, field)
+        value = 'setvalue'
+        newvalue = 'setnewvalue'
+
+        ret = main(['config', section_field, value])
+        self.assertEqual(ret, 0)
+        self.assertTrue(self._contains(section, field, value))
+
+        ret = main(['config', section_field, newvalue])
+        self.assertEqual(ret, 0)
+        self.assertTrue(self._contains(section, field, newvalue))
+        self.assertFalse(self._contains(section, field, value))
+
+        ret = main(['config', section_field, '--unset'])
+        self.assertEqual(ret, 0)
+        self.assertFalse(self._contains(section, field, value))