From: Daniele Varrazzo Date: Tue, 7 Apr 2020 12:39:37 +0000 (+1200) Subject: Dropped ArrayCaster, expose register_array function instead X-Git-Tag: 3.0.dev0~595 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=cd4e23a9ac2d88c802741e62e94f390073ee5f34;p=thirdparty%2Fpsycopg.git Dropped ArrayCaster, expose register_array function instead 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. --- diff --git a/psycopg3/types/__init__.py b/psycopg3/types/__init__.py index b07d7f770..7b2556dfd 100644 --- a/psycopg3/types/__init__.py +++ b/psycopg3/types/__init__.py @@ -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"] diff --git a/psycopg3/types/array.py b/psycopg3/types/array.py index f7bab1f26..5da427192 100644 --- a/psycopg3/types/array.py +++ b/psycopg3/types/array.py @@ -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) diff --git a/psycopg3/types/numeric.py b/psycopg3/types/numeric.py index 2d7d6c93b..9ef9dffac 100644 --- a/psycopg3/types/numeric.py +++ b/psycopg3/types/numeric.py @@ -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 diff --git a/psycopg3/types/oids.py b/psycopg3/types/oids.py index 95bdb2a5b..1b211fd65 100644 --- a/psycopg3/types/oids.py +++ b/psycopg3/types/oids.py @@ -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 diff --git a/psycopg3/types/text.py b/psycopg3/types/text.py index adfbf686b..ae3673c87 100644 --- a/psycopg3/types/text.py +++ b/psycopg3/types/text.py @@ -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 diff --git a/tests/test_adapt.py b/tests/test_adapt.py index 2a8107011..23ca1a970 100644 --- a/tests/test_adapt.py +++ b/tests/test_adapt.py @@ -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",))], diff --git a/tests/types/test_array.py b/tests/types/test_array.py index 3809cabdb..4feb496be 100644 --- a/tests/types/test_array.py +++ b/tests/types/test_array.py @@ -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]