ignore: introduce .dvcignore, apply when searching for stage files

This commit is contained in:
pared
2019-04-15 14:31:34 +02:00
parent 8f27c14a8c
commit c467ccebcb
14 changed files with 526 additions and 23 deletions
+134
View File
@@ -0,0 +1,134 @@
import os
from dulwich.ignore import match_pattern, read_ignore_patterns
from dvc.utils.compat import cast_bytes
from dvc.utils.fs import get_parent_dirs_up_to
class DvcIgnoreFileHandler(object):
def __init__(self, tree):
self.tree = tree
def read_patterns(self, path):
with self.tree.open(path, binary=True) as stream:
return self._read_patterns(stream)
def get_repo_root(self):
return self.tree.tree_root
def _read_patterns(self, binary_stream):
negate_patterns = []
patterns = []
for pattern in read_ignore_patterns(binary_stream):
if pattern.lstrip().startswith(b"!"):
negate_patterns.append(pattern)
else:
patterns.append(pattern)
return negate_patterns, patterns
class DvcIgnore(object):
DVCIGNORE_FILE = ".dvcignore"
def __call__(self, root, dirs, files):
raise NotImplementedError
class DvcIgnoreFromFile(DvcIgnore):
def __init__(self, ignore_file_path, ignore_handler):
self.ignore_file_path = ignore_file_path
self.dirname = os.path.normpath(os.path.dirname(ignore_file_path))
self.patterns = []
self.negate_patterns = []
self.negate_patterns, self.patterns = ignore_handler.read_patterns(
ignore_file_path
)
def __call__(self, root, dirs, files):
files = [f for f in files if not self.matches(root, f)]
dirs = [d for d in dirs if not self.matches(root, d)]
return dirs, files
def get_match(self, abs_path):
rel_path = os.path.relpath(abs_path, self.dirname)
if os.name == "nt":
rel_path = rel_path.replace("\\", "/")
rel_path = cast_bytes(rel_path, "utf-8")
for pattern in self.patterns:
if match_pattern(
rel_path, pattern
) and self._no_negate_pattern_matches(rel_path):
return (abs_path, pattern, self.ignore_file_path)
return None
def matches(self, dirname, basename):
if self.get_match(os.path.join(dirname, basename)):
return True
return False
def _no_negate_pattern_matches(self, path):
return all([not match_pattern(path, p) for p in self.negate_patterns])
def __hash__(self):
return hash(self.ignore_file_path)
class DvcIgnoreConstant(DvcIgnore):
def __init__(self, basename):
self.basename = basename
class DvcIgnoreDir(DvcIgnoreConstant):
def __call__(self, root, dirs, files):
dirs = [d for d in dirs if not d == self.basename]
return dirs, files
class DvcIgnoreFile(DvcIgnoreConstant):
def __call__(self, root, dirs, files):
files = [f for f in files if not f == self.basename]
return dirs, files
class DvcIgnoreFilter(object):
def __init__(self, wdir, ignore_file_handler=None):
self.ignores = [
DvcIgnoreDir(".git"),
DvcIgnoreDir(".hg"),
DvcIgnoreDir(".dvc"),
DvcIgnoreFile(".dvcignore"),
]
self.ignore_file_handler = ignore_file_handler
self._process_ignores_in_parent_dirs(wdir)
def _process_ignores_in_parent_dirs(self, wdir):
if self.ignore_file_handler:
wdir = os.path.normpath(os.path.abspath(wdir))
ignore_search_end_dir = self.ignore_file_handler.get_repo_root()
parent_dirs = get_parent_dirs_up_to(wdir, ignore_search_end_dir)
for d in parent_dirs:
self.update(d)
def update(self, wdir):
ignore_file_path = os.path.join(wdir, DvcIgnore.DVCIGNORE_FILE)
if os.path.exists(ignore_file_path):
file_ignore = DvcIgnoreFromFile(
ignore_file_path, ignore_handler=self.ignore_file_handler
)
self.ignores.append(file_ignore)
def __call__(self, root, dirs, files):
if self.ignore_file_handler:
self.update(root)
for ignore in self.ignores:
dirs, files = ignore(root, dirs, files)
return dirs, files
+7 -3
View File
@@ -11,7 +11,7 @@ from dvc.exceptions import (
OutputNotFoundError, OutputNotFoundError,
TargetNotDirectoryError, TargetNotDirectoryError,
) )
from dvc.ignore import DvcIgnoreFileHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -58,7 +58,7 @@ class Repo(object):
self.config = Config(self.dvc_dir) self.config = Config(self.dvc_dir)
self.tree = WorkingTree() self.tree = WorkingTree(self.root_dir)
self.scm = SCM(self.root_dir, repo=self) self.scm = SCM(self.root_dir, repo=self)
self.lock = Lock(self.dvc_dir) self.lock = Lock(self.dvc_dir)
@@ -390,7 +390,11 @@ class Repo(object):
stages = [] stages = []
outs = [] outs = []
for root, dirs, files in self.tree.walk(from_directory):
ignore_file_handler = DvcIgnoreFileHandler(self.tree)
for root, dirs, files in self.tree.walk(
from_directory, ignore_file_handler=ignore_file_handler
):
for fname in files: for fname in files:
path = os.path.join(root, fname) path = os.path.join(root, fname)
if not Stage.is_valid_filename(path): if not Stage.is_valid_filename(path):
+1 -1
View File
@@ -27,7 +27,7 @@ def brancher( # noqa: E302
if self.scm.is_dirty(): if self.scm.is_dirty():
from dvc.scm.tree import WorkingTree from dvc.scm.tree import WorkingTree
self.tree = WorkingTree() self.tree = WorkingTree(self.root_dir)
yield "Working Tree" yield "Working Tree"
if all_branches: if all_branches:
+32 -7
View File
@@ -1,7 +1,8 @@
import errno import errno
import os import os
from dvc.utils.compat import StringIO from dvc.ignore import DvcIgnoreFilter
from dvc.utils.compat import StringIO, BytesIO
from dvc.scm.tree import BaseTree from dvc.scm.tree import BaseTree
@@ -24,7 +25,11 @@ class GitTree(BaseTree):
self.git = git self.git = git
self.rev = rev self.rev = rev
def open(self, path): @property
def tree_root(self):
return self.git.working_dir
def open(self, path, binary=False):
relpath = os.path.relpath(path, self.git.working_dir) relpath = os.path.relpath(path, self.git.working_dir)
@@ -39,7 +44,10 @@ class GitTree(BaseTree):
# read it immediately, also it needs to be to decoded if we follow # read it immediately, also it needs to be to decoded if we follow
# the `open()` behavior (since data_stream.read() returns bytes, # the `open()` behavior (since data_stream.read() returns bytes,
# and `open` with default "r" mode returns str) # and `open` with default "r" mode returns str)
return StringIO(obj.data_stream.read().decode("utf-8")) data = obj.data_stream.read()
if binary:
return BytesIO(data)
return StringIO(data.decode("utf-8"))
def exists(self, path): def exists(self, path):
return self.git_object_by_path(path) is not None return self.git_object_by_path(path) is not None
@@ -81,8 +89,13 @@ class GitTree(BaseTree):
tree = tree[i] tree = tree[i]
return tree return tree
def _walk(self, tree, topdown=True): def _walk(
self,
tree,
topdown=True,
ignore_file_handler=None,
dvc_ignore_filter=None,
):
dirs, nondirs = [], [] dirs, nondirs = [], []
for i in tree: for i in tree:
if i.mode == GIT_MODE_DIR: if i.mode == GIT_MODE_DIR:
@@ -91,14 +104,26 @@ class GitTree(BaseTree):
nondirs.append(i.name) nondirs.append(i.name)
if topdown: if topdown:
if not dvc_ignore_filter:
dvc_ignore_filter = DvcIgnoreFilter(
tree.abspath, ignore_file_handler=ignore_file_handler
)
dirs, nondirs = dvc_ignore_filter(tree.path, dirs, nondirs)
yield os.path.normpath(tree.path), dirs, nondirs yield os.path.normpath(tree.path), dirs, nondirs
for i in dirs: for i in dirs:
for x in self._walk(tree[i], topdown=True): for x in self._walk(
tree[i],
topdown=True,
ignore_file_handler=ignore_file_handler,
dvc_ignore_filter=dvc_ignore_filter,
):
yield x yield x
if not topdown: if not topdown:
yield os.path.normpath(tree.path), dirs, nondirs yield os.path.normpath(tree.path), dirs, nondirs
def walk(self, top, topdown=True): def walk(self, top, topdown=True, ignore_file_handler=None):
"""Directory tree generator. """Directory tree generator.
See `os.walk` for the docs. Differences: See `os.walk` for the docs. Differences:
+23 -8
View File
@@ -1,12 +1,17 @@
import os import os
from dvc.utils import dvc_walk
from dvc.utils.compat import open from dvc.utils.compat import open
class BaseTree(object): class BaseTree(object):
"""Abstract class to represent access to files""" """Abstract class to represent access to files"""
def open(self, path): @property
def tree_root(self):
pass
def open(self, path, binary=False):
"""Open file and return a stream.""" """Open file and return a stream."""
def exists(self, path): def exists(self, path):
@@ -18,7 +23,7 @@ class BaseTree(object):
def isfile(self, path): def isfile(self, path):
"""Test whether a path is a regular file""" """Test whether a path is a regular file"""
def walk(self, top, topdown=True): def walk(self, top, topdown=True, ignore_file_handler=None):
"""Directory tree generator. """Directory tree generator.
See `os.walk` for the docs. Differences: See `os.walk` for the docs. Differences:
@@ -30,8 +35,17 @@ class BaseTree(object):
class WorkingTree(BaseTree): class WorkingTree(BaseTree):
"""Proxies the repo file access methods to working tree files""" """Proxies the repo file access methods to working tree files"""
def open(self, path): def __init__(self, repo_root=os.getcwd()):
self.repo_root = repo_root
@property
def tree_root(self):
return self.repo_root
def open(self, path, binary=False):
"""Open file and return a stream.""" """Open file and return a stream."""
if binary:
return open(path, "rb")
return open(path, encoding="utf-8") return open(path, encoding="utf-8")
def exists(self, path): def exists(self, path):
@@ -46,7 +60,7 @@ class WorkingTree(BaseTree):
"""Test whether a path is a regular file""" """Test whether a path is a regular file"""
return os.path.isfile(path) return os.path.isfile(path)
def walk(self, top, topdown=True): def walk(self, top, topdown=True, ignore_file_handler=None):
"""Directory tree generator. """Directory tree generator.
See `os.walk` for the docs. Differences: See `os.walk` for the docs. Differences:
@@ -57,9 +71,10 @@ class WorkingTree(BaseTree):
def onerror(e): def onerror(e):
raise e raise e
for root, dirs, files in os.walk( for root, dirs, files in dvc_walk(
top, topdown=topdown, onerror=onerror top,
topdown=topdown,
onerror=onerror,
ignore_file_handler=ignore_file_handler,
): ):
if topdown:
dirs[:] = [i for i in dirs if i not in (".git", ".hg", ".dvc")]
yield os.path.normpath(root), dirs, files yield os.path.normpath(root), dirs, files
+33 -2
View File
@@ -257,8 +257,39 @@ def load_stage_file_fobj(fobj, path):
raise StageFileCorruptedError(path) raise StageFileCorruptedError(path)
def walk_files(directory): def dvc_walk(
for root, _, files in os.walk(str(directory)): top,
topdown=True,
onerror=None,
followlinks=False,
ignore_file_handler=None,
):
"""
Proxy for `os.walk` directory tree generator.
Utilizes DvcIgnoreFilter functionality.
"""
ignore_filter = None
if topdown:
from dvc.ignore import DvcIgnoreFilter
ignore_filter = DvcIgnoreFilter(
top, ignore_file_handler=ignore_file_handler
)
for root, dirs, files in os.walk(
top, topdown=topdown, onerror=onerror, followlinks=followlinks
):
if ignore_filter:
dirs[:], files[:] = ignore_filter(root, dirs, files)
yield root, dirs, files
def walk_files(directory, ignore_file_handler=None):
for root, _, files in dvc_walk(
str(directory), ignore_file_handler=ignore_file_handler
):
for f in files: for f in files:
yield os.path.join(root, f) yield os.path.join(root, f)
+2 -1
View File
@@ -93,6 +93,7 @@ def _makedirs(name, mode=0o777, exist_ok=False):
if is_py2: if is_py2:
from urlparse import urlparse, urljoin # noqa: F401 from urlparse import urlparse, urljoin # noqa: F401
from StringIO import StringIO # noqa: F401 from StringIO import StringIO # noqa: F401
from io import BytesIO # noqa: F401
from BaseHTTPServer import HTTPServer # noqa: F401 from BaseHTTPServer import HTTPServer # noqa: F401
from SimpleHTTPServer import SimpleHTTPRequestHandler # noqa: F401 from SimpleHTTPServer import SimpleHTTPRequestHandler # noqa: F401
import ConfigParser # noqa: F401 import ConfigParser # noqa: F401
@@ -111,7 +112,7 @@ if is_py2:
elif is_py3: elif is_py3:
from os import makedirs # noqa: F401 from os import makedirs # noqa: F401
from urllib.parse import urlparse, urljoin # noqa: F401 from urllib.parse import urlparse, urljoin # noqa: F401
from io import StringIO # noqa: F401 from io import StringIO, BytesIO # noqa: F401
from http.server import ( # noqa: F401 from http.server import ( # noqa: F401
HTTPServer, # noqa: F401 HTTPServer, # noqa: F401
SimpleHTTPRequestHandler, # noqa: F401 SimpleHTTPRequestHandler, # noqa: F401
+21 -1
View File
@@ -7,6 +7,7 @@ import logging
from dvc.exceptions import DvcException from dvc.exceptions import DvcException
from dvc.system import System from dvc.system import System
from dvc.utils import dvc_walk
from dvc.utils.compat import str from dvc.utils.compat import str
@@ -24,7 +25,7 @@ def get_mtime_and_size(path):
mtime = os.path.getmtime(path) mtime = os.path.getmtime(path)
if os.path.isdir(path): if os.path.isdir(path):
for root, dirs, files in os.walk(str(path)): for root, dirs, files in dvc_walk(str(path)):
for name in dirs + files: for name in dirs + files:
entry = os.path.join(root, name) entry = os.path.join(root, name)
try: try:
@@ -63,3 +64,22 @@ def contains_symlink_up_to(path, base_path):
if os.path.dirname(path) == path: if os.path.dirname(path) == path:
return False return False
return contains_symlink_up_to(os.path.dirname(path), base_path) return contains_symlink_up_to(os.path.dirname(path), base_path)
def get_parent_dirs_up_to(wdir, root_dir):
assert os.path.isabs(wdir)
assert os.path.isabs(root_dir)
wdir = os.path.normpath(wdir)
root_dir = os.path.normpath(root_dir)
if root_dir not in wdir:
return []
dirs = []
dirs.append(wdir)
while wdir != root_dir:
wdir = os.path.dirname(wdir)
dirs.append(wdir)
return dirs
+1
View File
@@ -28,3 +28,4 @@ appdirs>=1.4.3
treelib>=1.5.5 treelib>=1.5.5
inflect>=2.1.0 inflect>=2.1.0
humanize>=0.5.1 humanize>=0.5.1
dulwich>=0.19.11
+1
View File
@@ -58,6 +58,7 @@ install_requires = [
"treelib>=1.5.5", "treelib>=1.5.5",
"inflect>=2.1.0", "inflect>=2.1.0",
"humanize>=0.5.1", "humanize>=0.5.1",
"dulwich>=0.19.11",
] ]
# Extra dependencies for remote integrations # Extra dependencies for remote integrations
+68
View File
@@ -0,0 +1,68 @@
import os
from dvc.ignore import DvcIgnoreFromFile, DvcIgnore, DvcIgnoreFileHandler
from dvc.utils.compat import cast_bytes
from tests.basic_env import TestDvc
class TestDvcIgnore(TestDvc):
def setUp(self):
super(TestDvcIgnore, self).setUp()
self.ignore_file_handler = DvcIgnoreFileHandler(self.dvc.tree)
def _get_all_paths(self):
paths = []
ignore_file_handler = DvcIgnoreFileHandler(self.dvc.tree)
for root, dirs, files in self.dvc.tree.walk(
self.dvc.root_dir, ignore_file_handler=ignore_file_handler
):
for dname in dirs:
paths.append(os.path.join(root, dname))
for fname in files:
paths.append(os.path.join(root, fname))
return paths
def test_ignore_comments(self):
ignore_file = os.path.join(self.dvc.root_dir, DvcIgnore.DVCIGNORE_FILE)
with open(ignore_file, "w") as fobj:
fobj.write(os.path.basename(self.DATA))
fobj.write(" #this is comment")
ignore = DvcIgnoreFromFile(ignore_file, self.ignore_file_handler)
self.assertEqual(1, len(ignore.patterns))
def test_ignore_in_child_dir(self):
ignore_file = os.path.join(self.dvc.root_dir, DvcIgnore.DVCIGNORE_FILE)
with open(ignore_file, "w") as fobj:
fobj.write("data_dir/data")
forbidden_path = os.path.join(self.dvc.root_dir, self.DATA)
all_paths = self._get_all_paths()
self.assertNotIn(forbidden_path, all_paths)
def test_ignore_in_child_dir_unicode(self):
ignore_file = os.path.join(self.dvc.root_dir, DvcIgnore.DVCIGNORE_FILE)
with open(ignore_file, "wb") as fobj:
fobj.write(cast_bytes(self.UNICODE, "utf-8"))
forbidden_path = os.path.join(self.dvc.root_dir, self.UNICODE)
all_paths = self._get_all_paths()
self.assertNotIn(forbidden_path, all_paths)
def test_ignore_in_parent_dir(self):
ignore_file = os.path.join(self.dvc.root_dir, DvcIgnore.DVCIGNORE_FILE)
with open(ignore_file, "w") as fobj:
fobj.write("data_dir/data")
os.chdir(self.DATA_DIR)
forbidden_path = os.path.join(self.dvc.root_dir, self.DATA)
all_paths = self._get_all_paths()
self.assertNotIn(forbidden_path, all_paths)
+26
View File
@@ -1,3 +1,5 @@
from dvc.main import main
from dvc.stage import Stage
from tests.basic_env import TestDvc from tests.basic_env import TestDvc
from dvc.scm.git import GitTree from dvc.scm.git import GitTree
@@ -44,3 +46,27 @@ class TestCollect(TestDvc):
) )
result = self._check("new_branch", "buzz", False, [["buzz"]]) result = self._check("new_branch", "buzz", False, [["buzz"]])
self.assertEqual([i.rel_path for i in result[0].deps], ["bar"]) self.assertEqual([i.rel_path for i in result[0].deps], ["bar"])
class TestIgnore(TestDvc):
def _stage_name(self, file):
return file + Stage.STAGE_FILE_SUFFIX
def test_should_not_gather_stage_files_from_ignored_d(self):
ret = main(["add", self.FOO, self.BAR, self.DATA, self.DATA_SUB])
self.assertEqual(0, ret)
stages = self.dvc.stages()
self.assertEqual(4, len(stages))
with open(".dvcignore", "w") as fobj:
fobj.write("data_dir")
stages = self.dvc.stages()
self.assertEqual(2, len(stages))
stagenames = [s.relpath for s in stages]
self.assertIn(self._stage_name(self.FOO), stagenames)
self.assertIn(self._stage_name(self.BAR), stagenames)
self.assertNotIn(self._stage_name(self.DATA), stagenames)
self.assertNotIn(self._stage_name(self.DATA_SUB), stagenames)
+132
View File
@@ -0,0 +1,132 @@
import os
import pytest
from dvc.ignore import DvcIgnoreFromFile, DvcIgnoreDir, DvcIgnoreFile
from mock import patch, Mock
from dvc.utils.compat import cast_bytes
def read_pattern(p):
return cast_bytes(p, "utf-8")
def mock_dvcignore(dvcignore_path, negate_patterns, patterns):
negate_patterns = [read_pattern(p) for p in negate_patterns]
patterns = [read_pattern(p) for p in patterns]
mock_ignore_file_handler = Mock()
with patch.object(
mock_ignore_file_handler,
"read_patterns",
return_value=(negate_patterns, patterns),
):
ignore_file = DvcIgnoreFromFile(
dvcignore_path, mock_ignore_file_handler
)
return ignore_file
def test_ignore_from_file_should_filter_dirs_and_files():
dvcignore_path = os.path.join(
os.path.sep, "full", "path", "to", "ignore", "file", ".dvcignore"
)
negate_patterns = []
patterns = ["dir_to_ignore", "file_to_ignore"]
root = os.path.dirname(dvcignore_path)
dirs = ["dir1", "dir2", "dir_to_ignore"]
files = ["file1", "file2", "file_to_ignore"]
ignore = mock_dvcignore(dvcignore_path, negate_patterns, patterns)
new_dirs, new_files = ignore(root, dirs, files)
assert {"dir1", "dir2"} == set(new_dirs)
assert {"file1", "file2"} == set(new_files)
@pytest.mark.parametrize(
"file_to_ignore_relpath, negate_patterns,patterns, expected_match",
[
("to_ignore", [], ["to_ignore"], True),
("to_ignore.txt", [], ["to_ignore*"], True),
(
os.path.join("rel", "p", "p2", "to_ignore"),
[],
["rel/**/to_ignore"],
True,
),
(
os.path.join(
os.path.sep,
"full",
"path",
"to",
"ignore",
"file",
"to_ignore",
),
[],
["to_ignore"],
True,
),
("to_ignore.txt", [], ["/*.txt"], True),
(
os.path.join("rel", "path", "path2", "to_ignore"),
[],
["rel/*/to_ignore"],
False,
),
(os.path.join("path", "to_ignore.txt"), [], ["/*.txt"], False),
(
os.path.join("rel", "path", "path2", "dont_ignore"),
[],
["rel/**/to_ignore"],
False,
),
("dont_ignore.txt", [], ["dont_ignore"], False),
("dont_ignore.txt", ["!dont_ignore.txt"], ["dont*"], False),
],
)
def test_match_ignore_from_file(
file_to_ignore_relpath, negate_patterns, patterns, expected_match
):
dvcignore_path = os.path.join(
os.path.sep, "full", "path", "to", "ignore", "file", ".dvcignore"
)
dvcignore_dirname = os.path.dirname(dvcignore_path)
ignore_file = mock_dvcignore(dvcignore_path, negate_patterns, patterns)
assert (
ignore_file.matches(dvcignore_dirname, file_to_ignore_relpath)
== expected_match
)
@pytest.mark.parametrize("omit_dir", [".git", ".hg", ".dvc"])
def test_should_ignore_dir(omit_dir):
ignore = DvcIgnoreDir(omit_dir)
root = os.path.join(os.path.sep, "walk", "dir", "root")
dirs = [omit_dir, "dir1", "dir2"]
files = []
new_dirs, _ = ignore(root, dirs, files)
assert set(new_dirs) == {"dir1", "dir2"}
def test_should_ignore_file():
dvcignore = ".dvcignore"
ignore = DvcIgnoreFile(dvcignore)
root = os.path.join(os.path.sep, "walk", "dir", "root")
dirs = []
files = ["file1", "file2", dvcignore]
_, new_files = ignore(root, dirs, files)
assert set(new_files) == {"file1", "file2"}
+45
View File
@@ -2,12 +2,14 @@ import os
from unittest import TestCase from unittest import TestCase
import dvc import dvc
import pytest
from dvc.system import System from dvc.system import System
from dvc.utils.compat import str from dvc.utils.compat import str
from dvc.utils.fs import ( from dvc.utils.fs import (
get_mtime_and_size, get_mtime_and_size,
contains_symlink_up_to, contains_symlink_up_to,
BasePathNotInCheckedPathException, BasePathNotInCheckedPathException,
get_parent_dirs_up_to,
) )
from mock import patch from mock import patch
from tests.basic_env import TestDir from tests.basic_env import TestDir
@@ -90,3 +92,46 @@ class TestContainsLink(TestCase):
System, "is_symlink", side_effect=base_path_is_symlink System, "is_symlink", side_effect=base_path_is_symlink
): ):
self.assertFalse(contains_symlink_up_to(target_path, base_path)) self.assertFalse(contains_symlink_up_to(target_path, base_path))
@pytest.mark.parametrize(
"path1, path2",
[
(
os.path.join("non", "abs", "path"),
os.path.join(os.sep, "full", "path"),
),
(
os.path.join(os.sep, "full", "path"),
os.path.join("non", "abs", "path"),
),
],
)
def test_get_parent_dirs_up_to_should_raise_on_no_absolute(path1, path2):
with pytest.raises(AssertionError):
get_parent_dirs_up_to(path1, path2)
@pytest.mark.parametrize(
"path1, path2, expected_dirs",
[
(
os.path.join(os.sep, "non", "matching", "path"),
os.path.join(os.sep, "other", "path"),
[],
),
(
os.path.join(os.sep, "some", "long", "path"),
os.path.join(os.sep, "some"),
[
os.path.join(os.sep, "some"),
os.path.join(os.sep, "some", "long"),
os.path.join(os.sep, "some", "long", "path"),
],
),
],
)
def test_get_parent_dirs_up_to(path1, path2, expected_dirs):
result = get_parent_dirs_up_to(path1, path2)
assert set(result) == set(expected_dirs)