Repository: dchevell/flask-executor

Issue:
Flask 2.2 breaks Executor
The recent Flask 2.2 release throws all kinds of context errors when submitting a job:
```
  File /site-packages/flask/ctx.py, line 256, in pop
    _cv_app.reset(self._cv_tokens.pop())
ValueError: <Token var=<ContextVar name='flask.app_ctx' at 0x7fb4152020b0> at 0x7fb4054b1f50> was created in a different Context
```

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1101e96..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-dist: xenial
-language: python
-python:
-  - 2.7
-  - 3.5
-  - 3.6
-  - 3.7
-  - 3.8-dev
-install:
-  - pip install -q "pytest>=3.6"
-  - pip install -q pytest-flask
-  - pip install -q pytest-cov coveralls
-  - pip install -e .
-script:
-  - py.test --cov=flask_executor/ tests/ .
-after_success:
-  - coveralls
diff --git a/README.md b/README.md
index 60f3700..76ddb55 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 Flask-Executor
 ==============
 
-[![Build Status](https://travis-ci.org/dchevell/flask-executor.svg?branch=master)](https://travis-ci.org/dchevell/flask-executor)
+[![Build Status](https://github.com/dchevell/flask-executor/actions/workflows/tests.yml/badge.svg)
 [![Coverage Status](https://coveralls.io/repos/github/dchevell/flask-executor/badge.svg)](https://coveralls.io/github/dchevell/flask-executor)
 [![PyPI Version](https://img.shields.io/pypi/v/Flask-Executor.svg)](https://pypi.python.org/pypi/Flask-Executor)
 [![GitHub license](https://img.shields.io/github/license/dchevell/flask-executor.svg)](https://github.com/dchevell/flask-executor/blob/master/LICENSE)
@@ -50,7 +50,7 @@ copy of both the current application context and current request context. Code t
 these contexts or that depends on information or configuration stored in `flask.current_app`,
 `flask.request` or `flask.g` can be submitted to the executor without modification.
 
-Note: due to limitations in Python's default object serialisation and a lack of shared memory space between subprocesses, contexts cannot be pushed to `ProcessPoolExecutor()` workers. 
+Note: due to limitations in Python's default object serialisation and a lack of shared memory space between subprocesses, contexts cannot be pushed to `ProcessPoolExecutor()` workers.
 
 
 Futures
@@ -124,7 +124,7 @@ checked for. To instead surface all exceptions thrown by background tasks, Flask
 a special default callback that raises any exceptions thrown by tasks submitted to the executor::
 
 ```python
-app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = True 
+app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = True
 ```
 
 
diff --git a/flask_executor/executor.py b/flask_executor/executor.py
index 2268d69..fe48d73 100644
--- a/flask_executor/executor.py
+++ b/flask_executor/executor.py
@@ -1,28 +1,31 @@
 import concurrent.futures
+import contextvars
+import copy
 import re
 
-from flask import copy_current_request_context
-from flask.globals import _app_ctx_stack, current_app
+from flask import copy_current_request_context, current_app, g
 
 from flask_executor.futures import FutureCollection, FutureProxy
 from flask_executor.helpers import InstanceProxy, str2bool
 
 
+def get_current_app_context():
+    try:
+        from flask.globals import _cv_app
+        return _cv_app.get(None)
+    except ImportError:
+        from flask.globals import _app_ctx_stack
+        return _app_ctx_stack.top
+
+
 def push_app_context(fn):
     app = current_app._get_current_object()
+    _g = copy.copy(g)
 
     def wrapper(*args, **kwargs):
         with app.app_context():
-            return fn(*args, **kwargs)
-
-    return wrapper
-
-
-def copy_current_app_context(fn):
-    app_context = _app_ctx_stack.top
-
-    def wrapper(*args, **kwargs):
-        with app_context:
+            ctx = get_current_app_context()
+            ctx.g = _g
             return fn(*args, **kwargs)
 
     return wrapper
@@ -93,7 +96,7 @@ class Executor(InstanceProxy, concurrent.futures._base.Executor):
             * :class:`concurrent.futures.ProcessPoolExecutor`
         """
         app.config.setdefault(self.EXECUTOR_TYPE, 'thread')
-        app.config.setdefault(self.EXECUTOR_PUSH_APP_CONTEXT, False)
+        app.config.setdefault(self.EXECUTOR_PUSH_APP_CONTEXT, True)
         futures_max_length = app.config.setdefault(self.EXECUTOR_FUTURES_MAX_LENGTH, None)
         propagate_exceptions = app.config.setdefault(self.EXECUTOR_PROPAGATE_EXCEPTIONS, False)
         if futures_max_length is not None:
@@ -122,8 +125,6 @@ class Executor(InstanceProxy, concurrent.futures._base.Executor):
             fn = copy_current_request_context(fn)
             if current_app.config[self.EXECUTOR_PUSH_APP_CONTEXT]:
                 fn = push_app_context(fn)
-            else:
-                fn = copy_current_app_context(fn)
         return fn
 
     def submit(self, fn, *args, **kwargs):
diff --git a/setup.py b/setup.py
index 7d8a8f5..69a21ae 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@ except ImportError:
         version = pattern.search(f.read()).group(1)
 
 
-with open("README.md", "r") as fh:
+with open('README.md', 'r') as fh:
     long_description = fh.read()
 
 
@@ -30,9 +30,9 @@ setuptools.setup(
     author_email='chevell@gmail.com',
     description='An easy to use Flask wrapper for concurrent.futures',
     long_description=long_description,
-    long_description_content_type="text/markdown",
+    long_description_content_type='text/markdown',
     url='https://github.com/dchevell/flask-executor',
-    packages=setuptools.find_packages(exclude=["tests"]),
+    packages=setuptools.find_packages(exclude=['tests']),
     keywords=['flask', 'concurrent.futures'],
     classifiers=[
         "Programming Language :: Python :: 3",
@@ -42,9 +42,9 @@ setuptools.setup(
     license='MIT',
     install_requires=['Flask'],
     extras_require={
-        ':python_version == "2.7"': ['futures>=3.1.1']
+        ':python_version == "2.7"': ['futures>=3.1.1'],
+        'test': ['pytest', 'flask-sqlalchemy'],
     },
-    tests_require=['pytest', 'pytest-flask', 'python-coveralls'],
     test_suite='tests',
     cmdclass={
         'test': pytest


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..be1c837
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,25 @@
+name: Flask-Executor tests
+
+on: [push]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.7", "3.8", "3.9", "3.10"]
+        flask-version: ["<2.2", ">=2.2"]
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -q "flask ${{ matrix.flask-version }}"
+          pip install -e .[test]
+      - name: Test with pytest
+        run: |
+          pytest
\ No newline at end of file
diff --git a/tests/test_executor.py b/tests/test_executor.py
index 42c03e0..e11d6c3 100644
--- a/tests/test_executor.py
+++ b/tests/test_executor.py
@@ -155,11 +155,11 @@ def test_process_decorator(default_app):
         assert 0
 
 
-def test_submit_app_context(app):
+def test_submit_app_context(default_app):
     test_value = random.randint(1, 101)
-    app.config['TEST_VALUE'] = test_value
-    executor = Executor(app)
-    with app.test_request_context(''):
+    default_app.config['TEST_VALUE'] = test_value
+    executor = Executor(default_app)
+    with default_app.test_request_context(''):
         future = executor.submit(app_context_test_value)
     assert future.result() == test_value
 
@@ -173,21 +173,21 @@ def test_submit_g_context_process(default_app):
     assert future.result() == test_value
 
 
-def test_submit_request_context(app):
+def test_submit_request_context(default_app):
     test_value = random.randint(1, 101)
-    executor = Executor(app)
-    with app.test_request_context(''):
+    executor = Executor(default_app)
+    with default_app.test_request_context(''):
         request.test_value = test_value
         future = executor.submit(request_context_test_value)
     assert future.result() == test_value
 
 
-def test_map_app_context(app):
+def test_map_app_context(default_app):
     test_value = random.randint(1, 101)
     iterator = list(range(5))
-    app.config['TEST_VALUE'] = test_value
-    executor = Executor(app)
-    with app.test_request_context(''):
+    default_app.config['TEST_VALUE'] = test_value
+    executor = Executor(default_app)
+    with default_app.test_request_context(''):
         results = executor.map(app_context_test_value, iterator)
     for r in results:
         assert r == test_value
@@ -204,11 +204,11 @@ def test_map_g_context_process(default_app):
         assert r == test_value
 
 
-def test_map_request_context(app):
+def test_map_request_context(default_app):
     test_value = random.randint(1, 101)
     iterator = list(range(5))
-    executor = Executor(app)
-    with app.test_request_context('/'):
+    executor = Executor(default_app)
+    with default_app.test_request_context('/'):
         request.test_value = test_value
         results = executor.map(request_context_test_value, iterator)
     for r in results:
@@ -285,7 +285,6 @@ def test_propagate_exception_callback(app, caplog):
     with pytest.raises(NameError):
         with app.test_request_context('/'):
             future = executor.submit(fail)
-            assert propagate_exceptions_callback in future._done_callbacks
             concurrent.futures.wait([future])
             future.result()
 
@@ -347,20 +346,6 @@ def test_teardown_appcontext_is_called(default_app):
         [propagate_exceptions_callback(future) for future in futures]
 
 
-def test_teardown_appcontext_is_not_called(default_app):
-    default_app.config['EXECUTOR_MAX_WORKERS'] = 1
-    default_app.config['EXECUTOR_PUSH_APP_CONTEXT'] = False
-    default_app.teardown_appcontext(clear_thread_local)
-
-    executor = Executor(default_app)
-    with pytest.raises(ValueError):
-        with default_app.test_request_context():
-            for i in range(2):
-                future = executor.submit(set_thread_local)
-                concurrent.futures.wait([future])
-                propagate_exceptions_callback(future)
-
-
 try:
     import flask_sqlalchemy
 except ImportError: