fresco 3.5.0__py3-none-any.whl → 3.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -99,7 +99,7 @@ class TestRequestProperties(object):
99
99
  CONTENT_LENGTH="2",
100
100
  CONTENT_TYPE="text/plain; charset=UTF-8",
101
101
  ) as c:
102
- c.request.body_bytes == b"\xc3a"
102
+ assert c.request.body_bytes == b"\xc3a"
103
103
 
104
104
  def test_get_json_decodes_json(self):
105
105
  with context(
@@ -107,7 +107,7 @@ class TestRequestProperties(object):
107
107
  CONTENT_LENGTH="14",
108
108
  CONTENT_TYPE="application/json",
109
109
  ) as c:
110
- c.request.get_json() == {"foo": "bar"}
110
+ assert c.request.get_json() == {"foo": "bar"}
111
111
 
112
112
  def test_get_json_ignores_mime_type(self):
113
113
  with context(
@@ -115,7 +115,7 @@ class TestRequestProperties(object):
115
115
  CONTENT_LENGTH="14",
116
116
  CONTENT_TYPE="application/broken",
117
117
  ) as c:
118
- c.request.get_json() == {"foo": "bar"}
118
+ assert c.request.get_json() == {"foo": "bar"}
119
119
 
120
120
  def test_get_json_passes_args_to_decoder(self):
121
121
  with context(
@@ -123,19 +123,30 @@ class TestRequestProperties(object):
123
123
  CONTENT_LENGTH="10",
124
124
  CONTENT_TYPE="application/broken",
125
125
  ) as c:
126
- c.request.get_json(parse_int=lambda s: s + "!") == {"foo": "1!"}
126
+ assert c.request.get_json(parse_int=lambda s: s + "!") == {"foo": "1!"}
127
127
 
128
- def test_get_does_type_conversion(self):
128
+ def test_get_required_raises_badrequest(self):
129
129
  with context(
130
130
  QUERY_STRING="x=10",
131
131
  CONTENT_LENGTH="10",
132
132
  CONTENT_TYPE="application/broken",
133
133
  ) as c:
134
- assert c.request.get("x") == "10"
135
- assert c.request.get("x", type=int) == 10
134
+ req = c.request
135
+ assert req.get("x", required=True) == "10"
136
136
  with pytest.raises(exceptions.BadRequest):
137
- c.request.get("y", type=int)
138
- assert c.request.get("y", None, type=int) is None
137
+ req.get("y", required=True)
138
+
139
+ def test_get_does_type_conversion(self):
140
+ with context(
141
+ QUERY_STRING="x=10",
142
+ CONTENT_LENGTH="10",
143
+ CONTENT_TYPE="application/broken",
144
+ ) as c:
145
+ req = c.request
146
+ assert req.get("x") == "10"
147
+ assert req.get("x", type=int) == 10
148
+ assert req.get("y", type=int) is None
149
+ assert req.get("y", "20", type=int) == "20"
139
150
 
140
151
  def test_is_secure_returns_correct_value(self):
141
152
  with context("https://example.org/") as c:
@@ -150,7 +161,7 @@ class TestRequestProperties(object):
150
161
  def test_getint_raises_badrequest(self):
151
162
  with pytest.raises(exceptions.BadRequest):
152
163
  with context("http://example.org/") as c:
153
- c.request.getint("a")
164
+ c.request.getint("a", required=True)
154
165
 
155
166
  with pytest.raises(exceptions.BadRequest):
156
167
  with context("http://example.org/?a=four") as c:
