Repository: tornadoweb/tornado

Issue:
Type checking problems. "Awaitable" vs "Future"
```
import threading
import tornado.ioloop
import time
from tornado.concurrent import Future

ioloop = tornado.ioloop.IOLoop.current()

def callback(future: Future[int]) -> None:
    print(f"Callback {future.result()} {threading.current_thread().name}")
    ioloop.stop()

def function() -> int:
    print(f"Function {threading.current_thread().name}")
    time.sleep(1)
    return 1

future = ioloop.run_in_executor(None, function)
ioloop.add_future(future, callback)
ioloop.start()
```

It does what's supposed to, runs a callback in the main thread after the the function runs in the executor. And everything is typed. However, using mypy:
```
$ mypy script.py 
script.py:17: error: Argument 1 to "add_future" of "IOLoop" has incompatible type "Awaitable[int]"; expected "Union[asyncio.futures.Future[int], concurrent.futures._base.Future[int]]"
```

I see the type annotations were all added for both functions on the same commit afe7dc588

Is there a reason `run_in_executor` returns `Awaitable[_T]` and not `Future[_T]`?


Discussion:
`run_in_executor` returns `Awaitable` because that's what the corresponding `asyncio` method is typed to return. If I'm following the various threads correctly (starting with https://github.com/python/typeshed/issues/3999), now that https://bugs.python.org/issue40782 has been fixed in cpython, we need a typeshed PR to change the return type of AbstractEventLoop.run_in_executor from Awaitable to Future, and *then* we can make the same change in Tornado. 
Typeshed now has this method returning a Future, so this is a change we could make next time we upgrade mypy/typeshed.

Resolving patch:
diff --git a/tornado/autoreload.py b/tornado/autoreload.py
--- a/tornado/autoreload.py
+++ b/tornado/autoreload.py
@@ -338,9 +338,7 @@ def main() -> None:
         # no longer in sys.modules.  Figure out where it is and watch it.
         loader = pkgutil.get_loader(opts.module)
         if loader is not None and isinstance(loader, importlib.abc.FileLoader):
-            # TODO: fix when we update typeshed
-            watch(loader.get_filename())  # type: ignore
-
+            watch(loader.get_filename())
     if opts.until_success and not exit_status:
         return
     wait()
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -711,7 +711,7 @@ def run_in_executor(
         executor: Optional[concurrent.futures.Executor],
         func: Callable[..., _T],
         *args: Any
-    ) -> Awaitable[_T]:
+    ) -> "Future[_T]":
         """Runs a function in a ``concurrent.futures.Executor``. If
         ``executor`` is ``None``, the IO loop's default executor will be used.
 
diff --git a/tornado/platform/asyncio.py b/tornado/platform/asyncio.py
--- a/tornado/platform/asyncio.py
+++ b/tornado/platform/asyncio.py
@@ -37,7 +37,6 @@
 
 from typing import (
     Any,
-    Awaitable,
     Callable,
     Dict,
     List,
@@ -237,7 +236,7 @@ def run_in_executor(
         executor: Optional[concurrent.futures.Executor],
         func: Callable[..., _T],
         *args: Any,
-    ) -> Awaitable[_T]:
+    ) -> "asyncio.Future[_T]":
         return self.asyncio_loop.run_in_executor(executor, func, *args)
 
     def set_default_executor(self, executor: concurrent.futures.Executor) -> None:


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -22,12 +22,12 @@ jobs:
         name: Install Python
         with:
           # Lint python version must be synced with tox.ini
-          python-version: '3.8'
+          python-version: '3.11'
       - name: Install tox
         run: python -m pip install tox -c requirements.txt
 
       - name: Run test suite
-        run: python -m tox -e py38,lint
+        run: python -m tox -e py311,lint
 
   test_tox:
     name: Run full tests
@@ -57,7 +57,7 @@ jobs:
             # Pypy is a lot slower due to jit warmup costs, so don't run the
             # "full" test config there.
             tox_env: pypy3
-          - python: '3.8'
+          - python: '3.11'
             # Docs python version must be synced with tox.ini
             tox_env: docs