Repository: beeware/briefcase
Pull request #2080 (2024-12-09 22:04:01)

Update to PyScript 2024.11.1
Removed all obsolete PyScript settings. Since these settings were required by the old template, also added a `platform_target_version`.

The corresponding template PR should be merged first:
* https://github.com/beeware/briefcase-web-static-template/pull/16

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct



Patch:
diff --git a/changes/2080.feature.rst b/changes/2080.feature.rst
new file mode 100644
index 00000000..a0beea04
--- /dev/null
+++ b/changes/2080.feature.rst
@@ -0,0 +1,1 @@
+The web template now targets PyScript version 2024.11.1. In addition, the web template can provide a base ``pyscript.toml`` that Briefcase will update as required during the build process.
diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py
index 5e09a94a..87132e71 100644
--- a/src/briefcase/platforms/web/static.py
+++ b/src/briefcase/platforms/web/static.py
@@ -32,6 +32,7 @@ from briefcase.exceptions import BriefcaseCommandError, BriefcaseConfigError
 class StaticWebMixin:
     output_format = "static"
     platform = "web"
+    platform_target_version = "0.3.21"
 
     def project_path(self, app):
         return self.bundle_path(app) / "www"
@@ -180,33 +181,40 @@ class StaticWebBuildCommand(StaticWebMixin, BuildCommand):
                 ) from e
 
         with self.input.wait_bar("Writing Pyscript configuration file..."):
+            # Load any pre-existing pyscript.toml provided by the template. If the file
+            # doesn't exist, assume an empty pyscript.toml as a starting point.
+            try:
+                with (self.project_path(app) / "pyscript.toml").open("rb") as f:
+                    config = tomllib.load(f)
+            except tomllib.TOMLDecodeError as e:
+                raise BriefcaseConfigError(
+                    f"pyscript.toml content isn't valid TOML: {e}"
+                ) from e
+            except FileNotFoundError:
+                config = {}
+
+            # Add the packages declaration to the existing pyscript.toml.
+            # Ensure that we're using Unix path separators, as the content
+            # will be parsed by pyscript in the browser.
+            config["packages"] = [
+                f'/{"/".join(wheel.relative_to(self.project_path(app)).parts)}'
+                for wheel in sorted(self.wheel_path(app).glob("*.whl"))
+            ]
+
+            # Parse any additional pyscript.toml content, and merge it into
+            # the overall content
+            try:
+                extra = tomllib.loads(app.extra_pyscript_toml_content)
+                config.update(extra)
+            except tomllib.TOMLDecodeError as e:
+                raise BriefcaseConfigError(
+                    f"Extra pyscript.toml content isn't valid TOML: {e}"
+                ) from e
+            except AttributeError:
+                pass
+
+            # Write the final configuration.
             with (self.project_path(app) / "pyscript.toml").open("wb") as f:
-                config = {
-                    "name": app.formal_name,
-                    "description": app.description,
-                    "version": app.version,
-                    "splashscreen": {"autoclose": True},
-                    "terminal": False,
-                    # Ensure that we're using Unix path separators, as the content
-                    # will be parsed by pyscript in the browser.
-                    "packages": [
-                        f'/{"/".join(wheel.relative_to(self.project_path(app)).parts)}'
-                        for wheel in sorted(self.wheel_path(app).glob("*.whl"))
-                    ],
-                }
-                # Parse any additional pyscript.toml content, and merge it into
-                # the overall content
-                try:
-                    extra = tomllib.loads(app.extra_pyscript_toml_content)
-                    config.update(extra)
-                except tomllib.TOMLDecodeError as e:
-                    raise BriefcaseConfigError(
-                        f"Extra pyscript.toml content isn't valid TOML: {e}"
-                    ) from e
-                except AttributeError:
-                    pass
-
-                # Write the final configuration.
                 tomli_w.dump(config, f)
 
         self.logger.info("Compile static web content from wheels")


Test patch:
diff --git a/tests/platforms/web/static/conftest.py b/tests/platforms/web/static/conftest.py
index 3a43f77b..d71fd25d 100644
--- a/tests/platforms/web/static/conftest.py
+++ b/tests/platforms/web/static/conftest.py
@@ -19,6 +19,15 @@ app_requirements_path="requirements.txt"
     # Create index.html
     create_file(bundle_path / "www/index.html", "<html></html>")
 
