Repository: beeware/briefcase

Issue:
Requires-python checks don't work with pre-release Python versions
### Describe the bug

#2029 added support for PEP 621 `requires-python` declarations. However, these version checks appear to *always* fail if the Python version is a prerelease.

### Steps to reproduce

1. Use a prerelease Python (e.g. 3.13.0.rc2, or 3.14.0a1)
2. Run the Briefcase test suite.

In [this CI run](https://github.com/beeware/briefcase/actions/runs/11339866353/job/31535352956), using Python 3.14.0a1, there are 4 test failures:
```
=========================== short test summary info ============================
FAILED tests/commands/base/test_verify_requires_python.py::test_requires_python_met[!= 3.2] - briefcase.exceptions.UnsupportedPythonVersion: Unable to run Briefcase command. The project configuration requires Python versions != 3.2, but the environment's Python version is 3.14.0a0. Please run Briefcase using a Python version that satisfies the project's requirements.
FAILED tests/commands/base/test_verify_requires_python.py::test_requires_python_met[>= 3.2] - briefcase.exceptions.UnsupportedPythonVersion: Unable to run Briefcase command. The project configuration requires Python versions >= 3.2, but the environment's Python version is 3.14.0a0. Please run Briefcase using a Python version that satisfies the project's requirements.
FAILED tests/commands/base/test_verify_requires_python.py::test_requires_python_met[> 3.2] - briefcase.exceptions.UnsupportedPythonVersion: Unable to run Briefcase command. The project configuration requires Python versions > 3.2, but the environment's Python version is 3.14.0a0. Please run Briefcase using a Python version that satisfies the project's requirements.
FAILED tests/commands/base/test_verify_requires_python.py::test_requires_python_met[< 3.100] - briefcase.exceptions.UnsupportedPythonVersion: Unable to run Briefcase command. The project configuration requires Python versions < 3.100, but the environment's Python version is 3.14.0a0. Please run Briefcase using a Python version that satisfies the project's requirements.
```


### Expected behavior

3.14.0a1 should pass `requires-python` checks that require `!=3.2`, `>3.2`, and `<3.100`.

### Screenshots

_No response_

### Environment

- Operating System: Any
- Python version: Any pre-release version (3.14.0a1 tested)
- Software versions:
  - Briefcase: 0.3.20+


### Logs

```

```


### Additional context

This came up in the context of trying to add Python 3.14 to Briefcase's CI matrix.

Discussion:
@freakboy3742: Tagging @sarayourfriend since you were working on this recently; no obligation to look into it, but if you've got the interest and spare cycles.@sarayourfriend: Interesting! I think it will be an easy fix by passing `preeleases=True` to [`spec.contains`](https://packaging.pypa.io/en/latest/specifiers.html#packaging.specifiers.Specifier.contains). I'll try that out and open a PR here shortly if it works.@sarayourfriend: I'll drop a note here to the PyPA docs on pre-release handling in version specifiers, because the defaults/intentions here were new to me: https://packaging.python.org/en/latest/specifications/version-specifiers/#handling-of-pre-releases

It's somewhat different for Briefcase's `requires-python` check because it isn't trying to resolve a particular version of a dependency, rather just checking that the active one matches the specifier. In that case, "accept already installed pre-releases for all version specifiers" from the set of default behaviours makes sense to follow.

Solution patch:
diff --git a/changes/2034.bugfix.rst b/changes/2034.bugfix.rst
new file mode 100644
index 000000000..d3d0339a6
--- /dev/null
+++ b/changes/2034.bugfix.rst
@@ -0,0 +1 @@
+Pre-release Python interpreter versions are no longer rejected as matching candidates in PEP 621 ``requires-python`` checks.
diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py
index 1e192666c..cddd3726c 100644
--- a/src/briefcase/commands/base.py
+++ b/src/briefcase/commands/base.py
@@ -681,7 +681,7 @@ def verify_required_python(self, app: AppConfig):
 
         running_version = platform.python_version()
 
-        if not spec.contains(running_version):
+        if not spec.contains(running_version, prereleases=True):
             raise UnsupportedPythonVersion(
                 version_specifier=requires_python, running_version=running_version
             )


Test patch:
diff --git a/tests/commands/base/test_verify_requires_python.py b/tests/commands/base/test_verify_requires_python.py
index 33a1ced52..b47c80c36 100644
--- a/tests/commands/base/test_verify_requires_python.py
+++ b/tests/commands/base/test_verify_requires_python.py
@@ -1,4 +1,5 @@
 import platform
+from unittest import mock
 
 import pytest
 
@@ -74,3 +75,12 @@ def test_requires_python_invalid_specifier(base_command, my_app):
 
     with pytest.raises(BriefcaseConfigError, match="Invalid requires-python"):
         base_command.verify_required_python(my_app)
+
+
+@mock.patch("platform.python_version")
+def test_requires_python_prerelease(python_version_mock, base_command, my_app):
+    """Verify that pre-release Python versions are included in matches."""
+    python_version_mock.return_value = "3.14.0a0"
+
+    base_command.global_config = _get_global_config(requires_python=">=3.12")
+    base_command.verify_required_python(my_app)