Bun version validation (#1002)

This commit is contained in:
Elijah Ahianyo 2023-05-12 23:43:11 +00:00 committed by GitHub
parent 5b3d0a51e8
commit 7e8a4930ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 147 additions and 29 deletions

View File

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

View File

@ -38,9 +38,9 @@ def init(
) )
raise typer.Exit() raise typer.Exit()
with console.status(f"[bold]Initializing {app_name}"): console.rule(f"[bold]Initializing {app_name}")
# Set up the web directory. # Set up the web directory.
prerequisites.install_bun() prerequisites.validate_and_install_bun()
prerequisites.initialize_web_directory() prerequisites.initialize_web_directory()
# Set up the app directory, only if the config doesn't exist. # Set up the app directory, only if the config doesn't exist.

View File

@ -48,18 +48,21 @@ def rule(title: str) -> None:
_console.rule(title) _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 """Takes a prompt question and optionally a list of choices
and returns the user input. and returns the user input.
Args: Args:
question (str): The question to ask the user. 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: Returns:
A string A string
""" """
return Prompt.ask(question, choices=choices) return Prompt.ask(question, choices=choices, default=default) # type: ignore
def status(msg: str) -> Status: def status(msg: str) -> Status:

View File

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

View File

@ -52,7 +52,9 @@ def get_bun_version() -> Optional[version.Version]:
try: try:
# Run the bun -v command and capture the output # Run the bun -v command and capture the output
result = subprocess.run( 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()) return version.parse(result.stdout.decode().strip())
except Exception: except Exception:
@ -213,12 +215,16 @@ def initialize_web_directory():
json.dump(pynecone_json, f, ensure_ascii=False) json.dump(pynecone_json, f, ensure_ascii=False)
def install_bun(): def validate_and_install_bun(initialize=True):
"""Install bun onto the user's system. """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: Raises:
FileNotFoundError: If the required packages are not installed.
Exit: If the bun version is not supported. Exit: If the bun version is not supported.
""" """
bun_version = get_bun_version() bun_version = get_bun_version()
if bun_version is not None and ( if bun_version is not None and (
@ -229,11 +235,37 @@ def install_bun():
console.print( 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}.""" 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( action = console.ask(
f"""[red]Upgrade by running the following command:[/red]\n\n{constants.INSTALL_BUN}""" "Enter 'yes' to install the latest supported bun version or 'no' to exit.",
choices=["yes", "no"],
default="no",
) )
if action == "yes":
remove_existing_bun_installation()
install_bun()
return
else:
raise typer.Exit() 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. # Bun is not supported on Windows.
if platform.system() == "Windows": if platform.system() == "Windows":
console.log("Skipping bun installation on Windows.") console.log("Skipping bun installation on Windows.")

View File

@ -2,10 +2,16 @@ import typing
from typing import Any, List, Union from typing import Any, List, Union
import pytest import pytest
from packaging import version
from pynecone.utils import build, format, imports, prerequisites, types from pynecone.utils import build, format, imports, prerequisites, types
from pynecone.vars import Var 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( @pytest.mark.parametrize(
"input,output", "input,output",
@ -231,6 +237,78 @@ def test_format_route(route: str, expected: bool):
assert format.format_route(route) == expected 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): def test_setup_frontend(tmp_path, mocker):
"""Test checking if assets content have been """Test checking if assets content have been
copied into the .web/public folder. copied into the .web/public folder.