+    # Create the initial pyscript.toml
+    create_file(
+        bundle_path / "www/pyscript.toml",
+        """
+existing-key-1 = "value-1"
+existing-key-2 = 2
+""",
+    )
+
     # Create the initial briefcase.css
     create_file(
         bundle_path / "www/static/css/briefcase.css",
diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py
index b4927ecc..0aa40e7c 100644
--- a/tests/platforms/web/static/test_build.py
+++ b/tests/platforms/web/static/test_build.py
@@ -15,7 +15,7 @@ from briefcase.exceptions import BriefcaseCommandError, BriefcaseConfigError
 from briefcase.integrations.subprocess import Subprocess
 from briefcase.platforms.web.static import StaticWebBuildCommand
 
-from ....utils import create_wheel
+from ....utils import create_file, create_wheel
 
 
 @pytest.fixture
@@ -124,11 +124,8 @@ def test_build_app(build_command, first_app_generated, logging_level, tmp_path):
     # Pyscript.toml has been written
     with (bundle_path / "www/pyscript.toml").open("rb") as f:
         assert tomllib.load(f) == {
-            "name": "First App",
-            "description": "The first simple app \\ demonstration",
-            "version": "0.0.1",
-            "splashscreen": {"autoclose": True},
-            "terminal": False,
+            "existing-key-1": "value-1",
+            "existing-key-2": 2,
             "packages": [
                 "/static/wheels/dependency-1.2.3-py3-none-any.whl",
                 "/static/wheels/first_app-1.2.3-py3-none-any.whl",
@@ -205,12 +202,10 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_
     # Pyscript.toml has been written
     with (bundle_path / "www/pyscript.toml").open("rb") as f:
         assert tomllib.load(f) == {
-            "name": "First App",
-            "description": "The first simple app \\ demonstration",
-            "version": "0.0.1",
+            "existing-key-1": "value-1",
+            "existing-key-2": 2,
             "something": "custom",
             "splashscreen": {"wiggle": False},
-            "terminal": False,
             "packages": ["something-custom"],
             "runtimes": [
                 {"src": "https://example.com/pyodide.js"},
@@ -218,7 +213,57 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_
         }
 
 
-def test_build_app_invalid_custom_pyscript_toml(
+def test_build_app_no_template_pyscript_toml(
+    build_command, first_app_generated, tmp_path
+):
+    """An app whose template doesn't provide pyscript.toml gets a basic config."""
+    # Remove the templated pyscript.toml
+    bundle_path = tmp_path / "base_path/build/first-app/web/static"
+    (bundle_path / "www/pyscript.toml").unlink()
+
+    # Mock the side effect of invoking shutil
+    build_command.tools.shutil.rmtree.side_effect = lambda *args: shutil.rmtree(
+        bundle_path / "www/static/wheels"
+    )
+
+    # Build the web app.
+    build_command.build_app(first_app_generated)
+
+    # Pyscript.toml has been written with only the packages content
+    with (bundle_path / "www/pyscript.toml").open("rb") as f:
+        assert tomllib.load(f) == {
+            "packages": [],
+        }
+
+
+def test_build_app_invalid_template_pyscript_toml(
+    build_command, first_app_generated, tmp_path
+):
+    """An app with an invalid pyscript.toml raises an error."""
+    # Re-write an invalid templated pyscript.toml
+    bundle_path = tmp_path / "base_path/build/first-app/web/static"
+    (bundle_path / "www/pyscript.toml").unlink()
+    create_file(
+        bundle_path / "www/pyscript.toml",
+        """
+This is not valid toml.
+""",
+    )
+
+    # Mock the side effect of invoking shutil
+    build_command.tools.shutil.rmtree.side_effect = lambda *args: shutil.rmtree(
+        bundle_path / "www/static/wheels"
+    )
+
+    # Building the web app raises an error
+    with pytest.raises(
+        BriefcaseConfigError,
+        match=r"Briefcase configuration error: pyscript.toml content isn't valid TOML: Expected",
+    ):
+        build_command.build_app(first_app_generated)
+
+
+def test_build_app_invalid_extra_pyscript_toml_content(
     build_command, first_app_generated, tmp_path
 ):
     """An app with invalid extra pyscript.toml content raises an error."""
@@ -374,11 +419,8 @@ def test_build_app_no_requirements(build_command, first_app_generated, tmp_path)
     # Pyscript.toml has been written
     with (bundle_path / "www/pyscript.toml").open("rb") as f:
         assert tomllib.load(f) == {
-            "name": "First App",
-            "description": "The first simple app \\ demonstration",
-            "version": "0.0.1",
-            "splashscreen": {"autoclose": True},
-            "terminal": False,
+            "existing-key-1": "value-1",
+            "existing-key-2": 2,
             "packages": [
                 "/static/wheels/first_app-1.2.3-py3-none-any.whl",
             ],
@@ -458,8 +500,12 @@ def test_app_package_fail(build_command, first_app_generated, tmp_path):
     # Wheels folder still exists
     assert (bundle_path / "www/static/wheels").is_dir()
 
-    # Pyscript.toml was not written
-    assert not (bundle_path / "www/pyscript.toml").exists()
+    # Pyscript.toml content has not changed
+    with (bundle_path / "www/pyscript.toml").open("rb") as f:
+        assert tomllib.load(f) == {
+            "existing-key-1": "value-1",
+            "existing-key-2": 2,
+        }
 
 
 def test_dependency_fail(build_command, first_app_generated, tmp_path):
@@ -530,5 +576,9 @@ def test_dependency_fail(build_command, first_app_generated, tmp_path):
     # Wheels folder still exists
     assert (bundle_path / "www/static/wheels").is_dir()
 
-    # Pyscript.toml was not written
-    assert not (bundle_path / "www/pyscript.toml").exists()
+    # Pyscript.toml content has not changed
+    with (bundle_path / "www/pyscript.toml").open("rb") as f:
+        assert tomllib.load(f) == {
+            "existing-key-1": "value-1",
+            "existing-key-2": 2,
+        }