Repository: pallets/werkzeug

Issue:
change ident_func RuntimeError to DeprecationWarning
I am not sure if this is a problem with werkzeug 2.0b1, or just Flask not declaring its dependency correctly, and installing werkzug 2.0b1 instead of a compatible version (1.x). The effect is that I get `RuntimeError: The __ident_func__ should not be used in Python 3.7+ as a ContextVar is used.` in all my tests (they worked fine before 2.0b1).

You can see the failed build here:
https://app.circleci.com/pipelines/github/epandurski/flask_signalbus/421/workflows/de9e9d0f-4346-48b0-979a-250e01400056/jobs/755

Here is the package that I am trying to build: https://github.com/epandurski/flask_signalbus
It uses Flask-SQLAlchemy. I guess that ```self.session = self.create_scoped_session(session_options)``` is the source of the problem, but I have not looked at it thoroughly.


Environment:

- Python version: 3.9.1
- Werkzeug version: 2.0b1



Discussion:
This happens with Werkzeug 2.0rc1 and Flask-SQLAlchemy 2.4.4 as well.

```python
  File "/home/adrian/dev/indico/py3/env/lib/python3.9/site-packages/flask_sqlalchemy/__init__.py", line 714, in __init__
    self.session = self.create_scoped_session(session_options)
  File "/home/adrian/dev/indico/py3/env/lib/python3.9/site-packages/flask_sqlalchemy/__init__.py", line 747, in create_scoped_session
    scopefunc = options.pop('scopefunc', _app_ctx_stack.__ident_func__)
  File "/home/adrian/dev/indico/py3/env/lib/python3.9/site-packages/werkzeug/local.py", line 201, in __ident_func__
    return self._local.__ident_func__
  File "/home/adrian/dev/indico/py3/env/lib/python3.9/site-packages/werkzeug/local.py", line 108, in __ident_func__
    raise RuntimeError(
RuntimeError: The __ident_func__ should not be used in Python 3.7+ as a ContextVar is used.
```

Edit: Ah, just noticed that in your case it's also flask-sqlalchemy causing it in the end.
Actually, going to move this back to Werkzeug, as it should be showing a deprecation warning not a runtime error in 2.0. Flask-SQLAlchemy should also update, I'll open a new issue there.

And if you did `pip install flask` and got Werkzeug 2.0b1, you need to upgrade your pip and setuptools versions immediately.
I do not think that the problem is the version of pip/setuptools. Here is the setup() function of the package that has Flask-SQLAlchemy as a dependency. The build bot installs the most recent python docker image.

```
setup(
    name='Flask-SignalBus',
    version='0.5.6',
    url='https://github.com/epandurski/flask_signalbus',
    license='MIT',
    author='Evgeni Pandurski',
    author_email='epandurski@gmail.com',
    description='A Flask-SQLAlchemy extension for atomically sending messages (signals) over a message bus',
    long_description=__doc__,
    packages=['flask_signalbus'],
    zip_safe=True,
    platforms='any',
    setup_requires=pytest_runner,
    install_requires=[
        'Flask-SQLAlchemy>=1.0',
        'marshmallow-sqlalchemy>=0.22.0',
    ],
    tests_require=[
        'pytest~=4.6',
        'pytest-cov~=2.7',
        'mock~=2.0',
    ],
    classifiers=[
        'Environment :: Web Environment',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: BSD License',
        'Operating System :: OS Independent',
        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
        'Topic :: Software Development :: Libraries :: Python Modules',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
    ]
)
```
The problem is that your setup installs prereleases. `python setup.py test` and letting it fetch dependencies via `tests_require` instead of using `pip` to install them seems to use super legacy workflows - for example, I see it building eggs instead of wheels, and installing prereleases...
Using setuptools for any commands except builds is deprecated by setuptools, so `setup.py test`, `install`, `develop` etc. You're now discovering why: none of those support the majority of the metadata that PyPI/wheels/pip uses. You need to use `pip` to install packages, and should use `pytest`directly to run tests.
Thanks for pointing this out. This behavior is kind of useful, though -- an early alarm for upcoming problems :)

