Repository: beeware/briefcase

Issue:
Ask user if the app is a console app during conversion
### What is the problem or limitation you are having?

Before I really understood the "console app" settings, I was a bit confused when my app wouldn't run following conversion. I finally realized that console apps require the `console_app = true` key to control `stdout` properly. After adding that setting, my app started working.

### Describe the solution you'd like

During the conversion process, ask the user if the existing app is a console app and configure `pyproject.toml` appropriately.

### Describe alternatives you've considered

Manually add the console app settings.

### Additional context

_No response_

Discussion:
logan-keede: 
> There's also a need for tests. Briefcase has 100% branch coverage, and any new feature requires tests of that new feature. The Briefcase contribution guide (in the docs) has pointers on how to set up a local testing environment.

![image](https://github.com/user-attachments/assets/dda745c3-15d0-4f20-916c-e60a10fda25c)

Hi, I get this error in coverage test, but I can not completely understand what I am doing wrong. I would be grateful if you can point me in right direction.

My analysis:-
The `self.values` that is being poped does not seem to be under my control, because all coverage test pass if I set `self.input.tool=DummyConsole("")` at  
[conftest.py#L26](https://github.com/beeware/briefcase/blob/a898beaed7126f07b3a0ae21df8b291d512f798f/tests/commands/convert/conftest.py#L26)  which seems to be equivalent of empty input instead of previous `no input at all`.
This `no input at all` does not bother other test case since we are using overrides, [(like build_app_context)](https://github.com/logan-keede/briefcase/blob/main/tests/commands/convert/test_build_app_context.py) but this particular test is referred as `def test_overrides_are_not_used(convert_command):` which adds to my confusion.

Possible solution:-
1. use `DummyConsole('')` but since `input.tools` is used by others, it may effect coverage quality?
2. add Overrides in this test(for new feature console_app) and change function name.
3. other?

Thanks,
logan

freakboy3742: > Hi, I get this error in coverage test, but I can not completely understand what I am doing wrong. I would be grateful if you can point me in right direction.

The underlying issue is that as a result of the changes you've made, the test in question is now asking for user input. This input *is* controllable, in multiple ways - see the tests for `test_input_*` in the same folder.

However, in this case, the test is misleadingly named, and it should be verifying that overrides are being used - so, your option 2.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a917757..9290367a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -111,7 +111,7 @@ jobs:
 
     - name: Store Coverage Data
       if: always() && contains('success,failure', steps.test.outcome)
-      uses: actions/upload-artifact@v4.4.3
+      uses: actions/upload-artifact@v4.5.0
       with:
         name: coverage-data-${{ matrix.platform }}-${{ matrix.python-version }}
         path: ".coverage.*"
@@ -167,7 +167,7 @@ jobs:
 
     - name: Upload Project Coverage HTML Report
       if: always() && steps.project-coverage.outcome == 'failure'
-      uses: actions/upload-artifact@v4.4.3
+      uses: actions/upload-artifact@v4.5.0
       with:
         name: html-coverage-report-project
         path: htmlcov
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 81bc4e69..74efe622 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ repos:
       - id: end-of-file-fixer
       - id: trailing-whitespace
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.19.0
+    rev: v3.19.1
     hooks:
       - id: pyupgrade
         args: [--py39-plus]
diff --git a/changes/1900.feature.rst b/changes/1900.feature.rst
new file mode 100644
index 00000000..5a3bdca8
--- /dev/null
+++ b/changes/1900.feature.rst
@@ -0,0 +1,1 @@
+The "briefcase convert" command can now be used to configure a console-based applications.
diff --git a/changes/2091.misc.rst b/changes/2091.misc.rst
new file mode 100644
index 00000000..b2113333
--- /dev/null
+++ b/changes/2091.misc.rst
@@ -0,0 +1,1 @@
+Updated sphinxcontrib-spelling from 8.0.0 to 8.0.1.
diff --git a/changes/2092.misc.rst b/changes/2092.misc.rst
new file mode 100644
index 00000000..9eb46067
--- /dev/null
+++ b/changes/2092.misc.rst
@@ -0,0 +1,1 @@
+The ``pre-commit`` hook for ``pyupgrade`` was updated to its latest version.
diff --git a/changes/2093.misc.rst b/changes/2093.misc.rst
new file mode 100644
index 00000000..fe61a218
--- /dev/null
+++ b/changes/2093.misc.rst
@@ -0,0 +1,1 @@
+Updated actions/upload-artifact from 4.4.3 to 4.5.0.
diff --git a/changes/2095.misc.rst b/changes/2095.misc.rst
new file mode 100644
index 00000000..4ef80fce
--- /dev/null
+++ b/changes/2095.misc.rst
@@ -0,0 +1,1 @@
+Updated coverage[toml] from 7.6.9 to 7.6.10.
diff --git a/pyproject.toml b/pyproject.toml
index 4ec56180..dd92d40e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,7 +95,7 @@ dependencies = [
 # Extras used by developers *of* briefcase are pinned to specific versions to
 # ensure environment consistency.
 dev = [
-    "coverage[toml] == 7.6.9",
+    "coverage[toml] == 7.6.10",
     "coverage-conditional-plugin == 0.9.0",
     "pre-commit == 4.0.1",
     "pytest == 8.3.4",
@@ -112,7 +112,7 @@ docs = [
     "sphinx_tabs == 3.4.7",
     "sphinx-autobuild == 2024.10.3",
     "sphinx-copybutton == 0.5.2",
-    "sphinxcontrib-spelling == 8.0.0",
+    "sphinxcontrib-spelling == 8.0.1",
 ]
 
 [project.urls]
diff --git a/src/briefcase/bootstraps/console.py b/src/briefcase/bootstraps/console.py
index 4e7075a1..1b6c4b34 100644
--- a/src/briefcase/bootstraps/console.py
+++ b/src/briefcase/bootstraps/console.py
@@ -1,9 +1,18 @@
+from __future__ import annotations
+
+from typing import Any
+
 from briefcase.bootstraps.base import BaseGuiBootstrap
 
 
 class ConsoleBootstrap(BaseGuiBootstrap):
     display_name_annotation = "does not support iOS/Android/Web deployment"
 
+    def extra_context(self) -> dict[str, Any] | None:
+        return {
+            "console_app": True,
+        }
+
     def app_source(self):
         return """\
 
@@ -22,7 +31,6 @@ if __name__ == "__main__":
 
     def pyproject_table_briefcase_app_extra_content(self):
         return """
-console_app = true
 requires = [
 ]
 test_requires = [
diff --git a/src/briefcase/commands/convert.py b/src/briefcase/commands/convert.py
index 5d408a8a..d4f16760 100644
--- a/src/briefcase/commands/convert.py
+++ b/src/briefcase/commands/convert.py
@@ -326,6 +326,23 @@ class ConvertCommand(NewCommand):
 
         return url
 
+    def input_console_app(self, override_value: str | None) -> bool:
+        """Ask about whether the app is a console application.
+
+        :returns: console_app
+        """
+        options = ["GUI", "Console"]
+        return (
+            self.select_option(
+                intro="Is this a GUI application or a console application?",
+                variable="interface style",
+                default=None,
+                options=options,
+                override_value=override_value,
+            )
+            == "Console"
+        )
+
     def input_bundle(self, url, app_name, override_value: str | None) -> str:
         default = ".".join(reversed(urlparse(url).netloc.split(".")))
         return self.input_text(
@@ -603,7 +620,14 @@ class ConvertCommand(NewCommand):
         # already is set up for a GUI-framework, then those dependencies should already be listed.
         # To prevent the same dependency being listed twice (once in the PEP621-section and once in the
         # briefcase-section), possibly with different versions, we set the GUI-framework to None here.
-        return {"gui_framework": "None"}
+
+        console_app = self.input_console_app(
+            override_value=project_overrides.pop("console_app", None)
+        )
+        return {
+            "gui_framework": "None",
+            "console_app": console_app,
+        }
 
     def merge_or_copy_pyproject(self, briefcase_config_file: Path) -> None:
         """Merge pyproject.toml file made by the cookiecutter with the one in the


Test patch:
diff --git a/tests/commands/convert/test_build_gui_context.py b/tests/commands/convert/test_build_gui_context.py
index 4aac9512..e9a410a8 100644
--- a/tests/commands/convert/test_build_gui_context.py
+++ b/tests/commands/convert/test_build_gui_context.py
@@ -1,5 +1,5 @@
-def test_overrides_are_not_used(convert_command):
-    overrides = {"gui_framework": "Toga"}
+def test_overrides_are_used(convert_command):
+    overrides = {"gui_framework": "Toga", "console_app": "Console"}
     out = convert_command.build_gui_context({}, overrides)
-    assert out == {"gui_framework": "None"}
+    assert out == {"gui_framework": "None", "console_app": True}
     assert overrides == {"gui_framework": "Toga"}
diff --git a/tests/commands/convert/test_input_console_app.py b/tests/commands/convert/test_input_console_app.py
new file mode 100644
index 00000000..880edbeb
--- /dev/null
+++ b/tests/commands/convert/test_input_console_app.py
@@ -0,0 +1,33 @@
+import pytest
+
+
+def test_overrides_are_used_for_console(convert_command):
+    overrides = {"console_app": "Console"}
+    out = convert_command.build_gui_context({}, overrides)
+    assert out == {"console_app": True, "gui_framework": "None"}
+
+
+def test_overrides_are_used_for_GUI(convert_command):
+    overrides = {"console_app": "GUI"}
+    out = convert_command.build_gui_context({}, overrides)
+    assert out == {"console_app": False, "gui_framework": "None"}
+
+
+@pytest.mark.parametrize(
+    "input, result",
+    [
+        # A GUI app
+        (["1"], False),
+        # A console app
+        (["2"], True),
+        # Default value is False (GUI app)
+        ([""], False),
+        # Invalid values are rejected until a valid value is provided.
+        (["x", "y", "2"], True),
+    ],
+)
+def test_app_type(convert_command, input, result):
+    """The user can be asked for the app type."""
+    convert_command.input.values = input
+    out = convert_command.input_console_app(None)
+    assert out == result
diff --git a/tests/commands/new/test_build_context.py b/tests/commands/new/test_build_context.py
index c2643b39..7e56bf56 100644
--- a/tests/commands/new/test_build_context.py
+++ b/tests/commands/new/test_build_context.py
@@ -350,6 +350,7 @@ def test_question_sequence_console(new_command):
         test_source_dir="tests",
         project_name="My Project",
         url="https://navy.mil/myapplication",
+        console_app=True,
         app_source="""\
 
 def main():
@@ -363,7 +364,6 @@ if __name__ == "__main__":
     main()
 """,
         pyproject_table_briefcase_app_extra_content="""
-console_app = true
 requires = [
 ]
 test_requires = [