@@ -47,6 +47,10 @@ from fresco.routing import (
47
47
  from . import fixtures
48
48
 
49
49
 
50
+ def dummyview() -> Response:
51
+ return Response()
52
+
53
+
50
54
  def assert_method_bound_to(method, ob):
51
55
  try:
52
56
  assert method.__self__ is ob
@@ -363,22 +367,22 @@ class TestRouteNames(object):
363
367
 
364
368
  class TestRouteCollection(object):
365
369
  def test_it_adds_routes_from_constructor(self):
366
- r1 = Route("/1", GET, lambda: None, name="1")
367
- r2 = Route("/2", POST, lambda: None, name="2")
370
+ r1 = Route("/1", GET, dummyview, name="1")
371
+ r2 = Route("/2", POST, dummyview, name="2")
368
372
  rc = RouteCollection([r1, r2])
369
373
  assert [r.name for r in rc] == ["1", "2"]
370
374
 
371
375
  def test_it_adds_routecollections_from_constructor(self):
372
- r1 = Route("/", GET, lambda: None, name="1")
373
- r2 = Route("/", POST, lambda: None, name="2")
374
- r3 = Route("/", POST, lambda: None, name="3")
376
+ r1 = Route("/", GET, dummyview, name="1")
377
+ r2 = Route("/", POST, dummyview, name="2")
378
+ r3 = Route("/", POST, dummyview, name="3")
375
379
  rc = RouteCollection([r1, RouteCollection([r2, r3])])
376
380
  assert [r.name for r in rc] == ["1", "2", "3"]
377
381
 
378
382
  def test_it_adds_dunderroutes_from_constructor(self):
379
- r1 = Route("/", GET, None, name="1")
380
- r2 = Route("/", POST, None, name="2")
381
- r3 = Route("/", POST, None, name="3")
383
+ r1 = Route("/", GET, dummyview, name="1")
384
+ r2 = Route("/", POST, dummyview, name="2")
385
+ r3 = Route("/", POST, dummyview, name="3")
382
386
 
383
387
  class A:
384
388
  __routes__ = [r2, r3]
@@ -387,8 +391,8 @@ class TestRouteCollection(object):
387
391
  assert [r.name for r in rc] == ["1", "2", "3"]
388
392
 
389
393
  def test_get_routes_matches_on_method(self):
390
- r_get = Route("/", GET, None)
391
- r_post = Route("/", POST, None)
394
+ r_get = Route("/", GET, dummyview)
395
+ r_post = Route("/", POST, dummyview)
392
396
 
393
397
  rc = RouteCollection([r_post, r_get])
394
398
 
@@ -396,8 +400,8 @@ class TestRouteCollection(object):
396
400
  assert [r.route for r in rc.get_route_traversals("/", POST)] == [r_post]
397
401
 
398
402
  def test_get_routes_matches_on_path(self):
399
- r1 = Route("/1", GET, None)
400
- r2 = Route("/2", GET, None)
403
+ r1 = Route("/1", GET, dummyview)
404
+ r2 = Route("/2", GET, dummyview)
401
405
 
402
406
  rc = RouteCollection([r1, r2])
403
407
 
@@ -405,8 +409,8 @@ class TestRouteCollection(object):
405
409
  assert [r.route for r in rc.get_route_traversals("/2", GET)] == [r2]
406
410
 
407
411
  def test_get_routes_can_match_all_methods(self):
408
- r1 = Route("/1", GET, None)
409
- r2 = Route("/1", POST, None)
412
+ r1 = Route("/1", GET, dummyview)
413
+ r2 = Route("/1", POST, dummyview)
410
414
 
411
415
  rc = RouteCollection([r1, r2])
412
416
  assert [r.route for r in rc.get_route_traversals("/1", None)] == [
@@ -418,8 +422,8 @@ class TestRouteCollection(object):
418
422
  a = RouteCollection()
419
423
  b = RouteCollection()
420
424
 
421
- a_route = Route("/harvey", GET, lambda: None)
422
- b_route = Route("/harvey", GET, lambda: None)
425
+ a_route = Route("/harvey", GET, dummyview)
426
+ b_route = Route("/harvey", GET, dummyview)
423
427
 
424
428
  a.add_route(a_route)
425
429
  b.add_route(b_route)
@@ -527,7 +531,9 @@ class TestRouteCollection(object):
527
531
  ]
528
532
 
529
533
  def test_add_prefix_returns_prefixed_collection(self):
530
- rc = RouteCollection([Route("/fish", GET, None), Route("/beans", GET, None)])
534
+ rc = RouteCollection(
535
+ [Route("/fish", GET, dummyview), Route("/beans", GET, dummyview)]
536
+ )
531
537
  prefixed = rc.add_prefix("/jelly")
532
538
  assert [str(r.pattern) for r in prefixed] == [
533
539
  "/jelly/fish",
@@ -630,15 +636,14 @@ class TestRouteCollection(object):
630
636
  assert list(response.content_iterator) == [b"ok"]
631
637
 
632
638
  def test_it_isolates_request_for_wsgi_app(self):
633
-
634
639
  params: t.Dict[str, t.Any] = {}
635
640
  outerapp = FrescoApp()
636
641
  innerapp = FrescoApp()
637
642
  innerapp.route("/y", GET=Response)
638
643
  outerapp.route_wsgi("/x", innerapp)
639
644
 
640
- innerapp.process_request(partial(params.__setitem__, 'inner_request'))
641
- outerapp.process_request(partial(params.__setitem__, 'outer_request'))
645
+ innerapp.process_request(partial(params.__setitem__, "inner_request"))
646
+ outerapp.process_request(partial(params.__setitem__, "outer_request"))
642
647
 
643
648
  with outerapp.requestcontext("/x/y"):
644
649
  outerapp.view()
@@ -913,25 +918,25 @@ class TestRRoute:
913
918
 
914
919
  class TestViewArgs(object):
915
920
  def test_it_uses_args(self):
916
- routes = RouteCollection([Route("/", GET, None, args=(1, 2))])
921
+ routes = RouteCollection([Route("/", GET, dummyview, args=(1, 2))])
917
922
  assert list(routes.get_route_traversals("/", GET)) == [
918
923
  tms.InstanceOf(RouteTraversal, args=(1, 2))
919
924
  ]
920
925
 
921
926
  def test_it_uses_view_args(self):
922
- routes = RouteCollection([Route("/", GET, None, view_args=(1, 2))])
927
+ routes = RouteCollection([Route("/", GET, dummyview, view_args=(1, 2))])
923
928
  assert list(routes.get_route_traversals("/", GET)) == [
924
929
  tms.InstanceOf(RouteTraversal, args=(1, 2))
925
930
  ]
926
931
 
927
932
  def test_it_appends_args_extracted_from_path(self):
928
- routes = RouteCollection([Route("/<:int>", GET, None, view_args=(1, 2))])
933
+ routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1, 2))])
929
934
  assert list(routes.get_route_traversals("/3", GET)) == [
930
935
  tms.InstanceOf(RouteTraversal, args=(1, 2, 3))
931
936
  ]
