diff --git a/docs/changelog/+subclass-file-lookup.bugfix.rst b/docs/changelog/+subclass-file-lookup.bugfix.rst new file mode 100644 index 00000000..44a3e407 --- /dev/null +++ b/docs/changelog/+subclass-file-lookup.bugfix.rst @@ -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. diff --git a/docs/howto.rst b/docs/howto.rst index 98204e21..c76c7d42 100644 --- a/docs/howto.rst +++ b/docs/howto.rst @@ -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 =================================== diff --git a/src/platformdirs/api.py b/src/platformdirs/api.py index ff2e83cc..8987e87b 100644 --- a/src/platformdirs/api.py +++ b/src/platformdirs/api.py @@ -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. @@ -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] diff --git a/tests/test_api.py b/tests/test_api.py index 763c90f2..2c812c2a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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 @@ -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