11 Commits

Author SHA1 Message Date
Miguel Grinberg
300f8563ed Release 2.0.2 2023-12-28 12:10:46 +00:00
Miguel Grinberg
1fc11193da Support binary data in the SSE extension 2023-12-28 12:04:17 +00:00
Miguel Grinberg
79452a4699 Upgrade micropython tests to use v1.22, initial circuitpython work 2023-12-27 20:39:20 +00:00
Miguel Grinberg
84842e39c3 Improvements to migration guide 2023-12-26 20:00:07 +00:00
Miguel Grinberg
2a3c889717 typo in documentation #nolog 2023-12-26 17:07:20 +00:00
Tak Tran
ad368be993 Remove spurious async in documentation example (#187) 2023-12-23 14:08:12 +00:00
Miguel Grinberg
3df56c6ffe Version 2.0.2.dev0 2023-12-23 12:50:03 +00:00
Miguel Grinberg
c2e18004f7 Release 2.0.1 2023-12-23 12:49:52 +00:00
Miguel Grinberg
bd18ceb442 Addressed some inadvertent mistakes in the template extensions 2023-12-23 12:48:27 +00:00
Miguel Grinberg
f38d6d760a Updated readme and change log #nolog 2023-12-23 00:04:22 +00:00
Miguel Grinberg
dee4914bdd Version 2.0.1.dev0 2023-12-22 20:42:53 +00:00
33 changed files with 182 additions and 183 deletions

View File

@@ -1,8 +1,26 @@
# Microdot change log # Microdot change log
**Release 2.0.2** - 2023-12-28
- Support binary data in the SSE extension ([commit](https://github.com/miguelgrinberg/microdot/commit/1fc11193da0d298f5539e2ad218836910a13efb2))
- Upgrade micropython tests to use v1.22 + initial CircuitPython testing work ([commit](https://github.com/miguelgrinberg/microdot/commit/79452a46992351ccad2c0317c20bf50be0d76641))
- Improvements to migration guide ([commit](https://github.com/miguelgrinberg/microdot/commit/84842e39c360a8b3ddf36feac8af201fb19bbb0b))
- Remove spurious async in documentation example [#187](https://github.com/miguelgrinberg/microdot/issues/187) ([commit](https://github.com/miguelgrinberg/microdot/commit/ad368be993e2e3007579f1d3880e36d60c71da92)) (thanks **Tak Tran**!)
**Release 2.0.1** - 2023-12-23
- Addressed some inadvertent mistakes in the template extensions ([commit](https://github.com/miguelgrinberg/microdot/commit/bd18ceb4424e9dfb52b1e6d498edd260aa24fc53))
**Release 2.0.0** - 2023-12-22 **Release 2.0.0** - 2023-12-22
- Major redesign switching to asyncio as the base implementation (See the [Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html) in the docs for details) [#186](https://github.com/miguelgrinberg/microdot/issues/186) ([commit](https://github.com/miguelgrinberg/microdot/commit/20ea305fe793eb206b52af9eb5c5f3c1e9f57dbb)) - Major redesign [#186](https://github.com/miguelgrinberg/microdot/issues/186) ([commit](https://github.com/miguelgrinberg/microdot/commit/20ea305fe793eb206b52af9eb5c5f3c1e9f57dbb))
- Code reorganization as a `microdot` package
- Asyncio is now the core implementation
- New support for Server-Sent Events (SSE)
- Several extensions redesigned
- Support for "partitioned" cookies
- [Cross-compiling and freezing](https://microdot.readthedocs.io/en/stable/freezing.html) guidance
- A [Migration Guide](https://microdot.readthedocs.io/en/stable/migrating.html) to help transition to version 2 from older releases
**Release 1.3.4** - 2023-11-08 **Release 1.3.4** - 2023-11-08

View File

@@ -32,5 +32,10 @@ describes the backwards incompatible changes that were made.
## Resources ## Resources
- Documentation: [Latest](https://microdot.readthedocs.io/en/latest/) [Stable](https://microdot.readthedocs.io/en/stable/) [Legacy v1.x](https://microdot.readthedocs.io/en/v1/) - Documentation
- [Stable](https://microdot.readthedocs.io/en/stable/)
- [Latest](https://microdot.readthedocs.io/en/latest/)
- Still using version 1?
- [Code](https://github.com/miguelgrinberg/microdot/tree/v1)
- [Documentation](https://microdot.readthedocs.io/en/v1/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md) - [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)

Binary file not shown.

View File

@@ -129,11 +129,10 @@ are the asynchronous versions of these two methods.
The default location from where templates are loaded is the *templates* The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the subdirectory. This location can be changed with the
:func:`init_templates <microdot.utemplate.init_templates>` function:: :func:`Template.initialize <microdot.utemplate.Template.initialize>` class
method::
from microdot.utemplate import init_templates Template.initialize('my_templates')
init_templates('my_templates')
Using the Jinja Engine Using the Jinja Engine
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
@@ -174,17 +173,15 @@ method, which returns a generator instead of a string.
The default location from where templates are loaded is the *templates* The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the subdirectory. This location can be changed with the
:func:`init_templates <microdot.utemplate.init_templates>` function:: :func:`Template.initialize <microdot.jinja.Template.initialize>` class method::
from microdot.jinja import init_templates Template.initialize('my_templates')
init_templates('my_templates') The ``initialize()`` method also accepts ``enable_async`` argument, which
The ``init_templates()`` function also accepts ``enable_async`` argument, which
can be set to ``True`` if asynchronous rendering of templates is desired. If can be set to ``True`` if asynchronous rendering of templates is desired. If
this option is enabled, then the this option is enabled, then the
:func:`render_async() <microdot.utemplate.Template.render_async>` and :func:`render_async() <microdot.jinja.Template.render_async>` and
:func:`generate_async() <microdot.utemplate.Template.generate_async>` methods :func:`generate_async() <microdot.jinja.Template.generate_async>` methods
must be used. must be used.
.. note:: .. note::

View File

@@ -297,7 +297,7 @@ match and the route will not be called.
A special type ``path`` can be used to capture the remainder of the path as a A special type ``path`` can be used to capture the remainder of the path as a
single argument. The difference between an argument of type ``path`` and one of single argument. The difference between an argument of type ``path`` and one of
type ``string`` is that the latter stops capturing when a ``/`` appears in the type ``string`` is that the latter stops capturing when a ``/`` appears in the
URL. URL::
@app.get('/tests/<path:path>') @app.get('/tests/<path:path>')
async def get_test(request, path): async def get_test(request, path):
@@ -462,7 +462,7 @@ the sub-applications to build the larger combined application::
from customers import customers_app from customers import customers_app
from orders import orders_app from orders import orders_app
async def create_app(): def create_app():
app = Microdot() app = Microdot()
app.mount(customers_app, url_prefix='/customers') app.mount(customers_app, url_prefix='/customers')
app.mount(orders_app, url_prefix='/orders') app.mount(orders_app, url_prefix='/orders')

View File

@@ -39,7 +39,7 @@ extension.
Any applications built using the asyncio extension will need to update their Any applications built using the asyncio extension will need to update their
imports from this:: imports from this::
from microdot.asyncio import Microdot from microdot_asyncio import Microdot
to this:: to this::
@@ -94,7 +94,7 @@ as a single string::
Streamed templates also have an asynchronous version:: Streamed templates also have an asynchronous version::
return await Template('index.html').generate_async(title='Home') return Template('index.html').generate_async(title='Home')
Class-based user sessions Class-based user sessions
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -138,5 +138,8 @@ deployed with standard WSGI servers such as Gunicorn.
WebSocket support when using the WSGI extension is enabled when using a WebSocket support when using the WSGI extension is enabled when using a
compatible web server. At this time only Gunicorn is supported for WebSocket. compatible web server. At this time only Gunicorn is supported for WebSocket.
Given that WebSocket support is asynchronous, it would be better to switch to
the ASGI extension, which has full support for WebSocket as defined in the ASGI
specification.
As before, the WSGI extension is not available under MicroPython. As before, the WSGI extension is not available under MicroPython.

View File

@@ -1,7 +1,7 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.jinja import template, init_templates from microdot.jinja import Template
init_templates('templates', enable_async=True) Template.initialize('templates', enable_async=True)
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -11,7 +11,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return await template('index.html').render_async(name=name) return await Template('index.html').render_async(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.jinja import template from microdot.jinja import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -7,12 +7,12 @@ Response.default_content_type = 'text/html'
@app.route('/') @app.route('/')
async def index(req): async def index(req):
return template('page1.html').render(page='Page 1') return Template('page1.html').render(page='Page 1')
@app.route('/page2') @app.route('/page2')
async def page2(req): async def page2(req):
return template('page2.html').render(page='Page 2') return Template('page2.html').render(page='Page 2')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.jinja import template from microdot.jinja import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').render(name=name) return Template('index.html').render(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot.asgi import Microdot, Response from microdot.asgi import Microdot, Response
from microdot.jinja import template from microdot.jinja import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').render(name=name) return Template('index.html').render(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot.wsgi import Microdot, Response from microdot.wsgi import Microdot, Response
from microdot.jinja import template from microdot.jinja import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').render(name=name) return Template('index.html').render(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.jinja import template from microdot.jinja import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').generate(name=name) return Template('index.html').generate(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.utemplate import template from microdot.utemplate import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return await template('index.html').render_async(name=name) return await Template('index.html').render_async(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.utemplate import template from microdot.utemplate import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -7,12 +7,12 @@ Response.default_content_type = 'text/html'
@app.route('/') @app.route('/')
async def index(req): async def index(req):
return template('page1.html').render(page='Page 1') return Template('page1.html').render(page='Page 1')
@app.route('/page2') @app.route('/page2')
async def page2(req): async def page2(req):
return template('page2.html').render(page='Page 2') return Template('page2.html').render(page='Page 2')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.utemplate import template from microdot.utemplate import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').render(name=name) return Template('index.html').render(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot.asgi import Microdot, Response from microdot.asgi import Microdot, Response
from microdot.utemplate import template from microdot.utemplate import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').render(name=name) return Template('index.html').render(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot.wsgi import Microdot, Response from microdot.wsgi import Microdot, Response
from microdot.utemplate import template from microdot.utemplate import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').render(name=name) return Template('index.html').render(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response from microdot import Microdot, Response
from microdot.utemplate import template from microdot.utemplate import Template
app = Microdot() app = Microdot()
Response.default_content_type = 'text/html' Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None name = None
if req.method == 'POST': if req.method == 'POST':
name = req.form.get('name') name = req.form.get('name')
return template('index.html').generate(name=name) return Template('index.html').generate(name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,79 +0,0 @@
from utime import *
from micropython import const
_TS_YEAR = const(0)
_TS_MON = const(1)
_TS_MDAY = const(2)
_TS_HOUR = const(3)
_TS_MIN = const(4)
_TS_SEC = const(5)
_TS_WDAY = const(6)
_TS_YDAY = const(7)
_TS_ISDST = const(8)
_WDAY = const(("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))
_MDAY = const(
(
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
)
)
def strftime(datefmt, ts):
from io import StringIO
fmtsp = False
ftime = StringIO()
for k in datefmt:
if fmtsp:
if k == "a":
ftime.write(_WDAY[ts[_TS_WDAY]][0:3])
elif k == "A":
ftime.write(_WDAY[ts[_TS_WDAY]])
elif k == "b":
ftime.write(_MDAY[ts[_TS_MON] - 1][0:3])
elif k == "B":
ftime.write(_MDAY[ts[_TS_MON] - 1])
elif k == "d":
ftime.write("%02d" % ts[_TS_MDAY])
elif k == "H":
ftime.write("%02d" % ts[_TS_HOUR])
elif k == "I":
ftime.write("%02d" % (ts[_TS_HOUR] % 12))
elif k == "j":
ftime.write("%03d" % ts[_TS_YDAY])
elif k == "m":
ftime.write("%02d" % ts[_TS_MON])
elif k == "M":
ftime.write("%02d" % ts[_TS_MIN])
elif k == "P":
ftime.write("AM" if ts[_TS_HOUR] < 12 else "PM")
elif k == "S":
ftime.write("%02d" % ts[_TS_SEC])
elif k == "w":
ftime.write(str(ts[_TS_WDAY]))
elif k == "y":
ftime.write("%02d" % (ts[_TS_YEAR] % 100))
elif k == "Y":
ftime.write(str(ts[_TS_YEAR]))
else:
ftime.write(k)
fmtsp = False
elif k == "%":
fmtsp = True
else:
ftime.write(k)
val = ftime.getvalue()
ftime.close()
return val

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "microdot" name = "microdot"
version = "2.0.0" version = "2.0.2"
authors = [ authors = [
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
] ]

View File

@@ -3,36 +3,37 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
_jinja_env = None _jinja_env = None
def init_templates(template_dir='templates', enable_async=False, **kwargs):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
:param enable_async: set to ``True`` to enable the async rendering engine
in Jinja, and the ``render_async()`` and
``generate_async()`` methods.
:param kwargs: any additional options to be passed to Jinja's
``Environment`` class.
"""
global _jinja_env
_jinja_env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(),
enable_async=enable_async,
**kwargs
)
class Template: class Template:
"""A template object. """A template object.
:param template: The filename of the template to render, relative to the :param template: The filename of the template to render, relative to the
configured template directory. configured template directory.
""" """
@classmethod
def initialize(cls, template_dir='templates', enable_async=False,
**kwargs):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load
templates from a *templates* subdirectory.
:param enable_async: set to ``True`` to enable the async rendering
engine in Jinja, and the ``render_async()`` and
``generate_async()`` methods.
:param kwargs: any additional options to be passed to Jinja's
``Environment`` class.
"""
global _jinja_env
_jinja_env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(),
enable_async=enable_async,
**kwargs
)
def __init__(self, template): def __init__(self, template):
if _jinja_env is None: # pragma: no cover if _jinja_env is None: # pragma: no cover
init_templates() self.initialize()
#: The name of the template #: The name of the template
self.name = template self.name = template
self.template = _jinja_env.get_template(template) self.template = _jinja_env.get_template(template)

View File

@@ -595,7 +595,7 @@ class Response:
if expires: if expires:
if isinstance(expires, str): if isinstance(expires, str):
http_cookie += '; Expires=' + expires http_cookie += '; Expires=' + expires
else: else: # pragma: no cover
http_cookie += '; Expires=' + time.strftime( http_cookie += '; Expires=' + time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', expires.timetuple()) '%a, %d %b %Y %H:%M:%S GMT', expires.timetuple())
if max_age: if max_age:

View File

@@ -8,13 +8,23 @@ class SSE:
self.queue = [] self.queue = []
async def send(self, data, event=None): async def send(self, data, event=None):
"""Send an event to the client.
:param data: the data to send. It can be given as a string, bytes, dict
or list. Dictionaries and lists are serialized to JSON.
Any other types are converted to string before sending.
:param event: an optional event name, to send along with the data. If
given, it must be a string.
"""
if isinstance(data, (dict, list)): if isinstance(data, (dict, list)):
data = json.dumps(data) data = json.dumps(data).encode()
elif not isinstance(data, str): elif isinstance(data, str):
data = str(data) data = data.encode()
data = f'data: {data}\n\n' elif not isinstance(data, bytes):
data = str(data).encode()
data = b'data: ' + data + b'\n\n'
if event: if event:
data = f'event: {event}\n{data}' data = b'event: ' + event.encode() + b'\n' + data
self.queue.append(data) self.queue.append(data)
self.event.set() self.event.set()

View File

@@ -3,30 +3,32 @@ from utemplate import recompile
_loader = None _loader = None
def init_templates(template_dir='templates', loader_class=recompile.Loader):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load templates
from a *templates* subdirectory.
:param loader_class: the ``utemplate.Loader`` class to use when loading
templates. This argument is optional. The default is
the ``recompile.Loader`` class, which automatically
recompiles templates when they change.
"""
global _loader
_loader = loader_class(None, template_dir)
class Template: class Template:
"""A template object. """A template object.
:param template: The filename of the template to render, relative to the :param template: The filename of the template to render, relative to the
configured template directory. configured template directory.
""" """
@classmethod
def initialize(cls, template_dir='templates',
loader_class=recompile.Loader):
"""Initialize the templating subsystem.
:param template_dir: the directory where templates are stored. This
argument is optional. The default is to load
templates from a *templates* subdirectory.
:param loader_class: the ``utemplate.Loader`` class to use when loading
templates. This argument is optional. The default
is the ``recompile.Loader`` class, which
automatically recompiles templates when they
change.
"""
global _loader
_loader = loader_class(None, template_dir)
def __init__(self, template): def __init__(self, template):
if _loader is None: # pragma: no cover if _loader is None: # pragma: no cover
init_templates() self.initialize()
#: The name of the template #: The name of the template
self.name = template self.name = template
self.template = _loader.load(template) self.template = _loader.load(template)

View File

@@ -2,10 +2,10 @@ import asyncio
import sys import sys
import unittest import unittest
from microdot import Microdot from microdot import Microdot
from microdot.jinja import Template, init_templates from microdot.jinja import Template
from microdot.test_client import TestClient from microdot.test_client import TestClient
init_templates('tests/templates') Template.initialize('tests/templates')
@unittest.skipIf(sys.implementation.name == 'micropython', @unittest.skipIf(sys.implementation.name == 'micropython',
@@ -49,7 +49,7 @@ class TestJinja(unittest.TestCase):
self.assertEqual(res.body, b'Hello, foo!') self.assertEqual(res.body, b'Hello, foo!')
def test_render_async_template_in_app(self): def test_render_async_template_in_app(self):
init_templates('tests/templates', enable_async=True) Template.initialize('tests/templates', enable_async=True)
app = Microdot() app = Microdot()
@@ -62,10 +62,10 @@ class TestJinja(unittest.TestCase):
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertEqual(res.body, b'Hello, foo!') self.assertEqual(res.body, b'Hello, foo!')
init_templates('tests/templates') Template.initialize('tests/templates')
def test_generate_async_template_in_app(self): def test_generate_async_template_in_app(self):
init_templates('tests/templates', enable_async=True) Template.initialize('tests/templates', enable_async=True)
app = Microdot() app = Microdot()
@@ -78,4 +78,4 @@ class TestJinja(unittest.TestCase):
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertEqual(res.body, b'Hello, foo!') self.assertEqual(res.body, b'Hello, foo!')
init_templates('tests/templates') Template.initialize('tests/templates')

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
from datetime import datetime
import unittest import unittest
from microdot import Response from microdot import Response
from tests.mock_socket import FakeStreamAsync from tests.mock_socket import FakeStreamAsync
@@ -186,12 +185,12 @@ class TestResponse(unittest.TestCase):
res.set_cookie('foo2', 'bar2', path='/', partitioned=True) res.set_cookie('foo2', 'bar2', path='/', partitioned=True)
res.set_cookie('foo3', 'bar3', domain='example.com:1234') res.set_cookie('foo3', 'bar3', domain='example.com:1234')
res.set_cookie('foo4', 'bar4', res.set_cookie('foo4', 'bar4',
expires=datetime(2019, 11, 5, 2, 23, 54)) expires='Tue, 05 Nov 2019 02:23:54 GMT')
res.set_cookie('foo5', 'bar5', max_age=123, res.set_cookie('foo5', 'bar5', max_age=123,
expires='Thu, 01 Jan 1970 00:00:00 GMT') expires='Thu, 01 Jan 1970 00:00:00 GMT')
res.set_cookie('foo6', 'bar6', secure=True, http_only=True) res.set_cookie('foo6', 'bar6', secure=True, http_only=True)
res.set_cookie('foo7', 'bar7', path='/foo', domain='example.com:1234', res.set_cookie('foo7', 'bar7', path='/foo', domain='example.com:1234',
expires=datetime(2019, 11, 5, 2, 23, 54), max_age=123, expires='Tue, 05 Nov 2019 02:23:54 GMT', max_age=123,
secure=True, http_only=True) secure=True, http_only=True)
res.delete_cookie('foo8', http_only=True) res.delete_cookie('foo8', http_only=True)
self.assertEqual(res.headers, {'Set-Cookie': [ self.assertEqual(res.headers, {'Set-Cookie': [

View File

@@ -26,6 +26,7 @@ class TestWebSocket(unittest.TestCase):
await sse.send({'foo': 'bar'}) await sse.send({'foo': 'bar'})
await sse.send([42, 'foo', 'bar']) await sse.send([42, 'foo', 'bar'])
await sse.send(ValueError('foo')) await sse.send(ValueError('foo'))
await sse.send(b'foo')
client = TestClient(app) client = TestClient(app)
response = self._run(client.get('/sse')) response = self._run(client.get('/sse'))
@@ -35,4 +36,5 @@ class TestWebSocket(unittest.TestCase):
'event: test\ndata: bar\n\n' 'event: test\ndata: bar\n\n'
'data: {"foo": "bar"}\n\n' 'data: {"foo": "bar"}\n\n'
'data: [42, "foo", "bar"]\n\n' 'data: [42, "foo", "bar"]\n\n'
'data: foo\n\n'
'data: foo\n\n')) 'data: foo\n\n'))

View File

@@ -2,9 +2,9 @@ import asyncio
import unittest import unittest
from microdot import Microdot from microdot import Microdot
from microdot.test_client import TestClient from microdot.test_client import TestClient
from microdot.utemplate import Template, init_templates from microdot.utemplate import Template
init_templates('tests/templates') Template.initialize('tests/templates')
class TestUTemplate(unittest.TestCase): class TestUTemplate(unittest.TestCase):

View File

@@ -1,23 +1,24 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG VERSION=master
ENV VERSION=$VERSION
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y build-essential libffi-dev git pkg-config python3 && \ apt-get install -y build-essential libffi-dev git pkg-config python3 && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
git clone https://github.com/micropython/micropython.git && \ git clone https://github.com/micropython/micropython.git && \
cd micropython && \ cd micropython && \
git checkout $VERSION && \
git submodule update --init && \ git submodule update --init && \
cd mpy-cross && \ cd mpy-cross && \
make && \ make && \
cd .. && \ cd .. && \
cd ports/unix && \ cd ports/unix && \
make && \ make && \
make test && \
make install && \ make install && \
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \ apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \
cd ../../.. && \ cd ../../.. && \
rm -rf micropython rm -rf micropython
CMD ["/usr/local/bin/micropython"] CMD ["/usr/local/bin/micropython"]

View File

@@ -0,0 +1,24 @@
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
ARG VERSION=main
ENV VERSION=$VERSION
RUN apt-get update && \
apt-get install -y build-essential libffi-dev git pkg-config python3 && \
rm -rf /var/lib/apt/lists/* && \
git clone https://github.com/adafruit/circuitpython.git && \
cd circuitpython && \
git checkout $VERSION && \
git submodule update --init lib tools frozen && \
cd mpy-cross && \
make && \
cd .. && \
cd ports/unix && \
make && \
make install && \
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \
cd ../../.. && \
rm -rf circuitpython
CMD ["/usr/local/bin/micropython"]

11
tools/update-circuitpython.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# this script updates the micropython binary in the /bin directory that is
# used to run unit tests under GitHub Actions builds
DOCKER=${DOCKER:-docker}
VERSION=${1:-main}
$DOCKER build -f Dockerfile.circuitpython --build-arg VERSION=$VERSION -t circuitpython .
$DOCKER create -t --name dummy-circuitpython circuitpython
$DOCKER cp dummy-circuitpython:/usr/local/bin/micropython ../bin/circuitpython
$DOCKER rm dummy-circuitpython

View File

@@ -3,8 +3,9 @@
# used to run unit tests under GitHub Actions builds # used to run unit tests under GitHub Actions builds
DOCKER=${DOCKER:-docker} DOCKER=${DOCKER:-docker}
VERSION=${1:-master}
$DOCKER build -t micropython . $DOCKER build --build-arg VERSION=$VERSION -t micropython .
$DOCKER create -it --name dummy-micropython micropython $DOCKER create -it --name dummy-micropython micropython
$DOCKER cp dummy-micropython:/usr/local/bin/micropython ../bin/micropython $DOCKER cp dummy-micropython:/usr/local/bin/micropython ../bin/micropython
$DOCKER rm dummy-micropython $DOCKER rm dummy-micropython

View File

@@ -1,5 +1,5 @@
[tox] [tox]
envlist=flake8,py38,py39,py310,py311,py312,upy,benchmark envlist=flake8,py38,py39,py310,py311,py312,upy,cpy,benchmark
skipsdist=True skipsdist=True
skip_missing_interpreters=True skip_missing_interpreters=True
@@ -29,6 +29,10 @@ setenv=
allowlist_externals=sh allowlist_externals=sh
commands=sh -c "bin/micropython run_tests.py" commands=sh -c "bin/micropython run_tests.py"
[testenv:cpy]
allowlist_externals=sh
commands=sh -c "bin/circuitpython run_tests.py"
[testenv:upy-mac] [testenv:upy-mac]
allowlist_externals=micropython allowlist_externals=micropython
commands=micropython run_tests.py commands=micropython run_tests.py