""" Entry point into the adaptation system. """ # Copyright (C) 2020 The Psycopg Team from abc import ABC, abstractmethod from typing import Any, Optional, Type, TYPE_CHECKING from . import pq, abc from . import _adapters_map from ._enums import PyFormat as PyFormat from ._cmodule import _psycopg if TYPE_CHECKING: from .connection import BaseConnection AdaptersMap = _adapters_map.AdaptersMap Buffer = abc.Buffer ORD_BS = ord("\\") class Dumper(abc.Dumper, ABC): """ Convert Python object of the type `!cls` to PostgreSQL representation. """ oid: int = 0 """The oid to pass to the server, if known.""" format: pq.Format = pq.Format.TEXT """The format of the data dumped.""" def __init__(self, cls: type, context: Optional[abc.AdaptContext] = None): self.cls = cls self.connection: Optional["BaseConnection[Any]"] = ( context.connection if context else None ) def __repr__(self) -> str: return ( f"<{type(self).__module__}.{type(self).__qualname__}" f" (oid={self.oid}) at 0x{id(self):x}>" ) @abstractmethod def dump(self, obj: Any) -> Buffer: ... def quote(self, obj: Any) -> Buffer: """ By default return the `dump()` value quoted and sanitised, so that the result can be used to build a SQL string. This works well for most types and you won't likely have to implement this method in a subclass. """ value = self.dump(obj) if self.connection: esc = pq.Escaping(self.connection.pgconn) # escaping and quoting return esc.escape_literal(value) # This path is taken when quote is asked without a connection, # usually it means by psycopg.sql.quote() or by # 'Composible.as_string(None)'. Most often than not this is done by # someone generating a SQL file to consume elsewhere. # No quoting, only quote escaping, random bs escaping. See further. esc = pq.Escaping() out = esc.escape_string(value) # b"\\" in memoryview doesn't work so search for the ascii value if ORD_BS not in out: # If the string has no backslash, the result is correct and we # don't need to bother with standard_conforming_strings. return b"'" + out + b"'" # The libpq has a crazy behaviour: PQescapeString uses the last # standard_conforming_strings setting seen on a connection. This # means that backslashes might be escaped or might not. # # A syntax E'\\' works everywhere, whereas E'\' is an error. OTOH, # if scs is off, '\\' raises a warning and '\' is an error. # # Check what the libpq does, and if it doesn't escape the backslash # let's do it on our own. Never mind the race condition. rv: bytes = b" E'" + out + b"'" if esc.escape_string(b"\\") == b"\\": rv = rv.replace(b"\\", b"\\\\") return rv def get_key(self, obj: Any, format: PyFormat) -> abc.DumperKey: """ Implementation of the `~psycopg.abc.Dumper.get_key()` member of the `~psycopg.abc.Dumper` protocol. Look at its definition for details. This implementation returns the `!cls` passed in the constructor. Subclasses needing to specialise the PostgreSQL type according to the *value* of the object dumped (not only according to to its type) should override this class. """ return self.cls def upgrade(self, obj: Any, format: PyFormat) -> "Dumper": """ Implementation of the `~psycopg.abc.Dumper.upgrade()` member of the `~psycopg.abc.Dumper` protocol. Look at its definition for details. This implementation just returns `!self`. If a subclass implements `get_key()` it should probably override `!upgrade()` too. """ return self class Loader(abc.Loader, ABC): """ Convert PostgreSQL values with type OID `!oid` to Python objects. """ format: pq.Format = pq.Format.TEXT """The format of the data loaded.""" def __init__(self, oid: int, context: Optional[abc.AdaptContext] = None): self.oid = oid self.connection: Optional["BaseConnection[Any]"] = ( context.connection if context else None ) @abstractmethod def load(self, data: Buffer) -> Any: """Convert a PostgreSQL value to a Python object.""" ... Transformer: Type["abc.Transformer"] # Override it with fast object if available if _psycopg: Transformer = _psycopg.Transformer else: from . import _transform Transformer = _transform.Transformer class RecursiveDumper(Dumper): """Dumper with a transformer to help dumping recursive types.""" def __init__(self, cls: type, context: Optional[abc.AdaptContext] = None): super().__init__(cls, context) self._tx = Transformer.from_context(context) class RecursiveLoader(Loader): """Loader with a transformer to help loading recursive types.""" def __init__(self, oid: int, context: Optional[abc.AdaptContext] = None): super().__init__(oid, context) self._tx = Transformer.from_context(context)