932
937
 
933
938
  def test_it_keeps_traversal_args_separate(self):
934
- routes = RouteCollection([Route("/<:int>", GET, None, view_args=(1,))])
939
+ routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1,))])
935
940
  assert list(routes.get_route_traversals("/2", GET)) == [
936
941
  tms.InstanceOf(
937
942
  RouteTraversal,
@@ -947,25 +952,27 @@ class TestViewArgs(object):
947
952
 
948
953
  class TestViewKwargs(object):
949
954
  def test_it_reads_from_route_kwargs(self):
950
- routes = RouteCollection([Route("/", GET, None, x=1)])
955
+ routes = RouteCollection([Route("/", GET, dummyview, x=1)])
951
956
  assert list(routes.get_route_traversals("/", GET)) == [
952
957
  tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
953
958
  ]
954
959
 
955
960
  def test_it_reads_from_kwargs(self):
956
- routes = RouteCollection([Route("/", GET, None, kwargs={"x": 1})])
961
+ routes = RouteCollection([Route("/", GET, dummyview, kwargs={"x": 1})])
957
962
  assert list(routes.get_route_traversals("/", GET)) == [
958
963
  tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
959
964
  ]
960
965
 
961
966
  def test_it_reads_from_view_kwargs(self):
962
- routes = RouteCollection([Route("/", GET, None, view_kwargs={"x": 1})])
967
+ routes = RouteCollection([Route("/", GET, dummyview, view_kwargs={"x": 1})])
963
968
  assert list(routes.get_route_traversals("/", GET)) == [
964
969
  tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
965
970
  ]
966
971
 
967
972
  def test_it_keeps_traversal_kwargs_separate(self):
968
- routes = RouteCollection([Route("/<x:int>", GET, None, view_kwargs={"y": 1})])
973
+ routes = RouteCollection(
974
+ [Route("/<x:int>", GET, dummyview, view_kwargs={"y": 1})]
975
+ )
969
976
  assert list(routes.get_route_traversals("/2", GET)) == [
970
977
  tms.InstanceOf(
971
978
  RouteTraversal,
@@ -1132,7 +1139,6 @@ class TestRouteCache(object):
1132
1139
 
1133
1140
 
1134
1141
  class TestRouteAll:
1135
-
1136
1142
  def test_route_all_matches_on_separator(self):
1137
1143
  def view():
1138
1144
  return Response()
@@ -1145,10 +1151,8 @@ class TestRouteAll:
1145
1151
  assert len(list(app.get_route_traversals("/xy", GET))) == 0
1146
1152
 
1147
1153
 
1148
- class TestRequestParamter:
1149
-
1154
+ class TestRequestParameter:
1150
1155
  def test_has_request_parameter(self):
1151
-
1152
1156
  def a(request: Request):
1153
1157
  pass
1154
1158
 
@@ -1175,7 +1179,6 @@ class TestRequestParamter:
1175
1179
  assert _has_request_parameter(f) is False
1176
1180
 
1177
1181
  def test_request_is_provided_automatically(self):
1178
-
1179
1182
  def a(request: Request):
1180
1183
  return Response("a")
1181
1184
 
@@ -42,7 +42,7 @@ class TestParseQueryString(object):
42
42
  return list(parse_querystring(value))
43
43
 
44
44
  def test_empty(self):
45
- self.p("") == []
45
+ assert self.p("") == []
46
46
 
47
47
  def test_simple_key_value(self):
48
48
  assert self.p("a=b") == [("a", "b")]
@@ -276,12 +276,10 @@ class TestParseFormEncodedData(object):
276
276
  class TestEncodeMultipart(object):
277
277
  def test_it_encodes_a_data_dict(self):
278
278
  data, headers = encode_multipart([("foo", "bar baf")])
279
- data = data.getvalue()
280
279
  assert b'Content-Disposition: form-data; name="foo"\r\n\r\nbar baf' in data
281
280
 
282
281
  def test_it_encodes_a_file_tuple(self):
283
282
  data, headers = encode_multipart(files=[("foo", "foo.txt", "ascii", "bar")])
284
- data = data.getvalue()
285
283
  expected = (
286
284
  b"Content-Disposition: form-data; "
287
285
  b'name="foo"; filename="foo.txt"\r\n'
@@ -15,6 +15,7 @@
15
15
  from fresco.core import FrescoApp
16
16
  from fresco.util.urls import normpath, make_query
17
17
  from fresco.util.urls import is_safe_url
18
+ from fresco.util.urls import add_query
18
19
 
19
20
  # Greek letters as unicode strings (require multi-byte representation in UTF-8)
20
21
  alpha = b"\xce\xb1".decode("utf8")
@@ -174,3 +175,22 @@ class TestSafeURL(object):
174
175
  ]
175
176
  for u in unsafe:
176
177
  assert is_safe_url(u, allowed_hosts={"good.example.org"}) is False
178
+
179
+
180
+ class TestAddQuery:
181
+
182
+ def test_it_adds_query(self):
183
+ url = add_query("http://localhost", {"foo": "bar"})
184
+ assert url == "http://localhost?foo=bar"
185
+
186
+ def test_it_adds_query_from_list(self):
187
+ url = add_query("http://localhost", [("foo", "bar")])
188
+ assert url == "http://localhost?foo=bar"
189
+
190
+ def test_it_adds_query_from_keywords(self):
191
+ url = add_query("http://localhost", foo="bar")
192
+ assert url == "http://localhost?foo=bar"
193
+
194
+ def test_it_appends_query(self):
195
+ url = add_query("http://localhost?foo=bar", {"foo": "baz"})
196
+ assert url == "http://localhost?foo=bar&foo=baz"
fresco/types.py CHANGED
@@ -1,6 +1,32 @@
1
+ from collections.abc import Mapping
2
+ from collections.abc import Iterable
3
+ from collections.abc import Callable
4
+ from types import TracebackType
5
+ from typing import Any
6
+ from typing import Optional
1
7
  import typing as t
2
8
 
9
+ import fresco.response # noqa
10
+
11
+
3
12
  QuerySpec = t.Union[
4
- t.Mapping[str, t.Any],
5
- t.Iterable[t.Tuple[str, t.Any]]
13
+ Mapping[str, Any],
14
+ Iterable[tuple[str, Any]]
15
+ ]
16
+ ViewCallable = Callable[..., "fresco.response.Response"]
17
+ ExcInfo = tuple[type[BaseException], BaseException, TracebackType]
18
+ OptionalExcInfo = Optional[ExcInfo]
19
+
20
+
21
+ HeaderList = list[tuple[str, str]]
22
+ HeadersList = HeaderList
23
+ WSGIEnviron = dict[str, Any]
24
+ WriteCallable = Callable[[bytes], object]
25
+ StartResponse = Callable[[str, HeaderList, OptionalExcInfo], WriteCallable]
26
+ WSGIApplication = Callable[
27
+ [
28
+ WSGIEnviron,
29
+ StartResponse,
30
+ ],
31
+ Iterable[bytes]
6
32
  ]
fresco/util/cache.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from typing import Any
2
+ from typing import Callable
2
3
  from typing import Dict
3
4
  from typing import List
4
5
  import sys
@@ -46,7 +47,7 @@ def cache_generator(original_function, maxsize):
46
47
  link[NEXT] = root
47
48
 
48
49
 
49
- def make_cache(original_function, maxsize=100):
50
+ def make_cache(original_function, maxsize=100) -> Callable[[Any], Any]:
50
51
  "Create a cache around a function that takes a single argument"
51
52
  c = cache_generator(original_function, maxsize)
52
53
  next(c)
fresco/util/http.py CHANGED
@@ -20,6 +20,8 @@ Utilities for working with data on the HTTP level
20
20
  from binascii import hexlify
21
21
  from collections import namedtuple
22
22
  from collections import deque
23
+ from collections.abc import Collection
24
+ from collections.abc import Mapping
23
25
  from email.header import Header
24
26
  from email.message import Message
25
27
  from email.parser import BytesFeedParser
@@ -43,6 +45,7 @@ from urllib.parse import unquote_plus
43
45
  from shutil import copyfileobj
44
46
 
45
47
  import fresco
48
+ from fresco.defaults import DEFAULT_CHARSET
46
49
  from fresco.exceptions import RequestParseError
47
50
  from fresco.util.io import io_iterator
48
51
  from fresco.util.io import ByteIterator
@@ -90,23 +93,23 @@ def get_content_type_info(
90
93
 
91
94
 
92
95
  class TooBig(RequestParseError):
93
- """\
96
+ """
94
97
  Request body is too big
95
98
  """
96
99
 
97
100
  def __init__(self, *args, **kwargs):
98
101
  super(TooBig, self).__init__(*args, **kwargs)
99
- self.response = fresco.response.Response.payload_too_large()
102
+ self.response = fresco.Response.payload_too_large()
100
103
 
101
104
 
102
105
  class MissingContentLength(RequestParseError):
103
- """\
106
+ """
104
107
  No ``Content-Length`` header given
105
108
  """
106
109
 
107
110
  def __init__(self, *args, **kwargs):
108
111
  super(MissingContentLength, self).__init__(*args, **kwargs)
109
- self.response = fresco.response.Response.length_required()
112
+ self.response = fresco.Response.length_required()
110
113
 
111
114
 
112
115
  def parse_parameters(s, preserve_backslashes=False) -> Dict[str, str]:
@@ -212,7 +215,7 @@ def parse_querystring(
212
215
 
213
216
  :param data: The query string to parse.
214
217
  :param charset: Character encoding used to decode values. If not specified,
215
- ``fresco.DEFAULT_CHARSET`` will be used.
218
+ ``fresco.defaults.DEFAULT_CHARSET`` will be used.
216
219
 
217
220
  :param keep_blank_values: if True, keys without associated values will be
218
221
  returned as empty strings. if False, no key,
@@ -223,7 +226,7 @@ def parse_querystring(
223
226
  """
224
227
 
225
228
  if charset is None:
226
- charset = fresco.DEFAULT_CHARSET
229
+ charset = DEFAULT_CHARSET
227
230
 
228
231
  result: List[Tuple[str, str]] = []
229
232
  append = result.append
@@ -275,7 +278,7 @@ def parse_post(
275
278
  ct, charset, ct_params = get_content_type_info(
276
279
  environ,
277
280
  "application/x-www-form-urlencoded",
278
- default_charset or fresco.DEFAULT_CHARSET,
281
+ default_charset or DEFAULT_CHARSET,
279
282
  )
280
283
 
281
284
  try:
@@ -454,14 +457,15 @@ def parse_multipart(
454
457
  f.close()
455
458
  raise
456
459
 
457
- close: Optional[Callable]
460
+ close: Optional[Callable] # type: ignore
458
461
  if open_files:
459
462
 
460
463
  def close():
461
464
  for f in open_files:
462
465
  f.close()
466
+
463
467
  else:
464
- close = None
468
+ close = None # type: ignore
465
469
 
466
470
  return fields, close
467
471
 
@@ -554,7 +558,6 @@ def read_until(
554
558
  return
555
559
 
556
560
  def remainder():
557
- nonlocal buf
558
561
  if buf:
559
562
  yield buf
560
563
  yield from stream
@@ -591,7 +594,21 @@ class FileUpload(object):
591
594
  copyfileobj(self.file, fileob)
592
595
 
593
596
 
594
- def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
597
+ def encode_multipart(
598
+ data: Optional[
599
+ Union[
600
+ Mapping[str, str],
601
+ Collection[tuple[str, str]],
602
+ ]
603
+ ] = None,
604
+ files: Optional[
605
+ Collection[
606
+ tuple[str, str, str, Union[bytes, t.Iterable[bytes], t.BinaryIO]]
607
+ ]
608
+ ] = None,
609
+ charset="UTF-8",
610
+ **kwargs
611
+ ) -> tuple[bytes, dict[str, str]]:
595
612
  """
596
613
  Encode ``data`` using multipart/form-data encoding, returning a tuple
597
614
  of ``(<encoded data>, <environ items>)``.
@@ -602,7 +619,7 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
602
619
  :param charset: Encoding used for any string values encountered in
603
620
  ``data``
604
621
 
605
- :param files: list of ``(name, filename, content_type, data)`` tuples.
622
+ :param files: collection of ``(name, filename, content_type, data)`` tuples.
606
623
  ``data`` may be either a byte string, iterator or
607
624
  file-like object.
608
625
 
@@ -614,45 +631,22 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
614
631
  dict.
615
632
  """
616
633
 
617
- def header_block(name):
618
- return [("Content-Disposition", 'form-data; name="%s"' % (name,))]
619
-
620
- def file_header_block(name, filename, content_type):
621
- return [
622
- (
623
- "Content-Disposition",
624
- 'form-data; name="%s"; filename="%s"' % (name, filename),
625
- ),
626
- ("Content-Type", content_type),
627
- ]
628
-
629
- def write_payload(stream, data):
630
- "Write ``data`` to ``stream``, encoding as required"
631
- if hasattr(data, "read"):
632
- copyfileobj(data, stream)
633
- elif isinstance(data, bytes):
634
- stream.write(data)
635
- elif isinstance(data, str):
636
- stream.write(data.encode(charset))
637
- else:
638
- raise ValueError(data)
639
-
640
- if data is None:
641
- data = {}
642
-
643
634
  if files is None:
644
635
  files = []
645
636
 
646
- try:
647
- data = data.items()
648
- except AttributeError:
649
- pass
637
+ data_items: Iterable[tuple[str, str]]
638
+ if data is None:
639
+ data_items = []
640
+ elif isinstance(data, Mapping):
641
+ data_items = iter(data.items()) # type: ignore
642
+ else:
643
+ data_items = data
650
644
 
651
- data = chain(data, kwargs.items())
645
+ data_items = chain(data_items, kwargs.items())
652
646
 
653
647
  boundary = b"-------" + hexlify(os.urandom(16))
654
648
  alldata = chain(
655
- ((header_block(k), payload) for k, payload in data),
649
+ ((header_block(k), payload) for k, payload in data_items),
656
650
  ((file_header_block(k, fn, ct), payload) for k, fn, ct, payload in files),
657
651
  )
658
652
 
@@ -664,7 +658,7 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
664
658
  for name, value in headers:
665
659
  post_data.write("{0}: {1}\r\n".format(name, value).encode("ascii"))
666
660
  post_data.write(CRLF)
667
- write_payload(post_data, payload)
661
+ write_payload(post_data, payload, charset)
668
662
  post_data.write(b"\r\n--" + boundary)
669
663
  post_data.write(b"--\r\n")
670
664
  length = post_data.tell()
@@ -676,4 +670,30 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
676
670
  ),
677
671
  }
678
672
 
679
- return (post_data, wsgienv)
673
+ return (post_data.getvalue(), wsgienv)
674
+
675
+
676
+ def header_block(name):
677
+ return [("Content-Disposition", 'form-data; name="%s"' % (name,))]
678
+
679
+
680
+ def file_header_block(name, filename, content_type):
681
+ return [
682
+ (
683
+ "Content-Disposition",
684
+ 'form-data; name="%s"; filename="%s"' % (name, filename),
685
+ ),
686
+ ("Content-Type", content_type),
687
+ ]
688
+
689
+
690
+ def write_payload(stream, data, charset):
691
+ "Write ``data`` to ``stream``, encoding as required"
692
+ if hasattr(data, "read"):
693
+ copyfileobj(data, stream)
694
+ elif isinstance(data, bytes):
695
+ stream.write(data)
696
+ elif isinstance(data, str):
697
+ stream.write(data.encode(charset))
698
+ else:
699
+ raise ValueError(data)