]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Dropped ArrayCaster, expose register_array function instead
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Tue, 7 Apr 2020 12:39:37 +0000 (00:39 +1200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Tue, 7 Apr 2020 12:44:00 +0000 (00:44 +1200)
The function only associates array_oid -> base_oid. Looking up the right
typecaster happens at cast time: this way composite adapters (record,
list) can use the caster register in the right context.

All the types for which there is an adapter gets an array caster now.

psycopg3/types/__init__.py
psycopg3/types/array.py
psycopg3/types/numeric.py
psycopg3/types/oids.py
psycopg3/types/text.py
tests/test_adapt.py
tests/types/test_array.py

index b07d7f770023f89d492133af9298e7e208997b30..7b2556dfdf15472ae8984a50d31bf2226b9c5844 100644 (file)
@@ -8,6 +8,9 @@ psycopg3 types package
 from .oids import builtins
 
 # Register default adapters
-from . import array, composite, numeric, text  # noqa
+from . import composite, numeric, text  # noqa
+
+# Register associations with array oids
+from . import array  # noqa
 
 __all__ = ["builtins"]
index f7bab1f26bc0663d7cb637357789d504ac6330c0..5da427192de709e802b3f1e3e6ee4d8e9e124e78 100644 (file)
@@ -6,11 +6,11 @@ Adapters for arrays
 
 import re
 import struct
-from typing import Any, Generator, List, Tuple
+from typing import Any, Generator, List, Optional, Tuple
 
 from .. import errors as e
-from ..adapt import Format, Adapter, TypeCaster, Transformer, UnknownCaster
-from ..adapt import AdaptContext, TypeCasterType, TypeCasterFunc
+from ..adapt import Format, Adapter, TypeCaster, Transformer
+from ..adapt import AdaptContext
 from .oids import builtins
 
 TEXT_OID = builtins["text"].oid
@@ -20,7 +20,7 @@ TEXT_ARRAY_OID = builtins["text"].array_oid
 class BaseListAdapter(Adapter):
     def __init__(self, src: type, context: AdaptContext = None):
         super().__init__(src, context)
-        self.tx = Transformer(context)
+        self._tx = Transformer(context)
 
     def _array_oid(self, base_oid: int) -> int:
         """
@@ -40,7 +40,7 @@ class BaseListAdapter(Adapter):
 
 
 @Adapter.text(list)
-class ListAdapter(BaseListAdapter):
+class TextListAdapter(BaseListAdapter):
     # from https://www.postgresql.org/docs/current/arrays.html#ARRAYS-IO
     #
     # The array output routine will put double quotes around element values if
@@ -78,7 +78,7 @@ class ListAdapter(BaseListAdapter):
                 elif item is None:
                     tokens.append(b"NULL")
                 else:
-                    ad = self.tx.adapt(item)
+                    ad = self._tx.adapt(item)
                     if isinstance(ad, tuple):
                         if oid == 0:
                             oid = ad[1]
@@ -135,7 +135,7 @@ class BinaryListAdapter(BaseListAdapter):
 
             if dim == len(dims) - 1:
                 for item in L:
-                    ad = self.tx.adapt(item, Format.BINARY)
+                    ad = self._tx.adapt(item, Format.BINARY)
                     if isinstance(ad, tuple):
                         if oid == 0:
                             oid = ad[1]
@@ -171,19 +171,15 @@ class BinaryListAdapter(BaseListAdapter):
 
 
 class ArrayCasterBase(TypeCaster):
-    base_caster: TypeCasterType
+    base_oid: int
 
     def __init__(self, oid: int, context: AdaptContext = None):
         super().__init__(oid, context)
-
-        self.caster_func: TypeCasterFunc
-        if isinstance(self.base_caster, type):
-            self.caster_func = self.base_caster(oid, context).cast
-        else:
-            self.caster_func = type(self).base_caster
+        self._tx = Transformer(context)
 
 
 class ArrayCasterText(ArrayCasterBase):
+
     # Tokenize an array representation into item and brackets
     # TODO: currently recognise only , as delimiter. Should be configured
     _re_parse = re.compile(
@@ -198,6 +194,8 @@ class ArrayCasterText(ArrayCasterBase):
     def cast(self, data: bytes) -> List[Any]:
         rv = None
         stack: List[Any] = []
+        cast = self._tx.get_cast_function(self.base_oid, Format.TEXT)
+
         for m in self._re_parse.finditer(data):
             t = m.group(1)
             if t == b"{":
@@ -226,7 +224,7 @@ class ArrayCasterText(ArrayCasterBase):
                 else:
                     if t.startswith(b'"'):
                         t = self._re_unescape.sub(br"\1", t[1:-1])
-                    v = self.caster_func(t)
+                    v = cast(t)
 
                 stack[-1].append(v)
 
@@ -242,16 +240,12 @@ _struct_len = struct.Struct("!i")
 
 
 class ArrayCasterBinary(ArrayCasterBase):
-    def __init__(self, oid: int, context: AdaptContext = None):
-        super().__init__(oid, context)
-        self.tx = Transformer(context)
-
     def cast(self, data: bytes) -> List[Any]:
         ndims, hasnull, oid = _struct_head.unpack_from(data[:12])
         if not ndims:
             return []
 
-        fcast = self.tx.get_cast_function(oid, Format.BINARY)
+        fcast = self._tx.get_cast_function(oid, Format.BINARY)
 
         p = 12 + 8 * ndims
         dims = [
@@ -280,19 +274,28 @@ class ArrayCasterBinary(ArrayCasterBase):
         return agg(dims)
 
 
-class ArrayCaster(TypeCaster):
-    @staticmethod
-    def register(
-        oid: int,  # array oid
-        caster: TypeCasterType,
-        context: AdaptContext = None,
-        format: Format = Format.TEXT,
-    ) -> TypeCasterType:
-        base = ArrayCasterText if format == Format.TEXT else ArrayCasterBinary
-        name = f"{caster.__name__}_{format.name.lower()}_array"
-        t = type(name, (base,), {"base_caster": caster})
-        return TypeCaster.register(oid, t, context=context, format=format)
-
-
-class UnknownArrayCaster(ArrayCasterText):
-    base_caster = UnknownCaster
+def register_array(
+    array_oid: int,
+    base_oid: int,
+    context: AdaptContext = None,
+    name: Optional[str] = None,
+) -> None:
+    if not name:
+        name = f"oid{base_oid}"
+
+    for format, base in (
+        (Format.TEXT, ArrayCasterText),
+        (Format.BINARY, ArrayCasterBinary),
+    ):
+        tcname = f"{name}_array_{format.name.lower()}"
+        t = type(tcname, (base,), {"base_oid": base_oid})
+        TypeCaster.register(array_oid, t, context=context, format=format)
+
+
+# Register associations between array and base oids
+for t in builtins:
+    if t.array_oid and (
+        (t.oid, Format.TEXT) in TypeCaster.globals
+        or (t.oid, Format.BINARY) in TypeCaster.globals
+    ):
+        register_array(t.array_oid, t.oid, name=t.name)
index 2d7d6c93b3ddcc304cb353d4510d4891930f00ea..9ef9dffac4d45d059c20bffaf44ff8d4b5b17ec8 100644 (file)
@@ -11,7 +11,6 @@ from typing import Tuple
 
 from ..adapt import Adapter, TypeCaster
 from .oids import builtins
-from .array import ArrayCaster
 
 FLOAT8_OID = builtins["float8"].oid
 NUMERIC_OID = builtins["numeric"].oid
@@ -68,37 +67,29 @@ def adapt_binary_bool(obj: bool) -> Tuple[bytes, int]:
 @TypeCaster.text(builtins["int4"].oid)
 @TypeCaster.text(builtins["int8"].oid)
 @TypeCaster.text(builtins["oid"].oid)
-@ArrayCaster.text(builtins["int2"].array_oid)
-@ArrayCaster.text(builtins["int4"].array_oid)
-@ArrayCaster.text(builtins["int8"].array_oid)
-@ArrayCaster.text(builtins["oid"].array_oid)
 def cast_int(data: bytes) -> int:
     return int(_decode(data)[0])
 
 
 @TypeCaster.binary(builtins["int2"].oid)
-@ArrayCaster.binary(builtins["int2"].array_oid)
 def cast_binary_int2(data: bytes) -> int:
     rv: int = _int2_struct.unpack(data)[0]
     return rv
 
 
 @TypeCaster.binary(builtins["int4"].oid)
-@ArrayCaster.binary(builtins["int4"].array_oid)
 def cast_binary_int4(data: bytes) -> int:
     rv: int = _int4_struct.unpack(data)[0]
     return rv
 
 
 @TypeCaster.binary(builtins["int8"].oid)
-@ArrayCaster.binary(builtins["int8"].array_oid)
 def cast_binary_int8(data: bytes) -> int:
     rv: int = _int8_struct.unpack(data)[0]
     return rv
 
 
 @TypeCaster.binary(builtins["oid"].oid)
-@ArrayCaster.binary(builtins["oid"].array_oid)
 def cast_binary_oid(data: bytes) -> int:
     rv: int = _oid_struct.unpack(data)[0]
     return rv
index 95bdb2a5bcfc123cf3ca3f7f534e1960199bee1d..1b211fd652b6ca3bfa38c769bcdeec24d746d59d 100644 (file)
@@ -8,7 +8,7 @@ to a Postgres server.
 # Copyright (C) 2020 The Psycopg Team
 
 import re
-from typing import Dict, Optional, NamedTuple, Union
+from typing import Dict, Generator, Optional, NamedTuple, Union
 
 INVALID_OID = 0
 
@@ -21,7 +21,7 @@ class TypeInfo(NamedTuple):
     delimiter: str
 
 
-class TypesInfo:
+class TypesRegistry:
     """
     Container for the information about types in a database.
     """
@@ -38,6 +38,13 @@ class TypesInfo:
         if info.alt_name not in self._by_name:
             self._by_name[info.alt_name] = info
 
+    def __iter__(self) -> Generator[TypeInfo, None, None]:
+        seen = set()
+        for t in self._by_oid.values():
+            if t.oid not in seen:
+                seen.add(t.oid)
+                yield t
+
     def __getitem__(self, key: Union[str, int]) -> TypeInfo:
         if isinstance(key, str):
             return self._by_name[key]
@@ -55,7 +62,7 @@ class TypesInfo:
             return None
 
 
-builtins = TypesInfo()
+builtins = TypesRegistry()
 
 for r in [
     # fmt: off
index adfbf686b5e152e6bcf60b268efc5e2351529eeb..ae3673c877eb60c711f169b6bdad2a489c464ed5 100644 (file)
@@ -11,7 +11,6 @@ from ..adapt import Adapter, TypeCaster, AdaptContext
 from ..utils.typing import EncodeFunc, DecodeFunc
 from ..pq import Escaping
 from .oids import builtins
-from .array import ArrayCaster
 
 TEXT_OID = builtins["text"].oid
 BYTEA_OID = builtins["bytea"].oid
@@ -38,8 +37,6 @@ class StringAdapter(Adapter):
 
 @TypeCaster.text(builtins["text"].oid)
 @TypeCaster.binary(builtins["text"].oid)
-@ArrayCaster.text(builtins["text"].array_oid)
-@ArrayCaster.binary(builtins["text"].array_oid)
 class StringCaster(TypeCaster):
 
     decode: Optional[DecodeFunc]
@@ -81,12 +78,10 @@ def adapt_bytes(b: bytes) -> Tuple[bytes, int]:
 
 
 @TypeCaster.text(builtins["bytea"].oid)
-@ArrayCaster.text(builtins["bytea"].array_oid)
 def cast_bytea(data: bytes) -> bytes:
     return Escaping().unescape_bytea(data)
 
 
 @TypeCaster.binary(builtins["bytea"].oid)
-@ArrayCaster.binary(builtins["bytea"].array_oid)
 def cast_bytea_binary(data: bytes) -> bytes:
     return data
index 2a8107011743c79a2abd2fb8919a25f0b85a7520..23ca1a97073bb2c1d40fc885d54a3a9b4ce98041 100644 (file)
@@ -104,7 +104,6 @@ def test_cast_cursor_ctx(conn):
     assert r == ("hellob",)
 
 
-@pytest.mark.xfail
 @pytest.mark.parametrize(
     "sql, obj",
     [("'{hello}'::text[]", ["helloc"]), ("row('hello'::text)", ("helloc",))],
index 3809cabdb4a0d05e8393e892468b086bf4e0e240..4feb496be026252c295f0b82e5803b4747e680d6 100644 (file)
@@ -1,8 +1,8 @@
 import pytest
 import psycopg3
 from psycopg3.types import builtins
-from psycopg3.adapt import TypeCaster, UnknownCaster, Format, Transformer
-from psycopg3.types.array import UnknownArrayCaster, ArrayCaster
+from psycopg3.adapt import Format, Transformer
+from psycopg3.types.array import register_array
 
 
 tests_str = [
@@ -100,26 +100,15 @@ def test_cast_list_int(conn, obj, want, fmt_out):
     assert cur.fetchone()[0] == want
 
 
-def test_unknown(conn):
-    # unknown for real
-    assert builtins["aclitem"].array_oid not in TypeCaster.globals
-    TypeCaster.register(
-        builtins["aclitem"].array_oid, UnknownArrayCaster, context=conn
-    )
-    cur = conn.cursor()
-    cur.execute("select '{postgres=arwdDxt/postgres}'::aclitem[]")
-    res = cur.fetchone()[0]
-    assert res == ["postgres=arwdDxt/postgres"]
-
-
 def test_array_register(conn):
+    # unknown for real
     cur = conn.cursor()
     cur.execute("select '{postgres=arwdDxt/postgres}'::aclitem[]")
     res = cur.fetchone()[0]
     assert res == "{postgres=arwdDxt/postgres}"
 
-    ArrayCaster.register(
-        builtins["aclitem"].array_oid, UnknownCaster, context=conn
+    register_array(
+        builtins["aclitem"].array_oid, builtins["aclitem"].oid, context=conn
     )
     cur.execute("select '{postgres=arwdDxt/postgres}'::aclitem[]")
     res = cur.fetchone()[0]