From: Daniele Varrazzo Date: Sat, 28 Aug 2021 03:21:20 +0000 (+0200) Subject: Register array dumpers for oid lookups X-Git-Tag: 3.0.beta1~24 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=5f02a46ef8d9afe5d2a535cb3b016e470da6ca38;p=thirdparty%2Fpsycopg.git Register array dumpers for oid lookups This allows, among other things, to dump composite types containing array in binary. --- diff --git a/psycopg/psycopg/_adapters_map.py b/psycopg/psycopg/_adapters_map.py index 4e301bec8..4fdcc9c0c 100644 --- a/psycopg/psycopg/_adapters_map.py +++ b/psycopg/psycopg/_adapters_map.py @@ -105,7 +105,7 @@ class AdaptersMap: return None def register_dumper( - self, cls: Union[type, str], dumper: Type[Dumper] + self, cls: Union[type, str, None], dumper: Type[Dumper] ) -> None: """ Configure the context to use *dumper* to convert object of type *cls*. @@ -123,7 +123,7 @@ class AdaptersMap: case it should be the fully qualified name of the object (e.g. ``"uuid.UUID"``). """ - if not isinstance(cls, (str, type)): + if not (cls is None or isinstance(cls, (str, type))): raise TypeError( f"dumpers should be registered on classes, got {cls} instead" ) @@ -133,12 +133,13 @@ class AdaptersMap: # Register the dumper both as its format and as auto # so that the last dumper registered is used in auto (%s) format - for fmt in (PyFormat.from_pq(dumper.format), PyFormat.AUTO): - if not self._own_dumpers[fmt]: - self._dumpers[fmt] = self._dumpers[fmt].copy() - self._own_dumpers[fmt] = True + if cls: + for fmt in (PyFormat.from_pq(dumper.format), PyFormat.AUTO): + if not self._own_dumpers[fmt]: + self._dumpers[fmt] = self._dumpers[fmt].copy() + self._own_dumpers[fmt] = True - self._dumpers[fmt][cls] = dumper + self._dumpers[fmt][cls] = dumper # Register the dumper by oid, if the oid of the dumper is fixed if dumper.oid: diff --git a/psycopg/psycopg/types/array.py b/psycopg/psycopg/types/array.py index b58cb520a..3238a59f5 100644 --- a/psycopg/psycopg/types/array.py +++ b/psycopg/psycopg/types/array.py @@ -35,9 +35,16 @@ TEXT_ARRAY_OID = postgres.types["text"].array_oid class BaseListDumper(RecursiveDumper): + element_oid = 0 + def __init__(self, cls: type, context: Optional[AdaptContext] = None): super().__init__(cls, context) self.sub_dumper: Optional[Dumper] = None + if self.element_oid and context: + sdclass = context.adapters.get_dumper_by_oid( + self.element_oid, self.format + ) + self.sub_dumper = sdclass(type(None), context) def _find_list_element(self, L: List[Any]) -> Any: """ @@ -417,23 +424,45 @@ class ArrayBinaryLoader(BaseArrayLoader): def register_array( info: TypeInfo, context: Optional[AdaptContext] = None ) -> None: + if not info.array_oid: + raise ValueError(f"the type info {info} doesn't describe an array") + adapters = context.adapters if context else postgres.adapters - base: Type[BaseArrayLoader] = ArrayLoader - lname = f"{info.name.title()}{base.__name__}" + base: Type = ArrayLoader + name = f"{info.name.title()}{base.__name__}" attribs = { "base_oid": info.oid, "delimiter": info.delimiter.encode("utf-8"), } - loader = type(lname, (base,), attribs) + loader = type(name, (base,), attribs) adapters.register_loader(info.array_oid, loader) base = ArrayBinaryLoader - lname = f"{info.name.title()}{base.__name__}" + name = f"{info.name.title()}{base.__name__}" attribs = {"base_oid": info.oid} - loader = type(lname, (base,), attribs) + loader = type(name, (base,), attribs) adapters.register_loader(info.array_oid, loader) + base = ListDumper + name = f"{info.name.title()}{base.__name__}" + attribs = { + "oid": info.array_oid, + "element_oid": info.oid, + "delimiter": info.delimiter.encode("utf-8"), + } + dumper = type(name, (base,), attribs) + adapters.register_dumper(None, dumper) + + base = ListBinaryDumper + name = f"{info.name.title()}{base.__name__}" + attribs = { + "oid": info.array_oid, + "element_oid": info.oid, + } + dumper = type(name, (base,), attribs) + adapters.register_dumper(None, dumper) + def register_default_adapters(context: AdaptContext) -> None: # The text dumper is more flexible as it can handle lists of mixed type, diff --git a/tests/types/test_composite.py b/tests/types/test_composite.py index 8b0e97d5c..2c314e9cf 100644 --- a/tests/types/test_composite.py +++ b/tests/types/test_composite.py @@ -4,6 +4,7 @@ from psycopg import pq, postgres from psycopg.sql import Identifier from psycopg.adapt import PyFormat as Format from psycopg.postgres import types as builtins +from psycopg.types.range import Range from psycopg.types.composite import CompositeInfo, register_composite from psycopg.types.composite import TupleDumper, TupleBinaryDumper @@ -63,6 +64,25 @@ def test_load_all_chars(conn, fmt_out): assert res == (s,) +@pytest.mark.parametrize("fmt_in", [Format.AUTO, Format.TEXT, Format.BINARY]) +def test_dump_builtin_empty_range(conn, fmt_in): + conn.execute( + """ + drop type if exists tmptype; + create type tmptype as (num integer, range daterange, nums integer[]) + """ + ) + info = CompositeInfo.fetch(conn, "tmptype") + register_composite(info, conn) + + cur = conn.execute( + f"select pg_typeof(%{fmt_in})", + [info.python_type(10, Range(empty=True), [])], + ) + print(cur._query.params[0]) + assert cur.fetchone()[0] == "tmptype" + + @pytest.mark.parametrize( "rec, want", [