Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop Python 3.6 #307

Merged
merged 3 commits into from Dec 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Expand Up @@ -14,7 +14,6 @@ jobs:
fail-fast: false
matrix:
python-version:
- 3.6
- 3.7
- 3.8
- 3.9
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Expand Up @@ -3,13 +3,13 @@ repos:
rev: v2.9.0
hooks:
- id: pyupgrade
args: ["--py36-plus"]
args: ["--py37-plus"]

- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
args: ["--target-version=py36"]
args: ["--target-version=py37"]

- repo: https://github.com/pycqa/isort
rev: 5.7.0
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -96,7 +96,7 @@ file handles for incoming POST bodies).
Dependencies
------------

``asgiref`` requires Python 3.6 or higher.
``asgiref`` requires Python 3.7 or higher.


Contributing
Expand Down
61 changes: 0 additions & 61 deletions asgiref/_pep562.py

This file was deleted.

16 changes: 0 additions & 16 deletions asgiref/compatibility.py
@@ -1,6 +1,5 @@
import asyncio
import inspect
import sys


def is_double_callable(application):
Expand Down Expand Up @@ -46,18 +45,3 @@ def guarantee_single_callable(application):
if is_double_callable(application):
application = double_to_single_callable(application)
return application


if sys.version_info >= (3, 7):
# these were introduced in 3.7
get_running_loop = asyncio.get_running_loop
run_future = asyncio.run
create_task = asyncio.create_task
current_task = asyncio.current_task
else:
# marked as deprecated in 3.10, did not exist before 3.7
get_running_loop = asyncio.get_event_loop
run_future = asyncio.ensure_future
# does nothing, this is fine for <3.7
create_task = lambda task: task
current_task = asyncio.Task.current_task
112 changes: 38 additions & 74 deletions asgiref/sync.py
@@ -1,4 +1,5 @@
import asyncio.coroutines
import contextvars
import functools
import inspect
import os
Expand All @@ -9,15 +10,9 @@
from concurrent.futures import Future, ThreadPoolExecutor
from typing import Any, Callable, Dict, Optional, overload

from .compatibility import current_task, get_running_loop
from .current_thread_executor import CurrentThreadExecutor
from .local import Local

if sys.version_info >= (3, 7):
import contextvars
else:
contextvars = None


def _restore_context(context):
# Check for changes in contextvars, and set them to the current
Expand Down Expand Up @@ -55,8 +50,6 @@ class ThreadSensitiveContext:
In Python 3.7+, the ThreadSensitiveContext() context manager may be used to
specify a thread pool per context.

In Python 3.6, usage of this context manager has no effect.

This context manager is re-entrant, so only the outer-most call to
ThreadSensitiveContext will set the context.

Expand All @@ -70,32 +63,22 @@ class ThreadSensitiveContext:
def __init__(self):
self.token = None

if contextvars:

async def __aenter__(self):
try:
SyncToAsync.thread_sensitive_context.get()
except LookupError:
self.token = SyncToAsync.thread_sensitive_context.set(self)

return self

async def __aexit__(self, exc, value, tb):
if not self.token:
return
async def __aenter__(self):
try:
SyncToAsync.thread_sensitive_context.get()
except LookupError:
self.token = SyncToAsync.thread_sensitive_context.set(self)

executor = SyncToAsync.context_to_thread_executor.pop(self, None)
if executor:
executor.shutdown()
SyncToAsync.thread_sensitive_context.reset(self.token)
return self

else:
async def __aexit__(self, exc, value, tb):
if not self.token:
return

async def __aenter__(self):
return self

async def __aexit__(self, exc, value, tb):
pass
executor = SyncToAsync.context_to_thread_executor.pop(self, None)
if executor:
executor.shutdown()
SyncToAsync.thread_sensitive_context.reset(self.token)


class AsyncToSync:
Expand Down Expand Up @@ -135,7 +118,7 @@ def __init__(self, awaitable, force_new_loop=False):
self.main_event_loop = None
else:
try:
self.main_event_loop = get_running_loop()
self.main_event_loop = asyncio.get_running_loop()
except RuntimeError:
# There's no event loop in this thread. Look for the threadlocal if
# we're inside SyncToAsync
Expand All @@ -154,7 +137,7 @@ def __init__(self, awaitable, force_new_loop=False):
def __call__(self, *args, **kwargs):
# You can't call AsyncToSync from a thread with a running event loop
try:
event_loop = get_running_loop()
event_loop = asyncio.get_running_loop()
except RuntimeError:
pass
else:
Expand All @@ -164,12 +147,9 @@ def __call__(self, *args, **kwargs):
"just await the async function directly."
)

if contextvars is not None:
# Wrapping context in list so it can be reassigned from within
# `main_wrap`.
context = [contextvars.copy_context()]
else:
context = None
# Wrapping context in list so it can be reassigned from within
# `main_wrap`.
context = [contextvars.copy_context()]

