Repository: reactive-python/reactpy-django (language: python)

Issue:
Rename cache backend to "idom"
There's a high chance we will use our cache backend for more than just web modules in the future. It's best to rename the backend now while there still aren't many django-idom users yet.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 928c33a9..788708ee 100644
--- a/README.md
+++ b/README.md
@@ -106,20 +106,14 @@ You may configure additional options as well:
 # the base URL for all IDOM-releated resources
 IDOM_BASE_URL: str = "_idom/"
 
-# Set cache size limit for loading JS files for IDOM.
-# Only applies when not using Django's caching framework (see below).
-IDOM_WEB_MODULE_LRU_CACHE_SIZE: int | None = None
-
 # Maximum seconds between two reconnection attempts that would cause the client give up.
 # 0 will disable reconnection.
 IDOM_WS_MAX_RECONNECT_DELAY: int = 604800
 
 # Configure a cache for loading JS files
 CACHES = {
-  # Configure a cache for loading JS files for IDOM
-  "idom_web_modules": {"BACKEND": ...},
-  # If the above cache is not configured, then we'll use the "default" instead
-  "default": {"BACKEND": ...},
+  # If "idom" cache is not configured, then we'll use the "default" instead
+  "idom": {"BACKEND": ...},
 }
 ```
 
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index 45cf27bc..eda77a13 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,2 +1,3 @@
 channels <4.0.0
 idom >=0.34.0, <0.35.0
+aiofile >=3.0, <4.0
diff --git a/setup.py b/setup.py
index adeb5937..9957e4ae 100644
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,7 @@ def list2cmdline(cmd_list):
 
 package = {
     "name": name,
-    "python_requires": ">=3.7",
+    "python_requires": ">=3.8",
     "packages": find_packages(str(src_dir)),
     "package_dir": {"": "src"},
     "description": "Control the web with Python",
@@ -52,15 +52,14 @@ def list2cmdline(cmd_list):
     "zip_safe": False,
     "classifiers": [
         "Framework :: Django",
-        "Framework :: Django :: 3.1",
-        "Framework :: Django :: 3.2",
+        "Framework :: Django :: 4.0",
         "Operating System :: OS Independent",
         "Intended Audience :: Developers",
         "Intended Audience :: Science/Research",
         "Topic :: Multimedia :: Graphics",
-        "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
         "Environment :: Web Environment",
     ],
 }
diff --git a/src/django_idom/config.py b/src/django_idom/config.py
index 1d527a9e..0664c964 100644
--- a/src/django_idom/config.py
+++ b/src/django_idom/config.py
@@ -1,7 +1,7 @@
 from typing import Dict
 
 from django.conf import settings
-from django.core.cache import DEFAULT_CACHE_ALIAS
+from django.core.cache import DEFAULT_CACHE_ALIAS, caches
 from idom.core.proto import ComponentConstructor
 
 
@@ -12,17 +12,7 @@
 IDOM_WEB_MODULES_URL = IDOM_BASE_URL + "web_module/"
 IDOM_WS_MAX_RECONNECT_DELAY = getattr(settings, "IDOM_WS_MAX_RECONNECT_DELAY", 604800)
 
-_CACHES = getattr(settings, "CACHES", {})
-if _CACHES:
-    if "idom_web_modules" in getattr(settings, "CACHES", {}):
-        IDOM_WEB_MODULE_CACHE = "idom_web_modules"
-    else:
-        IDOM_WEB_MODULE_CACHE = DEFAULT_CACHE_ALIAS
+if "idom" in getattr(settings, "CACHES", {}):
+    IDOM_CACHE = caches["idom"]
 else:
-    IDOM_WEB_MODULE_CACHE = None
-
-
-# the LRU cache size for the route serving IDOM_WEB_MODULES_DIR files
-IDOM_WEB_MODULE_LRU_CACHE_SIZE = getattr(
-    settings, "IDOM_WEB_MODULE_LRU_CACHE_SIZE", None
-)
+    IDOM_CACHE = caches[DEFAULT_CACHE_ALIAS]
diff --git a/src/django_idom/views.py b/src/django_idom/views.py
index 7dd9578a..4f0e4c82 100644
--- a/src/django_idom/views.py
+++ b/src/django_idom/views.py
@@ -1,43 +1,34 @@
-import asyncio
-import functools
 import os
 
-from django.core.cache import caches
+from aiofile import async_open
+from django.core.exceptions import SuspiciousOperation
 from django.http import HttpRequest, HttpResponse
 from idom.config import IDOM_WED_MODULES_DIR
 
-from .config import IDOM_WEB_MODULE_CACHE, IDOM_WEB_MODULE_LRU_CACHE_SIZE
-
-
-if IDOM_WEB_MODULE_CACHE is None:
-
-    def async_lru_cache(*lru_cache_args, **lru_cache_kwargs):
-        def async_lru_cache_decorator(async_function):
-            @functools.lru_cache(*lru_cache_args, **lru_cache_kwargs)
-            def cached_async_function(*args, **kwargs):
-                coroutine = async_function(*args, **kwargs)
-                return asyncio.ensure_future(coroutine)
-
-            return cached_async_function
-
-        return async_lru_cache_decorator
-
-    @async_lru_cache(IDOM_WEB_MODULE_LRU_CACHE_SIZE)
-    async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
-        file_path = IDOM_WED_MODULES_DIR.current.joinpath(*file.split("/"))
-        return HttpResponse(file_path.read_text(), content_type="text/javascript")
-
-else:
-    _web_module_cache = caches[IDOM_WEB_MODULE_CACHE]
-
-    async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
-        file = IDOM_WED_MODULES_DIR.current.joinpath(*file.split("/")).absolute()
-        last_modified_time = os.stat(file).st_mtime
-        cache_key = f"{file}:{last_modified_time}"
-
-        response = _web_module_cache.get(cache_key)
-        if response is None:
-            response = HttpResponse(file.read_text(), content_type="text/javascript")
-            _web_module_cache.set(cache_key, response, timeout=None)
-
-        return response
+from .config import IDOM_CACHE
+
+
+async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
+    """Gets JavaScript required for IDOM modules at runtime. These modules are
+    returned from cache if available."""
+    web_modules_dir = IDOM_WED_MODULES_DIR.current
+    path = web_modules_dir.joinpath(*file.split("/")).absolute()
+
+    # Prevent attempts to walk outside of the web modules dir
+    if str(web_modules_dir) != os.path.commonpath((path, web_modules_dir)):
+        raise SuspiciousOperation(
+            "Attempt to access a directory outside of IDOM_WED_MODULES_DIR."
+        )
+
+    # Fetch the file from cache, if available
+    last_modified_time = os.stat(path).st_mtime
+    cache_key = f"django_idom:web_module:{str(path).lstrip(str(web_modules_dir))}"
+    response = await IDOM_CACHE.aget(cache_key, version=last_modified_time)
+    if response is None:
+        async with async_open(path, "r") as fp:
+            response = HttpResponse(await fp.read(), content_type="text/javascript")
+        await IDOM_CACHE.adelete(cache_key)
+        await IDOM_CACHE.aset(
+            cache_key, response, timeout=None, version=last_modified_time
+        )
+    return response


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d3097b33..2675c040 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.7, 3.8, 3.9]
+        python-version: ["3.8", "3.9", "3.10"]
     steps:
       - uses: actions/checkout@v2
       - uses: nanasess/setup-chromedriver@master