From 2a004f1c3ee88aa1acf4ff5b15c67cc2c7b05348 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Thu, 11 May 2023 18:03:51 -0500 Subject: [PATCH 01/18] add a new run plugin cmd --- src/pyscript/plugins/run.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/pyscript/plugins/run.py diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py new file mode 100644 index 0000000..31d22b3 --- /dev/null +++ b/src/pyscript/plugins/run.py @@ -0,0 +1,27 @@ +from typing import Optional + +from pyscript import app, console, plugins + +try: + import rich_click.typer as typer +except ImportError: # pragma: no cover + import typer # type: ignore + + +@app.command() +def run( + path: str = typer.Option(".", help="The path of the project that will run."), + show: Optional[bool] = typer.Option(True, help="Open the app in web browser."), + port: Optional[int] = typer.Option(8000, help="The port that the app will run on."), +): + """ + Creates a local server to run the app on the path and port specified. + """ + + if show: + console.print("Opening in web browser!") + + +@plugins.register +def pyscript_subcommand(): + return run From 5d88c91997edca1f9c6c743ebf8bf992ee4cdd63 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Thu, 11 May 2023 18:15:17 -0500 Subject: [PATCH 02/18] add run cmd --- src/pyscript/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyscript/cli.py b/src/pyscript/cli.py index 398f381..d1e233b 100644 --- a/src/pyscript/cli.py +++ b/src/pyscript/cli.py @@ -7,7 +7,7 @@ from pyscript import __version__, app, console, plugins, typer from pyscript.plugins import hookspecs -DEFAULT_PLUGINS = ["create", "wrap"] +DEFAULT_PLUGINS = ["create", "wrap", "run"] def ok(msg: str = ""): @@ -62,4 +62,5 @@ def main( loaded = pm.load_setuptools_entrypoints("pyscript") for cmd in pm.hook.pyscript_subcommand(): + print(f"CHECKING.... {cmd}") plugins._add_cmd(cmd) From bfd84cf2ed6da2cf94ab2966a1fd2fd2b10e0d31 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Thu, 11 May 2023 18:17:17 -0500 Subject: [PATCH 03/18] add logic to run cmd so that it starts a static server and opens the browser at the right location --- src/pyscript/cli.py | 1 - src/pyscript/plugins/run.py | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pyscript/cli.py b/src/pyscript/cli.py index d1e233b..01fc547 100644 --- a/src/pyscript/cli.py +++ b/src/pyscript/cli.py @@ -62,5 +62,4 @@ def main( loaded = pm.load_setuptools_entrypoints("pyscript") for cmd in pm.hook.pyscript_subcommand(): - print(f"CHECKING.... {cmd}") plugins._add_cmd(cmd) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 31d22b3..dfd5eb5 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -1,3 +1,6 @@ +import socketserver +import webbrowser +from http.server import SimpleHTTPRequestHandler from typing import Optional from pyscript import app, console, plugins @@ -18,6 +21,11 @@ def run( Creates a local server to run the app on the path and port specified. """ + with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: + print(f"Serving at port {port}") + webbrowser.open_new_tab(f"http://localhost:{port}/") + httpd.serve_forever() + if show: console.print("Opening in web browser!") From 8368516a553b8ebacfd156e4d4f25263d9354f5c Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Thu, 11 May 2023 18:18:25 -0500 Subject: [PATCH 04/18] improve message after serving --- src/pyscript/plugins/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index dfd5eb5..8cb8b9b 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -22,7 +22,7 @@ def run( """ with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: - print(f"Serving at port {port}") + print(f"Serving at port {port}. To stop, press Ctrl+C.") webbrowser.open_new_tab(f"http://localhost:{port}/") httpd.serve_forever() From 430637b8bcc5dcbb1f292b6638ae031f256af4ce Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Sun, 14 May 2023 15:41:08 -0500 Subject: [PATCH 05/18] move starting server in its own function so we can better handle exceptions --- src/pyscript/plugins/run.py | 46 ++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 8cb8b9b..9d662df 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -1,5 +1,8 @@ +import os import socketserver +import threading import webbrowser +from functools import partial from http.server import SimpleHTTPRequestHandler from typing import Optional @@ -11,6 +14,20 @@ import typer # type: ignore +def start_server(path: str, show: bool, port: int): + """ + Creates a local server to run the app on the path and port specified. + """ + with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: + print(f"Serving at port {port}. To stop, press Ctrl+C.") + if show: + # Open the web browser in a separate thread after 0.5 seconds. + open_browser = partial(webbrowser.open_new_tab, f"http://localhost:{port}/") + threading.Timer(0.5, open_browser).start() + + httpd.serve_forever() + + @app.command() def run( path: str = typer.Option(".", help="The path of the project that will run."), @@ -21,13 +38,30 @@ def run( Creates a local server to run the app on the path and port specified. """ - with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: - print(f"Serving at port {port}. To stop, press Ctrl+C.") - webbrowser.open_new_tab(f"http://localhost:{port}/") - httpd.serve_forever() + try: + start_server(path, show, port) + except KeyboardInterrupt: + print("\nStopping server... Bye bye!") + raise typer.Exit() + except OSError as e: + if e.errno == 48: + console.print( + f"Error: Port {port} is already in use!", + style="red", + ) + kill_current_process = typer.prompt( + "Do you want to kill the current process and run this app instead?" + ) + breakpoint() + if kill_current_process == "y": + # Kill the current process serving on the port. + os.system(f"lsof -ti:{port} | xargs kill -9") + else: + console.print("Aborting... Choose another port and try again.") + raise typer.Exit() - if show: - console.print("Opening in web browser!") + console.print(f"Error: {e.strerror}", style="red") + raise typer.Exit() @plugins.register From b4d7e4ce11f50ef1b5f107e44faa97bf4dddb2f3 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 12:01:11 -0500 Subject: [PATCH 06/18] improve error management actually moving how we manage stopping the service into the actuall server context manager --- src/pyscript/plugins/run.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 9d662df..ed4bfab 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -1,4 +1,3 @@ -import os import socketserver import threading import webbrowser @@ -25,7 +24,15 @@ def start_server(path: str, show: bool, port: int): open_browser = partial(webbrowser.open_new_tab, f"http://localhost:{port}/") threading.Timer(0.5, open_browser).start() - httpd.serve_forever() + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nStopping server... Bye bye!") + + # Clean after ourselves.... + httpd.shutdown() + httpd.socket.close() + raise typer.Exit() @app.command() @@ -40,27 +47,16 @@ def run( try: start_server(path, show, port) - except KeyboardInterrupt: - print("\nStopping server... Bye bye!") - raise typer.Exit() except OSError as e: if e.errno == 48: console.print( - f"Error: Port {port} is already in use!", + f"Error: Port {port} is already in use! :( Please, stop the process using that port" + f"or ry another port using the --port option.", style="red", ) - kill_current_process = typer.prompt( - "Do you want to kill the current process and run this app instead?" - ) - breakpoint() - if kill_current_process == "y": - # Kill the current process serving on the port. - os.system(f"lsof -ti:{port} | xargs kill -9") - else: - console.print("Aborting... Choose another port and try again.") - raise typer.Exit() + else: + console.print(f"Error: {e.strerror}", style="red") - console.print(f"Error: {e.strerror}", style="red") raise typer.Exit() From 3ab3cc4a4d82ff297fd45a78c01dd80494cb6f43 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 12:07:00 -0500 Subject: [PATCH 07/18] manage socketserver allow_reuse_address to fix ghost instances of the server after process has terminated --- src/pyscript/plugins/run.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index ed4bfab..2990799 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -17,8 +17,15 @@ def start_server(path: str, show: bool, port: int): """ Creates a local server to run the app on the path and port specified. """ + # We need to set the allow_resuse_address to True because socketserver will + # keep the port in use for a while after the server is stopped. + # see https://stackoverflow.com/questions/31745040/ + socketserver.TCPServer.allow_reuse_address = True + + # Start the server within a context manager to make sure we clean up after with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: - print(f"Serving at port {port}. To stop, press Ctrl+C.") + console.print(f"Serving at port {port}. To stop, press Ctrl+C.", style="green") + if show: # Open the web browser in a separate thread after 0.5 seconds. open_browser = partial(webbrowser.open_new_tab, f"http://localhost:{port}/") @@ -27,7 +34,7 @@ def start_server(path: str, show: bool, port: int): try: httpd.serve_forever() except KeyboardInterrupt: - print("\nStopping server... Bye bye!") + console.print("\nStopping server... Bye bye!") # Clean after ourselves.... httpd.shutdown() From 2531a0a42cae29d271c1b61582f136fad7c8dc46 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 12:07:29 -0500 Subject: [PATCH 08/18] improve docstring --- src/pyscript/plugins/run.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 2990799..20f87fc 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -16,6 +16,14 @@ def start_server(path: str, show: bool, port: int): """ Creates a local server to run the app on the path and port specified. + + Args: + path(str): The path of the project that will run. + show(bool): Open the app in web browser. + port(int): The port that the app will run on. + + Returns: + None """ # We need to set the allow_resuse_address to True because socketserver will # keep the port in use for a while after the server is stopped. From 7d622a1ac041431e4b8debdfdadca91fc58a5750 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 12:45:32 -0500 Subject: [PATCH 09/18] make sure path exists --- src/pyscript/plugins/run.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 20f87fc..756223c 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -3,6 +3,7 @@ import webbrowser from functools import partial from http.server import SimpleHTTPRequestHandler +from pathlib import Path from typing import Optional from pyscript import app, console, plugins @@ -30,6 +31,7 @@ def start_server(path: str, show: bool, port: int): # see https://stackoverflow.com/questions/31745040/ socketserver.TCPServer.allow_reuse_address = True + breakpoint() # Start the server within a context manager to make sure we clean up after with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: console.print(f"Serving at port {port}. To stop, press Ctrl+C.", style="green") @@ -52,7 +54,7 @@ def start_server(path: str, show: bool, port: int): @app.command() def run( - path: str = typer.Option(".", help="The path of the project that will run."), + path: Path = typer.Option(Path("."), help="The path of the project that will run."), show: Optional[bool] = typer.Option(True, help="Open the app in web browser."), port: Optional[int] = typer.Option(8000, help="The port that the app will run on."), ): @@ -60,6 +62,11 @@ def run( Creates a local server to run the app on the path and port specified. """ + # First thing we need to do is to check if the path exists + if not path.exists(): + console.print(f"Error: Path {path} does not exist.", style="red") + raise typer.Exit() + try: start_server(path, show, port) except OSError as e: From 17e4d199cd52ece901e604dc93a44e5d8c398f52 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 16:04:05 -0500 Subject: [PATCH 10/18] add function to manage users passing explicit path --- src/pyscript/plugins/run.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 756223c..e0f9d91 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -14,6 +14,25 @@ import typer # type: ignore +def split_path_and_filename(path: Path) -> str: + """Receives a path to a pyscript project or file and returns the base + path of the project and the filename that should be opened (returns + None if the path points to a folder). + + Args: + path (str): The path to the pyscript project or file. + + + Returns: + tuple(str, str|None): The base path of the project and the filename + """ + abs_path = path.absolute() + if path.is_file(): + return abs_path.parts[:-1], abs_path.parts[-1] + else: + return abs_path, None + + def start_server(path: str, show: bool, port: int): """ Creates a local server to run the app on the path and port specified. @@ -31,7 +50,8 @@ def start_server(path: str, show: bool, port: int): # see https://stackoverflow.com/questions/31745040/ socketserver.TCPServer.allow_reuse_address = True - breakpoint() + parts = split_path_and_filename(path) + print(parts) # Start the server within a context manager to make sure we clean up after with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: console.print(f"Serving at port {port}. To stop, press Ctrl+C.", style="green") From 619896b8be5cb937773baa978160d26ebe93b515 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 16:25:20 -0500 Subject: [PATCH 11/18] add proper support for path --- src/pyscript/plugins/run.py | 42 ++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index e0f9d91..8f14f7e 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -14,23 +14,42 @@ import typer # type: ignore +def get_folder_based_http_request_handler(folder: Path) -> SimpleHTTPRequestHandler: + """ + Returns a FolderBasedHTTPRequestHandler with the specified directory. + + Args: + folder (str): The folder that will be served. + + Returns: + FolderBasedHTTPRequestHandler: The SimpleHTTPRequestHandler with the + specified directory. + """ + + class FolderBasedHTTPRequestHandler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=folder, **kwargs) + + return FolderBasedHTTPRequestHandler + + def split_path_and_filename(path: Path) -> str: """Receives a path to a pyscript project or file and returns the base - path of the project and the filename that should be opened (returns - None if the path points to a folder). + path of the project and the filename that should be opened (filename defaults + to "" (empty string) if the path points to a folder). Args: path (str): The path to the pyscript project or file. - Returns: - tuple(str, str|None): The base path of the project and the filename + Returns: + tuple(str, str): The base path of the project and the filename """ abs_path = path.absolute() if path.is_file(): - return abs_path.parts[:-1], abs_path.parts[-1] + return "/".join(abs_path.parts[:-1]), abs_path.parts[-1] else: - return abs_path, None + return abs_path, "" def start_server(path: str, show: bool, port: int): @@ -50,15 +69,18 @@ def start_server(path: str, show: bool, port: int): # see https://stackoverflow.com/questions/31745040/ socketserver.TCPServer.allow_reuse_address = True - parts = split_path_and_filename(path) - print(parts) + app_folder, filename = split_path_and_filename(path) + CustomHTTPRequestHandler = get_folder_based_http_request_handler(app_folder) + # Start the server within a context manager to make sure we clean up after - with socketserver.TCPServer(("", port), SimpleHTTPRequestHandler) as httpd: + with socketserver.TCPServer(("", port), CustomHTTPRequestHandler) as httpd: console.print(f"Serving at port {port}. To stop, press Ctrl+C.", style="green") if show: # Open the web browser in a separate thread after 0.5 seconds. - open_browser = partial(webbrowser.open_new_tab, f"http://localhost:{port}/") + open_browser = partial( + webbrowser.open_new_tab, f"http://localhost:{port}/{filename}" + ) threading.Timer(0.5, open_browser).start() try: From b3a5d59690eef9ba97f522783db3eb18cb1d5bb4 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 16:26:28 -0500 Subject: [PATCH 12/18] app folder to the serving message we print --- src/pyscript/plugins/run.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 8f14f7e..3c29c10 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -74,7 +74,10 @@ def start_server(path: str, show: bool, port: int): # Start the server within a context manager to make sure we clean up after with socketserver.TCPServer(("", port), CustomHTTPRequestHandler) as httpd: - console.print(f"Serving at port {port}. To stop, press Ctrl+C.", style="green") + console.print( + f"Serving from {app_folder} at port {port}. To stop, press Ctrl+C.", + style="green", + ) if show: # Open the web browser in a separate thread after 0.5 seconds. From e11799aa4be5d04de6af3e706a0282a9c9d783c2 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 21:33:27 -0500 Subject: [PATCH 13/18] improve exit conditions --- src/pyscript/plugins/run.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 3c29c10..44cb743 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Optional -from pyscript import app, console, plugins +from pyscript import app, cli, console, plugins try: import rich_click.typer as typer @@ -94,12 +94,14 @@ def start_server(path: str, show: bool, port: int): # Clean after ourselves.... httpd.shutdown() httpd.socket.close() - raise typer.Exit() + raise typer.Exit(1) @app.command() def run( - path: Path = typer.Option(Path("."), help="The path of the project that will run."), + path: Path = typer.Argument( + Path("."), help="The path of the project that will run." + ), show: Optional[bool] = typer.Option(True, help="Open the app in web browser."), port: Optional[int] = typer.Option(8000, help="The port that the app will run on."), ): @@ -109,8 +111,7 @@ def run( # First thing we need to do is to check if the path exists if not path.exists(): - console.print(f"Error: Path {path} does not exist.", style="red") - raise typer.Exit() + raise cli.Abort(f"Error: Path {str(path)} does not exist.", style="red") try: start_server(path, show, port) @@ -124,7 +125,7 @@ def run( else: console.print(f"Error: {e.strerror}", style="red") - raise typer.Exit() + raise cli.Abort("") @plugins.register From f271e173052447da100e906323f97a04f2aaf911 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 21:37:51 -0500 Subject: [PATCH 14/18] add run cmd test files --- tests/test_run_cli_cmd.py | 21 +++++++++++++++++++++ tests/utils.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/test_run_cli_cmd.py create mode 100644 tests/utils.py diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py new file mode 100644 index 0000000..457b124 --- /dev/null +++ b/tests/test_run_cli_cmd.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import pytest +from utils import CLIInvoker + + +@pytest.mark.parametrize( + "path", + ["non_existing_folder", "non_existing_file.html"], + # ids=["passig", "command_and_script", "command_no_output_or_show"], +) +def test_run_bad_paths(invoke_cli: CLIInvoker, path: str): + """ + Test that when wrap is called passing a bad path as input the command fails + """ + # GIVEN a call to wrap with a bad path as argument + result = invoke_cli("run", path) + # EXPECT the command to fail + assert result.exit_code == 1 + # EXPECT the right error message to be printed + assert f"Error: Path {path} does not exist." in result.stdout diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..5abc565 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Callable + +import pytest +from mypy_extensions import VarArg +from typer.testing import CliRunner, Result + +from pyscript.cli import app + +if TYPE_CHECKING: + from _pytest.monkeypatch import MonkeyPatch + +CLIInvoker = Callable[[VarArg(str)], Result] + + +@pytest.fixture() +def invoke_cli(tmp_path: Path, monkeypatch: "MonkeyPatch") -> CLIInvoker: + """Returns a function, which can be used to call the CLI from within a temporary directory.""" + runner = CliRunner() + + monkeypatch.chdir(tmp_path) + + def f(*args: str) -> Result: + return runner.invoke(app, args) + + return f From abe31dd8024809ac99ef6f272911d8e9014eb368 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 22:41:34 -0500 Subject: [PATCH 15/18] replace show option with silent --- src/pyscript/plugins/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 44cb743..9c14162 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -102,7 +102,7 @@ def run( path: Path = typer.Argument( Path("."), help="The path of the project that will run." ), - show: Optional[bool] = typer.Option(True, help="Open the app in web browser."), + silent: Optional[bool] = typer.Option(False, help="Open the app in web browser."), port: Optional[int] = typer.Option(8000, help="The port that the app will run on."), ): """ @@ -114,7 +114,7 @@ def run( raise cli.Abort(f"Error: Path {str(path)} does not exist.", style="red") try: - start_server(path, show, port) + start_server(path, not silent, port) except OSError as e: if e.errno == 48: console.print( From 02a437c02f96c0615d003b064360344312981440 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Mon, 15 May 2023 22:44:37 -0500 Subject: [PATCH 16/18] add test for multiple valid argument permutations --- tests/test_run_cli_cmd.py | 95 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py index 457b124..1da3b64 100644 --- a/tests/test_run_cli_cmd.py +++ b/tests/test_run_cli_cmd.py @@ -1,15 +1,19 @@ from __future__ import annotations +from pathlib import Path +from unittest import mock + import pytest -from utils import CLIInvoker +from utils import CLIInvoker, invoke_cli # noqa: F401 + +BASEPATH = str(Path(__file__).parent) @pytest.mark.parametrize( "path", ["non_existing_folder", "non_existing_file.html"], - # ids=["passig", "command_and_script", "command_no_output_or_show"], ) -def test_run_bad_paths(invoke_cli: CLIInvoker, path: str): +def test_run_bad_paths(invoke_cli: CLIInvoker, path: str): # noqa: F811 """ Test that when wrap is called passing a bad path as input the command fails """ @@ -19,3 +23,88 @@ def test_run_bad_paths(invoke_cli: CLIInvoker, path: str): assert result.exit_code == 1 # EXPECT the right error message to be printed assert f"Error: Path {path} does not exist." in result.stdout + + +def test_run_server_bad_port(invoke_cli: CLIInvoker): # noqa: F811 + """ + Test that when run is called passing a bad port as input the command fails + """ + # GIVEN a call to run with a bad port as argument + result = invoke_cli("run", "--port", "bad_port") + # EXPECT the command to fail + assert result.exit_code == 2 + # EXPECT the right error message to be printed + assert "Error" in result.stdout + assert ( + "Invalid value for '--port': 'bad_port' is not a valid integer" in result.stdout + ) + + +@mock.patch("pyscript.plugins.run.start_server") +def test_run_server_with_default_values( + start_server_mock, invoke_cli: CLIInvoker # noqa: F811 +): + """ + Test that when run is called without arguments the command runs with the + default values + """ + # GIVEN a call to run without arguments + result = invoke_cli("run") + # EXPECT the command to succeed + assert result.exit_code == 0 + # EXPECT start_server_mock function to be called with the default values: + # Path("."): path to local folder + # show=True: the opposite of the --silent option (which default to False) + # port=8000: that is the default port + start_server_mock.assert_called_once_with(Path("."), True, 8000) + + +@mock.patch("pyscript.plugins.run.start_server") +def test_run_server_with_silent_flag( + start_server_mock, invoke_cli: CLIInvoker # noqa: F811 +): + """ + Test that when run is called without arguments the command runs with the + default values + """ + # GIVEN a call to run without arguments + result = invoke_cli("run", "--silent") + # EXPECT the command to succeed + assert result.exit_code == 0 + # EXPECT start_server_mock function to be called with the default values: + # Path("."): path to local folder + # show=False: the opposite of the --silent option + # port=8000: that is the default port + start_server_mock.assert_called_once_with(Path("."), False, 8000) + + +@pytest.mark.parametrize( + "run_args, expected_values", + [ + (("--silent",), (Path("."), False, 8000)), + ((BASEPATH,), (Path(BASEPATH), True, 8000)), + (("--port=8001",), (Path("."), True, 8001)), + (("--silent", "--port=8001"), (Path("."), False, 8001)), + ((BASEPATH, "--silent"), (Path(BASEPATH), False, 8000)), + ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001)), + ((BASEPATH, "--silent", "--port=8001"), (Path(BASEPATH), False, 8001)), + ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001)), + ], +) +@mock.patch("pyscript.plugins.run.start_server") +def test_run_server_with_valid_combinations( + start_server_mock, invoke_cli: CLIInvoker, run_args, expected_values # noqa: F811 +): + """ + Test that when run is called without arguments the command runs with the + default values + """ + # GIVEN a call to run without arguments + result = invoke_cli("run", *run_args) + # EXPECT the command to succeed + assert result.exit_code == 0 + # EXPECT start_server_mock function to be called with the default values: + # Path("."): path to local folder + # show=False: the opposite of the --silent option + # port=8000: that is the default port + start_server_mock.assert_called_once_with(*expected_values) From ac0082abb014a065d078a56a6e8ea112301aea9b Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Tue, 16 May 2023 10:18:55 -0500 Subject: [PATCH 17/18] make mypy happy --- src/pyscript/plugins/run.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 9c14162..182e71e 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import socketserver import threading import webbrowser from functools import partial from http.server import SimpleHTTPRequestHandler from pathlib import Path -from typing import Optional from pyscript import app, cli, console, plugins @@ -14,7 +15,9 @@ import typer # type: ignore -def get_folder_based_http_request_handler(folder: Path) -> SimpleHTTPRequestHandler: +def get_folder_based_http_request_handler( + folder: Path, +) -> type[SimpleHTTPRequestHandler]: """ Returns a FolderBasedHTTPRequestHandler with the specified directory. @@ -33,7 +36,7 @@ def __init__(self, *args, **kwargs): return FolderBasedHTTPRequestHandler -def split_path_and_filename(path: Path) -> str: +def split_path_and_filename(path: Path) -> tuple[Path, str]: """Receives a path to a pyscript project or file and returns the base path of the project and the filename that should be opened (filename defaults to "" (empty string) if the path points to a folder). @@ -47,12 +50,12 @@ def split_path_and_filename(path: Path) -> str: """ abs_path = path.absolute() if path.is_file(): - return "/".join(abs_path.parts[:-1]), abs_path.parts[-1] + return Path("/".join(abs_path.parts[:-1])), abs_path.parts[-1] else: return abs_path, "" -def start_server(path: str, show: bool, port: int): +def start_server(path: Path, show: bool, port: int): """ Creates a local server to run the app on the path and port specified. @@ -102,8 +105,8 @@ def run( path: Path = typer.Argument( Path("."), help="The path of the project that will run." ), - silent: Optional[bool] = typer.Option(False, help="Open the app in web browser."), - port: Optional[int] = typer.Option(8000, help="The port that the app will run on."), + silent: bool = typer.Option(False, help="Open the app in web browser."), + port: int = typer.Option(8000, help="The port that the app will run on."), ): """ Creates a local server to run the app on the path and port specified. From 16c9e7631af785be3631846ac7e8e958701ad1ad Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Tue, 16 May 2023 12:59:04 -0500 Subject: [PATCH 18/18] fix wrong comment on test --- tests/test_run_cli_cmd.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py index 1da3b64..88cbc46 100644 --- a/tests/test_run_cli_cmd.py +++ b/tests/test_run_cli_cmd.py @@ -103,8 +103,5 @@ def test_run_server_with_valid_combinations( result = invoke_cli("run", *run_args) # EXPECT the command to succeed assert result.exit_code == 0 - # EXPECT start_server_mock function to be called with the default values: - # Path("."): path to local folder - # show=False: the opposite of the --silent option - # port=8000: that is the default port + # EXPECT start_server_mock function to be called with the expected values start_server_mock.assert_called_once_with(*expected_values)