# Make a future for the return information
call_result = Future()
Expand Down Expand Up @@ -218,8 +198,7 @@ def __call__(self, *args, **kwargs):
del self.executors.current
if old_current_executor:
self.executors.current = old_current_executor
if contextvars is not None:
_restore_context(context[0])
_restore_context(context[0])

# Wait for results from the future.
return call_result.result()
Expand All @@ -235,10 +214,7 @@ def _run_event_loop(self, loop, coro):
try:
# mimic asyncio.run() behavior
# cancel unexhausted async generators
if sys.version_info >= (3, 7, 0):
tasks = asyncio.all_tasks(loop)
else:
tasks = asyncio.Task.all_tasks(loop)
tasks = asyncio.all_tasks(loop)
for task in tasks:
task.cancel()

Expand Down Expand Up @@ -299,8 +275,7 @@ async def main_wrap(
finally:
del self.launch_map[current_task]

if context is not None:
context[0] = contextvars.copy_context()
context[0] = contextvars.copy_context()


class SyncToAsync:
Expand Down Expand Up @@ -345,21 +320,15 @@ class SyncToAsync:

# Maintain a contextvar for the current execution context. Optionally used
# for thread sensitive mode.
if sys.version_info >= (3, 7):
thread_sensitive_context: "contextvars.ContextVar[str]" = (
contextvars.ContextVar("thread_sensitive_context")
)
else:
thread_sensitive_context: None = None
thread_sensitive_context: "contextvars.ContextVar[str]" = contextvars.ContextVar(
"thread_sensitive_context"
)

# Contextvar that is used to detect if the single thread executor
# would be awaited on while already being used in the same context
if sys.version_info >= (3, 7):
deadlock_context: "contextvars.ContextVar[bool]" = contextvars.ContextVar(
"deadlock_context"
)
else:
deadlock_context: None = None
deadlock_context: "contextvars.ContextVar[bool]" = contextvars.ContextVar(
"deadlock_context"
)

# Maintaining a weak reference to the context ensures that thread pools are
# erased once the context goes out of scope. This terminates the thread pool.
Expand Down Expand Up @@ -388,7 +357,7 @@ def __init__(
pass

async def __call__(self, *args, **kwargs):
loop = get_running_loop()
loop = asyncio.get_running_loop()

# Work out what thread to run the code in
if self._thread_sensitive:
Expand Down Expand Up @@ -422,14 +391,11 @@ async def __call__(self, *args, **kwargs):
# Use the passed in executor, or the loop's default if it is None
executor = self._executor

if contextvars is not None:
context = contextvars.copy_context()
child = functools.partial(self.func, *args, **kwargs)
func = context.run
args = (child,)
kwargs = {}
else:
func = self.func
context = contextvars.copy_context()
child = functools.partial(self.func, *args, **kwargs)
func = context.run
args = (child,)
kwargs = {}

try:
# Run the code in the right thread
Expand All @@ -448,8 +414,7 @@ async def __call__(self, *args, **kwargs):
ret = await asyncio.wait_for(future, timeout=None)

finally:
if contextvars is not None:
_restore_context(context)
_restore_context(context)
if self.deadlock_context:
self.deadlock_context.set(False)

Expand Down Expand Up @@ -497,12 +462,11 @@ def thread_handler(self, loop, source_task, exc_info, func, *args, **kwargs):
@staticmethod
def get_current_task():
"""
Cross-version implementation of asyncio.current_task()

Returns None if there is no task.
Implementation of asyncio.current_task()
that returns None if there is no task.
"""
try:
return current_task()
return asyncio.current_task()
except RuntimeError:
return None

Expand Down
15 changes: 1 addition & 14 deletions asgiref/timeout.py
Expand Up @@ -10,8 +10,6 @@
from types import TracebackType
from typing import Any, Optional, Type

from .compatibility import current_task as asyncio_current_task


class timeout:
"""timeout context manager.
Expand Down Expand Up @@ -83,7 +81,7 @@ def _do_enter(self) -> "timeout":
if self._timeout is None:
return self

self._task = current_task(self._loop)
self._task = asyncio.current_task(self._loop)
if self._task is None:
raise RuntimeError(
"Timeout context manager should be used " "inside a task"
Expand Down Expand Up @@ -112,14 +110,3 @@ def _cancel_task(self) -> None:
if self._task is not None:
self._task.cancel()
self._cancelled = True


def current_task(loop: asyncio.AbstractEventLoop) -> "Optional[asyncio.Task[Any]]":
task = asyncio_current_task(loop=loop)
if task is None:
# this should be removed, tokio must use register_task and family API
fn = getattr(loop, "current_task", None)
if fn is not None:
task = fn()

return task
5 changes: 0 additions & 5 deletions asgiref/typing.py
Expand Up @@ -13,8 +13,6 @@
Union,
)

from asgiref._pep562 import pep562

if sys.version_info >= (3, 8):
from typing import Literal, Protocol, TypedDict
else:
Expand Down Expand Up @@ -282,6 +280,3 @@ def __getattr__(name: str) -> Any:

def __dir__() -> List[str]:
return sorted(list(__all__) + list(__deprecated__.keys()))


pep562(__name__)