Repository: pyscript/pyscript-cli

Issue:
create template still uses https://pyscript.net/alpha/pyscript.js
As the title mentions, HTML template references alpha https://pyscript.net/alpha/pyscript.js.

Ideally, the `create` cmd should be able to hit an API or something so that it can dynamically determine what's the latest release and pin/use that. 

Since we don't have an API right now, the scope of this issue is to pin the latest version statically as `default` value and add a new `create` cmd argument called `pyscript-version` that allows users to specify a different version instead of the `default`

Comments:

Patch:
diff --git a/src/pyscript/__init__.py b/src/pyscript/__init__.py
index eb2c702..6b07387 100644
--- a/src/pyscript/__init__.py
+++ b/src/pyscript/__init__.py
@@ -5,11 +5,11 @@ from pathlib import Path
 import platformdirs
 from rich.console import Console
 
+LATEST_PYSCRIPT_VERSION = "2022.12.1"
 APPNAME = "pyscript"
 APPAUTHOR = "python"
 DEFAULT_CONFIG_FILENAME = ".pyscriptconfig"
 
-
 # Default initial data for the command line.
 DEFAULT_CONFIG = {
     # Name of config file for PyScript projects.
diff --git a/src/pyscript/_generator.py b/src/pyscript/_generator.py
index aedac05..e46d373 100644
--- a/src/pyscript/_generator.py
+++ b/src/pyscript/_generator.py
@@ -6,7 +6,7 @@ from typing import Optional
 import jinja2
 import toml
 
-from pyscript import config
+from pyscript import LATEST_PYSCRIPT_VERSION, config
 
 _env = jinja2.Environment(loader=jinja2.PackageLoader("pyscript"))
 TEMPLATE_PYTHON_CODE = """# Replace the code below with your own
@@ -15,7 +15,11 @@ print("Hello, world!")
 
 
 def create_project_html(
-    title: str, python_file_path: str, config_file_path: str, output_file_path: Path
+    title: str,
+    python_file_path: str,
+    config_file_path: str,
+    output_file_path: Path,
+    pyscript_version: str = LATEST_PYSCRIPT_VERSION,
 ) -> None:
     """Write a Python script string to an HTML file template."""
     template = _env.get_template("basic.html")
@@ -25,6 +29,7 @@ def create_project_html(
                 python_file_path=python_file_path,
                 config_file_path=config_file_path,
                 title=title,
+                pyscript_version=pyscript_version,
             )
         )
 
@@ -49,7 +54,11 @@ def save_config_file(config_file: Path, configuration: dict):
 
 
 def string_to_html(
-    code: str, title: str, output_path: Path, template_name: str = "basic.html"
+    code: str,
+    title: str,
+    output_path: Path,
+    template_name: str = "basic.html",
+    pyscript_version: str = LATEST_PYSCRIPT_VERSION,
 ) -> None:
     """Write a Python script string to an HTML file template.
 
@@ -59,14 +68,18 @@ def string_to_html(
                      PyScript app template
         - title(str): application title, that will be placed as title of the html
                       app template
+        - output_path(Path): path where to write the new html file
         - template_name(str): name of the template to be used
+        - pyscript_version(str): version of pyscript to be used
 
     Output:
         (None)
     """
     template = _env.get_template(template_name)
     with output_path.open("w") as fp:
-        fp.write(template.render(code=code, title=title))
+        fp.write(
+            template.render(code=code, title=title, pyscript_version=pyscript_version)
+        )
 
 
 def file_to_html(
@@ -74,11 +87,12 @@ def file_to_html(
     title: str,
     output_path: Optional[Path],
     template_name: str = "basic.html",
+    pyscript_version: str = LATEST_PYSCRIPT_VERSION,
 ) -> None:
     """Write a Python script string to an HTML file template."""
     output_path = output_path or input_path.with_suffix(".html")
     with input_path.open("r") as fp:
-        string_to_html(fp.read(), title, output_path, template_name)
+        string_to_html(fp.read(), title, output_path, template_name, pyscript_version)
 
 
 def create_project(
@@ -86,6 +100,7 @@ def create_project(
     app_description: str,
     author_name: str,
     author_email: str,
+    pyscript_version: str = LATEST_PYSCRIPT_VERSION,
 ) -> None:
     """
     New files created:
@@ -124,4 +139,5 @@ def create_project(
         config["project_main_filename"],
         config["project_config_filename"],
         index_file,
+        pyscript_version=pyscript_version,
     )
diff --git a/src/pyscript/plugins/create.py b/src/pyscript/plugins/create.py
index aeed292..e51ecd0 100644
--- a/src/pyscript/plugins/create.py
+++ b/src/pyscript/plugins/create.py
@@ -1,4 +1,4 @@
-from pyscript import app, cli, plugins
+from pyscript import LATEST_PYSCRIPT_VERSION, app, cli, plugins
 from pyscript._generator import create_project
 
 try:
@@ -13,6 +13,11 @@ def create(
     app_description: str = typer.Option(..., prompt=True),
     author_name: str = typer.Option(..., prompt=True),
     author_email: str = typer.Option(..., prompt=True),
+    pyscript_version: str = typer.Option(
+        LATEST_PYSCRIPT_VERSION,
+        "--pyscript-version",
+        help="If provided, defines what version of pyscript will be used to create the app",
+    ),
 ):
     """
     Create a new pyscript project with the passed in name, creating a new
@@ -21,7 +26,9 @@ def create(
     TODO: Agree on the metadata to be collected from the user.
     """
     try:
-        create_project(app_name, app_description, author_name, author_email)
+        create_project(
+            app_name, app_description, author_name, author_email, pyscript_version
+        )
     except FileExistsError:
         raise cli.Abort(
             f"A directory called {app_name} already exists in this location."
diff --git a/src/pyscript/plugins/wrap.py b/src/pyscript/plugins/wrap.py
index 931f406..d4f68a7 100644
--- a/src/pyscript/plugins/wrap.py
+++ b/src/pyscript/plugins/wrap.py
@@ -3,7 +3,7 @@ import webbrowser
 from pathlib import Path
 from typing import Optional
 
-from pyscript import app, cli, console, plugins
+from pyscript import LATEST_PYSCRIPT_VERSION, app, cli, console, plugins
 from pyscript._generator import file_to_html, string_to_html
 
 try:
@@ -29,6 +29,11 @@ def wrap(
     ),
     show: Optional[bool] = typer.Option(None, help="Open output file in web browser."),
     title: Optional[str] = typer.Option(None, help="Add title to HTML file."),
+    pyscript_version: str = typer.Option(
+        LATEST_PYSCRIPT_VERSION,
+        "--pyscript-version",
+        help="If provided, defines what version of pyscript will be used to create the app",
+    ),
 ) -> None:
     """Wrap a Python script inside an HTML file."""
     title = title or "PyScript App"
@@ -52,9 +57,21 @@ def wrap(
         else:
             raise cli.Abort("Must provide an output file or use `--show` option")
     if input_file is not None:
-        file_to_html(input_file, title, output, template_name="wrap.html")
+        file_to_html(
+            input_file,
+            title,
+            output,
+            template_name="wrap.html",
+            pyscript_version=pyscript_version,
+        )
     if command:
-        string_to_html(command, title, output, template_name="wrap.html")
+        string_to_html(
+            command,
+            title,
+            output,
+            template_name="wrap.html",
+            pyscript_version=pyscript_version,
+        )
     if output:
         if show:
             console.print("Opening in web browser!")
diff --git a/src/pyscript/templates/basic.html b/src/pyscript/templates/basic.html
index 82817fa..256a406 100644
--- a/src/pyscript/templates/basic.html
+++ b/src/pyscript/templates/basic.html
@@ -2,8 +2,8 @@
 <html lang="en">
 <head>
     <title>{{ title }}</title>
-    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css"/>
-    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
+    <link rel="stylesheet" href="https://pyscript.net/releases/{{ pyscript_version }}/pyscript.css"/>
+    <script defer src="https://pyscript.net/releases/{{ pyscript_version }}/pyscript.js"></script>
 </head>
 <body>
     <py-config src="./{{ config_file_path }}"></py-config>
diff --git a/src/pyscript/templates/wrap.html b/src/pyscript/templates/wrap.html
index 16c1a3a..2b84760 100644
--- a/src/pyscript/templates/wrap.html
+++ b/src/pyscript/templates/wrap.html
@@ -2,8 +2,8 @@
 <html lang="en">
 <head>
     <title>{{ title }}</title>
-    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css"/>
-    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
+    <link rel="stylesheet" href="https://pyscript.net/releases/{{ pyscript_version }}/pyscript.css"/>
+    <script defer src="https://pyscript.net/releases/{{ pyscript_version }}/pyscript.js"></script>
 </head>
 <body>
 <py-script>


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 115bf94..2919986 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -8,7 +8,7 @@ import pytest
 from mypy_extensions import VarArg
 from typer.testing import CliRunner, Result
 
-from pyscript import __version__
+from pyscript import LATEST_PYSCRIPT_VERSION, __version__, config
 from pyscript.cli import app
 
 if TYPE_CHECKING:
@@ -167,3 +167,166 @@ def test_wrap_title(
     assert f"<py-script>\n{command}\n</py-script>" in html_text
 
     assert f"<title>{expected_title}</title>" in html_text
+
+
+@pytest.mark.parametrize(
+    "version, expected_version",
+    [(None, LATEST_PYSCRIPT_VERSION), ("2022.9.1", "2022.9.1")],
+)
+def test_wrap_pyscript_version(
+    invoke_cli: CLIInvoker,
+    version: Optional[str],
+    expected_version: str,
+    tmp_path: Path,
+) -> None:
+    """
+    Test that when wrap is called passing a string code input and an explicit pyscript version
+    the project is created correctly
+    """
+    command = 'print("Hello World!")'
+    args = ["wrap", "-c", command, "-o", "output.html"]
+    if version is not None:
+        args.extend(["--pyscript-version", version])
+
+    # GIVEN a call to wrap with a cmd input and specific pyscript version as arguments
+    result = invoke_cli(*args)
+    assert result.exit_code == 0
+
+    # EXPECT the output file to exist
+    expected_html_path = tmp_path / "output.html"
+    assert expected_html_path.exists()
+
+    with expected_html_path.open() as fp:
+        html_text = fp.read()
+
+    # EXPECT the right cmd to be present in the output file
+    assert f"<py-script>\n{command}\n</py-script>" in html_text
+
+    # EXPECT the right JS and CSS version to be present in the output file
+    version_str = (
+        f'<script defer src="https://pyscript.net/releases/{expected_version}'
+        '/pyscript.js"></script>'
+    )
+    css_version_str = (
+        '<link rel="stylesheet" href="https://pyscript.net/releases/'
+        f'{expected_version}/pyscript.css"/>'
+    )
+    assert version_str in html_text
+    assert css_version_str in html_text
+
+
+@pytest.mark.parametrize(
+    "version, expected_version",
+    [(None, LATEST_PYSCRIPT_VERSION), ("2022.9.1", "2022.9.1")],
+)
+def test_wrap_pyscript_version_file(
+    invoke_cli: CLIInvoker,
+    version: Optional[str],
+    expected_version: str,
+    tmp_path: Path,
+) -> None:
+    """
+    Test that when wrap is called passing a file input and an explicit pyscript version
+    the project is created correctly
+    """
+    command = 'print("Hello World!")'
+    input_file = tmp_path / "hello.py"
+    with input_file.open("w") as fp:
+        fp.write(command)
+
+    args = ["wrap", str(input_file), "-o", "output.html"]
+
+    if version is not None:
+        args.extend(["--pyscript-version", version])
+
+    # GIVEN a call to wrap with a file and specific pyscript version as arguments
+    result = invoke_cli(*args)
+    assert result.exit_code == 0
+
+    # EXPECT the output file to exist
+    expected_html_path = tmp_path / "output.html"
+    assert expected_html_path.exists()
+
+    with expected_html_path.open() as fp:
+        html_text = fp.read()
+
+    # EXPECT the right cmd to be present in the output file
+    assert f"<py-script>\n{command}\n</py-script>" in html_text
+
+    # EXPECT the right JS and CSS version to be present in the output file
+    version_str = (
+        f'<script defer src="https://pyscript.net/releases/{expected_version}'
+        '/pyscript.js"></script>'
+    )
+    css_version_str = (
+        '<link rel="stylesheet" href="https://pyscript.net/releases/'
+        f'{expected_version}/pyscript.css"/>'
+    )
+    assert version_str in html_text
+    assert css_version_str in html_text
+
+
+@pytest.mark.parametrize(
+    "create_args, expected_version",
+    [
+        (("myapp1",), LATEST_PYSCRIPT_VERSION),
+        (("myapp-w-version", "--pyscript-version", "2022.9.1"), "2022.9.1"),
+    ],
+)
+def test_create_project_version(
+    invoke_cli: CLIInvoker,
+    tmp_path: Path,
+    create_args: tuple[str],
+    expected_version: str,
+) -> None:
+    """
+    Test that project created with an explicit pyscript version are created correctly
+    """
+    command = 'print("Hello World!")'
+
+    input_file = tmp_path / "hello.py"
+    with input_file.open("w") as fp:
+        fp.write(command)
+
+    cmd_args = list(create_args) + [
+        "--app-description",
+        "",
+        "--author-name",
+        "tester",
+        "--author-email",
+        "tester@me.com",
+    ]
+
+    # GIVEN a call to wrap with a file and specific pyscript version as arguments
+    result = invoke_cli("create", *cmd_args)
+    assert result.exit_code == 0
+
+    # EXPECT the app folder to exist
+    expected_app_path = tmp_path / create_args[0]
+    assert expected_app_path.exists()
+
+    # EXPECT the app folder to contain the right index.html file
+    app_file = expected_app_path / "index.html"
+    assert app_file.exists()
+    with app_file.open() as fp:
+        html_text = fp.read()
+
+    # EXPECT the right JS and CSS version to be present in the html file
+    version_str = (
+        f'<script defer src="https://pyscript.net/releases/{expected_version}'
+        '/pyscript.js"></script>'
+    )
+    css_version_str = (
+        '<link rel="stylesheet" href="https://pyscript.net/releases/'
+        f'{expected_version}/pyscript.css"/>'
+    )
+    assert version_str in html_text
+    assert css_version_str in html_text
+
+    # EXPECT the folder to also contain the python main file
+    py_file = expected_app_path / config["project_main_filename"]
+    assert py_file.exists()
+
+    # EXPECT the folder to also contain the config file
+    config_file = expected_app_path / config["project_config_filename"]
+    assert config_file.exists()