From 2c270585abb383fd5fe9ec82b6422ce9a35985e9 Mon Sep 17 00:00:00 2001 From: jackie-pc Date: Tue, 16 Jan 2024 17:52:28 -0800 Subject: [PATCH] Skip frontend packages install if previously done (#2400) --- reflex/app.py | 2 +- reflex/utils/prerequisites.py | 58 ++++++++++++++++++++++++++++++++--- tests/test_prerequisites.py | 40 +++++++++++++++++++++++- 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 4c99b3982..46b4be7b0 100644 --- a/reflex/app.py +++ b/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()): diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 0d196b3ba..2cad261bf 100644 --- a/reflex/utils/prerequisites.py +++ b/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( diff --git a/tests/test_prerequisites.py b/tests/test_prerequisites.py index 846c9d7eb..711826cbc 100644 --- a/tests/test_prerequisites.py +++ b/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