Repository: pallets/werkzeug

Issue:
`parse_options_header` rejects q values "0" and "1"
provide an accept header such as `en;q=0` or `en;q=1` -> 

q values 0 or 1 are dropped.  The [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-quality-values) indicates that the decimal is optional however we are rejecting q values that do not contain a decimal

Environment:

- Python version: 3.11.4
- Werkzeug version: 2.3.6

`parse_options_header` rejects q values "0" and "1"
provide an accept header such as `en;q=0` or `en;q=1` -> 

q values 0 or 1 are dropped.  The [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-quality-values) indicates that the decimal is optional however we are rejecting q values that do not contain a decimal

Environment:

- Python version: 3.11.4
- Werkzeug version: 2.3.6



Discussion:
Happy to review a PR.
Happy to review a PR.

Resolving patch:
diff --git a/src/werkzeug/_internal.py b/src/werkzeug/_internal.py
--- a/src/werkzeug/_internal.py
+++ b/src/werkzeug/_internal.py
@@ -313,7 +313,6 @@ def _decode_idna(domain: str) -> str:
 
 
 _plain_int_re = re.compile(r"-?\d+", re.ASCII)
-_plain_float_re = re.compile(r"-?\d+\.\d+", re.ASCII)
 
 
 def _plain_int(value: str) -> int:
@@ -329,19 +328,3 @@ def _plain_int(value: str) -> int:
         raise ValueError
 
     return int(value)
-
-
-def _plain_float(value: str) -> float:
-    """Parse a float only if it is only ASCII digits and ``-``, and contains digits
-    before and after the ``.``.
-
-    This disallows ``+``, ``_``, non-ASCII digits, and ``.123``, which are accepted by
-    ``float`` but are not allowed in HTTP header values.
-
-    Any leading or trailing whitespace is stripped
-    """
-    value = value.strip()
-    if _plain_float_re.fullmatch(value) is None:
-        raise ValueError
-
-    return float(value)
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -18,7 +18,6 @@
 from urllib.request import parse_http_list as _parse_list_header
 
 from ._internal import _dt_as_utc
-from ._internal import _plain_float
 from ._internal import _plain_int
 
 if t.TYPE_CHECKING:
@@ -614,6 +613,7 @@ def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:
     return value, options
 
 
+_q_value_re = re.compile(r"-?\d+(\.\d+)?", re.ASCII)
 _TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")
 
 
@@ -656,13 +656,15 @@ def parse_accept_header(
         item, options = parse_options_header(item)
 
         if "q" in options:
-            try:
-                # pop q, remaining options are reconstructed
-                q = _plain_float(options.pop("q"))
-            except ValueError:
+            # pop q, remaining options are reconstructed
+            q_str = options.pop("q").strip()
+
+            if _q_value_re.fullmatch(q_str) is None:
                 # ignore an invalid q
                 continue
 
+            q = float(q_str)
+
             if q < 0 or q > 1:
                 # ignore an invalid q
                 continue


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -776,6 +776,12 @@ def test_accept_invalid_float(value):
     assert list(a.values()) == ["en"]
 
 
+def test_accept_valid_int_one_zero():
+    assert http.parse_accept_header("en;q=1") == http.parse_accept_header("en;q=1.0")
+    assert http.parse_accept_header("en;q=0") == http.parse_accept_header("en;q=0.0")
+    assert http.parse_accept_header("en;q=5") == http.parse_accept_header("en;q=5.0")
+
+
 @pytest.mark.parametrize("value", ["🯱🯲🯳", "+1-", "1-1_23"])
 def test_range_invalid_int(value):
     assert http.parse_range_header(value) is None