Repository: miguelgrinberg/aioflask Branch: main Commit: 7f447e79c81c Files: 60 Total size: 89.0 KB Directory structure: gitextract_sepaq9a1/ ├── .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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/tests.yml ================================================ name: build on: push: branches: - main pull_request: branches: - main jobs: lint: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: python -m pip install --upgrade pip wheel - run: pip install tox tox-gh-actions - run: tox -eflake8 tests: name: tests strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python: ['3.7', '3.8', '3.9', '3.10'] fail-fast: false runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - run: python -m pip install --upgrade pip wheel - run: pip install tox tox-gh-actions - run: tox coverage: name: coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - run: python -m pip install --upgrade pip wheel - run: pip install tox tox-gh-actions codecov - run: tox - run: codecov ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: .readthedocs.yaml ================================================ version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/conf.py python: install: - method: pip path: . extra_requirements: - docs ================================================ FILE: CHANGES.md ================================================ # aioflask change log **Release 0.4.0** - 2021-08-18 - Support for app factory functions with uvicorn ([commit](https://github.com/miguelgrinberg/aioflask/commit/7f51ca835a5b581b28915b9818428ea09f720081)) - Make app context async ([commit](https://github.com/miguelgrinberg/aioflask/commit/d07ea5449389ae58b286ceff389386e9481e6715)) - Make request context async ([commit](https://github.com/miguelgrinberg/aioflask/commit/4232b2819ad7cf3ed386578a679ba2cbc75f91b0)) - Make test and cli runner clients async ([commit](https://github.com/miguelgrinberg/aioflask/commit/12408cb1d6018fabac8d2749607687164fb1da50)) - Patcher for 3rd party decorators without async view support ([commit](https://github.com/miguelgrinberg/aioflask/commit/b7d4433acd153c43463bd047ddfa19b8c2087078)) - Flask-Login support ([commit #1](https://github.com/miguelgrinberg/aioflask/commit/cbe8abcc0d890bc03787b75ba3c7cb78d5333f38)) ([commit #2](https://github.com/miguelgrinberg/aioflask/commit/e0ab0e0fe1a3b51c3dc3b35abc47b21002f034c3)) - Fix handling of application context ([commit](https://github.com/miguelgrinberg/aioflask/commit/f0b14856b58bd1e85b2b054cd6b3028da0f89091)) ([commit #3](https://github.com/miguelgrinberg/aioflask/commit/a6f5a67a1d9eaa4046d075c1417f5c042dd30c38)) - 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)) - Update example documentation to use `flask aiorun` ([commit](https://github.com/miguelgrinberg/aioflask/commit/634ee10b7cbc934fa70d512a66334e78ddc39b3a)) **Release 0.3.0** - 2021-06-07 - Test client support, and some more unit tests ([commit](https://github.com/miguelgrinberg/aioflask/commit/c765e12f6382d685bbec1861dac062c13d63aea3)) - Started a change log ([commit](https://github.com/miguelgrinberg/aioflask/commit/e6f4c3a87964fb2e5ea3f4464853c2a1d5ecfc29)) - Improved example code ([commit](https://github.com/miguelgrinberg/aioflask/commit/496ce73f3f0ecb1bdbdd25bd957ce08e6742c191)) - One more example ([commit](https://github.com/miguelgrinberg/aioflask/commit/7edab525809f7ba19562f67bb363a033563d6158)) **Release 0.2.0** - 2021-05-15 - Flask 2.x changes ([commit](https://github.com/miguelgrinberg/aioflask/commit/52aef31fb9a7f8fe6a54b156fe257db1300c0ca6)) - Update README.md ([commit](https://github.com/miguelgrinberg/aioflask/commit/c232561ff3e1c954c49ab362be030da854ceb8ba)) - codecov.io integration ([commit](https://github.com/miguelgrinberg/aioflask/commit/d558dfde5f0717dc6f9b6ff0cedec142ffe60335)) - github actions build ([commit](https://github.com/miguelgrinberg/aioflask/commit/c5f43dacae3d8c73ac0c55a7a58b7c9ac985195a)) **Release 0.1** - 2020-11-07 - async render_template and CLI commands ([commit](https://github.com/miguelgrinberg/aioflask/commit/2e6944c111bd581e1c0eb345ffe88cb1ec014140)) - travis builds ([commit](https://github.com/miguelgrinberg/aioflask/commit/5834c8526fffe424bccfcbe62aa03e33c81b3018)) - app.run implementation and debug mode fixes ([commit](https://github.com/miguelgrinberg/aioflask/commit/2dc8426b5e5e52309639aa31db1f845c44226259)) - add note about the experimental nature of this thing ([commit](https://github.com/miguelgrinberg/aioflask/commit/f027e5ba95cc16ed3c513525d88a197c22001784)) - initial version ([commit](https://github.com/miguelgrinberg/aioflask/commit/4f1d1a343642fa88f76a4cc064f1d7268c9d7dc7)) - Initial commit ([commit](https://github.com/miguelgrinberg/aioflask/commit/b02c360cae72d2f7dd479c93e0cd7517d4dce259)) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Miguel Grinberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # aioflask ![Build status](https://github.com/miguelgrinberg/aioflask/workflows/build/badge.svg) [![codecov](https://codecov.io/gh/miguelgrinberg/aioflask/branch/main/graph/badge.svg?token=CDMKF3L0ID)](https://codecov.io/gh/miguelgrinberg/aioflask) Flask 2.x running on asyncio! Is there a purpose for this, now that Flask 2.0 is out with support for async views? Yes! Flask's own support for async handlers is very limited, as the application still runs inside a WSGI web server, which severely limits scalability. With aioflask you get a true ASGI application, running in a 100% async environment. WARNING: This is an experiment at this point. Not at all production ready! ## Quick start To use async view functions and other handlers, use the `aioflask` package instead of `flask`. The `aioflask.Flask` class is a subclass of `flask.Flask` that changes a few minor things to help the application run properly under the asyncio loop. In particular, it overrides the following aspects of the application instance: - The `route`, `before_request`, `before_first_request`, `after_request`, `teardown_request`, `teardown_appcontext`, `errorhandler` and `cli.command` decorators accept coroutines as well as regular functions. The handlers all run inside an asyncio loop, so when using regular functions, care must be taken to not block. - The WSGI callable entry point is replaced with an ASGI equivalent. - The `run()` method uses uvicorn as web server. There are also changes outside of the `Flask` class: - The `flask aiorun` command starts an ASGI application using the uvicorn web server. - The `render_template()` and `render_template_string()` functions are asynchronous and must be awaited. - The context managers for the Flask application and request contexts are async. - The test client and test CLI runner use coroutines. ## Example ```python import asyncio from aioflask import Flask, render_template app = Flask(__name__) @app.route('/') async def index(): await asyncio.sleep(1) return await render_template('index.html') ``` ================================================ FILE: examples/AsyncProgressBar/README.md ================================================ AsyncProgressBar ================ This is the *AsyncProgressBar* from Quart ported to Flask. You need to have a Redis server running on localhost:6379 for this example to run. ================================================ FILE: examples/AsyncProgressBar/progress_bar.py ================================================ import asyncio import random import aioredis import redis from aioflask import Flask, request, url_for, jsonify app = Flask(__name__) sr = redis.StrictRedis(host='localhost', port=6379) sr.execute_command('FLUSHDB') async def some_work(): global aredis await aredis.set('state', 'running') work_to_do = range(1, 26) await aredis.set('length_of_work', len(work_to_do)) for i in work_to_do: await aredis.set('processed', i) await asyncio.sleep(random.random()) await aredis.set('state', 'ready') await aredis.set('percent', 100) @app.route('/check_status/') async def check_status(): global aredis, sr status = dict() try: if await aredis.get('state') == b'running': if await aredis.get('processed') != await aredis.get('lastProcessed'): await aredis.set('percent', round( int(await aredis.get('processed')) / int(await aredis.get('length_of_work')) * 100, 2)) await aredis.set('lastProcessed', str(await aredis.get('processed'))) except: pass try: status['state'] = sr.get('state').decode() status['processed'] = sr.get('processed').decode() status['length_of_work'] = sr.get('length_of_work').decode() status['percent_complete'] = sr.get('percent').decode() except: status['state'] = sr.get('state') status['processed'] = sr.get('processed') status['length_of_work'] = sr.get('length_of_work') status['percent_complete'] = sr.get('percent') status['hint'] = 'refresh me.' return jsonify(status) @app.route('/progress/') async def progress(): return """ Asyncio Progress Bar Demo

