1 Commits

Author SHA1 Message Date
Miguel Grinberg
7ee1c7eef9 Authentication support 2022-09-24 19:54:26 +01:00
87 changed files with 811 additions and 1998 deletions

View File

@@ -1,26 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**IMPORTANT**: If you have a question, or you are not sure if you have found a bug in this package, then you are in the wrong place. Hit back in your web browser, and then open a GitHub Discussion instead. Likewise, if you are unable to provide the information requested below, open a discussion to troubleshoot your issue.
**Describe the bug**
A clear and concise description of what the bug is. If you are getting errors, please include the complete error message, including the stack trace.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Discussions
url: https://github.com/miguelgrinberg/microdot/discussions
about: Ask questions here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -11,8 +11,8 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -eflake8 - run: tox -eflake8
@@ -21,12 +21,12 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.7', '3.8', '3.9', '3.10', '3.11'] python: ['3.6', '3.7', '3.8', '3.9', '3.10']
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
@@ -36,8 +36,8 @@ jobs:
name: tests-micropython name: tests-micropython
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -eupy - run: tox -eupy
@@ -45,21 +45,18 @@ jobs:
name: coverage name: coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions codecov
- run: tox - run: tox
- uses: codecov/codecov-action@v3 - run: codecov
with:
files: ./coverage.xml
fail_ci_if_error: true
benchmark: benchmark:
name: benchmark name: benchmark
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-python@v3 - uses: actions/setup-python@v2
- run: python -m pip install --upgrade pip wheel - run: python -m pip install --upgrade pip wheel
- run: pip install tox tox-gh-actions - run: pip install tox tox-gh-actions
- run: tox -ebenchmark - run: tox -ebenchmark

5
.gitignore vendored
View File

@@ -103,8 +103,3 @@ venv.bak/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# other
*.der
*.pem
*_txt.py

View File

