Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

fix: Properly cache svg files in svg_path #5

Merged
merged 7 commits into from
Oct 9, 2023
Merged
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
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