13 Commits

Author SHA1 Message Date
Miguel Grinberg
5061145f5c Release 0.6.0 2021-08-11 10:36:42 +01:00
Miguel Grinberg
122c638bae Fix codecov badge link #nolog 2021-08-11 10:33:10 +01:00
Miguel Grinberg
bd74bcab74 Accept a custom reason phrase for the HTTP response (Fixes #25) 2021-08-11 10:29:08 +01:00
Miguel Grinberg
5cd3ace516 More unit tests 2021-08-02 15:53:13 +01:00
Miguel Grinberg
da32f23e35 Better handling of content types in form and json methods (Fixes #24) 2021-08-02 15:39:32 +01:00
Mark Blakeney
0641466faa Copy client headers to avoid write back (#23) 2021-07-28 10:43:54 +01:00
Miguel Grinberg
dd3fc20507 Make mime type check for form submissions more robust 2021-06-06 20:05:32 +01:00
Miguel Grinberg
46963ba464 Work around a bug in uasyncio's create_server() function 2021-06-06 20:05:12 +01:00
Miguel Grinberg
1a8db51cb3 Installation instructions 2021-06-06 12:24:09 +01:00
Miguel Grinberg
d903c42370 Minor wording update in the documentation #nolog 2021-06-06 12:17:22 +01:00
Miguel Grinberg
8b4ebbd953 Run tests with pytest 2021-06-06 12:09:03 +01:00
Miguel Grinberg
a82ed55f56 Last version of the microdot-asyncio package 2021-06-06 11:54:51 +01:00
Miguel Grinberg
ac87f0542f Version 0.5.1.dev0 2021-06-06 11:49:01 +01:00
16 changed files with 128 additions and 30 deletions

View File

@@ -2,10 +2,10 @@ name: build
on: on:
push: push:
branches: branches:
- master - main
pull_request: pull_request:
branches: branches:
- master - main
jobs: jobs:
lint: lint:
name: lint name: lint

View File

@@ -1,5 +1,17 @@
# Microdot change log # Microdot change log
**Release 0.6.0** - 2021-08-11
- Better handling of content types in form and json methods [#24](https://github.com/miguelgrinberg/microdot/issues/24) ([commit](https://github.com/miguelgrinberg/microdot/commit/da32f23e35f871470a40638e7000e84b0ff6d17f))
- Accept a custom reason phrase for the HTTP response [#25](https://github.com/miguelgrinberg/microdot/issues/25) ([commit](https://github.com/miguelgrinberg/microdot/commit/bd74bcab74f283c89aadffc8f9c20d6ff0f771ce))
- Make mime type check for form submissions more robust ([commit](https://github.com/miguelgrinberg/microdot/commit/dd3fc20507715a23d0fa6fa3aae3715c8fbc0351))
- Copy client headers to avoid write back [#23](https://github.com/miguelgrinberg/microdot/issues/23) ([commit](https://github.com/miguelgrinberg/microdot/commit/0641466faa9dda0c54f78939ac05993c0812e84a)) (thanks **Mark Blakeney**!)
- Work around a bug in uasyncio's create_server() function ([commit](https://github.com/miguelgrinberg/microdot/commit/46963ba4644d7abc8dc653c99bc76222af526964))
- More unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/5cd3ace5166ec549579b0b1149ae3d7be195974a))
- Installation instructions ([commit](https://github.com/miguelgrinberg/microdot/commit/1a8db51cb3754308da6dcc227512dcdeb4ce4557))
- Run tests with pytest ([commit](https://github.com/miguelgrinberg/microdot/commit/8b4ebbd9535b3c083fb2a955284609acba07f05e))
- Deprecated the microdot-asyncio package ([commit](https://github.com/miguelgrinberg/microdot/commit/a82ed55f56e14fbcea93e8171af86ab42657fa96))
**Release 0.5.0** - 2021-06-06 **Release 0.5.0** - 2021-06-06
- [Documentation](https://microdot.readthedocs.io/en/latest/) site ([commit](https://github.com/miguelgrinberg/microdot/commit/12cd60305b7b48ab151da52661fc5988684dbcd8)) - [Documentation](https://microdot.readthedocs.io/en/latest/) site ([commit](https://github.com/miguelgrinberg/microdot/commit/12cd60305b7b48ab151da52661fc5988684dbcd8))

View File

@@ -1,5 +1,5 @@
# microdot # microdot
[![Build status](https://github.com/miguelgrinberg/microdot/workflows/build/badge.svg)](https://github.com/miguelgrinberg/microdot/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/microdot/branch/master/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/microdot) [![Build status](https://github.com/miguelgrinberg/microdot/workflows/build/badge.svg)](https://github.com/miguelgrinberg/microdot/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/microdot/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/microdot)
A minimalistic Python web framework for microcontrollers inspired by Flask A minimalistic Python web framework for microcontrollers inspired by Flask

View File

@@ -12,8 +12,7 @@
# #
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('../microdot')) sys.path.insert(0, os.path.abspath('../src'))
sys.path.insert(1, os.path.abspath('../microdot-asyncio'))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------

View File

@@ -6,8 +6,8 @@
Microdot Microdot
======== ========
Microdot is a minimalistic Python web framework for microcontrollers inspired Microdot is a minimalistic Python web framework inspired by
by `Flask <https://flask.palletsprojects.com/>`_, and designed to run on `Flask <https://flask.palletsprojects.com/>`_, and designed to run on
systems with limited resources such as microcontrollers. It runs on standard systems with limited resources such as microcontrollers. It runs on standard
Python and on `MicroPython <https://micropython.org>`_. Python and on `MicroPython <https://micropython.org>`_.

View File

@@ -1,3 +1,13 @@
Installation
------------
Microdot can be installed with ``pip``::
pip install microdot
For platforms that do not support or cannot run ``pip``, you can also manually
copy and install the ``microdot.py`` and ``microdot_asyncio.py`` source files.
Examples Examples
-------- --------

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = microdot-asyncio name = microdot-asyncio
version = 0.4.0 version = 0.5.0
author = Miguel Grinberg author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com author_email = miguel.grinberg@gmail.com
description = AsyncIO support for the Microdot web framework' description = AsyncIO support for the Microdot web framework'

View File

@@ -1,7 +1,6 @@
import sys import sys
sys.path.insert(0, 'microdot') sys.path.insert(0, 'src')
sys.path.insert(1, 'microdot-asyncio')
sys.path.insert(2, 'tests/libs') sys.path.insert(2, 'tests/libs')
import unittest import unittest

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = microdot name = microdot
version = 0.5.0 version = 0.6.0
author = Miguel Grinberg author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython description = The impossibly small web framework for MicroPython

View File

@@ -235,7 +235,7 @@ class Request():
""" """
# request line # request line
line = client_stream.readline().strip().decode() line = client_stream.readline().strip().decode()
if not line: # pragma: no cover if not line:
return None return None
method, url, http_version = line.split() method, url, http_version = line.split()
http_version = http_version.split('/', 1)[1] http_version = http_version.split('/', 1)[1]
@@ -267,17 +267,23 @@ class Request():
@property @property
def json(self): def json(self):
if self.content_type != 'application/json':
return None
if self._json is None: if self._json is None:
if self.content_type is None:
return None
mime_type = self.content_type.split(';')[0]
if mime_type != 'application/json':
return None
self._json = json.loads(self.body.decode()) self._json = json.loads(self.body.decode())
return self._json return self._json
@property @property
def form(self): def form(self):
if self.content_type != 'application/x-www-form-urlencoded':
return None
if self._form is None: if self._form is None:
if self.content_type is None:
return None
mime_type = self.content_type.split(';')[0]
if mime_type != 'application/x-www-form-urlencoded':
return None
self._form = self._parse_urlencoded(self.body.decode()) self._form = self._parse_urlencoded(self.body.decode())
return self._form return self._form
@@ -303,9 +309,10 @@ class Response():
} }
send_file_buffer_size = 1024 send_file_buffer_size = 1024
def __init__(self, body='', status_code=200, headers=None): def __init__(self, body='', status_code=200, headers=None, reason=None):
self.status_code = status_code self.status_code = status_code
self.headers = headers or {} self.headers = headers.copy() if headers else {}
self.reason = reason
if isinstance(body, (dict, list)): if isinstance(body, (dict, list)):
self.body = json.dumps(body).encode() self.body = json.dumps(body).encode()
self.headers['Content-Type'] = 'application/json' self.headers['Content-Type'] = 'application/json'
@@ -358,9 +365,10 @@ class Response():
self.complete() self.complete()
# status code # status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format( stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, status_code=self.status_code, reason=reason).encode())
reason='OK' if self.status_code == 200 else 'N/A').encode())
# headers # headers
for header, value in self.headers.items(): for header, value in self.headers.items():

View File

@@ -74,9 +74,10 @@ class Response(BaseResponse):
self.complete() self.complete()
# status code # status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format( await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, status_code=self.status_code, reason=reason).encode())
reason='OK' if self.status_code == 200 else 'N/A').encode())
# headers # headers
for header, value in self.headers.items(): for header, value in self.headers.items():
@@ -95,7 +96,7 @@ class Response(BaseResponse):
await stream.awrite(buf) await stream.awrite(buf)
if len(buf) < self.send_file_buffer_size: if len(buf) < self.send_file_buffer_size:
break break
if hasattr(self.body, 'close'): if hasattr(self.body, 'close'): # pragma: no cover
self.body.close() self.body.close()
else: else:
await stream.awrite(self.body) await stream.awrite(self.body)
@@ -162,7 +163,14 @@ class Microdot(BaseMicrodot):
host=host, port=port)) host=host, port=port))
self.server = await asyncio.start_server(serve, host, port) self.server = await asyncio.start_server(serve, host, port)
await self.server.wait_closed() while True:
try:
await self.server.wait_closed()
break
except AttributeError: # pragma: no cover
# the task hasn't been initialized in the server object yet
# wait a bit and try again
await asyncio.sleep(0.1)
def run(self, host='0.0.0.0', port=5000, debug=False): def run(self, host='0.0.0.0', port=5000, debug=False):
"""Start the web server. This function does not normally return, as """Start the web server. This function does not normally return, as

View File

@@ -65,6 +65,16 @@ class TestMicrodot(unittest.TestCase):
self.assertIn(b'Content-Type: text/plain\r\n', fd.response) self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar')) self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
def test_empty_request(self):
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
assert fd.response == b''
def test_method_decorators(self): def test_method_decorators(self):
app = Microdot() app = Microdot()

View File

@@ -112,6 +112,28 @@ class TestResponse(unittest.TestCase):
self.assertEqual(res.headers, {'X-Test': 'Foo'}) self.assertEqual(res.headers, {'X-Test': 'Foo'})
self.assertEqual(res.body, b'foo') self.assertEqual(res.body, b'foo')
def test_create_with_reason(self):
res = Response('foo', reason='ALL GOOD!')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {})
self.assertEqual(res.reason, 'ALL GOOD!')
self.assertEqual(res.body, b'foo')
fd = io.BytesIO()
res.write(fd)
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 200 ALL GOOD!\r\n', response)
def test_create_with_status_and_reason(self):
res = Response('not found', 404, reason='NOT FOUND')
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers, {})
self.assertEqual(res.reason, 'NOT FOUND')
self.assertEqual(res.body, b'not found')
fd = io.BytesIO()
res.write(fd)
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 404 NOT FOUND\r\n', response)
def test_cookies(self): def test_cookies(self):
res = Response('ok') res = Response('ok')
res.set_cookie('foo1', 'bar1') res.set_cookie('foo1', 'bar1')

View File

@@ -76,6 +76,16 @@ class TestMicrodotAsync(unittest.TestCase):
self.assertIn(b'Content-Type: text/plain\r\n', fd2.response) self.assertIn(b'Content-Type: text/plain\r\n', fd2.response)
self.assertTrue(fd2.response.endswith(b'\r\n\r\nbar-async')) self.assertTrue(fd2.response.endswith(b'\r\n\r\nbar-async'))
def test_empty_request(self):
app = Microdot()
mock_socket.clear_requests()
fd = mock_socket.FakeStream(b'\n')
mock_socket._requests.append(fd)
self._add_shutdown(app)
app.run()
assert fd.response == b''
def test_before_after_request(self): def test_before_after_request(self):
app = Microdot() app = Microdot()

View File

@@ -85,6 +85,26 @@ class TestResponseAsync(unittest.TestCase):
self.assertIn(b'Content-Type: application/json\r\n', fd.response) self.assertIn(b'Content-Type: application/json\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n[1, "2"]')) self.assertTrue(fd.response.endswith(b'\r\n\r\n[1, "2"]'))
def test_create_with_reason(self):
res = Response('foo', reason='ALL GOOD!')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {})
self.assertEqual(res.reason, 'ALL GOOD!')
self.assertEqual(res.body, b'foo')
fd = FakeStreamAsync()
_run(res.write(fd))
self.assertIn(b'HTTP/1.0 200 ALL GOOD!\r\n', fd.response)
def test_create_with_status_and_reason(self):
res = Response('not found', 404, reason='NOT FOUND')
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers, {})
self.assertEqual(res.reason, 'NOT FOUND')
self.assertEqual(res.body, b'not found')
fd = FakeStreamAsync()
_run(res.write(fd))
self.assertIn(b'HTTP/1.0 404 NOT FOUND\r\n', fd.response)
def test_send_file(self): def test_send_file(self):
res = Response.send_file('tests/files/test.txt', res = Response.send_file('tests/files/test.txt',
content_type='text/html') content_type='text/html')

12
tox.ini
View File

@@ -13,17 +13,17 @@ python =
[testenv] [testenv]
commands= commands=
pip install -e microdot pip install -e .
pip install -e microdot-asyncio pytest -p no:logging --cov=src --cov-branch --cov-report=term-missing
coverage run --branch --include="microdot*.py" -m unittest tests deps=
coverage report --show-missing pytest
deps=coverage pytest-cov
[testenv:flake8] [testenv:flake8]
deps= deps=
flake8 flake8
commands= commands=
flake8 --ignore=W503 --exclude tests/libs microdot microdot-asyncio tests flake8 --ignore=W503 --exclude tests/libs src tests
[testenv:upy] [testenv:upy]
whitelist_externals=sh whitelist_externals=sh