1 Commits

Author SHA1 Message Date
Miguel Grinberg
a8ae47799c Release v2.0.0 2023-12-22 20:39:09 +00:00
33 changed files with 184 additions and 183 deletions

View File

@@ -1,26 +1,8 @@
# Microdot change log
**Release 2.0.2** - 2023-12-28
**Release v2.0.0** - 2023-12-22
- 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
- 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
- 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))
**Release 1.3.4** - 2023-11-08

View File

@@ -32,10 +32,5 @@ describes the backwards incompatible changes that were made.
## Resources
- 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/)
- Documentation: [Latest](https://microdot.readthedocs.io/en/latest/) [Stable](https://microdot.readthedocs.io/en/stable/) [Legacy v1.x](https://microdot.readthedocs.io/en/v1/)
- [Change Log](https://github.com/miguelgrinberg/microdot/blob/main/CHANGES.md)

Binary file not shown.

View File

@@ -129,10 +129,11 @@ are the asynchronous versions of these two methods.
The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the
:func:`Template.initialize <microdot.utemplate.Template.initialize>` class
method::
:func:`init_templates <microdot.utemplate.init_templates>` function::
Template.initialize('my_templates')
from microdot.utemplate import init_templates
init_templates('my_templates')
Using the Jinja Engine
^^^^^^^^^^^^^^^^^^^^^^
@@ -173,15 +174,17 @@ method, which returns a generator instead of a string.
The default location from where templates are loaded is the *templates*
subdirectory. This location can be changed with the
:func:`Template.initialize <microdot.jinja.Template.initialize>` class method::
:func:`init_templates <microdot.utemplate.init_templates>` function::
Template.initialize('my_templates')
from microdot.jinja import init_templates
The ``initialize()`` method also accepts ``enable_async`` argument, which
init_templates('my_templates')
The ``init_templates()`` function also accepts ``enable_async`` argument, which
can be set to ``True`` if asynchronous rendering of templates is desired. If
this option is enabled, then the
:func:`render_async() <microdot.jinja.Template.render_async>` and
:func:`generate_async() <microdot.jinja.Template.generate_async>` methods
:func:`render_async() <microdot.utemplate.Template.render_async>` and
:func:`generate_async() <microdot.utemplate.Template.generate_async>` methods
must be used.
.. 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
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
URL::
URL.
@app.get('/tests/<path: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 orders import orders_app
def create_app():
async def create_app():
app = Microdot()
app.mount(customers_app, url_prefix='/customers')
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
imports from this::
from microdot_asyncio import Microdot
from microdot.asyncio import Microdot
to this::
@@ -94,7 +94,7 @@ as a single string::
Streamed templates also have an asynchronous version::
return Template('index.html').generate_async(title='Home')
return await Template('index.html').generate_async(title='Home')
Class-based user sessions
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -138,8 +138,5 @@ deployed with standard WSGI servers such as Gunicorn.
WebSocket support when using the WSGI extension is enabled when using a
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.

View File

@@ -1,7 +1,7 @@
from microdot import Microdot, Response
from microdot.jinja import Template
from microdot.jinja import template, init_templates
Template.initialize('templates', enable_async=True)
init_templates('templates', enable_async=True)
app = Microdot()
Response.default_content_type = 'text/html'
@@ -11,7 +11,7 @@ async def index(req):
name = None
if req.method == 'POST':
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__':

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
from microdot import Microdot, Response
from microdot.utemplate import Template
from microdot.utemplate import template
app = Microdot()
Response.default_content_type = 'text/html'
@@ -10,7 +10,7 @@ async def index(req):
name = None
if req.method == 'POST':
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__':

View File

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

View File

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

View File

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

View File

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

View File

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

79
libs/micropython/time.py Normal file
View File

@@ -0,0 +1,79 @@
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]
name = "microdot"
version = "2.0.2"
version = "v2.0.0"
authors = [
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
]

View File

@@ -3,37 +3,36 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
_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:
"""A template object.
:param template: The filename of the template to render, relative to the
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):
if _jinja_env is None: # pragma: no cover
self.initialize()
init_templates()
#: The name of the template
self.name = template
self.template = _jinja_env.get_template(template)

View File

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

View File

@@ -8,23 +8,13 @@ class SSE:
self.queue = []
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)):
data = json.dumps(data).encode()
elif isinstance(data, str):
data = data.encode()
elif not isinstance(data, bytes):
data = str(data).encode()
data = b'data: ' + data + b'\n\n'
data = json.dumps(data)
elif not isinstance(data, str):
data = str(data)
data = f'data: {data}\n\n'
if event:
data = b'event: ' + event.encode() + b'\n' + data
data = f'event: {event}\n{data}'
self.queue.append(data)
self.event.set()

View File

@@ -3,32 +3,30 @@ from utemplate import recompile
_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:
"""A template object.
:param template: The filename of the template to render, relative to the
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):
if _loader is None: # pragma: no cover
self.initialize()
init_templates()
#: The name of the template
self.name = template
self.template = _loader.load(template)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,23 @@
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
ARG VERSION=master
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/micropython/micropython.git && \
cd micropython && \
git checkout $VERSION && \
git submodule update --init && \
cd mpy-cross && \
make && \
cd .. && \
cd ports/unix && \
make && \
make test && \
make install && \
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \
cd ../../.. && \
rm -rf micropython
CMD ["/usr/local/bin/micropython"]

View File

@@ -1,24 +0,0 @@
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"]

View File

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

View File

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