Repository: iterative/dvc

Issue:
list: granularity not working [qa]
## Bug Report

```console
λ dvc list https://github.com/iterative/example-get-started data/
.gitignore
data.xml
data.xml.dvc
features
prepared

λ dvc list https://github.com/iterative/example-get-started data/data.xml.dvc
data.xml.dvc

λ dvc list https://github.com/iterative/example-get-started data/features
test.pkl
train.pkl

λ dvc list https://github.com/iterative/example-get-started data/features/test.pkl
ERROR: failed to list 'https://github.com/iterative/example-get-started' - The path 'data/features/test.pkl' does not exist in the target repository 'ExternalRepo: 'C:\Users\poj12\AppData\Local\Temp\tmpo4ck0n0advc-clone'' neither as a DVC output nor as a Git-tracked file.
```
> Works for get and import.

- [ ] Also, the error message could be friendlier (the part about `target repository 'ExternalRepo...`).

### Please provide information about your setup

**Output of `dvc version`:**

```console
$ dvc version

```

**Additional Information (if any):**

If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.


Discussion:

Patch:
diff --git a/dvc/external_repo.py b/dvc/external_repo.py
index 5f2232147..809d04b9c 100644
--- a/dvc/external_repo.py
+++ b/dvc/external_repo.py
@@ -72,6 +72,9 @@ class BaseExternalRepo:
 
     _local_cache = None
 
+    def __str__(self):
+        return self.url
+
     @property
     def local_cache(self):
         if hasattr(self, "cache"):
diff --git a/dvc/repo/tree.py b/dvc/repo/tree.py
index 1b12ad68e..1639d144a 100644
--- a/dvc/repo/tree.py
+++ b/dvc/repo/tree.py
@@ -44,6 +44,7 @@ class DvcTree(BaseTree):  # pylint:disable=abstract-method
         return outs
 
     def _get_granular_checksum(self, path, out, remote=None):
+        assert isinstance(path, PathInfo)
         if not self.fetch and not self.stream:
             raise FileNotFoundError
         dir_cache = out.get_dir_cache(remote=remote)
@@ -128,7 +129,7 @@ class DvcTree(BaseTree):  # pylint:disable=abstract-method
         # for dir checksum, we need to check if this is a file inside the
         # directory
         try:
-            self._get_granular_checksum(path, out)
+            self._get_granular_checksum(path_info, out)
             return False
         except FileNotFoundError:
             return True


Test patch:
diff --git a/tests/func/test_ls.py b/tests/func/test_ls.py
index 14d57acf1..85dd9eb1b 100644
--- a/tests/func/test_ls.py
+++ b/tests/func/test_ls.py
@@ -472,3 +472,33 @@ def test_ls_granular(erepo_dir):
         {"isout": False, "isdir": False, "isexec": False, "path": "2"},
         {"isout": False, "isdir": True, "isexec": False, "path": "subdir"},
     ]
+
+
+@pytest.mark.parametrize("use_scm", [True, False])
+def test_ls_target(erepo_dir, use_scm):
+    with erepo_dir.chdir():
+        gen = erepo_dir.scm_gen if use_scm else erepo_dir.dvc_gen
+        gen(
+            {
+                "dir": {
+                    "1": "1",
+                    "2": "2",
+                    "subdir": {"foo": "foo", "bar": "bar"},
+                }
+            },
+            commit="create dir",
+        )
+
+    def _ls(path):
+        return Repo.ls(os.fspath(erepo_dir), path)
+
+    assert _ls(os.path.join("dir", "1")) == [
+        {"isout": False, "isdir": False, "isexec": False, "path": "1"}
+    ]
+    assert _ls(os.path.join("dir", "subdir", "foo")) == [
+        {"isout": False, "isdir": False, "isexec": False, "path": "foo"}
+    ]
+    assert _ls(os.path.join("dir", "subdir")) == [
+        {"isdir": False, "isexec": 0, "isout": False, "path": "bar"},
+        {"isdir": False, "isexec": 0, "isout": False, "path": "foo"},
+    ]
diff --git a/tests/func/test_tree.py b/tests/func/test_tree.py
index ebfd9aea6..8cb49d10d 100644
--- a/tests/func/test_tree.py
+++ b/tests/func/test_tree.py
@@ -211,7 +211,7 @@ def test_repotree_cache_save(tmp_dir, dvc, scm, erepo_dir, local_cloud):
     # into dvc.cache, not fetched or streamed from a remote
     tree = RepoTree(erepo_dir.dvc, stream=True)
     expected = [
-        tree.get_file_hash(erepo_dir / path)
+        tree.get_file_hash(PathInfo(erepo_dir / path))
         for path in ("dir/bar", "dir/subdir/foo")
     ]