Repository: beeware/briefcase (python)

Issue:
Properly Escape User Generated Content for `pyproject.toml`
**Describe the bug**
When creating a Briefcase project, the user specifies content such as formal name and description for the `pyproject.toml` file. This content may contain characters that are [reserved](https://toml.io/en/v0.3.0#string) for TOML.

If `pyproject.toml` is not valid, Briefcase will always immediately error.

```
❯ briefcase dev

Briefcase configuration error: Invalid pyproject.toml: Unescaped '\' in a string (at line 2, column 26)

Log saved to /home/user/tmp/beeware/formalname/briefcase.2022_10_07-11_14_05.dev.log
```

**To Reproduce**
Steps to reproduce the behavior:
Using `briefcase new`, include a literal backslash character, i.e. `\`, in the Formal Name or Description.

**Expected behavior**
A valid `pyproject.toml` file should always be created.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment:**
 - Operating System: Ubuntu 22.04
 - Python version: 3.10.7
 - Software versions:
   - Briefcase: cec41018b861876453a9f8e922dc84bd4e095028

**Additional Information**
#898 is currently proposing to introduce [`tomli-w`](https://pypi.org/project/tomli-w/) to write an entire TOML file for a static web app. This package only exposes support for writing an entire TOML document. Something like [`tomlkit`](https://pypi.org/project/tomlkit/) exposes a larger API for creating parts of a TOML document.

Pull request:
Add cookiecutter filter for escaping double quotes and backslashes #905
<!--- Describe your changes in detail -->
Add cookiecutter filter for escaping double quotes and backslashes in Formal Name and Description

<!--- What problem does this change solve? -->
Backslashes and double quotes are now safe to be used for Formal Name and Description

<!--- If this PR relates to an issue, include Refs #XXX or Fixes #XXX -->
Fixes #905 

## 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
- [] 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/905.bugfix.rst b/changes/905.bugfix.rst
new file mode 100644
index 00000000..51762c67
--- /dev/null
+++ b/changes/905.bugfix.rst
@@ -0,0 +1,1 @@
+Backslashes and double quotes are now safe to be used for Formal Name and Description
diff --git a/changes/931.misc.rst b/changes/931.misc.rst
deleted file mode 100644
index a5614b95..00000000
--- a/changes/931.misc.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-When generating a new template using `briefcase new`, the template & branch that created the project is logged.
diff --git a/src/briefcase/commands/new.py b/src/briefcase/commands/new.py
index c84e7175..65a2f512 100644
--- a/src/briefcase/commands/new.py
+++ b/src/briefcase/commands/new.py
@@ -492,13 +492,7 @@ What GUI toolkit do you want to use for this project?""",
                 f"A directory named '{context['app_name']}' already exists."
             )
 
-        # This is to have briefcase template file
-        # mentioning extra context on which template/branch
-        # the project was generated from.
-        context.update({"template": template, "branch": branch})
-
         try:
-            self.logger.info(f"Using app template: {template}, branch {branch}")
             # Unroll the new app template
             self.generate_template(
                 template=template,
diff --git a/src/briefcase/integrations/cookiecutter.py b/src/briefcase/integrations/cookiecutter.py
index d1b858a7..2e0f7d3e 100644
--- a/src/briefcase/integrations/cookiecutter.py
+++ b/src/briefcase/integrations/cookiecutter.py
@@ -51,3 +51,17 @@ class RGBExtension(Extension):
         environment.filters["float_red"] = float_red
         environment.filters["float_green"] = float_green
         environment.filters["float_blue"] = float_blue
+
+
+class TOMLEscape(Extension):
+    """Jinja2 extension to escape strings so TOML don't break."""
+
+    def __init__(self, environment):
+        """Initialize the extension with the given environment."""
+        super().__init__(environment)
+
+        def escape_toml(obj):
+            """Escapes double quotes and backslashes."""
+            return obj.replace('"', '"').replace("\\", "\\\\")
+
+        environment.filters["escape_toml"] = escape_toml


Test patch:
diff --git a/tests/commands/new/test_new_app.py b/tests/commands/new/test_new_app.py
index 093dfcbf..64eb4cc6 100644
--- a/tests/commands/new/test_new_app.py
+++ b/tests/commands/new/test_new_app.py
@@ -41,13 +41,12 @@ def test_new_app(
 ):
     """A new app can be created with the default template."""
     monkeypatch.setattr(briefcase, "__version__", briefcase_version)
-    new_command.build_app_context = mock.MagicMock(
-        return_value={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-        }
-    )
+    app_context = {
+        "formal_name": "My Application",
+        "class_name": "MyApplication",
+        "app_name": "myapplication",
+    }
+    new_command.build_app_context = mock.MagicMock(return_value=app_context)
     new_command.update_cookiecutter_cache = mock.MagicMock(
         return_value="~/.cookiecutters/briefcase-template"
     )
@@ -69,29 +68,19 @@ def test_new_app(
         no_input=True,
         output_dir=os.fsdecode(tmp_path),
         checkout=expected_branch,
-        extra_context={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-            # The expected app context
-            # should now also contain the
-            # default template and branch
-            "template": "https://github.com/beeware/briefcase-template",
-            "branch": expected_branch,
-        },
+        extra_context=app_context,
     )
 
 
 def test_new_app_missing_template(monkeypatch, new_command, tmp_path):
     """If a versioned branch doesn't exist, an error is raised."""
     monkeypatch.setattr(briefcase, "__version__", "37.42.7")
-    new_command.build_app_context = mock.MagicMock(
-        return_value={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-        }
-    )
+    app_context = {
+        "formal_name": "My Application",
+        "class_name": "MyApplication",
+        "app_name": "myapplication",
+    }
+    new_command.build_app_context = mock.MagicMock(return_value=app_context)
     new_command.update_cookiecutter_cache = mock.MagicMock(
         return_value="~/.cookiecutters/briefcase-template"
     )
@@ -121,16 +110,7 @@ def test_new_app_missing_template(monkeypatch, new_command, tmp_path):
         no_input=True,
         output_dir=os.fsdecode(tmp_path),
         checkout="v37.42.7",
-        extra_context={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-            # The expected app context
-            # should now also contain the
-            # default template and branch
-            "template": "https://github.com/beeware/briefcase-template",
-            "branch": "v37.42.7",
-        },
+        extra_context=app_context,
     )
 
 
@@ -142,13 +122,12 @@ def test_new_app_dev(monkeypatch, new_command, tmp_path, briefcase_version):
     """In a dev version, template will fall back to the 'main' branch if a
     versioned template doesn't exist."""
     monkeypatch.setattr(briefcase, "__version__", briefcase_version)
-    new_command.build_app_context = mock.MagicMock(
-        return_value={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-        }
-    )
+    app_context = {
+        "formal_name": "My Application",
+        "class_name": "MyApplication",
+        "app_name": "myapplication",
+    }
+    new_command.build_app_context = mock.MagicMock(return_value=app_context)
     new_command.update_cookiecutter_cache = mock.MagicMock(
         return_value="~/.cookiecutters/briefcase-template"
     )
@@ -187,32 +166,14 @@ def test_new_app_dev(monkeypatch, new_command, tmp_path, briefcase_version):
                 no_input=True,
                 output_dir=os.fsdecode(tmp_path),
                 checkout="v37.42.7",
-                extra_context={
-                    "formal_name": "My Application",
-                    "class_name": "MyApplication",
-                    "app_name": "myapplication",
-                    # The expected app context
-                    # should now also contain the
-                    # default template and branch
-                    "template": "https://github.com/beeware/briefcase-template",
-                    "branch": "v37.42.7",
-                },
+                extra_context=app_context,
             ),
             mock.call(
                 "~/.cookiecutters/briefcase-template",
                 no_input=True,
                 output_dir=os.fsdecode(tmp_path),
                 checkout="main",
-                extra_context={
-                    "formal_name": "My Application",
-                    "class_name": "MyApplication",
-                    "app_name": "myapplication",
-                    # The expected app context
-                    # should now also contain the
-                    # default template and branch
-                    "template": "https://github.com/beeware/briefcase-template",
-                    "branch": "v37.42.7",
-                },
+                extra_context=app_context,
             ),
         ]
     )
@@ -222,13 +183,12 @@ def test_new_app_with_template(monkeypatch, new_command, tmp_path):
     """A specific template can be requested."""
     monkeypatch.setattr(briefcase, "__version__", "37.42.7")
 
-    new_command.build_app_context = mock.MagicMock(
-        return_value={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-        }
-    )
+    app_context = {
+        "formal_name": "My Application",
+        "class_name": "MyApplication",
+        "app_name": "myapplication",
+    }
+    new_command.build_app_context = mock.MagicMock(return_value=app_context)
     new_command.update_cookiecutter_cache = mock.MagicMock(
         return_value="https://example.com/other.git"
     )
@@ -250,16 +210,7 @@ def test_new_app_with_template(monkeypatch, new_command, tmp_path):
         no_input=True,
         output_dir=os.fsdecode(tmp_path),
         checkout="v37.42.7",
-        extra_context={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-            # The expected app context
-            # should now also contain the
-            # template and branch
-            "template": "https://example.com/other.git",
-            "branch": "v37.42.7",
-        },
+        extra_context=app_context,
     )
 
 
@@ -267,13 +218,12 @@ def test_new_app_with_invalid_template(monkeypatch, new_command, tmp_path):
     """If the custom template is invalid, an error is raised."""
     monkeypatch.setattr(briefcase, "__version__", "37.42.7")
 
-    new_command.build_app_context = mock.MagicMock(
-        return_value={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-        }
-    )
+    app_context = {
+        "formal_name": "My Application",
+        "class_name": "MyApplication",
+        "app_name": "myapplication",
+    }
+    new_command.build_app_context = mock.MagicMock(return_value=app_context)
     new_command.update_cookiecutter_cache = mock.MagicMock(
         return_value="https://example.com/other.git"
     )
@@ -301,16 +251,7 @@ def test_new_app_with_invalid_template(monkeypatch, new_command, tmp_path):
         no_input=True,
         output_dir=os.fsdecode(tmp_path),
         checkout="v37.42.7",
-        extra_context={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-            # The expected app context
-            # should now also contain the
-            # template and branch
-            "template": "https://example.com/other.git",
-            "branch": "v37.42.7",
-        },
+        extra_context=app_context,
     )
 
 
@@ -319,13 +260,12 @@ def test_new_app_with_invalid_template_branch(monkeypatch, new_command, tmp_path
     is raised."""
     monkeypatch.setattr(briefcase, "__version__", "37.42.7")
 
-    new_command.build_app_context = mock.MagicMock(
-        return_value={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-        }
-    )
+    app_context = {
+        "formal_name": "My Application",
+        "class_name": "MyApplication",
+        "app_name": "myapplication",
+    }
+    new_command.build_app_context = mock.MagicMock(return_value=app_context)
     new_command.update_cookiecutter_cache = mock.MagicMock(
         return_value="https://example.com/other.git"
     )
@@ -355,16 +295,7 @@ def test_new_app_with_invalid_template_branch(monkeypatch, new_command, tmp_path
         no_input=True,
         output_dir=os.fsdecode(tmp_path),
         checkout="v37.42.7",
-        extra_context={
-            "formal_name": "My Application",
-            "class_name": "MyApplication",
-            "app_name": "myapplication",
-            # The expected app context
-            # should now also contain the
-            # template and branch
-            "template": "https://example.com/other.git",
-            "branch": "v37.42.7",
-        },
+        extra_context=app_context,
     )
 
 
diff --git a/tests/integrations/cookiecutter/test_TOMLEscape.py b/tests/integrations/cookiecutter/test_TOMLEscape.py
new file mode 100644
index 00000000..d9d62a43
--- /dev/null
+++ b/tests/integrations/cookiecutter/test_TOMLEscape.py
@@ -0,0 +1,21 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from briefcase.integrations.cookiecutter import TOMLEscape
+
+
+@pytest.mark.parametrize(
+    "value, expected",
+    [
+        # Single digit minor
+        ("Hello World", "Hello World"),
+        ('Hello " World', 'Hello " World'),
+        ("Hello \\ World", "Hello \\\\ World"),
+    ],
+)
+def test_escape_toml(value, expected):
+    env = MagicMock()
+    env.filters = {}
+    TOMLEscape(env)
+    assert env.filters["escape_toml"](value) == expected