Resolving patch:
diff --git a/examples/plnt/database.py b/examples/plnt/database.py
--- a/examples/plnt/database.py
+++ b/examples/plnt/database.py
@@ -11,7 +11,11 @@
 from sqlalchemy.orm import scoped_session
 
 from .utils import application
-from .utils import local_manager
+
+try:
+    from greenlet import getcurrent as get_ident
+except ImportError:
+    from threading import get_ident
 
 
 def new_db_session():
@@ -19,7 +23,7 @@ def new_db_session():
 
 
 metadata = MetaData()
-session = scoped_session(new_db_session, local_manager.get_ident)
+session = scoped_session(new_db_session, get_ident)
 
 
 blog_table = Table(
diff --git a/examples/simplewiki/database.py b/examples/simplewiki/database.py
--- a/examples/simplewiki/database.py
+++ b/examples/simplewiki/database.py
@@ -14,9 +14,12 @@
 from sqlalchemy.orm import scoped_session
 
 from .utils import application
-from .utils import local_manager
 from .utils import parse_creole
 
+try:
+    from greenlet import getcurrent as get_ident
+except ImportError:
+    from threading import get_ident
 
 # create a global metadata
 metadata = MetaData()
@@ -35,7 +38,7 @@ def new_db_session():
 
 # and create a new global session factory.  Calling this object gives
 # you the current active session
-session = scoped_session(new_db_session, local_manager.get_ident)
+session = scoped_session(new_db_session, get_ident)
 
 
 # our database tables.
diff --git a/src/werkzeug/local.py b/src/werkzeug/local.py
--- a/src/werkzeug/local.py
+++ b/src/werkzeug/local.py
@@ -8,16 +8,24 @@
 
 from .wsgi import ClosingIterator
 
-# Each thread has its own greenlet, use that as the identifier for the
-# context. If greenlets are not available fall back to the current
-# thread ident.
+if t.TYPE_CHECKING:
+    from wsgiref.types import WSGIApplication
+
 try:
-    from greenlet import getcurrent as get_ident
+    from greenlet import getcurrent as _get_ident
 except ImportError:
-    from threading import get_ident
+    from threading import get_ident as _get_ident
 
-if t.TYPE_CHECKING:
-    from wsgiref.types import WSGIApplication
+
+def get_ident():
+    warnings.warn(
+        "'get_ident' is deprecated and will be removed in Werkzeug"
+        " version 2.1. Use 'greenlet.getcurrent' or"
+        " 'threading.get_ident' for previous behavior.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return _get_ident()
 
 
 class _CannotUseContextVar(Exception):
@@ -50,17 +58,19 @@ class _CannotUseContextVar(Exception):
 except (ImportError, _CannotUseContextVar):
 
     class ContextVar:  # type: ignore
-        """A fake ContextVar for Python3.6 based on the ident function."""
+        """A fake ContextVar based on the previous greenlet/threading
+        ident function. Used on Python 3.6, eventlet, and old versions
+        of gevent.
+        """
 
         def __init__(self, _name):
-            self.ident_func = get_ident
             self.storage = {}
 
         def get(self, default):
-            return self.storage.get(self.ident_func(), default)
+            return self.storage.get(_get_ident(), default)
 
         def set(self, value):
-            self.storage[self.ident_func()] = value
+            self.storage[_get_ident()] = value
 
 
 def release_local(local: t.Union["Local", "LocalStack"]) -> None:
@@ -87,16 +97,16 @@ def release_local(local: t.Union["Local", "LocalStack"]) -> None:
 
 
 class Local:
-    __slots__ = ("_storage", "_ident_func")
+    __slots__ = ("_storage",)
 
     def __init__(self) -> None:
         object.__setattr__(self, "_storage", ContextVar("local_storage"))
-        object.__setattr__(self, "_ident_func", get_ident)
 
     @property
     def __storage__(self):
         warnings.warn(
-            "__storage__ is deprecated",
+            "'__storage__' is deprecated and will be removed in"
+            " Werkzeug version 2.1.",
             DeprecationWarning,
             stacklevel=2,
         )
@@ -104,33 +114,24 @@ def __storage__(self):
 
     @property
     def __ident_func__(self):
-        if not hasattr(self._storage, "ident_func"):
-            raise RuntimeError(
-                "The __ident_func__ should not be used in Python 3.7+ "
-                "as a ContextVar is used."
-            )
-        else:
-            warnings.warn(
-                "__ident_func__ is deprecated and does not work with Python 3.7+",
-                DeprecationWarning,
-                stacklevel=2,
-            )
-            return self._ident_func
+        warnings.warn(
+            "'__ident_func__' is deprecated and will be removed in"
+            " Werkzeug version 2.1. It should not be used in Python"
+            " 3.7+",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return _get_ident
 
     @__ident_func__.setter
     def __ident_func__(self, func):
-        if not hasattr(self._storage, "ident_func"):
-            raise RuntimeError(
-                "The __ident_func__ cannot be changed in Python 3.7+ "
-                "as a ContextVar is used."
-            )
-        else:
-            warnings.warn(
-                "__ident_func__ is deprecated and does not work with Python 3.7+",
-                DeprecationWarning,
-                stacklevel=2,
-            )
-            self._ident_func = func
+        warnings.warn(
+            "'__ident_func__' is deprecated and will be removed in"
+            " Werkzeug version 2.1. Setting it no longer has any"
+            " effect.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
 
     def __iter__(self) -> t.Iterator[t.Tuple[int, t.Any]]:
         return iter(self._storage.get({}).items())
@@ -247,25 +248,26 @@ def top(self) -> t.Any:
 
 class LocalManager:
     """Local objects cannot manage themselves. For that you need a local
-    manager.  You can pass a local manager multiple locals or add them later
-    by appending them to `manager.locals`.  Every time the manager cleans up,
-    it will clean up all the data left in the locals for this context.
+    manager. You can pass a local manager multiple locals or add them
+    later y appending them to `manager.locals`. Every time the manager
+    cleans up, it will clean up all the data left in the locals for this
+    context.
 
-    The `ident_func` parameter can be added to override the default ident
-    function for the wrapped locals.
+    .. versionchanged:: 2.0.0
+        ``ident_func`` is deprecated and will be removed in version 2.1.
 
     .. versionchanged:: 0.6.1
-       Instead of a manager the :func:`release_local` function can be used
-       as well.
+        The :func:`release_local` function can be used instead of a
+        manager.
 
     .. versionchanged:: 0.7
-       `ident_func` was added.
+        The ``ident_func`` parameter was added.
     """
 
     def __init__(
         self,
         locals: t.Optional[t.Iterable[t.Union[Local, LocalStack]]] = None,
-        ident_func: t.Optional[t.Callable[[], int]] = None,
+        ident_func: None = None,
     ) -> None:
         if locals is None:
             self.locals = []
@@ -273,12 +275,34 @@ def __init__(
             self.locals = [locals]
         else:
             self.locals = list(locals)
+
         if ident_func is not None:
-            self.ident_func = ident_func
-            for local in self.locals:
-                object.__setattr__(local, "__ident_func__", ident_func)
-        else:
-            self.ident_func = get_ident
+            warnings.warn(
+                "'ident_func' is deprecated and will be removed in"
+                " Werkzeug version 2.1. Setting it no longer has any"
+                " effect.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+
+    @property
+    def ident_func(self):
+        warnings.warn(
+            "'ident_func' is deprecated and will be removed in Werkzeug"
+            " version 2.1.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return _get_ident
+
+    @ident_func.setter
+    def ident_func(self, func):
+        warnings.warn(
+            "'ident_func' is deprecated and will be removedin Werkzeug"
+            " version 2.1. Setting it no longer has any effect.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
 
     def get_ident(self) -> int:
         """Return the context identifier the local objects use internally for
@@ -286,11 +310,19 @@ def get_ident(self) -> int:
         but use it to link other context local objects (such as SQLAlchemy's
         scoped sessions) to the Werkzeug locals.
 
+        .. deprecated:: 2.0.0
+            Will be removed in version 2.1.
+
         .. versionchanged:: 0.7
            You can pass a different ident function to the local manager that
            will then be propagated to all the locals passed to the
            constructor.
         """
+        warnings.warn(
+            "'get_ident' is deprecated and will be removed in Werkzeug version 2.1.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         return self.ident_func()
 
     def cleanup(self):


Test patch: