فهرست منبع

Bun version validation (#1002)

Elijah Ahianyo 2 سال پیش
والد
کامیت
7e8a4930ba
6فایلهای تغییر یافته به همراه149 افزوده شده و 31 حذف شده
  1. 4 2
      pynecone/constants.py
  2. 19 19
      pynecone/pc.py
  3. 6 3
      pynecone/utils/console.py
  4. 3 0
      pynecone/utils/exec.py
  5. 39 7
      pynecone/utils/prerequisites.py
  6. 78 0
      tests/test_utils.py

+ 4 - 2
pynecone/constants.py

@@ -18,7 +18,7 @@ VERSION = pkg_resources.get_distribution(PACKAGE_NAME).version
 MIN_NODE_VERSION = "16.6.0"
 
 # Valid bun versions.
-MIN_BUN_VERSION = "0.5.8"
+MIN_BUN_VERSION = "0.5.9"
 MAX_BUN_VERSION = "0.5.9"
 INVALID_BUN_VERSIONS = ["0.5.5", "0.5.6", "0.5.7"]
 
@@ -70,8 +70,10 @@ FRONTEND_PORT = "3000"
 BACKEND_PORT = "8000"
 # The backend api url.
 API_URL = "http://localhost:8000"
+# bun root location
+BUN_ROOT_PATH = "$HOME/.bun"
 # The default path where bun is installed.
-BUN_PATH = "$HOME/.bun/bin/bun"
+BUN_PATH = f"{BUN_ROOT_PATH}/bin/bun"
 # Command to install bun.
 INSTALL_BUN = f"curl -fsSL https://bun.sh/install | bash -s -- bun-v{MAX_BUN_VERSION}"
 # Default host in dev mode.

+ 19 - 19
pynecone/pc.py

@@ -38,25 +38,25 @@ def init(
         )
         raise typer.Exit()
 
-    with console.status(f"[bold]Initializing {app_name}"):
-        # Set up the web directory.
-        prerequisites.install_bun()
-        prerequisites.initialize_web_directory()
-
-        # Set up the app directory, only if the config doesn't exist.
-        if not os.path.exists(constants.CONFIG_FILE):
-            prerequisites.create_config(app_name)
-            prerequisites.initialize_app_directory(app_name, template)
-            build.set_pynecone_project_hash()
-            telemetry.send("init", get_config().telemetry_enabled)
-        else:
-            build.set_pynecone_project_hash()
-            telemetry.send("reinit", get_config().telemetry_enabled)
-
-        # Initialize the .gitignore.
-        prerequisites.initialize_gitignore()
-        # Finish initializing the app.
-        console.log(f"[bold green]Finished Initializing: {app_name}")
+    console.rule(f"[bold]Initializing {app_name}")
+    # Set up the web directory.
+    prerequisites.validate_and_install_bun()
+    prerequisites.initialize_web_directory()
+
+    # Set up the app directory, only if the config doesn't exist.
+    if not os.path.exists(constants.CONFIG_FILE):
+        prerequisites.create_config(app_name)
+        prerequisites.initialize_app_directory(app_name, template)
+        build.set_pynecone_project_hash()
+        telemetry.send("init", get_config().telemetry_enabled)
+    else:
+        build.set_pynecone_project_hash()
+        telemetry.send("reinit", get_config().telemetry_enabled)
+
+    # Initialize the .gitignore.
+    prerequisites.initialize_gitignore()
+    # Finish initializing the app.
+    console.log(f"[bold green]Finished Initializing: {app_name}")
 
 
 @cli.command()

+ 6 - 3
pynecone/utils/console.py

@@ -48,18 +48,21 @@ def rule(title: str) -> None:
     _console.rule(title)
 
 
-def ask(question: str, choices: Optional[List[str]] = None) -> str:
+def ask(
+    question: str, choices: Optional[List[str]] = None, default: Optional[str] = None
+) -> str:
     """Takes a prompt question and optionally a list of choices
      and returns the user input.
 
     Args:
         question (str): The question to ask the user.
-        choices (Optional[List[str]]): A list of choices to select from
+        choices (Optional[List[str]]): A list of choices to select from.
+        default(Optional[str]): The default option selected.
 
     Returns:
         A string
     """
-    return Prompt.ask(question, choices=choices)
+    return Prompt.ask(question, choices=choices, default=default)  # type: ignore
 
 
 def status(msg: str) -> Status:

+ 3 - 0
pynecone/utils/exec.py

@@ -38,6 +38,9 @@ def run_frontend(app: App, root: Path, port: str):
         root: root path of the project.
         port: port of the app.
     """
+    # validate bun version
+    prerequisites.validate_and_install_bun(initialize=False)
+
     # Set up the frontend.
     setup_frontend(root)
 

+ 39 - 7
pynecone/utils/prerequisites.py

@@ -52,7 +52,9 @@ def get_bun_version() -> Optional[version.Version]:
     try:
         # Run the bun -v command and capture the output
         result = subprocess.run(
-            ["bun", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
+            [os.path.expandvars(get_config().bun_path), "-v"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
         )
         return version.parse(result.stdout.decode().strip())
     except Exception:
@@ -213,12 +215,16 @@ def initialize_web_directory():
         json.dump(pynecone_json, f, ensure_ascii=False)
 
 
-def install_bun():
-    """Install bun onto the user's system.
+def validate_and_install_bun(initialize=True):
+    """Check that bun version requirements are met. If they are not,
+    ask user whether to install required version.
+
+    Args:
+        initialize: whether this function is called on `pc init` or `pc run`.
 
     Raises:
-        FileNotFoundError: If the required packages are not installed.
         Exit: If the bun version is not supported.
+
     """
     bun_version = get_bun_version()
     if bun_version is not None and (
@@ -229,11 +235,37 @@ def install_bun():
         console.print(
             f"""[red]Bun version {bun_version} is not supported by Pynecone. Please change your to bun version to be between {constants.MIN_BUN_VERSION} and {constants.MAX_BUN_VERSION}."""
         )
-        console.print(
-            f"""[red]Upgrade by running the following command:[/red]\n\n{constants.INSTALL_BUN}"""
+        action = console.ask(
+            "Enter 'yes' to install the latest supported bun version or 'no' to exit.",
+            choices=["yes", "no"],
+            default="no",
         )
-        raise typer.Exit()
 
+        if action == "yes":
+            remove_existing_bun_installation()
+            install_bun()
+            return
+        else:
+            raise typer.Exit()
+
+    if initialize:
+        install_bun()
+
+
+def remove_existing_bun_installation():
+    """Remove existing bun installation."""
+    package_manager = get_package_manager()
+    if os.path.exists(package_manager):
+        console.log("Removing bun...")
+        path_ops.rm(os.path.expandvars(constants.BUN_ROOT_PATH))
+
+
+def install_bun():
+    """Install bun onto the user's system.
+
+    Raises:
+        FileNotFoundError: if unzip or curl packages are not found.
+    """
     # Bun is not supported on Windows.
     if platform.system() == "Windows":
         console.log("Skipping bun installation on Windows.")

+ 78 - 0
tests/test_utils.py

@@ -2,10 +2,16 @@ import typing
 from typing import Any, List, Union
 
 import pytest
+from packaging import version
 
 from pynecone.utils import build, format, imports, prerequisites, types
 from pynecone.vars import Var
 
+V055 = version.parse("0.5.5")
+V059 = version.parse("0.5.9")
+V056 = version.parse("0.5.6")
+V0510 = version.parse("0.5.10")
+
 
 @pytest.mark.parametrize(
     "input,output",
@@ -231,6 +237,78 @@ def test_format_route(route: str, expected: bool):
     assert format.format_route(route) == expected
 
 
+@pytest.mark.parametrize(
+    "bun_version,is_valid, prompt_input",
+    [
+        (V055, False, "yes"),
+        (V059, True, None),
+        (V0510, False, "yes"),
+    ],
+)
+def test_bun_validate_and_install(mocker, bun_version, is_valid, prompt_input):
+    """Test that the bun version on host system is validated properly. Also test that
+    the required bun version is installed should the user opt for it.
+
+    Args:
+        mocker: Pytest mocker object.
+        bun_version: The bun version.
+        is_valid: Whether bun version is valid for running pynecone.
+        prompt_input: The input from user on whether to install bun.
+    """
+    mocker.patch(
+        "pynecone.utils.prerequisites.get_bun_version", return_value=bun_version
+    )
+    mocker.patch("pynecone.utils.prerequisites.console.ask", return_value=prompt_input)
+
+    bun_install = mocker.patch("pynecone.utils.prerequisites.install_bun")
+    remove_existing_bun_installation = mocker.patch(
+        "pynecone.utils.prerequisites.remove_existing_bun_installation"
+    )
+
+    prerequisites.validate_and_install_bun()
+    if not is_valid:
+        remove_existing_bun_installation.assert_called_once()
+    bun_install.assert_called_once()
+
+
+def test_bun_validation_exception(mocker):
+    """Test that an exception is thrown and program exists when user selects no when asked
+    whether to install bun or not.
+
+    Args:
+        mocker: Pytest mocker.
+    """
+    mocker.patch("pynecone.utils.prerequisites.get_bun_version", return_value=V056)
+    mocker.patch("pynecone.utils.prerequisites.console.ask", return_value="no")
+
+    with pytest.raises(RuntimeError):
+        prerequisites.validate_and_install_bun()
+
+
+def test_remove_existing_bun_installation(mocker, tmp_path):
+    """Test that existing bun installation is removed.
+
+    Args:
+        mocker: Pytest mocker.
+        tmp_path: test path.
+    """
+    bun_location = tmp_path / ".bun"
+    bun_location.mkdir()
+
+    mocker.patch(
+        "pynecone.utils.prerequisites.get_package_manager",
+        return_value=str(bun_location),
+    )
+    mocker.patch(
+        "pynecone.utils.prerequisites.os.path.expandvars",
+        return_value=str(bun_location),
+    )
+
+    prerequisites.remove_existing_bun_installation()
+
+    assert not bun_location.exists()
+
+
 def test_setup_frontend(tmp_path, mocker):
     """Test checking if assets content have been
     copied into the .web/public folder.