Skip to content

Commit

Permalink
fix: Properly cache svg files in svg_path (#5)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
3 people authored Oct 9, 2023
1 parent 548bc81 commit 5a9f864
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 13 deletions.
6 changes: 3 additions & 3 deletions src/pyconify/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
41 changes: 38 additions & 3 deletions src/pyconify/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 14 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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")
23 changes: 22 additions & 1 deletion tests/test_pyconify.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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")
Expand Down

0 comments on commit 5a9f864

Please sign in to comment.