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.
- {microdot-2.7.0/src/microdot.egg-info → microdot-2.8.0}/PKG-INFO +2 -2
- {microdot-2.7.0 → microdot-2.8.0}/pyproject.toml +2 -2
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/__init__.py +1 -1
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/microdot.py +3 -2
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/microdot.pyi +12 -9
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/multipart.py +6 -4
- {microdot-2.7.0 → microdot-2.8.0/src/microdot.egg-info}/PKG-INFO +2 -2
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/SOURCES.txt +1 -0
- microdot-2.8.0/tests/.test_multipart.py.swp +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_multipart.py +69 -11
- {microdot-2.7.0 → microdot-2.8.0}/LICENSE +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/MANIFEST.in +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/README.md +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/Makefile +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/_static/css/custom.css +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/asgi.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/auth.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/cors.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/csrf.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/index.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/jinja.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/login.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/microdot.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/multipart.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/sessions.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/sse.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/test_client.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/utemplate.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/websocket.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/api/wsgi.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/conf.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/auth.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/cors.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/csrf.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/index.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/login.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/multipart.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/production.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/sessions.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/sse.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/templates.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/test_client.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/extensions/websocket.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/implementation/freezing.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/implementation/index.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/implementation/migrating.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/index.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/intro.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/make.bat +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/concurrency.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/defining-routes.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/index.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/intro.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/request-object.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/docs/users-guide/responses.rst +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/setup.cfg +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/__init__.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/asgi.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/asgi.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/auth.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/auth.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/cors.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/cors.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/csrf.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/csrf.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/helpers.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/helpers.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/jinja.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/jinja.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/login.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/login.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/multipart.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/py.typed +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/session.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/session.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/sse.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/sse.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/test_client.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/test_client.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/utemplate.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/utemplate.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/websocket.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/websocket.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/wsgi.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot/wsgi.pyi +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/requires.txt +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/__init__.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.bin +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.css +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.gif +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.gz +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.html +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.jpg +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.js +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.json +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.png +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.txt +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/files/test.txt.gz +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/mock_socket.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_asgi.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_auth.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_cors.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_csrf.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_end2end.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_jinja.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_login.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_microdot.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_multidict.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_request.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_response.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_session.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_sse.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_url_pattern.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_urlencode.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_utemplate.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_websocket.py +0 -0
- {microdot-2.7.0 → microdot-2.8.0}/tests/test_wsgi.py +0 -0
- {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.
|
|
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.
|
|
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
|
]
|
|
@@ -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
|
|
499
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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:
|
|
54
|
+
def __setitem__(self, key: str, value: _T) -> None:
|
|
52
55
|
...
|
|
53
56
|
|
|
54
|
-
def __getitem__(self, key: str) ->
|
|
57
|
+
def __getitem__(self, key: str) -> _T:
|
|
55
58
|
...
|
|
56
59
|
|
|
57
|
-
def get(self, key: str, default:
|
|
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[
|
|
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) ->
|
|
144
|
+
def form(self) -> MultiDict[str] | None:
|
|
142
145
|
...
|
|
143
146
|
|
|
144
147
|
@property
|
|
145
|
-
def files(self) ->
|
|
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
|
|
297
|
-
|
|
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.
|
|
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
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
72
|
-
for
|
|
73
|
-
d[
|
|
74
|
-
|
|
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 _,
|
|
140
|
-
|
|
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
|
|
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
|