Progress of work is shown below

""" @app.route('/') async def index(): return 'This is the index page. Try the following to start some test work with a progress indicator.' @app.route('/start_work/') async def start_work(): global aredis loop = asyncio.get_event_loop() aredis = await aioredis.create_redis('redis://localhost', loop=loop) if await aredis.get('state') == b'running': return "
Please wait for current work to finish.
" else: await aredis.set('state', 'ready') if await aredis.get('state') == b'ready': loop.create_task(some_work()) body = '''
work started!
''' return body if __name__ == "__main__": app.run('localhost', port=5000, debug=True) ================================================ FILE: examples/AsyncProgressBar/requirements.txt ================================================ aioflask aioredis==1.3.1 async-timeout==3.0.1 click==7.1.2 Flask==1.1.2 greenlet==0.4.16 greenletio h11==0.9.0 hiredis==1.1.0 httptools==0.1.1 itsdangerous==1.1.0 Jinja2==2.11.2 MarkupSafe==1.1.1 redis==3.5.3 uvicorn==0.11.6 uvloop==0.14.0 websockets==8.1 Werkzeug==1.0.1 ================================================ FILE: examples/aioflaskr/.flaskenv ================================================ FLASK_APP=flaskr:create_app() ================================================ FILE: examples/aioflaskr/LICENSE ================================================ Copyright 2010 Pallets (original version) Copyright 2021 Miguel Grinberg (this version) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: examples/aioflaskr/README.md ================================================ aioflaskr ========= This is the "Flaskr" application from the tutorial section of the Flask documentation, adapted to work as an asyncio application with aioflask as the web framework and Alchemical for its database. The Flask-Login extension is used to maintain the logged in state of the user. Install ------- ```bash # clone the repository $ git clone https://github.com/miguelgrinberg/aioflask $ cd aioflask/examples/aioflaskr ``` Create a virtualenv and activate it: ```bash $ python3 -m venv venv $ . venv/bin/activate ``` Or on Windows cmd: ```text $ py -3 -m venv venv $ venv\Scripts\activate.bat ``` Install the requirements ```bash $ pip install -r requirements.txt ``` Run --- ```bash flask init-db flask aiorun ``` Open http://127.0.0.1:5000 in a browser. Test ---- ```bash $ pip install pytest pytest-asyncio $ python -m pytest ``` Run with coverage report: ```bash $ pip install pytest-cov $ python -m pytest --cov=flaskr --cov-branch --cov-report=term-missing ``` ================================================ FILE: examples/aioflaskr/flaskr/__init__.py ================================================ import os import click from aioflask import Flask from aioflask.cli import with_appcontext from alchemical.aioflask import Alchemical from aioflask.patched.flask_login import LoginManager db = Alchemical() login = LoginManager() login.login_view = 'auth.login' def create_app(test_config=None): """Create and configure an instance of the Flask application.""" app = Flask(__name__, instance_relative_config=True) # some deploy systems set the database url in the environ db_url = os.environ.get("DATABASE_URL") if db_url is None: # default to a sqlite database in the instance folder db_path = os.path.join(app.instance_path, "flaskr.sqlite") db_url = f"sqlite:///{db_path}" # ensure the instance folder exists os.makedirs(app.instance_path, exist_ok=True) app.config.from_mapping( SECRET_KEY=os.environ.get("SECRET_KEY", "dev"), ALCHEMICAL_DATABASE_URL=db_url, ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile("config.py", silent=True) else: # load the test config if passed in app.config.update(test_config) # initialize Flask-Alchemical and the init-db command db.init_app(app) app.cli.add_command(init_db_command) # initialize Flask-Login login.init_app(app) # apply the blueprints to the app from flaskr import auth, blog app.register_blueprint(auth.bp) app.register_blueprint(blog.bp) # make "index" point at "/", which is handled by "blog.index" app.add_url_rule("/", endpoint="index") return app async def init_db(): await db.drop_all() await db.create_all() @click.command("init-db") @with_appcontext async def init_db_command(): """Clear existing data and create new tables.""" await init_db() click.echo("Initialized the database.") ================================================ FILE: examples/aioflaskr/flaskr/auth.py ================================================ from aioflask import Blueprint from aioflask import flash from aioflask import redirect from aioflask import render_template from aioflask import request from aioflask import url_for from aioflask.patched.flask_login import login_user from aioflask.patched.flask_login import logout_user from sqlalchemy.exc import IntegrityError from flaskr import db, login from flaskr.models import User bp = Blueprint("auth", __name__, url_prefix="/auth") @login.user_loader async def load_user(id): return await db.session.get(User, int(id)) @bp.route("/register", methods=("GET", "POST")) async def register(): """Register a new user. Validates that the username is not already taken. Hashes the password for security. """ if request.method == "POST": username = request.form["username"] password = request.form["password"] error = None if not username: error = "Username is required." elif not password: error = "Password is required." if error is None: try: db.session.add(User(username=username, password=password)) await db.session.commit() except IntegrityError: # The username was already taken, which caused the # commit to fail. Show a validation error. error = f"User {username} is already registered." else: # Success, go to the login page. return redirect(url_for("auth.login")) flash(error) return await render_template("auth/register.html") @bp.route("/login", methods=("GET", "POST")) async def login(): """Log in a registered user by adding the user id to the session.""" if request.method == "POST": username = request.form["username"] password = request.form["password"] error = None query = User.select().filter_by(username=username) user = await db.session.scalar(query) if user is None: error = "Incorrect username." elif not user.check_password(password): error = "Incorrect password." if error is None: # store the user id in a new session and return to the index login_user(user) return redirect(url_for("index")) flash(error) return await render_template("auth/login.html") @bp.route("/logout") async def logout(): """Clear the current session, including the stored user id.""" logout_user() return redirect(url_for("index")) ================================================ FILE: examples/aioflaskr/flaskr/blog.py ================================================ from aioflask import Blueprint from aioflask import flash from aioflask import redirect from aioflask import render_template from aioflask import request from aioflask import url_for from werkzeug.exceptions import abort from aioflask.patched.flask_login import current_user from aioflask.patched.flask_login import login_required from flaskr import db from flaskr.models import Post bp = Blueprint("blog", __name__) @bp.route("/") async def index(): """Show all the posts, most recent first.""" posts = (await db.session.scalars(Post.select())).all() return await render_template("blog/index.html", posts=posts) async def get_post(id, check_author=True): """Get a post and its author by id. Checks that the id exists and optionally that the current user is the author. :param id: id of post to get :param check_author: require the current user to be the author :return: the post with author information :raise 404: if a post with the given id doesn't exist :raise 403: if the current user isn't the author """ post = await db.session.get(Post, id) if post is None: abort(404, f"Post id {id} doesn't exist.") if check_author and post.author != current_user: abort(403) return post @bp.route("/create", methods=("GET", "POST")) @login_required async def create(): """Create a new post for the current user.""" if request.method == "POST": title = request.form["title"] body = request.form["body"] error = None if not title: error = "Title is required." if error is not None: flash(error) else: db.session.add(Post(title=title, body=body, author=current_user)) await db.session.commit() return redirect(url_for("blog.index")) return await render_template("blog/create.html") @bp.route("//update", methods=("GET", "POST")) @login_required async def update(id): """Update a post if the current user is the author.""" post = await get_post(id) if request.method == "POST": title = request.form["title"] body = request.form["body"] error = None if not title: error = "Title is required." if error is not None: flash(error) else: post.title = title post.body = body await db.session.commit() return redirect(url_for("blog.index")) return await render_template("blog/update.html", post=post) @bp.route("//delete", methods=("POST",)) @login_required async def delete(id): """Delete a post. Ensures that the post exists and that the logged in user is the author of the post. """ post = await get_post(id) await db.session.delete(post) await db.session.commit() return redirect(url_for("blog.index")) ================================================ FILE: examples/aioflaskr/flaskr/models.py ================================================ from werkzeug.security import check_password_hash from werkzeug.security import generate_password_hash from aioflask import url_for from aioflask.patched.flask_login import UserMixin from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func from sqlalchemy.orm import relationship from flaskr import db class User(UserMixin, db.Model): id = Column(Integer, primary_key=True) username = Column(String, unique=True, nullable=False) password_hash = Column(String, nullable=False) @property def password(self): raise RuntimeError('Cannot get user passwords!') @password.setter def password(self, value): """Store the password as a hash for security.""" self.password_hash = generate_password_hash(value) def check_password(self, value): return check_password_hash(self.password_hash, value) class Post(db.Model): id = Column(Integer, primary_key=True) author_id = Column(ForeignKey(User.id), nullable=False) created = Column( DateTime, nullable=False, server_default=func.current_timestamp() ) title = Column(String, nullable=False) body = Column(String, nullable=False) # User object backed by author_id # lazy="joined" means the user is returned with the post in one query author = relationship(User, lazy="joined", backref="posts") @property def update_url(self): return url_for("blog.update", id=self.id) @property def delete_url(self): return url_for("blog.delete", id=self.id) ================================================ FILE: examples/aioflaskr/flaskr/static/style.css ================================================ html { font-family: sans-serif; background: #eee; padding: 1rem; } body { max-width: 960px; margin: 0 auto; background: white; } h1, h2, h3, h4, h5, h6 { font-family: serif; color: #377ba8; margin: 1rem 0; } a { color: #377ba8; } hr { border: none; border-top: 1px solid lightgray; } nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } nav h1 { flex: auto; margin: 0; } nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } nav ul { display: flex; list-style: none; margin: 0; padding: 0; } nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } .content { padding: 0 1rem 1rem; } .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } .post > header > div:first-of-type { flex: auto; } .post > header h1 { font-size: 1.5em; margin-bottom: 0; } .post .about { color: slategray; font-style: italic; } .post .body { white-space: pre-line; } .content:last-child { margin-bottom: 0; } .content form { margin: 1em 0; display: flex; flex-direction: column; } .content label { font-weight: bold; margin-bottom: 0.5em; } .content input, .content textarea { margin-bottom: 1em; } .content textarea { min-height: 12em; resize: vertical; } input.danger { color: #cc2f2e; } input[type=submit] { align-self: start; min-width: 10em; } ================================================ FILE: examples/aioflaskr/flaskr/templates/auth/login.html ================================================ {% extends 'base.html' %} {% block header %}

{% block title %}Log In{% endblock %}

{% endblock %} {% block content %}
{% endblock %} ================================================ FILE: examples/aioflaskr/flaskr/templates/auth/register.html ================================================ {% extends 'base.html' %} {% block header %}

{% block title %}Register{% endblock %}

{% endblock %} {% block content %}
{% endblock %} ================================================ FILE: examples/aioflaskr/flaskr/templates/base.html ================================================ {% block title %}{% endblock %} - Flaskr
{% block header %}{% endblock %}
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %} {% block content %}{% endblock %}
================================================ FILE: examples/aioflaskr/flaskr/templates/blog/create.html ================================================ {% extends 'base.html' %} {% block header %}

{% block title %}New Post{% endblock %}

{% endblock %} {% block content %}
{% endblock %} ================================================ FILE: examples/aioflaskr/flaskr/templates/blog/index.html ================================================ {% extends 'base.html' %} {% block header %}

{% block title %}Posts{% endblock %}

{% if current_user.is_authenticated %} New {% endif %} {% endblock %} {% block content %} {% for post in posts %}

{{ post.title }}

by {{ post.author.username }} on {{ post.created.strftime('%Y-%m-%d') }}
{% if current_user == post.author %} Edit {% endif %}

{{ post.body }}

{% if not loop.last %}
{% endif %} {% endfor %} {% endblock %} ================================================ FILE: examples/aioflaskr/flaskr/templates/blog/update.html ================================================ {% extends 'base.html' %} {% block header %}

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

{% endblock %} {% block content %}

{% endblock %} ================================================ FILE: examples/aioflaskr/requirements.txt ================================================ aioflask aiosqlite==0.17.0 alchemical asgiref==3.4.1 click==8.0.1 Flask==2.0.1 Flask-Login==0.5.0 greenlet==1.1.1 greenletio h11==0.12.0 importlib-metadata==4.6.3 itsdangerous==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 python-dotenv==0.19.0 SQLAlchemy==1.4.25 typing-extensions==3.10.0.0 uvicorn==0.14.0 Werkzeug==2.0.1 zipp==3.5.0 ================================================ FILE: examples/aioflaskr/tests/__init__.py ================================================ ================================================ FILE: examples/aioflaskr/tests/conftest.py ================================================ from datetime import datetime import pytest from flaskr import create_app from flaskr import db from flaskr import init_db from flaskr.models import User from flaskr.models import Post @pytest.fixture async def app(): """Create and configure a new app instance for each test.""" # create the app with common test config app = create_app({"TESTING": True, "ALCHEMICAL_DATABASE_URL": "sqlite:///:memory:"}) # create the database and load test data async with app.app_context(): await init_db() user = User(username="test", password="test") db.session.add_all( ( user, User(username="other", password="other"), Post( title="test title", body="test\nbody", author=user, created=datetime(2018, 1, 1), ), ) ) await db.session.commit() yield app @pytest.fixture def client(app): """A test client for the app.""" return app.test_client() @pytest.fixture def runner(app): """A test runner for the app's Click commands.""" return app.test_cli_runner() class AuthActions: def __init__(self, client): self._client = client async def login(self, username="test", password="test"): return await self._client.post( "/auth/login", data={"username": username, "password": password} ) async def logout(self): return await self._client.get("/auth/logout") @pytest.fixture def auth(client): return AuthActions(client) ================================================ FILE: examples/aioflaskr/tests/test_auth.py ================================================ import pytest from flaskr import db from flaskr.models import User @pytest.mark.asyncio async def test_register(client, app): # test that viewing the page renders without template errors assert (await client.get("/auth/register")).status_code == 200 # test that successful registration redirects to the login page response = await client.post("/auth/register", data={"username": "a", "password": "a"}) assert "/auth/login" == response.headers["Location"] # test that the user was inserted into the database async with app.app_context(): query = User.select().filter_by(username="a") assert await db.session.scalar(query) is not None def test_user_password(app): user = User(username="a", password="a") assert user.password_hash != "a" assert user.check_password("a") @pytest.mark.asyncio @pytest.mark.parametrize( ("username", "password", "message"), ( ("", "", b"Username is required."), ("a", "", b"Password is required."), ("test", "test", b"already registered"), ), ) async def test_register_validate_input(client, username, password, message): response = await client.post( "/auth/register", data={"username": username, "password": password} ) assert message in response.data @pytest.mark.asyncio async def test_login(client, auth): # test that viewing the page renders without template errors assert (await client.get("/auth/login")).status_code == 200 # test that successful login redirects to the index page response = await auth.login() assert response.headers["Location"] == "/" # login request set the user_id in the session # check that the user is loaded from the session async with client: response = await client.get("/") assert b"test" in response.data @pytest.mark.asyncio @pytest.mark.parametrize( ("username", "password", "message"), (("a", "test", b"Incorrect username."), ("test", "a", b"Incorrect password.")), ) async def test_login_validate_input(auth, username, password, message): response = await auth.login(username, password) assert message in response.data @pytest.mark.asyncio async def test_logout(client, auth): await auth.login() async with client: await auth.logout() response = await client.get("/") assert b"Log In" in response.data ================================================ FILE: examples/aioflaskr/tests/test_blog.py ================================================ import pytest from sqlalchemy import func from sqlalchemy import select from flaskr import db from flaskr.models import User from flaskr.models import Post @pytest.mark.asyncio async def test_index(client, auth): response = await client.get("/") assert b"Log In" in response.data assert b"Register" in response.data await auth.login() response = await client.get("/") assert b"test title" in response.data assert b"by test on 2018-01-01" in response.data assert b"test\nbody" in response.data assert b'href="/1/update"' in response.data @pytest.mark.asyncio @pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) async def test_login_required(client, path): response = await client.post(path) assert response.headers["Location"].startswith("/auth/login?next=") @pytest.mark.asyncio async def test_author_required(app, client, auth): # change the post author to another user async with app.app_context(): (await db.session.get(Post, 1)).author = await db.session.get(User, 2) await db.session.commit() await auth.login() # current user can't modify other user's post assert (await client.post("/1/update")).status_code == 403 assert (await client.post("/1/delete")).status_code == 403 # current user doesn't see edit link assert b'href="/1/update"' not in (await client.get("/")).data @pytest.mark.asyncio @pytest.mark.parametrize("path", ("/2/update", "/2/delete")) async def test_exists_required(client, auth, path): await auth.login() assert (await client.post(path)).status_code == 404 @pytest.mark.asyncio async def test_create(client, auth, app): await auth.login() assert (await client.get("/create")).status_code == 200 await client.post("/create", data={"title": "created", "body": ""}) async with app.app_context(): query = select(func.count()).select_from(Post) assert await db.session.scalar(query) == 2 @pytest.mark.asyncio async def test_update(client, auth, app): await auth.login() assert (await client.get("/1/update")).status_code == 200 await client.post("/1/update", data={"title": "updated", "body": ""}) async with app.app_context(): assert (await db.session.get(Post, 1)).title == "updated" @pytest.mark.asyncio @pytest.mark.parametrize("path", ("/create", "/1/update")) async def test_create_update_validate(client, auth, path): await auth.login() response = await client.post(path, data={"title": "", "body": ""}) assert b"Title is required." in response.data @pytest.mark.asyncio async def test_delete(client, auth, app): await auth.login() response = await client.post("/1/delete") assert response.headers["Location"] == "/" async with app.app_context(): assert (await db.session.get(Post, 1)) is None ================================================ FILE: examples/aioflaskr/tests/test_init.py ================================================ import pytest from flaskr import create_app def test_config(): """Test create_app without passing test config.""" assert not create_app().testing assert create_app({"TESTING": True}).testing def test_db_url_environ(monkeypatch): """Test DATABASE_URL environment variable.""" monkeypatch.setenv("DATABASE_URL", "sqlite:///environ") app = create_app() assert app.config["ALCHEMICAL_DATABASE_URL"] == "sqlite:///environ" @pytest.mark.asyncio async def test_init_db_command(runner, monkeypatch): class Recorder: called = False async def fake_init_db(): Recorder.called = True monkeypatch.setattr("flaskr.init_db", fake_init_db) result = await runner.invoke(args=["init-db"]) assert "Initialized" in result.output assert Recorder.called ================================================ FILE: examples/g/app.py ================================================ from aioflask import Flask, g import aiohttp app = Flask(__name__) @app.before_request async def before_request(): g.session = aiohttp.ClientSession() @app.teardown_appcontext async def teardown_appcontext(exc): await g.session.close() @app.route('/') async def index(): response = await g.session.get('https://api.quotable.io/random') return (await response.json())['content'] ================================================ FILE: examples/hello_world/app.py ================================================ from aioflask import Flask, render_template app = Flask(__name__) @app.route('/') async def index(): return await render_template('index.html') @app.cli.command() async def hello(): """Example async CLI handler.""" print('hello!') ================================================ FILE: examples/hello_world/templates/index.html ================================================ Hello (async) world!

