microdot 2.7.0__tar.gz → 2.8.0__tar.gz

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.
Files changed (124) hide show
  1. {microdot-2.7.0/src/microdot.egg-info → microdot-2.8.0}/PKG-INFO +2 -2
  2. {microdot-2.7.0 → microdot-2.8.0}/pyproject.toml +2 -2
  3. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/__init__.py +1 -1
  4. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/microdot.py +3 -2
  5. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/microdot.pyi +12 -9
  6. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/multipart.py +6 -4
  7. {microdot-2.7.0 → microdot-2.8.0/src/microdot.egg-info}/PKG-INFO +2 -2
  8. {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/SOURCES.txt +1 -0
  9. microdot-2.8.0/tests/.test_multipart.py.swp +0 -0
  10. {microdot-2.7.0 → microdot-2.8.0}/tests/test_multipart.py +69 -11
  11. {microdot-2.7.0 → microdot-2.8.0}/LICENSE +0 -0
  12. {microdot-2.7.0 → microdot-2.8.0}/MANIFEST.in +0 -0
  13. {microdot-2.7.0 → microdot-2.8.0}/README.md +0 -0
  14. {microdot-2.7.0 → microdot-2.8.0}/docs/Makefile +0 -0
  15. {microdot-2.7.0 → microdot-2.8.0}/docs/_static/css/custom.css +0 -0
  16. {microdot-2.7.0 → microdot-2.8.0}/docs/api/asgi.rst +0 -0
  17. {microdot-2.7.0 → microdot-2.8.0}/docs/api/auth.rst +0 -0
  18. {microdot-2.7.0 → microdot-2.8.0}/docs/api/cors.rst +0 -0
  19. {microdot-2.7.0 → microdot-2.8.0}/docs/api/csrf.rst +0 -0
  20. {microdot-2.7.0 → microdot-2.8.0}/docs/api/index.rst +0 -0
  21. {microdot-2.7.0 → microdot-2.8.0}/docs/api/jinja.rst +0 -0
  22. {microdot-2.7.0 → microdot-2.8.0}/docs/api/login.rst +0 -0
  23. {microdot-2.7.0 → microdot-2.8.0}/docs/api/microdot.rst +0 -0
  24. {microdot-2.7.0 → microdot-2.8.0}/docs/api/multipart.rst +0 -0
  25. {microdot-2.7.0 → microdot-2.8.0}/docs/api/sessions.rst +0 -0
  26. {microdot-2.7.0 → microdot-2.8.0}/docs/api/sse.rst +0 -0
  27. {microdot-2.7.0 → microdot-2.8.0}/docs/api/test_client.rst +0 -0
  28. {microdot-2.7.0 → microdot-2.8.0}/docs/api/utemplate.rst +0 -0
  29. {microdot-2.7.0 → microdot-2.8.0}/docs/api/websocket.rst +0 -0
  30. {microdot-2.7.0 → microdot-2.8.0}/docs/api/wsgi.rst +0 -0
  31. {microdot-2.7.0 → microdot-2.8.0}/docs/conf.py +0 -0
  32. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/auth.rst +0 -0
  33. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/cors.rst +0 -0
  34. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/csrf.rst +0 -0
  35. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/index.rst +0 -0
  36. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/login.rst +0 -0
  37. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/multipart.rst +0 -0
  38. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/production.rst +0 -0
  39. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/sessions.rst +0 -0
  40. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/sse.rst +0 -0
  41. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/templates.rst +0 -0
  42. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/test_client.rst +0 -0
  43. {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/websocket.rst +0 -0
  44. {microdot-2.7.0 → microdot-2.8.0}/docs/implementation/freezing.rst +0 -0
  45. {microdot-2.7.0 → microdot-2.8.0}/docs/implementation/index.rst +0 -0
  46. {microdot-2.7.0 → microdot-2.8.0}/docs/implementation/migrating.rst +0 -0
  47. {microdot-2.7.0 → microdot-2.8.0}/docs/index.rst +0 -0
  48. {microdot-2.7.0 → microdot-2.8.0}/docs/intro.rst +0 -0
  49. {microdot-2.7.0 → microdot-2.8.0}/docs/make.bat +0 -0
  50. {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/concurrency.rst +0 -0
  51. {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/defining-routes.rst +0 -0
  52. {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/index.rst +0 -0
  53. {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/intro.rst +0 -0
  54. {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/request-object.rst +0 -0
  55. {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/responses.rst +0 -0
  56. {microdot-2.7.0 → microdot-2.8.0}/setup.cfg +0 -0
  57. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/__init__.pyi +0 -0
  58. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/asgi.py +0 -0
  59. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/asgi.pyi +0 -0
  60. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/auth.py +0 -0
  61. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/auth.pyi +0 -0
  62. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/cors.py +0 -0
  63. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/cors.pyi +0 -0
  64. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/csrf.py +0 -0
  65. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/csrf.pyi +0 -0
  66. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/helpers.py +0 -0
  67. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/helpers.pyi +0 -0
  68. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/jinja.py +0 -0
  69. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/jinja.pyi +0 -0
  70. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/login.py +0 -0
  71. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/login.pyi +0 -0
  72. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/multipart.pyi +0 -0
  73. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/py.typed +0 -0
  74. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/session.py +0 -0
  75. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/session.pyi +0 -0
  76. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/sse.py +0 -0
  77. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/sse.pyi +0 -0
  78. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/test_client.py +0 -0
  79. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/test_client.pyi +0 -0
  80. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/utemplate.py +0 -0
  81. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/utemplate.pyi +0 -0
  82. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/websocket.py +0 -0
  83. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/websocket.pyi +0 -0
  84. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/wsgi.py +0 -0
  85. {microdot-2.7.0 → microdot-2.8.0}/src/microdot/wsgi.pyi +0 -0
  86. {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/dependency_links.txt +0 -0
  87. {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/not-zip-safe +0 -0
  88. {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/requires.txt +0 -0
  89. {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/top_level.txt +0 -0
  90. {microdot-2.7.0 → microdot-2.8.0}/tests/__init__.py +0 -0
  91. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.bin +0 -0
  92. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.css +0 -0
  93. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.gif +0 -0
  94. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.gz +0 -0
  95. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.html +0 -0
  96. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.jpg +0 -0
  97. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.js +0 -0
  98. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.json +0 -0
  99. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.png +0 -0
  100. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.txt +0 -0
  101. {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.txt.gz +0 -0
  102. {microdot-2.7.0 → microdot-2.8.0}/tests/mock_socket.py +0 -0
  103. {microdot-2.7.0 → microdot-2.8.0}/tests/templates/hello.jinja.txt +0 -0
  104. {microdot-2.7.0 → microdot-2.8.0}/tests/templates/hello.utemplate.txt +0 -0
  105. {microdot-2.7.0 → microdot-2.8.0}/tests/templates/hello_utemplate_txt.py +0 -0
  106. {microdot-2.7.0 → microdot-2.8.0}/tests/test_asgi.py +0 -0
  107. {microdot-2.7.0 → microdot-2.8.0}/tests/test_auth.py +0 -0
  108. {microdot-2.7.0 → microdot-2.8.0}/tests/test_cors.py +0 -0
  109. {microdot-2.7.0 → microdot-2.8.0}/tests/test_csrf.py +0 -0
  110. {microdot-2.7.0 → microdot-2.8.0}/tests/test_end2end.py +0 -0
  111. {microdot-2.7.0 → microdot-2.8.0}/tests/test_jinja.py +0 -0
  112. {microdot-2.7.0 → microdot-2.8.0}/tests/test_login.py +0 -0
  113. {microdot-2.7.0 → microdot-2.8.0}/tests/test_microdot.py +0 -0
  114. {microdot-2.7.0 → microdot-2.8.0}/tests/test_multidict.py +0 -0
  115. {microdot-2.7.0 → microdot-2.8.0}/tests/test_request.py +0 -0
  116. {microdot-2.7.0 → microdot-2.8.0}/tests/test_response.py +0 -0
  117. {microdot-2.7.0 → microdot-2.8.0}/tests/test_session.py +0 -0
  118. {microdot-2.7.0 → microdot-2.8.0}/tests/test_sse.py +0 -0
  119. {microdot-2.7.0 → microdot-2.8.0}/tests/test_url_pattern.py +0 -0
  120. {microdot-2.7.0 → microdot-2.8.0}/tests/test_urlencode.py +0 -0
  121. {microdot-2.7.0 → microdot-2.8.0}/tests/test_utemplate.py +0 -0
  122. {microdot-2.7.0 → microdot-2.8.0}/tests/test_websocket.py +0 -0
  123. {microdot-2.7.0 → microdot-2.8.0}/tests/test_wsgi.py +0 -0
  124. {microdot-2.7.0 → microdot-2.8.0}/tox.ini +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: The impossibly small web framework for MicroPython
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
+ License-Expression: MIT
6
7
  Project-URL: Homepage, https://code.miguelgrinberg.com/miguelgrinberg/microdot
7
8
  Project-URL: Documentation, https://microdot.readthedocs.io/
8
9
  Project-URL: Source Code, https://code.miguelgrinberg.com/miguelgrinberg/microdot
@@ -13,7 +14,6 @@ Classifier: Environment :: Web Environment
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: Programming Language :: Python :: 3
15
16
  Classifier: Programming Language :: Python :: Implementation :: MicroPython
16
- Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Requires-Python: >=3.8
19
19
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "microdot"
3
- version = "2.7.0"
3
+ version = "2.8.0"
4
4
  authors = [
5
5
  { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
6
6
  ]
@@ -10,9 +10,9 @@ classifiers = [
10
10
  "Intended Audience :: Developers",
11
11
  "Programming Language :: Python :: 3",
12
12
  "Programming Language :: Python :: Implementation :: MicroPython",
13
- "License :: OSI Approved :: MIT License",
14
13
  "Operating System :: OS Independent",
15
14
  ]
15
+ license = "MIT"
16
16
  requires-python = ">=3.8"
17
17
  dependencies = [
18
18
  ]
@@ -1,4 +1,4 @@
1
1
  from microdot.microdot import Microdot, Request, Response, abort, redirect, \
2
2
  send_file, URLPattern, AsyncBytesIO, iscoroutine # noqa: F401
3
3
 
4
- __version__ = '2.7.0'
4
+ __version__ = '2.8.0'
@@ -495,8 +495,9 @@ class Request:
495
495
 
496
496
  @property
497
497
  def files(self):
498
- """The files uploaded in the request as a dictionary, or ``None`` if
499
- the request does not have any files.
498
+ """The files uploaded in the request as a
499
+ :class:`MultiDict <microdot.MultiDict>` object, or ``None`` if the
500
+ request does not have any files.
500
501
 
501
502
  The :func:`with_form_data <microdot.multipart.with_form_data>`
502
503
  decorator must be added to the route that receives file uploads for
@@ -1,4 +1,4 @@
1
- from typing import Any, Awaitable, BinaryIO, Callable, Iterable, Tuple
1
+ from typing import Any, Awaitable, BinaryIO, Callable, Iterable, Tuple, TypeVar
2
2
  from asyncio import StreamReader, StreamWriter, Server
3
3
  from datetime import datetime
4
4
  from io import BytesIO
@@ -44,20 +44,23 @@ class NoCaseDict(dict):
44
44
  def mro(cls):
45
45
  ...
46
46
 
47
- class MultiDict(dict):
48
- def __init__(self, initial_dict: dict[str, Any] | None = ...) -> None:
47
+ _T = TypeVar('_T')
48
+
49
+
50
+ class MultiDict[_T](dict[str, _T]):
51
+ def __init__(self, initial_dict: dict[str, _T] | None = ...) -> None:
49
52
  ...
50
53
 
51
- def __setitem__(self, key: str, value: Any) -> None:
54
+ def __setitem__(self, key: str, value: _T) -> None:
52
55
  ...
53
56
 
54
- def __getitem__(self, key: str) -> Any:
57
+ def __getitem__(self, key: str) -> _T:
55
58
  ...
56
59
 
57
- def get(self, key: str, default: Any | None = ..., type: type | None = ...):
60
+ def get(self, key: str, default: _T | None = ..., type: type | None = ...) -> _T | None: # type: ignore[override]
58
61
  ...
59
62
 
60
- def getlist(self, key: str, type: type | None = ...) -> list[Any]:
63
+ def getlist(self, key: str, type: type | None = ...) -> list[_T]:
61
64
  ...
62
65
 
63
66
 
@@ -138,11 +141,11 @@ class Request:
138
141
  ...
139
142
 
140
143
  @property
141
- def form(self) -> dict[str, str] | None:
144
+ def form(self) -> MultiDict[str] | None:
142
145
  ...
143
146
 
144
147
  @property
145
- def files(self) -> dict[str, FileUpload]:
148
+ def files(self) -> MultiDict[FileUpload] | None:
146
149
  ...
147
150
 
148
151
  def after_request(self, f: Callable[[Request, "Response"], "Response" | Awaitable["Response"] | None | Awaitable[None]]):
@@ -2,6 +2,7 @@ import os
2
2
  from random import choice
3
3
  from microdot import abort, iscoroutine, AsyncBytesIO
4
4
  from microdot.helpers import wraps
5
+ from microdot.microdot import MultiDict
5
6
 
6
7
 
7
8
  class FormDataIter:
@@ -277,8 +278,8 @@ def with_form_data(f):
277
278
  """
278
279
  @wraps(f)
279
280
  async def wrapper(request, *args, **kwargs):
280
- form = {}
281
- files = {}
281
+ form = MultiDict()
282
+ files = MultiDict()
282
283
  async for name, value in FormDataIter(request):
283
284
  if isinstance(value, FileUpload):
284
285
  files[name] = await value.copy()
@@ -293,7 +294,8 @@ def with_form_data(f):
293
294
  ret = await ret
294
295
  finally:
295
296
  if request.files:
296
- for file in request.files.values():
297
- await file.close()
297
+ for files in request.files.values():
298
+ for file in files:
299
+ await file.close()
298
300
  return ret
299
301
  return wrapper
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdot
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: The impossibly small web framework for MicroPython
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
+ License-Expression: MIT
6
7
  Project-URL: Homepage, https://code.miguelgrinberg.com/miguelgrinberg/microdot
7
8
  Project-URL: Documentation, https://microdot.readthedocs.io/
8
9
  Project-URL: Source Code, https://code.miguelgrinberg.com/miguelgrinberg/microdot
@@ -13,7 +14,6 @@ Classifier: Environment :: Web Environment
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: Programming Language :: Python :: 3
15
16
  Classifier: Programming Language :: Python :: Implementation :: MicroPython
16
- Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Requires-Python: >=3.8
19
19
  Description-Content-Type: text/markdown
@@ -84,6 +84,7 @@ src/microdot.egg-info/dependency_links.txt
84
84
  src/microdot.egg-info/not-zip-safe
85
85
  src/microdot.egg-info/requires.txt
86
86
  src/microdot.egg-info/top_level.txt
87
+ tests/.test_multipart.py.swp
87
88
  tests/__init__.py
88
89
  tests/mock_socket.py
89
90
  tests/test_asgi.py
@@ -22,12 +22,18 @@ class TestMultipart(unittest.TestCase):
22
22
  @app.post('/sync')
23
23
  @with_form_data
24
24
  def sync_route(req):
25
- return dict(req.form)
25
+ d = {}
26
+ for key, value in req.form.items():
27
+ d[key] = value
28
+ return d
26
29
 
27
30
  @app.post('/async')
28
31
  @with_form_data
29
32
  async def async_route(req):
30
- return dict(req.form)
33
+ d = {}
34
+ for key, value in req.form.items():
35
+ d[key] = value
36
+ return d
31
37
 
32
38
  client = TestClient(app)
33
39
 
@@ -43,7 +49,7 @@ class TestMultipart(unittest.TestCase):
43
49
  b'--boundary--\r\n')
44
50
  ))
45
51
  self.assertEqual(res.status_code, 200)
46
- self.assertEqual(res.json, {'foo': 'bar', 'baz': 'baz'})
52
+ self.assertEqual(res.json, {'foo': ['bar'], 'baz': ['baz']})
47
53
 
48
54
  res = self._run(client.post(
49
55
  '/async', headers={
@@ -57,7 +63,7 @@ class TestMultipart(unittest.TestCase):
57
63
  b'--boundary--\r\n')
58
64
  ))
59
65
  self.assertEqual(res.status_code, 200)
60
- self.assertEqual(res.json, {'foo': 'bar', 'baz': 'baz'})
66
+ self.assertEqual(res.json, {'foo': ['bar'], 'baz': ['baz']})
61
67
 
62
68
  def test_form_with_files(self):
63
69
  saved_max_memory_size = FileUpload.max_memory_size
@@ -68,10 +74,13 @@ class TestMultipart(unittest.TestCase):
68
74
  @app.post('/async')
69
75
  @with_form_data
70
76
  async def async_route(req):
71
- d = dict(req.form)
72
- for name, file in req.files.items():
73
- d[name] = '{}|{}|{}'.format(file.filename, file.content_type,
74
- (await file.read()).decode())
77
+ d = {}
78
+ for key, value in req.form.items():
79
+ d[key] = value
80
+ for name, files in req.files.items():
81
+ d[name] = '{}|{}|{}'.format(files[0].filename,
82
+ files[0].content_type,
83
+ (await files[0].read()).decode())
75
84
  return d
76
85
 
77
86
  client = TestClient(app)
@@ -94,7 +103,7 @@ class TestMultipart(unittest.TestCase):
94
103
  b'--boundary--\r\n')
95
104
  ))
96
105
  self.assertEqual(res.status_code, 200)
97
- self.assertEqual(res.json, {'foo': 'bar', 'x': 'y',
106
+ self.assertEqual(res.json, {'foo': ['bar'], 'x': ['y'],
98
107
  'f': 'f|text/plain|baz',
99
108
  'g': 'g|text/html|<p>hello</p>'})
100
109
  FileUpload.max_memory_size = saved_max_memory_size
@@ -136,8 +145,9 @@ class TestMultipart(unittest.TestCase):
136
145
  @app.post('/async')
137
146
  @with_form_data
138
147
  async def async_route(req):
139
- for _, file in req.files.items():
140
- await file.save('_x.txt')
148
+ for _, files in req.files.items():
149
+ for file in files:
150
+ await file.save('_x.txt')
141
151
 
142
152
  client = TestClient(app)
143
153
 
@@ -235,3 +245,51 @@ class TestMultipart(unittest.TestCase):
235
245
  'h': 'hh|text/plain|yyzz',
236
246
  'i': 'i.1|text/plain|1234',
237
247
  })
248
+
249
+ def test_form_with_duplicates(self):
250
+ app = Microdot()
251
+
252
+ @app.post('/submit')
253
+ @with_form_data
254
+ async def submit(req):
255
+ d = {}
256
+ for key, value in req.form.items():
257
+ d[key] = value
258
+ for name, files in req.files.items():
259
+ if name not in d:
260
+ d[name] = []
261
+ for file in files:
262
+ d[name].append('{}|{}|{}'.format(
263
+ file.filename, file.content_type,
264
+ (await file.read()).decode()))
265
+ return d
266
+
267
+ client = TestClient(app)
268
+
269
+ res = self._run(client.post(
270
+ '/submit', headers={
271
+ 'Content-Type': 'multipart/form-data; boundary=boundary',
272
+ },
273
+ body=(
274
+ b'--boundary\r\n'
275
+ b'Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n'
276
+ b'--boundary\r\n'
277
+ b'Content-Disposition: form-data; name="foo"\r\n\r\nbaz\r\n'
278
+ b'--boundary\r\n'
279
+ b'Content-Disposition: form-data; name="f"; filename="f1"\r\n'
280
+ b'Content-Type: text/plain\r\n\r\nfile1\r\n'
281
+ b'--boundary\r\n'
282
+ b'Content-Disposition: form-data; name="f"; filename="f2"\r\n'
283
+ b'Content-Type: text/plain\r\n\r\nfile2\r\n'
284
+ b'--boundary\r\n'
285
+ b'Content-Disposition: form-data; name="g"; filename="g"\r\n'
286
+ b'Content-Type: text/html\r\n\r\n<p>hello</p>\r\n'
287
+ b'--boundary\r\n'
288
+ b'Content-Disposition: form-data; name="x"\r\n\r\ny\r\n'
289
+ b'--boundary--\r\n')
290
+ ))
291
+ self.assertEqual(res.status_code, 200)
292
+ self.assertEqual(res.json, {'foo': ['bar', 'baz'], 'x': ['y'],
293
+ 'f': ['f1|text/plain|file1',
294
+ 'f2|text/plain|file2'],
295
+ 'g': ['g|text/html|<p>hello</p>']})
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes