From 5a9f864a7be449dc9098de613c183edf44a7d070 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 9 Oct 2023 14:56:07 +0200 Subject: [PATCH] fix: Properly cache svg files in `svg_path` (#5) * properly cache in svg_path * style(pre-commit.ci): auto fixes [...] * fix typo * style(pre-commit.ci): auto fixes [...] * add test * style(pre-commit.ci): auto fixes [...] * update docs and test --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Talley Lambert --- src/pyconify/_cache.py | 6 +++--- src/pyconify/api.py | 41 ++++++++++++++++++++++++++++++++++++++--- tests/conftest.py | 20 ++++++++++++++------ tests/test_pyconify.py | 23 ++++++++++++++++++++++- 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/pyconify/_cache.py b/src/pyconify/_cache.py index e40f646..bb8933a 100644 --- a/src/pyconify/_cache.py +++ b/src/pyconify/_cache.py @@ -6,15 +6,15 @@ from typing import Iterator, MutableMapping _SVG_CACHE: MutableMapping[str, bytes] | None = None -PYCONIFY_CACHE = os.environ.get("PYCONIFY_CACHE", "") -DISABLE_CACHE = PYCONIFY_CACHE.lower() in ("0", "false", "no") +PYCONIFY_CACHE: str = os.environ.get("PYCONIFY_CACHE", "") +CACHE_DISABLED: bool = PYCONIFY_CACHE.lower() in {"0", "false", "no"} def svg_cache() -> MutableMapping[str, bytes]: # pragma: no cover """Return a cache for SVG files.""" global _SVG_CACHE if _SVG_CACHE is None: - if DISABLE_CACHE: + if CACHE_DISABLED: _SVG_CACHE = {} else: try: diff --git a/src/pyconify/api.py b/src/pyconify/api.py index 7d55a83..d200086 100644 --- a/src/pyconify/api.py +++ b/src/pyconify/api.py @@ -11,7 +11,7 @@ import requests -from ._cache import _SVGCache, cache_key, svg_cache +from ._cache import CACHE_DISABLED, _SVGCache, cache_key, svg_cache if TYPE_CHECKING: from typing import Callable, TypeVar @@ -141,6 +141,12 @@ def svg( Example: https://api.iconify.design/fluent-emoji-flat/alarm-clock.svg?height=48&width=48 + SVGs are cached to disk by default. To disable caching, set the `PYCONIFY_CACHE` + environment variable to `0` (before importing pyconify). To customize the location + of the cache, set the `PYCONIFY_CACHE` environment variable to the path of the + desired cache directory. To reveal the location of the cache, use + `pyconify.get_cache_directory()`. + Parameters ---------- key: str @@ -229,14 +235,43 @@ def svg_path( ) -> Path: """Similar to `svg` but returns a path to SVG file for `key`. - Arguments are the same as for `pyconfify.api.svg` except for `dir` which is the + Arguments are the same as for `pyconfify.api.svg()` except for `dir` which is the directory to save the SVG file to (it will be passed to `tempfile.mkstemp`). + + If `dir` is specified, the SVG will be downloaded to a temporary file in that + directory, and the path to that file will be returned. The temporary file will be + deleted when the program exits. + + If `dir` is `None` and caching is enabled (the default), the SVG will be downloaded + and cached to disk and the path to the cached file will be returned. If `dir` is + `None` and caching is disabled (by setting the `PYCONIFY_CACHE` environment variable + to `'0'` before import), a temporary file will be created (using `tempfile.mkstemp`) + and the path to that file will be returned. + + As with `pyconfify.api.svg`, calls to `svg_path` result in SVGs being cached to + disk. To disable caching, set the `PYCONIFY_CACHE` environment variable to `0` + (before importing pyconify). To customize the location of the cache, set the + `PYCONIFY_CACHE` environment variable to the path of the desired cache directory. + To reveal the location of the cache, use `pyconify.get_cache_directory()`. """ - # first look for SVG file in cache + # if there is no request to store outside cache + # and default cache is not disabled then get it from cache if dir is None: *_, svg_cache_key = _svg_keys(key, locals()) + if not CACHE_DISABLED and svg_cache_key not in svg_cache(): + # if required fetch the svg from server + svg( + *key, + color=color, + height=height, + width=width, + flip=flip, + rotate=rotate, + box=box, + ) if path := _svg_path(svg_cache_key): # if it exists return that string + # if cache is disabled globally, this will always be None return path # otherwise, we need to download it and save it to a temporary file diff --git a/tests/conftest.py b/tests/conftest.py index 32c0ec2..82c88ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,21 @@ +from pathlib import Path from typing import Iterator -from unittest.mock import patch import pytest -from pyconify import _cache, api +from pyconify import get_cache_directory @pytest.fixture(autouse=True, scope="session") -def no_cache(tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]: - tmp = tmp_path_factory.mktemp("pyconify") - TEST_CACHE = _cache._SVGCache(directory=tmp) - with patch.object(api, "svg_cache", lambda: TEST_CACHE): +def ensure_no_cache() -> Iterator[None]: + """Ensure that tests don't modify the user cache.""" + cache_dir = Path(get_cache_directory()) + exists = cache_dir.exists() + if exists: + # get hash of cache directory + cache_hash = hash(tuple(cache_dir.rglob("*"))) + try: yield + finally: + assert cache_dir.exists() == exists, "Cache directory was created or deleted" + if exists and cache_hash != hash(tuple(cache_dir.rglob("*"))): + raise AssertionError("User Cache directory was modified") diff --git a/tests/test_pyconify.py b/tests/test_pyconify.py index 4e6b58d..42802f4 100644 --- a/tests/test_pyconify.py +++ b/tests/test_pyconify.py @@ -1,7 +1,18 @@ from pathlib import Path +from typing import Iterator +from unittest.mock import patch import pyconify import pytest +from pyconify import _cache, api + + +@pytest.fixture +def no_cache(tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]: + tmp = tmp_path_factory.mktemp("pyconify") + TEST_CACHE = _cache._SVGCache(directory=tmp) + with patch.object(api, "svg_cache", lambda: TEST_CACHE): + yield def test_collections() -> None: @@ -29,6 +40,7 @@ def test_icon_data() -> None: pyconify.icon_data("not", "found") +@pytest.mark.usefixtures("no_cache") def test_svg() -> None: result = pyconify.svg("bi", "alarm", rotate=90, box=True) assert isinstance(result, bytes) @@ -38,7 +50,8 @@ def test_svg() -> None: pyconify.svg("not", "found") -def test_tmp_svg(tmp_path) -> None: +@pytest.mark.usefixtures("no_cache") +def test_tmp_svg(tmp_path: Path) -> None: result1 = pyconify.svg_path("bi", "alarm", rotate=90, box=True) assert isinstance(result1, Path) assert result1.read_bytes() == pyconify.svg("bi", "alarm", rotate=90, box=True) @@ -51,6 +64,14 @@ def test_tmp_svg(tmp_path) -> None: assert result2.read_bytes() == pyconify.svg("bi", "alarm", rotate=90, box=True) +def test_tmp_svg_with_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that we can set the cache directory to tmp_path with monkeypatch.""" + monkeypatch.setattr(_cache, "PYCONIFY_CACHE", str(tmp_path)) + monkeypatch.setattr(_cache, "_SVG_CACHE", None) + result3 = pyconify.svg_path("bi", "alarm-fill") + assert str(result3).startswith(str(_cache.get_cache_directory())) + + def test_css() -> None: result = pyconify.css("bi", "alarm") assert result.startswith(".icon--bi")