Hello (async) world!

================================================ FILE: examples/login/app.py ================================================ from aioflask import Flask, request, redirect from aioflask.patched.flask_login import LoginManager, login_required, UserMixin, login_user, logout_user, current_user import aiohttp app = Flask(__name__) app.secret_key = 'top-secret!' login = LoginManager(app) login.login_view = 'login' class User(UserMixin): def __init__(self, user_id): self.id = user_id @login.user_loader async def load_user(user_id): return User(user_id) @app.route('/') @login_required async def index(): return f'''

Logged in user: {current_user.id}

''' @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': return '''
''' else: login_user(User(request.form['username'])) return redirect(request.args.get('next', '/')) @app.route('/logout', methods=['POST']) def logout(): logout_user() return redirect('/') ================================================ FILE: examples/quotes-aiohttp/README.md ================================================ Quotes ====== Returns 10 famous quotes each time the page is refreshed. Quotes are obtained by sending concurrent HTTP requests to a Quotes API with the aiohttp asynchronous client. To run this example, set `FLASK_APP=quotes.py` in your environment and then use the standard `flask aiorun` command to start the server. ================================================ FILE: examples/quotes-aiohttp/quotes.py ================================================ import asyncio import aiohttp from aioflask import Flask, render_template_string app = Flask(__name__) template = ''' Quotes

