]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
create JoinedDispatcher subclasses up front
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 24 Jun 2024 19:07:41 +0000 (15:07 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 24 Jun 2024 19:13:41 +0000 (15:13 -0400)
Fixed additional issues in the event system triggered by unpickling of a
:class:`.Enum` datatype, continuing from :ticket:`11365` and
:ticket:`11360`,  where dynamically generated elements of the event
structure would not be present when unpickling in a new process.

Fixes: #11530
Change-Id: Ie1f2b3453d4891051f8719f6d3f6703302d5a86e

doc/build/changelog/unreleased_20/11530.rst [new file with mode: 0644]
lib/sqlalchemy/event/base.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_20/11530.rst b/doc/build/changelog/unreleased_20/11530.rst
new file mode 100644 (file)
index 0000000..30c60cd
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, events
+    :tickets: 11530
+
+    Fixed additional issues in the event system triggered by unpickling of a
+    :class:`.Enum` datatype, continuing from :ticket:`11365` and
+    :ticket:`11360`,  where dynamically generated elements of the event
+    structure would not be present when unpickling in a new process.
index 434886316f007d53f342106e191ee9bcbe2b7ada..cddfc982a6c675e536d9db2d0f097a48966a0071 100644 (file)
@@ -191,16 +191,7 @@ class _Dispatch(_DispatchCommon[_ET]):
         :class:`._Dispatch` objects.
 
         """
-        if "_joined_dispatch_cls" not in self.__class__.__dict__:
-            cls = type(
-                "Joined%s" % self.__class__.__name__,
-                (_JoinedDispatcher,),
-                {"__slots__": self._event_names},
-            )
-            self.__class__._joined_dispatch_cls = cls
-
-            # establish pickle capability by adding it to this module
-            globals()[cls.__name__] = cls
+        assert "_joined_dispatch_cls" in self.__class__.__dict__
 
         return self._joined_dispatch_cls(self, other)
 
@@ -332,6 +323,51 @@ class _HasEventsDispatch(Generic[_ET]):
             else:
                 dispatch_target_cls.dispatch = dispatcher(cls)
 
+        klass = type(
+            "Joined%s" % dispatch_cls.__name__,
+            (_JoinedDispatcher,),
+            {"__slots__": event_names},
+        )
+        dispatch_cls._joined_dispatch_cls = klass
+
+        # establish pickle capability by adding it to this module
+        globals()[klass.__name__] = klass
+
+
+class _JoinedDispatcher(_DispatchCommon[_ET]):
+    """Represent a connection between two _Dispatch objects."""
+
+    __slots__ = "local", "parent", "_instance_cls"
+
+    local: _DispatchCommon[_ET]
+    parent: _DispatchCommon[_ET]
+    _instance_cls: Optional[Type[_ET]]
+
+    def __init__(
+        self, local: _DispatchCommon[_ET], parent: _DispatchCommon[_ET]
+    ):
+        self.local = local
+        self.parent = parent
+        self._instance_cls = self.local._instance_cls
+
+    def __reduce__(self) -> Any:
+        return (self.__class__, (self.local, self.parent))
+
+    def __getattr__(self, name: str) -> _JoinedListener[_ET]:
+        # Assign _JoinedListeners as attributes on demand
+        # to reduce startup time for new dispatch objects.
+        ls = getattr(self.local, name)
+        jl = _JoinedListener(self.parent, ls.name, ls)
+        setattr(self, ls.name, jl)
+        return jl
+
+    def _listen(self, event_key: _EventKey[_ET], **kw: Any) -> None:
+        return self.parent._listen(event_key, **kw)
+
+    @property
+    def _events(self) -> Type[_HasEventsDispatch[_ET]]:
+        return self.parent._events
+
 
 class Events(_HasEventsDispatch[_ET]):
     """Define event listening functions for a particular target type."""
@@ -386,41 +422,6 @@ class Events(_HasEventsDispatch[_ET]):
         cls.dispatch._clear()
 
 
-class _JoinedDispatcher(_DispatchCommon[_ET]):
-    """Represent a connection between two _Dispatch objects."""
-
-    __slots__ = "local", "parent", "_instance_cls"
-
-    local: _DispatchCommon[_ET]
-    parent: _DispatchCommon[_ET]
-    _instance_cls: Optional[Type[_ET]]
-
-    def __init__(
-        self, local: _DispatchCommon[_ET], parent: _DispatchCommon[_ET]
-    ):
-        self.local = local
-        self.parent = parent
-        self._instance_cls = self.local._instance_cls
-
-    def __reduce__(self) -> Any:
-        return (self.__class__, (self.local, self.parent))
-
-    def __getattr__(self, name: str) -> _JoinedListener[_ET]:
-        # Assign _JoinedListeners as attributes on demand
-        # to reduce startup time for new dispatch objects.
-        ls = getattr(self.local, name)
-        jl = _JoinedListener(self.parent, ls.name, ls)
-        setattr(self, ls.name, jl)
-        return jl
-
-    def _listen(self, event_key: _EventKey[_ET], **kw: Any) -> None:
-        return self.parent._listen(event_key, **kw)
-
-    @property
-    def _events(self) -> Type[_HasEventsDispatch[_ET]]:
-        return self.parent._events
-
-
 class dispatcher(Generic[_ET]):
     """Descriptor used by target classes to
     deliver the _Dispatch class at the class level
index 5214ebac53c71a332572b782e6528910f085d2fa..36c6a74c27ef77e2ed3c6126649be130324db686 100644 (file)
@@ -3,6 +3,10 @@ import decimal
 import importlib
 import operator
 import os
+import pickle
+import subprocess
+import sys
+from tempfile import mkstemp
 
 import sqlalchemy as sa
 from sqlalchemy import and_
@@ -531,6 +535,59 @@ class PickleTypesTest(fixtures.TestBase):
             loads(dumps(column_type))
             loads(dumps(meta))
 
+    @testing.combinations(
+        ("Str", String()),
+        ("Tex", Text()),
+        ("Uni", Unicode()),
+        ("Boo", Boolean()),
+        ("Dat", DateTime()),
+        ("Dat", Date()),
+        ("Tim", Time()),
+        ("Lar", LargeBinary()),
+        ("Pic", PickleType()),
+        ("Int", Interval()),
+        ("Enu", Enum("one", "two", "three")),
+        argnames="name,type_",
+        id_="ar",
+    )
+    @testing.variation("use_adapt", [True, False])
+    def test_pickle_types_other_process(self, name, type_, use_adapt):
+        """test for #11530
+
+        this does a full exec of python interpreter so the number of variations
+        here is reduced to just a single pickler, else each case takes
+        a full second.
+
+        """
+
+        if use_adapt:
+            type_ = type_.copy()
+
+        column_type = Column(name, type_)
+        meta = MetaData()
+        Table("foo", meta, column_type)
+
+        for target in column_type, meta:
+            f, name = mkstemp("pkl")
+            with os.fdopen(f, "wb") as f:
+                pickle.dump(target, f)
+
+            name = name.replace(os.sep, "/")
+            code = (
+                "import sqlalchemy; import pickle; "
+                f"pickle.load(open('''{name}''', 'rb'))"
+            )
+            parts = list(sys.path)
+            if os.environ.get("PYTHONPATH"):
+                parts.append(os.environ["PYTHONPATH"])
+            pythonpath = os.pathsep.join(parts)
+            proc = subprocess.run(
+                [sys.executable, "-c", code],
+                env={**os.environ, "PYTHONPATH": pythonpath},
+            )
+            eq_(proc.returncode, 0)
+            os.unlink(name)
+
 
 class _UserDefinedTypeFixture:
     @classmethod