Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/+subclass-file-lookup.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Honor subclass overrides of ``iter_*_paths`` in ``find_*_file`` and ``find_*_files``, so custom search directories and
their precedence apply to file lookups.
8 changes: 4 additions & 4 deletions docs/howto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ site directories, and :meth:`~platformdirs.api.PlatformDirsABC.find_config_files
print(plugin_file)

Both search the directories of :meth:`~platformdirs.api.PlatformDirsABC.iter_config_paths` and match regular files only,
so a directory named ``config.toml`` is skipped. They never create a directory, even with ``ensure_exists=True``. The
name may contain subdirectories; an absolute path, a drive or a ``..`` component raises :class:`ValueError`. The same
pair exists for data, cache, state, log and runtime files, e.g.
:meth:`~platformdirs.api.PlatformDirsABC.find_data_file`.
so a directory named ``config.toml`` is skipped. Subclasses can override the corresponding ``iter_*_paths`` method to
customize the search paths and their order. They never create a directory, even with ``ensure_exists=True``. The name
may contain subdirectories; an absolute path, a drive or a ``..`` component raises :class:`ValueError`. The same pair
exists for data, cache, state, log and runtime files, e.g. :meth:`~platformdirs.api.PlatformDirsABC.find_data_file`.

Testing code that uses platformdirs
===================================
Expand Down
24 changes: 12 additions & 12 deletions src/platformdirs/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ def find_config_file(self, name: str | os.PathLike[str]) -> Path | None:
:raises ValueError: if ``name`` is absolute, has a drive or climbs out with ``..``.

"""
return next(self._find_files(PlatformDirsABC.iter_config_paths, name), None)
return next(self._find_files(type(self).iter_config_paths, name), None)

def find_config_files(self, name: str | os.PathLike[str]) -> list[Path]:
"""Find every ``name`` file in :meth:`iter_config_paths` order, without creating any directory.
Expand All @@ -633,47 +633,47 @@ def find_config_files(self, name: str | os.PathLike[str]) -> list[Path]:
:raises ValueError: see :meth:`find_config_file`.

"""
return list(self._find_files(PlatformDirsABC.iter_config_paths, name))
return list(self._find_files(type(self).iter_config_paths, name))

def find_data_file(self, name: str | os.PathLike[str]) -> Path | None:
"""Like :meth:`find_config_file`, searching :meth:`iter_data_paths`."""
return next(self._find_files(PlatformDirsABC.iter_data_paths, name), None)
return next(self._find_files(type(self).iter_data_paths, name), None)

def find_data_files(self, name: str | os.PathLike[str]) -> list[Path]:
"""Like :meth:`find_config_files`, searching :meth:`iter_data_paths`."""
return list(self._find_files(PlatformDirsABC.iter_data_paths, name))
return list(self._find_files(type(self).iter_data_paths, name))

def find_cache_file(self, name: str | os.PathLike[str]) -> Path | None:
"""Like :meth:`find_config_file`, searching :meth:`iter_cache_paths`."""
return next(self._find_files(PlatformDirsABC.iter_cache_paths, name), None)
return next(self._find_files(type(self).iter_cache_paths, name), None)

def find_cache_files(self, name: str | os.PathLike[str]) -> list[Path]:
"""Like :meth:`find_config_files`, searching :meth:`iter_cache_paths`."""
return list(self._find_files(PlatformDirsABC.iter_cache_paths, name))
return list(self._find_files(type(self).iter_cache_paths, name))

def find_state_file(self, name: str | os.PathLike[str]) -> Path | None:
"""Like :meth:`find_config_file`, searching :meth:`iter_state_paths`."""
return next(self._find_files(PlatformDirsABC.iter_state_paths, name), None)
return next(self._find_files(type(self).iter_state_paths, name), None)

def find_state_files(self, name: str | os.PathLike[str]) -> list[Path]:
"""Like :meth:`find_config_files`, searching :meth:`iter_state_paths`."""
return list(self._find_files(PlatformDirsABC.iter_state_paths, name))
return list(self._find_files(type(self).iter_state_paths, name))

def find_log_file(self, name: str | os.PathLike[str]) -> Path | None:
"""Like :meth:`find_config_file`, searching :meth:`iter_log_paths`."""
return next(self._find_files(PlatformDirsABC.iter_log_paths, name), None)
return next(self._find_files(type(self).iter_log_paths, name), None)

def find_log_files(self, name: str | os.PathLike[str]) -> list[Path]:
"""Like :meth:`find_config_files`, searching :meth:`iter_log_paths`."""
return list(self._find_files(PlatformDirsABC.iter_log_paths, name))
return list(self._find_files(type(self).iter_log_paths, name))

def find_runtime_file(self, name: str | os.PathLike[str]) -> Path | None:
"""Like :meth:`find_config_file`, searching :meth:`iter_runtime_paths`."""
return next(self._find_files(PlatformDirsABC.iter_runtime_paths, name), None)
return next(self._find_files(type(self).iter_runtime_paths, name), None)

def find_runtime_files(self, name: str | os.PathLike[str]) -> list[Path]:
"""Like :meth:`find_config_files`, searching :meth:`iter_runtime_paths`."""
return list(self._find_files(PlatformDirsABC.iter_runtime_paths, name))
return list(self._find_files(type(self).iter_runtime_paths, name))

def _find_files(
self, iter_paths: Callable[[PlatformDirsABC], Iterator[Path]], name: str | os.PathLike[str]
Expand Down
24 changes: 23 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@


if TYPE_CHECKING:
from collections.abc import Callable, Mapping, Sequence
from collections.abc import Callable, Iterator, Mapping, Sequence
from types import ModuleType

from pytest_mock import MockerFixture
Expand Down Expand Up @@ -470,6 +470,28 @@ def test_find_config_files_orders_user_before_site(dirs: Unix, config_paths: lis
assert dirs.find_config_files("app.toml") == expected


@pytest.mark.parametrize("suffix", _SUFFIXES)
@pytest.mark.parametrize("kind", _KINDS)
def test_find_uses_overridden_path_iterator(
dirs: Unix, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, kind: str, suffix: str
) -> None:
class CustomDirs(Unix):
pass

expected = [_touch(tmp_path / directory / "app.toml") for directory in ("first", "second")]

def iter_paths(self: CustomDirs) -> Iterator[Path]:
assert self.ensure_exists is False
yield from (path.parent for path in expected)

monkeypatch.setattr(CustomDirs, f"iter_{kind}_paths", iter_paths)
custom = CustomDirs(dirs.appname, ensure_exists=True)
result = getattr(custom, f"find_{kind}_{suffix}")("app.toml")
assert result == (expected[0] if suffix == "file" else expected)
assert custom.ensure_exists is True
assert sorted(path.name for path in tmp_path.iterdir()) == ["first", "second"]


def test_find_config_file_accepts_nested_path_like(dirs: Unix, config_paths: list[Path]) -> None:
nested = _touch(config_paths[1] / "sub" / "app.toml")
assert dirs.find_config_file(Path("sub", "app.toml")) == nested
Expand Down