Repository: pallets/werkzeug

Issue:
new deprecation warnings in Python 3.12
Starting flask in Python 3.12 raises warnings of deprecations in Python 3.14 within Werkzeug

```default
/Users/anthonyshaw/projects/cpython/.venv-312/lib/python3.12/site-packages/werkzeug/routing/rules.py:751: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead
  parts = parts or [ast.Str("")]
/Users/anthonyshaw/projects/cpython/.venv-312/lib/python3.12/site-packages/werkzeug/routing/rules.py:748: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead
  _convert(elem) if is_dynamic else ast.Str(s=elem)
/Users/anthonyshaw/projects/cpython/Lib/ast.py:587: DeprecationWarning: Attribute s is deprecated and will be removed in Python 3.14; use value instead
  return Constant(*args, **kwargs)
/Users/anthonyshaw/projects/cpython/.venv-312/lib/python3.12/site-packages/werkzeug/routing/rules.py:755: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead
  if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str):
/Users/anthonyshaw/projects/cpython/.venv-312/lib/python3.12/site-packages/werkzeug/routing/rules.py:756: DeprecationWarning: Attribute s is deprecated and will be removed in Python 3.14; use value instead
  ret[-1] = ast.Str(ret[-1].s + p.s)
/Users/anthonyshaw/projects/cpython/.venv-312/lib/python3.12/site-packages/werkzeug/routing/rules.py:756: DeprecationWarning: ast.Str is deprecated and will be removed in Python 3.14; use ast.Constant instead
  ret[-1] = ast.Str(ret[-1].s + p.s)
```

For example:

https://github.com/pallets/werkzeug/blob/76b049dd45fd072fb62a54bccc0e8d513b03f4d8/src/werkzeug/routing/rules.py#L757

The documentation states "Changed in version 3.8: Class [ast.Constant](https://docs.python.org/3/library/ast.html#ast.Constant) is now used for all constants." so I don't know if the `ast.Str` conditions are even working in versions 3.8+.

Considering Werkzeug just dropped support for Python 3.7, the rules code could be refactored to use the new type without breaking compatibility

Environment:

- Python version: 3.12a1
- Werkzeug version: 2.2.3



Discussion:
Got confused for a second because I was seeing the errors after fixing our uses. It's because pytest still has the issue internally as well: https://github.com/pytest-dev/pytest/issues/10977

Resolving patch:
diff --git a/src/werkzeug/datastructures/auth.py b/src/werkzeug/datastructures/auth.py
--- a/src/werkzeug/datastructures/auth.py
+++ b/src/werkzeug/datastructures/auth.py
@@ -393,7 +393,7 @@ def to_header(self) -> str:
             items = []
 
             for key, value in self.parameters.items():
-                if key in {"realm", "domain", "nonce", "opaque", "realm", "qop"}:
+                if key in {"realm", "domain", "nonce", "opaque", "qop"}:
                     value = quote_header_value(value, allow_token=False)
                 else:
                     value = quote_header_value(value)
diff --git a/src/werkzeug/middleware/shared_data.py b/src/werkzeug/middleware/shared_data.py
--- a/src/werkzeug/middleware/shared_data.py
+++ b/src/werkzeug/middleware/shared_data.py
@@ -10,9 +10,9 @@
 """
 from __future__ import annotations
 
+import importlib.util
 import mimetypes
 import os
-import pkgutil
 import posixpath
 import typing as t
 from datetime import datetime
@@ -158,8 +158,8 @@ def get_file_loader(self, filename: str) -> _TLoader:
 
     def get_package_loader(self, package: str, package_path: str) -> _TLoader:
         load_time = datetime.now(timezone.utc)
-        provider = pkgutil.get_loader(package)
-        reader = provider.get_resource_reader(package)  # type: ignore
+        spec = importlib.util.find_spec(package)
+        reader = spec.loader.get_resource_reader(package)  # type: ignore[union-attr]
 
         def loader(
             path: str | None,
diff --git a/src/werkzeug/routing/rules.py b/src/werkzeug/routing/rules.py
--- a/src/werkzeug/routing/rules.py
+++ b/src/werkzeug/routing/rules.py
@@ -754,15 +754,15 @@ def _convert(elem: str) -> ast.stmt:
 
         def _parts(ops: list[tuple[bool, str]]) -> list[ast.AST]:
             parts = [
-                _convert(elem) if is_dynamic else ast.Str(s=elem)
+                _convert(elem) if is_dynamic else ast.Constant(elem)
                 for is_dynamic, elem in ops
             ]
-            parts = parts or [ast.Str("")]
+            parts = parts or [ast.Constant("")]
             # constant fold
             ret = [parts[0]]
             for p in parts[1:]:
-                if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str):
-                    ret[-1] = ast.Str(ret[-1].s + p.s)
+                if isinstance(p, ast.Constant) and isinstance(ret[-1], ast.Constant):
+                    ret[-1] = ast.Constant(ret[-1].value + p.value)
                 else:
                     ret.append(p)
             return ret
@@ -798,7 +798,7 @@ def _join(parts: list[ast.AST]) -> ast.AST:
             func_ast.args.args.append(ast.arg(arg, None))
         func_ast.args.kwarg = ast.arg(".kwargs", None)
         for _ in kargs:
-            func_ast.args.defaults.append(ast.Str(""))
+            func_ast.args.defaults.append(ast.Constant(""))
         func_ast.body = body
 
         # Use `ast.parse` instead of `ast.Module` for better portability, since the


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -586,7 +586,8 @@ def test_server_name_interpolation():
 
     with pytest.warns(UserWarning):
         adapter = map.bind_to_environ(env, server_name="foo")
-        assert adapter.subdomain == "<invalid>"
+
+    assert adapter.subdomain == "<invalid>"
 
 
 def test_rule_emptying():
@@ -959,8 +960,7 @@ def test_build_drop_none():
     adapter = map.bind("", "/")
     params = {"flub": None, "flop": None}
     with pytest.raises(r.BuildError):
-        x = adapter.build("endp", params)
-        assert not x
+        adapter.build("endp", params)
     params = {"flub": "x", "flop": None}
     url = adapter.build("endp", params)
     assert "flop" not in url
@@ -1041,7 +1041,8 @@ def test_external_building_with_port_bind_to_environ_wrong_servername():
 
     with pytest.warns(UserWarning):
         adapter = map.bind_to_environ(environ, server_name="example.org")
-        assert adapter.subdomain == "<invalid>"
+
+    assert adapter.subdomain == "<invalid>"
 
 
 def test_converter_parser():
@@ -1495,8 +1496,7 @@ def test_strict_slashes_false():
 
 def test_invalid_rule():
     with pytest.raises(ValueError):
-        map_ = r.Map([r.Rule("/<int()>", endpoint="test")])
-        map_.bind("localhost")
+        r.Map([r.Rule("/<int()>", endpoint="test")])
 
 
 def test_multiple_converters_per_part():