Quotes

{% for quote in quotes %}

"{{ quote.content }}" — {{ quote.author }}

{% endfor %} ''' async def get_quote(session): response = await session.get('https://api.quotable.io/random') return await response.json() @app.route('/') async def index(): async with aiohttp.ClientSession() as session: tasks = [get_quote(session) for _ in range(10)] quotes = await asyncio.gather(*tasks) return await render_template_string(template, quotes=quotes) ================================================ FILE: examples/quotes-requests/README.md ================================================ Quotes ====== Returns 10 famous quotes each time the page is refreshed. Quotes are obtained by sending concurrent HTTP requests to a Quotes API with the requests client. This example shows how you can incorporate blocking code into your aioflask application without blocking the asyncio loop. To run this example, set `FLASK_APP=quotes.py` in your environment and then use the standard `flask aiorun` command to start the server. ================================================ FILE: examples/quotes-requests/quotes.py ================================================ import greenletio # import the application with blocking functions monkey patched with greenletio.patch_blocking(): from quotes_app import app ================================================ FILE: examples/quotes-requests/quotes_app.py ================================================ import asyncio from aioflask import Flask, render_template_string from greenletio import async_ import requests app = Flask(__name__) template = ''' Quotes

Quotes

{% for quote in quotes %}

"{{ quote.content }}" — {{ quote.author }}

