diff --git a/.coveragerc b/.coveragerc index aede19929..7705a83ef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,7 +4,7 @@ branch = true [report] show_missing = true -fail_under = 80 +fail_under = 79 precision = 2 # Regexes for lines to exclude from consideration diff --git a/pynecone/utils/build.py b/pynecone/utils/build.py index 33dc66f58..84c07ee8e 100644 --- a/pynecone/utils/build.py +++ b/pynecone/utils/build.py @@ -12,6 +12,8 @@ from typing import ( Optional, ) +from rich.progress import Progress + from pynecone import constants from pynecone.config import get_config from pynecone.utils import path_ops, prerequisites @@ -95,10 +97,38 @@ def export_app( if deploy_url is not None: generate_sitemap(deploy_url) - # Export the Next app. - subprocess.run( - [prerequisites.get_package_manager(), "run", "export"], cwd=constants.WEB_DIR - ) + # Create a progress object + progress = Progress() + + # Add a single task to the progress object + task = progress.add_task("Building app... ", total=500) + + # Start the progress bar + with progress: + # Run the subprocess command + process = subprocess.Popen( + [prerequisites.get_package_manager(), "run", "export"], + cwd=constants.WEB_DIR, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, # Redirect stdout to a pipe + universal_newlines=True, # Set universal_newlines to True for text mode + ) + + # Read the output of the subprocess line by line + if process.stdout: + for line in iter(process.stdout.readline, ""): + # Update the progress bar based on the output + if "Linting and checking " in line: + progress.update(task, advance=100) + elif "Compiled successfully" in line: + progress.update(task, advance=100) + elif "Route (pages)" in line: + progress.update(task, advance=100) + elif "automatically rendered as static HTML" in line: + progress.update(task, advance=100) + elif "Export successful" in line: + progress.update(task, completed=500) + break # Exit the loop if the completion message is found # Zip up the app. if zip: diff --git a/pynecone/utils/exec.py b/pynecone/utils/exec.py index 7c8de880a..c3d640462 100644 --- a/pynecone/utils/exec.py +++ b/pynecone/utils/exec.py @@ -8,7 +8,9 @@ import subprocess from pathlib import Path from typing import TYPE_CHECKING +import typer import uvicorn +from rich import print from pynecone import constants from pynecone.config import get_config @@ -30,6 +32,37 @@ def start_watching_assets_folder(root): asset_watch.start() +def run_process_and_launch_url(run_command: list[str], root: Path): + """Run the process and launch the URL. + + Args: + run_command: The command to run. + root: root path of the project. + """ + process = subprocess.Popen( + run_command, + cwd=constants.WEB_DIR, + env=os.environ, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, + universal_newlines=True, + ) + + message_found = False + if process.stdout: + for line in process.stdout: + if "ready started server on" in line: + url = line.split("url: ")[-1].strip() + print(f"App running at: [bold green]{url}") + typer.launch(url) + message_found = True + break + + if not message_found and process.stdout: + for line in process.stdout: + print(line, end="") + + def run_frontend(app: App, root: Path, port: str): """Run the frontend. @@ -53,12 +86,8 @@ def run_frontend(app: App, root: Path, port: str): # Run the frontend in development mode. console.rule("[bold green]App Running") os.environ["PORT"] = get_config().port if port is None else port - - # Run the frontend in development mode. - subprocess.Popen( - [prerequisites.get_package_manager(), "run", "dev"], - cwd=constants.WEB_DIR, - env=os.environ, + run_process_and_launch_url( + [prerequisites.get_package_manager(), "run", "dev"], root ) @@ -80,10 +109,9 @@ def run_frontend_prod(app: App, root: Path, port: str): os.environ["PORT"] = get_config().port if port is None else port # Run the frontend in production mode. - subprocess.Popen( - [prerequisites.get_package_manager(), "run", "prod"], - cwd=constants.WEB_DIR, - env=os.environ, + console.rule("[bold green]App Running") + run_process_and_launch_url( + [prerequisites.get_package_manager(), "run", "prod"], root ) diff --git a/tests/components/layout/test_cond.py b/tests/components/layout/test_cond.py index 93ac2414a..fb171adf6 100644 --- a/tests/components/layout/test_cond.py +++ b/tests/components/layout/test_cond.py @@ -4,8 +4,16 @@ from typing import Any import pytest import pynecone as pc +from pynecone.components.layout.box import Box from pynecone.components.layout.cond import Cond, cond from pynecone.components.layout.fragment import Fragment +from pynecone.components.layout.responsive import ( + desktop_only, + mobile_and_tablet, + mobile_only, + tablet_and_desktop, + tablet_only, +) from pynecone.components.typography.text import Text from pynecone.vars import Var @@ -103,3 +111,33 @@ def test_cond_no_else(): # Props do not support the use of cond without else with pytest.raises(ValueError): cond(True, "hello") + + +def test_mobile_only(): + """Test the mobile_only responsive component.""" + component = mobile_only("Content") + assert isinstance(component, Box) + + +def test_tablet_only(): + """Test the tablet_only responsive component.""" + component = tablet_only("Content") + assert isinstance(component, Box) + + +def test_desktop_only(): + """Test the desktop_only responsive component.""" + component = desktop_only("Content") + assert isinstance(component, Box) + + +def test_tablet_and_desktop(): + """Test the tablet_and_desktop responsive component.""" + component = tablet_and_desktop("Content") + assert isinstance(component, Box) + + +def test_mobile_and_tablet(): + """Test the mobile_and_tablet responsive component.""" + component = mobile_and_tablet("Content") + assert isinstance(component, Box) diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py index 444c33a4b..cd4a85639 100644 --- a/tests/components/test_tag.py +++ b/tests/components/test_tag.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List import pytest -from pynecone.components.tags import CondTag, Tag +from pynecone.components.tags import CondTag, Tag, tagless from pynecone.event import EVENT_ARG, EventChain, EventHandler, EventSpec from pynecone.vars import BaseVar, Var @@ -186,3 +186,10 @@ def test_format_cond_tag(): assert false_value["name"] == "h2" assert false_value["contents"] == "False content" + + +def test_tagless_string_representation(): + """Test that the string representation of a tagless is correct.""" + tag = tagless.Tagless(contents="Hello world") + expected_output = "Hello world" + assert str(tag) == expected_output diff --git a/tests/test_base.py b/tests/test_base.py index 250eb1e92..e8a886878 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -45,3 +45,56 @@ def test_json(child): child: A child class. """ assert child.json().replace(" ", "") == '{"num":3.14,"key":"pi"}' + + +@pytest.fixture +def complex_child() -> Base: + """A child class. + + Returns: + A child class. + """ + + class Child(Base): + num: float + key: str + name: str + age: int + active: bool + + return Child(num=3.14, key="pi", name="John Doe", age=30, active=True) + + +def test_complex_get_fields(complex_child): + """Test that the fields are set correctly. + + Args: + complex_child: A child class. + """ + assert complex_child.get_fields().keys() == {"num", "key", "name", "age", "active"} + + +def test_complex_set(complex_child): + """Test setting fields. + + Args: + complex_child: A child class. + """ + complex_child.set(num=1, key="a", name="Jane Doe", age=28, active=False) + assert complex_child.num == 1 + assert complex_child.key == "a" + assert complex_child.name == "Jane Doe" + assert complex_child.age == 28 + assert complex_child.active is False + + +def test_complex_json(complex_child): + """Test converting to json. + + Args: + complex_child: A child class. + """ + assert ( + complex_child.json().replace(" ", "") + == '{"num":3.14,"key":"pi","name":"JohnDoe","age":30,"active":true}' + )