prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  non-ASCII characters filename in multipart/form-data
  I tried to send a file with russian letters in the file name, and `httpx` escaped all non-ASCII characters (as I understood it the way specified in [RFC 7578](https://tools.ietf.org/html/rfc7578)). But this is different from `requests` [behavior](https://github.com/psf/requests/blob/589a82256759018a7e5e289302898dae32544949/requests/models.py#L110), where the file name is simply written as is (using [`RequestField.make_multipart`](https://github.com/urllib3/urllib3/blob/f0d9ebc41e51c4c4c9990b1eed02d297fd1b20d8/src/urllib3/fields.py#L248) from `urllib3`). Maybe `httpx` should just change [`FileField.render_headers`](https://github.com/encode/httpx/blob/3ba2e8c328f05b07ee06ae40f99dce7fe1e8292c/httpx/multipart.py#L57), so as not to escape the file name and instead just put it as bytes?
  
  ```diff
  def render_headers(self) -> bytes:
  -    name = quote(self.name, encoding="utf-8").encode("ascii")
  -    filename = quote(self.filename, encoding="utf-8").encode("ascii")
  -    content_type = self.content_type.encode("ascii")
       return b"".join(
           [
               b'Content-Disposition: form-data; name="',
  -            name,
  +            self.name.encode(),
               b'"; filename="',
  -            filename,
  +            self.filename.encode(),
               b'"\r\n',
               b"Content-Type: ",
  -            content_type,
  +            self.content_type.encode(),
               b"\r\n",
          ]
      )
  ```
  
  </issue>
  <code>
  [start of httpx/multipart.py]
  1 import binascii
  2 import mimetypes
  3 import os
  4 import typing
  5 from io import BytesIO
  6 from urllib.parse import quote
  7 
  8 
  9 class Field:
  10     def render_headers(self) -> bytes:
  11         raise NotImplementedError()  # pragma: nocover
  12 
  13     def render_data(self) -> bytes:
  14         raise NotImplementedError()  # pragma: nocover
  15 
  16 
  17 class DataField(Field):
  18     def __init__(self, name: str, value: typing.Union[str, bytes]) -> None:
  19         if not isinstance(name, str):
  20             raise TypeError("Invalid type for name. Expected str.")
  21         if not isinstance(value, (str, bytes)):
  22             raise TypeError("Invalid type for value. Expected str or bytes.")
  23         self.name = name
  24         self.value = value
  25 
  26     def render_headers(self) -> bytes:
  27         name = quote(self.name, encoding="utf-8").encode("ascii")
  28         return b"".join(
  29             [b'Content-Disposition: form-data; name="', name, b'"\r\n' b"\r\n"]
  30         )
  31 
  32     def render_data(self) -> bytes:
  33         return (
  34             self.value if isinstance(self.value, bytes) else self.value.encode("utf-8")
  35         )
  36 
  37 
  38 class FileField(Field):
  39     def __init__(
  40         self, name: str, value: typing.Union[typing.IO[typing.AnyStr], tuple]
  41     ) -> None:
  42         self.name = name
  43         if not isinstance(value, tuple):
  44             self.filename = os.path.basename(getattr(value, "name", "upload"))
  45             self.file = value  # type: typing.Union[typing.IO[str], typing.IO[bytes]]
  46             self.content_type = self.guess_content_type()
  47         else:
  48             self.filename = value[0]
  49             self.file = value[1]
  50             self.content_type = (
  51                 value[2] if len(value) > 2 else self.guess_content_type()
  52             )
  53 
  54     def guess_content_type(self) -> str:
  55         return mimetypes.guess_type(self.filename)[0] or "application/octet-stream"
  56 
  57     def render_headers(self) -> bytes:
  58         name = quote(self.name, encoding="utf-8").encode("ascii")
  59         filename = quote(self.filename, encoding="utf-8").encode("ascii")
  60         content_type = self.content_type.encode("ascii")
  61         return b"".join(
  62             [
  63                 b'Content-Disposition: form-data; name="',
  64                 name,
  65                 b'"; filename="',
  66                 filename,
  67                 b'"\r\n',
  68                 b"Content-Type: ",
  69                 content_type,
  70                 b"\r\n",
  71                 b"\r\n",
  72             ]
  73         )
  74 
  75     def render_data(self) -> bytes:
  76         content = self.file.read()
  77         return content.encode("utf-8") if isinstance(content, str) else content
  78 
  79 
  80 def iter_fields(data: dict, files: dict) -> typing.Iterator[Field]:
  81     for name, value in data.items():
  82         if isinstance(value, (list, dict)):
  83             for item in value:
  84                 yield DataField(name=name, value=item)
  85         else:
  86             yield DataField(name=name, value=value)
  87 
  88     for name, value in files.items():
  89         yield FileField(name=name, value=value)
  90 
  91 
  92 def multipart_encode(data: dict, files: dict) -> typing.Tuple[bytes, str]:
  93     body = BytesIO()
  94     boundary = binascii.hexlify(os.urandom(16))
  95 
  96     for field in iter_fields(data, files):
  97         body.write(b"--%s\r\n" % boundary)
  98         body.write(field.render_headers())
  99         body.write(field.render_data())
  100         body.write(b"\r\n")
  101 
  102     body.write(b"--%s--\r\n" % boundary)
  103 
  104     content_type = "multipart/form-data; boundary=%s" % boundary.decode("ascii")
  105 
  106     return body.getvalue(), content_type
  107
  [end of httpx/multipart.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/httpx/multipart.py b/httpx/multipart.py
  --- a/httpx/multipart.py
  +++ b/httpx/multipart.py
  @@ -1,9 +1,17 @@
   import binascii
   import mimetypes
   import os
  +import re
   import typing
   from io import BytesIO
  -from urllib.parse import quote
  +
  +_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
  +_HTML5_FORM_ENCODING_REPLACEMENTS.update(
  +    {chr(c): "%{:02X}".format(c) for c in range(0x00, 0x1F + 1) if c != 0x1B}
  +)
  +_HTML5_FORM_ENCODING_RE = re.compile(
  +    r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])
  +)
   
   
   class Field:
  @@ -24,10 +32,8 @@
           self.value = value
   
       def render_headers(self) -> bytes:
  -        name = quote(self.name, encoding="utf-8").encode("ascii")
  -        return b"".join(
  -            [b'Content-Disposition: form-data; name="', name, b'"\r\n' b"\r\n"]
  -        )
  +        name = _format_param("name", self.name)
  +        return b"".join([b"Content-Disposition: form-data; ", name, b"\r\n\r\n"])
   
       def render_data(self) -> bytes:
           return (
  @@ -55,20 +61,18 @@
           return mimetypes.guess_type(self.filename)[0] or "application/octet-stream"
   
       def render_headers(self) -> bytes:
  -        name = quote(self.name, encoding="utf-8").encode("ascii")
  -        filename = quote(self.filename, encoding="utf-8").encode("ascii")
  -        content_type = self.content_type.encode("ascii")
  +        name = _format_param("name", self.name)
  +        filename = _format_param("filename", self.filename)
  +        content_type = self.content_type.encode()
           return b"".join(
               [
  -                b'Content-Disposition: form-data; name="',
  +                b"Content-Disposition: form-data; ",
                   name,
  -                b'"; filename="',
  +                b"; ",
                   filename,
  -                b'"\r\n',
  -                b"Content-Type: ",
  +                b"\r\nContent-Type: ",
                   content_type,
  -                b"\r\n",
  -                b"\r\n",
  +                b"\r\n\r\n",
               ]
           )
   
  @@ -104,3 +108,14 @@
       content_type = "multipart/form-data; boundary=%s" % boundary.decode("ascii")
   
       return body.getvalue(), content_type
  +
  +
  +def _format_param(name: str, value: typing.Union[str, bytes]) -> bytes:
  +    if isinstance(value, bytes):
  +        value = value.decode()
  +        
  +    def replacer(match: typing.Match[str]) -> str:
  +        return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]
  +
  +    value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)
  +    return f'{name}="{value}"'.encode()
  
