Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-asgiref for openSUSE:Factory checked in at 2021-05-12 19:31:07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-asgiref (Old) and /work/SRC/openSUSE:Factory/.python-asgiref.new.2988 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asgiref" Wed May 12 19:31:07 2021 rev:4 rq:890807 version:3.3.4 Changes: -------- --- /work/SRC/openSUSE:Factory/python-asgiref/python-asgiref.changes 2021-01-18 11:30:28.096340415 +0100 +++ /work/SRC/openSUSE:Factory/.python-asgiref.new.2988/python-asgiref.changes 2021-05-12 19:31:11.139299760 +0200 @@ -1,0 +2,23 @@ +Wed May 5 17:30:59 UTC 2021 - Ben Greiner <c...@bnavigator.de> + +- Update to 3.3.4 + * The async_to_sync type error is now a warning due the + high false negative rate when trying to detect + coroutine-returning callables in Python. +- Release to 3.3.3 + * The sync conversion functions now correctly detect + functools.partial and other wrappers around async + functions on earlier Python releases. +- Release to 3.3.2 + * SyncToAsync now takes an optional "executor" argument if + you want to supply your own executor rather than using + the built-in one. + * async_to_sync and sync_to_async now check their + arguments are functions of the correct type. + * Raising CancelledError inside a SyncToAsync function no + longer stops a future call from functioning. + * ThreadSensitive now provides context hooks/override + options so it can be made to be sensitive in a unit + smaller than threads (e.g. per request) + +------------------------------------------------------------------- Old: ---- asgiref-3.3.1.tar.gz New: ---- asgiref-3.3.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-asgiref.spec ++++++ --- /var/tmp/diff_new_pack.KD3waj/_old 2021-05-12 19:31:12.075295602 +0200 +++ /var/tmp/diff_new_pack.KD3waj/_new 2021-05-12 19:31:12.079295584 +0200 @@ -19,19 +19,22 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-asgiref -Version: 3.3.1 +Version: 3.3.4 Release: 0 Summary: ASGI specs, helper code, and adapters License: BSD-3-Clause URL: https://github.com/django/asgiref/ Source: https://files.pythonhosted.org/packages/source/a/asgiref/asgiref-%{version}.tar.gz -BuildRequires: %{python_module base >= 3.5} -BuildRequires: %{python_module pytest >= 4.3.0} -BuildRequires: %{python_module pytest-asyncio >= 0.10.0} +BuildRequires: %{python_module base >= 3.6} +BuildRequires: %{python_module pytest-asyncio} +BuildRequires: %{python_module pytest} BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module typing_extensions if %python-base < 3.8} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python +%if 0%{python_version_nodots} < 38 +Requires: python-typing_extensions +%endif BuildArch: noarch %python_subpackages ++++++ asgiref-3.3.1.tar.gz -> asgiref-3.3.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/MANIFEST.in new/asgiref-3.3.4/MANIFEST.in --- old/asgiref-3.3.1/MANIFEST.in 2020-05-22 07:41:54.000000000 +0200 +++ new/asgiref-3.3.4/MANIFEST.in 2021-04-05 18:48:44.000000000 +0200 @@ -1,2 +1,3 @@ include LICENSE +include asgiref/py.typed recursive-include tests *.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/PKG-INFO new/asgiref-3.3.4/PKG-INFO --- old/asgiref-3.3.1/PKG-INFO 2020-11-09 16:55:38.710000000 +0100 +++ new/asgiref-3.3.4/PKG-INFO 2021-04-06 20:39:52.360000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: asgiref -Version: 3.3.1 +Version: 3.3.4 Summary: ASGI specs, helper code, and adapters Home-page: https://github.com/django/asgiref/ Author: Django Software Foundation @@ -107,7 +107,7 @@ Dependencies ------------ - ``asgiref`` requires Python 3.5 or higher. + ``asgiref`` requires Python 3.6 or higher. Contributing @@ -148,6 +148,18 @@ sphinx-autobuild . _build/html + Releasing + ''''''''' + + To release, first add details to CHANGELOG.txt and update the version number in ``asgiref/__init__.py``. + + Then, build and push the packages:: + + python -m build + twine upload dist/* + rm -r build/ dist/ + + Implementation Details ---------------------- @@ -220,11 +232,10 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP -Requires-Python: >=3.5 +Requires-Python: >=3.6 Provides-Extra: tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/README.rst new/asgiref-3.3.4/README.rst --- old/asgiref-3.3.1/README.rst 2020-11-09 16:53:04.000000000 +0100 +++ new/asgiref-3.3.4/README.rst 2021-04-05 21:33:58.000000000 +0200 @@ -96,7 +96,7 @@ Dependencies ------------ -``asgiref`` requires Python 3.5 or higher. +``asgiref`` requires Python 3.6 or higher. Contributing @@ -137,6 +137,18 @@ sphinx-autobuild . _build/html +Releasing +''''''''' + +To release, first add details to CHANGELOG.txt and update the version number in ``asgiref/__init__.py``. + +Then, build and push the packages:: + + python -m build + twine upload dist/* + rm -r build/ dist/ + + Implementation Details ---------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref/__init__.py new/asgiref-3.3.4/asgiref/__init__.py --- old/asgiref-3.3.1/asgiref/__init__.py 2020-11-09 16:55:08.000000000 +0100 +++ new/asgiref-3.3.4/asgiref/__init__.py 2021-04-06 20:28:19.000000000 +0200 @@ -1 +1 @@ -__version__ = "3.3.1" +__version__ = "3.3.4" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref/current_thread_executor.py new/asgiref-3.3.4/asgiref/current_thread_executor.py --- old/asgiref-3.3.1/asgiref/current_thread_executor.py 2020-05-22 07:41:54.000000000 +0200 +++ new/asgiref-3.3.4/asgiref/current_thread_executor.py 2021-04-05 18:48:44.000000000 +0200 @@ -1,10 +1,9 @@ import queue import threading -import time from concurrent.futures import Executor, Future -class _WorkItem(object): +class _WorkItem: """ Represents an item needing to be run in the executor. Copied from ThreadPoolExecutor (but it's private, so we're not going to rely on importing it) @@ -51,21 +50,17 @@ raise RuntimeError( "You cannot run CurrentThreadExecutor from a different thread" ) - # Keep getting work items and checking the future + future.add_done_callback(self._work_queue.put) + # Keep getting and running work items until we get the future we're waiting for + # back via the future's done callback. try: while True: # Get a work item and run it - try: - work_item = self._work_queue.get(block=False) - except queue.Empty: - # See if the future is done (we only exit if the work queue is empty) - if future.done(): - return - # Prevent hot-looping on nothing - time.sleep(0.001) - else: - work_item.run() - del work_item + work_item = self._work_queue.get() + if work_item is future: + return + work_item.run() + del work_item finally: self._broken = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref/local.py new/asgiref-3.3.4/asgiref/local.py --- old/asgiref-3.3.1/asgiref/local.py 2020-06-18 20:37:26.000000000 +0200 +++ new/asgiref-3.3.4/asgiref/local.py 2021-04-05 18:48:44.000000000 +0200 @@ -32,13 +32,13 @@ CLEANUP_INTERVAL = 60 # seconds - def __init__(self, thread_critical=False): + def __init__(self, thread_critical: bool = False) -> None: self._thread_critical = thread_critical self._thread_lock = threading.RLock() - self._context_refs = weakref.WeakSet() + self._context_refs: "weakref.WeakSet[object]" = weakref.WeakSet() # Random suffixes stop accidental reuse between different Locals, # though we try to force deletion as well. - self._attr_name = "_asgiref_local_impl_%s_%s" % ( + self._attr_name = "_asgiref_local_impl_{}_{}".format( id(self), "".join(random.choice(string.ascii_letters) for i in range(8)), ) @@ -104,7 +104,7 @@ if key in storage: return storage[key] else: - raise AttributeError("%r object has no attribute %r" % (self, key)) + raise AttributeError(f"{self!r} object has no attribute {key!r}") def __setattr__(self, key, value): if key in ("_context_refs", "_thread_critical", "_thread_lock", "_attr_name"): @@ -119,4 +119,4 @@ if key in storage: del storage[key] else: - raise AttributeError("%r object has no attribute %r" % (self, key)) + raise AttributeError(f"{self!r} object has no attribute {key!r}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref/server.py new/asgiref-3.3.4/asgiref/server.py --- old/asgiref-3.3.1/asgiref/server.py 2020-11-09 16:53:04.000000000 +0100 +++ new/asgiref-3.3.4/asgiref/server.py 2021-04-05 18:48:44.000000000 +0200 @@ -153,5 +153,5 @@ "Exception inside application: %s\n%s%s", exception, "".join(traceback.format_tb(exception.__traceback__)), - " {}".format(exception), + f" {exception}", ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref/sync.py new/asgiref-3.3.4/asgiref/sync.py --- old/asgiref-3.3.1/asgiref/sync.py 2020-11-09 16:53:04.000000000 +0100 +++ new/asgiref-3.3.4/asgiref/sync.py 2021-04-06 20:22:20.000000000 +0200 @@ -1,17 +1,20 @@ -import asyncio import asyncio.coroutines import functools +import inspect import os import sys import threading +import warnings +import weakref from concurrent.futures import Future, ThreadPoolExecutor +from typing import Any, Callable, Dict, Optional, Union from .current_thread_executor import CurrentThreadExecutor from .local import Local -try: - import contextvars # Python 3.7+ only. -except ImportError: +if sys.version_info >= (3, 7): + import contextvars +else: contextvars = None @@ -26,6 +29,74 @@ cvar.set(context.get(cvar)) +def _iscoroutinefunction_or_partial(func: Any) -> bool: + # Python < 3.8 does not correctly determine partially wrapped + # coroutine functions are coroutine functions, hence the need for + # this to exist. Code taken from CPython. + if sys.version_info >= (3, 8): + return asyncio.iscoroutinefunction(func) + else: + while inspect.ismethod(func): + func = func.__func__ + while isinstance(func, functools.partial): + func = func.func + + return asyncio.iscoroutinefunction(func) + + +class ThreadSensitiveContext: + """Async context manager to manage context for thread sensitive mode + + This context manager controls which thread pool executor is used when in + thread sensitive mode. By default, a single thread pool executor is shared + within a process. + + 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. + + Usage: + + >>> import time + >>> async with ThreadSensitiveContext(): + ... await sync_to_async(time.sleep, 1)() + """ + + 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 + + executor = SyncToAsync.context_to_thread_executor.pop(self, None) + if executor: + executor.shutdown() + SyncToAsync.thread_sensitive_context.reset(self.token) + + else: + + async def __aenter__(self): + return self + + async def __aexit__(self, exc, value, tb): + pass + + class AsyncToSync: """ Utility class which turns an awaitable that only works on the thread with @@ -40,13 +111,17 @@ """ # Maps launched Tasks to the threads that launched them (for locals impl) - launch_map = {} + launch_map: "Dict[asyncio.Task[object], threading.Thread]" = {} # Keeps track of which CurrentThreadExecutor to use. This uses an asgiref # Local, not a threadlocal, so that tasks can work out what their parent used. executors = Local() def __init__(self, awaitable, force_new_loop=False): + if not callable(awaitable) or not _iscoroutinefunction_or_partial(awaitable): + # Python does not have very reliable detection of async functions + # (lots of false negatives) so this is just a warning. + warnings.warn("async_to_sync was passed a non-async-marked callable") self.awaitable = awaitable try: self.__self__ = self.awaitable.__self__ @@ -206,11 +281,11 @@ if exc_info[1]: try: raise exc_info[1] - except Exception: + except BaseException: result = await self.awaitable(*args, **kwargs) else: result = await self.awaitable(*args, **kwargs) - except Exception as e: + except BaseException as e: call_result.set_exception(e) else: call_result.set_result(result) @@ -237,6 +312,10 @@ outermost), this will just be the main thread. This is achieved by idling with a CurrentThreadExecutor while AsyncToSync is blocking its sync parent, rather than just blocking. + + If executor is passed in, that will be used instead of the loop's default executor. + In order to pass in an executor, thread_sensitive must be set to False, otherwise + a TypeError will be raised. """ # If they've set ASGI_THREADS, update the default asyncio executor for now @@ -247,7 +326,7 @@ ) # Maps launched threads to the coroutines that spawned them - launch_map = {} + launch_map: "Dict[threading.Thread, asyncio.Task[object]]" = {} # Storage for main event loop references threadlocal = threading.local() @@ -255,13 +334,38 @@ # Single-thread executor for thread-sensitive code single_thread_executor = ThreadPoolExecutor(max_workers=1) - def __init__(self, func, thread_sensitive=True): + # 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 + + # 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. + context_to_thread_executor: "weakref.WeakKeyDictionary[object, ThreadPoolExecutor]" = ( + weakref.WeakKeyDictionary() + ) + + def __init__( + self, + func: Callable[..., Any], + thread_sensitive: bool = True, + executor: Optional["ThreadPoolExecutor"] = None, + ) -> None: + if not callable(func) or _iscoroutinefunction_or_partial(func): + raise TypeError("sync_to_async can only be applied to sync functions.") self.func = func functools.update_wrapper(self, func) self._thread_sensitive = thread_sensitive - self._is_coroutine = asyncio.coroutines._is_coroutine + self._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + if thread_sensitive and executor is not None: + raise TypeError("executor must not be set when thread_sensitive is True") + self._executor = executor try: - self.__self__ = func.__self__ + self.__self__ = func.__self__ # type: ignore except AttributeError: pass @@ -273,11 +377,26 @@ if hasattr(AsyncToSync.executors, "current"): # If we have a parent sync thread above somewhere, use that executor = AsyncToSync.executors.current + elif self.thread_sensitive_context and self.thread_sensitive_context.get( + None + ): + # If we have a way of retrieving the current context, attempt + # to use a per-context thread pool executor + thread_sensitive_context = self.thread_sensitive_context.get() + + if thread_sensitive_context in self.context_to_thread_executor: + # Re-use thread executor in current context + executor = self.context_to_thread_executor[thread_sensitive_context] + else: + # Create new thread executor in current context + executor = ThreadPoolExecutor(max_workers=1) + self.context_to_thread_executor[thread_sensitive_context] = executor else: # Otherwise, we run it in a fixed single thread executor = self.single_thread_executor else: - executor = None # Use default + # 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() @@ -298,7 +417,7 @@ sys.exc_info(), func, *args, - **kwargs + **kwargs, ), ) ret = await asyncio.wait_for(future, timeout=None) @@ -337,7 +456,7 @@ if exc_info[1]: try: raise exc_info[1] - except Exception: + except BaseException: return func(*args, **kwargs) else: return func(*args, **kwargs) @@ -369,7 +488,19 @@ async_to_sync = AsyncToSync -def sync_to_async(func=None, thread_sensitive=True): +def sync_to_async( + func: Optional[Callable[..., Any]] = None, + thread_sensitive: bool = True, + executor: Optional["ThreadPoolExecutor"] = None, +) -> Union[SyncToAsync, Callable[[Callable[..., Any]], SyncToAsync]]: if func is None: - return lambda f: SyncToAsync(f, thread_sensitive=thread_sensitive) - return SyncToAsync(func, thread_sensitive=thread_sensitive) + return lambda f: SyncToAsync( + f, + thread_sensitive=thread_sensitive, + executor=executor, + ) + return SyncToAsync( + func, + thread_sensitive=thread_sensitive, + executor=executor, + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref/timeout.py new/asgiref-3.3.4/asgiref/timeout.py --- old/asgiref-3.3.1/asgiref/timeout.py 2020-05-22 07:41:54.000000000 +0200 +++ new/asgiref-3.3.4/asgiref/timeout.py 2021-04-05 18:48:44.000000000 +0200 @@ -9,9 +9,7 @@ import asyncio import sys from types import TracebackType -from typing import Any, Optional, Type # noqa - -PY_37 = sys.version_info >= (3, 7) +from typing import Any, Optional, Type class timeout: @@ -33,7 +31,7 @@ self, timeout: Optional[float], *, - loop: Optional[asyncio.AbstractEventLoop] = None + loop: Optional[asyncio.AbstractEventLoop] = None, ) -> None: self._timeout = timeout if loop is None: @@ -115,14 +113,15 @@ self._cancelled = True -def current_task(loop: asyncio.AbstractEventLoop) -> "asyncio.Task[Any]": - if PY_37: - task = asyncio.current_task(loop=loop) # type: ignore +def current_task(loop: asyncio.AbstractEventLoop) -> "Optional[asyncio.Task[Any]]": + if sys.version_info >= (3, 7): + task = asyncio.current_task(loop=loop) else: task = asyncio.Task.current_task(loop=loop) if task is None: # this should be removed, tokio must use register_task and family API - if hasattr(loop, "current_task"): - task = loop.current_task() # type: ignore + fn = getattr(loop, "current_task", None) + if fn is not None: + task = fn() return task diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref/typing.py new/asgiref-3.3.4/asgiref/typing.py --- old/asgiref-3.3.1/asgiref/typing.py 1970-01-01 01:00:00.000000000 +0100 +++ new/asgiref-3.3.4/asgiref/typing.py 2021-04-05 18:48:44.000000000 +0200 @@ -0,0 +1,206 @@ +import sys +from typing import Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union + +if sys.version_info >= (3, 8): + from typing import Literal, Protocol, TypedDict +else: + from typing_extensions import Literal, Protocol, TypedDict + + +class ASGIVersions(TypedDict): + spec_version: str + version: Union[Literal["2.0"], Literal["3.0"]] + + +class HTTPScope(TypedDict): + type: Literal["http"] + asgi: ASGIVersions + http_version: str + method: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[Tuple[bytes, bytes]] + client: Optional[Tuple[str, int]] + server: Optional[Tuple[str, Optional[int]]] + extensions: Dict[str, Dict[object, object]] + + +class WebsocketScope(TypedDict): + type: Literal["websocket"] + asgi: ASGIVersions + http_version: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[Tuple[bytes, bytes]] + client: Optional[Tuple[str, int]] + server: Optional[Tuple[str, Optional[int]]] + subprotocols: Iterable[str] + extensions: Dict[str, Dict[object, object]] + + +class LifespanScope(TypedDict): + type: Literal["lifespan"] + asgi: ASGIVersions + + +WWWScope = Union[HTTPScope, WebsocketScope] +Scope = Union[HTTPScope, WebsocketScope, LifespanScope] + + +class HTTPRequestEvent(TypedDict): + type: Literal["http.request"] + body: bytes + more_body: bool + + +class HTTPResponseStartEvent(TypedDict): + type: Literal["http.response.start"] + status: int + headers: Iterable[Tuple[bytes, bytes]] + + +class HTTPResponseBodyEvent(TypedDict): + type: Literal["http.response.body"] + body: bytes + more_body: bool + + +class HTTPServerPushEvent(TypedDict): + type: Literal["http.response.push"] + path: str + headers: Iterable[Tuple[bytes, bytes]] + + +class HTTPDisconnectEvent(TypedDict): + type: Literal["http.disconnect"] + + +class WebsocketConnectEvent(TypedDict): + type: Literal["websocket.connect"] + + +class WebsocketAcceptEvent(TypedDict): + type: Literal["websocket.accept"] + subprotocol: Optional[str] + headers: Iterable[Tuple[bytes, bytes]] + + +class WebsocketReceiveEvent(TypedDict): + type: Literal["websocket.receive"] + bytes: Optional[bytes] + text: Optional[str] + + +class WebsocketSendEvent(TypedDict): + type: Literal["websocket.send"] + bytes: Optional[bytes] + text: Optional[str] + + +class WebsocketResponseStartEvent(TypedDict): + type: Literal["websocket.http.response.start"] + status: int + headers: Iterable[Tuple[bytes, bytes]] + + +class WebsocketResponseBodyEvent(TypedDict): + type: Literal["websocket.http.response.body"] + body: bytes + more_body: bool + + +class WebsocketDisconnectEvent(TypedDict): + type: Literal["websocket.disconnect"] + code: int + + +class WebsocketCloseEvent(TypedDict): + type: Literal["websocket.close"] + code: int + reason: Optional[str] + + +class LifespanStartupEvent(TypedDict): + type: Literal["lifespan.startup"] + + +class LifespanShutdownEvent(TypedDict): + type: Literal["lifespan.shutdown"] + + +class LifespanStartupCompleteEvent(TypedDict): + type: Literal["lifespan.startup.complete"] + + +class LifespanStartupFailedEvent(TypedDict): + type: Literal["lifespan.startup.failed"] + message: str + + +class LifespanShutdownCompleteEvent(TypedDict): + type: Literal["lifespan.shutdown.complete"] + + +class LifespanShutdownFailedEvent(TypedDict): + type: Literal["lifespan.shutdown.failed"] + message: str + + +ASGIReceiveEvent = Union[ + HTTPRequestEvent, + HTTPDisconnectEvent, + WebsocketConnectEvent, + WebsocketReceiveEvent, + WebsocketDisconnectEvent, + LifespanStartupEvent, + LifespanShutdownEvent, +] + + +ASGISendEvent = Union[ + HTTPResponseStartEvent, + HTTPResponseBodyEvent, + HTTPServerPushEvent, + HTTPDisconnectEvent, + WebsocketAcceptEvent, + WebsocketSendEvent, + WebsocketResponseStartEvent, + WebsocketResponseBodyEvent, + WebsocketCloseEvent, + LifespanStartupCompleteEvent, + LifespanStartupFailedEvent, + LifespanShutdownCompleteEvent, + LifespanShutdownFailedEvent, +] + + +ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]] +ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]] + + +class ASGI2Protocol(Protocol): + def __init__(self, scope: Scope) -> None: + ... + + async def __call__( + self, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: + ... + + +ASGI2Application = Type[ASGI2Protocol] +ASGI3Application = Callable[ + [ + Scope, + ASGIReceiveCallable, + ASGISendCallable, + ], + Awaitable[None], +] +ASGIApplication = Union[ASGI2Application, ASGI3Application] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref.egg-info/PKG-INFO new/asgiref-3.3.4/asgiref.egg-info/PKG-INFO --- old/asgiref-3.3.1/asgiref.egg-info/PKG-INFO 2020-11-09 16:55:38.000000000 +0100 +++ new/asgiref-3.3.4/asgiref.egg-info/PKG-INFO 2021-04-06 20:39:52.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: asgiref -Version: 3.3.1 +Version: 3.3.4 Summary: ASGI specs, helper code, and adapters Home-page: https://github.com/django/asgiref/ Author: Django Software Foundation @@ -107,7 +107,7 @@ Dependencies ------------ - ``asgiref`` requires Python 3.5 or higher. + ``asgiref`` requires Python 3.6 or higher. Contributing @@ -148,6 +148,18 @@ sphinx-autobuild . _build/html + Releasing + ''''''''' + + To release, first add details to CHANGELOG.txt and update the version number in ``asgiref/__init__.py``. + + Then, build and push the packages:: + + python -m build + twine upload dist/* + rm -r build/ dist/ + + Implementation Details ---------------------- @@ -220,11 +232,10 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP -Requires-Python: >=3.5 +Requires-Python: >=3.6 Provides-Extra: tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref.egg-info/SOURCES.txt new/asgiref-3.3.4/asgiref.egg-info/SOURCES.txt --- old/asgiref-3.3.1/asgiref.egg-info/SOURCES.txt 2020-11-09 16:55:38.000000000 +0100 +++ new/asgiref-3.3.4/asgiref.egg-info/SOURCES.txt 2021-04-06 20:39:52.000000000 +0200 @@ -7,10 +7,12 @@ asgiref/compatibility.py asgiref/current_thread_executor.py asgiref/local.py +asgiref/py.typed asgiref/server.py asgiref/sync.py asgiref/testing.py asgiref/timeout.py +asgiref/typing.py asgiref/wsgi.py asgiref.egg-info/PKG-INFO asgiref.egg-info/SOURCES.txt diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/asgiref.egg-info/requires.txt new/asgiref-3.3.4/asgiref.egg-info/requires.txt --- old/asgiref-3.3.1/asgiref.egg-info/requires.txt 2020-11-09 16:55:38.000000000 +0100 +++ new/asgiref-3.3.4/asgiref.egg-info/requires.txt 2021-04-06 20:39:52.000000000 +0200 @@ -1,4 +1,8 @@ +[:python_version < "3.8"] +typing_extensions + [tests] pytest pytest-asyncio +mypy>=0.800 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/setup.cfg new/asgiref-3.3.4/setup.cfg --- old/asgiref-3.3.1/setup.cfg 2020-11-09 16:55:38.710000000 +0100 +++ new/asgiref-3.3.4/setup.cfg 2021-04-06 20:39:52.360000000 +0200 @@ -16,7 +16,6 @@ Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -28,15 +27,18 @@ Changelog = https://github.com/django/asgiref/blob/master/CHANGELOG.txt [options] -python_requires = >=3.5 +python_requires = >=3.6 packages = find: include_package_data = true +install_requires = + typing_extensions; python_version < "3.8" zip_safe = false [options.extras_require] tests = pytest pytest-asyncio + mypy>=0.800 [tool:pytest] testpaths = tests @@ -46,6 +48,69 @@ ignore = E123,E128,E266,E402,W503,E731,W601 max-line-length = 119 +[isort] +line_length = 119 + +[mypy] +warn_unused_ignores = True +strict = True + +[mypy-asgiref.current_thread_executor] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-asgiref.local] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-asgiref.sync] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-asgiref.compatibility] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-asgiref.wsgi] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-asgiref.testing] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-asgiref.server] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-test_server] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-test_wsgi] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-test_testing] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-test_sync_contextvars] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-test_sync] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-test_local] +disallow_untyped_defs = False +check_untyped_defs = False + +[mypy-test_compatibility] +disallow_untyped_defs = False +check_untyped_defs = False + [egg_info] tag_build = tag_date = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/setup.py new/asgiref-3.3.4/setup.py --- old/asgiref-3.3.1/setup.py 2020-06-15 18:29:43.000000000 +0200 +++ new/asgiref-3.3.4/setup.py 2021-04-05 18:48:44.000000000 +0200 @@ -1,3 +1,3 @@ -from setuptools import setup +from setuptools import setup # type: ignore[import] -setup(name='asgiref') +setup() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/tests/test_server.py new/asgiref-3.3.4/tests/test_server.py --- old/asgiref-3.3.1/tests/test_server.py 2020-11-09 16:53:04.000000000 +0100 +++ new/asgiref-3.3.4/tests/test_server.py 2021-04-05 18:48:44.000000000 +0200 @@ -2,7 +2,7 @@ def test_stateless_server(): - """StatlessServer can be instantiated with an ASGI 3 application.""" + """StatelessServer can be instantiated with an ASGI 3 application.""" async def app(scope, receive, send): pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/tests/test_sync.py new/asgiref-3.3.4/tests/test_sync.py --- old/asgiref-3.3.1/tests/test_sync.py 2020-11-09 16:53:00.000000000 +0100 +++ new/asgiref-3.3.4/tests/test_sync.py 2021-04-06 20:26:19.000000000 +0200 @@ -1,4 +1,5 @@ import asyncio +import functools import multiprocessing import threading import time @@ -8,7 +9,7 @@ import pytest -from asgiref.sync import async_to_sync, sync_to_async +from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async @pytest.mark.asyncio @@ -45,6 +46,52 @@ loop.set_default_executor(old_executor) +def test_sync_to_async_fail_non_function(): + """ + async_to_sync raises a TypeError when called with a non-function. + """ + with pytest.raises(TypeError) as excinfo: + sync_to_async(1) + + assert excinfo.value.args == ( + "sync_to_async can only be applied to sync functions.", + ) + + +@pytest.mark.asyncio +async def test_sync_to_async_fail_async(): + """ + sync_to_async raises a TypeError when applied to a sync function. + """ + with pytest.raises(TypeError) as excinfo: + + @sync_to_async + async def test_function(): + pass + + assert excinfo.value.args == ( + "sync_to_async can only be applied to sync functions.", + ) + + +@pytest.mark.asyncio +async def test_async_to_sync_fail_partial(): + """ + sync_to_async raises a TypeError when applied to a sync partial. + """ + with pytest.raises(TypeError) as excinfo: + + async def test_function(*args): + pass + + partial_function = functools.partial(test_function, 42) + sync_to_async(partial_function) + + assert excinfo.value.args == ( + "sync_to_async can only be applied to sync functions.", + ) + + @pytest.mark.asyncio async def test_sync_to_async_decorator(): """ @@ -152,6 +199,33 @@ assert result["thread"] == threading.current_thread() +def test_async_to_sync_fail_non_function(): + """ + async_to_sync raises a TypeError when applied to a non-function. + """ + with pytest.warns(UserWarning) as warnings: + async_to_sync(1) + + assert warnings[0].message.args == ( + "async_to_sync was passed a non-async-marked callable", + ) + + +def test_async_to_sync_fail_sync(): + """ + async_to_sync raises a TypeError when applied to a sync function. + """ + with pytest.warns(UserWarning) as warnings: + + @async_to_sync + def test_function(self): + pass + + assert warnings[0].message.args == ( + "async_to_sync was passed a non-async-marked callable", + ) + + def test_async_to_sync(): """ Tests we can call async_to_sync outside of an outer event loop. @@ -246,6 +320,45 @@ assert result["worked"] +def test_async_to_sync_in_except(): + """ + Tests we can call async_to_sync inside an except block without it + re-propagating the exception. + """ + + # Define async function + @async_to_sync + async def test_function(): + return 42 + + # Run inside except + try: + raise ValueError("Boom") + except ValueError: + assert test_function() == 42 + + +def test_async_to_sync_partial(): + """ + Tests we can call async_to_sync on an async partial. + """ + result = {} + + # Define async function + async def inner_async_function(*args): + await asyncio.sleep(0) + result["worked"] = True + return [*args] + + partial_function = functools.partial(inner_async_function, 42) + + # Run it + sync_function = async_to_sync(partial_function) + out = sync_function(84) + assert out == [42, 84] + assert result["worked"] + + def test_async_to_async_method_self_attribute(): """ Tests async_to_async on a method copies __self__. @@ -322,6 +435,55 @@ assert result_1["thread"] == result_2["thread"] +@pytest.mark.asyncio +async def test_thread_sensitive_with_context_matches(): + result_1 = {} + result_2 = {} + + def store_thread(result): + result["thread"] = threading.current_thread() + + store_thread_async = sync_to_async(store_thread) + + async def fn(): + async with ThreadSensitiveContext(): + # Run it (in supposed parallel!) + await asyncio.wait( + [store_thread_async(result_1), store_thread_async(result_2)] + ) + + await fn() + + # They should not have run in the main thread, and on the same threads + assert result_1["thread"] != threading.current_thread() + assert result_1["thread"] == result_2["thread"] + + +@pytest.mark.asyncio +async def test_thread_sensitive_nested_context(): + result_1 = {} + result_2 = {} + + @sync_to_async + def store_thread(result): + result["thread"] = threading.current_thread() + + async with ThreadSensitiveContext(): + await store_thread(result_1) + async with ThreadSensitiveContext(): + await store_thread(result_2) + + # They should not have run in the main thread, and on the same threads + assert result_1["thread"] != threading.current_thread() + assert result_1["thread"] == result_2["thread"] + + +@pytest.mark.asyncio +async def test_thread_sensitive_context_without_sync_work(): + async with ThreadSensitiveContext(): + pass + + def test_thread_sensitive_double_nested_sync(): """ Tests that thread_sensitive SyncToAsync nests inside itself where the @@ -461,3 +623,40 @@ return test_queue.get(True, 1) assert await sync_to_async(fork_first)() == 42 + + +@pytest.mark.asyncio +async def test_sync_to_async_uses_executor(): + """ + Tests that SyncToAsync uses the passed in executor correctly. + """ + + class CustomExecutor: + def __init__(self): + self.executor = ThreadPoolExecutor(max_workers=1) + self.times_submit_called = 0 + + def submit(self, callable_, *args, **kwargs): + self.times_submit_called += 1 + return self.executor.submit(callable_, *args, **kwargs) + + expected_result = "expected_result" + + def sync_func(): + return expected_result + + custom_executor = CustomExecutor() + async_function = sync_to_async( + sync_func, thread_sensitive=False, executor=custom_executor + ) + actual_result = await async_function() + assert actual_result == expected_result + assert custom_executor.times_submit_called == 1 + + pytest.raises( + TypeError, + sync_to_async, + sync_func, + thread_sensitive=True, + executor=custom_executor, + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/tests/test_sync_contextvars.py new/asgiref-3.3.4/tests/test_sync_contextvars.py --- old/asgiref-3.3.1/tests/test_sync_contextvars.py 2020-06-15 23:32:05.000000000 +0200 +++ new/asgiref-3.3.4/tests/test_sync_contextvars.py 2021-04-05 18:48:44.000000000 +0200 @@ -1,9 +1,10 @@ import asyncio +import threading import time import pytest -from asgiref.sync import async_to_sync, sync_to_async +from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async contextvars = pytest.importorskip("contextvars") @@ -11,6 +12,27 @@ @pytest.mark.asyncio +async def test_thread_sensitive_with_context_different(): + result_1 = {} + result_2 = {} + + @sync_to_async + def store_thread(result): + result["thread"] = threading.current_thread() + + async def fn(result): + async with ThreadSensitiveContext(): + await store_thread(result) + + # Run it (in true parallel!) + await asyncio.wait([fn(result_1), fn(result_2)]) + + # They should not have run in the main thread, and on different threads + assert result_1["thread"] != threading.current_thread() + assert result_1["thread"] != result_2["thread"] + + +@pytest.mark.asyncio async def test_sync_to_async_contextvars(): """ Tests to make sure that contextvars from the calling context are diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.3.1/tests/test_wsgi.py new/asgiref-3.3.4/tests/test_wsgi.py --- old/asgiref-3.3.1/tests/test_wsgi.py 2020-10-09 18:25:45.000000000 +0200 +++ new/asgiref-3.3.4/tests/test_wsgi.py 2021-04-05 18:48:44.000000000 +0200 @@ -59,8 +59,8 @@ """ # Define WSGI app def wsgi_application(environ, start_response): - assert environ["SCRIPT_NAME"] == "/??????".encode("utf8").decode("latin-1") - assert environ["PATH_INFO"] == "/??????".encode("utf8").decode("latin-1") + assert environ["SCRIPT_NAME"] == "/??????".encode().decode("latin-1") + assert environ["PATH_INFO"] == "/??????".encode().decode("latin-1") start_response("200 OK", []) yield b""