Skip to content

Commit 5122ea2

Browse files
committed
Add "traverse_to_file" to paths.py
1 parent 8cdefe5 commit 5122ea2

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

domdf_python_tools/paths.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,16 @@
6868
"WindowsPathPlus",
6969
"in_directory",
7070
"_P",
71+
"traverse_to_file",
7172
]
7273

7374
newline_default = object()
74-
_P = TypeVar("_P", bound=pathlib.PurePath)
75+
76+
_P = TypeVar("_P", bound=pathlib.Path)
7577
"""
7678
.. versionadded:: 0.11.0
79+
80+
.. versionchanged:: 1.7.0 Now bound to :class:`pathlib.Path`.
7781
"""
7882

7983

@@ -768,3 +772,28 @@ def is_mount(self): # pragma: no cover
768772
"""
769773

770774
raise NotImplementedError("Path.is_mount() is unsupported on this system")
775+
776+
777+
def traverse_to_file(base_directory: _P, *filename: PathLike, height: int = -1) -> _P:
778+
r"""
779+
Traverse the parents of the given directory until the desired file is found.
780+
781+
:param base_directory: The directory to start searching from
782+
:param \*filename: The filename(s) to search for
783+
:param height: The maximum height to traverse to.
784+
785+
.. versionadded:: 1.6.0
786+
"""
787+
788+
if not filename:
789+
raise TypeError("traverse_to_file expected 2 or more arguments, got 1")
790+
791+
for level, directory in enumerate((base_directory, *base_directory.parents)):
792+
if height > 0 and ((level - 1) > height):
793+
break
794+
795+
for file in filename:
796+
if (directory / file).is_file():
797+
return directory
798+
799+
raise FileNotFoundError(f"'{filename[0]!s}' not found in {base_directory}")

tests/test_paths.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
# this package
2323
from domdf_python_tools import paths
24-
from domdf_python_tools.paths import PathPlus, clean_writer, copytree, in_directory
24+
from domdf_python_tools.paths import PathPlus, clean_writer, copytree, in_directory, traverse_to_file
2525
from domdf_python_tools.testing import not_pypy, not_windows
2626

2727

@@ -614,3 +614,36 @@ def test_in_directory(tmp_pathplus):
614614
assert str(os.getcwd()) == str(tmpdir)
615615

616616
assert os.getcwd() == cwd
617+
618+
619+
@pytest.mark.parametrize(
620+
"location, expected",
621+
[
622+
("foo.yml", ''),
623+
("foo/foo.yml", "foo"),
624+
("foo/bar/foo.yml", "foo/bar"),
625+
("foo/bar/baz/foo.yml", "foo/bar/baz"),
626+
]
627+
)
628+
def test_traverse_to_file(tmp_pathplus, location, expected):
629+
(tmp_pathplus / location).parent.maybe_make(parents=True)
630+
(tmp_pathplus / location).touch()
631+
assert traverse_to_file(tmp_pathplus / "foo" / "bar" / "baz", "foo.yml") == tmp_pathplus / expected
632+
633+
634+
# TODO: height
635+
636+
637+
def test_traverse_to_file_errors(tmp_pathplus):
638+
(tmp_pathplus / "foo/bar/baz").parent.maybe_make(parents=True)
639+
if os.sep == '/':
640+
with pytest.raises(FileNotFoundError, match="'foo.yml' not found in .*/foo/bar/baz"):
641+
traverse_to_file(tmp_pathplus / "foo" / "bar" / "baz", "foo.yml")
642+
elif os.sep == '\\':
643+
with pytest.raises(FileNotFoundError, match=r"'foo.yml' not found in .*\\foo\\bar\\baz"):
644+
traverse_to_file(tmp_pathplus / "foo" / "bar" / "baz", "foo.yml")
645+
else:
646+
raise NotImplementedError
647+
648+
with pytest.raises(TypeError, match="traverse_to_file expected 2 or more arguments, got 1"):
649+
traverse_to_file(tmp_pathplus)

0 commit comments

Comments
 (0)