]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
chore: add support for PyPy.
authorNick Pope <nick.pope@infogrid.io>
Tue, 28 Nov 2023 12:46:59 +0000 (12:46 +0000)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sat, 2 Dec 2023 01:33:20 +0000 (02:33 +0100)
29 files changed:
.github/workflows/tests.yml
docs/basic/install.rst
docs/news.rst
psycopg/psycopg/pq/pq_ctypes.py
psycopg/setup.cfg
psycopg/setup.py
psycopg_c/setup.cfg
psycopg_pool/setup.cfg
tests/conftest.py
tests/crdb/test_copy.py
tests/crdb/test_copy_async.py
tests/fix_pq.py
tests/pool/test_null_pool.py
tests/pool/test_pool.py
tests/pool/test_pool_async_noasyncio.py
tests/pq/test_pgconn.py
tests/test_client_cursor.py
tests/test_client_cursor_async.py
tests/test_connection.py
tests/test_connection_async.py
tests/test_copy.py
tests/test_copy_async.py
tests/test_cursor.py
tests/test_cursor_async.py
tests/test_errors.py
tests/test_server_cursor.py
tests/test_server_cursor_async.py
tests/types/test_array.py
tests/utils.py

index ac293782fcde4ef64c3d3ed1787081d470f7be5d..93a2138f233aed6c75b164b9e365897857115d33 100644 (file)
@@ -48,6 +48,10 @@ jobs:
           # Test with minimum dependencies versions
           - {impl: c, python: "3.7", 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
@@ -96,6 +100,11 @@ jobs:
           echo "DEPS=$DEPS shapely" >> $GITHUB_ENV
           echo "MARKERS=$MARKERS postgis" >> $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: |
index 120c08f4ac46ec428682ea321274b99c7691f121..7881f3b66dd25f9db50dd149e3035de20a4e90ce 100644 (file)
@@ -6,7 +6,7 @@ Installation
 In short, if you use a :ref:`supported system<supported-systems>`::
 
     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 <module-usage>`. Read further for
 alternative ways to install.
@@ -23,6 +23,10 @@ The Psycopg version documented here has *official and tested* support for:
 
   - Python 3.6 supported before Psycopg 3.1
 
+- 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
 
@@ -72,6 +76,10 @@ installation <local-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:
 
@@ -99,6 +107,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:
 
index 814c244bc35e314ff75f88287ba6f29baec78064..5344e32694fd1cb47025d09dd6973552d9872f96 100644 (file)
@@ -14,6 +14,7 @@ Psycopg 3.1.14 (unreleased)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 - Fix :ref:`interaction with gevent <gevent>` (:ticket:`#527`).
+- Add support for PyPy (:ticket:`#686`).
 
 .. _gevent: https://www.gevent.org/
 
index fdf2ad8a87242ae0cec44e57d12d4fe053a415d5..96c586174e9c957a6adc67b16aabea60aed69d96 100644 (file)
@@ -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
 
index 536447e43516386b7088e76552fa6a55bfc8e682..ece28036f35b6c8aa43ec1c7a3be62f7974add28 100644 (file)
@@ -28,6 +28,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
index c699c872b2d714f3b9473e2570dc17800e76bb29..5cc6d1a8ef639b056452996e8d2e0b27e82848db 100644 (file)
@@ -25,11 +25,11 @@ with open("psycopg/version.py") as f:
 extras_require = {
     # Install the C extension module (requires dev tools)
     "c": [
-        f"psycopg-c == {version}",
+        f"psycopg-c == {version}; implementation_name != 'pypy'",
     ],
     # Install the stand-alone C extension module
     "binary": [
-        f"psycopg-binary == {version}",
+        f"psycopg-binary == {version}; implementation_name != 'pypy'",
     ],
     # Install the connection pool
     "pool": [
index e6875dcd8abf4a5c78e6f00a10ccc44a40668da1..ccd8936cfaa22577032fce6bf1ce43b8ba9b9389 100644 (file)
@@ -21,6 +21,7 @@ classifiers =
     Operating System :: MacOS :: MacOS X
     Operating System :: Microsoft :: Windows
     Operating System :: POSIX
+    Programming Language :: Cython
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
@@ -28,6 +29,7 @@ classifiers =
     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
index 48380bdb5e5e1c67cf1b92402d870bd63f165060..bc04073d638c5d0c975eb7c7bdedb624981db71b 100644 (file)
@@ -28,6 +28,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
index de2b24fc751664e0f62815d7c710c5e61688a8c6..f5d1623571fc263d948fb91cfc8b16fba44cdb3d 100644 (file)
@@ -1,7 +1,8 @@
+import gc
 import sys
 import asyncio
 import selectors
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Tuple
 
 import pytest
 
@@ -97,3 +98,70 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
         terminalreporter.section("failed tests ignored")
         for msg in allow_fail_messages:
             terminalreporter.line(msg)
+
+
+NO_COUNT_TYPES: Tuple[type, ...] = ()
+
+if sys.version_info[:2] == (3, 10):
+    # On my laptop there are occasional creations of a single one of these objects
+    # with empty content, which might be some Decimal caching.
+    # Keeping the guard as strict as possible, to be extended if other types
+    # or versions are necessary.
+    try:
+        from _contextvars import Context  # type: ignore
+    except ImportError:
+        pass
+    else:
+        NO_COUNT_TYPES += (Context,)
+
+
+class GCFixture:
+    __slots__ = ()
+
+    @staticmethod
+    def collect() -> None:
+        """
+        gc.collect(), but more insisting.
+        """
+        for i in range(3):
+            gc.collect()
+
+    @staticmethod
+    def count() -> int:
+        """
+        len(gc.get_objects()), with subtleties.
+        """
+
+        if not NO_COUNT_TYPES:
+            return len(gc.get_objects())
+
+        # Note: not using a list comprehension because it pollutes the objects list.
+        rv = 0
+        for obj in gc.get_objects():
+            if isinstance(obj, NO_COUNT_TYPES):
+                continue
+            rv += 1
+
+        return rv
+
+
+@pytest.fixture(name="gc")
+def fixture_gc():
+    """
+    Provides a consistent way to run garbage collection and count references.
+
+    **Note:** This will skip tests on PyPy.
+    """
+    if sys.implementation.name == "pypy":
+        pytest.skip(reason="depends on refcount semantics")
+    return GCFixture()
+
+
+@pytest.fixture
+def gc_collect():
+    """
+    Provides a consistent way to run garbage collection.
+
+    **Note:** This will *not* skip tests on PyPy.
+    """
+    return GCFixture.collect
index b7d26aa516c07aa30ac10111732dacf92cf0215d..c3ccdfe11aa3fffea43c8de4ced1ba156256e6be 100644 (file)
@@ -7,7 +7,7 @@ from psycopg.pq import Format
 from psycopg.adapt import PyFormat
 from psycopg.types.numeric import Int4
 
-from ..utils import eur, gc_collect, gc_count
+from ..utils import eur
 from ..test_copy import sample_text, sample_binary  # noqa
 from ..test_copy import ensure_table, sample_records
 from ..test_copy import sample_tabledef as sample_tabledef_pg
@@ -191,7 +191,7 @@ from copy_in group by 1, 2, 3
     [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)],
 )
 @pytest.mark.crdb_skip("copy array")
-def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types):
+def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types, gc):
     faker.format = PyFormat.from_pq(fmt)
     faker.choose_schema(ncols=20)
     faker.make_records(20)
