Skip to content

Commit

Permalink
rename tmp_svg to svg_path, and return cached path if possible (#3)
Browse files Browse the repository at this point in the history
* wip

* update svg_path

* style(pre-commit.ci): auto fixes [...]

* add readme

* update tests

* fix test

* fix hint

* test deletion

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
tlambert03 and pre-commit-ci[bot] authored Sep 30, 2023
1 parent 9bb97f1 commit ac740a7
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 66 deletions.
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ data = pyconify.icon_data("fa-brands", "python")
# Get SVG
svg = pyconify.svg("fa-brands", "python")

# Get path to SVG temporary file for the session
file_name = pyconify.temp_svg("fa-brands", "python")
# Get path to SVG on disk
# will either return cached version, or write to temp file
file_name = pyconify.svg_path("fa-brands", "python")

# Get CSS
css = pyconify.css("fa-brands", "python")
Expand All @@ -58,14 +59,20 @@ See details for each of these results in the [Iconify API documentation](https:/
### cache

While the first fetch of any given SVG will require internet access,
pyconfiy caches svgs for faster retrieval and offline use. To
pyconfiy caches svgs for faster retrieval and offline use. To
see or clear cache directory:

```python
import pyconify

pyconify.get_cache_directory() # reveal location of cache
pyconify.clear_cache() # remove the cache directory
# reveal location of cache
# will be ~/.cache/pyconify on linux and macos
# will be %LOCALAPPDATA%\pyconify on windows
# falls back to ~/.pyconify if none of the above are available
pyconify.get_cache_directory()

# remove the cache directory (and all its contents)
pyconify.clear_cache()
```

If you'd like to precache a number of svgs, the current recommendation
Expand All @@ -82,3 +89,8 @@ for key in ICONS_TO_STORE:
```

Later calls to `svg()` will use the cached values.

To specify a custom cache directory, set the `PYCONIFY_CACHE` environment
variable to your desired directory.
To disable caching altogether, set the `PYCONIFY_CACHE` environment variable to
`false` or `0`.
4 changes: 2 additions & 2 deletions src/pyconify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"last_modified",
"search",
"svg",
"temp_svg",
"svg_path",
]

from ._cache import clear_cache, get_cache_directory
Expand All @@ -35,5 +35,5 @@
last_modified,
search,
svg,
temp_svg,
svg_path,
)
59 changes: 48 additions & 11 deletions src/pyconify/_cache.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
from __future__ import annotations

import os
from contextlib import suppress
from pathlib import Path
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")


def svg_cache() -> MutableMapping[str, bytes]: # pragma: no cover
"""Return a cache for SVG files."""
global _SVG_CACHE
if _SVG_CACHE is None:
try:
_SVG_CACHE = _SVGCache()
except Exception:
if DISABLE_CACHE:
_SVG_CACHE = {}
else:
try:
_SVG_CACHE = _SVGCache()
_delete_stale_svgs(_SVG_CACHE)
except Exception:
_SVG_CACHE = {}
return _SVG_CACHE


def clear_cache() -> None:
"""Clear the pyconify svg cache."""
import shutil

from .api import svg_path

shutil.rmtree(get_cache_directory(), ignore_errors=True)
global _SVG_CACHE
_SVG_CACHE = None
with suppress(AttributeError):
svg_path.cache_clear() # type: ignore


def get_cache_directory(app_name: str = "pyconify") -> Path:
"""Return the pyconify svg cache directory."""
if PYCONIFY_CACHE:
return Path(PYCONIFY_CACHE).expanduser().resolve()

if os.name == "posix":
return Path.home() / ".cache" / app_name
elif os.name == "nt":
Expand All @@ -38,14 +52,19 @@ def get_cache_directory(app_name: str = "pyconify") -> Path:
return Path.home() / f".{app_name}" # pragma: no cover


def cache_key(args: tuple, kwargs: dict) -> str:
# delimiter for the cache key
DELIM = "_"


def cache_key(args: tuple, kwargs: dict, last_modified: int) -> str:
"""Generate a key for the cache based on the function arguments."""
_keys: tuple = args
if kwargs:
for item in sorted(kwargs.items()):
if item[1] is not None:
_keys += item
return "-".join(map(str, _keys))
_keys += (last_modified,)
return DELIM.join(map(str, _keys))


class _SVGCache(MutableMapping[str, bytes]):
Expand All @@ -59,23 +78,41 @@ def __init__(self, directory: str | Path | None = None) -> None:
self.path.mkdir(parents=True, exist_ok=True)
self._extention = ".svg"

def path_for(self, _key: str) -> Path:
return self.path.joinpath(f"{_key}{self._extention}")

def _svg_files(self) -> Iterator[Path]:
yield from self.path.glob(f"*{self._extention}")

def __setitem__(self, _key: str, _value: bytes) -> None:
self.path.joinpath(f"{_key}{self._extention}").write_bytes(_value)
self.path_for(_key).write_bytes(_value)

def __getitem__(self, _key: str) -> bytes:
try:
return self.path.joinpath(f"{_key}{self._extention}").read_bytes()
return self.path_for(_key).read_bytes()
except FileNotFoundError:
raise KeyError(_key) from None

def __iter__(self) -> Iterator[str]:
yield from (x.stem for x in self.path.glob(f"*{self._extention}"))
yield from (x.stem for x in self._svg_files())

def __delitem__(self, _key: str) -> None:
self.path.joinpath(f"{_key}{self._extention}").unlink()
self.path_for(_key).unlink()

def __len__(self) -> int:
return len(list(self.path.glob("*{self._extention}")))
return len(list(self._svg_files()))

def __contains__(self, _key: object) -> bool:
return self.path.joinpath(f"{_key}{self._extention}").exists()
return self.path_for(_key).exists() if isinstance(_key, str) else False


def _delete_stale_svgs(cache: MutableMapping) -> None: # pragma: no cover
"""Remove all SVG files with an outdated last_modified date from the cache."""
from .api import last_modified

last_modified_dates = last_modified()
for key in list(cache):
with suppress(ValueError):
prefix, *_, cached_last_mod = key.split(DELIM)
if int(cached_last_mod) < last_modified_dates.get(prefix, 0):
del cache[key]
Loading

0 comments on commit ac740a7

Please sign in to comment.