{{ post.title }}
{{ post.body }}
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  [](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 """
{{ post.body }}
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 = '''"{{ 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 = '''"{{ 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/