@@ -219,12 +219,12 @@ def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types):
                 for got, want in zip(recs, faker.records):
                     faker.assert_record(got, want)
 
-    gc_collect()
+    gc.collect()
     n = []
     for i in range(3):
         work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
index 45ee5eca08c44a05f773aedfaddfc77ac3fbf815..06a702664e81c093608649d4e64a72c045a7e695 100644 (file)
@@ -7,7 +7,7 @@ from psycopg import sql, errors as e
 from psycopg.adapt import PyFormat
 from psycopg.types.numeric import Int4
 
-from ..utils import eur, gc_collect, gc_count
+from ..utils import eur
 from ..test_copy import sample_text, sample_binary  # noqa
 from ..test_copy import sample_records
 from ..test_copy_async import ensure_table
@@ -197,7 +197,7 @@ from copy_in group by 1, 2, 3
     [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)],
 )
 @pytest.mark.crdb_skip("copy array")
-async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types):
+async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types, gc):
     faker.format = PyFormat.from_pq(fmt)
     faker.choose_schema(ncols=20)
     faker.make_records(20)
@@ -225,11 +225,11 @@ async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types):
                 for got, want in zip(recs, faker.records):
                     faker.assert_record(got, want)
 
-    gc_collect()
+    gc.collect()
     n = []
     for i in range(3):
         await work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
