Repository: iterative/dvc

Issue:
Use path_isin instead of startswith to compare filepaths
The PR #2798 introduced `dvc.utils.fs.path_isin` that can be used to compare child paths and parent paths and therefore, *raw*  string compare with `startswith` can be replaced with this new function.

Refactor usage of `startswith` for paths with `path_isin`.


Discussion:
@efiop, can I take this? If yes, please assign me. :)
@skshetry Sure, go ahead 🙂 
Looked through the codebase. The difference between `path_isin` and `startswith` is that, the former will be false if they are same path. The latter is being used here in the cases of where the path are same as well. Found 5/6 uses of `startswith` only.

Resolving patch:
diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py
--- a/dvc/repo/__init__.py
+++ b/dvc/repo/__init__.py
@@ -20,6 +20,7 @@
 from dvc.path_info import PathInfo
 from dvc.remote.base import RemoteActionNotImplemented
 from dvc.utils import relpath
+from dvc.utils.fs import path_isin
 from dvc.utils.compat import FileNotFoundError
 from dvc.utils.compat import fspath_py35
 from dvc.utils.compat import open as _open
@@ -166,7 +167,7 @@ def _ignore(self):
             + updater.lock.files
         )
 
-        if self.cache.local.cache_dir.startswith(self.root_dir + os.sep):
+        if path_isin(self.cache.local.cache_dir, self.root_dir):
             flist += [self.cache.local.cache_dir]
 
         self.scm.ignore_list(flist)
@@ -193,7 +194,7 @@ def collect(self, target, with_deps=False, recursive=False, graph=None):
             ret = []
             for node in nodes:
                 stage = attrs[node]
-                if stage.path.startswith(target + os.sep):
+                if path_isin(stage.path, target):
                     ret.append(stage)
             return ret
 
diff --git a/dvc/scm/git/__init__.py b/dvc/scm/git/__init__.py
--- a/dvc/scm/git/__init__.py
+++ b/dvc/scm/git/__init__.py
@@ -17,6 +17,7 @@
 from dvc.utils import fix_env
 from dvc.utils import is_binary
 from dvc.utils import relpath
+from dvc.utils.fs import path_isin
 from dvc.utils.compat import cast_bytes_py2
 from dvc.utils.compat import open
 from dvc.utils.compat import str
@@ -134,7 +135,7 @@ def _get_gitignore(self, path):
 
         gitignore = os.path.join(ignore_file_dir, self.GITIGNORE)
 
-        if not gitignore.startswith(os.path.realpath(self.root_dir)):
+        if not path_isin(gitignore, os.path.realpath(self.root_dir)):
             raise FileNotInRepoError(path)
 
         return entry, gitignore
diff --git a/dvc/stage.py b/dvc/stage.py
--- a/dvc/stage.py
+++ b/dvc/stage.py
@@ -18,6 +18,7 @@
 from dvc.utils import dict_md5
 from dvc.utils import fix_env
 from dvc.utils import relpath
+from dvc.utils.fs import path_isin
 from dvc.utils.collections import apply_diff
 from dvc.utils.fs import contains_symlink_up_to
 from dvc.utils.stage import dump_stage_file
@@ -390,8 +391,8 @@ def _check_stage_path(repo, path):
         if not os.path.isdir(real_path):
             raise StagePathNotDirectoryError(path)
 
-        proj_dir = os.path.realpath(repo.root_dir) + os.path.sep
-        if not (real_path + os.path.sep).startswith(proj_dir):
+        proj_dir = os.path.realpath(repo.root_dir)
+        if real_path != proj_dir and not path_isin(real_path, proj_dir):
             raise StagePathOutsideError(path)
 
     @property


Test patch:
diff --git a/tests/func/test_external_repo.py b/tests/func/test_external_repo.py
--- a/tests/func/test_external_repo.py
+++ b/tests/func/test_external_repo.py
@@ -4,6 +4,7 @@
 
 from dvc.external_repo import external_repo
 from dvc.scm.git import Git
+from dvc.utils.fs import path_isin
 
 
 def test_external_repo(erepo):
@@ -22,8 +23,6 @@ def test_external_repo(erepo):
 
         # Check cache_dir is unset
         with external_repo(url) as repo:
-            assert repo.cache.local.cache_dir.startswith(
-                repo.root_dir + os.sep
-            )
+            assert path_isin(repo.cache.local.cache_dir, repo.root_dir)
 
         assert mock.call_count == 1