{% endfor %} ''' # this is a blocking function that is converted to asynchronous with # greenletio's @async_ decorator. For this to work, all the low-level I/O # functions started from this function must be asynchronous, which can be # achieved with greenletio's monkey patching feature. @async_ def get_quote(): response = requests.get('https://api.quotable.io/random') return response.json() @app.route('/') async def index(): tasks = [get_quote() for _ in range(10)] quotes = await asyncio.gather(*tasks) return await render_template_string(template, quotes=quotes) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = [ "setuptools>=42", "wheel" ] build-backend = "setuptools.build_meta" ================================================ FILE: setup.cfg ================================================ [metadata] name = aioflask version = 0.4.1.dev0 author = Miguel Grinberg author_email = miguel.grinberg@gmail.com description = Flask running on asyncio. long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/miguelgrinberg/aioflask project_urls = Bug Tracker = https://github.com/miguelgrinberg/aioflask/issues classifiers = Intended Audience :: Developers Programming Language :: Python :: 3 License :: OSI Approved :: MIT License Operating System :: OS Independent [options] zip_safe = False include_package_data = True package_dir = = src packages = find: python_requires = >=3.6 install_requires = greenletio flask >= 2 uvicorn [options.packages.find] where = src [options.entry_points] flask.commands = aiorun = aioflask.cli:run_command [options.extras_require] docs = sphinx ================================================ FILE: setup.py ================================================ import setuptools setuptools.setup() ================================================ FILE: src/aioflask/__init__.py ================================================ from flask import * from .app import Flask from .templating import render_template, render_template_string from .testing import FlaskClient ================================================ FILE: src/aioflask/app.py ================================================ import asyncio from functools import wraps from inspect import iscoroutinefunction import os from flask.app import * from flask.app import Flask as OriginalFlask from flask import cli from flask.globals import _app_ctx_stack, _request_ctx_stack from flask.helpers import get_debug_flag, get_env, get_load_dotenv from greenletio import await_ import uvicorn from .asgi import WsgiToAsgiInstance from .cli import show_server_banner, AppGroup from .ctx import AppContext, RequestContext from .testing import FlaskClient, FlaskCliRunner class Flask(OriginalFlask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.cli = AppGroup() self.jinja_options['enable_async'] = True self.test_client_class = FlaskClient self.test_cli_runner_class = FlaskCliRunner self.async_fixed = False def ensure_sync(self, func): if not iscoroutinefunction(func): return func def wrapped(*args, **kwargs): appctx = _app_ctx_stack.top reqctx = _request_ctx_stack.top async def _coro(): # app context is push internally to avoid changing reference # counts and emitting duplicate signals _app_ctx_stack.push(appctx) if reqctx: _request_ctx_stack.push(reqctx) ret = await func(*args, **kwargs) if reqctx: _request_ctx_stack.pop() _app_ctx_stack.pop() return ret return await_(_coro()) return wrapped def app_context(self): return AppContext(self) def request_context(self, environ): return RequestContext(self, environ) def _fix_async(self): # pragma: no cover self.async_fixed = True if os.environ.get('AIOFLASK_USE_DEBUGGER') == 'true': os.environ['WERKZEUG_RUN_MAIN'] = 'true' from werkzeug.debug import DebuggedApplication self.wsgi_app = DebuggedApplication(self.wsgi_app, evalex=True) async def asgi_app(self, scope, receive, send): # pragma: no cover if not self.async_fixed: self._fix_async() return await WsgiToAsgiInstance(self.wsgi_app)(scope, receive, send) async def __call__(self, scope, receive, send=None): # pragma: no cover if send is None: # we were called with two arguments, so this is likely a WSGI app raise RuntimeError('The WSGI interface is not supported by ' 'aioflask, use an ASGI web server instead.') return await self.asgi_app(scope, receive, send) def run(self, host=None, port=None, debug=None, load_dotenv=True, **options): if get_load_dotenv(load_dotenv): cli.load_dotenv() # if set, let env vars override previous values if "FLASK_ENV" in os.environ: self.env = get_env() self.debug = get_debug_flag() elif "FLASK_DEBUG" in os.environ: self.debug = get_debug_flag() # debug passed to method overrides all other sources if debug is not None: self.debug = bool(debug) server_name = self.config.get("SERVER_NAME") sn_host = sn_port = None if server_name: sn_host, _, sn_port = server_name.partition(":") if not host: if sn_host: host = sn_host else: host = "127.0.0.1" if port or port == 0: port = int(port) elif sn_port: port = int(sn_port) else: port = 5000 options.setdefault("use_reloader", self.debug) options.setdefault("use_debugger", self.debug) options.setdefault("threaded", True) options.setdefault("workers", 1) certfile = None keyfile = None cert = options.get('ssl_context') if cert is not None and len(cert) == 2: certfile = cert[0] keyfile = cert[1] elif cert == 'adhoc': raise RuntimeError( 'Aad-hoc certificates are not supported by aioflask.') if debug: os.environ['FLASK_DEBUG'] = 'true' if options['use_debugger']: os.environ['AIOFLASK_USE_DEBUGGER'] = 'true' show_server_banner(self.env, self.debug, self.name, False) uvicorn.run( self.import_name + ':app', host=host, port=port, reload=options['use_reloader'], workers=options['workers'], log_level='debug' if self.debug else 'info', ssl_certfile=certfile, ssl_keyfile=keyfile, ) ================================================ FILE: src/aioflask/asgi.py ================================================ import sys from tempfile import SpooledTemporaryFile from greenletio import async_, await_ class wsgi_to_asgi: # pragma: no cover """Wraps a WSGI application to make it into an ASGI application.""" def __init__(self, wsgi_application): self.wsgi_application = wsgi_application async def __call__(self, scope, receive, send): """ASGI application instantiation point. We return a new WsgiToAsgiInstance here with the WSGI app and the scope, ready to respond when it is __call__ed. """ await WsgiToAsgiInstance(self.wsgi_application)(scope, receive, send) class WsgiToAsgiInstance: # pragma: no cover """Per-socket instance of a wrapped WSGI application""" def __init__(self, wsgi_application): self.wsgi_application = wsgi_application self.response_started = False async def __call__(self, scope, receive, send): if scope["type"] != "http": raise ValueError("WSGI wrapper received a non-HTTP scope") self.scope = scope with SpooledTemporaryFile(max_size=65536) as body: # Alright, wait for the http.request messages while True: message = await receive() if message["type"] != "http.request": raise ValueError( "WSGI wrapper received a non-HTTP-request message") body.write(message.get("body", b"")) if not message.get("more_body"): break body.seek(0) # Wrap send so it can be called from the subthread self.sync_send = await_(send) # Call the WSGI app await self.run_wsgi_app(body) def build_environ(self, scope, body): """Builds a scope and request body into a WSGI environ object.""" environ = { "REQUEST_METHOD": scope["method"], "SCRIPT_NAME": scope.get("root_path", ""), "PATH_INFO": scope["path"], "QUERY_STRING": scope["query_string"].decode("ascii"), "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], "wsgi.version": (1, 0), "wsgi.url_scheme": scope.get("scheme", "http"), "wsgi.input": body, "wsgi.errors": sys.stderr, "wsgi.multithread": True, "wsgi.multiprocess": True, "wsgi.run_once": False, } # Get server name and port - required in WSGI, not in ASGI if "server" in scope: environ["SERVER_NAME"] = scope["server"][0] environ["SERVER_PORT"] = str(scope["server"][1]) else: environ["SERVER_NAME"] = "localhost" environ["SERVER_PORT"] = "80" if "client" in scope: environ["REMOTE_ADDR"] = scope["client"][0] # Go through headers and make them into environ entries for name, value in self.scope.get("headers", []): name = name.decode("latin1") if name == "content-length": corrected_name = "CONTENT_LENGTH" elif name == "content-type": corrected_name = "CONTENT_TYPE" else: corrected_name = "HTTP_%s" % name.upper().replace("-", "_") # HTTPbis say only ASCII chars are allowed in headers, but we # latin1 just in case value = value.decode("latin1") if corrected_name in environ: value = environ[corrected_name] + "," + value environ[corrected_name] = value return environ def start_response(self, status, response_headers, exc_info=None): """WSGI start_response callable.""" # Don't allow re-calling once response has begun if self.response_started: raise exc_info[1].with_traceback(exc_info[2]) # Don't allow re-calling without exc_info if hasattr(self, "response_start") and exc_info is None: raise ValueError( "You cannot call start_response a second time without exc_info" ) # Extract status code status_code, _ = status.split(" ", 1) status_code = int(status_code) # Extract headers headers = [ (name.lower().encode("ascii"), value.encode("ascii")) for name, value in response_headers ] # Build and send response start message. self.response_start = { "type": "http.response.start", "status": status_code, "headers": headers, } @async_ def run_wsgi_app(self, body): """WSGI app greenlet.""" # Translate the scope and incoming request body into a WSGI environ environ = self.build_environ(self.scope, body) # Run the WSGI app for output in self.wsgi_application(environ, self.start_response): # If this is the first response, include the response headers if not self.response_started: self.response_started = True self.sync_send(self.response_start) self.sync_send({"type": "http.response.body", "body": output, "more_body": True}) # Close connection if not self.response_started: self.response_started = True self.sync_send(self.response_start) self.sync_send({"type": "http.response.body"}) ================================================ FILE: src/aioflask/cli.py ================================================ from functools import wraps from inspect import iscoroutinefunction import os import sys from flask.cli import * from flask.cli import AppGroup, ScriptInfo, update_wrapper, \ SeparatedPathType, pass_script_info, get_debug_flag, NoAppException, \ prepare_import from flask.cli import _validate_key from flask.globals import _app_ctx_stack from flask.helpers import get_env from greenletio import await_ from werkzeug.utils import import_string import click import uvicorn try: import ssl except ImportError: # pragma: no cover ssl = None OriginalAppGroup = AppGroup def _ensure_sync(func, with_appcontext=False): if not iscoroutinefunction(func): return func def decorated(*args, **kwargs): if with_appcontext: appctx = _app_ctx_stack.top @await_ async def _coro(): with appctx: return await func(*args, **kwargs) else: @await_ async def _coro(): return await func(*args, **kwargs) return _coro() return decorated def with_appcontext(f): """Wraps a callback so that it's guaranteed to be executed with the script's application context. If callbacks are registered directly to the ``app.cli`` object then they are wrapped with this function by default unless it's disabled. """ @click.pass_context def decorator(__ctx, *args, **kwargs): with __ctx.ensure_object(ScriptInfo).load_app().app_context(): return __ctx.invoke(_ensure_sync(f, True), *args, **kwargs) return update_wrapper(decorator, f) class AppGroup(OriginalAppGroup): """This works similar to a regular click :class:`~click.Group` but it changes the behavior of the :meth:`command` decorator so that it automatically wraps the functions in :func:`with_appcontext`. Not to be confused with :class:`FlaskGroup`. """ def command(self, *args, **kwargs): """This works exactly like the method of the same name on a regular :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` unless it's disabled by passing ``with_appcontext=False``. """ wrap_for_ctx = kwargs.pop("with_appcontext", True) def decorator(f): if wrap_for_ctx: f = with_appcontext(f) return click.Group.command(self, *args, **kwargs)(_ensure_sync(f)) return decorator def show_server_banner(env, debug, app_import_path, eager_loading): """Show extra startup messages the first time the server is run, ignoring the reloader. """ if app_import_path is not None: message = f" * Serving Flask app {app_import_path!r}" click.echo(message) click.echo(f" * Environment: {env}") if debug is not None: click.echo(f" * Debug mode: {'on' if debug else 'off'}") class CertParamType(click.ParamType): """Click option type for the ``--cert`` option. Allows either an existing file, the string ``'adhoc'``, or an import for a :class:`~ssl.SSLContext` object. """ name = "path" def __init__(self): self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) def convert(self, value, param, ctx): if ssl is None: raise click.BadParameter('Using "--cert" requires Python to be ' 'compiled with SSL support.', ctx, param) try: return self.path_type(value, param, ctx) except click.BadParameter: value = click.STRING(value, param, ctx).lower() if value == "adhoc": raise click.BadParameter("Aad-hoc certificates are currently " "not supported by aioflask.", ctx, param) return value obj = import_string(value, silent=True) if isinstance(obj, ssl.SSLContext): return obj raise @click.command("run", short_help="Run a development server.") @click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") @click.option("--port", "-p", default=5000, help="The port to bind to.") @click.option( "--cert", type=CertParamType(), help="Specify a certificate file to use HTTPS." ) @click.option( "--key", type=click.Path(exists=True, dir_okay=False, resolve_path=True), callback=_validate_key, expose_value=False, help="The key file to use when specifying a certificate.", ) @click.option( "--reload/--no-reload", default=None, help="Enable or disable the reloader. By default the reloader " "is active if debug is enabled.", ) @click.option( "--debugger/--no-debugger", default=None, help="Enable or disable the debugger. By default the debugger " "is active if debug is enabled.", ) @click.option( "--eager-loading/--lazy-loading", default=None, help="Enable or disable eager loading. By default eager " "loading is enabled if the reloader is disabled.", ) @click.option( "--with-threads/--without-threads", default=True, help="Enable or disable multithreading.", ) @click.option( "--extra-files", default=None, type=SeparatedPathType(), help=( "Extra files that trigger a reload on change. Multiple paths" f" are separated by {os.path.pathsep!r}." ), ) @pass_script_info def run_command(info, host, port, reload, debugger, eager_loading, with_threads, cert, extra_files): """Run a local development server. This server is for development purposes only. It does not provide the stability, security, or performance of production WSGI servers. The reloader and debugger are enabled by default if FLASK_ENV=development or FLASK_DEBUG=1. """ debug = get_debug_flag() if reload is None: reload = debug if debugger is None: debugger = debug if debugger: os.environ['AIOFLASK_USE_DEBUGGER'] = 'true' certfile = None keyfile = None if cert is not None and len(cert) == 2: certfile = cert[0] keyfile = cert[1] show_server_banner(get_env(), debug, info.app_import_path, eager_loading) app_import_path = info.app_import_path if app_import_path is None: for path in ('wsgi', 'app'): if os.path.exists(path) or os.path.exists(path + '.py'): app_import_path = path break if app_import_path is None: raise NoAppException( "Could not locate a Flask application. You did not provide " 'the "FLASK_APP" environment variable, and a "wsgi.py" or ' '"app.py" module was not found in the current directory.' ) if app_import_path.endswith('.py'): app_import_path = app_import_path[:-3] factory = False if app_import_path.endswith('()'): # TODO: this needs to be expanded to accept arguments for the factory # function app_import_path = app_import_path[:-2] factory = True if ':' not in app_import_path: app_import_path += ':app' import_name, app_name = app_import_path.split(':') import_name = prepare_import(import_name) uvicorn.run( import_name + ':' + app_name, factory=factory, host=host, port=port, reload=reload, workers=1, log_level='debug' if debug else 'info', ssl_certfile=certfile, ssl_keyfile=keyfile, ) # currently not supported: # - eager_loading # - with_threads # - adhoc certs # - extra_files ================================================ FILE: src/aioflask/ctx.py ================================================ import sys from greenletio import async_ from flask.ctx import * from flask.ctx import AppContext as OriginalAppContext, \ RequestContext as OriginalRequestContext, _sentinel, _app_ctx_stack, \ _request_ctx_stack, appcontext_popped class AppContext(OriginalAppContext): async def apush(self): """Binds the app context to the current context.""" self.push() async def apop(self, exc=_sentinel): """Pops the app context.""" try: self._refcnt -= 1 if self._refcnt <= 0: if exc is _sentinel: # pragma: no cover exc = sys.exc_info()[1] @async_ def do_teardown_async(): _app_ctx_stack.push(self) self.app.do_teardown_appcontext(exc) _app_ctx_stack.pop() await do_teardown_async() finally: rv = _app_ctx_stack.pop() assert rv is self, \ f"Popped wrong app context. ({rv!r} instead of {self!r})" appcontext_popped.send(self.app) async def __aenter__(self): await self.apush() return self async def __aexit__(self, exc_type, exc_value, tb): await self.apop(exc_value) class RequestContext(OriginalRequestContext): async def apush(self): self.push() async def apop(self, exc=_sentinel): app_ctx = self._implicit_app_ctx_stack.pop() clear_request = False try: if not self._implicit_app_ctx_stack: if hasattr(self, 'preserved'): # Flask < 2.2 self.preserved = False self._preserved_exc = None if exc is _sentinel: # pragma: no cover exc = sys.exc_info()[1] @async_ def do_teardown(): _request_ctx_stack.push(self) self.app.do_teardown_request(exc) _request_ctx_stack.pop() await do_teardown() request_close = getattr(self.request, "close", None) if request_close is not None: # pragma: no branch request_close() clear_request = True finally: rv = _request_ctx_stack.pop() # get rid of circular dependencies at the end of the request # so that we don't require the GC to be active. if clear_request: rv.request.environ["werkzeug.request"] = None # Get rid of the app as well if necessary. if app_ctx is not None: await app_ctx.apop(exc) assert ( rv is self ), f"Popped wrong request context. ({rv!r} instead of {self!r})" async def aauto_pop(self, exc): if hasattr(self, 'preserved'): # Flask < 2.2 if self.request.environ.get("flask._preserve_context") or ( exc is not None and self.app.preserve_context_on_exception ): # pragma: no cover self.preserved = True self._preserved_exc = exc else: await self.apop(exc) else: await self.apop(exc) async def __aenter__(self): await self.apush() return self async def __aexit__(self, exc_type, exc_value, tb): await self.aauto_pop(exc_value) ================================================ FILE: src/aioflask/patch.py ================================================ from functools import wraps from aioflask import current_app def patch_decorator(decorator): def patched_decorator(f): @wraps(f) def ensure_sync(*a, **kw): return current_app.ensure_sync(f)(*a, **kw) return decorator(ensure_sync) return patched_decorator def patch_decorator_with_args(decorator): def patched_decorator(*args, **kwargs): def inner_patched_decorator(f): @wraps(f) def ensure_sync(*a, **kw): return current_app.ensure_sync(f)(*a, **kw) return decorator(*args, **kwargs)(ensure_sync) return inner_patched_decorator return patched_decorator def patch_decorator_method(class_, method_name): original_decorator = getattr(class_, method_name) def patched_decorator_method(self, f): @wraps(f) def ensure_sync(*a, **kw): return current_app.ensure_sync(f)(*a, **kw) return original_decorator(self, ensure_sync) return patched_decorator_method def patch_decorator_method_with_args(class_, method_name): original_decorator = getattr(class_, method_name) def patched_decorator_method(self, *args, **kwargs): def inner_patched_decorator_method(f): @wraps(f) def ensure_sync(*a, **kw): return current_app.ensure_sync(f)(*a, **kw) return original_decorator(self, *args, **kwargs)(ensure_sync) return inner_patched_decorator_method return patched_decorator_method ================================================ FILE: src/aioflask/patched/__init__.py ================================================ ================================================ FILE: src/aioflask/patched/flask_login/__init__.py ================================================ from functools import wraps import sys from werkzeug.local import LocalProxy from aioflask import current_app, g from flask import _request_ctx_stack from aioflask.patch import patch_decorator, patch_decorator_method import flask_login from flask_login import login_required, fresh_login_required, \ LoginManager as OriginalLoginManager for symbol in flask_login.__all__: try: globals()[symbol] = getattr(flask_login, symbol) except AttributeError: pass def _user_context_processor(): return {'current_user': _get_user()} def _load_user(): # Obtain the current user and preserve it in the g object. Flask-Login # saves the user in a custom attribute of the request context, but that # doesn't work with aioflask because when a copy of the request context is # made, custom attributes are not carried over to the copy. current_app.login_manager._load_user() g.flask_login_current_user = _request_ctx_stack.top.user def _get_user(): # Return the current user. This function is linked to the current_user # context local, but unlike the original in Flask-Login, it does not # attempt to load the user, it just returns the user that was pre-loaded. # This avoids the somewhat tricky complication of triggering database # operations that need to be awaited, which would require using something # like (await current_user) if hasattr(g, 'flask_login_current_user'): return g.flask_login_current_user return current_app.login_manager.anonymous_user() class LoginManager(OriginalLoginManager): def init_app(self, app, add_context_processor=True): super().init_app(app, add_context_processor=False) if add_context_processor: app.context_processor(_user_context_processor) # To prevent the current_user context local from triggering I/O at a # random time when it is first referenced (which is a big complication # if the I/O is async and needs to be awaited), we force the user to be # loaded before each request. This isn't a perfect solution, because # a before request handler registered before this one will not see the # current user. app.before_request(_load_user) # the decorators that register callbacks need to be patched to support # async views user_loader = patch_decorator_method(OriginalLoginManager, 'user_loader') header_loader = patch_decorator_method( OriginalLoginManager, 'header_loader') request_loader = patch_decorator_method( OriginalLoginManager, 'request_loader') unauthorized_handler = patch_decorator_method( OriginalLoginManager, 'unauthorized_handler') needs_refresh_handler = patch_decorator_method( OriginalLoginManager, 'needs_refresh_handler') # patch the two login_required decorators so that they accept async views login_required = patch_decorator(login_required) fresh_login_required = patch_decorator(fresh_login_required) # redefine the current_user context local current_user = LocalProxy(_get_user) # patch the _get_user() function in the flask_login.utils module so that any # calls to get current_user in Flask-Login functions are redirected here setattr(sys.modules['flask_login.utils'], '_get_user', _get_user) ================================================ FILE: src/aioflask/templating.py ================================================ from flask.templating import * from flask.templating import _app_ctx_stack, before_render_template, \ template_rendered async def _render(template, context, app): """Renders the template and fires the signal""" before_render_template.send(app, template=template, context=context) rv = await template.render_async(context) template_rendered.send(app, template=template, context=context) return rv async def render_template(template_name_or_list, **context): """Renders a template from the template folder with the given context. :param template_name_or_list: the name of the template to be rendered, or an iterable with template names the first one existing will be rendered :param context: the variables that should be available in the context of the template. """ ctx = _app_ctx_stack.top ctx.app.update_template_context(context) return await _render( ctx.app.jinja_env.get_or_select_template(template_name_or_list), context, ctx.app, ) async def render_template_string(source, **context): """Renders a template from the given template source string with the given context. Template variables will be autoescaped. :param source: the source code of the template to be rendered :param context: the variables that should be available in the context of the template. """ ctx = _app_ctx_stack.top ctx.app.update_template_context(context) return await _render(ctx.app.jinja_env.from_string(source), context, ctx.app) ================================================ FILE: src/aioflask/testing.py ================================================ from flask.testing import * from flask.testing import FlaskClient as OriginalFlaskClient, \ FlaskCliRunner as OriginalFlaskCliRunner from flask import _request_ctx_stack from werkzeug.test import run_wsgi_app from greenletio import async_ class FlaskClient(OriginalFlaskClient): def run_wsgi_app(self, environ, buffered=False): """Runs the wrapped WSGI app with the given environment. :meta private: """ if self.cookie_jar is not None: self.cookie_jar.inject_wsgi(environ) rv = run_wsgi_app(self.application.wsgi_app, environ, buffered=buffered) if self.cookie_jar is not None: self.cookie_jar.extract_wsgi(environ, rv[2]) return rv async def get(self, *args, **kwargs): return await async_(super().get)(*args, **kwargs) async def post(self, *args, **kwargs): return await async_(super().post)(*args, **kwargs) async def put(self, *args, **kwargs): return await async_(super().put)(*args, **kwargs) async def patch(self, *args, **kwargs): return await async_(super().patch)(*args, **kwargs) async def delete(self, *args, **kwargs): return await async_(super().delete)(*args, **kwargs) async def head(self, *args, **kwargs): return await async_(super().head)(*args, **kwargs) async def options(self, *args, **kwargs): return await async_(super().options)(*args, **kwargs) async def trace(self, *args, **kwargs): return await async_(super().trace)(*args, **kwargs) async def __aenter__(self): if self.preserve_context: raise RuntimeError("Cannot nest client invocations") self.preserve_context = True return self async def __aexit__(self, exc_type, exc_value, tb): self.preserve_context = False # Normally the request context is preserved until the next # request in the same thread comes. When the client exits we # want to clean up earlier. Pop request contexts until the stack # is empty or a non-preserved one is found. while True: top = _request_ctx_stack.top if top is not None and top.preserved: await top.apop() else: break class FlaskCliRunner(OriginalFlaskCliRunner): async def invoke(self, *args, **kwargs): return await async_(super().invoke)(*args, **kwargs) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/templates/template.html ================================================ {{ g.x }}{{ session.y }} ================================================ FILE: tests/test_app.py ================================================ import asyncio import os import unittest from unittest import mock import aioflask from .utils import async_test class TestApp(unittest.TestCase): @async_test async def test_app(self): app = aioflask.Flask(__name__) @app.route('/async') async def async_route(): await asyncio.sleep(0) assert aioflask.current_app._get_current_object() == app return 'async' @app.route('/sync') def sync_route(): assert aioflask.current_app._get_current_object() == app return 'sync' client = app.test_client() response = await client.get('/async') assert response.data == b'async' response = await client.get('/sync') assert response.data == b'sync' @async_test async def test_g(self): app = aioflask.Flask(__name__) app.secret_key = 'secret' @app.before_request async def async_before_request(): aioflask.g.asyncvar = 'async' @app.before_request def sync_before_request(): aioflask.g.syncvar = 'sync' @app.route('/async') async def async_route(): aioflask.session['a'] = 'async' return f'{aioflask.g.asyncvar}-{aioflask.g.syncvar}' @app.route('/sync') async def sync_route(): aioflask.session['s'] = 'sync' return f'{aioflask.g.asyncvar}-{aioflask.g.syncvar}' @app.route('/session') async def session(): return f'{aioflask.session.get("a")}-{aioflask.session.get("s")}' @app.after_request async def after_request(rv): rv.data += f'/{aioflask.g.asyncvar}-{aioflask.g.syncvar}'.encode() return rv client = app.test_client() response = await client.get('/session') assert response.data == b'None-None/async-sync' response = await client.get('/async') assert response.data == b'async-sync/async-sync' response = await client.get('/session') assert response.data == b'async-None/async-sync' response = await client.get('/sync') assert response.data == b'async-sync/async-sync' response = await client.get('/session') assert response.data == b'async-sync/async-sync' @mock.patch('aioflask.app.uvicorn') def test_app_run(self, uvicorn): app = aioflask.Flask(__name__) app.run() uvicorn.run.assert_called_with('tests.test_app:app', host='127.0.0.1', port=5000, reload=False, workers=1, log_level='info', ssl_certfile=None, ssl_keyfile=None) app.run(host='1.2.3.4', port=3000) uvicorn.run.assert_called_with('tests.test_app:app', host='1.2.3.4', port=3000, reload=False, workers=1, log_level='info', ssl_certfile=None, ssl_keyfile=None) app.run(debug=True) uvicorn.run.assert_called_with('tests.test_app:app', host='127.0.0.1', port=5000, reload=True, workers=1, log_level='debug', ssl_certfile=None, ssl_keyfile=None) app.run(debug=True, use_reloader=False) uvicorn.run.assert_called_with('tests.test_app:app', host='127.0.0.1', port=5000, reload=False, workers=1, log_level='debug', ssl_certfile=None, ssl_keyfile=None) if 'FLASK_DEBUG' in os.environ: del os.environ['FLASK_DEBUG'] if 'AIOFLASK_USE_DEBUGGER' in os.environ: del os.environ['AIOFLASK_USE_DEBUGGER'] ================================================ FILE: tests/test_cli.py ================================================ import os import unittest from unittest import mock import click from click.testing import CliRunner import aioflask import aioflask.cli from .utils import async_test class TestCli(unittest.TestCase): @async_test async def test_command_with_appcontext(self): app = aioflask.Flask('testapp') @app.cli.command(with_appcontext=True) async def testcmd(): click.echo(aioflask.current_app.name) result = await app.test_cli_runner().invoke(testcmd) assert result.exit_code == 0 assert result.output == "testapp\n" @async_test async def test_command_without_appcontext(self): app = aioflask.Flask('testapp') @app.cli.command(with_appcontext=False) async def testcmd(): click.echo(aioflask.current_app.name) result = await app.test_cli_runner().invoke(testcmd) assert result.exit_code == 1 assert type(result.exception) == RuntimeError @async_test async def test_with_appcontext(self): @click.command() @aioflask.cli.with_appcontext async def testcmd(): click.echo(aioflask.current_app.name) app = aioflask.Flask('testapp') result = await app.test_cli_runner().invoke(testcmd) assert result.exit_code == 0 assert result.output == "testapp\n" @mock.patch('aioflask.cli.uvicorn') def test_aiorun(self, uvicorn): app = aioflask.Flask('testapp') obj = aioflask.cli.ScriptInfo(app_import_path='app.py', create_app=lambda: app) result = CliRunner().invoke(aioflask.cli.run_command, obj=obj) assert result.exit_code == 0 uvicorn.run.assert_called_with('app:app', factory=False, host='127.0.0.1', port=5000, reload=False, workers=1, log_level='info', ssl_certfile=None, ssl_keyfile=None) result = CliRunner().invoke(aioflask.cli.run_command, '--host 1.2.3.4 --port 3000', obj=obj) assert result.exit_code == 0 uvicorn.run.assert_called_with('app:app', factory=False, host='1.2.3.4', port=3000, reload=False, workers=1, log_level='info', ssl_certfile=None, ssl_keyfile=None) os.environ['FLASK_DEBUG'] = 'true' result = CliRunner().invoke(aioflask.cli.run_command, obj=obj) assert result.exit_code == 0 uvicorn.run.assert_called_with('app:app', factory=False, host='127.0.0.1', port=5000, reload=True, workers=1, log_level='debug', ssl_certfile=None, ssl_keyfile=None) os.environ['FLASK_DEBUG'] = 'true' result = CliRunner().invoke(aioflask.cli.run_command, '--no-reload', obj=obj) assert result.exit_code == 0 uvicorn.run.assert_called_with('app:app', factory=False, host='127.0.0.1', port=5000, reload=False, workers=1, log_level='debug', ssl_certfile=None, ssl_keyfile=None) if 'FLASK_DEBUG' in os.environ: del os.environ['FLASK_DEBUG'] if 'AIOFLASK_USE_DEBUGGER' in os.environ: del os.environ['AIOFLASK_USE_DEBUGGER'] @mock.patch('aioflask.cli.uvicorn') def test_aiorun_with_factory(self, uvicorn): app = aioflask.Flask('testapp') obj = aioflask.cli.ScriptInfo(app_import_path='app:create_app()', create_app=lambda: app) result = CliRunner().invoke(aioflask.cli.run_command, obj=obj) assert result.exit_code == 0 uvicorn.run.assert_called_with('app:create_app', factory=True, host='127.0.0.1', port=5000, reload=False, workers=1, log_level='info', ssl_certfile=None, ssl_keyfile=None) ================================================ FILE: tests/test_ctx.py ================================================ import unittest import pytest import aioflask from .utils import async_test class TestApp(unittest.TestCase): @async_test async def test_app_context(self): app = aioflask.Flask(__name__) called_t1 = False called_t2 = False @app.teardown_appcontext async def t1(exc): nonlocal called_t1 called_t1 = True @app.teardown_appcontext def t2(exc): nonlocal called_t2 called_t2 = True async with app.app_context(): assert aioflask.current_app == app async with app.app_context(): assert aioflask.current_app == app assert aioflask.current_app == app assert called_t1 assert called_t2 with pytest.raises(RuntimeError): print(aioflask.current_app) @async_test async def test_req_context(self): app = aioflask.Flask(__name__) called_t1 = False called_t2 = False @app.teardown_appcontext async def t1(exc): nonlocal called_t1 called_t1 = True @app.teardown_appcontext def t2(exc): nonlocal called_t2 called_t2 = True async with app.test_request_context('/foo'): assert aioflask.current_app == app assert aioflask.request.path == '/foo' assert called_t1 assert called_t2 async with app.app_context(): async with app.test_request_context('/bar') as reqctx: assert aioflask.current_app == app assert aioflask.request.path == '/bar' async with reqctx: assert aioflask.current_app == app assert aioflask.request.path == '/bar' with pytest.raises(RuntimeError): print(aioflask.current_app) ================================================ FILE: tests/test_patch.py ================================================ import unittest import aioflask import aioflask.patch from .utils import async_test class TestPatch(unittest.TestCase): @async_test async def test_decorator(self): def foo(f): def decorator(*args, **kwargs): return f(*args, **kwargs) + '-decorated' return decorator foo = aioflask.patch.patch_decorator(foo) app = aioflask.Flask(__name__) @app.route('/abc/') @foo async def abc(id): return str(id) client = app.test_client() response = await client.get('/abc/123') assert response.data == b'123-decorated' @async_test async def test_decorator_with_args(self): def foo(value): def inner_foo(f): def decorator(*args, **kwargs): return f(*args, **kwargs) + str(value) return decorator return inner_foo foo = aioflask.patch.patch_decorator_with_args(foo) app = aioflask.Flask(__name__) @app.route('/abc/') @foo(456) async def abc(id): return str(id) client = app.test_client() response = await client.get('/abc/123') assert response.data == b'123456' @async_test async def test_decorator_method(self): class Foo: def __init__(self, value): self.value = value def deco(self, f): def decorator(*args, **kwargs): return f(*args, **kwargs) + str(self.value) return decorator Foo.deco = aioflask.patch.patch_decorator_method(Foo, 'deco') app = aioflask.Flask(__name__) foo = Foo(456) @app.route('/abc/') @foo.deco async def abc(id): return str(id) client = app.test_client() response = await client.get('/abc/123') assert response.data == b'123456' @async_test async def test_decorator_method_with_args(self): class Foo: def __init__(self, value): self.value = value def deco(self, value2): def decorator(f): def inner_decorator(*args, **kwargs): return f(*args, **kwargs) + str(self.value) + \ str(value2) return inner_decorator return decorator Foo.deco = aioflask.patch.patch_decorator_method_with_args(Foo, 'deco') app = aioflask.Flask(__name__) foo = Foo(456) @app.route('/abc/') @foo.deco(789) async def abc(id): return str(id) client = app.test_client() response = await client.get('/abc/123') assert response.data == b'123456789' ================================================ FILE: tests/test_templating.py ================================================ import asyncio import os import unittest from unittest import mock import aioflask from .utils import async_test class TestTemplating(unittest.TestCase): @async_test async def test_template_strng(self): app = aioflask.Flask(__name__) app.secret_key = 'secret' @app.before_request def before_request(): aioflask.g.x = 'foo' aioflask.session['y'] = 'bar' @app.route('/') async def async_route(): return await aioflask.render_template_string( '{{ g.x }}{{ session.y }}') client = app.test_client() response = await client.get('/') assert response.data == b'foobar' @async_test async def test_template(self): app = aioflask.Flask(__name__) app.secret_key = 'secret' @app.before_request def before_request(): aioflask.g.x = 'foo' aioflask.session['y'] = 'bar' @app.route('/') async def async_route(): return await aioflask.render_template('template.html') client = app.test_client() response = await client.get('/') assert response.data == b'foobar' ================================================ FILE: tests/utils.py ================================================ import asyncio from greenletio.core import bridge def async_test(f): def wrapper(*args, **kwargs): asyncio.get_event_loop().run_until_complete(f(*args, **kwargs)) return wrapper ================================================ FILE: tox.ini ================================================ [tox] envlist=flake8,,py37,py38,py39,py310,pypy3,docs skip_missing_interpreters=True [gh-actions] python = 3.7: py37 3.8: py38 3.9: py39 3.10: py310 pypy3: pypy-3 [testenv] commands= pip install -e . pytest -p no:logging --cov=src/aioflask --cov-branch examples/aioflaskr/tests pytest -p no:logging --cov=src/aioflask --cov-branch --cov-report=term-missing --cov-report=xml --cov-append tests deps= aiosqlite greenletio alchemical flask-login pytest pytest-asyncio pytest-cov [testenv:flake8] deps= flake8 commands= flake8 --ignore=F401,F403 --exclude=".*" src/aioflask tests [testenv:docs] changedir=docs deps= sphinx whitelist_externals= make commands= make html