Repository: lundberg/respx

Issue:
`files` pattern not handling `str` and `io.BytesIO`
When using the `files` parameter in a helper (http) function, both `str` and `io.BytesIO` payloads are not deemed equal and raise an error.

# Input

## `str` payload
```py
@respx.mock
def test_file_str_payload():
    FILE_DATA = {"upload": ("image/png", "str", "image/png")}

    respx.patch("https://api.example.com/endpoint/123", files=FILE_DATA)

    httpx.patch("https://api.example.com/endpoint/123", files=FILE_DATA)

```

## `io.BytesIO` payload
```py
@respx.mock
def test_file_bytesio_payload():
    FILE_DATA = {
        "upload": ("image/png", io.BytesIO(b"some image content"), "image/png")
    }

    respx.patch("https://api.example.com/endpoint/123", files=FILE_DATA)

    httpx.patch("https://api.example.com/endpoint/123", files=FILE_DATA)
```

# Output

## Error raised
```bash
raise AllMockedAssertionError(f"RESPX: {request!r} not mocked!")
respx.models.AllMockedAssertionError: RESPX: <Request('PATCH', 'https://api.example.com/endpoint/123')> not mocked!
```

Comments:

Patch:
diff --git a/respx/patterns.py b/respx/patterns.py
index 8d80148..da2022a 100644
--- a/respx/patterns.py
+++ b/respx/patterns.py
@@ -1,3 +1,4 @@
+import io
 import json as jsonlib
 import operator
 import pathlib
@@ -562,7 +563,7 @@ class Files(MultiItemsMixin, Pattern):
     key = "files"
     value: MultiItems
 
-    def _normalize_file_value(self, value: FileTypes) -> Tuple[Any, ...]:
+    def _normalize_file_value(self, value: FileTypes) -> Tuple[Any, Any]:
         # Mimic httpx `FileField` to normalize `files` kwarg to shortest tuple style
         if isinstance(value, tuple):
             filename, fileobj = value[:2]
@@ -573,6 +574,12 @@ class Files(MultiItemsMixin, Pattern):
                 filename = ANY
             fileobj = value
 
+        # Normalize file-like objects and strings to bytes to allow equality check
+        if isinstance(fileobj, io.BytesIO):
+            fileobj = fileobj.read()
+        elif isinstance(fileobj, str):
+            fileobj = fileobj.encode()
+
         return filename, fileobj
 
     def clean(self, value: RequestFiles) -> MultiItems:


Test patch:
diff --git a/tests/test_patterns.py b/tests/test_patterns.py
index 4b119aa..451b0dd 100644
--- a/tests/test_patterns.py
+++ b/tests/test_patterns.py
@@ -1,3 +1,4 @@
+import io
 import re
 from unittest.mock import ANY
 
@@ -456,6 +457,18 @@ def test_data_pattern(lookup, data, request_data, expected):
             },
             False,
         ),
+        (
+            Lookup.EQUAL,
+            {"file_1": ("filename.png", io.BytesIO(b"some..image..data"), "image/png")},
+            None,
+            True,
+        ),
+        (
+            Lookup.EQUAL,
+            {"file_1": ("filename.png", "some..image..data", "image/png")},  # str data
+            {"file_1": ("filename.png", io.BytesIO(b"some..image..data"), "image/png")},
+            True,
+        ),
         (
             Lookup.CONTAINS,
             {
@@ -487,6 +500,15 @@ def test_data_pattern(lookup, data, request_data, expected):
             },
             True,
         ),
+        (
+            Lookup.CONTAINS,
+            {"file_1": "foo..."},  # str data
+            {
+                "file_1": ("filename_1.txt", io.BytesIO(b"foo...")),
+                "file_2": ("filename_2.txt", io.BytesIO(b"bar...")),
+            },
+            True,
+        ),
         (
             Lookup.CONTAINS,
             [("file_1", b"ham...")],