]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Load the uuid and network modules lazily
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 9 Nov 2020 00:55:11 +0000 (00:55 +0000)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 9 Nov 2020 01:56:56 +0000 (01:56 +0000)
The uuid module is slow to import (was fixed in psycopg2 2.4.3, but
there is no ticket open for it). Similarly, lazy load other non-default
modules.

psycopg3/psycopg3/_transform.py
psycopg3/psycopg3/adapt.py
psycopg3/psycopg3/proto.py
psycopg3/psycopg3/types/network.py
psycopg3/psycopg3/types/uuid.py
psycopg3_c/psycopg3_c/transform.pyx
tests/types/test_network.py
tests/types/test_uuid.py

index dfe79fbd4a8134f205231b03d939df5bbb01bea7..383b3c1597651a434387dbf1e00317cc9295727f 100644 (file)
@@ -157,9 +157,25 @@ class Transformer:
         for dmap in self._dumpers_maps:
             for scls in cls.__mro__:
                 key = (scls, format)
-                if key in dmap:
-                    self._dumpers_cache[key] = dumper = dmap[key](scls, self)
-                    return dumper
+                dumper_class = dmap.get(key)
+                if not dumper_class:
+                    continue
+
+                self._dumpers_cache[key] = dumper = dumper_class(scls, self)
+                return dumper
+
+        # If the adapter is not found, look for its name as a string
+        for dmap in self._dumpers_maps:
+            for scls in cls.__mro__:
+                fqn = f"{cls.__module__}.{scls.__qualname__}"
+                dumper_class = dmap.get((fqn, format))
+                if dumper_class is None:
+                    continue
+
+                key = (scls, format)
+                dmap[key] = dumper_class
+                self._dumpers_cache[key] = dumper = dumper_class(scls, self)
+                return dumper
 
         raise e.ProgrammingError(
             f"cannot adapt type {type(obj).__name__}"
index 6b40fee270f22da6a552f23c0f819b4630dc689e..93f26a774961af9766e868f2c4e17c4870009d1a 100644 (file)
@@ -4,7 +4,7 @@ Entry point into the adaptation system.
 
 # Copyright (C) 2020 The Psycopg Team
 
-from typing import Any, Callable, Optional, Type
+from typing import Any, Callable, Optional, Type, Union
 
 from . import pq
 from . import proto
@@ -46,11 +46,11 @@ class Dumper:
     @classmethod
     def register(
         cls,
-        src: type,
+        src: Union[type, str],
         context: AdaptContext = None,
         format: Format = Format.TEXT,
     ) -> None:
-        if not isinstance(src, type):
+        if not isinstance(src, (str, type)):
             raise TypeError(
                 f"dumpers should be registered on classes, got {src} instead"
             )
@@ -59,11 +59,13 @@ class Dumper:
         where[src, format] = cls
 
     @classmethod
-    def register_binary(cls, src: type, context: AdaptContext = None) -> None:
+    def register_binary(
+        cls, src: Union[type, str], context: AdaptContext = None
+    ) -> None:
         cls.register(src, context, format=Format.BINARY)
 
     @classmethod
-    def text(cls, src: type) -> Callable[[DumperType], DumperType]:
+    def text(cls, src: Union[type, str]) -> Callable[[DumperType], DumperType]:
         def text_(dumper: DumperType) -> DumperType:
             dumper.register(src)
             return dumper
@@ -71,7 +73,9 @@ class Dumper:
         return text_
 
     @classmethod
-    def binary(cls, src: type) -> Callable[[DumperType], DumperType]:
+    def binary(
+        cls, src: Union[type, str]
+    ) -> Callable[[DumperType], DumperType]:
         def binary_(dumper: DumperType) -> DumperType:
             dumper.register_binary(src)
             return dumper
index 3a0130e4b266084389c73f22bf73941f90c64624..c0bdfd66453777d92792df5df4b9e71383967a3c 100644 (file)
@@ -35,7 +35,7 @@ AdaptContext = Union[None, "BaseConnection", "BaseCursor", "Transformer"]
 
 DumpFunc = Callable[[Any], bytes]
 DumperType = Type["Dumper"]
-DumpersMap = Dict[Tuple[type, Format], DumperType]
+DumpersMap = Dict[Tuple[Union[type, str], Format], DumperType]
 
 LoadFunc = Callable[[bytes], Any]
 LoaderType = Type["Loader"]
index b192aeee58a46a671db32d855e3a636e24c5ed85..6fdd4f8b97fd107b60ac63c90f041a7f268433aa 100644 (file)
@@ -4,30 +4,29 @@ Adapters for network types.
 
 # Copyright (C) 2020 The Psycopg Team
 
-# TODO: consiter lazy dumper registration.
-import ipaddress
-from ipaddress import IPv4Address, IPv4Interface, IPv4Network
-from ipaddress import IPv6Address, IPv6Interface, IPv6Network
-
-from typing import cast, Callable, Union
+from typing import Callable, Union, TYPE_CHECKING
 
 from ..oids import builtins
 from ..adapt import Dumper, Loader
+from ..proto import AdaptContext
+
+if TYPE_CHECKING:
+    import ipaddress
 
-Address = Union[IPv4Address, IPv6Address]
-Interface = Union[IPv4Interface, IPv6Interface]
-Network = Union[IPv4Network, IPv6Network]
+Address = Union["ipaddress.IPv4Address", "ipaddress.IPv6Address"]
+Interface = Union["ipaddress.IPv4Interface", "ipaddress.IPv6Interface"]
+Network = Union["ipaddress.IPv4Network", "ipaddress.IPv6Network"]
 
-# in typeshed these types are commented out
-ip_address = cast(Callable[[str], Address], ipaddress.ip_address)
-ip_interface = cast(Callable[[str], Interface], ipaddress.ip_interface)
-ip_network = cast(Callable[[str], Network], ipaddress.ip_network)
+# These functions will be imported lazily
+ip_address: Callable[[str], Address]
+ip_interface: Callable[[str], Interface]
+ip_network: Callable[[str], Network]
 
 
-@Dumper.text(IPv4Address)
-@Dumper.text(IPv6Address)
-@Dumper.text(IPv4Interface)
-@Dumper.text(IPv6Interface)
+@Dumper.text("ipaddress.IPv4Address")
+@Dumper.text("ipaddress.IPv6Address")
+@Dumper.text("ipaddress.IPv4Interface")
+@Dumper.text("ipaddress.IPv6Interface")
 class InterfaceDumper(Dumper):
 
     oid = builtins["inet"].oid
@@ -36,8 +35,8 @@ class InterfaceDumper(Dumper):
         return str(obj).encode("utf8")
 
 
-@Dumper.text(IPv4Network)
-@Dumper.text(IPv6Network)
+@Dumper.text("ipaddress.IPv4Network")
+@Dumper.text("ipaddress.IPv6Network")
 class NetworkDumper(Dumper):
 
     oid = builtins["cidr"].oid
@@ -46,8 +45,15 @@ class NetworkDumper(Dumper):
         return str(obj).encode("utf8")
 
 
+class _LazyIpaddress(Loader):
+    def __init__(self, oid: int, context: AdaptContext = None):
+        super().__init__(oid, context)
+        global ip_address, ip_interface, ip_network
+        from ipaddress import ip_address, ip_interface, ip_network
+
+
 @Loader.text(builtins["inet"].oid)
-class InetLoader(Loader):
+class InetLoader(_LazyIpaddress):
     def load(self, data: bytes) -> Union[Address, Interface]:
         if b"/" in data:
             return ip_interface(data.decode("utf8"))
@@ -56,6 +62,6 @@ class InetLoader(Loader):
 
 
 @Loader.text(builtins["cidr"].oid)
-class CidrLoader(Loader):
+class CidrLoader(_LazyIpaddress):
     def load(self, data: bytes) -> Network:
         return ip_network(data.decode("utf8"))
index 73cb30ef2a89adeec3d2c8cd9efcb8a53aa02b1b..eb50a1eb84a67932cb67c3806fc315f6ba7e6781 100644 (file)
@@ -4,36 +4,46 @@ Adapters for the UUID type.
 
 # Copyright (C) 2020 The Psycopg Team
 
-# TODO: importing uuid is slow. Don't import it at module level.
-# Should implement lazy dumper registration.
-from uuid import UUID
+from typing import Callable, TYPE_CHECKING
 
 from ..oids import builtins
 from ..adapt import Dumper, Loader
+from ..proto import AdaptContext
 
+if TYPE_CHECKING:
+    import uuid
 
-@Dumper.text(UUID)
+# Importing the uuid module is slow, so import it only on request.
+UUID: Callable[..., "uuid.UUID"]
+
+
+@Dumper.text("uuid.UUID")
 class UUIDDumper(Dumper):
 
     oid = builtins["uuid"].oid
 
-    def dump(self, obj: UUID) -> bytes:
+    def dump(self, obj: "uuid.UUID") -> bytes:
         return obj.hex.encode("utf8")
 
 
-@Dumper.binary(UUID)
+@Dumper.binary("uuid.UUID")
 class UUIDBinaryDumper(UUIDDumper):
-    def dump(self, obj: UUID) -> bytes:
+    def dump(self, obj: "uuid.UUID") -> bytes:
         return obj.bytes
 
 
 @Loader.text(builtins["uuid"].oid)
 class UUIDLoader(Loader):
-    def load(self, data: bytes) -> UUID:
+    def __init__(self, oid: int, context: AdaptContext = None):
+        super().__init__(oid, context)
+        global UUID
+        from uuid import UUID
+
+    def load(self, data: bytes) -> "uuid.UUID":
         return UUID(data.decode("utf8"))
 
 
 @Loader.binary(builtins["uuid"].oid)
-class UUIDBinaryLoader(Loader):
-    def load(self, data: bytes) -> UUID:
+class UUIDBinaryLoader(UUIDLoader):
+    def load(self, data: bytes) -> "uuid.UUID":
         return UUID(bytes=data)
index a8cde18326103d9c0a3b2b399493829173cffd32..7d7e79fc63e5c2f31d1a43a0eb39ed91c71fc084 100644 (file)
@@ -192,9 +192,25 @@ cdef class Transformer:
         for dmap in self._dumpers_maps:
             for scls in cls.__mro__:
                 key = (scls, format)
-                if key in dmap:
-                    self._dumpers_cache[key] = dumper = dmap[key](scls, self)
-                    return dumper
+                dumper_class = dmap.get(key)
+                if not dumper_class:
+                    continue
+
+                self._dumpers_cache[key] = dumper = dumper_class(scls, self)
+                return dumper
+
+        # If the adapter is not found, look for its name as a string
+        for dmap in self._dumpers_maps:
+            for scls in cls.__mro__:
+                fqn = f"{cls.__module__}.{scls.__qualname__}"
+                dumper_class = dmap.get((fqn, format))
+                if dumper_class is None:
+                    continue
+
+                key = (scls, format)
+                dmap[key] = dumper_class
+                self._dumpers_cache[key] = dumper = dumper_class(scls, self)
+                return dumper
 
         raise e.ProgrammingError(
             f"cannot adapt type {type(obj).__name__}"
index c04a9113fac90975208cde44daa5255b56bfa43a..3d3bbf4aad8062410d6a2c02863d90e60031ddc5 100644 (file)
@@ -1,4 +1,7 @@
+import os
+import sys
 import ipaddress
+import subprocess as sp
 
 import pytest
 
@@ -87,3 +90,28 @@ def test_cidr_load(conn, fmt_out, val):
 def binary_check(fmt):
     if fmt == Format.BINARY:
         pytest.xfail("inet binary not implemented")
+
+
+def test_lazy_load(dsn):
+    script = f"""\
+import sys
+import psycopg3
+
+# In 3.6 it seems already loaded (at least on Travis).
+if sys.version_info >= (3, 7):
+    assert 'ipaddress' not in sys.modules
+
+conn = psycopg3.connect({dsn!r})
+with conn.cursor() as cur:
+    cur.execute("select '127.0.0.1'::inet")
+    cur.fetchone()
+
+conn.close()
+assert 'ipaddress' in sys.modules
+"""
+
+    # TODO: debug this. Importing c module fails on travis in this scenario
+    env = dict(os.environ)
+    env.pop("PSYCOPG3_IMPL", None)
+
+    sp.check_call([sys.executable, "-s", "-c", script], env=env)
index 7352f3c7f3df7a25c4b97a8c5497495b19383193..a231041c3b01b0ce1df72f13f7fa27a168c79ca9 100644 (file)
@@ -1,4 +1,7 @@
+import os
+import sys
 from uuid import UUID
+import subprocess as sp
 
 import pytest
 
@@ -20,3 +23,26 @@ def test_uuid_load(conn, fmt_out):
     val = "12345678123456781234567812345679"
     cur.execute("select %s::uuid", (val,))
     assert cur.fetchone()[0] == UUID(val)
+
+
+def test_lazy_load(dsn):
+    script = f"""\
+import sys
+import psycopg3
+
+assert 'uuid' not in sys.modules
+
+conn = psycopg3.connect({dsn!r})
+with conn.cursor() as cur:
+    cur.execute("select repeat('1', 32)::uuid")
+    cur.fetchone()
+
+conn.close()
+assert 'uuid' in sys.modules
+"""
+
+    # TODO: debug this. Importing c module fails on travis in this scenario
+    env = dict(os.environ)
+    env.pop("PSYCOPG3_IMPL", None)
+
+    sp.check_call([sys.executable, "-c", script], env=env)