From: Nick Pope Date: Tue, 28 Nov 2023 12:46:59 +0000 (+0000) Subject: chore: add support for PyPy. X-Git-Tag: 3.2.0~124^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=094c8cf2b74c2431a9a478e7e2b23bfc20f48e9e;p=thirdparty%2Fpsycopg.git chore: add support for PyPy. --- diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a1195fe27..0e9af93bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,6 +48,10 @@ jobs: # Test with minimum dependencies versions - {impl: c, python: "3.8", ext: min, postgres: "postgres:15"} + # Test with PyPy. + - {impl: python, python: "pypy3.9", postgres: "postgres:13"} + - {impl: python, python: "pypy3.10", postgres: "postgres:14"} + env: PSYCOPG_IMPL: ${{ matrix.impl }} DEPS: ./psycopg[test] ./psycopg_pool @@ -101,6 +105,11 @@ jobs: echo "DEPS=$DEPS numpy" >> $GITHUB_ENV echo "MARKERS=$MARKERS numpy" >> $GITHUB_ENV + - name: Exclude certain tests from pypy + if: ${{ startsWith(matrix.python, 'pypy') }} + run: | + echo "NOT_MARKERS=$NOT_MARKERS timing" >> $GITHUB_ENV + - name: Configure to use the oldest dependencies if: ${{ matrix.ext == 'min' }} run: | diff --git a/docs/basic/install.rst b/docs/basic/install.rst index f09fbc637..13719d404 100644 --- a/docs/basic/install.rst +++ b/docs/basic/install.rst @@ -6,7 +6,7 @@ Installation In short, if you use a :ref:`supported system`:: pip install --upgrade pip # upgrade pip to at least 20.3 - pip install "psycopg[binary]" + pip install "psycopg[binary]" # remove [binary] for PyPy and you should be :ref:`ready to start `. Read further for alternative ways to install. @@ -27,6 +27,10 @@ The Psycopg version documented here has *official and tested* support for: - Python 3.6 supported before Psycopg 3.1 - Python 3.7 supported before Psycopg 3.2 +- PyPy: from version 3.9 to 3.10 + + - **Note:** Only the pure Python version is supported. + - PostgreSQL: from version 10 to 16 - OS: Linux, macOS, Windows @@ -76,6 +80,10 @@ installation ` or a :ref:`pure Python installation For further information about the differences between the packages see :ref:`pq-impl`. +.. warning:: + + The binary installation is not supported by PyPy. + .. _local-installation: @@ -103,6 +111,10 @@ If your build prerequisites are in place you can run:: pip install "psycopg[c]" +.. warning:: + + The local installation is not supported by PyPy. + .. _pure-python-installation: diff --git a/docs/news.rst b/docs/news.rst index 38be61432..de009ffce 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -34,6 +34,7 @@ Psycopg 3.1.14 (unreleased) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Fix :ref:`interaction with gevent ` (:ticket:`#527`). +- Add support for PyPy (:ticket:`#686`). .. _gevent: https://www.gevent.org/ diff --git a/psycopg/psycopg/pq/pq_ctypes.py b/psycopg/psycopg/pq/pq_ctypes.py index 5f32031ca..f04a80367 100644 --- a/psycopg/psycopg/pq/pq_ctypes.py +++ b/psycopg/psycopg/pq/pq_ctypes.py @@ -47,7 +47,10 @@ def version() -> int: @impl.PQnoticeReceiver # type: ignore def notice_receiver(arg: c_void_p, result_ptr: impl.PGresult_struct) -> None: - pgconn = cast(arg, POINTER(py_object)).contents.value() + pgconn = cast(arg, POINTER(py_object)).contents.value + if callable(pgconn): # Not a weak reference on PyPy. + pgconn = pgconn() + if not (pgconn and pgconn.notice_handler): return diff --git a/psycopg/setup.cfg b/psycopg/setup.cfg index 900418f57..891b53bfc 100644 --- a/psycopg/setup.cfg +++ b/psycopg/setup.cfg @@ -38,6 +38,8 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy Topic :: Database Topic :: Database :: Front-Ends Topic :: Software Development @@ -58,9 +60,9 @@ install_requires = [options.extras_require] c = - psycopg-c == 3.2.0.dev1 + psycopg-c == 3.2.0.dev1; implementation_name != "pypy" binary = - psycopg-binary == 3.2.0.dev1 + psycopg-binary == 3.2.0.dev1; implementation_name != "pypy" pool = psycopg-pool test = diff --git a/psycopg_c/setup.cfg b/psycopg_c/setup.cfg index 730596f4d..87bc8a39a 100644 --- a/psycopg_c/setup.cfg +++ b/psycopg_c/setup.cfg @@ -22,12 +22,14 @@ classifiers = Operating System :: MacOS :: MacOS X Operating System :: Microsoft :: Windows Operating System :: POSIX + Programming Language :: Cython Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: Implementation :: CPython Topic :: Database Topic :: Database :: Front-Ends Topic :: Software Development diff --git a/psycopg_pool/setup.cfg b/psycopg_pool/setup.cfg index feafbae2a..f14f31e9b 100644 --- a/psycopg_pool/setup.cfg +++ b/psycopg_pool/setup.cfg @@ -32,6 +32,8 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy Topic :: Database Topic :: Database :: Front-Ends Topic :: Software Development diff --git a/tests/fix_pq.py b/tests/fix_pq.py index 6811a26c3..917dfc919 100644 --- a/tests/fix_pq.py +++ b/tests/fix_pq.py @@ -53,7 +53,7 @@ def libpq(): # Not available when testing the binary package libname = find_libpq_full_path() assert libname, "libpq libname not found" - return ctypes.pydll.LoadLibrary(libname) + return ctypes.cdll.LoadLibrary(libname) except Exception as e: if pq.__impl__ == "binary": pytest.skip(f"can't load libpq for testing: {e}") diff --git a/tests/pool/test_pool.py b/tests/pool/test_pool.py index 6323420e2..11a4cf30d 100644 --- a/tests/pool/test_pool.py +++ b/tests/pool/test_pool.py @@ -12,7 +12,7 @@ import psycopg from psycopg.pq import TransactionStatus from psycopg.rows import class_row, Row, TupleRow -from ..utils import assert_type, Counter, set_autocommit +from ..utils import assert_type, Counter, gc_collect, set_autocommit from ..acompat import Event, spawn, gather, sleep, skip_sync from .test_pool_common import delay_connection @@ -367,6 +367,7 @@ def test_del_no_warning(dsn, recwarn): p.wait() ref = weakref.ref(p) del p + gc_collect() assert not ref() assert not recwarn, [str(w.message) for w in recwarn.list] diff --git a/tests/pool/test_pool_async.py b/tests/pool/test_pool_async.py index 7319e00b4..0d6dbdfd0 100644 --- a/tests/pool/test_pool_async.py +++ b/tests/pool/test_pool_async.py @@ -9,7 +9,7 @@ import psycopg from psycopg.pq import TransactionStatus from psycopg.rows import class_row, Row, TupleRow -from ..utils import assert_type, Counter, set_autocommit +from ..utils import assert_type, Counter, gc_collect, set_autocommit from ..acompat import AEvent, spawn, gather, asleep, skip_sync from .test_pool_common_async import delay_connection @@ -371,6 +371,7 @@ async def test_del_no_warning(dsn, recwarn): await p.wait() ref = weakref.ref(p) del p + gc_collect() assert not ref() assert not recwarn, [str(w.message) for w in recwarn.list] diff --git a/tests/pool/test_pool_common.py b/tests/pool/test_pool_common.py index ee8d03ffd..9165eb785 100644 --- a/tests/pool/test_pool_common.py +++ b/tests/pool/test_pool_common.py @@ -9,7 +9,7 @@ import pytest import psycopg -from ..utils import set_autocommit +from ..utils import gc_collect, set_autocommit from ..acompat import Event, spawn, gather, sleep, is_alive, skip_async, skip_sync try: @@ -352,6 +352,7 @@ def test_del_stops_threads(pool_cls, dsn): assert p._sched_runner is not None ts = [p._sched_runner] + p._workers del p + gc_collect() sleep(0.1) for t in ts: assert not is_alive(t), t diff --git a/tests/pool/test_pool_common_async.py b/tests/pool/test_pool_common_async.py index dbcfbbe46..e1b7a2584 100644 --- a/tests/pool/test_pool_common_async.py +++ b/tests/pool/test_pool_common_async.py @@ -6,7 +6,7 @@ import pytest import psycopg -from ..utils import set_autocommit +from ..utils import gc_collect, set_autocommit from ..acompat import AEvent, spawn, gather, asleep, is_alive, skip_async, skip_sync try: @@ -369,6 +369,7 @@ async def test_del_stops_threads(pool_cls, dsn): assert p._sched_runner is not None ts = [p._sched_runner] + p._workers del p + gc_collect() await asleep(0.1) for t in ts: assert not is_alive(t), t diff --git a/tests/test_connection.py b/tests/test_connection.py index c72fd7d06..ef066653f 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -145,11 +145,13 @@ def test_connection_warn_close(conn_cls, dsn, recwarn): conn = conn_cls.connect(dsn) del conn + gc_collect() assert "IDLE" in str(recwarn.pop(ResourceWarning).message) conn = conn_cls.connect(dsn) conn.execute("select 1") del conn + gc_collect() assert "INTRANS" in str(recwarn.pop(ResourceWarning).message) conn = conn_cls.connect(dsn) diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index e61307e31..d0110a01a 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -142,11 +142,13 @@ async def test_connection_warn_close(aconn_cls, dsn, recwarn): conn = await aconn_cls.connect(dsn) del conn + gc_collect() assert "IDLE" in str(recwarn.pop(ResourceWarning).message) conn = await aconn_cls.connect(dsn) await conn.execute("select 1") del conn + gc_collect() assert "INTRANS" in str(recwarn.pop(ResourceWarning).message) conn = await aconn_cls.connect(dsn) diff --git a/tests/test_copy.py b/tests/test_copy.py index 4b3e182e6..25d15f2f8 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -3,6 +3,7 @@ # DO NOT CHANGE! Change the original file instead. import string import hashlib +import sys from io import BytesIO, StringIO from random import choice, randrange from itertools import cycle @@ -673,6 +674,9 @@ def test_connection_writer(conn, format, buffer): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)] ) @@ -728,6 +732,9 @@ def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)] ) diff --git a/tests/test_copy_async.py b/tests/test_copy_async.py index 68c21d8ef..807d94bd5 100644 --- a/tests/test_copy_async.py +++ b/tests/test_copy_async.py @@ -1,5 +1,6 @@ import string import hashlib +import sys from io import BytesIO, StringIO from random import choice, randrange from itertools import cycle @@ -690,6 +691,9 @@ async def test_connection_writer(aconn, format, buffer): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)], @@ -746,6 +750,9 @@ async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize( "fmt, set_types", [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)], diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 86d2fd7ef..845b0941c 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -7,6 +7,7 @@ Tests for psycopg.Cursor that are not supposed to pass for subclasses. import pytest import psycopg +import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat @@ -65,6 +66,9 @@ def test_query_params_executemany(conn): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) diff --git a/tests/test_cursor_async.py b/tests/test_cursor_async.py index 1ebc827cd..00e10883c 100644 --- a/tests/test_cursor_async.py +++ b/tests/test_cursor_async.py @@ -4,6 +4,7 @@ Tests for psycopg.Cursor that are not supposed to pass for subclasses. import pytest import psycopg +import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat @@ -64,6 +65,9 @@ async def test_query_params_executemany(aconn): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) diff --git a/tests/test_cursor_client.py b/tests/test_cursor_client.py index bac6567ca..a0284d17a 100644 --- a/tests/test_cursor_client.py +++ b/tests/test_cursor_client.py @@ -5,6 +5,7 @@ import datetime as dt import pytest import psycopg +import sys from psycopg import rows from .utils import gc_collect, gc_count @@ -78,6 +79,9 @@ def test_query_params_executemany(conn): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) def test_leak(conn_cls, dsn, faker, fetch, row_factory): diff --git a/tests/test_cursor_client_async.py b/tests/test_cursor_client_async.py index d1abbe849..a3ba12ae6 100644 --- a/tests/test_cursor_client_async.py +++ b/tests/test_cursor_client_async.py @@ -2,6 +2,7 @@ import datetime as dt import pytest import psycopg +import sys from psycopg import rows from .utils import gc_collect, gc_count @@ -77,6 +78,9 @@ async def test_query_params_executemany(aconn): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) @pytest.mark.parametrize("row_factory", ["tuple_row", "dict_row", "namedtuple_row"]) async def test_leak(aconn_cls, dsn, faker, fetch, row_factory): diff --git a/tests/test_cursor_raw.py b/tests/test_cursor_raw.py index 683aa6446..76726aa8d 100644 --- a/tests/test_cursor_raw.py +++ b/tests/test_cursor_raw.py @@ -3,6 +3,7 @@ # DO NOT CHANGE! Change the original file instead. import pytest import psycopg +import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat @@ -71,6 +72,9 @@ def test_query_params_executemany(conn): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) diff --git a/tests/test_cursor_raw_async.py b/tests/test_cursor_raw_async.py index b207b28c8..24d69cebb 100644 --- a/tests/test_cursor_raw_async.py +++ b/tests/test_cursor_raw_async.py @@ -1,5 +1,6 @@ import pytest import psycopg +import sys from psycopg import pq, rows, errors as e from psycopg.adapt import PyFormat @@ -68,6 +69,9 @@ async def test_query_params_executemany(aconn): @pytest.mark.slow +@pytest.mark.skipif( + sys.implementation.name == "pypy", reason="depends on refcount semantics" +) @pytest.mark.parametrize("fmt", PyFormat) @pytest.mark.parametrize("fmt_out", pq.Format) @pytest.mark.parametrize("fetch", ["one", "many", "all", "iter"]) diff --git a/tests/test_cursor_server.py b/tests/test_cursor_server.py index 03dc7af53..9fdfdbda7 100644 --- a/tests/test_cursor_server.py +++ b/tests/test_cursor_server.py @@ -7,6 +7,7 @@ import psycopg from psycopg import rows, errors as e from psycopg.pq import Format +from .utils import gc_collect pytestmark = pytest.mark.crdb_skip("server-side cursor") @@ -260,6 +261,7 @@ def test_warn_close(conn, recwarn): cur = conn.cursor("foo") cur.execute("select generate_series(1, 10) as bar") del cur + gc_collect() assert ".close()" in str(recwarn.pop(ResourceWarning).message) diff --git a/tests/test_cursor_server_async.py b/tests/test_cursor_server_async.py index 7317c52d8..0417ab3c3 100644 --- a/tests/test_cursor_server_async.py +++ b/tests/test_cursor_server_async.py @@ -5,6 +5,7 @@ from psycopg import rows, errors as e from psycopg.pq import Format from .acompat import alist +from .utils import gc_collect pytestmark = pytest.mark.crdb_skip("server-side cursor") @@ -266,6 +267,7 @@ async def test_warn_close(aconn, recwarn): cur = aconn.cursor("foo") await cur.execute("select generate_series(1, 10) as bar") del cur + gc_collect() assert ".close()" in str(recwarn.pop(ResourceWarning).message)