瀏覽代碼

Skip frontend packages install if previously done (#2400)

jackie-pc 1 年之前
父節點
當前提交
2c270585ab
共有 3 個文件被更改,包括 94 次插入6 次删除
  1. 1 1
      reflex/app.py
  2. 54 4
      reflex/utils/prerequisites.py
  3. 39 1
      tests/test_prerequisites.py

+ 1 - 1
reflex/app.py

@@ -595,7 +595,7 @@ class App(Base):
                 continue
             _frontend_packages.append(package)
         page_imports.update(_frontend_packages)
-        prerequisites.install_frontend_packages(page_imports)
+        prerequisites.install_frontend_packages(page_imports, get_config())
 
     def _app_root(self, app_wrappers: dict[tuple[int, str], Component]) -> Component:
         for component in tuple(app_wrappers.values()):

+ 54 - 4
reflex/utils/prerequisites.py

@@ -16,6 +16,7 @@ import zipfile
 from fileinput import FileInput
 from pathlib import Path
 from types import ModuleType
+from typing import Callable
 
 import httpx
 import pkg_resources
@@ -26,7 +27,7 @@ from redis.asyncio import Redis
 
 from reflex import constants, model
 from reflex.compiler import templates
-from reflex.config import get_config
+from reflex.config import Config, get_config
 from reflex.utils import console, path_ops, processes
 
 
@@ -619,14 +620,64 @@ def install_bun():
     )
 
 
-def install_frontend_packages(packages: set[str]):
+def _write_cached_procedure_file(payload: str, cache_file: str):
+    with open(cache_file, "w") as f:
+        f.write(payload)
+
+
+def _read_cached_procedure_file(cache_file: str) -> str | None:
+    if os.path.exists(cache_file):
+        with open(cache_file, "r") as f:
+            return f.read()
+    return None
+
+
+def _clear_cached_procedure_file(cache_file: str):
+    if os.path.exists(cache_file):
+        os.remove(cache_file)
+
+
+def cached_procedure(cache_file: str, payload_fn: Callable[..., str]):
+    """Decorator to cache the runs of a procedure on disk. Procedures should not have
+       a return value.
+
+    Args:
+        cache_file: The file to store the cache payload in.
+        payload_fn: Function that computes cache payload from function args
+
+    Returns:
+        The decorated function.
+    """
+
+    def _inner_decorator(func):
+        def _inner(*args, **kwargs):
+            payload = _read_cached_procedure_file(cache_file)
+            new_payload = payload_fn(*args, **kwargs)
+            if payload != new_payload:
+                _clear_cached_procedure_file(cache_file)
+                func(*args, **kwargs)
+                _write_cached_procedure_file(new_payload, cache_file)
+
+        return _inner
+
+    return _inner_decorator
+
+
+@cached_procedure(
+    cache_file=os.path.join(
+        constants.Dirs.WEB, "reflex.install_frontend_packages.cached"
+    ),
+    payload_fn=lambda p, c: f"{repr(sorted(list(p)))},{c.json()}",
+)
+def install_frontend_packages(packages: set[str], config: Config):
     """Installs the base and custom frontend packages.
 
     Args:
         packages: A list of package names to be installed.
+        config: The config object.
 
     Example:
-        >>> install_frontend_packages(["react", "react-dom"])
+        >>> install_frontend_packages(["react", "react-dom"], get_config())
     """
     # Install the base packages.
     process = processes.new_process(
@@ -637,7 +688,6 @@ def install_frontend_packages(packages: set[str]):
 
     processes.show_status("Installing base frontend packages", process)
 
-    config = get_config()
     if config.tailwind is not None:
         # install tailwind and tailwind plugins as dev dependencies.
         process = processes.new_process(

+ 39 - 1
tests/test_prerequisites.py

@@ -1,10 +1,15 @@
+import tempfile
 from unittest.mock import Mock, mock_open
 
 import pytest
 
 from reflex import constants
 from reflex.config import Config
-from reflex.utils.prerequisites import _update_next_config, initialize_requirements_txt
+from reflex.utils.prerequisites import (
+    _update_next_config,
+    cached_procedure,
+    initialize_requirements_txt,
+)
 
 
 @pytest.mark.parametrize(
@@ -139,3 +144,36 @@ def test_requirements_txt_other_encoding(mocker):
         open_mock().write.call_args[0][0]
         == f"\n{constants.RequirementsTxt.DEFAULTS_STUB}{constants.Reflex.VERSION}\n"
     )
+
+
+def test_cached_procedure():
+    call_count = 0
+
+    @cached_procedure(tempfile.mktemp(), payload_fn=lambda: "constant")
+    def _function_with_no_args():
+        nonlocal call_count
+        call_count += 1
+
+    _function_with_no_args()
+    assert call_count == 1
+    _function_with_no_args()
+    assert call_count == 1
+
+    call_count = 0
+
+    @cached_procedure(
+        tempfile.mktemp(),
+        payload_fn=lambda *args, **kwargs: f"{repr(args), repr(kwargs)}",
+    )
+    def _function_with_some_args(*args, **kwargs):
+        nonlocal call_count
+        call_count += 1
+
+    _function_with_some_args(1, y=2)
+    assert call_count == 1
+    _function_with_some_args(1, y=2)
+    assert call_count == 1
+    _function_with_some_args(100, y=300)
+    assert call_count == 2
+    _function_with_some_args(100, y=300)
+    assert call_count == 2