Repository: pallets/werkzeug

Issue:
send_file calls max_age with Path instead of str
We use the following code to override the `SEND_FILE_MAX_AGE_DEFAULT` for certain file types:

```
# Set WOFF and WOFF2 caching to return 1 year as they should never change
class MyServer(Flask):
    def get_send_file_max_age(self, name):
        if name.lower().endswith('.woff') or name.lower().endswith('.woff2'):
            return 31536000
        return Flask.get_send_file_max_age(self, name)


# Initialize The Server
app = MyServer(__name__, template_folder=TEMPLATES_DIR, static_folder=STATIC_DIR)
```

As of the newly released Flask 2.0 this is failing with the following error:

```
  File "/Users/barry/almanac.httparchive.org/src/server/__init__.py", line 19, in get_send_file_max_age
    if filename.lower().endswith('.woff') or filename.lower().endswith('.woff2'):
AttributeError: 'PosixPath' object has no attribute 'lower'
```

So we have to change this to this to convert the name to a string:

```
# Set WOFF and WOFF2 caching to return 1 year as they should never change
class MyServer(Flask):
    def get_send_file_max_age(self, name):
        if str(name).lower().endswith('.woff') or str(name).lower().endswith('.woff2'):
            return 31536000
        return Flask.get_send_file_max_age(self, name)


# Initialize The Server
app = MyServer(__name__, template_folder=TEMPLATES_DIR, static_folder=STATIC_DIR)
```

Is this an intended behaviour change as [the documentation still says this uses a string as an argument](https://flask.palletsprojects.com/en/2.0.x/api/#flask.Flask.get_send_file_max_age) rather than a PosixPath?


Environment:

- Python version: 3.8
- Flask version: 2.0



Discussion:
You'll need to call `name = os.fspath(name)` first if your code only expects to be working with strings and not `Path` objects.
Then should the documentation be updated to note it's now a Path object and not a string: https://flask.palletsprojects.com/en/2.0.x/api/#flask.Flask.get_send_file_max_age

> **Parameters:** **filename** ([str](https://docs.python.org/3/library/stdtypes.html#str)) –

Note that your implementation already has an issue, as `get_send_file_max_age` could be called with `None` when sending file objects instead of paths. I'll change `send_file` to always pass a string or `None`, and update the annotations too.
Good spot on the `None` issue. Will update my code.

BTW I was happy to leave as `Path` but just wanted to check it was an intentional change. Then again I'm probably not the only one who will experience this as that code snippet is all over Stack Overflow (think that's where I got it from!) so maybe best to revert as you say.

Thanks for the quick response btw!

Resolving patch:
diff --git a/src/werkzeug/utils.py b/src/werkzeug/utils.py
--- a/src/werkzeug/utils.py
+++ b/src/werkzeug/utils.py
@@ -2,7 +2,6 @@
 import io
 import mimetypes
 import os
-import pathlib
 import pkgutil
 import re
 import sys
@@ -596,7 +595,7 @@ def send_file(
     etag: t.Union[bool, str] = True,
     last_modified: t.Optional[t.Union[datetime, int, float]] = None,
     max_age: t.Optional[
-        t.Union[int, t.Callable[[t.Optional[t.Union[os.PathLike, str]]], int]]
+        t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
     ] = None,
     use_x_sendfile: bool = False,
     response_class: t.Optional[t.Type["Response"]] = None,
@@ -675,7 +674,7 @@ def send_file(
 
         response_class = Response
 
-    path: t.Optional[pathlib.Path] = None
+    path: t.Optional[str] = None
     file: t.Optional[t.BinaryIO] = None
     size: t.Optional[int] = None
     mtime: t.Optional[float] = None
@@ -689,18 +688,18 @@ def send_file(
         # Flask will pass app.root_path, allowing its send_file wrapper
         # to not have to deal with paths.
         if _root_path is not None:
-            path = pathlib.Path(_root_path, path_or_file)
+            path = os.path.join(_root_path, path_or_file)
         else:
-            path = pathlib.Path(path_or_file).absolute()
+            path = os.path.abspath(path_or_file)
 
-        stat = path.stat()
+        stat = os.stat(path)
         size = stat.st_size
         mtime = stat.st_mtime
     else:
         file = path_or_file
 
     if download_name is None and path is not None:
-        download_name = path.name
+        download_name = os.path.basename(path)
 
     if mimetype is None:
         if download_name is None:
@@ -737,12 +736,12 @@ def send_file(
             " 'download_name' or pass a path instead of a file."
         )
 
-    if use_x_sendfile and path:
-        headers["X-Sendfile"] = str(path)
+    if use_x_sendfile and path is not None:
+        headers["X-Sendfile"] = path
         data = None
     else:
         if file is None:
-            file = path.open("rb")  # type: ignore
+            file = open(path, "rb")  # type: ignore
         elif isinstance(file, io.BytesIO):
             size = file.getbuffer().nbytes
         elif isinstance(file, io.TextIOBase):
@@ -780,7 +779,7 @@ def send_file(
     if isinstance(etag, str):
         rv.set_etag(etag)
     elif etag and path is not None:
-        check = adler32(str(path).encode("utf-8")) & 0xFFFFFFFF
+        check = adler32(path.encode("utf-8")) & 0xFFFFFFFF
         rv.set_etag(f"{mtime}-{size}-{check}")
 
     if conditional:


Test patch: