Repository: iterative/dvc

Issue:
Replace cache pull to cache fetch 
Why don't we use Git approach:

`dvc cache fetch` instead of `dvc cache pull`
`dvc cache pull` --> `dvc cache fetch && dvc checkout`


Discussion:
I realized that `dvc cache pull` is already working as `dvc cache fetch && dvc checkout`
So, we should think about introducing `dvc cache fetch` without checking out.
I totally agree. I'll introduce 'dvc cache fetch' soon, it is a quick addon after all.

Resolving patch:
diff --git a/dvc/cli.py b/dvc/cli.py
--- a/dvc/cli.py
+++ b/dvc/cli.py
@@ -10,7 +10,7 @@
 from dvc.command.remove import CmdRemove
 from dvc.command.run import CmdRun
 from dvc.command.repro import CmdRepro
-from dvc.command.data_sync import CmdDataPush, CmdDataPull, CmdDataStatus
+from dvc.command.data_sync import CmdDataPush, CmdDataPull, CmdDataStatus, CmdDataFetch
 from dvc.command.gc import CmdGC
 from dvc.command.add import CmdAdd
 from dvc.command.config import CmdConfig
@@ -161,6 +161,13 @@ def parse_args(argv=None):
                         help='Push data files to the cloud')
     push_parser.set_defaults(func=CmdDataPush)
 
+    #Fetch
+    fetch_parser = cache_subparsers.add_parser(
+                        'fetch',
+                        parents=[parent_cache_parser],
+                        help='Fetch data files from the cloud')
+    fetch_parser.set_defaults(func=CmdDataFetch)
+
     # Status
     status_parser = cache_subparsers.add_parser(
                         'status',
@@ -194,10 +201,10 @@ def parse_args(argv=None):
     remove_parser = subparsers.add_parser(
                         'remove',
                         parents=[parent_parser],
-                        help='Remove data file or data directory')
+                        help='Remove outputs of DVC file.')
     remove_parser.add_argument('targets',
                         nargs='+',
-                        help='Target to remove - file or directory.')
+                        help='DVC files.')
     remove_parser.set_defaults(func=CmdRemove)
 
     # Garbage collector
diff --git a/dvc/command/data_sync.py b/dvc/command/data_sync.py
--- a/dvc/command/data_sync.py
+++ b/dvc/command/data_sync.py
@@ -40,6 +40,16 @@ def do_run(self, target=None):
         return 0
 
 
+class CmdDataFetch(CmdDataBase):
+    def do_run(self, target=None):
+        try:
+            self.project.fetch(target=target, jobs=self.args.jobs)
+        except DvcException as exc:
+            self.project.logger.error('Failed to fetch data from the cloud', exc)
+            return 1
+        return 0
+
+
 class CmdDataStatus(CmdDataBase):
     def _show(self, status):
         for s in status:
diff --git a/dvc/project.py b/dvc/project.py
--- a/dvc/project.py
+++ b/dvc/project.py
@@ -91,21 +91,15 @@ def add(self, fname):
         stage.dump()
         return stage
 
-    def remove(self, fname):
-        stages = []
-        output = Output.loads(self, fname)
-        for out in self.outs():
-            if out.path == output.path:
-                stage = out.stage()
-                stages.append(stage)
-
-        if len(stages) == 0:
-            raise StageNotFoundError(fname) 
+    def remove(self, target):
+        if not Stage.is_stage_file(target):
+            raise StageNotFoundError(target)
 
-        for stage in stages:
-            stage.remove()
+        stage = Stage.load(self, target)
+        for out in stage.outs:
+            out.remove()
 
-        return stages
+        return stage
 
     def run(self,
             cmd=None,
@@ -207,8 +201,11 @@ def gc(self):
     def push(self, target=None, jobs=1):
         return self.cloud.push(self._used_cache(target), jobs)
 
+    def fetch(self, target=None, jobs=1):
+        return self.cloud.pull(self._used_cache(target), jobs)
+
     def pull(self, target=None, jobs=1):
-        ret = self.cloud.pull(self._used_cache(target), jobs)
+        ret = self.fetch(target, jobs)
         self.checkout()
         return ret
 


Test patch:
diff --git a/tests/test_remove.py b/tests/test_remove.py
--- a/tests/test_remove.py
+++ b/tests/test_remove.py
@@ -12,43 +12,35 @@
 class TestRemove(TestDvc):
     def test(self):
         stage = self.dvc.add(self.FOO)
-        stages_removed = self.dvc.remove(self.FOO)
+        stage_removed = self.dvc.remove(stage.path)
 
-        self.assertEqual(len(stages_removed), 1)
-        self.assertIsInstance(stages_removed[0], Stage)
-        self.assertEqual(stage.path, stages_removed[0].path)
+        self.assertIsInstance(stage_removed, Stage)
+        self.assertEqual(stage.path, stage_removed.path)
+        self.assertFalse(os.path.isfile(self.FOO))
 
 
 class TestRemoveNonExistentFile(TestDvc):
     def test(self):
         with self.assertRaises(StageNotFoundError) as cx:
-            self.dvc.remove('non_existent_file')
-
-
-class TestRemoveFileOutsideOfRepo(TestDvc):
-    def test(self):
-        with self.assertRaises(CmdOutputOutsideOfRepoError) as cx:
-            self.dvc.remove(os.path.join(os.path.dirname(self.dvc.root_dir), self.FOO))
+            self.dvc.remove('non_existent_dvc_file')
 
 
 class TestRemoveDirectory(TestDvc):
     def test(self):
         stage_add = self.dvc.add(self.DATA_DIR)
-        stages_removed = self.dvc.remove(self.DATA_DIR)
-        self.assertEqual(len(stages_removed), 1)
-        stage_removed = stages_removed[0]
+        stage_removed = self.dvc.remove(stage_add.path)
         self.assertEqual(stage_add.path, stage_removed.path)
         self.assertFalse(os.path.exists(self.DATA_DIR))
-        self.assertFalse(os.path.exists(stage_removed.path))
+        self.assertTrue(os.path.exists(stage_removed.path))
 
 
 class TestCmdRemove(TestDvc):
     def test(self):
-        self.dvc.add(self.FOO)
+        stage = self.dvc.add(self.FOO)
         ret = main(['remove',
-                    self.FOO])
+                    stage.path])
         self.assertEqual(ret, 0)
 
         ret = main(['remove',
-                    'non-existing-file'])
+                    'non-existing-dvc-file'])
         self.assertNotEqual(ret, 0)