From 540d1819cf8a996f1a73339e46b5c90201d696b7 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 16 Jan 2024 10:55:20 -0500 Subject: [PATCH 1/6] Run test on 3.11 and 3.12 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7839eb7..8895890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Install Python ${{ matrix.python-version }} From 7e4431c99f8a129d58d02a0223025b56d2ae68fe Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 16 Jan 2024 11:03:25 -0500 Subject: [PATCH 2/6] Fix expected error class on Python 3.11+ --- test/asynccontextmanager_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/asynccontextmanager_test.py b/test/asynccontextmanager_test.py index d40fb54..7071958 100644 --- a/test/asynccontextmanager_test.py +++ b/test/asynccontextmanager_test.py @@ -1,3 +1,4 @@ +import sys from contextlib import asynccontextmanager import pytest @@ -117,6 +118,7 @@ async def b(): @pytest.mark.asyncio async def test_asynccontextmanager_with_in_async(): r = s.create_async(Resource)() - with pytest.raises(AttributeError): + err_cls = AttributeError if sys.version_info < (3, 11) else TypeError + with pytest.raises(err_cls): with r.wrap(): pass From 03d0629cfd6ab1b1ca4e780b04f22416326a5f03 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 16 Jan 2024 13:54:41 -0500 Subject: [PATCH 3/6] Attempt to handle lack of __dict__ on TypeVar in Python 3.12 --- synchronicity/synchronizer.py | 38 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/synchronicity/synchronizer.py b/synchronicity/synchronizer.py index c829ae8..3edfed8 100644 --- a/synchronicity/synchronizer.py +++ b/synchronicity/synchronizer.py @@ -243,12 +243,17 @@ def _translate_scalar_out(self, obj, interface): interface = Interface.BLOCKING # If it's an internal object, translate it to the external interface - if inspect.isclass(obj) or isinstance(obj, typing.TypeVar): # TODO: functions? + if inspect.isclass(obj): # TODO: functions? cls_dct = obj.__dict__ if self._wrapped_attr in cls_dct: return cls_dct[self._wrapped_attr][interface] else: return obj + elif isinstance(obj, typing.TypeVar): + if hasattr(obj, self._wrapped_attr): + return getattr(obj, self._wrapped_attr)[interface] + else: + return obj else: cls_dct = obj.__class__.__dict__ if self._wrapped_attr in cls_dct: @@ -620,16 +625,22 @@ def _wrap( # It wraps the object, and caches the wrapped object # Get the list of existing interfaces - if self._wrapped_attr not in obj.__dict__: - if isinstance(obj.__dict__, dict): - # This works for instances - obj.__dict__.setdefault(self._wrapped_attr, {}) - else: - # This works for classes & functions + if hasattr(obj, "__dict__"): + if self._wrapped_attr not in obj.__dict__: + if isinstance(obj.__dict__, dict): + # This works for instances + obj.__dict__.setdefault(self._wrapped_attr, {}) + else: + # This works for classes & functions + setattr(obj, self._wrapped_attr, {}) + interfaces = obj.__dict__[self._wrapped_attr] + else: + # e.g., TypeVar in Python>=3.12 + if not hasattr(obj, self._wrapped_attr): setattr(obj, self._wrapped_attr, {}) + interfaces = getattr(obj, self._wrapped_attr) # If this is already wrapped, return the existing interface - interfaces = obj.__dict__[self._wrapped_attr] if interface in interfaces: if self._multiwrap_warning: warnings.warn(f"Object {obj} is already wrapped, but getting wrapped again") @@ -670,12 +681,13 @@ def _wrap_type_var(self, obj, interface, name, target_module): # TODO(elias): Refactor - since this isn't used for live apps, move type stub generation into genstub new_obj = typing.TypeVar(name, bound=obj.__bound__) # noqa - new_obj.__dict__[self._original_attr] = obj - new_obj.__dict__[SYNCHRONIZER_ATTR] = self - new_obj.__dict__[TARGET_INTERFACE_ATTR] = interface + setattr(new_obj, self._original_attr, obj) + setattr(new_obj, SYNCHRONIZER_ATTR, self) + setattr(new_obj, TARGET_INTERFACE_ATTR, interface) new_obj.__module__ = target_module - obj.__dict__.setdefault(self._wrapped_attr, {}) - obj.__dict__[self._wrapped_attr][interface] = new_obj + if not hasattr(obj, self._wrapped_attr): + setattr(obj, self._wrapped_attr, {}) + getattr(obj, self._wrapped_attr)[interface] = new_obj return new_obj def nowrap(self, obj): From 27ea305be816fda1d4e8329400f9b9ade6c0f62c Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 16 Jan 2024 14:02:08 -0500 Subject: [PATCH 4/6] Disable ruff check on type comparison that looks intentional --- synchronicity/synchronizer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synchronicity/synchronizer.py b/synchronicity/synchronizer.py index 3edfed8..fbfb3f9 100644 --- a/synchronicity/synchronizer.py +++ b/synchronicity/synchronizer.py @@ -263,11 +263,11 @@ def _translate_scalar_out(self, obj, interface): return obj def _recurse_map(self, mapper, obj): - if type(obj) == list: + if type(obj) == list: # noqa: E721 return list(self._recurse_map(mapper, item) for item in obj) - elif type(obj) == tuple: + elif type(obj) == tuple: # noqa: E721 return tuple(self._recurse_map(mapper, item) for item in obj) - elif type(obj) == dict: + elif type(obj) == dict: # noqa: E721 return dict((key, self._recurse_map(mapper, item)) for key, item in obj.items()) else: return mapper(obj) From c89a02fc984aca282d9cd3c298c3226d61958ed4 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 23 Jan 2024 15:34:47 +0000 Subject: [PATCH 5/6] Swallow RuntimeError triggered at interpreter shutdown --- synchronicity/synchronizer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/synchronicity/synchronizer.py b/synchronicity/synchronizer.py index fbfb3f9..32310ab 100644 --- a/synchronicity/synchronizer.py +++ b/synchronicity/synchronizer.py @@ -156,7 +156,14 @@ async def loop_inner(): is_ready.set() await self._stopping.wait() # wait until told to stop - asyncio.run(loop_inner()) + try: + asyncio.run(loop_inner()) + except RuntimeError as exc: + # Python 3.12 raises a RuntimeError when new threads are created at shutdown. + # Swallowing it here is innocuous, but ideally we will revisit this after + # refactoring the shutdown handlers that modal uses to avoid triggering it. + if "can't create new thread at interpreter shutdown" not in str(exc): + raise exc self._thread = threading.Thread(target=thread_inner, daemon=True) self._thread.start() From 4c7aeeaaba140974f4e3f1dd186e9a9141d039d4 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 23 Jan 2024 15:39:26 +0000 Subject: [PATCH 6/6] Drop Python 3.7 --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8895890..3f149a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Install Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 9a8596f..3560d2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" dependencies = [ "sigtools==4.0.1", ] -requires-python = ">=3.7.9" +requires-python = ">=3.8" [build-system] requires = ["setuptools", "wheel"]