@@ -1,71 +1,5 @@
# Microdot change log # Microdot change log
**Release 1.3.2** - 2023-06-13
- In ASGI, return headers as strings and not binary [#144](https://github.com/miguelgrinberg/microdot/issues/144) ([commit](https://github.com/miguelgrinberg/microdot/commit/e92310fa55bbffcdcbb33f560e27c3579d7ac451))
- Incorrect import in `static_async.py` example ([commit](https://github.com/miguelgrinberg/microdot/commit/c07a53943508e64baea160748e67efc92e75b036))
**Release 1.3.1** - 2023-05-21
- Support negative numbers for int path components [#137](https://github.com/miguelgrinberg/microdot/issues/137) ([commit](https://github.com/miguelgrinberg/microdot/commit/a0dd7c8ab6d681932324e56ed101aba861a105a0))
- Use a more conservative default for socket timeout [#130](https://github.com/miguelgrinberg/microdot/issues/130) ([commit](https://github.com/miguelgrinberg/microdot/commit/239cf4ff37268a7e2467b93be44fe9f91cee8aee))
- More robust check for socket timeout error code [#106](https://github.com/miguelgrinberg/microdot/issues/106) ([commit](https://github.com/miguelgrinberg/microdot/commit/efec9f14be7b6f3451e4d1d0fe7e528ce6ca74dc))
- WebSocket error when handling PING packet [#129](https://github.com/miguelgrinberg/microdot/issues/129) ([commit](https://github.com/miguelgrinberg/microdot/commit/87cd098f66e24bed6bbad29b1490a129e355bbb3))
- Explicitly set UTF-8 encoding for HTML files in examples [#132](https://github.com/miguelgrinberg/microdot/issues/132) ([commit](https://github.com/miguelgrinberg/microdot/commit/f81de6d9582f4905b9c2735d3c639b92d7e77994))
**Release 1.3.0** - 2023-04-08
- Cross-Origin Resource Sharing (CORS) extension [#45](https://github.com/miguelgrinberg/microdot/issues/45) ([commit](https://github.com/miguelgrinberg/microdot/commit/67798f7dbffb30018ab4b62a9aaa297f63bc9e64))
- Respond to `HEAD` and `OPTIONS` requests ([commit](https://github.com/miguelgrinberg/microdot/commit/6a31f89673518e79fef5659c04e609b7976a5e34))
- Tolerate slightly invalid formats in query strings [#126](https://github.com/miguelgrinberg/microdot/issues/126) ([commit](https://github.com/miguelgrinberg/microdot/commit/a1b061656fa19dae583951596b0f1f0603652a56))
- Support compressed files in `send_file()` [#93](https://github.com/miguelgrinberg/microdot/issues/93) ([commit](https://github.com/miguelgrinberg/microdot/commit/daf1001ec55ab38e6cdfee4931729a3b7506858b))
- Add `max_age` argument to `send_file()` ([commit](https://github.com/miguelgrinberg/microdot/commit/e684ee32d91d3e2ab9569bb5fd342986c010ffeb))
- Add `update()` method to `NoCaseDict` class ([commit](https://github.com/miguelgrinberg/microdot/commit/ea6766cea96b756b36ed777f9c1b6a6680db09ba))
- Set exit code to 1 for failed MicroPython test runs ([commit](https://github.com/miguelgrinberg/microdot/commit/a350e8fd1e55fac12c9e5b909cfa82d880b177ef))
**Release 1.2.4** - 2023-03-03
- One more attempt to correct build issues ([commit](https://github.com/miguelgrinberg/microdot/commit/cb39898829f4edc233ab4e7ba3f7ef3c5c50f196))
**Release 1.2.3** - 2023-03-03
- Corrected a problem with previous build.
**Release 1.2.2** - 2023-03-03
- Add a socket read timeout to abort incomplete requests [#99](https://github.com/miguelgrinberg/microdot/issues/99) ([commit](https://github.com/miguelgrinberg/microdot/commit/d0d358f94a63f8565d6406feff0c6e7418cc7f81))
- More robust timeout handling [#106](https://github.com/miguelgrinberg/microdot/issues/106) ([commit](https://github.com/miguelgrinberg/microdot/commit/4d432a7d6cd88b874a8b825fb62891ed22881f74))
- Add @after_error_handler decorator [#97](https://github.com/miguelgrinberg/microdot/issues/97) ([commit](https://github.com/miguelgrinberg/microdot/commit/fcaeee69052b5681706f65b022e667baeee30d4d))
- Return headers as lowercase byte sequences as required by ASGI ([commit](https://github.com/miguelgrinberg/microdot/commit/ddb3b8f442d3683df04554104edaf8acd9c68148))
- Async example of static file serving ([commit](https://github.com/miguelgrinberg/microdot/commit/680cd9c023352f0ff03d67f1041ea174b7b7385b))
- Fixing broken links to examples in documentation [#101](https://github.com/miguelgrinberg/microdot/issues/101) ([commit](https://github.com/miguelgrinberg/microdot/commit/c00b24c9436e1b8f3d4c9bb6f2adfca988902e91)) (thanks **Eric Welch**!)
- Add scrollbar to documentation's left sidebar ([commit](https://github.com/miguelgrinberg/microdot/commit/2aa90d42451dc64c84efcc4f40a1b6c8d1ef1e8d))
- Documentation typo [#90](https://github.com/miguelgrinberg/microdot/issues/90) ([commit](https://github.com/miguelgrinberg/microdot/commit/81394980234f24aac834faf8e2e8225231e9014b)) (thanks **William Wheeler**!)
- Add CPU timing to benchmark ([commit](https://github.com/miguelgrinberg/microdot/commit/9398c960752f87bc32d7c4349cbf594e5d678e99))
- Upgrade uasyncio release used in tests ([commit](https://github.com/miguelgrinberg/microdot/commit/3d6815119ca1ec989f704f626530f938c857a8e5))
- Update unittest library for MicroPython ([commit](https://github.com/miguelgrinberg/microdot/commit/ecd84ecb7bd3c29d5af96739442b908badeab804))
- New build of micropython for unit tests ([commit](https://github.com/miguelgrinberg/microdot/commit/818f98d9a4e531e01c0f913813425ab2b40c289d))
- Remove 3.6, add 3.11 to builds ([commit](https://github.com/miguelgrinberg/microdot/commit/dd15d90239b73b5fd413515c9cd4ac23f6d42f67))
**Release 1.2.1** - 2022-12-06
- Error handling invokes parent exceptions [#74](https://github.com/miguelgrinberg/microdot/issues/74) ([commit](https://github.com/miguelgrinberg/microdot/commit/24d74fb8483b04e8abe6e303e06f0a310f32700b)) (thanks **Diego Pomares**!)
- Addressed error when deleting a user session in async app [#86](https://github.com/miguelgrinberg/microdot/issues/86) ([commit](https://github.com/miguelgrinberg/microdot/commit/5a589afd5e519e94e84fc1ee69033f2dad51c3ea))
- Add asyncio file upload example ([commit](https://github.com/miguelgrinberg/microdot/commit/c841cbedda40f59a9d87f6895fdf9fd954f854a2))
- New Jinja and uTemplate examples with Bootstrap ([commit](https://github.com/miguelgrinberg/microdot/commit/211ad953aeedb4c7f73fe210424aa173b4dc7fee))
- Fix typos in documentation [#77](https://github.com/miguelgrinberg/microdot/issues/77) ([commit](https://github.com/miguelgrinberg/microdot/commit/4a9b92b800d3fd87110f7bc9f546c10185ee13bc)) (thanks **Diego Pomares**!)
- Add missing exception argument to error handler example in documentation [#73](https://github.com/miguelgrinberg/microdot/issues/73) ([commit](https://github.com/miguelgrinberg/microdot/commit/c443599089f2127d1cb052dfba8a05c1969d65e3)) (thanks **Diego Pomares**!)
**Release 1.2.0** - 2022-09-25
- Use a case insensitive dict for headers ([commit #1](https://github.com/miguelgrinberg/microdot/commit/b0fd6c432371ca5cb10d07ff84c4deed7aa0ce2e) [commit #2](https://github.com/miguelgrinberg/microdot/commit/a8515c97b030f942fa6ca85cbe1772291468fb0d))
- urlencode() helper function ([commit #1](https://github.com/miguelgrinberg/microdot/commit/672512e086384e808489305502e6ebebcc5a888f) [commit #2](https://github.com/miguelgrinberg/microdot/commit/b133dcc34368853ee685396a1bcb50360e807813))
- Added `request.url` attribute with the complete URL of the request ([commit](https://github.com/miguelgrinberg/microdot/commit/1547e861ee28d43d10fe4c4ed1871345d4b81086))
- Do not log HTTPException occurrences ([commit](https://github.com/miguelgrinberg/microdot/commit/cbefb6bf3a3fdcff8b7a8bacad3449be18e46e3b))
- Cache user session for performance ([commit](https://github.com/miguelgrinberg/microdot/commit/01947b101ebe198312c88d73872e3248024918f0))
- File upload example ([commit](https://github.com/miguelgrinberg/microdot/commit/8ebe81c09b604ddc1123e78ad6bc87ceda5f8597))
- Minor documentation styling fixes ([commit](https://github.com/miguelgrinberg/microdot/commit/4f263c63ab7bb1ce0dd48d8e00f3c6891e1bf07e))
**Release 1.1.1** - 2022-09-18 **Release 1.1.1** - 2022-09-18
- Make WebSocket internals consistent between TLS and non-TLS [#61](https://github.com/miguelgrinberg/microdot/issues/61) ([commit](https://github.com/miguelgrinberg/microdot/commit/5693b812ceb2c0d51ec3c991adf6894a87e6fcc7)) - Make WebSocket internals consistent between TLS and non-TLS [#61](https://github.com/miguelgrinberg/microdot/issues/61) ([commit](https://github.com/miguelgrinberg/microdot/commit/5693b812ceb2c0d51ec3c991adf6894a87e6fcc7))

Binary file not shown.

View File

@@ -1,8 +1,3 @@
.py.class, .py.function, .py.method, .py.property { .py.class, .py.function, .py.method, .py.property {
margin-top: 20px; margin-top: 20px;
} }
div.sphinxsidebar {
max-height: 100%;
overflow-y: auto;
}

View File

@@ -52,12 +52,6 @@ API Reference
.. automodule:: microdot_session .. automodule:: microdot_session
:members: :members:
``microdot_cors`` module
------------------------
.. automodule:: microdot_cors
:members:
``microdot_websocket`` module ``microdot_websocket`` module
------------------------------ ------------------------------

View File

@@ -23,7 +23,7 @@ Asynchronous Support with Asyncio
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_ | MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples * - Examples
- | `hello_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_async.py>`_ - | `hello_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_async.py>`_
Microdot can be extended to use an asynchronous programming model based on the Microdot can be extended to use an asynchronous programming model based on the
``asyncio`` package. When the :class:`Microdot <microdot_asyncio.Microdot>` ``asyncio`` package. When the :class:`Microdot <microdot_asyncio.Microdot>`
@@ -68,8 +68,8 @@ Using the uTemplate Engine
- | `utemplate <https://github.com/pfalcon/utemplate/tree/master/utemplate>`_ - | `utemplate <https://github.com/pfalcon/utemplate/tree/master/utemplate>`_
* - Examples * - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/utemplate/hello.py>`_ - | `hello_utemplate.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_utemplate.py>`_
| `hello_utemplate_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_utemplate_async.py>`_ | `hello_utemplate_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_utemplate_async.py>`_
The :func:`render_template <microdot_utemplate.render_template>` function is The :func:`render_template <microdot_utemplate.render_template>` function is
used to render HTML templates with the uTemplate engine. The first argument is used to render HTML templates with the uTemplate engine. The first argument is
@@ -110,7 +110,7 @@ Using the Jinja Engine
- | `Jinja2 <https://jinja.palletsprojects.com/>`_ - | `Jinja2 <https://jinja.palletsprojects.com/>`_
* - Examples * - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/templates/jinja/hello.py>`_ - | `hello_jinja.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_jinja.py>`_
The :func:`render_template <microdot_jinja.render_template>` function is used The :func:`render_template <microdot_jinja.render_template>` function is used
to render HTML templates with the Jinja engine. The first argument is the to render HTML templates with the Jinja engine. The first argument is the
@@ -156,7 +156,7 @@ Maintaing Secure User Sessions
`hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_ `hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
* - Examples * - Examples
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/sessions/login.py>`_ - | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/login.py>`_
The session extension provides a secure way for the application to maintain The session extension provides a secure way for the application to maintain
user sessions. The session is stored as a signed cookie in the client's user sessions. The session is stored as a signed cookie in the client's
@@ -208,42 +208,6 @@ Example::
delete_session(req) delete_session(req)
return redirect('/') return redirect('/')
Cross-Origin Resource Sharing (CORS)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_cors.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_cors.py>`_
* - Required external dependencies
- | None
* - Examples
- | `cors.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/cors/cors.py>`_
The CORS extension provides support for `Cross-Origin Resource Sharing
(CORS) <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_. CORS is a
mechanism that allows web applications running on different origins to access
resources from each other. For example, a web application running on
``https://example.com`` can access resources from ``https://api.example.com``.
To enable CORS support, create an instance of the
:class:`CORS <microdot_cors.CORS>` class and configure the desired options.
Example::
from microdot import Microdot
from microdot_cors import CORS
app = Microdot()
cors = CORS(app, allowed_origins=['https://example.com'],
allow_credentials=True)
WebSocket Support WebSocket Support
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
@@ -280,7 +244,7 @@ Example::
ws.send(message) ws.send(message)
.. note:: .. note::
An unsupported *microdot_websocket_alt.py* module, with the same An unsupported *microsoft_websocket_alt.py* module, with the same
interface, is also provided. This module uses the native WebSocket support interface, is also provided. This module uses the native WebSocket support
in MicroPython that powers the WebREPL, and may provide slightly better in MicroPython that powers the WebREPL, and may provide slightly better
performance for MicroPython low-end boards. This module is not compatible performance for MicroPython low-end boards. This module is not compatible
@@ -312,9 +276,9 @@ This extension has the same interface as the synchronous WebSocket extension,
but the ``receive()`` and ``send()`` methods are asynchronous. but the ``receive()`` and ``send()`` methods are asynchronous.
.. note:: .. note::
An unsupported *microdot_asgi_websocket.py* module, with the same An unsupported *microsoft_asgi_websocket.py* module, with the same
interface, is also provided. This module must be used instead of interface, is also provided. This module must be used instead of
*microdot_asyncio_websocket.py* when the ASGI support is used. The *microsoft_asyncio_websocket.py* when the ASGI support is used. The
`echo_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_asgi.py>`_ `echo_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_asgi.py>`_
example shows how to use this module. example shows how to use this module.
@@ -333,7 +297,7 @@ HTTPS Support
* - Examples * - Examples
- | `hello_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_tls.py>`_ - | `hello_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_tls.py>`_
| `hello_async_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_async_tls.py>`_ | `hello_asyncio_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_asyncio_tls.py>`_
The ``run()`` function accepts an optional ``ssl`` argument, through which an The ``run()`` function accepts an optional ``ssl`` argument, through which an
initialized ``SSLContext`` object can be passed. MicroPython does not currently initialized ``SSLContext`` object can be passed. MicroPython does not currently
@@ -459,7 +423,7 @@ Using a WSGI Web Server
- | A WSGI web server, such as `Gunicorn <https://gunicorn.org/>`_. - | A WSGI web server, such as `Gunicorn <https://gunicorn.org/>`_.
* - Examples * - Examples
- | `hello_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_wsgi.py>`_ - | `hello_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_wsgi.py>`_
The ``microdot_wsgi`` module provides an extended ``Microdot`` class that The ``microdot_wsgi`` module provides an extended ``Microdot`` class that
@@ -504,7 +468,7 @@ Using an ASGI Web Server
- | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_. - | An ASGI web server, such as `Uvicorn <https://uvicorn.org/>`_.
* - Examples * - Examples
- | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello_asgi.py>`_ - | `hello_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello_asgi.py>`_
The ``microdot_asgi`` module provides an extended ``Microdot`` class that The ``microdot_asgi`` module provides an extended ``Microdot`` class that
implements the ASGI protocol and can be used with a compliant ASGI server such implements the ASGI protocol and can be used with a compliant ASGI server such

View File

@@ -283,7 +283,7 @@ handled::
def start_timer(request): def start_timer(request):
request.g.start_time = time.time() request.g.start_time = time.time()
@app.after_request @ap.after_request
def end_timer(request, response): def end_timer(request, response):
duration = time.time() - request.g.start_time duration = time.time() - request.g.start_time
print(f'Request took {duration:0.2f} seconds') print(f'Request took {duration:0.2f} seconds')
@@ -293,12 +293,6 @@ The function can return a modified response object to replace the original. If
the function does not return a value, then the original response object is the function does not return a value, then the original response object is
used. used.
The after request handlers are only invoked for successful requests. The
:func:`after_error_request() <microdot.Microdot.after_error_request>`
decorator can be used to register a function that is called after an error
occurs. The function receives the request and the error response and is
expected to return an updated response object.
.. note:: .. note::
The :ref:`request.g <The "g" Object>` object is a special object that allows The :ref:`request.g <The "g" Object>` object is a special object that allows
the before and after request handlers, as well sa the route function to the before and after request handlers, as well sa the route function to
@@ -320,7 +314,7 @@ automatically handled by Microdot are:
While the above errors are fully complaint with the HTTP specification, the While the above errors are fully complaint with the HTTP specification, the
application might want to provide custom responses for them. The application might want to provide custom responses for them. The
:func:`errorhandler() <microdot.Microdot.errorhandler>` decorator registers :func:`errorhandler() <microdot.Microdot.errorhandler>` decorator registers
functions to respond to specific error codes. The following example shows a a functions to respond to specific error codes. The following example shows a
custom error handler for 404 errors:: custom error handler for 404 errors::
@app.errorhandler(404) @app.errorhandler(404)
@@ -328,18 +322,14 @@ custom error handler for 404 errors::
return {'error': 'resource not found'}, 404 return {'error': 'resource not found'}, 404
The ``errorhandler()`` decorator has a second form, in which it takes an The ``errorhandler()`` decorator has a second form, in which it takes an
exception class as an argument. Microdot will then invoke the handler when the exception class as an argument. Microdot will then invoke the handler when an
exception is an instance of the given class is raised. The next example exception of that class is raised. The next example provides a custom response
provides a custom response for division by zero errors:: for division by zero errors::
@app.errorhandler(ZeroDivisionError) @app.errorhandler(ZeroDivisionError)
def division_by_zero(request, exception): def division_by_zero(request):
return {'error': 'division by zero'}, 500 return {'error': 'division by zero'}, 500
When the raised exception class does not have an error handler defined, but
one or more of its base classes do, Microdot makes an attempt to invoke the
most specific handler.
Mounting a Sub-Application Mounting a Sub-Application
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -491,7 +481,7 @@ Accessing the Raw Request Body
For cases in which neither JSON nor form data is expected, the For cases in which neither JSON nor form data is expected, the
:attr:`body <microdot.Request.body>` request attribute returns the entire body :attr:`body <microdot.Request.body>` request attribute returns the entire body
of the request as a byte sequence. of the request as a byte sequence.
If the expected body is too large to fit in memory, the application can use the If the expected body is too large to fit in memory, the application can use the
:attr:`stream <microdot.Request.stream>` request attribute to read the body :attr:`stream <microdot.Request.stream>` request attribute to read the body
@@ -655,22 +645,13 @@ File Responses
The :func:`send_file <microdot.Response.send_file>` function builds a response The :func:`send_file <microdot.Response.send_file>` function builds a response
object for a file:: object for a file::
from microdot import send_file from microdot import send_file
@app.get('/') @app.get('/')
def index(request): def index(request):
return send_file('/static/index.html') return send_file('/static/index.html')
A suggested caching duration can be returned to the client in the ``max_age``
argument::
from microdot import send_file
@app.get('/')
def image(request):
return send_file('/static/image.jpg', max_age=3600) # in seconds
.. note:: .. note::
Unlike other web frameworks, Microdot does not automatically configure a Unlike other web frameworks, Microdot does not automatically configure a
route to serve static files. The following is an example route that can be route to serve static files. The following is an example route that can be
@@ -682,7 +663,7 @@ argument::
if '..' in path: if '..' in path:
# directory traversal is not allowed # directory traversal is not allowed
return 'Not found', 404 return 'Not found', 404
return send_file('static/' + path, max_age=86400) return send_file('static/' + path)
Streaming Responses Streaming Responses
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1,27 @@
from microdot import Microdot
from microdot_auth import BasicAuth
app = Microdot()
basic_auth = BasicAuth()
USERS = {
'susan': 'hello',
'david': 'bye',
}
@basic_auth.callback
def verify_password(request, username, password):
if username in USERS and USERS[username] == password:
request.g.user = username
return True
@app.route('/')
@basic_auth
def index(request):
return f'Hello, {request.g.user}!'
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,60 @@
from microdot import Microdot, redirect
from microdot_session import set_session_secret_key
from microdot_login import LoginAuth
app = Microdot()
set_session_secret_key('top-secret')
login_auth = LoginAuth()
USERS = {
'susan': 'hello',
'david': 'bye',
}
@login_auth.callback
def check_user(request, user_id):
request.g.user = user_id
return True
@app.route('/')
@login_auth
def index(request):
return f'''
<h1>Login Auth Example</h1>
<p>Hello, {request.g.user}!</p>
<form method="POST" action="/logout">
<button type="submit">Logout</button>
</form>
''', {'Content-Type': 'text/html'}
@app.route('/login', methods=['GET', 'POST'])
def login(request):
if request.method == 'GET':
return '''
<h1>Login Auth Example</h1>
<form method="POST">
<input name="username" placeholder="username">
<input name="password" type="password" placeholder="password">
<button type="submit">Login</button>
</form>
''', {'Content-Type': 'text/html'}
username = request.form['username']
password = request.form['password']
if USERS.get(username) == password:
login_auth.login_user(request, username)
return login_auth.redirect_to_next(request)
else:
return redirect('/login')
@app.post('/logout')
def logout(request):
login_auth.logout_user(request)
return redirect('/')
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -0,0 +1,27 @@
from microdot import Microdot
from microdot_auth import TokenAuth
app = Microdot()
token_auth = TokenAuth()
TOKENS = {
'hello': 'susan',
'bye': 'david',
}
@token_auth.callback
def verify_token(request, token):
if token in TOKENS:
request.g.user = TOKENS[token]
return True
@app.route('/')
@token_auth
def index(request):
return f'Hello, {request.g.user}!'
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -1,7 +1,7 @@
aiofiles==0.8.0 aiofiles==0.8.0
anyio==3.6.1 anyio==3.6.1
blinker==1.5 blinker==1.5
certifi==2022.12.7 certifi==2022.6.15
charset-normalizer==2.1.0 charset-normalizer==2.1.0
click==8.1.3 click==8.1.3
fastapi==0.79.0 fastapi==0.79.0
@@ -22,12 +22,12 @@ priority==2.0.0
psutil==5.9.1 psutil==5.9.1
pydantic==1.9.1 pydantic==1.9.1
quart==0.18.0 quart==0.18.0
requests==2.31.0 requests==2.28.1
sniffio==1.2.0 sniffio==1.2.0
starlette==0.27.0 starlette==0.19.1
toml==0.10.2 toml==0.10.2
typing_extensions==4.3.0 typing_extensions==4.3.0
urllib3==1.26.11 urllib3==1.26.11
uvicorn==0.18.2 uvicorn==0.18.2
Werkzeug==2.2.3 Werkzeug==2.2.1
wsproto==1.1.0 wsproto==1.1.0

View File

@@ -0,0 +1,14 @@
curl -X GET http://localhost:5000/ <-- microdot
{"ram": 8429568}%
curl -X GET http://localhost:5000/ <-- microdot_asyncio
{"ram": 12410880}%
curl -X GET http://localhost:8000/ <-- microdot_wsgi
{"ram": 9101312}%
curl -X GET http://localhost:8000/ <-- microdot_asgi
{"ram": 18620416}%
curl -X GET http://localhost:5000/ <-- flask app.run
{"ram":25460736}
curl -X GET http://localhost:5000/ <-- flask run
{"ram":26210304}
curl -X GET http://localhost:5000/ <-- quart run
{"ram":31748096}%

View File

@@ -1,7 +1,6 @@
import os import os
import subprocess import subprocess
import time import time
from timeit import timeit
import requests import requests
import psutil import psutil
import humanize import humanize
@@ -77,23 +76,19 @@ apps = [
for app, env, name in apps: for app, env, name in apps:
p = subprocess.Popen( p = subprocess.Popen(
app.split() if isinstance(app, str) else app, app.split() if isinstance(app, str) else app,
env={'PATH': os.environ['PATH'] + ':../../bin', **env}, env={'PATH': os.environ['PATH'], **env},
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
) )
time.sleep(1) time.sleep(1)
tm = 0
if not name.startswith('baseline'): if not name.startswith('baseline'):
def req(): r = requests.get('http://localhost:5000')
r = requests.get('http://localhost:5000') r.raise_for_status()
r.raise_for_status()
tm = timeit(req, number=1000)
proc = psutil.Process(p.pid) proc = psutil.Process(p.pid)
mem = proc.memory_info().rss mem = proc.memory_info().rss
for child in proc.children(recursive=True): for child in proc.children(recursive=True):
mem += child.memory_info().rss mem += child.memory_info().rss
bar = '*' * (mem // (1024 * 1024)) bar = '*' * (mem // (1024 * 1024))
print(f'{name:<28}{tm:10.2f}s {humanize.naturalsize(mem):>10} {bar}') print(f'{name:<28}{humanize.naturalsize(mem):>10} {bar}')
p.terminate() p.terminate()
time.sleep(1) time.sleep(1)

View File

@@ -1 +0,0 @@
This directory contains Cross-Origin Resource Sharing (CORS) examples.

View File

@@ -1,14 +0,0 @@
from microdot import Microdot
from microdot_cors import CORS
app = Microdot()
CORS(app, allowed_origins=['https://example.org'], allow_credentials=True)
@app.route('/')
def index(request):
return 'Hello World!'
if __name__ == '__main__':
app.run()

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot GPIO Example</title> <title>Microdot GPIO Example</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script> <script>
function getCookie(name) { function getCookie(name) {

View File

@@ -6,7 +6,6 @@ htmldoc = '''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Microdot Example Page</title> <title>Microdot Example Page</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<div> <div>

View File

@@ -6,7 +6,6 @@ htmldoc = '''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Microdot Example Page</title> <title>Microdot Example Page</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<div> <div>

View File

@@ -6,7 +6,6 @@ htmldoc = '''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Microdot Example Page</title> <title>Microdot Example Page</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<div> <div>

View File

@@ -10,7 +10,7 @@ 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 render_template('index.html', name=name) return render_template('index_jinja.html', name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -10,7 +10,7 @@ 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 render_template('index.html', name=name) return render_template('index_utemplate.html', name=name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -6,7 +6,6 @@ htmldoc = '''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Microdot Example Page</title> <title>Microdot Example Page</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<div> <div>

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot + Jinja example</title> <title>Microdot + Jinja example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot + Jinja example</h1> <h1>Microdot + Jinja example</h1>

View File

@@ -3,7 +3,6 @@
<html> <html>
<head> <head>
<title>Microdot + uTemplate example</title> <title>Microdot + uTemplate example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot + uTemplate example</h1> <h1>Microdot + uTemplate example</h1>

View File

@@ -6,7 +6,6 @@ BASE_TEMPLATE = '''<!doctype html>
<html> <html>
<head> <head>
<title>Microdot login example</title> <title>Microdot login example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot login example</h1> <h1>Microdot login example</h1>
@@ -18,7 +17,7 @@ LOGGED_OUT = '''<p>You are not logged in.</p>
<form method="POST"> <form method="POST">
<p> <p>
Username: Username:
<input name="username" autofocus /> <input type="text" name="username" autofocus />
</p> </p>
<input type="submit" value="Submit" /> <input type="submit" value="Submit" />
</form>''' </form>'''

View File

@@ -1,20 +0,0 @@
from microdot import Microdot, send_file
app = Microdot()
@app.route('/')
def index(request):
return send_file('gzstatic/index.html', compressed=True,
file_extension='.gz')
@app.route('/static/<path:path>')
def static(request, path):
if '..' in path:
# directory traversal is not allowed
return 'Not found', 404
return send_file('gzstatic/' + path, compressed=True, file_extension='.gz')
app.run(debug=True)

Binary file not shown.

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Static File Serving Demo</title> <title>Static File Serving Demo</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Static File Serving Demo</h1> <h1>Static File Serving Demo</h1>

View File

@@ -1,18 +0,0 @@
from microdot_asyncio import Microdot, send_file
app = Microdot()
@app.route('/')
async def index(request):
return send_file('static/index.html')
@app.route('/static/<path:path>')
async def static(request, path):
if '..' in path:
# directory traversal is not allowed
return 'Not found', 404
return send_file('static/' + path)
app.run(debug=True)

View File

@@ -19,7 +19,6 @@ def index(request):
<html> <html>
<head> <head>
<title>Microdot Video Streaming</title> <title>Microdot Video Streaming</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot Video Streaming</h1> <h1>Microdot Video Streaming</h1>

View File

@@ -21,7 +21,6 @@ def index(request):
<html> <html>
<head> <head>
<title>Microdot Video Streaming</title> <title>Microdot Video Streaming</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot Video Streaming</h1> <h1>Microdot Video Streaming</h1>

View File

@@ -1,19 +0,0 @@
from microdot import Microdot, Response
from microdot_jinja import render_template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
def index(req):
return render_template('page1.html', page='Page 1')
@app.route('/page2')
def page2(req):
return render_template('page2.html', page='Page 2')
if __name__ == '__main__':
app.run()

View File

@@ -1,48 +0,0 @@
<!--
This is based on the Bootstrap 5 starter template from the documentation:
https://getbootstrap.com/docs/5.0/getting-started/introduction/#starter-template
-->
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Microdot + Jinja + Bootstrap</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/">Microdot + Jinja + Bootstrap</a>
</div>
</nav>
<br>
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</symbol>
</svg>
<div class="alert alert-primary d-flex align-items-center" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
<div>This example demonstrates how to create an application that uses <a href="https://getbootstrap.com" class="alert-link">Bootstrap</a> styling. The page layout is defined in a base template that is inherited by several pages.</div>
</div>
{% block content %}{% endblock %}
</div>
<!-- Optional JavaScript; choose one of the two! -->
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
</body>
</html>

View File

@@ -1,6 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>This is {{ page }}</h2>
<p>Go to <a href="/page2">Page 2</a>.</p>
{% endblock %}

View File

@@ -1,6 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>This is {{ page }}</h2>
<p>Go back <a href="/">Page 1</a>.</p>
{% endblock %}

View File

@@ -1,19 +0,0 @@
from microdot import Microdot, Response
from microdot_utemplate import render_template
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
def index(req):
return render_template('page1.html', page='Page 1')
@app.route('/page2')
def page2(req):
return render_template('page2.html', page='Page 2')
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -1,14 +0,0 @@
</div>
<!-- Optional JavaScript; choose one of the two! -->
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
</body>
</html>

View File

@@ -1,33 +0,0 @@
<!--
This is based on the Bootstrap 5 starter template from the documentation:
https://getbootstrap.com/docs/5.0/getting-started/introduction/#starter-template
-->
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>Microdot + uTemplate + Bootstrap</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/">Microdot + uTemplate + Bootstrap</a>
</div>
</nav>
<br>
<div class="container">
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</symbol>
</svg>
<div class="alert alert-primary d-flex align-items-center" role="alert">
<svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
<div>This example demonstrates how to create an application that uses <a href="https://getbootstrap.com" class="alert-link">Bootstrap</a> styling. The page layout is defined in a base template that is inherited by several pages.</div>
</div>

View File

@@ -1,7 +0,0 @@
{% args page %}
{% include 'base_header.html' %}
<h2>This is {{ page }}</h2>
<p>Go to <a href="/page2">Page 2</a>.</p>
{% include 'base_footer.html' %}

View File

@@ -1,7 +0,0 @@
{% args page %}
{% include 'base_header.html' %}
<h2>This is {{ page }}</h2>
<p>Go back <a href="/">Page 1</a>.</p>
{% include 'base_footer.html' %}

View File

@@ -7,7 +7,6 @@ htmldoc = '''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Microdot Example Page</title> <title>Microdot Example Page</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<div> <div>

View File

@@ -8,7 +8,6 @@ htmldoc = '''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Microdot Example Page</title> <title>Microdot Example Page</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<div> <div>

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot TLS WebSocket Demo</title> <title>Microdot TLS WebSocket Demo</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot TLS WebSocket Demo</h1> <h1>Microdot TLS WebSocket Demo</h1>

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot Upload Example</title> <title>Microdot Upload Example</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot Upload Example</h1> <h1>Microdot Upload Example</h1>

View File

@@ -1,7 +1,6 @@
from microdot import Microdot, send_file, Request from microdot import Microdot, send_file
app = Microdot() app = Microdot()
Request.max_content_length = 1024 * 1024 # 1MB (change as needed)
@app.get('/') @app.get('/')
@@ -31,4 +30,4 @@ def upload(request):
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run()

View File

@@ -1,34 +0,0 @@
from microdot_asyncio import Microdot, send_file, Request
app = Microdot()
Request.max_content_length = 1024 * 1024 # 1MB (change as needed)
@app.get('/')
async def index(request):
return send_file('index.html')
@app.post('/upload')
async def upload(request):
# obtain the filename and size from request headers
filename = request.headers['Content-Disposition'].split(
'filename=')[1].strip('"')
size = int(request.headers['Content-Length'])
# sanitize the filename
filename = filename.replace('/', '_')
# write the file to the files directory in 1K chunks
with open('files/' + filename, 'wb') as f:
while size > 0:
chunk = await request.stream.read(min(size, 1024))
f.write(chunk)
size -= len(chunk)
print('Successfully saved file: ' + filename)
return ''
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Microdot WebSocket Demo</title> <title>Microdot WebSocket Demo</title>
<meta charset="UTF-8">
</head> </head>
<body> <body>
<h1>Microdot WebSocket Demo</h1> <h1>Microdot WebSocket Demo</h1>

View File

@@ -41,7 +41,7 @@ class SingletonGenerator:
def __next__(self): def __next__(self):
if self.state is not None: if self.state is not None:
_task_queue.push(cur_task, self.state) _task_queue.push_sorted(cur_task, self.state)
self.state = None self.state = None
return None return None
else: else:
@@ -115,11 +115,11 @@ class IOQueue:
# print('poll', s, sm, ev) # print('poll', s, sm, ev)
if ev & ~select.POLLOUT and sm[0] is not None: if ev & ~select.POLLOUT and sm[0] is not None:
# POLLIN or error # POLLIN or error
_task_queue.push(sm[0]) _task_queue.push_head(sm[0])
sm[0] = None sm[0] = None
if ev & ~select.POLLIN and sm[1] is not None: if ev & ~select.POLLIN and sm[1] is not None:
# POLLOUT or error # POLLOUT or error
_task_queue.push(sm[1]) _task_queue.push_head(sm[1])
sm[1] = None sm[1] = None
if sm[0] is None and sm[1] is None: if sm[0] is None and sm[1] is None:
self._dequeue(s) self._dequeue(s)
@@ -142,7 +142,7 @@ def create_task(coro):
if not hasattr(coro, "send"): if not hasattr(coro, "send"):
raise TypeError("coroutine expected") raise TypeError("coroutine expected")
t = Task(coro, globals()) t = Task(coro, globals())
_task_queue.push(t) _task_queue.push_head(t)
return t return t
@@ -167,7 +167,7 @@ def run_until_complete(main_task=None):
_io_queue.wait_io_event(dt) _io_queue.wait_io_event(dt)
# Get next task to run and continue it # Get next task to run and continue it
t = _task_queue.pop() t = _task_queue.pop_head()
cur_task = t cur_task = t
try: try:
# Continue running the coroutine, it's responsible for rescheduling itself # Continue running the coroutine, it's responsible for rescheduling itself
@@ -175,10 +175,6 @@ def run_until_complete(main_task=None):
if not exc: if not exc:
t.coro.send(None) t.coro.send(None)
else: else:
# If the task is finished and on the run queue and gets here, then it
# had an exception and was not await'ed on. Throwing into it now will
# raise StopIteration and the code below will catch this and run the
# call_exception_handler function.
t.data = None t.data = None
t.coro.throw(exc) t.coro.throw(exc)
except excs_all as er: except excs_all as er:
@@ -189,37 +185,22 @@ def run_until_complete(main_task=None):
if isinstance(er, StopIteration): if isinstance(er, StopIteration):
return er.value return er.value
raise er raise er
if t.state: # Schedule any other tasks waiting on the completion of this task
# Task was running but is now finished. waiting = False
waiting = False if hasattr(t, "waiting"):
if t.state is True: while t.waiting.peek():
# "None" indicates that the task is complete and not await'ed on (yet). _task_queue.push_head(t.waiting.pop_head())
t.state = None
elif callable(t.state):
# The task has a callback registered to be called on completion.
t.state(t, er)
t.state = False
waiting = True waiting = True
else: t.waiting = None # Free waiting queue head
# Schedule any other tasks waiting on the completion of this task. if not waiting and not isinstance(er, excs_stop):
while t.state.peek(): # An exception ended this detached task, so queue it for later
_task_queue.push(t.state.pop()) # execution to handle the uncaught exception if no other task retrieves
waiting = True # the exception in the meantime (this is handled by Task.throw).
# "False" indicates that the task is complete and has been await'ed on. _task_queue.push_head(t)
t.state = False # Indicate task is done by setting coro to the task object itself
if not waiting and not isinstance(er, excs_stop): t.coro = t
# An exception ended this detached task, so queue it for later # Save return value of coro to pass up to caller
# execution to handle the uncaught exception if no other task retrieves t.data = er
# the exception in the meantime (this is handled by Task.throw).
_task_queue.push(t)
# Save return value of coro to pass up to caller.
t.data = er
elif t.state is None:
# Task is already finished and nothing await'ed on the task,
# so call the exception handler.
_exc_context["exception"] = exc
_exc_context["future"] = t
Loop.call_exception_handler(_exc_context)
# Create a new task from a coroutine and run it until it finishes # Create a new task from a coroutine and run it until it finishes
@@ -256,7 +237,7 @@ class Loop:
def stop(): def stop():
global _stop_task global _stop_task
if _stop_task is not None: if _stop_task is not None:
_task_queue.push(_stop_task) _task_queue.push_head(_stop_task)
# If stop() is called again, do nothing # If stop() is called again, do nothing
_stop_task = None _stop_task = None

View File

@@ -17,7 +17,7 @@ class Event:
# Note: This must not be called from anything except the thread running # Note: This must not be called from anything except the thread running
# the asyncio loop (i.e. neither hard or soft IRQ, or a different thread). # the asyncio loop (i.e. neither hard or soft IRQ, or a different thread).
while self.waiting.peek(): while self.waiting.peek():
core._task_queue.push(self.waiting.pop()) core._task_queue.push_head(self.waiting.pop_head())
self.state = True self.state = True
def clear(self): def clear(self):
@@ -26,7 +26,7 @@ class Event:
async def wait(self): async def wait(self):
if not self.state: if not self.state:
# Event not set, put the calling task on the event's waiting queue # Event not set, put the calling task on the event's waiting queue
self.waiting.push(core.cur_task) self.waiting.push_head(core.cur_task)
# Set calling task's data to the event's queue so it can be removed if needed # Set calling task's data to the event's queue so it can be removed if needed
core.cur_task.data = self.waiting core.cur_task.data = self.waiting
yield yield
@@ -36,29 +36,27 @@ class Event:
# MicroPython-extension: This can be set from outside the asyncio event loop, # MicroPython-extension: This can be set from outside the asyncio event loop,
# such as other threads, IRQs or scheduler context. Implementation is a stream # such as other threads, IRQs or scheduler context. Implementation is a stream
# that asyncio will poll until a flag is set. # that asyncio will poll until a flag is set.
# Note: Unlike Event, this is self-clearing after a wait(). # Note: Unlike Event, this is self-clearing.
try: try:
import uio import uio
class ThreadSafeFlag(uio.IOBase): class ThreadSafeFlag(uio.IOBase):
def __init__(self): def __init__(self):
self.state = 0 self._flag = 0
def ioctl(self, req, flags): def ioctl(self, req, flags):
if req == 3: # MP_STREAM_POLL if req == 3: # MP_STREAM_POLL
return self.state * flags return self._flag * flags
return None return None
def set(self): def set(self):
self.state = 1 self._flag = 1
def clear(self):
self.state = 0
async def wait(self): async def wait(self):
if not self.state: if not self._flag:
yield core._io_queue.queue_read(self) yield core._io_queue.queue_read(self)
self.state = 0 self._flag = 0
except ImportError: except ImportError:
pass pass

View File

@@ -1,51 +1,49 @@
# MicroPython uasyncio module # MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2022 Damien P. George # MIT license; Copyright (c) 2019-2020 Damien P. George
from . import core from . import core
def _run(waiter, aw):
try:
result = await aw
status = True
except BaseException as er:
result = None
status = er
if waiter.data is None:
# The waiter is still waiting, cancel it.
if waiter.cancel():
# Waiter was cancelled by us, change its CancelledError to an instance of
# CancelledError that contains the status and result of waiting on aw.
# If the wait_for task subsequently gets cancelled externally then this
# instance will be reset to a CancelledError instance without arguments.
waiter.data = core.CancelledError(status, result)
async def wait_for(aw, timeout, sleep=core.sleep): async def wait_for(aw, timeout, sleep=core.sleep):
aw = core._promote_to_task(aw) aw = core._promote_to_task(aw)
if timeout is None: if timeout is None:
return await aw return await aw
def runner(waiter, aw):
nonlocal status, result
try:
result = await aw
s = True
except BaseException as er:
s = er
if status is None:
# The waiter is still waiting, set status for it and cancel it.
status = s
waiter.cancel()
# Run aw in a separate runner task that manages its exceptions. # Run aw in a separate runner task that manages its exceptions.
runner_task = core.create_task(_run(core.cur_task, aw)) status = None
result = None
runner_task = core.create_task(runner(core.cur_task, aw))
try: try:
# Wait for the timeout to elapse. # Wait for the timeout to elapse.
await sleep(timeout) await sleep(timeout)
except core.CancelledError as er: except core.CancelledError as er:
status = er.value if status is True:
if status is None: # aw completed successfully and cancelled the sleep, so return aw's result.
return result
elif status is None:
# This wait_for was cancelled externally, so cancel aw and re-raise. # This wait_for was cancelled externally, so cancel aw and re-raise.
status = True
runner_task.cancel() runner_task.cancel()
raise er raise er
elif status is True:
# aw completed successfully and cancelled the sleep, so return aw's result.
return er.args[1]
else: else:
# aw raised an exception, propagate it out to the caller. # aw raised an exception, propagate it out to the caller.
raise status raise status
# The sleep finished before aw, so cancel aw and raise TimeoutError. # The sleep finished before aw, so cancel aw and raise TimeoutError.
status = True
runner_task.cancel() runner_task.cancel()
await runner_task await runner_task
raise core.TimeoutError raise core.TimeoutError
@@ -55,75 +53,22 @@ def wait_for_ms(aw, timeout):
return wait_for(aw, timeout, core.sleep_ms) return wait_for(aw, timeout, core.sleep_ms)
class _Remove:
@staticmethod
def remove(t):
pass
async def gather(*aws, return_exceptions=False): async def gather(*aws, return_exceptions=False):
if not aws:
return []
def done(t, er):
# Sub-task "t" has finished, with exception "er".
nonlocal state
if gather_task.data is not _Remove:
# The main gather task has already been scheduled, so do nothing.
# This happens if another sub-task already raised an exception and
# woke the main gather task (via this done function), or if the main
# gather task was cancelled externally.
return
elif not return_exceptions and not isinstance(er, StopIteration):
# A sub-task raised an exception, indicate that to the gather task.
state = er
else:
state -= 1
if state:
# Still some sub-tasks running.
return
# Gather waiting is done, schedule the main gather task.
core._task_queue.push(gather_task)
ts = [core._promote_to_task(aw) for aw in aws] ts = [core._promote_to_task(aw) for aw in aws]
for i in range(len(ts)): for i in range(len(ts)):
if ts[i].state is not True: try:
# Task is not running, gather not currently supported for this case. # TODO handle cancel of gather itself
raise RuntimeError("can't gather") # if ts[i].coro:
# Register the callback to call when the task is done. # iter(ts[i]).waiting.push_head(cur_task)
ts[i].state = done # try:
# yield
# Set the state for execution of the gather. # except CancelledError as er:
gather_task = core.cur_task # # cancel all waiting tasks
state = len(ts) # raise er
cancel_all = False ts[i] = await ts[i]
except Exception as er:
# Wait for the a sub-task to need attention. if return_exceptions:
gather_task.data = _Remove ts[i] = er
try: else:
yield raise er
except core.CancelledError as er:
cancel_all = True
state = er
# Clean up tasks.
for i in range(len(ts)):
if ts[i].state is done:
# Sub-task is still running, deregister the callback and cancel if needed.
ts[i].state = True
if cancel_all:
ts[i].cancel()
elif isinstance(ts[i].data, StopIteration):
# Sub-task ran to completion, get its return value.
ts[i] = ts[i].data.value
else:
# Sub-task had an exception with return_exceptions==True, so get its exception.
ts[i] = ts[i].data
# Either this gather was cancelled, or one of the sub-tasks raised an exception with
# return_exceptions==False, so reraise the exception here.
if state is not 0:
raise state
# Return the list of return values of each sub-task.
return ts return ts

View File

@@ -22,8 +22,8 @@ class Lock:
raise RuntimeError("Lock not acquired") raise RuntimeError("Lock not acquired")
if self.waiting.peek(): if self.waiting.peek():
# Task(s) waiting on lock, schedule next Task # Task(s) waiting on lock, schedule next Task
self.state = self.waiting.pop() self.state = self.waiting.pop_head()
core._task_queue.push(self.state) core._task_queue.push_head(self.state)
else: else:
# No Task waiting so unlock # No Task waiting so unlock
self.state = 0 self.state = 0
@@ -31,7 +31,7 @@ class Lock:
async def acquire(self): async def acquire(self):
if self.state != 0: if self.state != 0:
# Lock unavailable, put the calling Task on the waiting queue # Lock unavailable, put the calling Task on the waiting queue
self.waiting.push(core.cur_task) self.waiting.push_head(core.cur_task)
# Set calling task's data to the lock's queue so it can be removed if needed # Set calling task's data to the lock's queue so it can be removed if needed
core.cur_task.data = self.waiting core.cur_task.data = self.waiting
try: try:

View File

@@ -1,15 +1,13 @@
# This list of package files doesn't include task.py because that's provided # This list of frozen files doesn't include task.py because that's provided by the C module.
# by the C module. freeze(
package( "..",
"uasyncio",
( (
"__init__.py", "uasyncio/__init__.py",
"core.py", "uasyncio/core.py",
"event.py", "uasyncio/event.py",
"funcs.py", "uasyncio/funcs.py",
"lock.py", "uasyncio/lock.py",
"stream.py", "uasyncio/stream.py",
), ),
base_path="..",
opt=3, opt=3,
) )

View File

@@ -26,21 +26,9 @@ class Stream:
# TODO yield? # TODO yield?
self.s.close() self.s.close()
async def read(self, n=-1): async def read(self, n):
r = b""
while True:
yield core._io_queue.queue_read(self.s)
r2 = self.s.read(n)
if r2 is not None:
if n >= 0:
return r2
if not len(r2):
return r
r += r2
async def readinto(self, buf):
yield core._io_queue.queue_read(self.s) yield core._io_queue.queue_read(self.s)
return self.s.readinto(buf) return self.s.read(n)
async def readexactly(self, n): async def readexactly(self, n):
r = b"" r = b""
@@ -64,19 +52,9 @@ class Stream:
return l return l
def write(self, buf): def write(self, buf):
if not self.out_buf:
# Try to write immediately to the underlying stream.
ret = self.s.write(buf)
if ret == len(buf):
return
if ret is not None:
buf = buf[ret:]
self.out_buf += buf self.out_buf += buf
async def drain(self): async def drain(self):
if not self.out_buf:
# Drain must always yield, so a tight loop of write+drain can't block the scheduler.
return await core.sleep_ms(0)
mv = memoryview(self.out_buf) mv = memoryview(self.out_buf)
off = 0 off = 0
while off < len(mv): while off < len(mv):
@@ -97,8 +75,8 @@ async def open_connection(host, port):
from uerrno import EINPROGRESS from uerrno import EINPROGRESS
import usocket as socket import usocket as socket
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] # TODO this is blocking! ai = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket(ai[0], ai[1], ai[2]) s = socket.socket()
s.setblocking(False) s.setblocking(False)
ss = Stream(s) ss = Stream(s)
try: try:
@@ -125,7 +103,16 @@ class Server:
async def wait_closed(self): async def wait_closed(self):
await self.task await self.task
async def _serve(self, s, cb): async def _serve(self, cb, host, port, backlog):
import usocket as socket
ai = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket()
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(ai[-1])
s.listen(backlog)
self.task = core.cur_task
# Accept incoming connections # Accept incoming connections
while True: while True:
try: try:
@@ -147,20 +134,9 @@ class Server:
# Helper function to start a TCP stream server, running as a new task # Helper function to start a TCP stream server, running as a new task
# TODO could use an accept-callback on socket read activity instead of creating a task # TODO could use an accept-callback on socket read activity instead of creating a task
async def start_server(cb, host, port, backlog=5): async def start_server(cb, host, port, backlog=5):
import usocket as socket s = Server()
core.create_task(s._serve(cb, host, port, backlog))
# Create and bind server socket. return s
host = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
s = socket.socket()
s.setblocking(False)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(host[-1])
s.listen(backlog)
# Create and return server object and task.
srv = Server()
srv.task = core.create_task(srv._serve(s, cb))
return srv
################################################################################ ################################################################################

View File

@@ -99,18 +99,19 @@ class TaskQueue:
def peek(self): def peek(self):
return self.heap return self.heap
def push(self, v, key=None): def push_sorted(self, v, key):
assert v.ph_child is None
assert v.ph_next is None
v.data = None v.data = None
v.ph_key = key if key is not None else core.ticks() v.ph_key = key
v.ph_child = None
v.ph_next = None
self.heap = ph_meld(v, self.heap) self.heap = ph_meld(v, self.heap)
def pop(self): def push_head(self, v):
self.push_sorted(v, core.ticks())
def pop_head(self):
v = self.heap v = self.heap
assert v.ph_next is None self.heap = ph_pairing(self.heap.ph_child)
self.heap = ph_pairing(v.ph_child)
v.ph_child = None
return v return v
def remove(self, v): def remove(self, v):
@@ -122,7 +123,6 @@ class Task:
def __init__(self, coro, globals=None): def __init__(self, coro, globals=None):
self.coro = coro # Coroutine of this Task self.coro = coro # Coroutine of this Task
self.data = None # General data for queue it is waiting on self.data = None # General data for queue it is waiting on
self.state = True # None, False, True, a callable, or a TaskQueue instance
self.ph_key = 0 # Pairing heap self.ph_key = 0 # Pairing heap
self.ph_child = None # Paring heap self.ph_child = None # Paring heap
self.ph_child_last = None # Paring heap self.ph_child_last = None # Paring heap
@@ -130,33 +130,30 @@ class Task:
self.ph_rightmost_parent = None # Paring heap self.ph_rightmost_parent = None # Paring heap
def __iter__(self): def __iter__(self):
if not self.state: if self.coro is self:
# Task finished, signal that is has been await'ed on. # Signal that the completed-task has been await'ed on.
self.state = False self.waiting = None
elif self.state is True: elif not hasattr(self, "waiting"):
# Allocated head of linked list of Tasks waiting on completion of this task. # Lazily allocated head of linked list of Tasks waiting on completion of this task.
self.state = TaskQueue() self.waiting = TaskQueue()
elif type(self.state) is not TaskQueue:
# Task has state used for another purpose, so can't also wait on it.
raise RuntimeError("can't wait")
return self return self
def __next__(self): def __next__(self):
if not self.state: if self.coro is self:
# Task finished, raise return value to caller so it can continue. # Task finished, raise return value to caller so it can continue.
raise self.data raise self.data
else: else:
# Put calling task on waiting queue. # Put calling task on waiting queue.
self.state.push(core.cur_task) self.waiting.push_head(core.cur_task)
# Set calling task's data to this task that it waits on, to double-link it. # Set calling task's data to this task that it waits on, to double-link it.
core.cur_task.data = self core.cur_task.data = self
def done(self): def done(self):
return not self.state return self.coro is self
def cancel(self): def cancel(self):
# Check if task is already finished. # Check if task is already finished.
if not self.state: if self.coro is self:
return False return False
# Can't cancel self (not supported yet). # Can't cancel self (not supported yet).
if self is core.cur_task: if self is core.cur_task:
@@ -168,10 +165,20 @@ class Task:
if hasattr(self.data, "remove"): if hasattr(self.data, "remove"):
# Not on the main running queue, remove the task from the queue it's on. # Not on the main running queue, remove the task from the queue it's on.
self.data.remove(self) self.data.remove(self)
core._task_queue.push(self) core._task_queue.push_head(self)
elif core.ticks_diff(self.ph_key, core.ticks()) > 0: elif core.ticks_diff(self.ph_key, core.ticks()) > 0:
# On the main running queue but scheduled in the future, so bring it forward to now. # On the main running queue but scheduled in the future, so bring it forward to now.
core._task_queue.remove(self) core._task_queue.remove(self)
core._task_queue.push(self) core._task_queue.push_head(self)
self.data = core.CancelledError self.data = core.CancelledError
return True return True
def throw(self, value):
# This task raised an exception which was uncaught; handle that now.
# Set the data because it was cleared by the main scheduling loop.
self.data = value
if not hasattr(self, "waiting"):
# Nothing await'ed on the task so call the exception handler.
core._exc_context["exception"] = value
core._exc_context["future"] = self
core.Loop.call_exception_handler(core._exc_context)

View File

@@ -1,18 +1,12 @@
import io
import os
import sys import sys
try:
import traceback
except ImportError:
traceback = None
class SkipTest(Exception): class SkipTest(Exception):
pass pass
class AssertRaisesContext: class AssertRaisesContext:
def __init__(self, exc): def __init__(self, exc):
self.expected = exc self.expected = exc
@@ -20,98 +14,29 @@ class AssertRaisesContext:
return self return self
def __exit__(self, exc_type, exc_value, tb): def __exit__(self, exc_type, exc_value, tb):
self.exception = exc_value
if exc_type is None: if exc_type is None:
assert False, "%r not raised" % self.expected assert False, "%r not raised" % self.expected
if issubclass(exc_type, self.expected): if issubclass(exc_type, self.expected):
# store exception for later retrieval
self.exception = exc_value
return True return True
return False return False
# These are used to provide required context to things like subTest
__current_test__ = None
__test_result__ = None
class SubtestContext:
def __init__(self, msg=None, params=None):
self.msg = msg
self.params = params
def __enter__(self):
pass
def __exit__(self, *exc_info):
if exc_info[0] is not None:
# Exception raised
global __test_result__, __current_test__
test_details = __current_test__
if self.msg:
test_details += (f" [{self.msg}]",)
if self.params:
detail = ", ".join(f"{k}={v}" for k, v in self.params.items())
test_details += (f" ({detail})",)
_handle_test_exception(test_details, __test_result__, exc_info, False)
# Suppress the exception as we've captured it above
return True
class NullContext:
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, traceback):
pass
class TestCase: class TestCase:
def __init__(self):
pass
def addCleanup(self, func, *args, **kwargs): def fail(self, msg=''):
if not hasattr(self, "_cleanups"):
self._cleanups = []
self._cleanups.append((func, args, kwargs))
def doCleanups(self):
if hasattr(self, "_cleanups"):
while self._cleanups:
func, args, kwargs = self._cleanups.pop()
func(*args, **kwargs)
def subTest(self, msg=None, **params):
return SubtestContext(msg=msg, params=params)
def skipTest(self, reason):
raise SkipTest(reason)
def fail(self, msg=""):
assert False, msg assert False, msg
def assertEqual(self, x, y, msg=""): def assertEqual(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r vs (expected) %r" % (x, y) msg = "%r vs (expected) %r" % (x, y)
assert x == y, msg assert x == y, msg
def assertNotEqual(self, x, y, msg=""): def assertNotEqual(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r not expected to be equal %r" % (x, y) msg = "%r not expected to be equal %r" % (x, y)
assert x != y, msg assert x != y, msg
def assertLessEqual(self, x, y, msg=None): def assertAlmostEqual(self, x, y, places=None, msg='', delta=None):
if msg is None:
msg = "%r is expected to be <= %r" % (x, y)
assert x <= y, msg
def assertGreaterEqual(self, x, y, msg=None):
if msg is None:
msg = "%r is expected to be >= %r" % (x, y)
assert x >= y, msg
def assertAlmostEqual(self, x, y, places=None, msg="", delta=None):
if x == y: if x == y:
return return
if delta is not None and places is not None: if delta is not None and places is not None:
@@ -121,18 +46,18 @@ class TestCase:
if abs(x - y) <= delta: if abs(x - y) <= delta:
return return
if not msg: if not msg:
msg = "%r != %r within %r delta" % (x, y, delta) msg = '%r != %r within %r delta' % (x, y, delta)
else: else:
if places is None: if places is None:
places = 7 places = 7
if round(abs(y - x), places) == 0: if round(abs(y-x), places) == 0:
return return
if not msg: if not msg:
msg = "%r != %r within %r places" % (x, y, places) msg = '%r != %r within %r places' % (x, y, places)
assert False, msg assert False, msg
def assertNotAlmostEqual(self, x, y, places=None, msg="", delta=None): def assertNotAlmostEqual(self, x, y, places=None, msg='', delta=None):
if delta is not None and places is not None: if delta is not None and places is not None:
raise TypeError("specify delta or places not both") raise TypeError("specify delta or places not both")
@@ -140,53 +65,53 @@ class TestCase:
if not (x == y) and abs(x - y) > delta: if not (x == y) and abs(x - y) > delta:
return return
if not msg: if not msg:
msg = "%r == %r within %r delta" % (x, y, delta) msg = '%r == %r within %r delta' % (x, y, delta)
else: else:
if places is None: if places is None:
places = 7 places = 7
if not (x == y) and round(abs(y - x), places) != 0: if not (x == y) and round(abs(y-x), places) != 0:
return return
if not msg: if not msg:
msg = "%r == %r within %r places" % (x, y, places) msg = '%r == %r within %r places' % (x, y, places)
assert False, msg assert False, msg
def assertIs(self, x, y, msg=""): def assertIs(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r is not %r" % (x, y) msg = "%r is not %r" % (x, y)
assert x is y, msg assert x is y, msg
def assertIsNot(self, x, y, msg=""): def assertIsNot(self, x, y, msg=''):
if not msg: if not msg:
msg = "%r is %r" % (x, y) msg = "%r is %r" % (x, y)
assert x is not y, msg assert x is not y, msg
def assertIsNone(self, x, msg=""): def assertIsNone(self, x, msg=''):
if not msg: if not msg:
msg = "%r is not None" % x msg = "%r is not None" % x
assert x is None, msg assert x is None, msg
def assertIsNotNone(self, x, msg=""): def assertIsNotNone(self, x, msg=''):
if not msg: if not msg:
msg = "%r is None" % x msg = "%r is None" % x
assert x is not None, msg assert x is not None, msg
def assertTrue(self, x, msg=""): def assertTrue(self, x, msg=''):
if not msg: if not msg:
msg = "Expected %r to be True" % x msg = "Expected %r to be True" % x
assert x, msg assert x, msg
def assertFalse(self, x, msg=""): def assertFalse(self, x, msg=''):
if not msg: if not msg:
msg = "Expected %r to be False" % x msg = "Expected %r to be False" % x
assert not x, msg assert not x, msg
def assertIn(self, x, y, msg=""): def assertIn(self, x, y, msg=''):
if not msg: if not msg:
msg = "Expected %r to be in %r" % (x, y) msg = "Expected %r to be in %r" % (x, y)
assert x in y, msg assert x in y, msg
def assertIsInstance(self, x, y, msg=""): def assertIsInstance(self, x, y, msg=''):
assert isinstance(x, y), msg assert isinstance(x, y), msg
def assertRaises(self, exc, func=None, *args, **kwargs): def assertRaises(self, exc, func=None, *args, **kwargs):
@@ -195,15 +120,12 @@ class TestCase:
try: try:
func(*args, **kwargs) func(*args, **kwargs)
assert False, "%r not raised" % exc
except Exception as e: except Exception as e:
if isinstance(e, exc): if isinstance(e, exc):
return return
raise raise
assert False, "%r not raised" % exc
def assertWarns(self, warn):
return NullContext()
def skip(msg): def skip(msg):
@@ -211,252 +133,92 @@ def skip(msg):
# We just replace original fun with _inner # We just replace original fun with _inner
def _inner(self): def _inner(self):
raise SkipTest(msg) raise SkipTest(msg)
return _inner return _inner
return _decor return _decor
def skipIf(cond, msg): def skipIf(cond, msg):
if not cond: if not cond:
return lambda x: x return lambda x: x
return skip(msg) return skip(msg)
def skipUnless(cond, msg): def skipUnless(cond, msg):
if cond: if cond:
return lambda x: x return lambda x: x
return skip(msg) return skip(msg)
def expectedFailure(test):
def test_exp_fail(*args, **kwargs):
try:
test(*args, **kwargs)
except:
pass
else:
assert False, "unexpected success"
return test_exp_fail
class TestSuite: class TestSuite:
def __init__(self, name=""): def __init__(self):
self._tests = [] self.tests = []
self.name = name
def addTest(self, cls): def addTest(self, cls):
self._tests.append(cls) self.tests.append(cls)
def run(self, result):
for c in self._tests:
_run_suite(c, result, self.name)
return result
def _load_module(self, mod):
for tn in dir(mod):
c = getattr(mod, tn)
if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
self.addTest(c)
elif tn.startswith("test") and callable(c):
self.addTest(c)
class TestRunner: class TestRunner:
def run(self, suite: TestSuite): def run(self, suite):
res = TestResult() res = TestResult()
suite.run(res) for c in suite.tests:
run_class(c, res)
res.printErrors()
print("----------------------------------------------------------------------")
print("Ran %d tests\n" % res.testsRun) print("Ran %d tests\n" % res.testsRun)
if res.failuresNum > 0 or res.errorsNum > 0: if res.failuresNum > 0 or res.errorsNum > 0:
print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum)) print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum))
else: else:
msg = "OK" msg = "OK"
if res.skippedNum > 0: if res.skippedNum > 0:
msg += " (skipped=%d)" % res.skippedNum msg += " (%d skipped)" % res.skippedNum
print(msg) print(msg)
return res return res
TextTestRunner = TestRunner
class TestResult: class TestResult:
def __init__(self): def __init__(self):
self.errorsNum = 0 self.errorsNum = 0
self.failuresNum = 0 self.failuresNum = 0
self.skippedNum = 0 self.skippedNum = 0
self.testsRun = 0 self.testsRun = 0
self.errors = []
self.failures = []
self.skipped = []
self._newFailures = 0
def wasSuccessful(self): def wasSuccessful(self):
return self.errorsNum == 0 and self.failuresNum == 0 return self.errorsNum == 0 and self.failuresNum == 0
def printErrors(self): # TODO: Uncompliant
print() def run_class(c, test_result):
self.printErrorList(self.errors) o = c()
self.printErrorList(self.failures)
def printErrorList(self, lst):
sep = "----------------------------------------------------------------------"
for c, e in lst:
detail = " ".join((str(i) for i in c))
print("======================================================================")
print(f"FAIL: {detail}")
print(sep)
print(e)
def __repr__(self):
# Format is compatible with CPython.
return "<unittest.result.TestResult run=%d errors=%d failures=%d>" % (
self.testsRun,
self.errorsNum,
self.failuresNum,
)
def __add__(self, other):
self.errorsNum += other.errorsNum
self.failuresNum += other.failuresNum
self.skippedNum += other.skippedNum
self.testsRun += other.testsRun
self.errors.extend(other.errors)
self.failures.extend(other.failures)
self.skipped.extend(other.skipped)
return self
def _capture_exc(exc, exc_traceback):
buf = io.StringIO()
if hasattr(sys, "print_exception"):
sys.print_exception(exc, buf)
elif traceback is not None:
traceback.print_exception(None, exc, exc_traceback, file=buf)
return buf.getvalue()
def _handle_test_exception(
current_test: tuple, test_result: TestResult, exc_info: tuple, verbose=True
):
exc = exc_info[1]
traceback = exc_info[2]
ex_str = _capture_exc(exc, traceback)
if isinstance(exc, AssertionError):
test_result.failuresNum += 1
test_result.failures.append((current_test, ex_str))
if verbose:
print(" FAIL")
else:
test_result.errorsNum += 1
test_result.errors.append((current_test, ex_str))
if verbose:
print(" ERROR")
test_result._newFailures += 1
def _run_suite(c, test_result: TestResult, suite_name=""):
if isinstance(c, TestSuite):
c.run(test_result)
return
if isinstance(c, type):
o = c()
else:
o = c
set_up_class = getattr(o, "setUpClass", lambda: None)
tear_down_class = getattr(o, "tearDownClass", lambda: None)
set_up = getattr(o, "setUp", lambda: None) set_up = getattr(o, "setUp", lambda: None)
tear_down = getattr(o, "tearDown", lambda: None) tear_down = getattr(o, "tearDown", lambda: None)
exceptions = [] for name in dir(o):
try: if name.startswith("test"):
suite_name += "." + c.__qualname__ print("%s (%s) ..." % (name, c.__qualname__), end="")
except AttributeError: m = getattr(o, name)
pass set_up()
def run_one(test_function):
global __test_result__, __current_test__
print("%s (%s) ..." % (name, suite_name), end="")
set_up()
__test_result__ = test_result
test_container = f"({suite_name})"
__current_test__ = (name, test_container)
try:
test_result._newFailures = 0
test_result.testsRun += 1
test_function()
# No exception occurred, test passed
if test_result._newFailures:
print(" FAIL")
else:
print(" ok")
except SkipTest as e:
reason = e.args[0]
print(" skipped:", reason)
test_result.skippedNum += 1
test_result.skipped.append((name, c, reason))
except Exception as ex:
_handle_test_exception(
current_test=(name, c), test_result=test_result, exc_info=(type(ex), ex, None)
)
# Uncomment to investigate failure in detail
# raise
finally:
__test_result__ = None
__current_test__ = None
tear_down()
try: try:
o.doCleanups() test_result.testsRun += 1
except AttributeError: m()
pass print(" ok")
except SkipTest as e:
set_up_class() print(" skipped:", e.args[0])
try: test_result.skippedNum += 1
if hasattr(o, "runTest"): except:
name = str(o) print(" FAIL")
run_one(o.runTest) test_result.failuresNum += 1
return # Uncomment to investigate failure in detail
#raise
for name in dir(o): continue
if name.startswith("test"): finally:
m = getattr(o, name) tear_down()
if not callable(m):
continue
run_one(m)
if callable(o):
name = o.__name__
run_one(o)
finally:
tear_down_class()
return exceptions
# This supports either: def main(module="__main__"):
# def test_cases(m):
# >>> import mytest for tn in dir(m):
# >>> unitttest.main(mytest) c = getattr(m, tn)
# if isinstance(c, object) and isinstance(c, type) and issubclass(c, TestCase):
# >>> unittest.main("mytest") yield c
#
# Or, a script that ends with:
# if __name__ == "__main__":
# unittest.main()
# e.g. run via `mpremote run mytest.py`
def main(module="__main__", testRunner=None):
if testRunner is None:
testRunner = TestRunner()
elif isinstance(testRunner, type):
testRunner = testRunner()
if isinstance(module, str): m = __import__(module)
module = __import__(module) suite = TestSuite()
suite = TestSuite(module.__name__) for c in test_cases(m):
suite._load_module(module) suite.addTest(c)
return testRunner.run(suite) runner = TestRunner()
result = runner.run(suite)
# Terminate with non zero return code in case of failures
sys.exit(result.failuresNum > 0)

View File

@@ -6,5 +6,4 @@ sys.path.insert(3, 'libs/micropython')
import unittest import unittest
if not unittest.main('tests').wasSuccessful(): unittest.main('tests')
sys.exit(1)

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = microdot name = microdot
version = 1.3.2 version = 1.1.2.dev0
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
@@ -28,6 +28,8 @@ py_modules =
microdot_utemplate microdot_utemplate
microdot_jinja microdot_jinja
microdot_session microdot_session
microdot_auth
microdot_login
microdot_websocket microdot_websocket
microdot_websocket_alt microdot_websocket_alt
microdot_asyncio_websocket microdot_asyncio_websocket

View File

@@ -43,13 +43,11 @@ try:
except ImportError: except ImportError:
import re import re
socket_timeout_error = OSError
try: try:
import usocket as socket import usocket as socket
except ImportError: except ImportError:
try: try:
import socket import socket
socket_timeout_error = socket.timeout
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
socket = None socket = None
@@ -94,9 +92,8 @@ def urldecode_bytes(s):
def urlencode(s): def urlencode(s):
return s.replace('+', '%2B').replace(' ', '+').replace( return s.replace(' ', '+').replace('%', '%25').replace('?', '%3F').replace(
'%', '%25').replace('?', '%3F').replace('#', '%23').replace( '#', '%23').replace('&', '%26').replace('+', '%2B')
'&', '%26').replace('=', '%3D')
class NoCaseDict(dict): class NoCaseDict(dict):
@@ -146,43 +143,6 @@ class NoCaseDict(dict):
kl = key.lower() kl = key.lower()
return super().get(self.keymap.get(kl, kl), default) return super().get(self.keymap.get(kl, kl), default)
def update(self, other_dict):
for key, value in other_dict.items():
self[key] = value
def mro(cls): # pragma: no cover
"""Return the method resolution order of a class.
This is a helper function that returns the method resolution order of a
class. It is used by Microdot to find the best error handler to invoke for
the raised exception.
In CPython, this function returns the ``__mro__`` attribute of the class.
In MicroPython, this function implements a recursive depth-first scanning
of the class hierarchy.
"""
if hasattr(cls, 'mro'):
return cls.__mro__
def _mro(cls):
m = [cls]
for base in cls.__bases__:
m += _mro(base)
return m
mro_list = _mro(cls)
# If a class appears multiple times (due to multiple inheritance) remove
# all but the last occurence. This matches the method resolution order
# of MicroPython, but not CPython.
mro_pruned = []
for i in range(len(mro_list)):
base = mro_list.pop(0)
if base not in mro_list:
mro_pruned.append(base)
return mro_pruned
class MultiDict(dict): class MultiDict(dict):
"""A subclass of dictionary that can hold multiple values for the same """A subclass of dictionary that can hold multiple values for the same
@@ -306,12 +266,6 @@ class Request():
#: Request.max_readline = 16 * 1024 # 16KB lines allowed #: Request.max_readline = 16 * 1024 # 16KB lines allowed
max_readline = 2 * 1024 max_readline = 2 * 1024
#: Specify a suggested read timeout to use when reading the request. Set to
#: 0 to disable the use of a timeout. This timeout should be considered a
#: suggestion only, as some platforms may not support it. The default is
#: 1 second.
socket_read_timeout = 1
class G: class G:
pass pass
@@ -405,11 +359,11 @@ class Request():
if len(urlencoded) > 0: if len(urlencoded) > 0:
if isinstance(urlencoded, str): if isinstance(urlencoded, str):
for k, v in [pair.split('=', 1) for k, v in [pair.split('=', 1)
for pair in urlencoded.split('&') if pair]: for pair in urlencoded.split('&')]:
data[urldecode_str(k)] = urldecode_str(v) data[urldecode_str(k)] = urldecode_str(v)
elif isinstance(urlencoded, bytes): # pragma: no branch elif isinstance(urlencoded, bytes): # pragma: no branch
for k, v in [pair.split(b'=', 1) for k, v in [pair.split(b'=', 1)
for pair in urlencoded.split(b'&') if pair]: for pair in urlencoded.split(b'&')]:
data[urldecode_bytes(k)] = urldecode_bytes(v) data[urldecode_bytes(k)] = urldecode_bytes(v)
return data return data
@@ -484,9 +438,6 @@ class Request():
return response return response
return 'Hello, World!' return 'Hello, World!'
Note that the function is not called if the request handler raises an
exception and an error response is returned instead.
""" """
self.after_request_handlers.append(f) self.after_request_handlers.append(f)
return f return f
@@ -530,10 +481,6 @@ class Response():
#: ``Content-Type`` header. #: ``Content-Type`` header.
default_content_type = 'text/plain' default_content_type = 'text/plain'
#: The default cache control max age used by :meth:`send_file`. A value
#: of ``None`` means that no ``Cache-Control`` header is added.
default_send_file_max_age = None
#: Special response used to signal that a response does not need to be #: Special response used to signal that a response does not need to be
#: written to the client. Used to exit WebSocket connections cleanly. #: written to the client. Used to exit WebSocket connections cleanly.
already_handled = None already_handled = None
@@ -553,7 +500,6 @@ class Response():
else: else:
# this applies to bytes, file-like objects or generators # this applies to bytes, file-like objects or generators
self.body = body self.body = body
self.is_head = False
def set_cookie(self, cookie, value, path=None, domain=None, expires=None, def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
max_age=None, secure=False, http_only=False): max_age=None, secure=False, http_only=False):
@@ -618,20 +564,19 @@ class Response():
stream.write(b'\r\n') stream.write(b'\r\n')
# body # body
if not self.is_head: can_flush = hasattr(stream, 'flush')
can_flush = hasattr(stream, 'flush') try:
try: for body in self.body_iter():
for body in self.body_iter(): if isinstance(body, str): # pragma: no cover
if isinstance(body, str): # pragma: no cover body = body.encode()
body = body.encode() stream.write(body)
stream.write(body) if can_flush: # pragma: no cover
if can_flush: # pragma: no cover stream.flush()
stream.flush() except OSError as exc: # pragma: no cover
except OSError as exc: # pragma: no cover if exc.errno in MUTED_SOCKET_ERRORS:
if exc.errno in MUTED_SOCKET_ERRORS: pass
pass else:
else: raise
raise
def body_iter(self): def body_iter(self):
if self.body: if self.body:
@@ -662,9 +607,7 @@ class Response():
return cls(status_code=status_code, headers={'Location': location}) return cls(status_code=status_code, headers={'Location': location})
@classmethod @classmethod
def send_file(cls, filename, status_code=200, content_type=None, def send_file(cls, filename, status_code=200, content_type=None):
stream=None, max_age=None, compressed=False,
file_extension=''):
"""Send file contents in a response. """Send file contents in a response.
:param filename: The filename of the file. :param filename: The filename of the file.
@@ -672,25 +615,7 @@ class Response():
default is 302. default is 302.
:param content_type: The ``Content-Type`` header to use in the :param content_type: The ``Content-Type`` header to use in the
response. If omitted, it is generated response. If omitted, it is generated
automatically from the file extension of the automatically from the file extension.
``filename`` parameter.
:param stream: A file-like object to read the file contents from. If
a stream is given, the ``filename`` parameter is only
used when generating the ``Content-Type`` header.
:param max_age: The ``Cache-Control`` header's ``max-age`` value in
seconds. If omitted, the value of the
:attr:`Response.default_send_file_max_age` attribute is
used.
:param compressed: Whether the file is compressed. If ``True``, the
``Content-Encoding`` header is set to ``gzip``. A
string with the header value can also be passed.
Note that when using this option the file must have
been compressed beforehand. This option only sets
the header.
:param file_extension: A file extension to append to the ``filename``
parameter when opening the file, including the
dot. The extension given here is not considered
when generating the ``Content-Type`` header.
Security note: The filename is assumed to be trusted. Never pass Security note: The filename is assumed to be trusted. Never pass
filenames provided by the user without validating and sanitizing them filenames provided by the user without validating and sanitizing them
@@ -702,19 +627,9 @@ class Response():
content_type = Response.types_map[ext] content_type = Response.types_map[ext]
else: else:
content_type = 'application/octet-stream' content_type = 'application/octet-stream'
headers = {'Content-Type': content_type} f = open(filename, 'rb')
return cls(body=f, status_code=status_code,
if max_age is None: headers={'Content-Type': content_type})
max_age = cls.default_send_file_max_age
if max_age is not None:
headers['Cache-Control'] = 'max-age={}'.format(max_age)
if compressed:
headers['Content-Encoding'] = compressed \
if isinstance(compressed, str) else 'gzip'
f = stream or open(filename + file_extension, 'rb')
return cls(body=f, status_code=status_code, headers=headers)
class URLPattern(): class URLPattern():
@@ -736,7 +651,7 @@ class URLPattern():
if type_ == 'string': if type_ == 'string':
pattern = '[^/]+' pattern = '[^/]+'
elif type_ == 'int': elif type_ == 'int':
pattern = '-?\\d+' pattern = '\\d+'
elif type_ == 'path': elif type_ == 'path':
pattern = '.+' pattern = '.+'
elif type_.startswith('re:'): elif type_.startswith('re:'):
@@ -797,10 +712,8 @@ class Microdot():
self.url_map = [] self.url_map = []
self.before_request_handlers = [] self.before_request_handlers = []
self.after_request_handlers = [] self.after_request_handlers = []
self.after_error_request_handlers = []
self.error_handlers = {} self.error_handlers = {}
self.shutdown_requested = False self.shutdown_requested = False
self.options_handler = self.default_options_handler
self.debug = False self.debug = False
self.server = None self.server = None
@@ -836,8 +749,7 @@ class Microdot():
""" """
def decorated(f): def decorated(f):
self.url_map.append( self.url_map.append(
([m.upper() for m in (methods or ['GET'])], (methods or ['GET'], URLPattern(url_pattern), f))
URLPattern(url_pattern), f))
return f return f
return decorated return decorated
@@ -961,24 +873,6 @@ class Microdot():
self.after_request_handlers.append(f) self.after_request_handlers.append(f)
return f return f
def after_error_request(self, f):
"""Decorator to register a function to run after an error response is
generated. The decorated function must take two arguments, the request
and response objects. The return value of the function must be an
updated response object. The handler is invoked for error responses
generated by Microdot, as well as those returned by application-defined
error handlers.
Example::
@app.after_error_request
def func(request, response):
# ...
return response
"""
self.after_error_request_handlers.append(f)
return f
def errorhandler(self, status_code_or_exception_class): def errorhandler(self, status_code_or_exception_class):
"""Decorator to register a function as an error handler. Error handler """Decorator to register a function as an error handler. Error handler
functions for numeric HTTP status codes must accept a single argument, functions for numeric HTTP status codes must accept a single argument,
@@ -1019,8 +913,6 @@ class Microdot():
self.before_request_handlers.append(handler) self.before_request_handlers.append(handler)
for handler in subapp.after_request_handlers: for handler in subapp.after_request_handlers:
self.after_request_handlers.append(handler) self.after_request_handlers.append(handler)
for handler in subapp.after_error_request_handlers:
self.after_error_request_handlers.append(handler)
for status_code, handler in subapp.error_handlers.items(): for status_code, handler in subapp.error_handlers.items():
self.error_handlers[status_code] = handler self.error_handlers[status_code] = handler
@@ -1123,36 +1015,18 @@ class Microdot():
self.shutdown_requested = True self.shutdown_requested = True
def find_route(self, req): def find_route(self, req):
method = req.method.upper()
if method == 'OPTIONS' and self.options_handler:
return self.options_handler(req)
if method == 'HEAD':
method = 'GET'
f = 404 f = 404
for route_methods, route_pattern, route_handler in self.url_map: for route_methods, route_pattern, route_handler in self.url_map:
req.url_args = route_pattern.match(req.path) req.url_args = route_pattern.match(req.path)
if req.url_args is not None: if req.url_args is not None:
if method in route_methods: if req.method in route_methods:
f = route_handler f = route_handler
break break
else: else:
f = 405 f = 405
return f return f
def default_options_handler(self, req):
allow = []
for route_methods, route_pattern, route_handler in self.url_map:
if route_pattern.match(req.path) is not None:
allow.extend(route_methods)
if 'GET' in allow:
allow.append('HEAD')
allow.append('OPTIONS')
return {'Allow': ', '.join(allow)}
def handle_request(self, sock, addr): def handle_request(self, sock, addr):
if Request.socket_read_timeout and \
hasattr(sock, 'settimeout'): # pragma: no cover
sock.settimeout(Request.socket_read_timeout)
if not hasattr(sock, 'readline'): # pragma: no cover if not hasattr(sock, 'readline'): # pragma: no cover
stream = sock.makefile("rwb") stream = sock.makefile("rwb")
else: else:
@@ -1163,9 +1037,6 @@ class Microdot():
try: try:
req = Request.create(self, stream, addr, sock) req = Request.create(self, stream, addr, sock)
res = self.dispatch_request(req) res = self.dispatch_request(req)
except socket_timeout_error as exc: # pragma: no cover
if exc.errno and exc.errno != errno.ETIMEDOUT:
print_exception(exc) # not a timeout
except Exception as exc: # pragma: no cover except Exception as exc: # pragma: no cover
print_exception(exc) print_exception(exc)
try: try:
@@ -1189,7 +1060,6 @@ class Microdot():
status_code=res.status_code)) status_code=res.status_code))
def dispatch_request(self, req): def dispatch_request(self, req):
after_request_handled = False
if req: if req:
if req.content_length > req.max_content_length: if req.content_length > req.max_content_length:
if 413 in self.error_handlers: if 413 in self.error_handlers:
@@ -1222,9 +1092,6 @@ class Microdot():
res = handler(req, res) or res res = handler(req, res) or res
for handler in req.after_request_handlers: for handler in req.after_request_handlers:
res = handler(req, res) or res res = handler(req, res) or res
after_request_handled = True
elif isinstance(f, dict):
res = Response(headers=f)
elif f in self.error_handlers: elif f in self.error_handlers:
res = self.error_handlers[f](req) res = self.error_handlers[f](req)
else: else:
@@ -1236,18 +1103,10 @@ class Microdot():
res = exc.reason, exc.status_code res = exc.reason, exc.status_code
except Exception as exc: except Exception as exc:
print_exception(exc) print_exception(exc)
exc_class = None
res = None res = None
if exc.__class__ in self.error_handlers: if exc.__class__ in self.error_handlers:
exc_class = exc.__class__
else:
for c in mro(exc.__class__)[1:]:
if c in self.error_handlers:
exc_class = c
break
if exc_class:
try: try:
res = self.error_handlers[exc_class](req, exc) res = self.error_handlers[exc.__class__](req, exc)
except Exception as exc2: # pragma: no cover except Exception as exc2: # pragma: no cover
print_exception(exc2) print_exception(exc2)
if res is None: if res is None:
@@ -1265,10 +1124,6 @@ class Microdot():
res = Response(*res) res = Response(*res)
elif not isinstance(res, Response): elif not isinstance(res, Response):
res = Response(res) res = Response(res)
if not after_request_handled:
for handler in self.after_error_request_handlers:
res = handler(req, res) or res
res.is_head = (req and req.method == 'HEAD')
return res return res

View File

@@ -59,9 +59,8 @@ class Microdot(BaseMicrodot):
headers = NoCaseDict() headers = NoCaseDict()
content_length = 0 content_length = 0
for key, value in scope.get('headers', []): for key, value in scope.get('headers', []):
key = key.decode().title() headers[key] = value
headers[key] = value.decode() if key.lower() == 'content-length':
if key == 'Content-Length':
content_length = int(value) content_length = int(value)
if content_length and content_length <= Request.max_body_length: if content_length and content_length <= Request.max_body_length:
@@ -94,10 +93,10 @@ class Microdot(BaseMicrodot):
header_list = [] header_list = []
for name, value in res.headers.items(): for name, value in res.headers.items():
if not isinstance(value, list): if not isinstance(value, list):
header_list.append((name.lower().encode(), value.encode())) header_list.append((name, value))
else: else:
for v in value: for v in value:
header_list.append((name.lower().encode(), v.encode())) header_list.append((name, v))
if scope['type'] != 'http': # pragma: no cover if scope['type'] != 'http': # pragma: no cover
return return

View File

@@ -17,7 +17,6 @@ except ImportError:
import io import io
from microdot import Microdot as BaseMicrodot from microdot import Microdot as BaseMicrodot
from microdot import mro
from microdot import NoCaseDict from microdot import NoCaseDict
from microdot import Request as BaseRequest from microdot import Request as BaseRequest
from microdot import Response as BaseResponse from microdot import Response as BaseResponse
@@ -151,11 +150,10 @@ class Response(BaseResponse):
await stream.awrite(b'\r\n') await stream.awrite(b'\r\n')
# body # body
if not self.is_head: async for body in self.body_iter():
async for body in self.body_iter(): if isinstance(body, str): # pragma: no cover
if isinstance(body, str): # pragma: no cover body = body.encode()
body = body.encode() await stream.awrite(body)
await stream.awrite(body)
except OSError as exc: # pragma: no cover except OSError as exc: # pragma: no cover
if exc.errno in MUTED_SOCKET_ERRORS or \ if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost': exc.args[0] == 'Connection lost':
@@ -348,7 +346,6 @@ class Microdot(BaseMicrodot):
status_code=res.status_code)) status_code=res.status_code))
async def dispatch_request(self, req): async def dispatch_request(self, req):
after_request_handled = False
if req: if req:
if req.content_length > req.max_content_length: if req.content_length > req.max_content_length:
if 413 in self.error_handlers: if 413 in self.error_handlers:
@@ -383,11 +380,7 @@ class Microdot(BaseMicrodot):
res = await self._invoke_handler( res = await self._invoke_handler(
handler, req, res) or res handler, req, res) or res
for handler in req.after_request_handlers: for handler in req.after_request_handlers:
res = await self._invoke_handler( res = await handler(req, res) or res
handler, req, res) or res
after_request_handled = True
elif isinstance(f, dict):
res = Response(headers=f)
elif f in self.error_handlers: elif f in self.error_handlers:
res = await self._invoke_handler( res = await self._invoke_handler(
self.error_handlers[f], req) self.error_handlers[f], req)
@@ -400,19 +393,11 @@ class Microdot(BaseMicrodot):
res = exc.reason, exc.status_code res = exc.reason, exc.status_code
except Exception as exc: except Exception as exc:
print_exception(exc) print_exception(exc)
exc_class = None
res = None res = None
if exc.__class__ in self.error_handlers: if exc.__class__ in self.error_handlers:
exc_class = exc.__class__
else:
for c in mro(exc.__class__)[1:]:
if c in self.error_handlers:
exc_class = c
break
if exc_class:
try: try:
res = await self._invoke_handler( res = await self._invoke_handler(
self.error_handlers[exc_class], req, exc) self.error_handlers[exc.__class__], req, exc)
except Exception as exc2: # pragma: no cover except Exception as exc2: # pragma: no cover
print_exception(exc2) print_exception(exc2)
if res is None: if res is None:
@@ -430,11 +415,6 @@ class Microdot(BaseMicrodot):
res = Response(*res) res = Response(*res)
elif not isinstance(res, Response): elif not isinstance(res, Response):
res = Response(res) res = Response(res)
if not after_request_handled:
for handler in self.after_error_request_handlers:
res = await self._invoke_handler(
handler, req, res) or res
res.is_head = (req and req.method == 'HEAD')
return res return res
async def _invoke_handler(self, f_or_coro, *args, **kwargs): async def _invoke_handler(self, f_or_coro, *args, **kwargs):

View File

@@ -17,7 +17,7 @@ class WebSocket(BaseWebSocket):
opcode, payload = await self._read_frame() opcode, payload = await self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload) send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover if send_opcode: # pragma: no cover
await self.send(data, send_opcode) await self.send(send_opcode, data)
elif data: # pragma: no branch elif data: # pragma: no branch
return data return data

65
src/microdot_auth.py Normal file
View File

@@ -0,0 +1,65 @@
from microdot import abort
class BaseAuth:
def __init__(self, header='Authorization', scheme=None):
self.auth_callback = None
self.error_callback = self.auth_failed
self.header = header
self.scheme = scheme.lower()
def callback(self, f):
"""Decorator to configure the authentication callback.
Microdot calls the authentication callback to allow the application to
check user credentials.
"""
self.auth_callback = f
def errorhandler(self, f):
"""Decorator to configure the error callback.
Microdot calls the error callback to allow the application to generate
a custom error response. The default error response is to call
``abort(401)``.
"""
self.error_callback = f
def auth_failed(self):
abort(401)
def __call__(self, func):
def wrapper(request, *args, **kwargs):
auth = request.headers.get(self.header)
if not auth:
return self.error_callback()
if self.header == 'Authorization':
if ' ' not in auth:
return self.error_callback()
scheme, auth = auth.split(' ', 1)
if scheme.lower() != self.scheme:
return self.error_callback()
if not self.auth_callback(request, *self._get_auth_args(auth)):
return self.error_callback()
return func(request, *args, **kwargs)
return wrapper
class BasicAuth(BaseAuth):
def __init__(self):
super().__init__(scheme='Basic')
def _get_auth_args(self, auth):
import binascii
username, password = binascii.a2b_base64(auth).decode('utf-8').split(
':', 1)
return (username, password)
class TokenAuth(BaseAuth):
def __init__(self, header='Authorization', scheme='Bearer'):
super().__init__(header=header, scheme=scheme)
def _get_auth_args(self, token):
return (token,)

View File

@@ -1,110 +0,0 @@
class CORS:
"""Add CORS headers to HTTP responses.
:param app: The application to add CORS headers to.
:param allowed_origins: A list of origins that are allowed to make
cross-site requests. If set to '*', all origins are
allowed.
:param allow_credentials: If set to True, the
``Access-Control-Allow-Credentials`` header will
be set to ``true`` to indicate to the browser
that it can expose cookies and authentication
headers.
:param allowed_methods: A list of methods that are allowed to be used when
making cross-site requests. If not set, all methods
are allowed.
:param expose_headers: A list of headers that the browser is allowed to
exposed.
:param allowed_headers: A list of headers that are allowed to be used when
making cross-site requests. If not set, all headers
are allowed.
:param max_age: The maximum amount of time in seconds that the browser
should cache the results of a preflight request.
:param handle_cors: If set to False, CORS headers will not be added to
responses. This can be useful if you want to add CORS
headers manually.
"""
def __init__(self, app=None, allowed_origins=None, allow_credentials=False,
allowed_methods=None, expose_headers=None,
allowed_headers=None, max_age=None, handle_cors=True):
self.allowed_origins = allowed_origins
self.allow_credentials = allow_credentials
self.allowed_methods = allowed_methods
self.expose_headers = expose_headers
self.allowed_headers = None if allowed_headers is None \
else [h.lower() for h in allowed_headers]
self.max_age = max_age
if app is not None:
self.initialize(app, handle_cors=handle_cors)
def initialize(self, app, handle_cors=True):
"""Initialize the CORS object for the given application.
:param app: The application to add CORS headers to.
:param handle_cors: If set to False, CORS headers will not be added to
responses. This can be useful if you want to add
CORS headers manually.
"""
self.default_options_handler = app.options_handler
if handle_cors:
app.options_handler = self.options_handler
app.after_request(self.after_request)
app.after_error_request(self.after_request)
def options_handler(self, request):
headers = self.default_options_handler(request)
headers.update(self.get_cors_headers(request))
return headers
def get_cors_headers(self, request):
"""Return a dictionary of CORS headers to add to a given request.
:param request: The request to add CORS headers to.
"""
cors_headers = {}
origin = request.headers.get('Origin')
if self.allowed_origins == '*':
cors_headers['Access-Control-Allow-Origin'] = origin or '*'
if origin:
cors_headers['Vary'] = 'Origin'
elif origin in (self.allowed_origins or []):
cors_headers['Access-Control-Allow-Origin'] = origin
cors_headers['Vary'] = 'Origin'
if self.allow_credentials and \
'Access-Control-Allow-Origin' in cors_headers:
cors_headers['Access-Control-Allow-Credentials'] = 'true'
if self.expose_headers:
cors_headers['Access-Control-Expose-Headers'] = \
', '.join(self.expose_headers)
if request.method == 'OPTIONS':
# handle preflight request
if self.max_age:
cors_headers['Access-Control-Max-Age'] = str(self.max_age)
method = request.headers.get('Access-Control-Request-Method')
if method:
method = method.upper()
if self.allowed_methods is None or \
method in self.allowed_methods:
cors_headers['Access-Control-Allow-Methods'] = method
headers = request.headers.get('Access-Control-Request-Headers')
if headers:
if self.allowed_headers is None:
cors_headers['Access-Control-Allow-Headers'] = headers
else:
headers = [h.strip() for h in headers.split(',')]
headers = [h for h in headers
if h.lower() in self.allowed_headers]
cors_headers['Access-Control-Allow-Headers'] = \
', '.join(headers)
return cors_headers
def after_request(self, request, response):
saved_vary = response.headers.get('Vary')
response.headers.update(self.get_cors_headers(request))
if saved_vary and saved_vary != response.headers.get('Vary'):
response.headers['Vary'] = (
saved_vary + ', ' + response.headers['Vary'])

46
src/microdot_login.py Normal file
View File

@@ -0,0 +1,46 @@
from microdot import redirect, urlencode
from microdot_session import get_session, update_session
class LoginAuth:
def __init__(self, login_url='/login'):
super().__init__()
self.login_url = login_url
self.user_callback = self._accept_user
def callback(self, f):
self.user_callback = f
def login_user(self, request, user_id):
session = get_session(request)
session['user_id'] = user_id
update_session(request, session)
return session
def logout_user(self, request):
session = get_session(request)
session.pop('user_id', None)
update_session(request, session)
return session
def redirect_to_next(self, request, default_url='/'):
next_url = request.args.get('next', default_url)
if not next_url.startswith('/'):
next_url = default_url
return redirect(next_url)
def __call__(self, func):
def wrapper(request, *args, **kwargs):
session = get_session(request)
if 'user_id' not in session:
return redirect(self.login_url + '?next=' + urlencode(
request.url))
if not self.user_callback(request, session['user_id']):
return redirect(self.login_url + '?next=' + urlencode(
request.url))
return func(request, *args, **kwargs)
return wrapper
def _accept_user(self, request, user_id):
return True

View File

@@ -58,7 +58,6 @@ class TestResponse:
test_res._initialize_body(res) test_res._initialize_body(res)
test_res._process_text_body() test_res._process_text_body()
test_res._process_json_body() test_res._process_json_body()
test_res.is_head = res.is_head
return test_res return test_res

View File

@@ -28,7 +28,7 @@ class WebSocket:
opcode, payload = self._read_frame() opcode, payload = self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload) send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover if send_opcode: # pragma: no cover
self.send(data, send_opcode) self.send(send_opcode, data)
elif data: # pragma: no branch elif data: # pragma: no branch
return data return data

Binary file not shown.

View File

@@ -1,158 +0,0 @@
import unittest
from microdot import Microdot
from microdot_test_client import TestClient
from microdot_cors import CORS
class TestCORS(unittest.TestCase):
def test_origin(self):
app = Microdot()
cors = CORS(allowed_origins=['https://example.com'],
allow_credentials=True)
cors.initialize(app)
@app.get('/')
def index(req):
return 'foo'
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Origin' in res.headers)
self.assertFalse('Access-Control-Allow-Credentials' in res.headers)
self.assertFalse('Vary' in res.headers)
res = client.get('/', headers={'Origin': 'https://example.com'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertEqual(res.headers['Access-Control-Allow-Credentials'],
'true')
self.assertEqual(res.headers['Vary'], 'Origin')
cors.allow_credentials = False
res = client.get('/foo', headers={'Origin': 'https://example.com'})
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertFalse('Access-Control-Allow-Credentials' in res.headers)
self.assertEqual(res.headers['Vary'], 'Origin')
res = client.get('/', headers={'Origin': 'https://bad.com'})
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Origin' in res.headers)
self.assertFalse('Access-Control-Allow-Credentials' in res.headers)
self.assertFalse('Vary' in res.headers)
def test_all_origins(self):
app = Microdot()
CORS(app, allowed_origins='*', expose_headers=['X-Test', 'X-Test2'])
@app.get('/')
def index(req):
return 'foo'
@app.get('/foo')
def foo(req):
return 'foo', {'Vary': 'X-Foo, X-Bar'}
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'], '*')
self.assertFalse('Vary' in res.headers)
self.assertEqual(res.headers['Access-Control-Expose-Headers'],
'X-Test, X-Test2')
res = client.get('/', headers={'Origin': 'https://example.com'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertEqual(res.headers['Vary'], 'Origin')
self.assertEqual(res.headers['Access-Control-Expose-Headers'],
'X-Test, X-Test2')
res = client.get('/bad', headers={'Origin': 'https://example.com'})
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertEqual(res.headers['Vary'], 'Origin')
self.assertEqual(res.headers['Access-Control-Expose-Headers'],
'X-Test, X-Test2')
res = client.get('/foo', headers={'Origin': 'https://example.com'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Vary'], 'X-Foo, X-Bar, Origin')
def test_cors_preflight(self):
app = Microdot()
CORS(app, allowed_origins='*')
@app.route('/', methods=['GET', 'POST'])
def index(req):
return 'foo'
client = TestClient(app)
res = client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'X-Test, X-Test2'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertFalse('Access-Control-Max-Age' in res.headers)
self.assertEqual(res.headers['Access-Control-Allow-Methods'], 'POST')
self.assertEqual(res.headers['Access-Control-Allow-Headers'],
'X-Test, X-Test2')
res = client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertFalse('Access-Control-Max-Age' in res.headers)
self.assertFalse('Access-Control-Allow-Methods' in res.headers)
self.assertFalse('Access-Control-Allow-Headers' in res.headers)
def test_cors_preflight_with_options(self):
app = Microdot()
CORS(app, allowed_origins='*', max_age=3600, allowed_methods=['POST'],
allowed_headers=['X-Test'])
@app.route('/', methods=['GET', 'POST'])
def index(req):
return 'foo'
client = TestClient(app)
res = client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'X-Test, X-Test2'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Access-Control-Allow-Origin'],
'https://example.com')
self.assertEqual(res.headers['Access-Control-Max-Age'], '3600')
self.assertEqual(res.headers['Access-Control-Allow-Methods'], 'POST')
self.assertEqual(res.headers['Access-Control-Allow-Headers'], 'X-Test')
res = client.request('OPTIONS', '/', headers={
'Origin': 'https://example.com',
'Access-Control-Request-Method': 'GET'})
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Methods' in res.headers)
self.assertFalse('Access-Control-Allow-Headers' in res.headers)
def test_cors_disabled(self):
app = Microdot()
CORS(app, allowed_origins='*', handle_cors=False)
@app.get('/')
def index(req):
return 'foo'
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertFalse('Access-Control-Allow-Origin' in res.headers)
self.assertFalse('Vary' in res.headers)

View File

@@ -63,52 +63,6 @@ class TestMicrodot(unittest.TestCase):
self.assertEqual(res.headers['Content-Length'], '3') self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'bar') self.assertEqual(res.text, 'bar')
def test_head_request(self):
self._mock()
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('HEAD', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n'))
self._unmock()
def test_options_request(self):
app = Microdot()
@app.route('/', methods=['GET', 'DELETE'])
def index(req):
return 'foo'
@app.post('/')
def index_post(req):
return 'bar'
@app.route('/foo', methods=['POST', 'PUT'])
def foo(req):
return 'baz'
client = TestClient(app)
res = client.request('OPTIONS', '/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'],
'GET, DELETE, POST, HEAD, OPTIONS')
res = client.request('OPTIONS', '/foo')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'], 'POST, PUT, OPTIONS')
def test_empty_request(self): def test_empty_request(self):
self._mock() self._mock()
@@ -325,39 +279,6 @@ class TestMicrodot(unittest.TestCase):
self.assertEqual(res.headers['Content-Length'], '3') self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'baz') self.assertEqual(res.text, 'baz')
def test_after_error_request(self):
app = Microdot()
@app.after_error_request
def after_error_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_error_request
def after_error_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/foo')
def foo(req):
return 'foo'
client = TestClient(app)
res = client.get('/foo')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertFalse('X-One' in res.headers)
self.assertFalse('Set-Cookie' in res.headers)
res = client.get('/bar')
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(client.cookies['foo'], 'bar')
def test_400(self): def test_400(self):
self._mock() self._mock()
@@ -544,90 +465,6 @@ class TestMicrodot(unittest.TestCase):
'text/plain; charset=UTF-8') 'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501') self.assertEqual(res.text, '501')
def test_exception_handler_parent(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(LookupError)
def handle_lookup_error(req, exc):
return exc.__class__.__name__, 501
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'IndexError')
def test_exception_handler_redundant_parent(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(LookupError)
def handle_lookup_error(req, exc):
return 'LookupError', 501
@app.errorhandler(IndexError)
def handle_index_error(req, exc):
return 'IndexError', 501
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'IndexError')
def test_exception_handler_multiple_parents(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(Exception)
def handle_generic_exception(req, exc):
return 'Exception', 501
@app.errorhandler(LookupError)
def handle_lookup_error(req, exc):
return 'LookupError', 501
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'LookupError')
def test_exception_handler_no_viable_parents(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(RuntimeError)
def handle_runtime_error(req, exc):
return 'RuntimeError', 501
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Internal server error')
def test_abort(self): def test_abort(self):
app = Microdot() app = Microdot()
@@ -740,11 +577,7 @@ class TestMicrodot(unittest.TestCase):
@subapp.after_request @subapp.after_request
def after(req, res): def after(req, res):
res.body += b':after' return res.body + b':after'
@subapp.after_error_request
def after_error(req, res):
res.body += b':errorafter'
@subapp.errorhandler(404) @subapp.errorhandler(404)
def not_found(req): def not_found(req):
@@ -763,7 +596,7 @@ class TestMicrodot(unittest.TestCase):
self.assertEqual(res.status_code, 404) self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'], self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8') 'text/plain; charset=UTF-8')
self.assertEqual(res.text, '404:errorafter') self.assertEqual(res.text, '404')
res = client.get('/sub/app') res = client.get('/sub/app')
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)

View File

@@ -53,9 +53,9 @@ class TestMicrodotASGI(unittest.TestCase):
'type': 'http', 'type': 'http',
'path': '/foo/bar', 'path': '/foo/bar',
'query_string': b'baz=1', 'query_string': b'baz=1',
'headers': [(b'Authorization', b'Bearer 123'), 'headers': [('Authorization', 'Bearer 123'),
(b'Cookie', b'session=xyz'), ('Cookie', 'session=xyz'),
(b'Content-Length', b'4')], ('Content-Length', 4)],
'client': ['1.2.3.4', 1234], 'client': ['1.2.3.4', 1234],
'method': 'POST', 'method': 'POST',
'http_version': '1.1', 'http_version': '1.1',
@@ -83,10 +83,10 @@ class TestMicrodotASGI(unittest.TestCase):
if packet['type'] == 'http.response.start': if packet['type'] == 'http.response.start':
self.assertEqual(packet['status'], 200) self.assertEqual(packet['status'], 200)
expected_headers = [ expected_headers = [
(b'content-length', b'8'), ('Content-Length', '8'),
(b'content-type', b'text/plain; charset=UTF-8'), ('Content-Type', 'text/plain; charset=UTF-8'),
(b'set-cookie', b'foo=foo'), ('Set-Cookie', 'foo=foo'),
(b'set-cookie', b'bar=bar; HttpOnly') ('Set-Cookie', 'bar=bar; HttpOnly')
] ]
self.assertEqual(len(packet['headers']), len(expected_headers)) self.assertEqual(len(packet['headers']), len(expected_headers))
for header in expected_headers: for header in expected_headers:
@@ -114,9 +114,9 @@ class TestMicrodotASGI(unittest.TestCase):
scope = { scope = {
'type': 'http', 'type': 'http',
'path': '/foo/bar', 'path': '/foo/bar',
'headers': [(b'Authorization', b'Bearer 123'), 'headers': [('Authorization', 'Bearer 123'),
(b'Cookie', b'session=xyz'), ('Cookie', 'session=xyz'),
(b'Content-Length', b'4')], ('Content-Length', 4)],
'client': ['1.2.3.4', 1234], 'client': ['1.2.3.4', 1234],
'method': 'POST', 'method': 'POST',
'http_version': '1.1', 'http_version': '1.1',

View File

@@ -101,48 +101,6 @@ class TestMicrodotAsync(unittest.TestCase):
self.assertEqual(res.body, b'bar-async') self.assertEqual(res.body, b'bar-async')
self.assertEqual(res.json, None) self.assertEqual(res.json, None)
def test_head_request(self):
app = Microdot()
@app.route('/foo')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('HEAD', '/foo')
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n'))
def test_options_request(self):
app = Microdot()
@app.route('/', methods=['GET', 'DELETE'])
async def index(req):
return 'foo'
@app.post('/')
async def index_post(req):
return 'bar'
@app.route('/foo', methods=['POST', 'PUT'])
async def foo(req):
return 'baz'
client = TestClient(app)
res = self._run(client.request('OPTIONS', '/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'],
'GET, DELETE, POST, HEAD, OPTIONS')
res = self._run(client.request('OPTIONS', '/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Allow'], 'POST, PUT, OPTIONS')
def test_empty_request(self): def test_empty_request(self):
app = Microdot() app = Microdot()
@@ -356,39 +314,6 @@ class TestMicrodotAsync(unittest.TestCase):
self.assertEqual(res.headers['Content-Length'], '3') self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'baz') self.assertEqual(res.text, 'baz')
def test_after_error_request(self):
app = Microdot()
@app.after_error_request
def after_error_request_one(req, res):
res.headers['X-One'] = '1'
@app.after_error_request
def after_error_request_two(req, res):
res.set_cookie('foo', 'bar')
return res
@app.route('/foo')
def foo(req):
return 'foo'
client = TestClient(app)
res = self._run(client.get('/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertFalse('X-One' in res.headers)
self.assertFalse('Set-Cookie' in res.headers)
res = self._run(client.get('/bar'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(client.cookies['foo'], 'bar')
def test_400(self): def test_400(self):
self._mock() self._mock()
@@ -575,90 +500,6 @@ class TestMicrodotAsync(unittest.TestCase):
'text/plain; charset=UTF-8') 'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501') self.assertEqual(res.text, '501')
def test_exception_handler_parent(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(LookupError)
async def handle_lookup_error(req, exc):
return exc.__class__.__name__, 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'IndexError')
def test_exception_handler_redundant_parent(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(LookupError)
async def handle_lookup_error(req, exc):
return 'LookupError', 501
@app.errorhandler(IndexError)
async def handle_index_error(req, exc):
return 'IndexError', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'IndexError')
def test_exception_handler_multiple_parents(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(Exception)
async def handle_generic_exception(req, exc):
return 'Exception', 501
@app.errorhandler(LookupError)
async def handle_lookup_error(req, exc):
return 'LookupError', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'LookupError')
def test_exception_handler_no_viable_parents(self):
app = Microdot()
@app.route('/')
def index(req):
foo = []
return foo[1]
@app.errorhandler(RuntimeError)
async def handle_runtime_error(req, exc):
return 'RuntimeError', 501
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Internal server error')
def test_abort(self): def test_abort(self):
app = Microdot() app = Microdot()

113
tests/test_microdot_auth.py Normal file
View File

@@ -0,0 +1,113 @@
import binascii
import unittest
from microdot import Microdot
from microdot_auth import BasicAuth, TokenAuth
from microdot_test_client import TestClient
class TestAuth(unittest.TestCase):
def test_basic_auth(self):
app = Microdot()
basic_auth = BasicAuth()
@basic_auth.callback
def authenticate(request, username, password):
if username == 'foo' and password == 'bar':
request.g.user = {'username': username}
return True
@app.route('/')
@basic_auth
def index(request):
return request.g.user['username']
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={
'Authorization': 'Basic ' + binascii.b2a_base64(
b'foo:bar').decode()})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'foo')
res = client.get('/', headers={
'Authorization': 'Basic ' + binascii.b2a_base64(
b'foo:baz').decode()})
self.assertEqual(res.status_code, 401)
def test_token_auth(self):
app = Microdot()
token_auth = TokenAuth()
@token_auth.callback
def authenticate(request, token):
if token == 'foo':
request.g.user = 'user'
return True
@app.route('/')
@token_auth
def index(request):
return request.g.user
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'Authorization': 'Basic foo'})
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'Authorization': 'foo'})
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'Authorization': 'Bearer foo'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'user')
def test_token_auth_custom_header(self):
app = Microdot()
token_auth = TokenAuth(header='X-Auth-Token')
@token_auth.callback
def authenticate(request, token):
if token == 'foo':
request.g.user = 'user'
return True
@app.route('/')
@token_auth
def index(request):
return request.g.user
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'Authorization': 'Basic foo'})
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'Authorization': 'foo'})
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'Authorization': 'Bearer foo'})
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'X-Token-Auth': 'Bearer foo'})
self.assertEqual(res.status_code, 401)
res = client.get('/', headers={'X-Auth-Token': 'foo'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'user')
res = client.get('/', headers={'x-auth-token': 'foo'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'user')
@token_auth.errorhandler
def error_handler():
return {'status_code': 403}, 403
res = client.get('/')
self.assertEqual(res.status_code, 403)
self.assertEqual(res.json, {'status_code': 403})

View File

@@ -0,0 +1,134 @@
import unittest
from microdot import Microdot
from microdot_login import LoginAuth
from microdot_session import set_session_secret_key, with_session
from microdot_test_client import TestClient
set_session_secret_key('top-secret!')
class TestLogin(unittest.TestCase):
def test_login_auth(self):
app = Microdot()
login_auth = LoginAuth()
@app.get('/')
@login_auth
def index(request):
return 'ok'
@app.post('/login')
def login(request):
login_auth.login_user(request, 'user')
return login_auth.redirect_to_next(request)
@app.post('/logout')
def logout(request):
login_auth.logout_user(request)
return 'ok'
client = TestClient(app)
res = client.get('/?foo=bar')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/login?next=/%3Ffoo%3Dbar')
res = client.post('/login?next=/%3Ffoo=bar')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/?foo=bar')
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'ok')
res = client.post('/logout')
self.assertEqual(res.status_code, 200)
res = client.get('/')
self.assertEqual(res.status_code, 302)
def test_login_auth_with_session(self):
app = Microdot()
login_auth = LoginAuth(login_url='/foo')
@app.get('/')
@login_auth
@with_session
def index(request, session):
return session['user_id']
@app.post('/foo')
def login(request):
login_auth.login_user(request, 'user')
return login_auth.redirect_to_next(request)
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/foo?next=/')
res = client.post('/foo')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/')
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'user')
def test_login_auth_user_callback(self):
app = Microdot()
login_auth = LoginAuth()
@login_auth.callback
def check_user(request, user_id):
request.g.user_id = user_id
return user_id == 'user'
@app.get('/')
@login_auth
def index(request):
return request.g.user_id
@app.post('/good-login')
def good_login(request):
login_auth.login_user(request, 'user')
return login_auth.redirect_to_next(request)
@app.post('/bad-login')
def bad_login(request):
login_auth.login_user(request, 'foo')
return login_auth.redirect_to_next(request)
client = TestClient(app)
res = client.post('/good-login')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/')
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'user')
res = client.post('/bad-login')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/')
res = client.get('/')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/login?next=/')
def test_login_auth_bad_redirect(self):
app = Microdot()
login_auth = LoginAuth()
@app.get('/')
@login_auth
def index(request):
return 'ok'
@app.post('/login')
def login(request):
login_auth.login_user(request, 'user')
return login_auth.redirect_to_next(request)
client = TestClient(app)
res = client.post('/login?next=http://example.com')
self.assertEqual(res.status_code, 302)
self.assertEqual(res.headers['Location'], '/')

View File

@@ -58,11 +58,3 @@ class TestMultiDict(unittest.TestCase):
del d['oNE'] del d['oNE']
self.assertEqual(list(d.items()), [('two', 5)]) self.assertEqual(list(d.items()), [('two', 5)])
self.assertEqual(list(d.values()), [5]) self.assertEqual(list(d.values()), [5])
d.update({'oNe': 1, 'two': 2, 'three': 3})
self.assertEqual(d['one'], 1)
self.assertEqual(d['ONE'], 1)
self.assertEqual(d['two'], 2)
self.assertEqual(d['TWO'], 2)
self.assertEqual(d['three'], 3)
self.assertEqual(d['THREE'], 3)

View File

@@ -45,13 +45,6 @@ class TestRequest(unittest.TestCase):
self.assertEqual(req.args, MultiDict( self.assertEqual(req.args, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'})) {'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
def test_badly_formatted_args(self):
fd = get_request_fd('GET', '/?&foo=bar&abc=def&&&x=%2f%%')
req = Request.create('app', fd, 'addr')
self.assertEqual(req.query_string, '&foo=bar&abc=def&&&x=%2f%%')
self.assertEqual(req.args, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
def test_json(self): def test_json(self):
fd = get_request_fd('GET', '/foo', headers={ fd = get_request_fd('GET', '/foo', headers={
'Content-Type': 'application/json'}, body='{"foo":"bar"}') 'Content-Type': 'application/json'}, body='{"foo":"bar"}')

View File

@@ -235,39 +235,6 @@ class TestResponse(unittest.TestCase):
b'HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\nfoo\n') b'HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\nfoo\n')
Response.send_file_buffer_size = original_buffer_size Response.send_file_buffer_size = original_buffer_size
def test_send_file_max_age(self):
res = Response.send_file('tests/files/test.txt', max_age=123)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Cache-Control'], 'max-age=123')
Response.default_send_file_max_age = 456
res = Response.send_file('tests/files/test.txt')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Cache-Control'], 'max-age=456')
res = Response.send_file('tests/files/test.txt', max_age=123)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Cache-Control'], 'max-age=123')
Response.default_send_file_max_age = None
def test_send_file_compressed(self):
res = Response.send_file('tests/files/test.txt', compressed=True)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Encoding'], 'gzip')
res = Response.send_file('tests/files/test.txt', compressed='foo')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Encoding'], 'foo')
res = Response.send_file('tests/files/test', compressed=True,
file_extension='.gz')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'],
'application/octet-stream')
self.assertEqual(res.headers['Content-Encoding'], 'gzip')
def test_default_content_type(self): def test_default_content_type(self):
original_content_type = Response.default_content_type original_content_type = Response.default_content_type
res = Response('foo') res = Response('foo')

View File

@@ -1,24 +1,22 @@
try:
import uasyncio as asyncio
except ImportError:
import asyncio
import unittest import unittest
from microdot import Microdot from microdot import Microdot
from microdot_asyncio import Microdot as MicrodotAsync
from microdot_session import set_session_secret_key, get_session, \ from microdot_session import set_session_secret_key, get_session, \
update_session, delete_session, with_session update_session, delete_session, with_session
from microdot_test_client import TestClient from microdot_test_client import TestClient
from microdot_asyncio_test_client import TestClient as TestClientAsync
set_session_secret_key('top-secret!') set_session_secret_key('top-secret!')
class TestSession(unittest.TestCase): class TestSession(unittest.TestCase):
def test_session(self): def setUp(self):
app = Microdot() self.app = Microdot()
client = TestClient(app) self.client = TestClient(self.app)
@app.get('/') def tearDown(self):
pass
def test_session(self):
@self.app.get('/')
def index(req): def index(req):
session = get_session(req) session = get_session(req)
session2 = get_session(req) session2 = get_session(req)
@@ -26,106 +24,52 @@ class TestSession(unittest.TestCase):
self.assertEqual(session['foo'], 'bar') self.assertEqual(session['foo'], 'bar')
return str(session.get('name')) return str(session.get('name'))
@app.get('/with') @self.app.get('/with')
@with_session @with_session
def session_context_manager(req, session): def session_context_manager(req, session):
return str(session.get('name')) return str(session.get('name'))
@app.post('/set') @self.app.post('/set')
def set_session(req): def set_session(req):
update_session(req, {'name': 'joe'}) update_session(req, {'name': 'joe'})
return 'OK' return 'OK'
@app.post('/del') @self.app.post('/del')
def del_session(req): def del_session(req):
delete_session(req) delete_session(req)
return 'OK' return 'OK'
res = client.get('/') res = self.client.get('/')
self.assertEqual(res.text, 'None') self.assertEqual(res.text, 'None')
res = client.get('/with') res = self.client.get('/with')
self.assertEqual(res.text, 'None') self.assertEqual(res.text, 'None')
res = client.post('/set') res = self.client.post('/set')
self.assertEqual(res.text, 'OK') self.assertEqual(res.text, 'OK')
res = client.get('/') res = self.client.get('/')
self.assertEqual(res.text, 'joe') self.assertEqual(res.text, 'joe')
res = client.get('/with') res = self.client.get('/with')
self.assertEqual(res.text, 'joe') self.assertEqual(res.text, 'joe')
res = client.post('/del') res = self.client.post('/del')
self.assertEqual(res.text, 'OK') self.assertEqual(res.text, 'OK')
res = client.get('/') res = self.client.get('/')
self.assertEqual(res.text, 'None') self.assertEqual(res.text, 'None')
res = client.get('/with') res = self.client.get('/with')
self.assertEqual(res.text, 'None')
def _run(self, coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
def test_session_async(self):
app = MicrodotAsync()
client = TestClientAsync(app)
@app.get('/')
async def index(req):
session = get_session(req)
session2 = get_session(req)
session2['foo'] = 'bar'
self.assertEqual(session['foo'], 'bar')
return str(session.get('name'))
@app.get('/with')
@with_session
async def session_context_manager(req, session):
return str(session.get('name'))
@app.post('/set')
async def set_session(req):
update_session(req, {'name': 'joe'})
return 'OK'
@app.post('/del')
async def del_session(req):
delete_session(req)
return 'OK'
res = self._run(client.get('/'))
self.assertEqual(res.text, 'None')
res = self._run(client.get('/with'))
self.assertEqual(res.text, 'None')
res = self._run(client.post('/set'))
self.assertEqual(res.text, 'OK')
res = self._run(client.get('/'))
self.assertEqual(res.text, 'joe')
res = self._run(client.get('/with'))
self.assertEqual(res.text, 'joe')
res = self._run(client.post('/del'))
self.assertEqual(res.text, 'OK')
res = self._run(client.get('/'))
self.assertEqual(res.text, 'None')
res = self._run(client.get('/with'))
self.assertEqual(res.text, 'None') self.assertEqual(res.text, 'None')
def test_session_no_secret_key(self): def test_session_no_secret_key(self):
set_session_secret_key(None) set_session_secret_key(None)
app = Microdot()
client = TestClient(app)
@app.get('/') @self.app.get('/')
def index(req): def index(req):
self.assertRaises(ValueError, get_session, req) self.assertRaises(ValueError, get_session, req)
self.assertRaises(ValueError, update_session, req, {}) self.assertRaises(ValueError, update_session, req, {})
return '' return ''
res = client.get('/') res = self.client.get('/')
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
set_session_secret_key('top-secret!') set_session_secret_key('top-secret!')

View File

@@ -56,12 +56,10 @@ class TestURLPattern(unittest.TestCase):
p = URLPattern('/users/<int:id>/<int:id2>/') p = URLPattern('/users/<int:id>/<int:id2>/')
self.assertEqual(p.match('/users/123/456/'), {'id': 123, 'id2': 456}) self.assertEqual(p.match('/users/123/456/'), {'id': 123, 'id2': 456})
self.assertEqual(p.match('/users/123/-456/'), {'id': 123, 'id2': -456})
self.assertIsNone(p.match('/users/')) self.assertIsNone(p.match('/users/'))
self.assertIsNone(p.match('/users/123/-456')) self.assertIsNone(p.match('/users/123/456'))
self.assertIsNone(p.match('/users/123/abc/')) self.assertIsNone(p.match('/users/123/abc/'))
self.assertIsNone(p.match('/users/123/-456/abc')) self.assertIsNone(p.match('/users/123/456/abc'))
self.assertIsNone(p.match('/users/--123/456/'))
def test_path_argument(self): def test_path_argument(self):
p = URLPattern('/users/<path:path>') p = URLPattern('/users/<path:path>')

View File

@@ -1,11 +0,0 @@
import unittest
from microdot import urlencode, urldecode_str, urldecode_bytes
class TestURLEncode(unittest.TestCase):
def test_urlencode(self):
self.assertEqual(urlencode('?foo=bar&x'), '%3Ffoo%3Dbar%26x')
def test_urldecode(self):
self.assertEqual(urldecode_str('%3Ffoo%3Dbar%26x'), '?foo=bar&x')
self.assertEqual(urldecode_bytes(b'%3Ffoo%3Dbar%26x'), '?foo=bar&x')

View File

@@ -1,4 +1,4 @@
FROM ubuntu:22.04 FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive

View File

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

10
tox.ini
View File

@@ -1,21 +1,21 @@
[tox] [tox]
envlist=flake8,py37,py38,py39,py310,py311,upy,benchmark envlist=flake8,py36,py37,py38,py39,py310,upy,benchmark
skipsdist=True skipsdist=True
skip_missing_interpreters=True skip_missing_interpreters=True
[gh-actions] [gh-actions]
python = python =
3.6: py36
3.7: py37 3.7: py37
3.8: py38 3.8: py38
3.9: py39 3.9: py39
3.10: py310 3.10: py310
3.11: py311
pypy3: pypy3 pypy3: pypy3
[testenv] [testenv]
commands= commands=
pip install -e . pip install -e .
pytest -p no:logging --cov=src --cov-config=.coveragerc --cov-branch --cov-report=term-missing --cov-report=xml pytest -p no:logging --cov=src --cov-config=.coveragerc --cov-branch --cov-report=term-missing
deps= deps=
pytest pytest
pytest-cov pytest-cov
@@ -31,11 +31,11 @@ commands=
flake8 --ignore=W503 --exclude src/utemplate,tests/libs src tests examples flake8 --ignore=W503 --exclude src/utemplate,tests/libs src tests examples
[testenv:upy] [testenv:upy]
allowlist_externals=sh whitelist_externals=sh
commands=sh -c "bin/micropython run_tests.py" commands=sh -c "bin/micropython run_tests.py"
[testenv:upy-mac] [testenv:upy-mac]
allowlist_externals=micropython whitelist_externals=micropython
commands=micropython run_tests.py commands=micropython run_tests.py
deps= deps=