{{ post.title }}
16 |{{ post.body }}
23 |├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGES.md ├── LICENSE ├── README.md ├── examples ├── AsyncProgressBar │ ├── README.md │ ├── progress_bar.py │ └── requirements.txt ├── aioflaskr │ ├── .flaskenv │ ├── LICENSE │ ├── README.md │ ├── flaskr │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── blog.py │ │ ├── models.py │ │ ├── static │ │ │ └── style.css │ │ └── templates │ │ │ ├── auth │ │ │ ├── login.html │ │ │ └── register.html │ │ │ ├── base.html │ │ │ └── blog │ │ │ ├── create.html │ │ │ ├── index.html │ │ │ └── update.html │ ├── requirements.txt │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_auth.py │ │ ├── test_blog.py │ │ └── test_init.py ├── g │ └── app.py ├── hello_world │ ├── app.py │ └── templates │ │ └── index.html ├── login │ └── app.py ├── quotes-aiohttp │ ├── README.md │ └── quotes.py └── quotes-requests │ ├── README.md │ ├── quotes.py │ └── quotes_app.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── aioflask │ ├── __init__.py │ ├── app.py │ ├── asgi.py │ ├── cli.py │ ├── ctx.py │ ├── patch.py │ ├── patched │ ├── __init__.py │ └── flask_login │ │ └── __init__.py │ ├── templating.py │ └── testing.py ├── tests ├── __init__.py ├── templates │ └── template.html ├── test_app.py ├── test_cli.py ├── test_ctx.py ├── test_patch.py ├── test_templating.py └── utils.py └── tox.ini /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-python@v2 16 | - run: python -m pip install --upgrade pip wheel 17 | - run: pip install tox tox-gh-actions 18 | - run: tox -eflake8 19 | tests: 20 | name: tests 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | python: ['3.7', '3.8', '3.9', '3.10'] 25 | fail-fast: false 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python }} 32 | - run: python -m pip install --upgrade pip wheel 33 | - run: pip install tox tox-gh-actions 34 | - run: tox 35 | coverage: 36 | name: coverage 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-python@v2 41 | - run: python -m pip install --upgrade pip wheel 42 | - run: pip install tox tox-gh-actions codecov 43 | - run: tox 44 | - run: codecov 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | extra_requirements: 16 | - docs 17 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # aioflask change log 2 | 3 | **Release 0.4.0** - 2021-08-18 4 | 5 | - Support for app factory functions with uvicorn ([commit](https://github.com/miguelgrinberg/aioflask/commit/7f51ca835a5b581b28915b9818428ea09f720081)) 6 | - Make app context async ([commit](https://github.com/miguelgrinberg/aioflask/commit/d07ea5449389ae58b286ceff389386e9481e6715)) 7 | - Make request context async ([commit](https://github.com/miguelgrinberg/aioflask/commit/4232b2819ad7cf3ed386578a679ba2cbc75f91b0)) 8 | - Make test and cli runner clients async ([commit](https://github.com/miguelgrinberg/aioflask/commit/12408cb1d6018fabac8d2749607687164fb1da50)) 9 | - Patcher for 3rd party decorators without async view support ([commit](https://github.com/miguelgrinberg/aioflask/commit/b7d4433acd153c43463bd047ddfa19b8c2087078)) 10 | - Flask-Login support ([commit #1](https://github.com/miguelgrinberg/aioflask/commit/cbe8abcc0d890bc03787b75ba3c7cb78d5333f38)) ([commit #2](https://github.com/miguelgrinberg/aioflask/commit/e0ab0e0fe1a3b51c3dc3b35abc47b21002f034c3)) 11 | - Fix handling of application context ([commit](https://github.com/miguelgrinberg/aioflask/commit/f0b14856b58bd1e85b2b054cd6b3028da0f89091)) ([commit #3](https://github.com/miguelgrinberg/aioflask/commit/a6f5a67a1d9eaa4046d075c1417f5c042dd30c38)) 12 | - More unit tests ([commit](https://github.com/miguelgrinberg/aioflask/commit/cf061caa60c9c32975db7560de6c6e8dbe746e7d)) ([commit](https://github.com/miguelgrinberg/aioflask/commit/28f6bd5d62baa8857310aedd2ba728ab3e7322b6)) ([commit](https://github.com/miguelgrinberg/aioflask/commit/0b5f9e7bb0ba98f3c291dd5aa2c2e55ebce4aa61)) ([commit](https://github.com/miguelgrinberg/aioflask/commit/d4275e15474906c30acb80acac0a41766ef1d5d7)) 13 | - Update example documentation to use `flask aiorun` ([commit](https://github.com/miguelgrinberg/aioflask/commit/634ee10b7cbc934fa70d512a66334e78ddc39b3a)) 14 | 15 | **Release 0.3.0** - 2021-06-07 16 | 17 | - Test client support, and some more unit tests ([commit](https://github.com/miguelgrinberg/aioflask/commit/c765e12f6382d685bbec1861dac062c13d63aea3)) 18 | - Started a change log ([commit](https://github.com/miguelgrinberg/aioflask/commit/e6f4c3a87964fb2e5ea3f4464853c2a1d5ecfc29)) 19 | - Improved example code ([commit](https://github.com/miguelgrinberg/aioflask/commit/496ce73f3f0ecb1bdbdd25bd957ce08e6742c191)) 20 | - One more example ([commit](https://github.com/miguelgrinberg/aioflask/commit/7edab525809f7ba19562f67bb363a033563d6158)) 21 | 22 | **Release 0.2.0** - 2021-05-15 23 | 24 | - Flask 2.x changes ([commit](https://github.com/miguelgrinberg/aioflask/commit/52aef31fb9a7f8fe6a54b156fe257db1300c0ca6)) 25 | - Update README.md ([commit](https://github.com/miguelgrinberg/aioflask/commit/c232561ff3e1c954c49ab362be030da854ceb8ba)) 26 | - codecov.io integration ([commit](https://github.com/miguelgrinberg/aioflask/commit/d558dfde5f0717dc6f9b6ff0cedec142ffe60335)) 27 | - github actions build ([commit](https://github.com/miguelgrinberg/aioflask/commit/c5f43dacae3d8c73ac0c55a7a58b7c9ac985195a)) 28 | 29 | **Release 0.1** - 2020-11-07 30 | 31 | - async render_template and CLI commands ([commit](https://github.com/miguelgrinberg/aioflask/commit/2e6944c111bd581e1c0eb345ffe88cb1ec014140)) 32 | - travis builds ([commit](https://github.com/miguelgrinberg/aioflask/commit/5834c8526fffe424bccfcbe62aa03e33c81b3018)) 33 | - app.run implementation and debug mode fixes ([commit](https://github.com/miguelgrinberg/aioflask/commit/2dc8426b5e5e52309639aa31db1f845c44226259)) 34 | - add note about the experimental nature of this thing ([commit](https://github.com/miguelgrinberg/aioflask/commit/f027e5ba95cc16ed3c513525d88a197c22001784)) 35 | - initial version ([commit](https://github.com/miguelgrinberg/aioflask/commit/4f1d1a343642fa88f76a4cc064f1d7268c9d7dc7)) 36 | - Initial commit ([commit](https://github.com/miguelgrinberg/aioflask/commit/b02c360cae72d2f7dd479c93e0cd7517d4dce259)) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Miguel Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aioflask 2 | 3 |  [](https://codecov.io/gh/miguelgrinberg/aioflask) 4 | 5 | Flask 2.x running on asyncio! 6 | 7 | Is there a purpose for this, now that Flask 2.0 is out with support for async 8 | views? Yes! Flask's own support for async handlers is very limited, as the 9 | application still runs inside a WSGI web server, which severely limits 10 | scalability. With aioflask you get a true ASGI application, running in a 100% 11 | async environment. 12 | 13 | WARNING: This is an experiment at this point. Not at all production ready! 14 | 15 | ## Quick start 16 | 17 | To use async view functions and other handlers, use the `aioflask` package 18 | instead of `flask`. 19 | 20 | The `aioflask.Flask` class is a subclass of `flask.Flask` that changes a few 21 | minor things to help the application run properly under the asyncio loop. In 22 | particular, it overrides the following aspects of the application instance: 23 | 24 | - The `route`, `before_request`, `before_first_request`, `after_request`, 25 | `teardown_request`, `teardown_appcontext`, `errorhandler` and `cli.command` 26 | decorators accept coroutines as well as regular functions. The handlers all 27 | run inside an asyncio loop, so when using regular functions, care must be 28 | taken to not block. 29 | - The WSGI callable entry point is replaced with an ASGI equivalent. 30 | - The `run()` method uses uvicorn as web server. 31 | 32 | There are also changes outside of the `Flask` class: 33 | 34 | - The `flask aiorun` command starts an ASGI application using the uvicorn web 35 | server. 36 | - The `render_template()` and `render_template_string()` functions are 37 | asynchronous and must be awaited. 38 | - The context managers for the Flask application and request contexts are 39 | async. 40 | - The test client and test CLI runner use coroutines. 41 | 42 | ## Example 43 | 44 | ```python 45 | import asyncio 46 | from aioflask import Flask, render_template 47 | 48 | app = Flask(__name__) 49 | 50 | @app.route('/') 51 | async def index(): 52 | await asyncio.sleep(1) 53 | return await render_template('index.html') 54 | ``` 55 | -------------------------------------------------------------------------------- /examples/AsyncProgressBar/README.md: -------------------------------------------------------------------------------- 1 | AsyncProgressBar 2 | ================ 3 | 4 | This is the *AsyncProgressBar* from Quart ported to Flask. You need to have a 5 | Redis server running on localhost:6379 for this example to run. 6 | -------------------------------------------------------------------------------- /examples/AsyncProgressBar/progress_bar.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import aioredis 4 | import redis 5 | from aioflask import Flask, request, url_for, jsonify 6 | 7 | app = Flask(__name__) 8 | 9 | sr = redis.StrictRedis(host='localhost', port=6379) 10 | sr.execute_command('FLUSHDB') 11 | 12 | 13 | async def some_work(): 14 | global aredis 15 | await aredis.set('state', 'running') 16 | work_to_do = range(1, 26) 17 | await aredis.set('length_of_work', len(work_to_do)) 18 | for i in work_to_do: 19 | await aredis.set('processed', i) 20 | await asyncio.sleep(random.random()) 21 | await aredis.set('state', 'ready') 22 | await aredis.set('percent', 100) 23 | 24 | 25 | @app.route('/check_status/') 26 | async def check_status(): 27 | global aredis, sr 28 | status = dict() 29 | try: 30 | if await aredis.get('state') == b'running': 31 | if await aredis.get('processed') != await aredis.get('lastProcessed'): 32 | await aredis.set('percent', round( 33 | int(await aredis.get('processed')) / int(await aredis.get('length_of_work')) * 100, 2)) 34 | await aredis.set('lastProcessed', str(await aredis.get('processed'))) 35 | except: 36 | pass 37 | 38 | try: 39 | status['state'] = sr.get('state').decode() 40 | status['processed'] = sr.get('processed').decode() 41 | status['length_of_work'] = sr.get('length_of_work').decode() 42 | status['percent_complete'] = sr.get('percent').decode() 43 | except: 44 | status['state'] = sr.get('state') 45 | status['processed'] = sr.get('processed') 46 | status['length_of_work'] = sr.get('length_of_work') 47 | status['percent_complete'] = sr.get('percent') 48 | 49 | status['hint'] = 'refresh me.' 50 | 51 | return jsonify(status) 52 | 53 | 54 | @app.route('/progress/') 55 | async def progress(): 56 | return """ 57 | 58 | 59 |
60 | 61 | 62 |{{ post.body }}
23 |Logged in user: {current_user.id}
28 | 31 | 32 | ''' 33 | 34 | 35 | @app.route('/login', methods=['GET', 'POST']) 36 | def login(): 37 | if request.method == 'GET': 38 | return ''' 39 | 40 | 41 | 45 | 46 | ''' 47 | else: 48 | login_user(User(request.form['username'])) 49 | return redirect(request.args.get('next', '/')) 50 | 51 | 52 | @app.route('/logout', methods=['POST']) 53 | def logout(): 54 | logout_user() 55 | return redirect('/') 56 | -------------------------------------------------------------------------------- /examples/quotes-aiohttp/README.md: -------------------------------------------------------------------------------- 1 | Quotes 2 | ====== 3 | 4 | Returns 10 famous quotes each time the page is refreshed. Quotes are obtained 5 | by sending concurrent HTTP requests to a Quotes API with the aiohttp 6 | asynchronous client. 7 | 8 | To run this example, set `FLASK_APP=quotes.py` in your environment and then use 9 | the standard `flask aiorun` command to start the server. 10 | -------------------------------------------------------------------------------- /examples/quotes-aiohttp/quotes.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | from aioflask import Flask, render_template_string 4 | 5 | app = Flask(__name__) 6 | template = ''' 7 | 8 | 9 |"{{ quote.content }}" — {{ quote.author }}
15 | {% endfor %} 16 | 17 | ''' 18 | 19 | 20 | async def get_quote(session): 21 | response = await session.get('https://api.quotable.io/random') 22 | return await response.json() 23 | 24 | 25 | @app.route('/') 26 | async def index(): 27 | async with aiohttp.ClientSession() as session: 28 | tasks = [get_quote(session) for _ in range(10)] 29 | quotes = await asyncio.gather(*tasks) 30 | return await render_template_string(template, quotes=quotes) 31 | -------------------------------------------------------------------------------- /examples/quotes-requests/README.md: -------------------------------------------------------------------------------- 1 | Quotes 2 | ====== 3 | 4 | Returns 10 famous quotes each time the page is refreshed. Quotes are obtained 5 | by sending concurrent HTTP requests to a Quotes API with the requests client. 6 | 7 | This example shows how you can incorporate blocking code into your aioflask 8 | application without blocking the asyncio loop. 9 | 10 | To run this example, set `FLASK_APP=quotes.py` in your environment and then use 11 | the standard `flask aiorun` command to start the server. 12 | -------------------------------------------------------------------------------- /examples/quotes-requests/quotes.py: -------------------------------------------------------------------------------- 1 | import greenletio 2 | 3 | # import the application with blocking functions monkey patched 4 | with greenletio.patch_blocking(): 5 | from quotes_app import app 6 | -------------------------------------------------------------------------------- /examples/quotes-requests/quotes_app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aioflask import Flask, render_template_string 3 | from greenletio import async_ 4 | import requests 5 | 6 | app = Flask(__name__) 7 | template = ''' 8 | 9 | 10 |"{{ quote.content }}" — {{ quote.author }}
16 | {% endfor %} 17 | 18 | ''' 19 | 20 | 21 | # this is a blocking function that is converted to asynchronous with 22 | # greenletio's @async_ decorator. For this to work, all the low-level I/O 23 | # functions started from this function must be asynchronous, which can be 24 | # achieved with greenletio's monkey patching feature. 25 | @async_ 26 | def get_quote(): 27 | response = requests.get('https://api.quotable.io/random') 28 | return response.json() 29 | 30 | 31 | @app.route('/') 32 | async def index(): 33 | tasks = [get_quote() for _ in range(10)] 34 | quotes = await asyncio.gather(*tasks) 35 | return await render_template_string(template, quotes=quotes) 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = aioflask 3 | version = 0.4.1.dev0 4 | author = Miguel Grinberg 5 | author_email = miguel.grinberg@gmail.com 6 | description = Flask running on asyncio. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/miguelgrinberg/aioflask 10 | project_urls = 11 | Bug Tracker = https://github.com/miguelgrinberg/aioflask/issues 12 | classifiers = 13 | Intended Audience :: Developers 14 | Programming Language :: Python :: 3 15 | License :: OSI Approved :: MIT License 16 | Operating System :: OS Independent 17 | 18 | [options] 19 | zip_safe = False 20 | include_package_data = True 21 | package_dir = 22 | = src 23 | packages = find: 24 | python_requires = >=3.6 25 | install_requires = 26 | greenletio 27 | flask >= 2 28 | uvicorn 29 | 30 | [options.packages.find] 31 | where = src 32 | 33 | [options.entry_points] 34 | flask.commands = 35 | aiorun = aioflask.cli:run_command 36 | 37 | [options.extras_require] 38 | docs = 39 | sphinx 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /src/aioflask/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import * 2 | from .app import Flask 3 | from .templating import render_template, render_template_string 4 | from .testing import FlaskClient 5 | -------------------------------------------------------------------------------- /src/aioflask/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import wraps 3 | from inspect import iscoroutinefunction 4 | import os 5 | from flask.app import * 6 | from flask.app import Flask as OriginalFlask 7 | from flask import cli 8 | from flask.globals import _app_ctx_stack, _request_ctx_stack 9 | from flask.helpers import get_debug_flag, get_env, get_load_dotenv 10 | from greenletio import await_ 11 | import uvicorn 12 | from .asgi import WsgiToAsgiInstance 13 | from .cli import show_server_banner, AppGroup 14 | from .ctx import AppContext, RequestContext 15 | from .testing import FlaskClient, FlaskCliRunner 16 | 17 | 18 | class Flask(OriginalFlask): 19 | def __init__(self, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | self.cli = AppGroup() 22 | self.jinja_options['enable_async'] = True 23 | self.test_client_class = FlaskClient 24 | self.test_cli_runner_class = FlaskCliRunner 25 | self.async_fixed = False 26 | 27 | def ensure_sync(self, func): 28 | if not iscoroutinefunction(func): 29 | return func 30 | 31 | def wrapped(*args, **kwargs): 32 | appctx = _app_ctx_stack.top 33 | reqctx = _request_ctx_stack.top 34 | 35 | async def _coro(): 36 | # app context is push internally to avoid changing reference 37 | # counts and emitting duplicate signals 38 | _app_ctx_stack.push(appctx) 39 | if reqctx: 40 | _request_ctx_stack.push(reqctx) 41 | ret = await func(*args, **kwargs) 42 | if reqctx: 43 | _request_ctx_stack.pop() 44 | _app_ctx_stack.pop() 45 | return ret 46 | 47 | return await_(_coro()) 48 | 49 | return wrapped 50 | 51 | def app_context(self): 52 | return AppContext(self) 53 | 54 | def request_context(self, environ): 55 | return RequestContext(self, environ) 56 | 57 | def _fix_async(self): # pragma: no cover 58 | self.async_fixed = True 59 | 60 | if os.environ.get('AIOFLASK_USE_DEBUGGER') == 'true': 61 | os.environ['WERKZEUG_RUN_MAIN'] = 'true' 62 | from werkzeug.debug import DebuggedApplication 63 | self.wsgi_app = DebuggedApplication(self.wsgi_app, evalex=True) 64 | 65 | async def asgi_app(self, scope, receive, send): # pragma: no cover 66 | if not self.async_fixed: 67 | self._fix_async() 68 | return await WsgiToAsgiInstance(self.wsgi_app)(scope, receive, send) 69 | 70 | async def __call__(self, scope, receive, send=None): # pragma: no cover 71 | if send is None: 72 | # we were called with two arguments, so this is likely a WSGI app 73 | raise RuntimeError('The WSGI interface is not supported by ' 74 | 'aioflask, use an ASGI web server instead.') 75 | return await self.asgi_app(scope, receive, send) 76 | 77 | def run(self, host=None, port=None, debug=None, load_dotenv=True, 78 | **options): 79 | 80 | if get_load_dotenv(load_dotenv): 81 | cli.load_dotenv() 82 | 83 | # if set, let env vars override previous values 84 | if "FLASK_ENV" in os.environ: 85 | self.env = get_env() 86 | self.debug = get_debug_flag() 87 | elif "FLASK_DEBUG" in os.environ: 88 | self.debug = get_debug_flag() 89 | 90 | # debug passed to method overrides all other sources 91 | if debug is not None: 92 | self.debug = bool(debug) 93 | 94 | server_name = self.config.get("SERVER_NAME") 95 | sn_host = sn_port = None 96 | 97 | if server_name: 98 | sn_host, _, sn_port = server_name.partition(":") 99 | 100 | if not host: 101 | if sn_host: 102 | host = sn_host 103 | else: 104 | host = "127.0.0.1" 105 | 106 | if port or port == 0: 107 | port = int(port) 108 | elif sn_port: 109 | port = int(sn_port) 110 | else: 111 | port = 5000 112 | 113 | options.setdefault("use_reloader", self.debug) 114 | options.setdefault("use_debugger", self.debug) 115 | options.setdefault("threaded", True) 116 | options.setdefault("workers", 1) 117 | 118 | certfile = None 119 | keyfile = None 120 | cert = options.get('ssl_context') 121 | if cert is not None and len(cert) == 2: 122 | certfile = cert[0] 123 | keyfile = cert[1] 124 | elif cert == 'adhoc': 125 | raise RuntimeError( 126 | 'Aad-hoc certificates are not supported by aioflask.') 127 | 128 | if debug: 129 | os.environ['FLASK_DEBUG'] = 'true' 130 | 131 | if options['use_debugger']: 132 | os.environ['AIOFLASK_USE_DEBUGGER'] = 'true' 133 | 134 | show_server_banner(self.env, self.debug, self.name, False) 135 | 136 | uvicorn.run( 137 | self.import_name + ':app', 138 | host=host, 139 | port=port, 140 | reload=options['use_reloader'], 141 | workers=options['workers'], 142 | log_level='debug' if self.debug else 'info', 143 | ssl_certfile=certfile, 144 | ssl_keyfile=keyfile, 145 | ) 146 | -------------------------------------------------------------------------------- /src/aioflask/asgi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from tempfile import SpooledTemporaryFile 3 | from greenletio import async_, await_ 4 | 5 | 6 | class wsgi_to_asgi: # pragma: no cover 7 | """Wraps a WSGI application to make it into an ASGI application.""" 8 | 9 | def __init__(self, wsgi_application): 10 | self.wsgi_application = wsgi_application 11 | 12 | async def __call__(self, scope, receive, send): 13 | """ASGI application instantiation point. 14 | 15 | We return a new WsgiToAsgiInstance here with the WSGI app 16 | and the scope, ready to respond when it is __call__ed. 17 | """ 18 | await WsgiToAsgiInstance(self.wsgi_application)(scope, receive, send) 19 | 20 | 21 | class WsgiToAsgiInstance: # pragma: no cover 22 | """Per-socket instance of a wrapped WSGI application""" 23 | 24 | def __init__(self, wsgi_application): 25 | self.wsgi_application = wsgi_application 26 | self.response_started = False 27 | 28 | async def __call__(self, scope, receive, send): 29 | if scope["type"] != "http": 30 | raise ValueError("WSGI wrapper received a non-HTTP scope") 31 | self.scope = scope 32 | with SpooledTemporaryFile(max_size=65536) as body: 33 | # Alright, wait for the http.request messages 34 | while True: 35 | message = await receive() 36 | if message["type"] != "http.request": 37 | raise ValueError( 38 | "WSGI wrapper received a non-HTTP-request message") 39 | body.write(message.get("body", b"")) 40 | if not message.get("more_body"): 41 | break 42 | body.seek(0) 43 | 44 | # Wrap send so it can be called from the subthread 45 | self.sync_send = await_(send) 46 | # Call the WSGI app 47 | await self.run_wsgi_app(body) 48 | 49 | def build_environ(self, scope, body): 50 | """Builds a scope and request body into a WSGI environ object.""" 51 | environ = { 52 | "REQUEST_METHOD": scope["method"], 53 | "SCRIPT_NAME": scope.get("root_path", ""), 54 | "PATH_INFO": scope["path"], 55 | "QUERY_STRING": scope["query_string"].decode("ascii"), 56 | "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], 57 | "wsgi.version": (1, 0), 58 | "wsgi.url_scheme": scope.get("scheme", "http"), 59 | "wsgi.input": body, 60 | "wsgi.errors": sys.stderr, 61 | "wsgi.multithread": True, 62 | "wsgi.multiprocess": True, 63 | "wsgi.run_once": False, 64 | } 65 | # Get server name and port - required in WSGI, not in ASGI 66 | if "server" in scope: 67 | environ["SERVER_NAME"] = scope["server"][0] 68 | environ["SERVER_PORT"] = str(scope["server"][1]) 69 | else: 70 | environ["SERVER_NAME"] = "localhost" 71 | environ["SERVER_PORT"] = "80" 72 | 73 | if "client" in scope: 74 | environ["REMOTE_ADDR"] = scope["client"][0] 75 | 76 | # Go through headers and make them into environ entries 77 | for name, value in self.scope.get("headers", []): 78 | name = name.decode("latin1") 79 | if name == "content-length": 80 | corrected_name = "CONTENT_LENGTH" 81 | elif name == "content-type": 82 | corrected_name = "CONTENT_TYPE" 83 | else: 84 | corrected_name = "HTTP_%s" % name.upper().replace("-", "_") 85 | # HTTPbis say only ASCII chars are allowed in headers, but we 86 | # latin1 just in case 87 | value = value.decode("latin1") 88 | if corrected_name in environ: 89 | value = environ[corrected_name] + "," + value 90 | environ[corrected_name] = value 91 | return environ 92 | 93 | def start_response(self, status, response_headers, exc_info=None): 94 | """WSGI start_response callable.""" 95 | 96 | # Don't allow re-calling once response has begun 97 | if self.response_started: 98 | raise exc_info[1].with_traceback(exc_info[2]) 99 | # Don't allow re-calling without exc_info 100 | if hasattr(self, "response_start") and exc_info is None: 101 | raise ValueError( 102 | "You cannot call start_response a second time without exc_info" 103 | ) 104 | # Extract status code 105 | status_code, _ = status.split(" ", 1) 106 | status_code = int(status_code) 107 | # Extract headers 108 | headers = [ 109 | (name.lower().encode("ascii"), value.encode("ascii")) 110 | for name, value in response_headers 111 | ] 112 | # Build and send response start message. 113 | self.response_start = { 114 | "type": "http.response.start", 115 | "status": status_code, 116 | "headers": headers, 117 | } 118 | 119 | @async_ 120 | def run_wsgi_app(self, body): 121 | """WSGI app greenlet.""" 122 | # Translate the scope and incoming request body into a WSGI environ 123 | environ = self.build_environ(self.scope, body) 124 | # Run the WSGI app 125 | for output in self.wsgi_application(environ, self.start_response): 126 | # If this is the first response, include the response headers 127 | if not self.response_started: 128 | self.response_started = True 129 | self.sync_send(self.response_start) 130 | self.sync_send({"type": "http.response.body", "body": output, 131 | "more_body": True}) 132 | # Close connection 133 | if not self.response_started: 134 | self.response_started = True 135 | self.sync_send(self.response_start) 136 | self.sync_send({"type": "http.response.body"}) 137 | -------------------------------------------------------------------------------- /src/aioflask/cli.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from inspect import iscoroutinefunction 3 | import os 4 | import sys 5 | 6 | from flask.cli import * 7 | from flask.cli import AppGroup, ScriptInfo, update_wrapper, \ 8 | SeparatedPathType, pass_script_info, get_debug_flag, NoAppException, \ 9 | prepare_import 10 | from flask.cli import _validate_key 11 | from flask.globals import _app_ctx_stack 12 | from flask.helpers import get_env 13 | from greenletio import await_ 14 | from werkzeug.utils import import_string 15 | import click 16 | import uvicorn 17 | 18 | try: 19 | import ssl 20 | except ImportError: # pragma: no cover 21 | ssl = None 22 | 23 | OriginalAppGroup = AppGroup 24 | 25 | 26 | def _ensure_sync(func, with_appcontext=False): 27 | if not iscoroutinefunction(func): 28 | return func 29 | 30 | def decorated(*args, **kwargs): 31 | if with_appcontext: 32 | appctx = _app_ctx_stack.top 33 | 34 | @await_ 35 | async def _coro(): 36 | with appctx: 37 | return await func(*args, **kwargs) 38 | else: 39 | @await_ 40 | async def _coro(): 41 | return await func(*args, **kwargs) 42 | 43 | return _coro() 44 | 45 | return decorated 46 | 47 | 48 | def with_appcontext(f): 49 | """Wraps a callback so that it's guaranteed to be executed with the 50 | script's application context. If callbacks are registered directly 51 | to the ``app.cli`` object then they are wrapped with this function 52 | by default unless it's disabled. 53 | """ 54 | 55 | @click.pass_context 56 | def decorator(__ctx, *args, **kwargs): 57 | with __ctx.ensure_object(ScriptInfo).load_app().app_context(): 58 | return __ctx.invoke(_ensure_sync(f, True), *args, **kwargs) 59 | 60 | return update_wrapper(decorator, f) 61 | 62 | 63 | class AppGroup(OriginalAppGroup): 64 | """This works similar to a regular click :class:`~click.Group` but it 65 | changes the behavior of the :meth:`command` decorator so that it 66 | automatically wraps the functions in :func:`with_appcontext`. 67 | Not to be confused with :class:`FlaskGroup`. 68 | """ 69 | 70 | def command(self, *args, **kwargs): 71 | """This works exactly like the method of the same name on a regular 72 | :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` 73 | unless it's disabled by passing ``with_appcontext=False``. 74 | """ 75 | wrap_for_ctx = kwargs.pop("with_appcontext", True) 76 | 77 | def decorator(f): 78 | if wrap_for_ctx: 79 | f = with_appcontext(f) 80 | return click.Group.command(self, *args, **kwargs)(_ensure_sync(f)) 81 | 82 | return decorator 83 | 84 | 85 | def show_server_banner(env, debug, app_import_path, eager_loading): 86 | """Show extra startup messages the first time the server is run, 87 | ignoring the reloader. 88 | """ 89 | if app_import_path is not None: 90 | message = f" * Serving Flask app {app_import_path!r}" 91 | 92 | click.echo(message) 93 | 94 | click.echo(f" * Environment: {env}") 95 | 96 | if debug is not None: 97 | click.echo(f" * Debug mode: {'on' if debug else 'off'}") 98 | 99 | 100 | class CertParamType(click.ParamType): 101 | """Click option type for the ``--cert`` option. Allows either an 102 | existing file, the string ``'adhoc'``, or an import for a 103 | :class:`~ssl.SSLContext` object. 104 | """ 105 | 106 | name = "path" 107 | 108 | def __init__(self): 109 | self.path_type = click.Path(exists=True, dir_okay=False, 110 | resolve_path=True) 111 | 112 | def convert(self, value, param, ctx): 113 | if ssl is None: 114 | raise click.BadParameter('Using "--cert" requires Python to be ' 115 | 'compiled with SSL support.', 116 | ctx, param) 117 | 118 | try: 119 | return self.path_type(value, param, ctx) 120 | except click.BadParameter: 121 | value = click.STRING(value, param, ctx).lower() 122 | 123 | if value == "adhoc": 124 | raise click.BadParameter("Aad-hoc certificates are currently " 125 | "not supported by aioflask.", 126 | ctx, param) 127 | 128 | return value 129 | 130 | obj = import_string(value, silent=True) 131 | 132 | if isinstance(obj, ssl.SSLContext): 133 | return obj 134 | 135 | raise 136 | 137 | 138 | @click.command("run", short_help="Run a development server.") 139 | @click.option("--host", "-h", default="127.0.0.1", 140 | help="The interface to bind to.") 141 | @click.option("--port", "-p", default=5000, help="The port to bind to.") 142 | @click.option( 143 | "--cert", type=CertParamType(), 144 | help="Specify a certificate file to use HTTPS." 145 | ) 146 | @click.option( 147 | "--key", 148 | type=click.Path(exists=True, dir_okay=False, resolve_path=True), 149 | callback=_validate_key, 150 | expose_value=False, 151 | help="The key file to use when specifying a certificate.", 152 | ) 153 | @click.option( 154 | "--reload/--no-reload", 155 | default=None, 156 | help="Enable or disable the reloader. By default the reloader " 157 | "is active if debug is enabled.", 158 | ) 159 | @click.option( 160 | "--debugger/--no-debugger", 161 | default=None, 162 | help="Enable or disable the debugger. By default the debugger " 163 | "is active if debug is enabled.", 164 | ) 165 | @click.option( 166 | "--eager-loading/--lazy-loading", 167 | default=None, 168 | help="Enable or disable eager loading. By default eager " 169 | "loading is enabled if the reloader is disabled.", 170 | ) 171 | @click.option( 172 | "--with-threads/--without-threads", 173 | default=True, 174 | help="Enable or disable multithreading.", 175 | ) 176 | @click.option( 177 | "--extra-files", 178 | default=None, 179 | type=SeparatedPathType(), 180 | help=( 181 | "Extra files that trigger a reload on change. Multiple paths" 182 | f" are separated by {os.path.pathsep!r}." 183 | ), 184 | ) 185 | @pass_script_info 186 | def run_command(info, host, port, reload, debugger, eager_loading, 187 | with_threads, cert, extra_files): 188 | """Run a local development server. 189 | This server is for development purposes only. It does not provide 190 | the stability, security, or performance of production WSGI servers. 191 | The reloader and debugger are enabled by default if 192 | FLASK_ENV=development or FLASK_DEBUG=1. 193 | """ 194 | debug = get_debug_flag() 195 | 196 | if reload is None: 197 | reload = debug 198 | 199 | if debugger is None: 200 | debugger = debug 201 | if debugger: 202 | os.environ['AIOFLASK_USE_DEBUGGER'] = 'true' 203 | 204 | certfile = None 205 | keyfile = None 206 | if cert is not None and len(cert) == 2: 207 | certfile = cert[0] 208 | keyfile = cert[1] 209 | 210 | show_server_banner(get_env(), debug, info.app_import_path, eager_loading) 211 | 212 | app_import_path = info.app_import_path 213 | if app_import_path is None: 214 | for path in ('wsgi', 'app'): 215 | if os.path.exists(path) or os.path.exists(path + '.py'): 216 | app_import_path = path 217 | break 218 | if app_import_path is None: 219 | raise NoAppException( 220 | "Could not locate a Flask application. You did not provide " 221 | 'the "FLASK_APP" environment variable, and a "wsgi.py" or ' 222 | '"app.py" module was not found in the current directory.' 223 | ) 224 | if app_import_path.endswith('.py'): 225 | app_import_path = app_import_path[:-3] 226 | 227 | factory = False 228 | if app_import_path.endswith('()'): 229 | # TODO: this needs to be expanded to accept arguments for the factory 230 | # function 231 | app_import_path = app_import_path[:-2] 232 | factory = True 233 | 234 | if ':' not in app_import_path: 235 | app_import_path += ':app' 236 | 237 | import_name, app_name = app_import_path.split(':') 238 | import_name = prepare_import(import_name) 239 | 240 | uvicorn.run( 241 | import_name + ':' + app_name, 242 | factory=factory, 243 | host=host, 244 | port=port, 245 | reload=reload, 246 | workers=1, 247 | log_level='debug' if debug else 'info', 248 | ssl_certfile=certfile, 249 | ssl_keyfile=keyfile, 250 | ) 251 | 252 | # currently not supported: 253 | # - eager_loading 254 | # - with_threads 255 | # - adhoc certs 256 | # - extra_files 257 | -------------------------------------------------------------------------------- /src/aioflask/ctx.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from greenletio import async_ 3 | from flask.ctx import * 4 | from flask.ctx import AppContext as OriginalAppContext, \ 5 | RequestContext as OriginalRequestContext, _sentinel, _app_ctx_stack, \ 6 | _request_ctx_stack, appcontext_popped 7 | 8 | 9 | class AppContext(OriginalAppContext): 10 | async def apush(self): 11 | """Binds the app context to the current context.""" 12 | self.push() 13 | 14 | async def apop(self, exc=_sentinel): 15 | """Pops the app context.""" 16 | try: 17 | self._refcnt -= 1 18 | if self._refcnt <= 0: 19 | if exc is _sentinel: # pragma: no cover 20 | exc = sys.exc_info()[1] 21 | 22 | @async_ 23 | def do_teardown_async(): 24 | _app_ctx_stack.push(self) 25 | self.app.do_teardown_appcontext(exc) 26 | _app_ctx_stack.pop() 27 | 28 | await do_teardown_async() 29 | finally: 30 | rv = _app_ctx_stack.pop() 31 | assert rv is self, \ 32 | f"Popped wrong app context. ({rv!r} instead of {self!r})" 33 | appcontext_popped.send(self.app) 34 | 35 | async def __aenter__(self): 36 | await self.apush() 37 | return self 38 | 39 | async def __aexit__(self, exc_type, exc_value, tb): 40 | await self.apop(exc_value) 41 | 42 | 43 | class RequestContext(OriginalRequestContext): 44 | async def apush(self): 45 | self.push() 46 | 47 | async def apop(self, exc=_sentinel): 48 | app_ctx = self._implicit_app_ctx_stack.pop() 49 | clear_request = False 50 | 51 | try: 52 | if not self._implicit_app_ctx_stack: 53 | if hasattr(self, 'preserved'): # Flask < 2.2 54 | self.preserved = False 55 | self._preserved_exc = None 56 | if exc is _sentinel: # pragma: no cover 57 | exc = sys.exc_info()[1] 58 | 59 | @async_ 60 | def do_teardown(): 61 | _request_ctx_stack.push(self) 62 | self.app.do_teardown_request(exc) 63 | _request_ctx_stack.pop() 64 | 65 | await do_teardown() 66 | 67 | request_close = getattr(self.request, "close", None) 68 | if request_close is not None: # pragma: no branch 69 | request_close() 70 | clear_request = True 71 | finally: 72 | rv = _request_ctx_stack.pop() 73 | 74 | # get rid of circular dependencies at the end of the request 75 | # so that we don't require the GC to be active. 76 | if clear_request: 77 | rv.request.environ["werkzeug.request"] = None 78 | 79 | # Get rid of the app as well if necessary. 80 | if app_ctx is not None: 81 | await app_ctx.apop(exc) 82 | 83 | assert ( 84 | rv is self 85 | ), f"Popped wrong request context. ({rv!r} instead of {self!r})" 86 | 87 | async def aauto_pop(self, exc): 88 | if hasattr(self, 'preserved'): # Flask < 2.2 89 | if self.request.environ.get("flask._preserve_context") or ( 90 | exc is not None and self.app.preserve_context_on_exception 91 | ): # pragma: no cover 92 | self.preserved = True 93 | self._preserved_exc = exc 94 | else: 95 | await self.apop(exc) 96 | else: 97 | await self.apop(exc) 98 | 99 | async def __aenter__(self): 100 | await self.apush() 101 | return self 102 | 103 | async def __aexit__(self, exc_type, exc_value, tb): 104 | await self.aauto_pop(exc_value) 105 | -------------------------------------------------------------------------------- /src/aioflask/patch.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from aioflask import current_app 3 | 4 | 5 | def patch_decorator(decorator): 6 | def patched_decorator(f): 7 | @wraps(f) 8 | def ensure_sync(*a, **kw): 9 | return current_app.ensure_sync(f)(*a, **kw) 10 | 11 | return decorator(ensure_sync) 12 | return patched_decorator 13 | 14 | 15 | def patch_decorator_with_args(decorator): 16 | def patched_decorator(*args, **kwargs): 17 | def inner_patched_decorator(f): 18 | @wraps(f) 19 | def ensure_sync(*a, **kw): 20 | return current_app.ensure_sync(f)(*a, **kw) 21 | 22 | return decorator(*args, **kwargs)(ensure_sync) 23 | return inner_patched_decorator 24 | return patched_decorator 25 | 26 | 27 | def patch_decorator_method(class_, method_name): 28 | original_decorator = getattr(class_, method_name) 29 | 30 | def patched_decorator_method(self, f): 31 | @wraps(f) 32 | def ensure_sync(*a, **kw): 33 | return current_app.ensure_sync(f)(*a, **kw) 34 | 35 | return original_decorator(self, ensure_sync) 36 | return patched_decorator_method 37 | 38 | 39 | def patch_decorator_method_with_args(class_, method_name): 40 | original_decorator = getattr(class_, method_name) 41 | 42 | def patched_decorator_method(self, *args, **kwargs): 43 | def inner_patched_decorator_method(f): 44 | @wraps(f) 45 | def ensure_sync(*a, **kw): 46 | return current_app.ensure_sync(f)(*a, **kw) 47 | 48 | return original_decorator(self, *args, **kwargs)(ensure_sync) 49 | return inner_patched_decorator_method 50 | return patched_decorator_method 51 | -------------------------------------------------------------------------------- /src/aioflask/patched/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/aioflask/7f447e79c81ce7ca46edc5f5852845ff9806ce6a/src/aioflask/patched/__init__.py -------------------------------------------------------------------------------- /src/aioflask/patched/flask_login/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import sys 3 | from werkzeug.local import LocalProxy 4 | from aioflask import current_app, g 5 | from flask import _request_ctx_stack 6 | from aioflask.patch import patch_decorator, patch_decorator_method 7 | import flask_login 8 | from flask_login import login_required, fresh_login_required, \ 9 | LoginManager as OriginalLoginManager 10 | 11 | for symbol in flask_login.__all__: 12 | try: 13 | globals()[symbol] = getattr(flask_login, symbol) 14 | except AttributeError: 15 | pass 16 | 17 | 18 | def _user_context_processor(): 19 | return {'current_user': _get_user()} 20 | 21 | 22 | def _load_user(): 23 | # Obtain the current user and preserve it in the g object. Flask-Login 24 | # saves the user in a custom attribute of the request context, but that 25 | # doesn't work with aioflask because when a copy of the request context is 26 | # made, custom attributes are not carried over to the copy. 27 | current_app.login_manager._load_user() 28 | g.flask_login_current_user = _request_ctx_stack.top.user 29 | 30 | 31 | def _get_user(): 32 | # Return the current user. This function is linked to the current_user 33 | # context local, but unlike the original in Flask-Login, it does not 34 | # attempt to load the user, it just returns the user that was pre-loaded. 35 | # This avoids the somewhat tricky complication of triggering database 36 | # operations that need to be awaited, which would require using something 37 | # like (await current_user) 38 | if hasattr(g, 'flask_login_current_user'): 39 | return g.flask_login_current_user 40 | return current_app.login_manager.anonymous_user() 41 | 42 | 43 | class LoginManager(OriginalLoginManager): 44 | def init_app(self, app, add_context_processor=True): 45 | super().init_app(app, add_context_processor=False) 46 | if add_context_processor: 47 | app.context_processor(_user_context_processor) 48 | 49 | # To prevent the current_user context local from triggering I/O at a 50 | # random time when it is first referenced (which is a big complication 51 | # if the I/O is async and needs to be awaited), we force the user to be 52 | # loaded before each request. This isn't a perfect solution, because 53 | # a before request handler registered before this one will not see the 54 | # current user. 55 | app.before_request(_load_user) 56 | 57 | # the decorators that register callbacks need to be patched to support 58 | # async views 59 | user_loader = patch_decorator_method(OriginalLoginManager, 'user_loader') 60 | header_loader = patch_decorator_method( 61 | OriginalLoginManager, 'header_loader') 62 | request_loader = patch_decorator_method( 63 | OriginalLoginManager, 'request_loader') 64 | unauthorized_handler = patch_decorator_method( 65 | OriginalLoginManager, 'unauthorized_handler') 66 | needs_refresh_handler = patch_decorator_method( 67 | OriginalLoginManager, 'needs_refresh_handler') 68 | 69 | 70 | # patch the two login_required decorators so that they accept async views 71 | login_required = patch_decorator(login_required) 72 | fresh_login_required = patch_decorator(fresh_login_required) 73 | 74 | 75 | # redefine the current_user context local 76 | current_user = LocalProxy(_get_user) 77 | 78 | # patch the _get_user() function in the flask_login.utils module so that any 79 | # calls to get current_user in Flask-Login functions are redirected here 80 | setattr(sys.modules['flask_login.utils'], '_get_user', _get_user) 81 | -------------------------------------------------------------------------------- /src/aioflask/templating.py: -------------------------------------------------------------------------------- 1 | from flask.templating import * 2 | from flask.templating import _app_ctx_stack, before_render_template, \ 3 | template_rendered 4 | 5 | 6 | async def _render(template, context, app): 7 | """Renders the template and fires the signal""" 8 | 9 | before_render_template.send(app, template=template, context=context) 10 | rv = await template.render_async(context) 11 | template_rendered.send(app, template=template, context=context) 12 | return rv 13 | 14 | 15 | async def render_template(template_name_or_list, **context): 16 | """Renders a template from the template folder with the given 17 | context. 18 | :param template_name_or_list: the name of the template to be 19 | rendered, or an iterable with template names 20 | the first one existing will be rendered 21 | :param context: the variables that should be available in the 22 | context of the template. 23 | """ 24 | ctx = _app_ctx_stack.top 25 | ctx.app.update_template_context(context) 26 | return await _render( 27 | ctx.app.jinja_env.get_or_select_template(template_name_or_list), 28 | context, 29 | ctx.app, 30 | ) 31 | 32 | 33 | async def render_template_string(source, **context): 34 | """Renders a template from the given template source string 35 | with the given context. Template variables will be autoescaped. 36 | :param source: the source code of the template to be 37 | rendered 38 | :param context: the variables that should be available in the 39 | context of the template. 40 | """ 41 | ctx = _app_ctx_stack.top 42 | ctx.app.update_template_context(context) 43 | return await _render(ctx.app.jinja_env.from_string(source), context, 44 | ctx.app) 45 | -------------------------------------------------------------------------------- /src/aioflask/testing.py: -------------------------------------------------------------------------------- 1 | from flask.testing import * 2 | from flask.testing import FlaskClient as OriginalFlaskClient, \ 3 | FlaskCliRunner as OriginalFlaskCliRunner 4 | from flask import _request_ctx_stack 5 | from werkzeug.test import run_wsgi_app 6 | from greenletio import async_ 7 | 8 | 9 | class FlaskClient(OriginalFlaskClient): 10 | def run_wsgi_app(self, environ, buffered=False): 11 | """Runs the wrapped WSGI app with the given environment. 12 | :meta private: 13 | """ 14 | if self.cookie_jar is not None: 15 | self.cookie_jar.inject_wsgi(environ) 16 | 17 | rv = run_wsgi_app(self.application.wsgi_app, environ, 18 | buffered=buffered) 19 | 20 | if self.cookie_jar is not None: 21 | self.cookie_jar.extract_wsgi(environ, rv[2]) 22 | 23 | return rv 24 | 25 | async def get(self, *args, **kwargs): 26 | return await async_(super().get)(*args, **kwargs) 27 | 28 | async def post(self, *args, **kwargs): 29 | return await async_(super().post)(*args, **kwargs) 30 | 31 | async def put(self, *args, **kwargs): 32 | return await async_(super().put)(*args, **kwargs) 33 | 34 | async def patch(self, *args, **kwargs): 35 | return await async_(super().patch)(*args, **kwargs) 36 | 37 | async def delete(self, *args, **kwargs): 38 | return await async_(super().delete)(*args, **kwargs) 39 | 40 | async def head(self, *args, **kwargs): 41 | return await async_(super().head)(*args, **kwargs) 42 | 43 | async def options(self, *args, **kwargs): 44 | return await async_(super().options)(*args, **kwargs) 45 | 46 | async def trace(self, *args, **kwargs): 47 | return await async_(super().trace)(*args, **kwargs) 48 | 49 | async def __aenter__(self): 50 | if self.preserve_context: 51 | raise RuntimeError("Cannot nest client invocations") 52 | self.preserve_context = True 53 | return self 54 | 55 | async def __aexit__(self, exc_type, exc_value, tb): 56 | self.preserve_context = False 57 | 58 | # Normally the request context is preserved until the next 59 | # request in the same thread comes. When the client exits we 60 | # want to clean up earlier. Pop request contexts until the stack 61 | # is empty or a non-preserved one is found. 62 | while True: 63 | top = _request_ctx_stack.top 64 | 65 | if top is not None and top.preserved: 66 | await top.apop() 67 | else: 68 | break 69 | 70 | 71 | class FlaskCliRunner(OriginalFlaskCliRunner): 72 | async def invoke(self, *args, **kwargs): 73 | return await async_(super().invoke)(*args, **kwargs) 74 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/aioflask/7f447e79c81ce7ca46edc5f5852845ff9806ce6a/tests/__init__.py -------------------------------------------------------------------------------- /tests/templates/template.html: -------------------------------------------------------------------------------- 1 | {{ g.x }}{{ session.y }} 2 | -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import unittest 4 | from unittest import mock 5 | import aioflask 6 | from .utils import async_test 7 | 8 | 9 | class TestApp(unittest.TestCase): 10 | @async_test 11 | async def test_app(self): 12 | app = aioflask.Flask(__name__) 13 | 14 | @app.route('/async') 15 | async def async_route(): 16 | await asyncio.sleep(0) 17 | assert aioflask.current_app._get_current_object() == app 18 | return 'async' 19 | 20 | @app.route('/sync') 21 | def sync_route(): 22 | assert aioflask.current_app._get_current_object() == app 23 | return 'sync' 24 | 25 | client = app.test_client() 26 | response = await client.get('/async') 27 | assert response.data == b'async' 28 | response = await client.get('/sync') 29 | assert response.data == b'sync' 30 | 31 | @async_test 32 | async def test_g(self): 33 | app = aioflask.Flask(__name__) 34 | app.secret_key = 'secret' 35 | 36 | @app.before_request 37 | async def async_before_request(): 38 | aioflask.g.asyncvar = 'async' 39 | 40 | @app.before_request 41 | def sync_before_request(): 42 | aioflask.g.syncvar = 'sync' 43 | 44 | @app.route('/async') 45 | async def async_route(): 46 | aioflask.session['a'] = 'async' 47 | return f'{aioflask.g.asyncvar}-{aioflask.g.syncvar}' 48 | 49 | @app.route('/sync') 50 | async def sync_route(): 51 | aioflask.session['s'] = 'sync' 52 | return f'{aioflask.g.asyncvar}-{aioflask.g.syncvar}' 53 | 54 | @app.route('/session') 55 | async def session(): 56 | return f'{aioflask.session.get("a")}-{aioflask.session.get("s")}' 57 | 58 | @app.after_request 59 | async def after_request(rv): 60 | rv.data += f'/{aioflask.g.asyncvar}-{aioflask.g.syncvar}'.encode() 61 | return rv 62 | 63 | client = app.test_client() 64 | response = await client.get('/session') 65 | assert response.data == b'None-None/async-sync' 66 | response = await client.get('/async') 67 | assert response.data == b'async-sync/async-sync' 68 | response = await client.get('/session') 69 | assert response.data == b'async-None/async-sync' 70 | response = await client.get('/sync') 71 | assert response.data == b'async-sync/async-sync' 72 | response = await client.get('/session') 73 | assert response.data == b'async-sync/async-sync' 74 | 75 | @mock.patch('aioflask.app.uvicorn') 76 | def test_app_run(self, uvicorn): 77 | app = aioflask.Flask(__name__) 78 | 79 | app.run() 80 | uvicorn.run.assert_called_with('tests.test_app:app', 81 | host='127.0.0.1', port=5000, 82 | reload=False, workers=1, 83 | log_level='info', ssl_certfile=None, 84 | ssl_keyfile=None) 85 | app.run(host='1.2.3.4', port=3000) 86 | uvicorn.run.assert_called_with('tests.test_app:app', 87 | host='1.2.3.4', port=3000, 88 | reload=False, workers=1, 89 | log_level='info', ssl_certfile=None, 90 | ssl_keyfile=None) 91 | app.run(debug=True) 92 | uvicorn.run.assert_called_with('tests.test_app:app', 93 | host='127.0.0.1', port=5000, 94 | reload=True, workers=1, 95 | log_level='debug', ssl_certfile=None, 96 | ssl_keyfile=None) 97 | app.run(debug=True, use_reloader=False) 98 | uvicorn.run.assert_called_with('tests.test_app:app', 99 | host='127.0.0.1', port=5000, 100 | reload=False, workers=1, 101 | log_level='debug', ssl_certfile=None, 102 | ssl_keyfile=None) 103 | if 'FLASK_DEBUG' in os.environ: 104 | del os.environ['FLASK_DEBUG'] 105 | if 'AIOFLASK_USE_DEBUGGER' in os.environ: 106 | del os.environ['AIOFLASK_USE_DEBUGGER'] 107 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from unittest import mock 4 | import click 5 | from click.testing import CliRunner 6 | import aioflask 7 | import aioflask.cli 8 | from .utils import async_test 9 | 10 | 11 | class TestCli(unittest.TestCase): 12 | @async_test 13 | async def test_command_with_appcontext(self): 14 | app = aioflask.Flask('testapp') 15 | 16 | @app.cli.command(with_appcontext=True) 17 | async def testcmd(): 18 | click.echo(aioflask.current_app.name) 19 | 20 | result = await app.test_cli_runner().invoke(testcmd) 21 | assert result.exit_code == 0 22 | assert result.output == "testapp\n" 23 | 24 | @async_test 25 | async def test_command_without_appcontext(self): 26 | app = aioflask.Flask('testapp') 27 | 28 | @app.cli.command(with_appcontext=False) 29 | async def testcmd(): 30 | click.echo(aioflask.current_app.name) 31 | 32 | result = await app.test_cli_runner().invoke(testcmd) 33 | assert result.exit_code == 1 34 | assert type(result.exception) == RuntimeError 35 | 36 | @async_test 37 | async def test_with_appcontext(self): 38 | @click.command() 39 | @aioflask.cli.with_appcontext 40 | async def testcmd(): 41 | click.echo(aioflask.current_app.name) 42 | 43 | app = aioflask.Flask('testapp') 44 | 45 | result = await app.test_cli_runner().invoke(testcmd) 46 | assert result.exit_code == 0 47 | assert result.output == "testapp\n" 48 | 49 | @mock.patch('aioflask.cli.uvicorn') 50 | def test_aiorun(self, uvicorn): 51 | app = aioflask.Flask('testapp') 52 | obj = aioflask.cli.ScriptInfo(app_import_path='app.py', 53 | create_app=lambda: app) 54 | 55 | result = CliRunner().invoke(aioflask.cli.run_command, obj=obj) 56 | assert result.exit_code == 0 57 | uvicorn.run.assert_called_with('app:app', factory=False, 58 | host='127.0.0.1', port=5000, 59 | reload=False, workers=1, 60 | log_level='info', ssl_certfile=None, 61 | ssl_keyfile=None) 62 | result = CliRunner().invoke(aioflask.cli.run_command, 63 | '--host 1.2.3.4 --port 3000', obj=obj) 64 | assert result.exit_code == 0 65 | uvicorn.run.assert_called_with('app:app', factory=False, 66 | host='1.2.3.4', port=3000, 67 | reload=False, workers=1, 68 | log_level='info', ssl_certfile=None, 69 | ssl_keyfile=None) 70 | os.environ['FLASK_DEBUG'] = 'true' 71 | result = CliRunner().invoke(aioflask.cli.run_command, obj=obj) 72 | assert result.exit_code == 0 73 | uvicorn.run.assert_called_with('app:app', factory=False, 74 | host='127.0.0.1', port=5000, 75 | reload=True, workers=1, 76 | log_level='debug', ssl_certfile=None, 77 | ssl_keyfile=None) 78 | os.environ['FLASK_DEBUG'] = 'true' 79 | result = CliRunner().invoke(aioflask.cli.run_command, '--no-reload', 80 | obj=obj) 81 | assert result.exit_code == 0 82 | uvicorn.run.assert_called_with('app:app', factory=False, 83 | host='127.0.0.1', port=5000, 84 | reload=False, workers=1, 85 | log_level='debug', ssl_certfile=None, 86 | ssl_keyfile=None) 87 | 88 | if 'FLASK_DEBUG' in os.environ: 89 | del os.environ['FLASK_DEBUG'] 90 | if 'AIOFLASK_USE_DEBUGGER' in os.environ: 91 | del os.environ['AIOFLASK_USE_DEBUGGER'] 92 | 93 | @mock.patch('aioflask.cli.uvicorn') 94 | def test_aiorun_with_factory(self, uvicorn): 95 | app = aioflask.Flask('testapp') 96 | obj = aioflask.cli.ScriptInfo(app_import_path='app:create_app()', 97 | create_app=lambda: app) 98 | 99 | result = CliRunner().invoke(aioflask.cli.run_command, obj=obj) 100 | assert result.exit_code == 0 101 | uvicorn.run.assert_called_with('app:create_app', factory=True, 102 | host='127.0.0.1', port=5000, 103 | reload=False, workers=1, 104 | log_level='info', ssl_certfile=None, 105 | ssl_keyfile=None) 106 | -------------------------------------------------------------------------------- /tests/test_ctx.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pytest 3 | import aioflask 4 | from .utils import async_test 5 | 6 | 7 | class TestApp(unittest.TestCase): 8 | @async_test 9 | async def test_app_context(self): 10 | app = aioflask.Flask(__name__) 11 | called_t1 = False 12 | called_t2 = False 13 | 14 | @app.teardown_appcontext 15 | async def t1(exc): 16 | nonlocal called_t1 17 | called_t1 = True 18 | 19 | @app.teardown_appcontext 20 | def t2(exc): 21 | nonlocal called_t2 22 | called_t2 = True 23 | 24 | async with app.app_context(): 25 | assert aioflask.current_app == app 26 | async with app.app_context(): 27 | assert aioflask.current_app == app 28 | assert aioflask.current_app == app 29 | 30 | assert called_t1 31 | assert called_t2 32 | with pytest.raises(RuntimeError): 33 | print(aioflask.current_app) 34 | 35 | @async_test 36 | async def test_req_context(self): 37 | app = aioflask.Flask(__name__) 38 | called_t1 = False 39 | called_t2 = False 40 | 41 | @app.teardown_appcontext 42 | async def t1(exc): 43 | nonlocal called_t1 44 | called_t1 = True 45 | 46 | @app.teardown_appcontext 47 | def t2(exc): 48 | nonlocal called_t2 49 | called_t2 = True 50 | 51 | async with app.test_request_context('/foo'): 52 | assert aioflask.current_app == app 53 | assert aioflask.request.path == '/foo' 54 | 55 | assert called_t1 56 | assert called_t2 57 | 58 | async with app.app_context(): 59 | async with app.test_request_context('/bar') as reqctx: 60 | assert aioflask.current_app == app 61 | assert aioflask.request.path == '/bar' 62 | async with reqctx: 63 | assert aioflask.current_app == app 64 | assert aioflask.request.path == '/bar' 65 | 66 | with pytest.raises(RuntimeError): 67 | print(aioflask.current_app) 68 | -------------------------------------------------------------------------------- /tests/test_patch.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import aioflask 3 | import aioflask.patch 4 | from .utils import async_test 5 | 6 | 7 | class TestPatch(unittest.TestCase): 8 | @async_test 9 | async def test_decorator(self): 10 | def foo(f): 11 | def decorator(*args, **kwargs): 12 | return f(*args, **kwargs) + '-decorated' 13 | 14 | return decorator 15 | 16 | foo = aioflask.patch.patch_decorator(foo) 17 | 18 | app = aioflask.Flask(__name__) 19 | 20 | @app.route('/abc/