From 4ca4659023ed7a20679d179a21882ee14a2870ce Mon Sep 17 00:00:00 2001 From: Josh Wiedemeier Date: Wed, 22 May 2024 14:11:53 -0400 Subject: [PATCH 01/10] add --default-file option to pyscript run --- src/pyscript/plugins/run.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 196135e..46c3836 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -14,6 +14,7 @@ def get_folder_based_http_request_handler( folder: Path, + default_file: Path = None, ) -> type[SimpleHTTPRequestHandler]: """ Returns a FolderBasedHTTPRequestHandler with the specified directory. @@ -36,6 +37,16 @@ def end_headers(self): self.send_header("Cross-Origin-Resource-Policy", "cross-origin") self.send_header("Cache-Control", "no-cache, must-revalidate") SimpleHTTPRequestHandler.end_headers(self) + + def do_GET(self): + # intercept accesses to nonexistent files; replace them with the default file + # this is to service SPA use cases (see Github Issue #132) + if default_file: + path = Path(self.translate_path(self.path)) + if not path.exists(): + self.path = f'/{default_file}' + + return super().do_GET() return FolderBasedHTTPRequestHandler @@ -58,7 +69,7 @@ def split_path_and_filename(path: Path) -> tuple[Path, str]: return abs_path, "" -def start_server(path: Path, show: bool, port: int): +def start_server(path: Path, show: bool, port: int, default_file: Path = None): """ Creates a local server to run the app on the path and port specified. @@ -76,7 +87,7 @@ def start_server(path: Path, show: bool, port: int): socketserver.TCPServer.allow_reuse_address = True app_folder, filename = split_path_and_filename(path) - CustomHTTPRequestHandler = get_folder_based_http_request_handler(app_folder) + CustomHTTPRequestHandler = get_folder_based_http_request_handler(app_folder, default_file=default_file) # Start the server within a context manager to make sure we clean up after with socketserver.TCPServer(("", port), CustomHTTPRequestHandler) as httpd: @@ -110,6 +121,7 @@ def run( ), view: bool = typer.Option(True, help="Open the app in web browser."), port: int = typer.Option(8000, help="The port that the app will run on."), + default_file: Path = typer.Option(None, help="A default file to serve when a nonexistent file is accessed."), ): """ Creates a local server to run the app on the path and port specified. @@ -120,7 +132,7 @@ def run( raise cli.Abort(f"Error: Path {str(path)} does not exist.", style="red") try: - start_server(path, view, port) + start_server(path, view, port, default_file=default_file) except OSError as e: if e.errno == 48: console.print( From a47a2448029e74752cd0b7fc01f87e58f4ad945c Mon Sep 17 00:00:00 2001 From: Josh Wiedemeier Date: Wed, 22 May 2024 14:12:11 -0400 Subject: [PATCH 02/10] add documentation for --default-file option in pyscript run --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 2cccfe7..bb42b28 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,12 @@ To avoid opening a browser window, use `--no-view` option. $ pyscript run --no-view ``` +To serve a default file (e.g., `index.html`) instead of a 404 HTTP status when a nonexistent file is accessed, use `--default-file` option. + +```shell +pyscript run --default-file +``` + ### create #### Create a new pyscript project with the passed in name, creating a new directory From 76c460f0a27064cc03bcc5e0088a1aa2908d2d8e Mon Sep 17 00:00:00 2001 From: Josh Wiedemeier Date: Wed, 22 May 2024 14:26:19 -0400 Subject: [PATCH 03/10] update unit tests to include default_file kwarg --- tests/test_run_cli_cmd.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py index 3a71cd3..7d922f5 100644 --- a/tests/test_run_cli_cmd.py +++ b/tests/test_run_cli_cmd.py @@ -59,7 +59,7 @@ def test_run_server_with_default_values( # Path("."): path to local folder # show=True: same as passing the --view option (which defaults to True) # port=8000: that is the default port - start_server_mock.assert_called_once_with(Path("."), True, 8000) + start_server_mock.assert_called_once_with(Path("."), True, 8000, default_file=None) @mock.patch("pyscript.plugins.run.start_server") @@ -78,25 +78,33 @@ def test_run_server_with_no_view_flag( # Path("."): path to local folder # show=False: same as passing the --no-view option # port=8000: that is the default port - start_server_mock.assert_called_once_with(Path("."), False, 8000) + start_server_mock.assert_called_once_with(Path("."), False, 8000, default_file=None) @pytest.mark.parametrize( - "run_args, expected_values", + "run_args, expected_posargs, expected_kwargs", [ - (("--no-view",), (Path("."), False, 8000)), - ((BASEPATH,), (Path(BASEPATH), True, 8000)), - (("--port=8001",), (Path("."), True, 8001)), - (("--no-view", "--port=8001"), (Path("."), False, 8001)), - ((BASEPATH, "--no-view"), (Path(BASEPATH), False, 8000)), - ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001)), - ((BASEPATH, "--no-view", "--port=8001"), (Path(BASEPATH), False, 8001)), - ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001)), + (("--no-view",), (Path("."), False, 8000), {"default_file": None}), + ((BASEPATH,), (Path(BASEPATH), True, 8000), {"default_file": None}), + (("--port=8001",), (Path("."), True, 8001), {"default_file": None}), + (("--no-view", "--port=8001"), (Path("."), False, 8001), {"default_file": None}), + ((BASEPATH, "--no-view"), (Path(BASEPATH), False, 8000), {"default_file": None}), + ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001), {"default_file": None}), + ((BASEPATH, "--no-view", "--port=8001"), (Path(BASEPATH), False, 8001), {"default_file": None}), + ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001), {"default_file": None}), + (("--no-view", "--default-file=index.html"), (Path("."), False, 8000), {"default_file": Path("index.html")}), + ((BASEPATH, "--default-file=index.html"), (Path(BASEPATH), True, 8000), {"default_file": Path("index.html")}), + (("--port=8001", "--default-file=index.html"), (Path("."), True, 8001), {"default_file": Path("index.html")}), + (("--no-view", "--port=8001", "--default-file=index.html"), (Path("."), False, 8001), {"default_file": Path("index.html")}), + ((BASEPATH, "--no-view", "--default-file=index.html"), (Path(BASEPATH), False, 8000), {"default_file": Path("index.html")}), + ((BASEPATH, "--port=8001", "--default-file=index.html"), (Path(BASEPATH), True, 8001), {"default_file": Path("index.html")}), + ((BASEPATH, "--no-view", "--port=8001", "--default-file=index.html"), (Path(BASEPATH), False, 8001), {"default_file": Path("index.html")}), + ((BASEPATH, "--port=8001", "--default-file=index.html"), (Path(BASEPATH), True, 8001), {"default_file": Path("index.html")}), ], ) @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 + start_server_mock, invoke_cli: CLIInvoker, run_args, expected_posargs, expected_kwargs # noqa: F811 ): """ Test that when run is called without arguments the command runs with the @@ -107,7 +115,7 @@ def test_run_server_with_valid_combinations( # EXPECT the command to succeed assert result.exit_code == 0 # EXPECT start_server_mock function to be called with the expected values - start_server_mock.assert_called_once_with(*expected_values) + start_server_mock.assert_called_once_with(*expected_posargs, **expected_kwargs) class TestFolderBasedHTTPRequestHandler: From 4c8a6822090096325182e42f5b1423e58791547f Mon Sep 17 00:00:00 2001 From: Josh Wiedemeier Date: Wed, 22 May 2024 14:27:16 -0400 Subject: [PATCH 04/10] update comments on unit tests --- tests/test_run_cli_cmd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py index 7d922f5..190f85a 100644 --- a/tests/test_run_cli_cmd.py +++ b/tests/test_run_cli_cmd.py @@ -59,6 +59,7 @@ def test_run_server_with_default_values( # Path("."): path to local folder # show=True: same as passing the --view option (which defaults to True) # port=8000: that is the default port + # default_file=None: default behavior is to have no default file start_server_mock.assert_called_once_with(Path("."), True, 8000, default_file=None) @@ -78,6 +79,7 @@ def test_run_server_with_no_view_flag( # Path("."): path to local folder # show=False: same as passing the --no-view option # port=8000: that is the default port + # default_file=None: default behavior is to have no default file start_server_mock.assert_called_once_with(Path("."), False, 8000, default_file=None) From c93aca70a411115a294b07ed06a7a1a10deb560b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 18:33:42 +0000 Subject: [PATCH 05/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pyscript/plugins/run.py | 14 ++++--- tests/test_run_cli_cmd.py | 84 ++++++++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 46c3836..6929528 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -37,15 +37,15 @@ def end_headers(self): self.send_header("Cross-Origin-Resource-Policy", "cross-origin") self.send_header("Cache-Control", "no-cache, must-revalidate") SimpleHTTPRequestHandler.end_headers(self) - + def do_GET(self): # intercept accesses to nonexistent files; replace them with the default file # this is to service SPA use cases (see Github Issue #132) if default_file: path = Path(self.translate_path(self.path)) if not path.exists(): - self.path = f'/{default_file}' - + self.path = f"/{default_file}" + return super().do_GET() return FolderBasedHTTPRequestHandler @@ -87,7 +87,9 @@ def start_server(path: Path, show: bool, port: int, default_file: Path = None): socketserver.TCPServer.allow_reuse_address = True app_folder, filename = split_path_and_filename(path) - CustomHTTPRequestHandler = get_folder_based_http_request_handler(app_folder, default_file=default_file) + CustomHTTPRequestHandler = get_folder_based_http_request_handler( + app_folder, default_file=default_file + ) # Start the server within a context manager to make sure we clean up after with socketserver.TCPServer(("", port), CustomHTTPRequestHandler) as httpd: @@ -121,7 +123,9 @@ def run( ), view: bool = typer.Option(True, help="Open the app in web browser."), port: int = typer.Option(8000, help="The port that the app will run on."), - default_file: Path = typer.Option(None, help="A default file to serve when a nonexistent file is accessed."), + default_file: Path = typer.Option( + None, help="A default file to serve when a nonexistent file is accessed." + ), ): """ Creates a local server to run the app on the path and port specified. diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py index 190f85a..0efa42c 100644 --- a/tests/test_run_cli_cmd.py +++ b/tests/test_run_cli_cmd.py @@ -89,24 +89,80 @@ def test_run_server_with_no_view_flag( (("--no-view",), (Path("."), False, 8000), {"default_file": None}), ((BASEPATH,), (Path(BASEPATH), True, 8000), {"default_file": None}), (("--port=8001",), (Path("."), True, 8001), {"default_file": None}), - (("--no-view", "--port=8001"), (Path("."), False, 8001), {"default_file": None}), - ((BASEPATH, "--no-view"), (Path(BASEPATH), False, 8000), {"default_file": None}), - ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001), {"default_file": None}), - ((BASEPATH, "--no-view", "--port=8001"), (Path(BASEPATH), False, 8001), {"default_file": None}), - ((BASEPATH, "--port=8001"), (Path(BASEPATH), True, 8001), {"default_file": None}), - (("--no-view", "--default-file=index.html"), (Path("."), False, 8000), {"default_file": Path("index.html")}), - ((BASEPATH, "--default-file=index.html"), (Path(BASEPATH), True, 8000), {"default_file": Path("index.html")}), - (("--port=8001", "--default-file=index.html"), (Path("."), True, 8001), {"default_file": Path("index.html")}), - (("--no-view", "--port=8001", "--default-file=index.html"), (Path("."), False, 8001), {"default_file": Path("index.html")}), - ((BASEPATH, "--no-view", "--default-file=index.html"), (Path(BASEPATH), False, 8000), {"default_file": Path("index.html")}), - ((BASEPATH, "--port=8001", "--default-file=index.html"), (Path(BASEPATH), True, 8001), {"default_file": Path("index.html")}), - ((BASEPATH, "--no-view", "--port=8001", "--default-file=index.html"), (Path(BASEPATH), False, 8001), {"default_file": Path("index.html")}), - ((BASEPATH, "--port=8001", "--default-file=index.html"), (Path(BASEPATH), True, 8001), {"default_file": Path("index.html")}), + ( + ("--no-view", "--port=8001"), + (Path("."), False, 8001), + {"default_file": None}, + ), + ( + (BASEPATH, "--no-view"), + (Path(BASEPATH), False, 8000), + {"default_file": None}, + ), + ( + (BASEPATH, "--port=8001"), + (Path(BASEPATH), True, 8001), + {"default_file": None}, + ), + ( + (BASEPATH, "--no-view", "--port=8001"), + (Path(BASEPATH), False, 8001), + {"default_file": None}, + ), + ( + (BASEPATH, "--port=8001"), + (Path(BASEPATH), True, 8001), + {"default_file": None}, + ), + ( + ("--no-view", "--default-file=index.html"), + (Path("."), False, 8000), + {"default_file": Path("index.html")}, + ), + ( + (BASEPATH, "--default-file=index.html"), + (Path(BASEPATH), True, 8000), + {"default_file": Path("index.html")}, + ), + ( + ("--port=8001", "--default-file=index.html"), + (Path("."), True, 8001), + {"default_file": Path("index.html")}, + ), + ( + ("--no-view", "--port=8001", "--default-file=index.html"), + (Path("."), False, 8001), + {"default_file": Path("index.html")}, + ), + ( + (BASEPATH, "--no-view", "--default-file=index.html"), + (Path(BASEPATH), False, 8000), + {"default_file": Path("index.html")}, + ), + ( + (BASEPATH, "--port=8001", "--default-file=index.html"), + (Path(BASEPATH), True, 8001), + {"default_file": Path("index.html")}, + ), + ( + (BASEPATH, "--no-view", "--port=8001", "--default-file=index.html"), + (Path(BASEPATH), False, 8001), + {"default_file": Path("index.html")}, + ), + ( + (BASEPATH, "--port=8001", "--default-file=index.html"), + (Path(BASEPATH), True, 8001), + {"default_file": Path("index.html")}, + ), ], ) @mock.patch("pyscript.plugins.run.start_server") def test_run_server_with_valid_combinations( - start_server_mock, invoke_cli: CLIInvoker, run_args, expected_posargs, expected_kwargs # noqa: F811 + start_server_mock, + invoke_cli: CLIInvoker, + run_args, + expected_posargs, + expected_kwargs, # noqa: F811 ): """ Test that when run is called without arguments the command runs with the From d35ed424719a62b22e6b8a561230056a26723bea Mon Sep 17 00:00:00 2001 From: Josh Wiedemeier Date: Wed, 22 May 2024 14:37:28 -0400 Subject: [PATCH 06/10] nits to appease precommit ci --- tests/test_generator.py | 2 +- tests/test_run_cli_cmd.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_generator.py b/tests/test_generator.py index c9c8c4a..fa9bbd4 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -237,7 +237,7 @@ def check_plugin_project_files( assert dedent( f"""

