From: Daniele Varrazzo Date: Sun, 22 Nov 2020 17:28:36 +0000 (+0000) Subject: Cursor.callproc dropped X-Git-Tag: 3.0.dev0~333 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9369a3b3717c12d4a398fe0934994ad19cbc564a;p=thirdparty%2Fpsycopg.git Cursor.callproc dropped The method has a simplistic semantic which doesn't account for PostgreSQL positional parameters, procedures, set-returning functions. Users can use a normal `Cursor.execute()` with SELECT function_name(...)` or CALL procedure_name(...)` instead. --- diff --git a/docs/from_pg2.rst b/docs/from_pg2.rst index 29eb3865f..5a5f1f2ec 100644 --- a/docs/from_pg2.rst +++ b/docs/from_pg2.rst @@ -75,11 +75,17 @@ See :ref:`copy` for the details. Other differences ----------------- -When the connection is used as context manager, at the end of the context the -connection will be closed. In psycopg2 only the transaction is closed, so a -connection can be used in several contexts, but the behaviour is surprising -for people used to several other Python classes wrapping resources, such as -files. +- When the connection is used as context manager, at the end of the context + the connection will be closed. In psycopg2 only the transaction is closed, + so a connection can be used in several contexts, but the behaviour is + surprising for people used to several other Python classes wrapping + resources, such as files. + +- `cursor.callproc()` is not implemented. The method has a simplistic + semantic which doesn't account for PostgreSQL positional parameters, + procedures, set-returning functions. Use a normal + `~psycopg3.Cursor.execute()` with :sql:`SELECT function_name(...)` or + :sql:`CALL procedure_name(...)` instead. What's new in psycopg3 diff --git a/psycopg3/psycopg3/cursor.py b/psycopg3/psycopg3/cursor.py index c08d1ed33..4a96df26b 100644 --- a/psycopg3/psycopg3/cursor.py +++ b/psycopg3/psycopg3/cursor.py @@ -7,13 +7,12 @@ psycopg3 cursor objects import sys from types import TracebackType from typing import Any, AsyncIterator, Callable, Generic, Iterator, List -from typing import Mapping, Optional, Sequence, Type, TYPE_CHECKING, Union +from typing import Optional, Sequence, Type, TYPE_CHECKING from operator import attrgetter from contextlib import contextmanager from . import errors as e from . import pq -from . import sql from .oids import builtins from .copy import Copy, AsyncCopy from .proto import ConnectionType, Query, Params, DumpersMap, LoadersMap, PQGen @@ -361,55 +360,6 @@ class BaseCursor(Generic[ConnectionType]): "the last operation didn't produce a result" ) - def _callproc_sql( - self, - name: Union[str, sql.Identifier], - args: Optional[Params] = None, - kwargs: Optional[Mapping[str, Any]] = None, - ) -> sql.Composable: - if args and not isinstance(args, (Sequence, Mapping)): - raise TypeError( - f"callproc args should be a sequence or a mapping," - f" got {type(args).__name__}" - ) - if isinstance(args, Mapping) and kwargs: - raise TypeError( - "callproc supports only one args sequence and one kwargs mapping" - ) - - if not kwargs and isinstance(args, Mapping): - kwargs = args - args = None - - if kwargs and not isinstance(kwargs, Mapping): - raise TypeError( - f"callproc kwargs should be a mapping," - f" got {type(kwargs).__name__}" - ) - - qparts: List[sql.Composable] = [ - sql.SQL("select * from "), - name if isinstance(name, sql.Identifier) else sql.Identifier(name), - sql.SQL("("), - ] - - if args: - for i, item in enumerate(args): - if i: - qparts.append(sql.SQL(",")) - qparts.append(sql.Literal(item)) - - if kwargs: - for i, (k, v) in enumerate(kwargs.items()): - if i: - qparts.append(sql.SQL(",")) - qparts.extend( - [sql.Identifier(k), sql.SQL(":="), sql.Literal(v)] - ) - - qparts.append(sql.SQL(")")) - return sql.Composed(qparts) - def _check_copy_results(self, results: Sequence["PGresult"]) -> None: """ Check that the value returned in a copy() operation is a legit COPY. @@ -495,18 +445,6 @@ class Cursor(BaseCursor["Connection"]): return self - def callproc( - self, - name: Union[str, sql.Identifier], - args: Optional[Params] = None, - kwargs: Optional[Mapping[str, Any]] = None, - ) -> Optional[Params]: - """ - Call a stored procedure to the database. - """ - self.execute(self._callproc_sql(name, args)) - return args - def fetchone(self) -> Optional[Sequence[Any]]: """ Return the next record from the current recordset. @@ -642,15 +580,6 @@ class AsyncCursor(BaseCursor["AsyncConnection"]): return self - async def callproc( - self, - name: Union[str, sql.Identifier], - args: Optional[Params] = None, - kwargs: Optional[Mapping[str, Any]] = None, - ) -> Optional[Params]: - await self.execute(self._callproc_sql(name, args, kwargs)) - return args - async def fetchone(self) -> Optional[Sequence[Any]]: self._check_result() rv = self._transformer.load_row(self._pos) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 7b3adfedc..8a0829b47 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -1,10 +1,8 @@ import gc import pytest import weakref -from collections import namedtuple import psycopg3 -from psycopg3 import sql from psycopg3.oids import builtins @@ -194,83 +192,6 @@ def test_executemany_badquery(conn, query): cur.executemany(query, [(10, "hello"), (20, "world")]) -def test_callproc_args(conn): - cur = conn.cursor() - cur.execute( - """ - create function testfunc(a int, b text) returns text[] language sql as - 'select array[$1::text, $2]' - """ - ) - assert cur.callproc("testfunc", [10, "twenty"]) == [10, "twenty"] - assert cur.fetchone() == (["10", "twenty"],) - - -def test_callproc_badparam(conn): - cur = conn.cursor() - with pytest.raises(TypeError): - cur.callproc("lower", 42) - with pytest.raises(TypeError): - cur.callproc(42, ["lower"]) - - -def make_testfunc(conn): - # This parameter name tests for injection and quote escaping - paramname = """Robert'); drop table "students" --""" - procname = "randall" - - # Set up the temporary function - stmt = ( - sql.SQL( - """ - create function {}({} numeric) returns numeric language sql as - 'select $1 * $1' - """ - ) - .format(sql.Identifier(procname), sql.Identifier(paramname)) - .as_string(conn) - .encode(conn.client_encoding) - ) - - # execute regardless of sync/async conn - conn.pgconn.exec_(stmt) - - return namedtuple("Thang", "name, param")(procname, paramname) - - -def test_callproc_dict(conn): - - testfunc = make_testfunc(conn) - cur = conn.cursor() - - cur.callproc(testfunc.name, [2]) - assert cur.fetchone() == (4,) - cur.callproc(testfunc.name, {testfunc.param: 2}) - assert cur.fetchone() == (4,) - cur.callproc(sql.Identifier(testfunc.name), {testfunc.param: 2}) - assert cur.fetchone() == (4,) - - -@pytest.mark.parametrize( - "args, exc", - [ - ({"_p": 2, "foo": "bar"}, psycopg3.ProgrammingError), - ({"_p": "two"}, psycopg3.DataError), - ({"bj\xc3rn": 2}, psycopg3.ProgrammingError), - ({3: 2}, TypeError), - ({(): 2}, TypeError), - ], -) -def test_callproc_dict_bad(conn, args, exc): - testfunc = make_testfunc(conn) - if "_p" in args: - args[testfunc.param] = args.pop("_p") - - cur = conn.cursor() - with pytest.raises(exc): - cur.callproc(testfunc.name, args) - - def test_rowcount(conn): cur = conn.cursor() diff --git a/tests/test_cursor_async.py b/tests/test_cursor_async.py index 7ec01a67e..7603126da 100644 --- a/tests/test_cursor_async.py +++ b/tests/test_cursor_async.py @@ -3,9 +3,6 @@ import pytest import weakref import psycopg3 -from psycopg3 import sql - -from .test_cursor import make_testfunc pytestmark = pytest.mark.asyncio @@ -196,59 +193,6 @@ async def test_executemany_badquery(aconn, query): await cur.executemany(query, [(10, "hello"), (20, "world")]) -async def test_callproc_args(aconn): - cur = await aconn.cursor() - await cur.execute( - """ - create function testfunc(a int, b text) returns text[] language sql as - 'select array[$1::text, $2]' - """ - ) - assert (await cur.callproc("testfunc", [10, "twenty"])) == [10, "twenty"] - assert (await cur.fetchone()) == (["10", "twenty"],) - - -async def test_callproc_badparam(aconn): - cur = await aconn.cursor() - with pytest.raises(TypeError): - await cur.callproc("lower", 42) - with pytest.raises(TypeError): - await cur.callproc(42, ["lower"]) - - -async def test_callproc_dict(aconn): - testfunc = make_testfunc(aconn) - - cur = await aconn.cursor() - - await cur.callproc(testfunc.name, [2]) - assert (await cur.fetchone()) == (4,) - await cur.callproc(testfunc.name, {testfunc.param: 2}) - assert await (cur.fetchone()) == (4,) - await cur.callproc(sql.Identifier(testfunc.name), {testfunc.param: 2}) - assert await (cur.fetchone()) == (4,) - - -@pytest.mark.parametrize( - "args, exc", - [ - ({"_p": 2, "foo": "bar"}, psycopg3.ProgrammingError), - ({"_p": "two"}, psycopg3.DataError), - ({"bj\xc3rn": 2}, psycopg3.ProgrammingError), - ({3: 2}, TypeError), - ({(): 2}, TypeError), - ], -) -async def test_callproc_dict_bad(aconn, args, exc): - testfunc = make_testfunc(aconn) - if "_p" in args: - args[testfunc.param] = args.pop("_p") - - cur = await aconn.cursor() - with pytest.raises(exc): - await cur.callproc(testfunc.name, args) - - async def test_rowcount(aconn): cur = await aconn.cursor()