index 6811a26c32a3337bf0d1364684be9c06c82ee3a9..917dfc9193e9c558778034979587cf175a188b7f 100644 (file)
@@ -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}")
index 51cc67ad9924233036c92c4d6aae1a8294ec037a..68a03ddf7520ccfcaf616f680b55c1f9c479f427 100644 (file)
@@ -618,12 +618,13 @@ def test_putconn_wrong_pool(dsn):
 
 
 @pytest.mark.slow
-def test_del_stop_threads(dsn):
+def test_del_stop_threads(dsn, gc):
     p = NullConnectionPool(dsn)
     assert p._sched_runner is not None
     ts = [p._sched_runner] + p._workers
     del p
     sleep(0.1)
+    gc.collect()
     for t in ts:
         assert not t.is_alive()
 
index 5b4c435d720ce9c7177e47a05e4b5c7f1e68d53a..8eb8911443f42314c7502a88134b009d64a54d7d 100644 (file)
@@ -588,7 +588,7 @@ def test_putconn_wrong_pool(dsn):
                 p2.putconn(conn)
 
 
-def test_del_no_warning(dsn, recwarn):
+def test_del_no_warning(dsn, recwarn, gc_collect):
     p = pool.ConnectionPool(dsn, min_size=2)
     with p.connection() as conn:
         conn.execute("select 1")
@@ -596,17 +596,19 @@ 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]
 
 
 @pytest.mark.slow
-def test_del_stop_threads(dsn):
+def test_del_stop_threads(dsn, gc):
     p = pool.ConnectionPool(dsn)
     assert p._sched_runner is not None
     ts = [p._sched_runner] + p._workers
     del p
     sleep(0.1)
+    gc.collect()
     for t in ts:
         assert not t.is_alive()
 
index 982b9288e8a74dcb79ee91b09d75ac3e2acf8bb7..c820209c6332e4ca1cff3edbe31cecd7d34eb46d 100644 (file)
@@ -5,8 +5,6 @@ import asyncio
 
 import pytest
 
-from ..utils import gc_collect
-
 try:
     import psycopg_pool as pool
 except ImportError:
@@ -61,7 +59,7 @@ def test_cant_create_open_outside_loop(dsn):
 
 
 @pytest.fixture