Description

-

{ plugin_description }

+

{plugin_description}

""" ) assert f'' in contents diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py index 0efa42c..47cee62 100644 --- a/tests/test_run_cli_cmd.py +++ b/tests/test_run_cli_cmd.py @@ -159,10 +159,10 @@ def test_run_server_with_no_view_flag( @mock.patch("pyscript.plugins.run.start_server") def test_run_server_with_valid_combinations( start_server_mock, - invoke_cli: CLIInvoker, + invoke_cli: CLIInvoker, # noqa: F811 run_args, expected_posargs, - expected_kwargs, # noqa: F811 + expected_kwargs, ): """ Test that when run is called without arguments the command runs with the From cd7822b2c24e927470a065f7ae5101c1d6c3f640 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 18:37:56 +0000 Subject: [PATCH 07/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_run_cli_cmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_run_cli_cmd.py b/tests/test_run_cli_cmd.py index 47cee62..7efd284 100644 --- a/tests/test_run_cli_cmd.py +++ b/tests/test_run_cli_cmd.py @@ -159,7 +159,7 @@ def test_run_server_with_no_view_flag( @mock.patch("pyscript.plugins.run.start_server") def test_run_server_with_valid_combinations( start_server_mock, - invoke_cli: CLIInvoker, # noqa: F811 + invoke_cli: CLIInvoker, # noqa: F811 run_args, expected_posargs, expected_kwargs, From a4675375cd93c575a86380d50e3f7228fbbedb67 Mon Sep 17 00:00:00 2001 From: Josh Wiedemeier Date: Wed, 22 May 2024 14:40:25 -0400 Subject: [PATCH 08/10] update default_file to use Optional[Path] type --- src/pyscript/plugins/run.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 6929528..1d7531e 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Optional + import socketserver import threading import webbrowser @@ -14,7 +16,7 @@ def get_folder_based_http_request_handler( folder: Path, - default_file: Path = None, + default_file: Optional[Path] = None, ) -> type[SimpleHTTPRequestHandler]: """ Returns a FolderBasedHTTPRequestHandler with the specified directory. @@ -69,7 +71,7 @@ def split_path_and_filename(path: Path) -> tuple[Path, str]: return abs_path, "" -def start_server(path: Path, show: bool, port: int, default_file: Path = None): +def start_server(path: Path, show: bool, port: int, default_file: Optional[Path] = None): """ Creates a local server to run the app on the path and port specified. @@ -123,7 +125,7 @@ def run( ), view: bool = typer.Option(True, help="Open the app in web browser."), port: int = typer.Option(8000, help="The port that the app will run on."), - default_file: Path = typer.Option( + default_file: Optional[Path] = typer.Option( None, help="A default file to serve when a nonexistent file is accessed." ), ): From 6adaa4076a1fc57ff9f278f9578b4120fc3b5083 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 18:40:34 +0000 Subject: [PATCH 09/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pyscript/plugins/run.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 1d7531e..42e8fd3 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -1,13 +1,12 @@ from __future__ import annotations -from typing import Optional - import socketserver import threading import webbrowser from functools import partial from http.server import SimpleHTTPRequestHandler from pathlib import Path +from typing import Optional import typer @@ -16,7 +15,7 @@ def get_folder_based_http_request_handler( folder: Path, - default_file: Optional[Path] = None, + default_file: Path | None = None, ) -> type[SimpleHTTPRequestHandler]: """ Returns a FolderBasedHTTPRequestHandler with the specified directory. @@ -71,7 +70,7 @@ def split_path_and_filename(path: Path) -> tuple[Path, str]: return abs_path, "" -def start_server(path: Path, show: bool, port: int, default_file: Optional[Path] = None): +def start_server(path: Path, show: bool, port: int, default_file: Path | None = None): """ Creates a local server to run the app on the path and port specified. @@ -125,7 +124,7 @@ def run( ), view: bool = typer.Option(True, help="Open the app in web browser."), port: int = typer.Option(8000, help="The port that the app will run on."), - default_file: Optional[Path] = typer.Option( + default_file: Path | None = typer.Option( None, help="A default file to serve when a nonexistent file is accessed." ), ): From 39bcd5587354cb0834fffe8164f520568d116377 Mon Sep 17 00:00:00 2001 From: Josh Wiedemeier Date: Wed, 22 May 2024 14:41:47 -0400 Subject: [PATCH 10/10] more precommit ci nits --- src/pyscript/plugins/run.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyscript/plugins/run.py b/src/pyscript/plugins/run.py index 42e8fd3..090c3ad 100644 --- a/src/pyscript/plugins/run.py +++ b/src/pyscript/plugins/run.py @@ -6,7 +6,6 @@ from functools import partial from http.server import SimpleHTTPRequestHandler from pathlib import Path -from typing import Optional import typer