Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Make compatible with Python 3.12 #120

Merged
merged 6 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v2
- name: Install Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
53 changes: 36 additions & 17 deletions synchronicity/synchronizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -243,12 +250,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:
Expand All @@ -258,11 +270,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)
Expand Down Expand Up @@ -620,16 +632,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):
mwaskom marked this conversation as resolved.
Show resolved Hide resolved
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")
Expand Down Expand Up @@ -670,12 +688,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):
Expand Down
4 changes: 3 additions & 1 deletion test/asynccontextmanager_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from contextlib import asynccontextmanager
import pytest

Expand Down Expand Up @@ -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
Loading