From 8efea0ccbbf0985e0c3c001e5bd90b4912a18f54 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 3 Oct 2020 07:05:05 -0700 Subject: [PATCH] Break reference cycle created by default in Flask instances. Flask instances with static folders were creating a reference cycle via their "static" view function (which held a strong reference back to the Flask instance to call its `send_static_file` method). This prevented CPython from freeing the memory for a Flask instance when all external references to it were released. Now use a weakref for the back reference to avoid this. Co-authored-by: Joshua Bronson --- src/flask/app.py | 6 +++++- tests/test_basic.py | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/flask/app.py b/src/flask/app.py index f4d35658..eaeb613e 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1,5 +1,6 @@ import os import sys +import weakref from datetime import timedelta from itertools import chain from threading import Lock @@ -478,11 +479,14 @@ class Flask(Scaffold): assert ( bool(static_host) == host_matching ), "Invalid static_host/host_matching combination" + # Use a weakref to avoid creating a reference cycle between the app + # and the view function (see #3761). + self_ref = weakref.ref(self) self.add_url_rule( f"{self.static_url_path}/", endpoint="static", host=static_host, - view_func=self.send_static_file, + view_func=lambda **kw: self_ref().send_static_file(**kw), ) # Set the name of the Click group in case someone wants to add diff --git a/tests/test_basic.py b/tests/test_basic.py index 6c45b008..76d3e1fc 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,8 +1,11 @@ +import gc import re import sys import time import uuid +import weakref from datetime import datetime +from platform import python_implementation from threading import Thread import pytest @@ -16,6 +19,11 @@ from werkzeug.routing import BuildError import flask +require_cpython_gc = pytest.mark.skipif( + python_implementation() != "CPython", reason="Requires CPython GC behavior", +) + + def test_options_work(app, client): @app.route("/", methods=["GET", "POST"]) def index(): @@ -1970,3 +1978,19 @@ def test_max_cookie_size(app, client, recwarn): client.get("/") assert len(recwarn) == 0 + + +@require_cpython_gc +def test_app_freed_on_zero_refcount(): + # A Flask instance should not create a reference cycle that prevents CPython + # from freeing it when all external references to it are released (see #3761). + gc.disable() + try: + app = flask.Flask(__name__) + assert app.view_functions["static"] + weak = weakref.ref(app) + assert weak() is not None + del app + assert weak() is None + finally: + gc.enable()