Repository: iterative/dvc

Issue:
"Error: Failed to pull data from the cloud" when pulled data is a directory
When pulling data from remote storage, I execute the following command:
`dvc pull train.dvc`

with content of the file:
_train.dvc_
```
cmd: python train.py
deps:
- md5: 454410a0e96e7c268914287865e94bbc
  path: data/dataset.csv
md5: c1b4f121d34f4dbd426089bbbec26d3c
outs:
- cache: true
  md5: 9f1f44cc23e76e794f7423d4a76147a3.dir
  path: outputs/models/
```

Then obtain the following error:
```
[##############################] 100% Collecting information
[##############################] 100% outputs/models
[##############################] 100% Collecting information
(1/3): [##############################] 100% outputs/models/task_a.pkl
(2/3): [##############################] 100% outputs/models/task_b.pkl
(3/3): [##############################] 100% outputs/models/task_c.pkl
Checking out ' outputs/models' with cache '9f1f44cc23e76e794f7423d4a76147a3.dir'.
Linking directory 'outputs/models'.
Error: Failed to pull data from the cloud: stat: path should be string, bytes, os.PathLike or integer, not NoneType
```

It seems that it happens because of the output of `train.dvc` is a directory. It works fine when it's a file.

some infos:
dvc==0.21.0 installed with pip
macOS 10.14
import: Set output as an optional value, defaulting to the name of the file.
e.g. `dvc import s3://gdelt-open-data/events/20181114.export.csv` would set the output name to `20181114.export.csv`


Discussion:
Hi @guerrapin !

I receive a little bit different error, but none the less, can confirm the bug:

```
#!/bin/bash

set -e
set -x

rm -rf myrepo
mkdir myrepo
cd myrepo

git init
dvc init

mkdir dir
echo foo > dir/foo

dvc add dir

dvc remote add -d upstream $(mktemp -d)

dvc push

rm -rf dir
rm -rf .dvc/cache

dvc pull
```

Looks like something is wrong with directory linking. Looking into it right now.
Correction: the error is the same.


Resolving patch:
diff --git a/dvc/remote/local.py b/dvc/remote/local.py
--- a/dvc/remote/local.py
+++ b/dvc/remote/local.py
@@ -326,10 +326,11 @@ def checkout(self, path_info, checksum_info, force=False):
             entry_checksum_info = {self.PARAM_MD5: m}
 
             if self.changed(entry_info, entry_checksum_info):
-                if force or self._already_cached(p):
-                    remove(p)
-                else:
-                    self._safe_remove(p)
+                if os.path.exists(p):
+                    if force or self._already_cached(p):
+                        remove(p)
+                    else:
+                        self._safe_remove(p)
 
                 self.link(c, p)
 
@@ -346,6 +347,9 @@ def checkout(self, path_info, checksum_info, force=False):
     def _already_cached(self, path):
         current_md5 = self.state.update(path)
 
+        if not current_md5:
+            return False
+
         return not self.changed_cache(current_md5)
 
     def _discard_working_directory_changes(self, path, dir_info, force=False):


Test patch:
diff --git a/tests/test_checkout.py b/tests/test_checkout.py
--- a/tests/test_checkout.py
+++ b/tests/test_checkout.py
@@ -359,3 +359,16 @@ def test(self):
 
         self.assertTrue(os.path.exists(self.FOO))
         self.assertTrue(os.path.exists(self.file1))
+
+
+class TestCheckoutDirectory(TestRepro):
+    def test(self):
+        stage = self.dvc.add(self.DATA_DIR)[0]
+
+        shutil.rmtree(self.DATA_DIR)
+        self.assertFalse(os.path.exists(self.DATA_DIR))
+
+        ret = main(['checkout', stage.path])
+        self.assertEqual(ret, 0)
+
+        self.assertTrue(os.path.exists(self.DATA_DIR))