verification_info: {"golden_diff": "diff --git a/httpx/multipart.py b/httpx/multipart.py\n--- a/httpx/multipart.py\n+++ b/httpx/multipart.py\n@@ -1,9 +1,17 @@\n import binascii\n import mimetypes\n import os\n+import re\n import typing\n from io import BytesIO\n-from urllib.parse import quote\n+\n+_HTML5_FORM_ENCODING_REPLACEMENTS = {'\"': \"%22\", \"\\\\\": \"\\\\\\\\\"}\n+_HTML5_FORM_ENCODING_REPLACEMENTS.update(\n+    {chr(c): \"%{:02X}\".format(c) for c in range(0x00, 0x1F + 1) if c != 0x1B}\n+)\n+_HTML5_FORM_ENCODING_RE = re.compile(\n+    r\"|\".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])\n+)\n \n \n class Field:\n@@ -24,10 +32,8 @@\n         self.value = value\n \n     def render_headers(self) -> bytes:\n-        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n-        return b\"\".join(\n-            [b'Content-Disposition: form-data; name=\"', name, b'\"\\r\\n' b\"\\r\\n\"]\n-        )\n+        name = _format_param(\"name\", self.name)\n+        return b\"\".join([b\"Content-Disposition: form-data; \", name, b\"\\r\\n\\r\\n\"])\n \n     def render_data(self) -> bytes:\n         return (\n@@ -55,20 +61,18 @@\n         return mimetypes.guess_type(self.filename)[0] or \"application/octet-stream\"\n \n     def render_headers(self) -> bytes:\n-        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n-        filename = quote(self.filename, encoding=\"utf-8\").encode(\"ascii\")\n-        content_type = self.content_type.encode(\"ascii\")\n+        name = _format_param(\"name\", self.name)\n+        filename = _format_param(\"filename\", self.filename)\n+        content_type = self.content_type.encode()\n         return b\"\".join(\n             [\n-                b'Content-Disposition: form-data; name=\"',\n+                b\"Content-Disposition: form-data; \",\n                 name,\n-                b'\"; filename=\"',\n+                b\"; \",\n                 filename,\n-                b'\"\\r\\n',\n-                b\"Content-Type: \",\n+                b\"\\r\\nContent-Type: \",\n                 content_type,\n-                b\"\\r\\n\",\n-                b\"\\r\\n\",\n+                b\"\\r\\n\\r\\n\",\n             ]\n         )\n \n@@ -104,3 +108,14 @@\n     content_type = \"multipart/form-data; boundary=%s\" % boundary.decode(\"ascii\")\n \n     return body.getvalue(), content_type\n+\n+\n+def _format_param(name: str, value: typing.Union[str, bytes]) -> bytes:\n+    if isinstance(value, bytes):\n+        value = value.decode()\n+        \n+    def replacer(match: typing.Match[str]) -> str:\n+        return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]\n+\n+    value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)\n+    return f'{name}=\"{value}\"'.encode()\n", "issue": "non-ASCII characters filename in multipart/form-data\nI tried to send a file with russian letters in the file name, and `httpx` escaped all non-ASCII characters (as I understood it the way specified in [RFC 7578](https://tools.ietf.org/html/rfc7578)). But this is different from `requests` [behavior](https://github.com/psf/requests/blob/589a82256759018a7e5e289302898dae32544949/requests/models.py#L110), where the file name is simply written as is (using [`RequestField.make_multipart`](https://github.com/urllib3/urllib3/blob/f0d9ebc41e51c4c4c9990b1eed02d297fd1b20d8/src/urllib3/fields.py#L248) from `urllib3`). Maybe `httpx` should just change [`FileField.render_headers`](https://github.com/encode/httpx/blob/3ba2e8c328f05b07ee06ae40f99dce7fe1e8292c/httpx/multipart.py#L57), so as not to escape the file name and instead just put it as bytes?\r\n\r\n```diff\r\ndef render_headers(self) -> bytes:\r\n-    name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\r\n-    filename = quote(self.filename, encoding=\"utf-8\").encode(\"ascii\")\r\n-    content_type = self.content_type.encode(\"ascii\")\r\n     return b\"\".join(\r\n         [\r\n             b'Content-Disposition: form-data; name=\"',\r\n-            name,\r\n+            self.name.encode(),\r\n             b'\"; filename=\"',\r\n-            filename,\r\n+            self.filename.encode(),\r\n             b'\"\\r\\n',\r\n             b\"Content-Type: \",\r\n-            content_type,\r\n+            self.content_type.encode(),\r\n             b\"\\r\\n\",\r\n        ]\r\n    )\r\n```\n", "before_files": [{"content": "import binascii\nimport mimetypes\nimport os\nimport typing\nfrom io import BytesIO\nfrom urllib.parse import quote\n\n\nclass Field:\n    def render_headers(self) -> bytes:\n        raise NotImplementedError()  # pragma: nocover\n\n    def render_data(self) -> bytes:\n        raise NotImplementedError()  # pragma: nocover\n\n\nclass DataField(Field):\n    def __init__(self, name: str, value: typing.Union[str, bytes]) -> None:\n        if not isinstance(name, str):\n            raise TypeError(\"Invalid type for name. Expected str.\")\n        if not isinstance(value, (str, bytes)):\n            raise TypeError(\"Invalid type for value. Expected str or bytes.\")\n        self.name = name\n        self.value = value\n\n    def render_headers(self) -> bytes:\n        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n        return b\"\".join(\n            [b'Content-Disposition: form-data; name=\"', name, b'\"\\r\\n' b\"\\r\\n\"]\n        )\n\n    def render_data(self) -> bytes:\n        return (\n            self.value if isinstance(self.value, bytes) else self.value.encode(\"utf-8\")\n        )\n\n\nclass FileField(Field):\n    def __init__(\n        self, name: str, value: typing.Union[typing.IO[typing.AnyStr], tuple]\n    ) -> None:\n        self.name = name\n        if not isinstance(value, tuple):\n            self.filename = os.path.basename(getattr(value, \"name\", \"upload\"))\n            self.file = value  # type: typing.Union[typing.IO[str], typing.IO[bytes]]\n            self.content_type = self.guess_content_type()\n        else:\n            self.filename = value[0]\n            self.file = value[1]\n            self.content_type = (\n                value[2] if len(value) > 2 else self.guess_content_type()\n            )\n\n    def guess_content_type(self) -> str:\n        return mimetypes.guess_type(self.filename)[0] or \"application/octet-stream\"\n\n    def render_headers(self) -> bytes:\n        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n        filename = quote(self.filename, encoding=\"utf-8\").encode(\"ascii\")\n        content_type = self.content_type.encode(\"ascii\")\n        return b\"\".join(\n            [\n                b'Content-Disposition: form-data; name=\"',\n                name,\n                b'\"; filename=\"',\n                filename,\n                b'\"\\r\\n',\n                b\"Content-Type: \",\n                content_type,\n                b\"\\r\\n\",\n                b\"\\r\\n\",\n            ]\n        )\n\n    def render_data(self) -> bytes:\n        content = self.file.read()\n        return content.encode(\"utf-8\") if isinstance(content, str) else content\n\n\ndef iter_fields(data: dict, files: dict) -> typing.Iterator[Field]:\n    for name, value in data.items():\n        if isinstance(value, (list, dict)):\n            for item in value:\n                yield DataField(name=name, value=item)\n        else:\n            yield DataField(name=name, value=value)\n\n    for name, value in files.items():\n        yield FileField(name=name, value=value)\n\n\ndef multipart_encode(data: dict, files: dict) -> typing.Tuple[bytes, str]:\n    body = BytesIO()\n    boundary = binascii.hexlify(os.urandom(16))\n\n    for field in iter_fields(data, files):\n        body.write(b\"--%s\\r\\n\" % boundary)\n        body.write(field.render_headers())\n        body.write(field.render_data())\n        body.write(b\"\\r\\n\")\n\n    body.write(b\"--%s--\\r\\n\" % boundary)\n\n    content_type = \"multipart/form-data; boundary=%s\" % boundary.decode(\"ascii\")\n\n    return body.getvalue(), content_type\n", "path": "httpx/multipart.py"}]}