-def asyncio_run(anyio_backend_options, recwarn):
+def asyncio_run(anyio_backend_options, recwarn, gc_collect):
     """Fixture reuturning asyncio.run, but managing resources at exit.
 
     In certain runs, fd objects are leaked and the error will only be caught
index 05661511ad0b14f16561f6d1bea07549fff0d83f..ff18379a44ed7cabe19ff44506241b796102baea 100644 (file)
@@ -11,8 +11,6 @@ import psycopg
 from psycopg import pq
 import psycopg.generators
 
-from ..utils import gc_collect
-
 
 def test_connectdb(dsn):
     conn = pq.PGconn.connect(dsn.encode())
@@ -82,7 +80,7 @@ def test_finish(pgconn):
 
 
 @pytest.mark.slow
-def test_weakref(dsn):
+def test_weakref(dsn, gc_collect):
     conn = pq.PGconn.connect(dsn.encode())
     w = weakref.ref(conn)
     conn.finish()
index 3d6ec4dbf906a713003f54e8f9aba290a021d647..416f9fbde1dd3ee4f24a35a82f859165f239728f 100644 (file)
@@ -11,7 +11,6 @@ from psycopg.adapt import PyFormat
 from psycopg.postgres import types as builtins
 from psycopg.types import TypeInfo
 
-from .utils import gc_collect, gc_count
 from .test_cursor import my_row_factory
 from .fix_crdb import is_crdb, crdb_encoding, crdb_time_precision
 
@@ -117,7 +116,7 @@ def test_context(conn):
 
 
 @pytest.mark.slow
-def test_weakref(conn):
+def test_weakref(conn, gc_collect):
     cur = conn.cursor()
     w = weakref.ref(cur)
     cur.close()
@@ -793,7 +792,7 @@ def test_str(conn):
 @pytest.mark.slow
 @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):
+def test_leak(conn_cls, dsn, faker, fetch, row_factory, gc):
     faker.choose_schema(ncols=5)
     faker.make_records(10)
     row_factory = getattr(rows, row_factory)
@@ -825,11 +824,11 @@ def test_leak(conn_cls, dsn, faker, fetch, row_factory):
                         pass
 
     n = []
-    gc_collect()
+    gc.collect()
     for i in range(3):
         work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
 
index 67b314acb3a78ccee7a56058505a082941ecf9f4..e7b55d4a472b935a60e33ee7614c73d88f414c53 100644 (file)
@@ -8,7 +8,7 @@ from psycopg import sql, rows
 from psycopg.adapt import PyFormat
 from psycopg.types import TypeInfo
 
-from .utils import alist, gc_collect, gc_count
+from .utils import alist
 from .test_cursor import my_row_factory
 from .test_cursor import execmany, _execmany  # noqa: F401
 from .fix_crdb import crdb_encoding
@@ -120,7 +120,7 @@ async def test_context(aconn):
 
 
 @pytest.mark.slow
-async def test_weakref(aconn):
+async def test_weakref(aconn, gc_collect):
     cur = aconn.cursor()
     w = weakref.ref(cur)
     await cur.close()
@@ -638,7 +638,7 @@ async def test_str(aconn):
 @pytest.mark.slow
 @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):
+async def test_leak(aconn_cls, dsn, faker, fetch, row_factory, gc):
     faker.choose_schema(ncols=5)
     faker.make_records(10)
     row_factory = getattr(rows, row_factory)
@@ -671,11 +671,11 @@ async def test_leak(aconn_cls, dsn, faker, fetch, row_factory):
                         pass
 
     n = []
-    gc_collect()
+    gc.collect()
     for i in range(3):
         await work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
index 10f0a6ca7a05893f43f57ee6e3fb99c748f9b1bf..3df8475d8230ff84e8e2edac2e9bdbc9d5315d23 100644 (file)
@@ -11,7 +11,6 @@ from psycopg import Notify, pq, errors as e
 from psycopg.rows import tuple_row
 from psycopg.conninfo import conninfo_to_dict, make_conninfo
 
-from .utils import gc_collect
 from .test_cursor import my_row_factory
 from .test_adapt import make_bin_dumper, make_dumper
 
@@ -127,19 +126,22 @@ def test_cursor_closed(conn):
     (pq.__impl__ in ("c", "binary") and sys.version_info[:2] == (3, 12)),
     reason="Something with Exceptions, C, Python 3.12",
 )
-def test_connection_warn_close(conn_cls, dsn, recwarn):
+def test_connection_warn_close(conn_cls, dsn, recwarn, gc_collect):
     conn = conn_cls.connect(dsn)
     conn.close()
     del conn
+    gc_collect()
     assert not recwarn, [str(w.message) for w in recwarn.list]
 
     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)
@@ -154,6 +156,7 @@ def test_connection_warn_close(conn_cls, dsn, recwarn):
     with conn_cls.connect(dsn) as conn:
         pass
     del conn
+    gc_collect()
     assert not recwarn, [str(w.message) for w in recwarn.list]
 
 
@@ -238,7 +241,7 @@ def test_context_active_rollback_no_clobber(conn_cls, dsn, caplog):
 
 
 @pytest.mark.slow
-def test_weakref(conn_cls, dsn):
+def test_weakref(conn_cls, dsn, gc_collect):
     conn = conn_cls.connect(dsn)
     w = weakref.ref(conn)
     conn.close()
index b4c100b9be36f35f7f4dbc3d0d049cae63bbfd2e..17ecbe3d641fab0917be09733ba41c9e851559f1 100644 (file)
@@ -9,7 +9,6 @@ from psycopg import Notify, errors as e
 from psycopg.rows import tuple_row
 from psycopg.conninfo import conninfo_to_dict, make_conninfo
 
-from .utils import gc_collect
 from .test_cursor import my_row_factory
 from .test_connection import tx_params, tx_params_isolation, tx_values_map
 from .test_connection import conninfo_params_timeout, drop_default_args_from_conninfo
@@ -126,19 +125,22 @@ async def test_cursor_closed(aconn):
         aconn.cursor()
 
 
-async def test_connection_warn_close(aconn_cls, dsn, recwarn):
+async def test_connection_warn_close(aconn_cls, dsn, recwarn, gc_collect):
     conn = await aconn_cls.connect(dsn)
     await conn.close()
     del conn
+    gc_collect()
     assert not recwarn, [str(w.message) for w in recwarn.list]
 
     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)
@@ -147,11 +149,13 @@ async def test_connection_warn_close(aconn_cls, dsn, recwarn):
     except Exception:
         pass
     del conn
+    gc_collect()
     assert "INERROR" in str(recwarn.pop(ResourceWarning).message)
 
     async with await aconn_cls.connect(dsn) as conn:
         pass
     del conn
+    gc_collect()
     assert not recwarn, [str(w.message) for w in recwarn.list]
 
 
@@ -229,7 +233,7 @@ async def test_context_active_rollback_no_clobber(aconn_cls, dsn, caplog):
 
 
 @pytest.mark.slow
-async def test_weakref(aconn_cls, dsn):
+async def test_weakref(aconn_cls, dsn, gc_collect):
     conn = await aconn_cls.connect(dsn)
     w = weakref.ref(conn)
     await conn.close()
index 2c21368ae5c7d9915ddcbff0a3149cbb0edf3fbe..bc3628c6abbe55689b0d4d9159e90940ad9e2071 100644 (file)
@@ -18,7 +18,7 @@ from psycopg.types import TypeInfo
 from psycopg.types.hstore import register_hstore
 from psycopg.types.numeric import Int4
 
-from .utils import eur, gc_collect, gc_count
+from .utils import eur
 
 pytestmark = pytest.mark.crdb_skip("copy")
 
@@ -691,7 +691,7 @@ def test_connection_writer(conn, format, buffer):
     [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)],
 )
 @pytest.mark.parametrize("method", ["read", "iter", "row", "rows"])
-def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method):
+def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method, gc):
     faker.format = PyFormat.from_pq(fmt)
     faker.choose_schema(ncols=20)
     faker.make_records(20)
@@ -731,12 +731,12 @@ def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method):
                     elif method == "rows":
                         list(copy.rows())
 
-    gc_collect()
+    gc.collect()
     n = []
     for i in range(3):
         work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
@@ -746,7 +746,7 @@ def test_copy_to_leaks(conn_cls, dsn, faker, fmt, set_types, method):
     "fmt, set_types",
     [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)],
 )
-def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types):
+def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types, gc):
     faker.format = PyFormat.from_pq(fmt)
     faker.choose_schema(ncols=20)
     faker.make_records(20)
@@ -774,12 +774,12 @@ def test_copy_from_leaks(conn_cls, dsn, faker, fmt, set_types):
                 for got, want in zip(recs, faker.records):
                     faker.assert_record(got, want)
 
-    gc_collect()
+    gc.collect()
     n = []
     for i in range(3):
         work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
index dd11d4bd278aa6429b428968ea21889fde897e55..ea8f23028066e2fb733f68e37ca7ab854fa71273 100644 (file)
@@ -18,7 +18,7 @@ from psycopg.adapt import PyFormat
 from psycopg.types.hstore import register_hstore
 from psycopg.types.numeric import Int4
 
-from .utils import alist, eur, gc_collect, gc_count
+from .utils import alist, eur
 from .test_copy import sample_text, sample_binary, sample_binary_rows  # noqa
 from .test_copy import sample_values, sample_records, sample_tabledef
 from .test_copy import py_to_raw, special_chars
@@ -697,7 +697,7 @@ async def test_connection_writer(aconn, format, buffer):
     [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)],
 )
 @pytest.mark.parametrize("method", ["read", "iter", "row", "rows"])
-async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method):
+async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method, gc):
     faker.format = PyFormat.from_pq(fmt)
     faker.choose_schema(ncols=20)
     faker.make_records(20)
@@ -737,12 +737,12 @@ async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method):
                     elif method == "rows":
                         await alist(copy.rows())
 
-    gc_collect()
+    gc.collect()
     n = []
     for i in range(3):
         await work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
@@ -752,7 +752,7 @@ async def test_copy_to_leaks(aconn_cls, dsn, faker, fmt, set_types, method):
     "fmt, set_types",
     [(Format.TEXT, True), (Format.TEXT, False), (Format.BINARY, True)],
 )
-async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types):
+async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types, gc):
     faker.format = PyFormat.from_pq(fmt)
     faker.choose_schema(ncols=20)
     faker.make_records(20)
@@ -780,12 +780,12 @@ async def test_copy_from_leaks(aconn_cls, dsn, faker, fmt, set_types):
                 for got, want in zip(recs, faker.records):
                     faker.assert_record(got, want)
 
-    gc_collect()
+    gc.collect()
     n = []
     for i in range(3):
         await work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
index f48714715d89aa9224dbbb1d6ec65bb219125a7b..c217acf82cae090a2feaadeb651aa7b7879e09f4 100644 (file)
@@ -12,7 +12,6 @@ from psycopg.adapt import PyFormat
 from psycopg.postgres import types as builtins
 from psycopg.rows import RowMaker
 
-from .utils import gc_collect, gc_count
 from .fix_crdb import is_crdb, crdb_encoding, crdb_time_precision
 
 
@@ -100,7 +99,7 @@ def test_context(conn):
 
 
 @pytest.mark.slow
-def test_weakref(conn):
+def test_weakref(conn, gc_collect):
     cur = conn.cursor()
     w = weakref.ref(cur)
     cur.close()
@@ -913,7 +912,7 @@ def test_str(conn):
 @pytest.mark.parametrize("fmt_out", pq.Format)
 @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, fmt, fmt_out, fetch, row_factory):
+def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory, gc):
     faker.format = fmt
     faker.choose_schema(ncols=5)
     faker.make_records(10)
@@ -946,11 +945,12 @@ def test_leak(conn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory):
                         pass
 
     n = []
-    gc_collect()
+    gc.collect()
     for i in range(3):
         work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
+
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
 
 
index e2081535523655f759030d6cef479548feca1195..6427064efef2a88d091997996048124d620c126c 100644 (file)
@@ -7,7 +7,6 @@ import psycopg
 from psycopg import pq, sql, rows
 from psycopg.adapt import PyFormat
 
-from .utils import gc_collect, gc_count
 from .test_cursor import my_row_factory
 from .test_cursor import execmany, _execmany  # noqa: F401
 from .fix_crdb import crdb_encoding
@@ -99,7 +98,7 @@ async def test_context(aconn):
 
 
 @pytest.mark.slow
-async def test_weakref(aconn):
+async def test_weakref(aconn, gc_collect):
     cur = aconn.cursor()
     w = weakref.ref(cur)
     await cur.close()
@@ -760,7 +759,7 @@ async def test_str(aconn):
 @pytest.mark.parametrize("fmt_out", pq.Format)
 @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, fmt, fmt_out, fetch, row_factory):
+async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory, gc):
     faker.format = fmt
     faker.choose_schema(ncols=5)
     faker.make_records(10)
@@ -794,10 +793,10 @@ async def test_leak(aconn_cls, dsn, faker, fmt, fmt_out, fetch, row_factory):
                         pass
 
     n = []
-    gc_collect()
+    gc.collect()
     for i in range(3):
         await work()
-        gc_collect()
-        n.append(gc_count())
+        gc.collect()
+        n.append(gc.count())
 
     assert n[0] == n[1] == n[2], f"objects leaked: {n[1] - n[0]}, {n[2] - n[1]}"
index ddf57513ce78218f2731708ffcfe6f82957e1d78..a5016ae32dc95da28d5bd208f671d1e5378b4c71 100644 (file)
@@ -9,7 +9,7 @@ import psycopg
 from psycopg import pq
 from psycopg import errors as e
 
-from .utils import eur, gc_collect
+from .utils import eur
 from .fix_crdb import is_crdb
 
 
@@ -187,7 +187,7 @@ def test_diag_pickle(conn):
     (pq.__impl__ in ("c", "binary") and sys.version_info[:2] == (3, 12)),
     reason="Something with Exceptions, C, Python 3.12",
 )
-def test_diag_survives_cursor(conn):
+def test_diag_survives_cursor(conn, gc_collect):
     cur = conn.cursor()
     with pytest.raises(e.Error) as exc:
         cur.execute("select * from nosuchtable")
index 385dcd774839ac981c5df2ea9c397a66c20dc6cb..6b2ce2f14f056c8ec05d1f1244cc8c554e85fa5f 100644 (file)
@@ -251,11 +251,12 @@ def test_close_no_clobber(conn):
             cur.fetchall()
 
 
-def test_warn_close(conn, recwarn):
+def test_warn_close(conn, recwarn, gc_collect):
     recwarn.clear()
     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)
 
 
index 99ff9cc76686775f1949af1ab955f12557ae70f8..a2e3cc90cfe4e32c0649fbdd9a0cfbec0f0f5c5a 100644 (file)
@@ -4,9 +4,7 @@ import psycopg
 from psycopg import rows, errors as e
 from psycopg.pq import Format
 
-pytestmark = [
-    pytest.mark.crdb_skip("server-side cursor"),
-]
+pytestmark = pytest.mark.crdb_skip("server-side cursor")
 
 
 async def test_init_row_factory(aconn):
@@ -261,11 +259,12 @@ async def test_close_no_clobber(aconn):
             await cur.fetchall()
 
 
-async def test_warn_close(aconn, recwarn):
+async def test_warn_close(aconn, recwarn, gc_collect):
     recwarn.clear()
     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)
 
 
index 8100bc7a85deb5f53811e2ce32d934c5c1bee8f2..8a40ab3b9d5a2f7eb315a85f4f9f30658d5fb97c 100644 (file)
@@ -13,8 +13,6 @@ from psycopg._compat import prod
 from psycopg.postgres import types as builtins
 from psycopg.types.array import register_array
 
-from ..utils import gc_collect
-
 
 tests_str = [
     ([[[[[["a"]]]]]], "{{{{{{a}}}}}}"),
@@ -342,7 +340,7 @@ def test_all_chars_with_bounds(conn, fmt_out):
 
 
 @pytest.mark.slow
-def test_register_array_leak(conn):
+def test_register_array_leak(conn, gc_collect):
     info = TypeInfo.fetch(conn, "date")
     ntypes = []
     for i in range(2):
index 871f65d0e1435f079e2c1e59b9145ba62184e930..064fab1c6b00743930755916401b7059e2fd5ef0 100644 (file)
@@ -1,6 +1,4 @@
-import gc
 import re
-import sys
 import operator
 from typing import Callable, Optional, Tuple
 
@@ -135,45 +133,5 @@ class VersionCheck:
         return (ver_maj, ver_min, ver_fix)
 
 
-def gc_collect():
-    """
-    gc.collect(), but more insisting.
-    """
-    for i in range(3):
-        gc.collect()
-
-
-NO_COUNT_TYPES: Tuple[type, ...] = ()
-
-if sys.version_info[:2] == (3, 10):
-    # On my laptop there are occasional creations of a single one of these objects
-    # with empty content, which might be some Decimal caching.
-    # Keeping the guard as strict as possible, to be extended if other types
-    # or versions are necessary.
-    try:
-        from _contextvars import Context  # type: ignore
-    except ImportError:
-        pass
-    else:
-        NO_COUNT_TYPES += (Context,)
-
-
-def gc_count() -> int:
-    """
-    len(gc.get_objects()), with subtleties.
-    """
-    if not NO_COUNT_TYPES:
-        return len(gc.get_objects())
-
-    # Note: not using a list comprehension because it pollutes the objects list.
-    rv = 0
-    for obj in gc.get_objects():
-        if isinstance(obj, NO_COUNT_TYPES):
-            continue
-        rv += 1
-
-    return rv
-
-
 async def alist(it):
     return [i async for i in it]