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 2022-06-06 11:10:34 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-asgiref (Old) and /work/SRC/openSUSE:Factory/.python-asgiref.new.1548 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asgiref" Mon Jun 6 11:10:34 2022 rev:7 rq:980784 version:3.5.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-asgiref/python-asgiref.changes 2021-09-04 22:32:11.719900300 +0200 +++ /work/SRC/openSUSE:Factory/.python-asgiref.new.1548/python-asgiref.changes 2022-06-06 11:10:43.195324766 +0200 @@ -1,0 +2,20 @@ +Sat Jun 4 15:28:14 UTC 2022 - Dirk M??ller <[email protected]> + +- update to 3.5.2: + * Fix async-to-async typo + * Add tests for sync_to_async + * Improved docs - Starlette supports WebSockets + * Use get_event_loop in class-level code + * Changed how StatelessServer handles event loops + * Fixed pytest_asyncio deprecation warning. + * Drop python 3.6, add python 3.10 + * Fix allowed values for spec_version + * Rewrote multiprocessing test to use no local functions + * Fixed a typographical error + * Remove SOCK_NONBLOCK from socket creation on tests + * Preserve CurrentThreadExecutor across create_task + * Don't warn 'non-async-marked callable' for async callable instance + * Disallow async callable class instances as callable + * Fix root_path in WebSocket Connection Scope + +------------------------------------------------------------------- Old: ---- asgiref-3.4.1.tar.gz New: ---- asgiref-3.5.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-asgiref.spec ++++++ --- /var/tmp/diff_new_pack.hwqtOf/_old 2022-06-06 11:10:43.775325605 +0200 +++ /var/tmp/diff_new_pack.hwqtOf/_new 2022-06-06 11:10:43.783325617 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-asgiref # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2022 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-asgiref -Version: 3.4.1 +Version: 3.5.2 Release: 0 Summary: ASGI specs, helper code, and adapters License: BSD-3-Clause ++++++ asgiref-3.4.1.tar.gz -> asgiref-3.5.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/PKG-INFO new/asgiref-3.5.2/PKG-INFO --- old/asgiref-3.4.1/PKG-INFO 2021-07-01 18:17:25.970000000 +0200 +++ new/asgiref-3.5.2/PKG-INFO 2022-05-16 22:39:02.462978600 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: asgiref -Version: 3.4.1 +Version: 3.5.2 Summary: ASGI specs, helper code, and adapters Home-page: https://github.com/django/asgiref/ Author: Django Software Foundation @@ -9,7 +9,6 @@ Project-URL: Documentation, https://asgi.readthedocs.io/ Project-URL: Further Documentation, https://docs.djangoproject.com/en/stable/topics/async/#async-adapter-functions Project-URL: Changelog, https://github.com/django/asgiref/blob/master/CHANGELOG.txt -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers @@ -18,12 +17,12 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -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: Programming Language :: Python :: 3.10 Classifier: Topic :: Internet :: WWW/HTTP -Requires-Python: >=3.6 +Requires-Python: >=3.7 Provides-Extra: tests License-File: LICENSE @@ -125,7 +124,7 @@ Dependencies ------------ -``asgiref`` requires Python 3.6 or higher. +``asgiref`` requires Python 3.7 or higher. Contributing @@ -240,5 +239,3 @@ This repository is part of the Channels project. For the shepherd and maintenance team, please see the `main Channels readme <https://github.com/django/channels/blob/master/README.rst>`_. - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/README.rst new/asgiref-3.5.2/README.rst --- old/asgiref-3.4.1/README.rst 2021-06-27 22:11:39.000000000 +0200 +++ new/asgiref-3.5.2/README.rst 2022-01-22 17:43:01.000000000 +0100 @@ -96,7 +96,7 @@ Dependencies ------------ -``asgiref`` requires Python 3.6 or higher. +``asgiref`` requires Python 3.7 or higher. Contributing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref/__init__.py new/asgiref-3.5.2/asgiref/__init__.py --- old/asgiref-3.4.1/asgiref/__init__.py 2021-07-01 18:16:27.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/__init__.py 2022-05-16 22:29:01.000000000 +0200 @@ -1 +1 @@ -__version__ = "3.4.1" +__version__ = "3.5.2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref/_pep562.py new/asgiref-3.5.2/asgiref/_pep562.py --- old/asgiref-3.4.1/asgiref/_pep562.py 2021-06-27 22:22:20.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/_pep562.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,61 +0,0 @@ -""" -Backport of PEP 562. -https://pypi.org/search/?q=pep562 -Licensed under MIT -Copyright (c) 2018 Isaac Muse <[email protected]> -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions -of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. -""" -import sys -from typing import Any, Callable, List, Optional - - -class Pep562: - """ - Backport of PEP 562 <https://pypi.org/search/?q=pep562>. - Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`. - The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed. - """ - - def __init__(self, name: str) -> None: - """Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7.""" - - self._module = sys.modules[name] - self._get_attr = getattr(self._module, "__getattr__", None) - self._get_dir: Optional[Callable[..., List[str]]] = getattr( - self._module, "__dir__", None - ) - sys.modules[name] = self # type: ignore[assignment] - - def __dir__(self) -> List[str]: - """Return the overridden `dir` if one was provided, else apply `dir` to the module.""" - - return self._get_dir() if self._get_dir else dir(self._module) - - def __getattr__(self, name: str) -> Any: - """ - Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present. - """ - - try: - return getattr(self._module, name) - except AttributeError: - if self._get_attr: - return self._get_attr(name) - raise - - -def pep562(module_name: str) -> None: - """Helper function to apply PEP 562.""" - - if sys.version_info < (3, 7): - Pep562(module_name) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref/compatibility.py new/asgiref-3.5.2/asgiref/compatibility.py --- old/asgiref-3.4.1/asgiref/compatibility.py 2021-06-27 22:11:39.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/compatibility.py 2022-01-22 17:43:01.000000000 +0100 @@ -1,6 +1,5 @@ import asyncio import inspect -import sys def is_double_callable(application): @@ -46,16 +45,3 @@ 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 -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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref/local.py new/asgiref-3.5.2/asgiref/local.py --- old/asgiref-3.4.1/asgiref/local.py 2021-04-05 18:48:44.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/local.py 2021-08-18 16:20:12.000000000 +0200 @@ -30,8 +30,6 @@ 3.7 only, we can then reimplement the storage more nicely. """ - CLEANUP_INTERVAL = 60 # seconds - def __init__(self, thread_critical: bool = False) -> None: self._thread_critical = thread_critical self._thread_lock = threading.RLock() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref/server.py new/asgiref-3.5.2/asgiref/server.py --- old/asgiref-3.4.1/asgiref/server.py 2021-06-27 22:11:39.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/server.py 2021-11-22 18:42:08.000000000 +0100 @@ -3,7 +3,7 @@ import time import traceback -from .compatibility import get_running_loop, guarantee_single_callable, run_future +from .compatibility import guarantee_single_callable logger = logging.getLogger(__name__) @@ -56,7 +56,7 @@ """ Runs the asyncio event loop with our handler loop. """ - event_loop = get_running_loop() + event_loop = asyncio.get_event_loop() asyncio.ensure_future(self.application_checker()) try: event_loop.run_until_complete(self.handle()) @@ -88,7 +88,7 @@ input_queue = asyncio.Queue() application_instance = guarantee_single_callable(self.application) # Run it, and stash the future for later checking - future = run_future( + future = asyncio.ensure_future( application_instance( scope=scope, receive=input_queue.get, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref/sync.py new/asgiref-3.5.2/asgiref/sync.py --- old/asgiref-3.4.1/asgiref/sync.py 2021-06-30 21:02:12.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/sync.py 2022-05-16 22:28:11.000000000 +0200 @@ -1,4 +1,6 @@ +import asyncio import asyncio.coroutines +import contextvars import functools import inspect import os @@ -9,15 +11,9 @@ from concurrent.futures import Future, ThreadPoolExecutor from typing import Any, Callable, Dict, Optional, overload -from .compatibility import 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 @@ -55,8 +51,6 @@ 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. @@ -70,32 +64,22 @@ 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): + try: + SyncToAsync.thread_sensitive_context.get() + except LookupError: + self.token = SyncToAsync.thread_sensitive_context.set(self) - async def __aenter__(self): - return self + return self - async def __aexit__(self, exc, value, tb): - pass + 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) class AsyncToSync: @@ -118,11 +102,22 @@ # Local, not a threadlocal, so that tasks can work out what their parent used. executors = Local() + # When we can't find a CurrentThreadExecutor from the context, such as + # inside create_task, we'll look it up here from the running event loop. + loop_thread_executors: "Dict[asyncio.AbstractEventLoop, CurrentThreadExecutor]" = {} + def __init__(self, awaitable, force_new_loop=False): - if not callable(awaitable) or not _iscoroutinefunction_or_partial(awaitable): + if not callable(awaitable) or ( + not _iscoroutinefunction_or_partial(awaitable) + and not _iscoroutinefunction_or_partial( + getattr(awaitable, "__call__", 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") + warnings.warn( + "async_to_sync was passed a non-async-marked callable", stacklevel=2 + ) self.awaitable = awaitable try: self.__self__ = self.awaitable.__self__ @@ -133,7 +128,7 @@ 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 @@ -152,7 +147,7 @@ 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: @@ -162,12 +157,9 @@ "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() @@ -182,6 +174,7 @@ old_current_executor = None current_executor = CurrentThreadExecutor() self.executors.current = current_executor + loop = None # Use call_soon_threadsafe to schedule a synchronous callback on the # main event loop's thread if it's there, otherwise make a new loop # in this thread. @@ -193,6 +186,7 @@ if not (self.main_event_loop and self.main_event_loop.is_running()): # Make our own event loop - in a new thread - and run inside that. loop = asyncio.new_event_loop() + self.loop_thread_executors[loop] = current_executor loop_executor = ThreadPoolExecutor(max_workers=1) loop_future = loop_executor.submit( self._run_event_loop, loop, awaitable @@ -212,12 +206,13 @@ current_executor.run_until_future(call_result) finally: # Clean up any executor we were running + if loop is not None: + del self.loop_thread_executors[loop] if hasattr(self.executors, "current"): 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() @@ -233,10 +228,7 @@ 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() @@ -297,8 +289,7 @@ finally: del self.launch_map[current_task] - if context is not None: - context[0] = contextvars.copy_context() + context[0] = contextvars.copy_context() class SyncToAsync: @@ -325,7 +316,9 @@ # If they've set ASGI_THREADS, update the default asyncio executor for now if "ASGI_THREADS" in os.environ: - loop = get_running_loop() + # We use get_event_loop here - not get_running_loop - as this will + # be run at import time, and we want to update the main thread's loop. + loop = asyncio.get_event_loop() loop.set_default_executor( ThreadPoolExecutor(max_workers=int(os.environ["ASGI_THREADS"])) ) @@ -341,21 +334,15 @@ # 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. @@ -369,7 +356,11 @@ thread_sensitive: bool = True, executor: Optional["ThreadPoolExecutor"] = None, ) -> None: - if not callable(func) or _iscoroutinefunction_or_partial(func): + if ( + not callable(func) + or _iscoroutinefunction_or_partial(func) + or _iscoroutinefunction_or_partial(getattr(func, "__call__", func)) + ): raise TypeError("sync_to_async can only be applied to sync functions.") self.func = func functools.update_wrapper(self, func) @@ -384,7 +375,7 @@ 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: @@ -405,6 +396,9 @@ # Create new thread executor in current context executor = ThreadPoolExecutor(max_workers=1) self.context_to_thread_executor[thread_sensitive_context] = executor + elif loop in AsyncToSync.loop_thread_executors: + # Re-use thread executor for running loop + executor = AsyncToSync.loop_thread_executors[loop] elif self.deadlock_context and self.deadlock_context.get(False): raise RuntimeError( "Single thread executor already being used, would deadlock" @@ -418,14 +412,11 @@ # 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 @@ -444,8 +435,7 @@ 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) @@ -493,17 +483,11 @@ @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: - if hasattr(asyncio, "current_task"): - # Python 3.7 and up - return asyncio.current_task() - else: - # Python 3.6 - return asyncio.Task.current_task() + return asyncio.current_task() except RuntimeError: return None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref/timeout.py new/asgiref-3.5.2/asgiref/timeout.py --- old/asgiref-3.4.1/asgiref/timeout.py 2021-04-05 18:48:44.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/timeout.py 2022-01-22 17:43:01.000000000 +0100 @@ -7,7 +7,6 @@ import asyncio -import sys from types import TracebackType from typing import Any, Optional, Type @@ -82,7 +81,7 @@ 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" @@ -111,17 +110,3 @@ if self._task is not None: self._task.cancel() self._cancelled = True - - -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 - 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.4.1/asgiref/typing.py new/asgiref-3.5.2/asgiref/typing.py --- old/asgiref-3.4.1/asgiref/typing.py 2021-06-27 22:24:56.000000000 +0200 +++ new/asgiref-3.5.2/asgiref/typing.py 2022-01-22 17:53:53.000000000 +0100 @@ -1,19 +1,5 @@ import sys -import warnings -from typing import ( - Any, - Awaitable, - Callable, - Dict, - Iterable, - List, - Optional, - Tuple, - Type, - Union, -) - -from asgiref._pep562 import pep562 +from typing import Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union if sys.version_info >= (3, 8): from typing import Literal, Protocol, TypedDict @@ -254,34 +240,3 @@ Awaitable[None], ] ASGIApplication = Union[ASGI2Application, ASGI3Application] - -__deprecated__ = { - "WebsocketConnectEvent": WebSocketConnectEvent, - "WebsocketAcceptEvent": WebSocketAcceptEvent, - "WebsocketReceiveEvent": WebSocketReceiveEvent, - "WebsocketSendEvent": WebSocketSendEvent, - "WebsocketResponseStartEvent": WebSocketResponseStartEvent, - "WebsocketResponseBodyEvent": WebSocketResponseBodyEvent, - "WebsocketDisconnectEvent": WebSocketDisconnectEvent, - "WebsocketCloseEvent": WebSocketCloseEvent, -} - - -def __getattr__(name: str) -> Any: - deprecated = __deprecated__.get(name) - if deprecated: - stacklevel = 3 if sys.version_info >= (3, 7) else 4 - warnings.warn( - f"'{name}' is deprecated. Use '{deprecated.__name__}' instead.", - category=DeprecationWarning, - stacklevel=stacklevel, - ) - return deprecated - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -def __dir__() -> List[str]: - return sorted(list(__all__) + list(__deprecated__.keys())) - - -pep562(__name__) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref.egg-info/PKG-INFO new/asgiref-3.5.2/asgiref.egg-info/PKG-INFO --- old/asgiref-3.4.1/asgiref.egg-info/PKG-INFO 2021-07-01 18:17:25.000000000 +0200 +++ new/asgiref-3.5.2/asgiref.egg-info/PKG-INFO 2022-05-16 22:39:02.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: asgiref -Version: 3.4.1 +Version: 3.5.2 Summary: ASGI specs, helper code, and adapters Home-page: https://github.com/django/asgiref/ Author: Django Software Foundation @@ -9,7 +9,6 @@ Project-URL: Documentation, https://asgi.readthedocs.io/ Project-URL: Further Documentation, https://docs.djangoproject.com/en/stable/topics/async/#async-adapter-functions Project-URL: Changelog, https://github.com/django/asgiref/blob/master/CHANGELOG.txt -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers @@ -18,12 +17,12 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -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: Programming Language :: Python :: 3.10 Classifier: Topic :: Internet :: WWW/HTTP -Requires-Python: >=3.6 +Requires-Python: >=3.7 Provides-Extra: tests License-File: LICENSE @@ -125,7 +124,7 @@ Dependencies ------------ -``asgiref`` requires Python 3.6 or higher. +``asgiref`` requires Python 3.7 or higher. Contributing @@ -240,5 +239,3 @@ This repository is part of the Channels project. For the shepherd and maintenance team, please see the `main Channels readme <https://github.com/django/channels/blob/master/README.rst>`_. - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/asgiref.egg-info/SOURCES.txt new/asgiref-3.5.2/asgiref.egg-info/SOURCES.txt --- old/asgiref-3.4.1/asgiref.egg-info/SOURCES.txt 2021-07-01 18:17:25.000000000 +0200 +++ new/asgiref-3.5.2/asgiref.egg-info/SOURCES.txt 2022-05-16 22:39:02.000000000 +0200 @@ -4,7 +4,6 @@ setup.cfg setup.py asgiref/__init__.py -asgiref/_pep562.py asgiref/compatibility.py asgiref/current_thread_executor.py asgiref/local.py @@ -22,7 +21,6 @@ asgiref.egg-info/requires.txt asgiref.egg-info/top_level.txt tests/test_compatibility.py -tests/test_deprecated_types.py tests/test_local.py tests/test_server.py tests/test_sync.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/setup.cfg new/asgiref-3.5.2/setup.cfg --- old/asgiref-3.4.1/setup.cfg 2021-07-01 18:17:25.980000000 +0200 +++ new/asgiref-3.5.2/setup.cfg 2022-05-16 22:39:02.462978600 +0200 @@ -16,10 +16,10 @@ Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Topic :: Internet :: WWW/HTTP project_urls = Documentation = https://asgi.readthedocs.io/ @@ -27,7 +27,7 @@ Changelog = https://github.com/django/asgiref/blob/master/CHANGELOG.txt [options] -python_requires = >=3.6 +python_requires = >=3.7 packages = find: include_package_data = true install_requires = @@ -42,6 +42,7 @@ [tool:pytest] testpaths = tests +asyncio_mode = strict [flake8] exclude = venv/*,tox/*,specs/* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/tests/test_deprecated_types.py new/asgiref-3.5.2/tests/test_deprecated_types.py --- old/asgiref-3.4.1/tests/test_deprecated_types.py 2021-06-27 22:22:20.000000000 +0200 +++ new/asgiref-3.5.2/tests/test_deprecated_types.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,20 +0,0 @@ -import importlib - -import pytest - -from asgiref import typing - - [email protected]("deprecated_type", typing.__deprecated__.keys()) -def test_deprecated_types(deprecated_type: str) -> None: - with pytest.warns(DeprecationWarning) as record: - getattr(importlib.import_module("asgiref.typing"), deprecated_type) - assert len(record) == 1 - assert deprecated_type in str(record.list[0]) - - [email protected]("available_type", typing.__all__) -def test_available_types(available_type: str) -> None: - with pytest.warns(None) as record: - getattr(importlib.import_module("asgiref.typing"), available_type) - assert len(record) == 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/tests/test_server.py new/asgiref-3.5.2/tests/test_server.py --- old/asgiref-3.4.1/tests/test_server.py 2021-04-05 18:48:44.000000000 +0200 +++ new/asgiref-3.5.2/tests/test_server.py 2022-05-16 22:28:11.000000000 +0200 @@ -1,11 +1,153 @@ +import asyncio +import socket as sock +from functools import partial + +import pytest + from asgiref.server import StatelessServer -def test_stateless_server(): - """StatelessServer can be instantiated with an ASGI 3 application.""" +async def sock_recvfrom(sock, n): + while True: + try: + return sock.recvfrom(n) + except BlockingIOError: + await asyncio.sleep(0) + + +class Server(StatelessServer): + def __init__(self, application, max_applications=1000): + super().__init__( + application, + max_applications=max_applications, + ) + self._sock = sock.socket(sock.AF_INET, sock.SOCK_DGRAM) + self._sock.setblocking(False) + self._sock.bind(("127.0.0.1", 0)) + + @property + def address(self): + return self._sock.getsockname() + + async def handle(self): + while True: + data, addr = await sock_recvfrom(self._sock, 4096) + data = data.decode("utf-8") + + if data.startswith("Register"): + _, usr_name = data.split(" ") + input_quene = self.get_or_create_application_instance(usr_name, addr) + input_quene.put_nowait(b"Welcome") + + elif data.startswith("To"): + _, usr_name, msg = data.split(" ", 2) + input_quene = self.get_or_create_application_instance(usr_name, addr) + input_quene.put_nowait(msg.encode("utf-8")) + + async def application_send(self, scope, message): + self._sock.sendto(message, scope) + def close(self): + self._sock.close() + for details in self.application_instances.values(): + details["future"].cancel() + + +class Client: + def __init__(self, name): + self._sock = sock.socket(sock.AF_INET, sock.SOCK_DGRAM) + self._sock.setblocking(False) + self.name = name + + async def register(self, server_addr, name=None): + name = name or self.name + self._sock.sendto(f"Register {name}".encode("utf-8"), server_addr) + + async def send(self, server_addr, to, msg): + self._sock.sendto(f"To {to} {msg}".encode("utf-8"), server_addr) + + async def get_msg(self): + msg, server_addr = await sock_recvfrom(self._sock, 4096) + return msg, server_addr + + def close(self): + self._sock.close() + + [email protected](scope="function") +def server(): async def app(scope, receive, send): - pass + while True: + msg = await receive() + await send(msg) + + server = Server(app, 10) + yield server + server.close() + + +async def check_client_msg(client, expected_address, expected_msg): + msg, server_addr = await asyncio.wait_for(client.get_msg(), timeout=1.0) + assert msg == expected_msg + assert server_addr == expected_address + + +async def server_auto_close(fut, timeout): + """Server run based on run_until_complete. It will block forever with handle + function because it is a while True loop without break. Use this method to close + server automatically.""" + loop = asyncio.get_running_loop() + task = asyncio.ensure_future(fut, loop=loop) + await asyncio.sleep(timeout) + task.cancel() + + +def test_stateless_server(server): + """StatelessServer can be instantiated with an ASGI 3 application.""" + """Create a UDP Server can register instance based on name from message of client. + Clients can communicate to other client by name through server""" + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + server.handle = partial(server_auto_close, fut=server.handle(), timeout=1.0) + + client1 = Client(name="client1") + client2 = Client(name="client2") + + async def check_client1_behavior(): + await client1.register(server.address) + await check_client_msg(client1, server.address, b"Welcome") + await client1.send(server.address, "client2", "Hello") + + async def check_client2_behavior(): + await client2.register(server.address) + await check_client_msg(client2, server.address, b"Welcome") + await check_client_msg(client2, server.address, b"Hello") + + task1 = loop.create_task(check_client1_behavior()) + task2 = loop.create_task(check_client2_behavior()) + + server.run() + + assert task1.done() + assert task2.done() + + +def test_server_delete_instance(server): + """The max_applications of Server is 10. After 20 times register, application number should be 10.""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + server.handle = partial(server_auto_close, fut=server.handle(), timeout=1.0) + + client1 = Client(name="client1") + + async def client1_multiple_register(): + for i in range(20): + await client1.register(server.address, name=f"client{i}") + print(f"client{i}") + await check_client_msg(client1, server.address, b"Welcome") + + task = loop.create_task(client1_multiple_register()) + server.run() - server = StatelessServer(app) - server.get_or_create_application_instance("scope_id", {}) + assert task.done() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/tests/test_sync.py new/asgiref-3.5.2/tests/test_sync.py --- old/asgiref-3.4.1/tests/test_sync.py 2021-06-30 21:01:55.000000000 +0200 +++ new/asgiref-3.5.2/tests/test_sync.py 2022-05-16 22:28:11.000000000 +0200 @@ -1,17 +1,17 @@ import asyncio import functools import multiprocessing -import sys import threading import time +import warnings from concurrent.futures import ThreadPoolExecutor from functools import wraps from unittest import TestCase import pytest -from asgiref.compatibility import create_task, get_running_loop from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async +from asgiref.timeout import timeout @pytest.mark.asyncio @@ -35,15 +35,15 @@ assert result == 42 assert end - start >= 1 # Set workers to 1, call it twice and make sure that works right - loop = get_running_loop() + loop = asyncio.get_running_loop() old_executor = loop._default_executor or ThreadPoolExecutor() loop.set_default_executor(ThreadPoolExecutor(max_workers=1)) try: start = time.monotonic() await asyncio.wait( [ - create_task(async_function()), - create_task(async_function()), + asyncio.create_task(async_function()), + asyncio.create_task(async_function()), ] ) end = time.monotonic() @@ -100,6 +100,18 @@ @pytest.mark.asyncio +async def test_sync_to_async_raises_typeerror_for_async_callable_instance(): + class CallableClass: + async def __call__(self): + return None + + with pytest.raises( + TypeError, match="sync_to_async can only be applied to sync functions." + ): + sync_to_async(CallableClass()) + + [email protected] async def test_sync_to_async_decorator(): """ Tests sync_to_async as a decorator @@ -366,9 +378,32 @@ assert result["worked"] -def test_async_to_async_method_self_attribute(): +def test_async_to_sync_on_callable_object(): + """ + Tests async_to_sync on a callable class instance + """ + + result = {} + + class CallableClass: + async def __call__(self, value): + await asyncio.sleep(0) + result["worked"] = True + return value + + # Run it (without warnings) + with warnings.catch_warnings(): + warnings.simplefilter("error") + sync_function = async_to_sync(CallableClass()) + out = sync_function(42) + + assert out == 42 + assert result["worked"] is True + + +def test_async_to_sync_method_self_attribute(): """ - Tests async_to_async on a method copies __self__. + Tests async_to_sync on a method copies __self__. """ # Define async function. class TestClass: @@ -398,15 +433,21 @@ @async_to_sync async def middle(): await inner() + await asyncio.create_task(inner_task()) - # Inner sync function + # Inner sync functions @sync_to_async def inner(): result["thread"] = threading.current_thread() + @sync_to_async + def inner_task(): + result["thread2"] = threading.current_thread() + # Run it middle() assert result["thread"] == threading.current_thread() + assert result["thread2"] == threading.current_thread() @pytest.mark.asyncio @@ -435,7 +476,9 @@ result["thread"] = threading.current_thread() # Run it (in supposed parallel!) - await asyncio.wait([create_task(outer(result_1)), create_task(inner(result_2))]) + await asyncio.wait( + [asyncio.create_task(outer(result_1)), asyncio.create_task(inner(result_2))] + ) # They should not have run in the main thread, but in the same thread assert result_1["thread"] != threading.current_thread() @@ -457,8 +500,8 @@ # Run it (in supposed parallel!) await asyncio.wait( [ - create_task(store_thread_async(result_1)), - create_task(store_thread_async(result_2)), + asyncio.create_task(store_thread_async(result_1)), + asyncio.create_task(store_thread_async(result_2)), ] ) @@ -606,32 +649,33 @@ assert asyncio.iscoroutinefunction(sync_to_async(sync_func)) +async def async_process(queue): + queue.put(42) + + +def sync_process(queue): + """Runs async_process synchronously""" + async_to_sync(async_process)(queue) + + +def fork_first(): + """Forks process before running sync_process""" + queue = multiprocessing.Queue() + fork = multiprocessing.Process(target=sync_process, args=[queue]) + fork.start() + fork.join(3) + # Force cleanup in failed test case + if fork.is_alive(): + fork.terminate() + return queue.get(True, 1) + + @pytest.mark.asyncio async def test_multiprocessing(): """ Tests that a forked process can use async_to_sync without it looking for the event loop from the parent process. """ - - test_queue = multiprocessing.Queue() - - async def async_process(): - test_queue.put(42) - - def sync_process(): - """Runs async_process synchronously""" - async_to_sync(async_process)() - - def fork_first(): - """Forks process before running sync_process""" - fork = multiprocessing.Process(target=sync_process) - fork.start() - fork.join(3) - # Force cleanup in failed test case - if fork.is_alive(): - fork.terminate() - return test_queue.get(True, 1) - assert await sync_to_async(fork_first)() == 42 @@ -672,7 +716,6 @@ ) [email protected](sys.version_info < (3, 7), reason="Issue persists with 3.6") def test_sync_to_async_deadlock_raises(): def db_write(): pass @@ -696,7 +739,6 @@ asyncio.run(server_entry()) [email protected](sys.version_info < (3, 7), reason="Issue persists with 3.6") def test_sync_to_async_deadlock_ignored_with_exception(): """ Ensures that throwing an exception from inside a deadlock-protected block @@ -717,3 +759,75 @@ pass asyncio.run(server_entry()) + + [email protected] [email protected] +async def test_sync_to_async_with_blocker_thread_sensitive(): + """ + Tests sync_to_async running on a long-time blocker in a thread_sensitive context. + Expected to fail at the moment. + """ + + delay = 1 # second + event = multiprocessing.Event() + + async def async_process_waiting_on_event(): + """Wait for the event to be set.""" + await sync_to_async(event.wait)() + return 42 + + async def async_process_that_triggers_event(): + """Sleep, then set the event.""" + await asyncio.sleep(delay) + await sync_to_async(event.set)() + + # Run the event setter as a task. + trigger_task = asyncio.ensure_future(async_process_that_triggers_event()) + + try: + # wait on the event waiter, which is now blocking the event setter. + async with timeout(delay + 1): + assert await async_process_waiting_on_event() == 42 + except asyncio.TimeoutError: + # In case of timeout, set the event to unblock things, else + # downstream tests will get fouled up. + event.set() + raise + finally: + await trigger_task + + [email protected] +async def test_sync_to_async_with_blocker_non_thread_sensitive(): + """ + Tests sync_to_async running on a long-time blocker in a non_thread_sensitive context. + """ + + delay = 1 # second + event = multiprocessing.Event() + + async def async_process_waiting_on_event(): + """Wait for the event to be set.""" + await sync_to_async(event.wait, thread_sensitive=False)() + return 42 + + async def async_process_that_triggers_event(): + """Sleep, then set the event.""" + await asyncio.sleep(1) + await sync_to_async(event.set)() + + # Run the event setter as a task. + trigger_task = asyncio.ensure_future(async_process_that_triggers_event()) + + try: + # wait on the event waiter, which is now blocking the event setter. + async with timeout(delay + 1): + assert await async_process_waiting_on_event() == 42 + except asyncio.TimeoutError: + # In case of timeout, set the event to unblock things, else + # downstream tests will get fouled up. + event.set() + raise + finally: + await trigger_task diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asgiref-3.4.1/tests/test_sync_contextvars.py new/asgiref-3.5.2/tests/test_sync_contextvars.py --- old/asgiref-3.4.1/tests/test_sync_contextvars.py 2021-06-27 22:11:39.000000000 +0200 +++ new/asgiref-3.5.2/tests/test_sync_contextvars.py 2022-01-22 17:43:01.000000000 +0100 @@ -4,7 +4,6 @@ import pytest -from asgiref.compatibility import create_task from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async contextvars = pytest.importorskip("contextvars") @@ -26,7 +25,9 @@ await store_thread(result) # Run it (in true parallel!) - await asyncio.wait([create_task(fn(result_1)), create_task(fn(result_2))]) + await asyncio.wait( + [asyncio.create_task(fn(result_1)), asyncio.create_task(fn(result_2))] + ) # They should not have run in the main thread, and on different threads assert result_1["thread"] != threading.current_thread()
