From: Daniele Varrazzo Date: Fri, 14 May 2021 22:24:24 +0000 (+0200) Subject: Drop Connection.client_encoding attribute X-Git-Tag: 3.0~54^2~1 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=631af96801e20a8485909dc4edfc47398f4a03ed;p=thirdparty%2Fpsycopg.git Drop Connection.client_encoding attribute Expose it as `Connection.info.encoding`, but use pgconn_encoding() internally. --- diff --git a/docs/api/connections.rst b/docs/api/connections.rst index 19d3ae2c5..8c9e6a379 100644 --- a/docs/api/connections.rst +++ b/docs/api/connections.rst @@ -225,34 +225,6 @@ The `!Connection` class .. autoattribute:: info - .. autoattribute:: client_encoding - - The value returned is always normalized to the Python codec - `~codecs.CodecInfo.name`:: - - conn.client_encoding = 'latin9' - conn.client_encoding - 'iso8859-15' - - and it reflects the current connection property, even if it is set - outside Python:: - - conn.execute("SET client_encoding TO LATIN1") - conn.client_encoding - 'iso8859-1' - - A few PostgreSQL encodings are not available in Python and cannot be - selected (currently ``EUC_TW``, ``MULE_INTERNAL``). The PostgreSQL - ``SQL_ASCII`` encoding has the special meaning of "no encoding": see - :ref:`adapt-string` for details. - - .. seealso:: - - The `PostgreSQL supported encodings`__. - - .. __: https://www.postgresql.org/docs/current/multibyte.html - - .. autoattribute:: prepare_threshold See :ref:`prepared-statements` for details. @@ -433,6 +405,26 @@ Connection support objects ``standard_conforming_strings``... See :pq:`PQparameterStatus()` for all the available parameters. + .. autoattribute:: encoding + + The value returned is always normalized to the Python codec + `~codecs.CodecInfo.name`:: + + conn.execute("SET client_encoding TO LATIN9") + conn.info.encoding + 'iso8859-15' + + A few PostgreSQL encodings are not available in Python and cannot be + selected (currently ``EUC_TW``, ``MULE_INTERNAL``). The PostgreSQL + ``SQL_ASCII`` encoding has the special meaning of "no encoding": see + :ref:`adapt-string` for details. + + .. seealso:: + + The `PostgreSQL supported encodings`__. + + .. __: https://www.postgresql.org/docs/current/multibyte.html + .. rubric:: Objects involved in :ref:`transactions` diff --git a/docs/basic/adapt.rst b/docs/basic/adapt.rst index 99b3a2a89..024f22692 100644 --- a/docs/basic/adapt.rst +++ b/docs/basic/adapt.rst @@ -102,11 +102,11 @@ such as :sql:`text` and :sql:`varchar` are converted back to Python `!str`: 'Crème Brûlée at 4.99€' PostgreSQL databases `have an encoding`__, and `the session has an encoding`__ -too, exposed in the `Connection.client_encoding` attribute. If your database -and connection are in UTF-8 encoding you will likely have no problem, -otherwise you will have to make sure that your application only deals with the -non-ASCII chars that the database can handle; failing to do so may result in -encoding/decoding errors: +too, exposed in the `!Connection.info.`\ `~ConnectionInfo.encoding` +attribute. If your database and connection are in UTF-8 encoding you will +likely have no problem, otherwise you will have to make sure that your +application only deals with the non-ASCII chars that the database can handle; +failing to do so may result in encoding/decoding errors: .. __: https://www.postgresql.org/docs/current/sql-createdatabase.html .. __: https://www.postgresql.org/docs/current/multibyte.html @@ -114,17 +114,17 @@ encoding/decoding errors: .. code:: python # The encoding is set at connection time according to the db configuration - conn.client_encoding + conn.info.encoding 'utf-8' # The Latin-9 encoding can manage some European accented letters # and the Euro symbol - conn.client_encoding = 'latin9' + conn.execute("SET client_encoding TO LATIN9") conn.execute("SELECT entry FROM menu WHERE id = 1").fetchone()[0] 'Crème Brûlée at 4.99€' # The Latin-1 encoding doesn't have a representation for the Euro symbol - conn.client_encoding = 'latin1' + conn.execute("SET client_encoding TO LATIN1") conn.execute("SELECT entry FROM menu WHERE id = 1").fetchone()[0] # Traceback (most recent call last) # ... @@ -132,13 +132,12 @@ encoding/decoding errors: # in encoding "UTF8" has no equivalent in encoding "LATIN1" In rare cases you may have strings with unexpected encodings in the database. -Using the ``SQL_ASCII`` client encoding (or setting -`~Connection.client_encoding` ``= "ascii"``) will disable decoding of the data +Using the ``SQL_ASCII`` client encoding will disable decoding of the data coming from the database, which will be returned as `bytes`: .. code:: python - conn.client_encoding = "ascii" + conn.execute("SET client_encoding TO SQL_ASCII") conn.execute("SELECT entry FROM menu WHERE id = 1").fetchone()[0] b'Cr\xc3\xa8me Br\xc3\xbbl\xc3\xa9e at 4.99\xe2\x82\xac' diff --git a/psycopg/psycopg/_column.py b/psycopg/psycopg/_column.py index 69d7c8dcc..60773f9de 100644 --- a/psycopg/psycopg/_column.py +++ b/psycopg/psycopg/_column.py @@ -8,6 +8,7 @@ from typing import Any, NamedTuple, Optional, Sequence, TYPE_CHECKING from operator import attrgetter from . import errors as e +from ._encodings import pgconn_encoding if TYPE_CHECKING: from .cursor import BaseCursor @@ -31,7 +32,7 @@ class Column(Sequence[Any]): if not fname: raise e.InterfaceError(f"no name available for column {index}") - self._name = fname.decode(cursor.connection.client_encoding) + self._name = fname.decode(pgconn_encoding(cursor._pgconn)) self._data = ColumnData( ftype=res.ftype(index), diff --git a/psycopg/psycopg/_encodings.py b/psycopg/psycopg/_encodings.py index 5d92ed4d4..705e1cadf 100644 --- a/psycopg/psycopg/_encodings.py +++ b/psycopg/psycopg/_encodings.py @@ -5,10 +5,13 @@ Mappings between PostgreSQL and Python encodings. # Copyright (C) 2020-2021 The Psycopg Team import codecs -from typing import Dict, Union +from typing import Dict, Union, TYPE_CHECKING from .errors import NotSupportedError +if TYPE_CHECKING: + from .pq.abc import PGconn + _py_codecs = { "BIG5": "big5", "EUC_CN": "gb2312", @@ -63,6 +66,11 @@ py_codecs.update((k.encode(), v) for k, v in _py_codecs.items()) pg_codecs = {v: k.encode() for k, v in _py_codecs.items()} +def pgconn_encoding(pgconn: "PGconn") -> str: + pgenc = pgconn.parameter_status(b"client_encoding") or b"UTF8" + return pg2pyenc(pgenc) + + def py2pgenc(name: str) -> bytes: """Convert a Python encoding name to PostgreSQL encoding name. diff --git a/psycopg/psycopg/_queries.py b/psycopg/psycopg/_queries.py index 2c1f72dcc..a974d599c 100644 --- a/psycopg/psycopg/_queries.py +++ b/psycopg/psycopg/_queries.py @@ -14,6 +14,7 @@ from . import errors as e from .sql import Composable from .abc import Buffer, Query, Params from ._enums import PyFormat +from ._encodings import pgconn_encoding if TYPE_CHECKING: from .abc import Transformer @@ -53,7 +54,7 @@ class PostgresQuery: conn = transformer.connection if conn: - self._encoding = conn.client_encoding + self._encoding = pgconn_encoding(conn.pgconn) def convert(self, query: Query, vars: Optional[Params]) -> None: """ diff --git a/psycopg/psycopg/connection.py b/psycopg/psycopg/connection.py index 8e89250fc..b24d5e307 100644 --- a/psycopg/psycopg/connection.py +++ b/psycopg/psycopg/connection.py @@ -30,7 +30,7 @@ from .cursor import Cursor from ._cmodule import _psycopg from .conninfo import make_conninfo, conninfo_to_dict, ConnectionInfo from .generators import notifies -from ._encodings import pg2pyenc +from ._encodings import pgconn_encoding from ._preparing import PrepareManager from .transaction import Transaction from .server_cursor import ServerCursor @@ -259,12 +259,6 @@ class BaseConnection(Generic[Row]): f"{TransactionStatus(status).name}" ) - @property - def client_encoding(self) -> str: - """The Python codec name of the connection's client encoding.""" - pgenc = self.pgconn.parameter_status(b"client_encoding") or b"UTF8" - return pg2pyenc(pgenc) - @property def info(self) -> ConnectionInfo: """A `ConnectionInfo` attribute to inspect connection properties.""" @@ -313,7 +307,7 @@ class BaseConnection(Generic[Row]): if not (self and self._notice_handler): return - diag = e.Diagnostic(res, self.client_encoding) + diag = e.Diagnostic(res, pgconn_encoding(self.pgconn)) for cb in self._notice_handlers: try: cb(diag) @@ -342,7 +336,7 @@ class BaseConnection(Generic[Row]): if not (self and self._notify_handlers): return - enc = self.client_encoding + enc = pgconn_encoding(self.pgconn) n = Notify(pgn.relname.decode(enc), pgn.extra.decode(enc), pgn.be_pid) for cb in self._notify_handlers: cb(n) @@ -419,7 +413,7 @@ class BaseConnection(Generic[Row]): ) if isinstance(command, str): - command = command.encode(self.client_encoding) + command = command.encode(pgconn_encoding(self.pgconn)) elif isinstance(command, Composable): command = command.as_bytes(self) @@ -434,7 +428,7 @@ class BaseConnection(Generic[Row]): if result.status not in (ExecStatus.COMMAND_OK, ExecStatus.TUPLES_OK): if result.status == ExecStatus.FATAL_ERROR: raise e.error_from_result( - result, encoding=self.client_encoding + result, encoding=pgconn_encoding(self.pgconn) ) else: raise e.InterfaceError( @@ -754,7 +748,7 @@ class Connection(BaseConnection[Row]): while 1: with self.lock: ns = self.wait(notifies(self.pgconn)) - enc = self.client_encoding + enc = pgconn_encoding(self.pgconn) for pgn in ns: n = Notify( pgn.relname.decode(enc), pgn.extra.decode(enc), pgn.be_pid diff --git a/psycopg/psycopg/connection_async.py b/psycopg/psycopg/connection_async.py index 7f6b3a3af..015adf8ee 100644 --- a/psycopg/psycopg/connection_async.py +++ b/psycopg/psycopg/connection_async.py @@ -20,6 +20,7 @@ from .adapt import AdaptersMap from ._enums import IsolationLevel from ._compat import asynccontextmanager from .conninfo import make_conninfo, conninfo_to_dict +from ._encodings import pgconn_encoding from .connection import BaseConnection, CursorRow, Notify from .generators import notifies from .transaction import AsyncTransaction @@ -268,7 +269,7 @@ class AsyncConnection(BaseConnection[Row]): while 1: async with self.lock: ns = await self.wait(notifies(self.pgconn)) - enc = self.client_encoding + enc = pgconn_encoding(self.pgconn) for pgn in ns: n = Notify( pgn.relname.decode(enc), pgn.extra.decode(enc), pgn.be_pid diff --git a/psycopg/psycopg/conninfo.py b/psycopg/psycopg/conninfo.py index af924c7ef..129b05455 100644 --- a/psycopg/psycopg/conninfo.py +++ b/psycopg/psycopg/conninfo.py @@ -12,7 +12,7 @@ from datetime import tzinfo from . import pq from . import errors as e from ._tz import get_tzinfo -from ._encodings import pg2pyenc +from ._encodings import pgconn_encoding def make_conninfo(conninfo: str = "", **kwargs: Any) -> str: @@ -171,7 +171,7 @@ class ConnectionInfo: `~Connection.connect()` or from environment variables. The password is never returned (you can read it using the `password` attribute). """ - pyenc = self._pyenc + pyenc = self.encoding # Get the known defaults to avoid reporting them defaults = { @@ -222,8 +222,8 @@ class ConnectionInfo: Return `None` is the parameter is unknown. """ - res = self.pgconn.parameter_status(param_name.encode(self._pyenc)) - return res.decode(self._pyenc) if res is not None else None + res = self.pgconn.parameter_status(param_name.encode(self.encoding)) + return res.decode(self.encoding) if res is not None else None @property def server_version(self) -> int: @@ -259,11 +259,11 @@ class ConnectionInfo: """The Python timezone info of the connection's timezone.""" return get_tzinfo(self.pgconn) + @property + def encoding(self) -> str: + """The Python codec name of the connection's client encoding.""" + return pgconn_encoding(self.pgconn) + def _get_pgconn_attr(self, name: str) -> str: value: bytes = getattr(self.pgconn, name) - return value.decode(self._pyenc) - - @property - def _pyenc(self) -> str: - pgenc = self.pgconn.parameter_status(b"client_encoding") or b"UTF8" - return pg2pyenc(pgenc) + return value.decode(self.encoding) diff --git a/psycopg/psycopg/copy.py b/psycopg/psycopg/copy.py index b26041a08..572b9699d 100644 --- a/psycopg/psycopg/copy.py +++ b/psycopg/psycopg/copy.py @@ -21,6 +21,7 @@ from .abc import ConnectionType, PQGen, Transformer from .adapt import PyFormat from ._compat import create_task from ._cmodule import _psycopg +from ._encodings import pgconn_encoding from .generators import copy_from, copy_to, copy_end if TYPE_CHECKING: @@ -66,7 +67,7 @@ class BaseCopy(Generic[ConnectionType]): if self._pgresult.binary_tuples == pq.Format.TEXT: self.formatter = TextFormatter( - tx, encoding=self.connection.client_encoding + tx, encoding=pgconn_encoding(self._pgconn) ) else: self.formatter = BinaryFormatter(tx) @@ -149,7 +150,7 @@ class BaseCopy(Generic[ConnectionType]): bmsg: Optional[bytes] if exc: msg = f"error from Python: {type(exc).__qualname__} - {exc}" - bmsg = msg.encode(self.connection.client_encoding, "replace") + bmsg = msg.encode(pgconn_encoding(self._pgconn), "replace") else: bmsg = None diff --git a/psycopg/psycopg/cursor.py b/psycopg/psycopg/cursor.py index 20e35cbe8..58deeaba3 100644 --- a/psycopg/psycopg/cursor.py +++ b/psycopg/psycopg/cursor.py @@ -22,6 +22,7 @@ from .rows import Row, RowMaker, RowFactory from ._column import Column from ._cmodule import _psycopg from ._queries import PostgresQuery +from ._encodings import pgconn_encoding from ._preparing import Prepare if TYPE_CHECKING: @@ -53,6 +54,7 @@ class BaseCursor(Generic[ConnectionType, Row]): _tx: "Transformer" _make_row: RowMaker[Row] + _pgconn: "PGconn" def __init__(self, connection: ConnectionType): self._conn = connection @@ -238,7 +240,7 @@ class BaseCursor(Generic[ConnectionType, Row]): (result,) = yield from execute(self._pgconn) if result.status == ExecStatus.FATAL_ERROR: raise e.error_from_result( - result, encoding=self._conn.client_encoding + result, encoding=pgconn_encoding(self._pgconn) ) self._send_query_prepared(name, pgq, binary=binary) @@ -412,7 +414,7 @@ class BaseCursor(Generic[ConnectionType, Row]): badstats = statuses.difference(self._status_ok) if results[-1].status == ExecStatus.FATAL_ERROR: raise e.error_from_result( - results[-1], encoding=self._conn.client_encoding + results[-1], encoding=pgconn_encoding(self._pgconn) ) elif statuses.intersection(self._status_copy): raise e.ProgrammingError( @@ -457,7 +459,7 @@ class BaseCursor(Generic[ConnectionType, Row]): return elif status == ExecStatus.FATAL_ERROR: raise e.error_from_result( - result, encoding=self._conn.client_encoding + result, encoding=pgconn_encoding(self._pgconn) ) else: raise e.ProgrammingError( diff --git a/psycopg/psycopg/server_cursor.py b/psycopg/psycopg/server_cursor.py index d74ee321a..1485b43ff 100644 --- a/psycopg/psycopg/server_cursor.py +++ b/psycopg/psycopg/server_cursor.py @@ -15,6 +15,7 @@ from .abc import ConnectionType, Query, Params, PQGen from .rows import Row, RowFactory, AsyncRowFactory from .cursor import AnyCursor, BaseCursor, Cursor, execute from .cursor_async import AsyncCursor +from ._encodings import pgconn_encoding if TYPE_CHECKING: from .connection import Connection @@ -83,7 +84,7 @@ class ServerCursorHelper(Generic[ConnectionType, Row]): ) -> PQGen[None]: conn = cur._conn conn.pgconn.send_describe_portal( - self.name.encode(conn.client_encoding) + self.name.encode(pgconn_encoding(conn.pgconn)) ) results = yield from execute(conn.pgconn) cur._execute_results(results, format=self.format) @@ -158,7 +159,7 @@ class ServerCursorHelper(Generic[ConnectionType, Row]): ) -> sql.Composable: if isinstance(query, bytes): - query = query.decode(cur._conn.client_encoding) + query = query.decode(pgconn_encoding(cur._conn.pgconn)) if not isinstance(query, sql.Composable): query = sql.SQL(query) diff --git a/psycopg/psycopg/sql.py b/psycopg/psycopg/sql.py index fa2e3b1a2..f715aab04 100644 --- a/psycopg/psycopg/sql.py +++ b/psycopg/psycopg/sql.py @@ -12,6 +12,7 @@ from typing import Any, Iterator, List, Optional, Sequence, Union from .pq import Escaping from .abc import AdaptContext from .adapt import Transformer, PyFormat +from ._encodings import pgconn_encoding def quote(obj: Any, context: Optional[AdaptContext] = None) -> str: @@ -75,7 +76,7 @@ class Composable(ABC): """ conn = context.connection if context else None - enc = conn.client_encoding if conn else "utf-8" + enc = pgconn_encoding(conn.pgconn) if conn else "utf-8" b = self.as_bytes(context) if isinstance(b, bytes): return b.decode(enc) @@ -207,7 +208,7 @@ class SQL(Composable): if context: conn = context.connection if conn: - enc = conn.client_encoding + enc = pgconn_encoding(conn.pgconn) return self._obj.encode(enc) def format(self, *args: Any, **kwargs: Any) -> Composed: @@ -366,7 +367,7 @@ class Identifier(Composable): if not conn: raise ValueError("a connection is necessary for Identifier") esc = Escaping(conn.pgconn) - enc = conn.client_encoding + enc = pgconn_encoding(conn.pgconn) escs = [esc.escape_identifier(s.encode(enc)) for s in self._obj] return b".".join(escs) @@ -451,7 +452,7 @@ class Placeholder(Composable): def as_bytes(self, context: Optional[AdaptContext]) -> bytes: conn = context.connection if context else None - enc = conn.client_encoding if conn else "utf-8" + enc = pgconn_encoding(conn.pgconn) if conn else "utf-8" return self.as_string(context).encode(enc) diff --git a/psycopg/psycopg/types/string.py b/psycopg/psycopg/types/string.py index 9f8deb079..7dcc98baf 100644 --- a/psycopg/psycopg/types/string.py +++ b/psycopg/psycopg/types/string.py @@ -11,6 +11,7 @@ from ..pq import Format, Escaping from ..abc import AdaptContext from ..adapt import Buffer, Dumper, Loader from ..errors import DataError +from .._encodings import pgconn_encoding if TYPE_CHECKING: from ..pq.abc import Escaping as EscapingProto @@ -25,7 +26,7 @@ class _BaseStrDumper(Dumper): conn = self.connection if conn: - enc = conn.client_encoding + enc = pgconn_encoding(conn.pgconn) if enc != "ascii": self._encoding = enc @@ -85,7 +86,7 @@ class TextLoader(Loader): super().__init__(oid, context) conn = self.connection if conn: - enc = conn.client_encoding + enc = pgconn_encoding(conn.pgconn) self._encoding = enc if enc != "ascii" else "" def load(self, data: Buffer) -> Union[bytes, str]: diff --git a/tests/fix_db.py b/tests/fix_db.py index 3cba37b59..c63e283dd 100644 --- a/tests/fix_db.py +++ b/tests/fix_db.py @@ -112,7 +112,7 @@ def patch_exec(conn, monkeypatch): def _exec_command(command, *args, **kwargs): cmdcopy = command if isinstance(cmdcopy, bytes): - cmdcopy = cmdcopy.decode(conn.client_encoding) + cmdcopy = cmdcopy.decode(conn.info.encoding) elif isinstance(cmdcopy, sql.Composable): cmdcopy = cmdcopy.as_string(conn) diff --git a/tests/test_connection.py b/tests/test_connection.py index f6324d8e2..b75866e91 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -7,7 +7,6 @@ import weakref from threading import Thread import psycopg -from psycopg._encodings import pg2pyenc from psycopg import Connection, Notify from psycopg.rows import tuple_row from psycopg.errors import UndefinedTable @@ -299,52 +298,6 @@ def test_autocommit_unknown(conn): assert not conn.autocommit -def test_get_encoding(conn): - (enc,) = conn.cursor().execute("show client_encoding").fetchone() - assert conn.client_encoding == pg2pyenc(enc) - - -@pytest.mark.parametrize( - "enc, out, codec", - [ - ("utf8", "UTF8", "utf-8"), - ("utf-8", "UTF8", "utf-8"), - ("utf_8", "UTF8", "utf-8"), - ("eucjp", "EUC_JP", "euc_jp"), - ("euc-jp", "EUC_JP", "euc_jp"), - ("latin9", "LATIN9", "iso8859-15"), - ], -) -def test_normalize_encoding(conn, enc, out, codec): - conn.execute("select set_config('client_encoding', %s, false)", [enc]) - assert conn.pgconn.parameter_status(b"client_encoding").decode() == out - assert conn.client_encoding == codec - - -@pytest.mark.parametrize( - "enc, out, codec", - [ - ("utf8", "UTF8", "utf-8"), - ("utf-8", "UTF8", "utf-8"), - ("utf_8", "UTF8", "utf-8"), - ("eucjp", "EUC_JP", "euc_jp"), - ("euc-jp", "EUC_JP", "euc_jp"), - ], -) -def test_encoding_env_var(dsn, monkeypatch, enc, out, codec): - monkeypatch.setenv("PGCLIENTENCODING", enc) - conn = psycopg.connect(dsn) - assert conn.pgconn.parameter_status(b"client_encoding").decode() == out - assert conn.client_encoding == codec - - -def test_set_encoding_unsupported(conn): - cur = conn.cursor() - cur.execute("set client_encoding to EUC_TW") - with pytest.raises(psycopg.NotSupportedError): - cur.execute("select 'x'") - - @pytest.mark.parametrize( "args, kwargs, want", [ diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 4ae40fe63..64b30a928 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -10,7 +10,6 @@ from psycopg import AsyncConnection, Notify from psycopg.rows import tuple_row from psycopg.errors import UndefinedTable from psycopg.conninfo import conninfo_to_dict, make_conninfo -from psycopg._encodings import pg2pyenc from .utils import gc_collect from .test_cursor import my_row_factory @@ -310,56 +309,6 @@ async def test_autocommit_unknown(aconn): assert not aconn.autocommit -async def test_get_encoding(aconn): - cur = aconn.cursor() - await cur.execute("show client_encoding") - (enc,) = await cur.fetchone() - assert aconn.client_encoding == pg2pyenc(enc) - - -@pytest.mark.parametrize( - "enc, out, codec", - [ - ("utf8", "UTF8", "utf-8"), - ("utf-8", "UTF8", "utf-8"), - ("utf_8", "UTF8", "utf-8"), - ("eucjp", "EUC_JP", "euc_jp"), - ("euc-jp", "EUC_JP", "euc_jp"), - ("latin9", "LATIN9", "iso8859-15"), - ], -) -async def test_normalize_encoding(aconn, enc, out, codec): - await aconn.execute( - "select set_config('client_encoding', %s, false)", [enc] - ) - assert aconn.pgconn.parameter_status(b"client_encoding").decode() == out - assert aconn.client_encoding == codec - - -@pytest.mark.parametrize( - "enc, out, codec", - [ - ("utf8", "UTF8", "utf-8"), - ("utf-8", "UTF8", "utf-8"), - ("utf_8", "UTF8", "utf-8"), - ("eucjp", "EUC_JP", "euc_jp"), - ("euc-jp", "EUC_JP", "euc_jp"), - ], -) -async def test_encoding_env_var(dsn, monkeypatch, enc, out, codec): - monkeypatch.setenv("PGCLIENTENCODING", enc) - aconn = await psycopg.AsyncConnection.connect(dsn) - assert aconn.pgconn.parameter_status(b"client_encoding").decode() == out - assert aconn.client_encoding == codec - - -async def test_set_encoding_unsupported(aconn): - cur = aconn.cursor() - await cur.execute("set client_encoding to EUC_TW") - with pytest.raises(psycopg.NotSupportedError): - await cur.execute("select 'x'") - - @pytest.mark.parametrize( "args, kwargs, want", [ diff --git a/tests/test_conninfo.py b/tests/test_conninfo.py index 8410aad2f..c210a3b37 100644 --- a/tests/test_conninfo.py +++ b/tests/test_conninfo.py @@ -6,6 +6,7 @@ import pytest import psycopg from psycopg import ProgrammingError from psycopg.conninfo import make_conninfo, conninfo_to_dict, ConnectionInfo +from psycopg._encodings import pg2pyenc snowman = "\u2603" @@ -245,3 +246,45 @@ class TestConnectionInfo: conn.info.timezone assert len(caplog.records) == 2 assert "FOOBAAR0" in caplog.records[1].message + + def test_encoding(self, conn): + enc = conn.execute("show client_encoding").fetchone()[0] + assert conn.info.encoding == pg2pyenc(enc) + + @pytest.mark.parametrize( + "enc, out, codec", + [ + ("utf8", "UTF8", "utf-8"), + ("utf-8", "UTF8", "utf-8"), + ("utf_8", "UTF8", "utf-8"), + ("eucjp", "EUC_JP", "euc_jp"), + ("euc-jp", "EUC_JP", "euc_jp"), + ("latin9", "LATIN9", "iso8859-15"), + ], + ) + def test_normalize_encoding(self, conn, enc, out, codec): + conn.execute("select set_config('client_encoding', %s, false)", [enc]) + assert conn.info.parameter_status("client_encoding") == out + assert conn.info.encoding == codec + + @pytest.mark.parametrize( + "enc, out, codec", + [ + ("utf8", "UTF8", "utf-8"), + ("utf-8", "UTF8", "utf-8"), + ("utf_8", "UTF8", "utf-8"), + ("eucjp", "EUC_JP", "euc_jp"), + ("euc-jp", "EUC_JP", "euc_jp"), + ], + ) + def test_encoding_env_var(self, dsn, monkeypatch, enc, out, codec): + monkeypatch.setenv("PGCLIENTENCODING", enc) + conn = psycopg.connect(dsn) + assert conn.info.parameter_status("client_encoding") == out + assert conn.info.encoding == codec + + def test_set_encoding_unsupported(self, conn): + cur = conn.cursor() + cur.execute("set client_encoding to EUC_TW") + with pytest.raises(psycopg.NotSupportedError): + cur.execute("select 'x'")