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""
 

Reply via email to