Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-psycopg for openSUSE:Factory checked in at 2026-05-18 17:48:18 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-psycopg (Old) and /work/SRC/openSUSE:Factory/.python-psycopg.new.1966 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-psycopg" Mon May 18 17:48:18 2026 rev:14 rq:1353778 version:3.3.4 Changes: -------- --- /work/SRC/openSUSE:Factory/python-psycopg/python-psycopg.changes 2026-02-23 16:14:47.362875171 +0100 +++ /work/SRC/openSUSE:Factory/.python-psycopg.new.1966/python-psycopg.changes 2026-05-18 17:49:13.731307809 +0200 @@ -1,0 +2,11 @@ +Mon May 18 10:30:30 UTC 2026 - Dirk Müller <[email protected]> + +- update to 3.3.4: + * Fix possible spurious connection timeout in systems with very + long uptimes in C extension (:ticket:`#1280`). + * Fix client-side adaptation of enums whose name require quotes + (:ticket:`#1298`). + * Consistently populate ~Cursor.statusmessage after + ~Cursor.executemany() (:ticket:`#1302`). + +------------------------------------------------------------------- Old: ---- psycopg-3.3.3.tar.gz New: ---- psycopg-3.3.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-psycopg.spec ++++++ --- /var/tmp/diff_new_pack.piYz9g/_old 2026-05-18 17:49:15.323373597 +0200 +++ /var/tmp/diff_new_pack.piYz9g/_new 2026-05-18 17:49:15.343374423 +0200 @@ -19,7 +19,7 @@ %{?sle15_python_module_pythons} Name: python-psycopg # This needs to upgraded in lockstep with python-psycopg-c -Version: 3.3.3 +Version: 3.3.4 Release: 0 Summary: PostgreSQL database adapter for Python License: LGPL-3.0-only ++++++ psycopg-3.3.3.tar.gz -> psycopg-3.3.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/.flake8 new/psycopg-3.3.4/.flake8 --- old/psycopg-3.3.3/.flake8 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/.flake8 2026-05-01 22:41:15.000000000 +0200 @@ -6,7 +6,7 @@ [flake8] max-line-length = 88 ignore = W503, E203, E704 -extend-exclude = .venv build tests/test_tstring.py +extend-exclude = .venv build per-file-ignores = # Autogenerated section psycopg/psycopg/errors.py: E125, E128, E302 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/.github/workflows/build-and-cache-libpq.yml new/psycopg-3.3.4/.github/workflows/build-and-cache-libpq.yml --- old/psycopg-3.3.3/.github/workflows/build-and-cache-libpq.yml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/.github/workflows/build-and-cache-libpq.yml 2026-05-01 22:41:15.000000000 +0200 @@ -81,7 +81,7 @@ - name: Set up QEMU for multi-arch build # Check https://github.com/docker/setup-qemu-action for newer versions. - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: # https://github.com/pypa/cibuildwheel/discussions/2256 image: tonistiigi/binfmt:qemu-v8.1.5 @@ -93,7 +93,7 @@ key: libpq-${{ matrix.platform }}-${{ matrix.arch }}-${{ env.LIBPQ_VERSION }}-${{ env.OPENSSL_VERSION }}${{ env.PQ_FLAGS }} - name: Build wheels - uses: pypa/[email protected] + uses: pypa/[email protected] with: package-dir: psycopg_c env: @@ -137,7 +137,7 @@ key: libpq-macos-${{ env.LIBPQ_VERSION }}-${{ matrix.arch }}-${{ env.OPENSSL_VERSION }}${{ env.PQ_FLAGS }} - name: Build wheels - uses: pypa/[email protected] + uses: pypa/[email protected] with: package-dir: psycopg_c env: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/.github/workflows/lint.yml new/psycopg-3.3.4/.github/workflows/lint.yml --- old/psycopg-3.3.3/.github/workflows/lint.yml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/.github/workflows/lint.yml 2026-05-01 22:41:15.000000000 +0200 @@ -24,7 +24,7 @@ - uses: actions/setup-python@v6 with: - python-version: "3.11" + python-version: "3.14" - name: install packages to tests run: pip install ./psycopg[dev,test] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/.github/workflows/packages-bin.yml new/psycopg-3.3.4/.github/workflows/packages-bin.yml --- old/psycopg-3.3.3/.github/workflows/packages-bin.yml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/.github/workflows/packages-bin.yml 2026-05-01 22:41:15.000000000 +0200 @@ -57,7 +57,7 @@ - name: Set up QEMU for multi-arch build # Check https://github.com/docker/setup-qemu-action for newer versions. - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: # https://github.com/pypa/cibuildwheel/discussions/2256 image: tonistiigi/binfmt:qemu-v8.1.5 @@ -72,7 +72,7 @@ run: python3 ./tools/ci/copy_to_binary.py - name: Build wheels - uses: pypa/[email protected] + uses: pypa/[email protected] with: package-dir: psycopg_binary env: @@ -100,7 +100,7 @@ PSYCOPG_TEST_WANT_LIBPQ_IMPORT=${{ env.LIBPQ_VERSION }} PYTEST_ADDOPTS="-m 'not slow and not flakey' --color yes" - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: linux-${{matrix.pyver}}-${{matrix.platform}}_${{matrix.arch}} path: ./wheelhouse/*.whl @@ -146,7 +146,7 @@ run: python3 ./tools/ci/copy_to_binary.py - name: Build wheels - uses: pypa/[email protected] + uses: pypa/[email protected] with: package-dir: psycopg_binary env: @@ -167,7 +167,7 @@ PYTEST_ADDOPTS="-m 'not slow and not flakey and not proxy' --color yes" - name: Upload artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: macos-${{matrix.pyver}}-${{matrix.arch}} path: ./wheelhouse/*.whl @@ -204,7 +204,7 @@ shell: powershell - name: Export GitHub Actions cache environment variables - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const path = require('path') @@ -217,7 +217,7 @@ run: python3 ./tools/ci/copy_to_binary.py - name: Build wheels - uses: pypa/[email protected] + uses: pypa/[email protected] with: package-dir: psycopg_binary env: @@ -237,7 +237,7 @@ PSYCOPG_TEST_WANT_LIBPQ_IMPORT=">= 16" PYTEST_ADDOPTS="-m 'not slow and not flakey and not proxy' --color yes" - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: windows-${{matrix.pyver}}-${{matrix.arch}} path: ./wheelhouse/*.whl @@ -253,7 +253,7 @@ - windows steps: - name: Merge Artifacts - uses: actions/upload-artifact/merge@v6 + uses: actions/upload-artifact/merge@v7 with: name: psycopg-binary-artifact delete-merged: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/.github/workflows/packages-pool.yml new/psycopg-3.3.4/.github/workflows/packages-pool.yml --- old/psycopg-3.3.3/.github/workflows/packages-pool.yml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/.github/workflows/packages-pool.yml 2026-05-01 22:41:15.000000000 +0200 @@ -25,7 +25,7 @@ - uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.14" - name: Install the build package run: pip install build @@ -42,7 +42,7 @@ PSYCOPG_TEST_DSN: "host=127.0.0.1 user=postgres" PGPASSWORD: password - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: ${{ matrix.package }}-${{ matrix.format }} path: ./dist/* @@ -67,7 +67,7 @@ - sdist steps: - name: Merge Artifacts - uses: actions/upload-artifact/merge@v6 + uses: actions/upload-artifact/merge@v7 with: name: psycopg-pool-artifact delete-merged: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/.github/workflows/packages-src.yml new/psycopg-3.3.4/.github/workflows/packages-src.yml --- old/psycopg-3.3.3/.github/workflows/packages-src.yml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/.github/workflows/packages-src.yml 2026-05-01 22:41:15.000000000 +0200 @@ -27,7 +27,7 @@ - uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.14" - name: Install the build package run: pip install build @@ -50,7 +50,7 @@ PSYCOPG_TEST_DSN: "host=127.0.0.1 user=postgres" PGPASSWORD: password - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: ${{ matrix.package }}-${{ matrix.format }}-${{ matrix.impl }} path: ./dist/* @@ -74,7 +74,7 @@ - sdist steps: - name: Merge Artifacts - uses: actions/upload-artifact/merge@v6 + uses: actions/upload-artifact/merge@v7 with: name: psycopg-src-artifact delete-merged: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/.github/workflows/tests.yml new/psycopg-3.3.4/.github/workflows/tests.yml --- old/psycopg-3.3.3/.github/workflows/tests.yml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/.github/workflows/tests.yml 2026-05-01 22:41:15.000000000 +0200 @@ -315,7 +315,7 @@ - name: Export GitHub Actions cache environment variables # https://learn.microsoft.com/en-us/vcpkg/consume/binary-caching-github-actions-cache - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const path = require('path') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/docs/api/pool.rst new/psycopg-3.3.4/docs/api/pool.rst --- old/psycopg-3.3.3/docs/api/pool.rst 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/docs/api/pool.rst 2026-05-01 22:41:15.000000000 +0200 @@ -228,6 +228,8 @@ with ConnectionPool(...) as pool: # code using the pool + .. autoattribute:: closed + .. automethod:: wait .. attribute:: name diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/docs/news.rst new/psycopg-3.3.4/docs/news.rst --- old/psycopg-3.3.3/docs/news.rst 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/docs/news.rst 2026-05-01 22:41:15.000000000 +0200 @@ -10,6 +10,17 @@ Current release --------------- +Psycopg 3.3.4 +^^^^^^^^^^^^^ + +- Fix possible spurious connection timeout in systems with very long uptimes + in C extension (:ticket:`#1280`). +- Fix client-side adaptation of enums whose name require quotes + (:ticket:`#1298`). +- Consistently populate `~Cursor.statusmessage` after `~Cursor.executemany()` + (:ticket:`#1302`). + + Psycopg 3.3.3 ^^^^^^^^^^^^^ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/docs/news_pool.rst new/psycopg-3.3.4/docs/news_pool.rst --- old/psycopg-3.3.3/docs/news_pool.rst 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/docs/news_pool.rst 2026-05-01 22:41:15.000000000 +0200 @@ -10,6 +10,15 @@ Current release --------------- +psycopg_pool 3.3.1 +^^^^^^^^^^^^^^^^^^ + +- Fix residual race condition catching `~asyncio.CancelledError` on connection + (:ticket:`#1275`). +- Fix race condition constructing the lock that makes sync + `~ConnectionPool.open()` thread-safe (:ticket:`#1300`). + + psycopg_pool 3.3.0 ------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg/psycopg/_adapters_map.py new/psycopg-3.3.4/psycopg/psycopg/_adapters_map.py --- old/psycopg-3.3.3/psycopg/psycopg/_adapters_map.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg/psycopg/_adapters_map.py 2026-05-01 22:41:15.000000000 +0200 @@ -212,8 +212,10 @@ # Look for the right class, including looking at superclasses for scls in cls.__mro__: - if scls in dmap: + try: return dmap[scls] + except KeyError: + pass # If the adapter is not found, look for its name as a string fqn = scls.__module__ + "." + scls.__qualname__ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg/psycopg/_capabilities.py new/psycopg-3.3.4/psycopg/psycopg/_capabilities.py --- old/psycopg-3.3.3/psycopg/psycopg/_capabilities.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg/psycopg/_capabilities.py 2026-05-01 22:41:15.000000000 +0200 @@ -98,9 +98,9 @@ The expletive messages, are left to the user. """ - if feature in self._cache: + try: msg = self._cache[feature] - else: + except KeyError: msg = self._get_unsupported_message(feature, want_version) self._cache[feature] = msg diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg/psycopg/_cursor_base.py new/psycopg-3.3.4/psycopg/psycopg/_cursor_base.py --- old/psycopg-3.3.3/psycopg/psycopg/_cursor_base.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg/psycopg/_cursor_base.py 2026-05-01 22:41:15.000000000 +0200 @@ -49,7 +49,7 @@ __slots__ = """ _conn format _adapters arraysize _closed _results pgresult _pos _iresult _rowcount _query _tx _last_query _row_factory _make_row - _pgconn _execmany_returning + _pgconn _execmany_returning _statusmessage __weakref__ """.split() @@ -79,6 +79,7 @@ self._pos = 0 self._iresult = 0 self._rowcount = -1 + self._statusmessage: bytes | None = None self._query: PostgresQuery | None # None if executemany() not executing, True/False according to returning state self._execmany_returning: bool | None = None @@ -178,8 +179,7 @@ `!None` if the cursor doesn't have a result available. """ - msg = self.pgresult.command_status if self.pgresult else None - return msg.decode() if msg else None + return self._statusmessage.decode() if self._statusmessage else None def _make_row_maker(self) -> RowMaker[Row]: raise NotImplementedError @@ -525,6 +525,7 @@ nrows = self.pgresult.command_tuples self._rowcount = nrows if nrows is not None else -1 + self._statusmessage = res.command_status self._make_row = self._make_row_maker() def _set_results(self, results: list[PGresult]) -> None: @@ -540,9 +541,12 @@ self._select_current_result(0) else: # In non-returning case, set rowcount to the cumulated number of - # rows of executed queries. - for res in results: - self._rowcount += res.command_tuples or 0 + # rows of executed queries. Keep the last result's command_status + # so that statusmessage is available after the batch completes. + if results: + self._statusmessage = results[-1].command_status + for res in results: + self._rowcount += res.command_tuples or 0 @classmethod def _loaders_changed( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg/psycopg/types/composite.py new/psycopg-3.3.4/psycopg/psycopg/types/composite.py --- old/psycopg-3.3.3/psycopg/psycopg/types/composite.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg/psycopg/types/composite.py 2026-05-01 22:41:15.000000000 +0200 @@ -206,8 +206,10 @@ return tx.load_sequence(record) def _get_transformer(self, key: tuple[int, ...]) -> abc.Transformer: - if key in self._txs: + try: return self._txs[key] + except KeyError: + pass tx = Transformer(self._ctx) tx.set_loader_types([*key], self.format) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg/psycopg/types/enum.py new/psycopg-3.3.4/psycopg/psycopg/types/enum.py --- old/psycopg-3.3.3/psycopg/psycopg/types/enum.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg/psycopg/types/enum.py 2026-05-01 22:41:15.000000000 +0200 @@ -43,9 +43,13 @@ name: str, oid: int, array_oid: int, + # A bit ugly: this should have been a keyword-only argument, but it has + # been it the wild accepting a positional argument for too long to fix. labels: Sequence[str], + *, + regtype: str = "", ): - super().__init__(name, oid, array_oid) + super().__init__(name, oid, array_oid, regtype=regtype) self.labels = labels # Will be set by register_enum() self.enum: type[Enum] | None = None @@ -53,18 +57,18 @@ @classmethod def _get_info_query(cls, conn: BaseConnection[Any]) -> QueryNoTemplate: return sql.SQL("""\ -SELECT name, oid, array_oid, array_agg(label) AS labels +SELECT name, oid, array_oid, regtype, array_agg(label) AS labels FROM ( SELECT t.typname AS name, t.oid AS oid, t.typarray AS array_oid, - e.enumlabel AS label + t.oid::regtype::text AS regtype, e.enumlabel AS label FROM pg_type t LEFT JOIN pg_enum e ON e.enumtypid = t.oid WHERE t.oid = {regtype} ORDER BY e.enumsortorder ) x -GROUP BY name, oid, array_oid +GROUP BY name, oid, array_oid, regtype """).format(regtype=cls._to_regtype(conn)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg/psycopg/types/uuid.py new/psycopg-3.3.4/psycopg/psycopg/types/uuid.py --- old/psycopg-3.3.3/psycopg/psycopg/types/uuid.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg/psycopg/types/uuid.py 2026-05-01 22:41:15.000000000 +0200 @@ -25,14 +25,14 @@ oid = _oids.UUID_OID def dump(self, obj: uuid.UUID) -> Buffer | None: - return obj.hex.encode() + return b"%032x" % obj.int class UUIDBinaryDumper(UUIDDumper): format = Format.BINARY def dump(self, obj: uuid.UUID) -> Buffer | None: - return obj.bytes + return obj.int.to_bytes(16, "big") class UUIDLoader(Loader): @@ -43,18 +43,14 @@ from uuid import UUID def load(self, data: Buffer) -> uuid.UUID: - if isinstance(data, memoryview): - data = bytes(data) - return UUID(data.decode()) + return UUID((bytes(data) if isinstance(data, memoryview) else data).decode()) class UUIDBinaryLoader(UUIDLoader): format = Format.BINARY def load(self, data: Buffer) -> uuid.UUID: - if isinstance(data, memoryview): - data = bytes(data) - return UUID(bytes=data) + return UUID(bytes=(bytes(data) if isinstance(data, memoryview) else data)) def register_default_adapters(context: AdaptContext) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg/pyproject.toml new/psycopg-3.3.4/psycopg/pyproject.toml --- old/psycopg-3.3.3/psycopg/pyproject.toml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg/pyproject.toml 2026-05-01 22:41:15.000000000 +0200 @@ -7,7 +7,7 @@ description = "PostgreSQL database adapter for Python" # STOP AND READ! if you change: -version = "3.3.3" +version = "3.3.4" # also change: # - `docs/news.rst` to declare this as the current version or an unreleased one; # - `psycopg_c/pyproject.toml` to the same version; @@ -60,10 +60,10 @@ [project.optional-dependencies] c = [ - "psycopg-c == 3.3.3; implementation_name != \"pypy\"", + "psycopg-c == 3.3.4; implementation_name != \"pypy\"", ] binary = [ - "psycopg-binary == 3.3.3; implementation_name != \"pypy\"", + "psycopg-binary == 3.3.4; implementation_name != \"pypy\"", ] pool = [ "psycopg-pool", @@ -93,10 +93,10 @@ "wheel >= 0.37", ] docs = [ - "Sphinx >= 5.0", - "furo == 2022.6.21", - "sphinx-autobuild >= 2021.3.14", - "sphinx-autodoc-typehints >= 1.12", + "Sphinx >= 9.1", + "furo == 2025.12.19", + "sphinx-autobuild >= 2025.8.25", + "sphinx-autodoc-typehints >= 3.10.2", ] [tool.setuptools] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg_c/build_backend/psycopg_build_ext.py new/psycopg-3.3.4/psycopg_c/build_backend/psycopg_build_ext.py --- old/psycopg-3.3.3/psycopg_c/build_backend/psycopg_build_ext.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg_c/build_backend/psycopg_build_ext.py 2026-05-01 22:41:15.000000000 +0200 @@ -9,10 +9,12 @@ import os import sys +import logging import subprocess as sp -from distutils import log from distutils.command.build_ext import build_ext +log = logging.getLogger(__name__) + def get_config(what: str) -> str: pg_config = "pg_config" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg_c/psycopg_c/_psycopg/generators.pyx new/psycopg-3.3.4/psycopg_c/psycopg_c/_psycopg/generators.pyx --- old/psycopg-3.3.3/psycopg_c/psycopg_c/_psycopg/generators.pyx 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg_c/psycopg_c/_psycopg/generators.pyx 2026-05-01 22:41:15.000000000 +0200 @@ -38,7 +38,7 @@ cdef int conn_status = libpq.PQstatus(pgconn_ptr) cdef int poll_status cdef object wait, ready - cdef float deadline = 0.0 + cdef double deadline = 0.0 if timeout: deadline = monotonic() + timeout @@ -89,7 +89,7 @@ def cancel(pq.PGcancelConn cancel_conn, *, timeout: float = 0.0) -> PQGenConn[None]: cdef libpq.PGcancelConn *pgcancelconn_ptr = cancel_conn.pgcancelconn_ptr cdef int status - cdef float deadline = 0.0 + cdef double deadline = 0.0 if timeout: deadline = monotonic() + timeout diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg_c/pyproject.toml new/psycopg-3.3.4/psycopg_c/pyproject.toml --- old/psycopg-3.3.3/psycopg_c/pyproject.toml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg_c/pyproject.toml 2026-05-01 22:41:15.000000000 +0200 @@ -24,7 +24,7 @@ [project] name = "psycopg-c" description = "PostgreSQL database adapter for Python -- C optimisation distribution" -version = "3.3.3" +version = "3.3.4" license = "LGPL-3.0-only" license-files = ["LICENSE.txt"] classifiers = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg_pool/psycopg_pool/base.py new/psycopg-3.3.4/psycopg_pool/psycopg_pool/base.py --- old/psycopg-3.3.3/psycopg_pool/psycopg_pool/base.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg_pool/psycopg_pool/base.py 2026-05-01 22:41:15.000000000 +0200 @@ -11,8 +11,6 @@ from typing import TYPE_CHECKING, Any from collections import Counter, deque -from psycopg import errors as e - from .errors import PoolClosed if TYPE_CHECKING: @@ -126,15 +124,13 @@ if max_size < min_size: raise ValueError("max_size must be greater or equal than min_size") if min_size == max_size == 0: - raise ValueError("if min_size is 0 max_size must be greater or than 0") + raise ValueError("if min_size is 0 max_size must be greater than 0") return min_size, max_size def _check_open(self) -> None: if self._closed and self._opened: - raise e.OperationalError( - "pool has already been opened/closed and cannot be reused" - ) + raise PoolClosed("pool has already been opened/closed and cannot be reused") def _check_open_getconn(self) -> None: if self._closed: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg_pool/psycopg_pool/pool.py new/psycopg-3.3.4/psycopg_pool/psycopg_pool/pool.py --- old/psycopg-3.3.3/psycopg_pool/psycopg_pool/pool.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg_pool/psycopg_pool/pool.py 2026-05-01 22:41:15.000000000 +0200 @@ -105,6 +105,10 @@ num_workers=num_workers, ) + # Construct the lock during single-threaded `__init__` so that + # threads concurrently calling `open()` can't race on it. + self._lock = Lock() + if open is None: open = self._open_implicit = True @@ -261,6 +265,8 @@ try: conn = pos.wait(timeout=timeout) except CLIENT_EXCEPTIONS: + if pos.conn: + self.run_task(ReturnConnection(self, pos.conn, from_getconn=True)) self._stats[self._REQUESTS_ERRORS] += 1 raise finally: @@ -378,8 +384,6 @@ because the pool was initialized with *open* = `!True`) but you cannot currently re-open a closed pool. """ - # Make sure the lock is created after there is an event loop - self._ensure_lock() with self._lock: self._open() @@ -393,9 +397,6 @@ self._check_open() - # A lock has been most likely, but not necessarily, created in `open()`. - self._ensure_lock() - # Create these objects now to attach them to the right loop. # See #219 self._tasks = Queue() @@ -407,17 +408,6 @@ self._start_workers() self._start_initial_tasks() - def _ensure_lock(self) -> None: - """Make sure the pool lock is created. - - In async code, also make sure that the loop is running. - """ - - try: - self._lock - except AttributeError: - self._lock = Lock() - def _start_workers(self) -> None: self._sched_runner = spawn(self._sched.run, name=f"{self.name}-scheduler") assert not self._workers @@ -894,11 +884,11 @@ except CLIENT_EXCEPTIONS as ex: self.error = ex - if self.conn: - return self.conn - else: - assert self.error + if self.error: raise self.error + else: + assert self.conn + return self.conn def set(self, conn: CT) -> bool: """Signal the client waiting that a connection is ready. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg_pool/psycopg_pool/pool_async.py new/psycopg-3.3.4/psycopg_pool/psycopg_pool/pool_async.py --- old/psycopg-3.3.3/psycopg_pool/psycopg_pool/pool_async.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg_pool/psycopg_pool/pool_async.py 2026-05-01 22:41:15.000000000 +0200 @@ -117,6 +117,10 @@ if True: # ASYNC if open: self._warn_open_async() + else: + # Construct the lock during single-threaded `__init__` so that + # threads concurrently calling `open()` can't race on it. + self._lock = ALock() if open is None: open = self._open_implicit = True @@ -298,6 +302,8 @@ try: conn = await pos.wait(timeout=timeout) except CLIENT_EXCEPTIONS: + if pos.conn: + self.run_task(ReturnConnection(self, pos.conn, from_getconn=True)) self._stats[self._REQUESTS_ERRORS] += 1 raise finally: @@ -416,8 +422,9 @@ because the pool was initialized with *open* = `!True`) but you cannot currently re-open a closed pool. """ - # Make sure the lock is created after there is an event loop - self._ensure_lock() + if True: # ASYNC + # Make sure the lock is created after there is an event loop + self._ensure_lock() async with self._lock: self._open() @@ -431,8 +438,10 @@ self._check_open() - # A lock has been most likely, but not necessarily, created in `open()`. - self._ensure_lock() + if True: # ASYNC + # A lock has been most likely, but not necessarily, created + # in `open()`. The sync pool creates it in `__init__()`. + self._ensure_lock() # Create these objects now to attach them to the right loop. # See #219 @@ -445,12 +454,16 @@ self._start_workers() self._start_initial_tasks() - def _ensure_lock(self) -> None: - """Make sure the pool lock is created. + if True: # ASYNC + + def _ensure_lock(self) -> None: + """Make sure the pool lock is created and the loop is running.""" + try: + self._lock + return + except AttributeError: + pass - In async code, also make sure that the loop is running. - """ - if True: # ASYNC try: asyncio.get_running_loop() except RuntimeError: @@ -458,9 +471,6 @@ f"{type(self).__name__} open with no running loop" ) from None - try: - self._lock - except AttributeError: self._lock = ALock() def _start_workers(self) -> None: @@ -957,11 +967,11 @@ except CLIENT_EXCEPTIONS as ex: self.error = ex - if self.conn: - return self.conn - else: - assert self.error + if self.error: raise self.error + else: + assert self.conn + return self.conn async def set(self, conn: ACT) -> bool: """Signal the client waiting that a connection is ready. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/psycopg_pool/pyproject.toml new/psycopg-3.3.4/psycopg_pool/pyproject.toml --- old/psycopg-3.3.3/psycopg_pool/pyproject.toml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/psycopg_pool/pyproject.toml 2026-05-01 22:41:15.000000000 +0200 @@ -7,7 +7,7 @@ description = "Connection Pool for Psycopg" # STOP AND READ! if you change: -version = "3.3.1.dev1" +version = "3.3.1" # also change: # - `docs/news_pool.rst` to declare this version current or unreleased diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/pyproject.toml new/psycopg-3.3.4/pyproject.toml --- old/psycopg-3.3.3/pyproject.toml 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/pyproject.toml 2026-05-01 22:41:15.000000000 +0200 @@ -42,6 +42,16 @@ | tests/test_tstring\.py )''' +# Don't use SQLite cache as it highlights Mypy concurrency issues. +# See https://github.com/python/mypy/issues/21136 +# The issue is triggered by parallel mypy runs under pre-commit, setting +# the parameter here allows the use of the same cache both by pre-commit and +# every other Mypy usage. +# +# Check https://github.com/python/mypy/issues/13916 in the future to see +# if new ways to run mypy under pre-commit emerge. +sqlite_cache = false + [[tool.mypy.overrides]] module = [ "numpy.*", @@ -68,6 +78,3 @@ length_sort = true multi_line_output = 9 sort_order = "psycopg" # requires the isort-psycopg module - -[tool.black] -force_exclude = "tests/test_tstring.py" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/constraints.txt new/psycopg-3.3.4/tests/constraints.txt --- old/psycopg-3.3.3/tests/constraints.txt 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/constraints.txt 2026-05-01 22:41:15.000000000 +0200 @@ -24,10 +24,10 @@ wheel == 0.37 # From the 'docs' extra -Sphinx == 5.0 -furo == 2022.6.21 -sphinx-autobuild == 2021.3.14 -sphinx-autodoc-typehints == 1.12.0 +Sphinx == 9.1.0 +furo == 2025.12.19 +sphinx-autobuild == 2025.8.25 +sphinx-autodoc-typehints == 3.10.2 # Build tools wheel == 0.37 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/fix_crdb.py new/psycopg-3.3.4/tests/fix_crdb.py --- old/psycopg-3.3.3/tests/fix_crdb.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/fix_crdb.py 2026-05-01 22:41:15.000000000 +0200 @@ -86,6 +86,7 @@ "batch statements": 44803, "begin_read_only": 87012, "binary decimal": 82492, + "broken regtype": 169272, "cancel": 41335, "cast adds tz": 51692, "cidr": 18846, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/pool/test_pool.py new/psycopg-3.3.4/tests/pool/test_pool.py --- old/psycopg-3.3.3/tests/pool/test_pool.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/pool/test_pool.py 2026-05-01 22:41:15.000000000 +0200 @@ -15,8 +15,9 @@ from psycopg.pq import TransactionStatus from psycopg.rows import Row, TupleRow, class_row +from .. import acompat from ..utils import assert_type, set_autocommit, skip_free_threaded -from ..acompat import Event, gather, skip_sync, sleep, spawn +from ..acompat import Event, gather, sleep, spawn from .test_pool_common import delay_connection try: @@ -520,7 +521,9 @@ @pytest.mark.slow @pytest.mark.timing [email protected]("async_cb", [pytest.param(True, marks=skip_sync), False]) [email protected]( + "async_cb", [pytest.param(True, marks=acompat.skip_sync), False] +) def test_reconnect_failure(proxy, async_cb): proxy.start() @@ -937,60 +940,6 @@ logger.setLevel(old_level) -@skip_sync -def test_cancellation_in_queue(dsn): - # https://github.com/psycopg/psycopg/issues/509 - - nconns = 3 - - with pool.ConnectionPool(dsn, min_size=nconns, timeout=1) as p: - p.wait() - - got_conns = [] - ev = Event() - - def worker(i): - try: - logging.info("worker %s started", i) - with p.connection() as conn: - logging.info("worker %s got conn", i) - cur = conn.execute("select 1") - assert cur.fetchone() == (1,) - - got_conns.append(conn) - if len(got_conns) >= nconns: - ev.set() - - sleep(5) - except BaseException as ex: - logging.info("worker %s stopped: %r", i, ex) - raise - - # Start tasks taking up all the connections and getting in the queue - tasks = [spawn(worker, (i,)) for i in range(nconns * 3)] - - # wait until the pool has served all the connections and clients are queued. - assert ev.wait(3.0) - for i in range(10): - if p.get_stats().get("requests_queued", 0): - break - else: - sleep(0.1) - else: - pytest.fail("no client got in the queue") - - [task.cancel() for task in reversed(tasks)] - gather(*tasks, return_exceptions=True, timeout=1.0) - - stats = p.get_stats() - assert stats["pool_available"] == 3 - assert stats.get("requests_waiting", 0) == 0 - - with p.connection() as conn: - cur = conn.execute("select 1") - assert cur.fetchone() == (1,) - - @pytest.mark.slow @pytest.mark.timing def test_check_backoff(dsn, caplog, monkeypatch): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/pool/test_pool_async.py new/psycopg-3.3.4/tests/pool/test_pool_async.py --- old/psycopg-3.3.3/tests/pool/test_pool_async.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/pool/test_pool_async.py 2026-05-01 22:41:15.000000000 +0200 @@ -12,6 +12,7 @@ from psycopg.pq import TransactionStatus from psycopg.rows import Row, TupleRow, class_row +from .. import acompat from ..utils import assert_type, set_autocommit, skip_free_threaded from ..acompat import AEvent, asleep, gather, skip_sync, spawn from .test_pool_common_async import delay_connection @@ -522,7 +523,9 @@ @pytest.mark.slow @pytest.mark.timing [email protected]("async_cb", [pytest.param(True, marks=skip_sync), False]) [email protected]( + "async_cb", [pytest.param(True, marks=acompat.skip_sync), False] +) async def test_reconnect_failure(proxy, async_cb): proxy.start() @@ -993,6 +996,69 @@ assert await cur.fetchone() == (1,) +@skip_sync [email protected]_skip("backend pid") +async def test_cancelled_waiter_assigned_conn_is_reclaimed(dsn, monkeypatch): + from asyncio import CancelledError + + from psycopg_pool.pool_async import WaitingClient + + from .test_pool_common_async import ensure_waiting + + assigned = AEvent() + release = AEvent() + + async def set_blocked(self, conn): + async with self._cond: + if self.conn or self.error: + return False + + self.conn = conn + assigned.set() + await release.wait() + self._cond.notify_all() + return True + + monkeypatch.setattr(WaitingClient, "set", set_blocked) + + async with pool.AsyncConnectionPool(dsn, min_size=1, max_size=1, timeout=1) as p: + await p.wait() + + held_conn = await p.getconn() + held_pid = held_conn.info.backend_pid + waiter = spawn(p.getconn) + await ensure_waiting(p) + + putter = spawn(p.putconn, args=(held_conn,)) + await assigned.wait() + + waiter.cancel() + release.set() + + try: + unexpected_conn = await waiter + except CancelledError: + pass + else: + await p.putconn(unexpected_conn) + pytest.fail("cancelled waiter returned a connection instead of raising") + + await gather(putter) + + stats = p.get_stats() + assert stats["pool_available"] == 1 + assert stats.get("requests_waiting", 0) == 0 + assert stats["requests_errors"] == 1 + + reclaimed_conn = await p.getconn() + try: + assert reclaimed_conn.info.backend_pid == held_pid + cur = await reclaimed_conn.execute("select 1") + assert await cur.fetchone() == (1,) + finally: + await p.putconn(reclaimed_conn) + + @pytest.mark.slow @pytest.mark.timing async def test_check_backoff(dsn, caplog, monkeypatch): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/pool/test_pool_common.py new/psycopg-3.3.4/tests/pool/test_pool_common.py --- old/psycopg-3.3.3/tests/pool/test_pool_common.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/pool/test_pool_common.py 2026-05-01 22:41:15.000000000 +0200 @@ -6,14 +6,13 @@ import logging from time import time from typing import Any -from asyncio import CancelledError import pytest import psycopg from ..utils import set_autocommit -from ..acompat import Event, gather, is_alive, skip_async, skip_sync, sleep, spawn +from ..acompat import Event, gather, is_alive, skip_async, sleep, spawn try: import psycopg_pool as pool @@ -492,7 +491,7 @@ assert p._sched_runner is None assert not p._workers - with pytest.raises(psycopg.OperationalError, match="cannot be reused"): + with pytest.raises(pool.PoolClosed, match="cannot be reused"): p.open() @@ -640,112 +639,6 @@ assert time() - t0 <= 1.5 -@skip_sync -def test_cancellation_in_queue(pool_cls, dsn): - # https://github.com/psycopg/psycopg/issues/509 - - nconns = 3 - - with pool_cls( - dsn, min_size=min_size(pool_cls, nconns), max_size=nconns, timeout=1 - ) as p: - p.wait() - - got_conns = [] - ev = Event() - - def worker(i): - try: - logging.info("worker %s started", i) - with p.connection() as conn: - logging.info("worker %s got conn", i) - cur = conn.execute("select 1") - assert cur.fetchone() == (1,) - - got_conns.append(conn) - if len(got_conns) >= nconns: - ev.set() - - sleep(5) - except BaseException as ex: - logging.info("worker %s stopped: %r", i, ex) - raise - - # Start tasks taking up all the connections and getting in the queue - tasks = [spawn(worker, (i,)) for i in range(nconns * 3)] - - # wait until the pool has served all the connections and clients are queued. - assert ev.wait(3.0) - for i in range(10): - if p.get_stats().get("requests_queued", 0): - break - else: - sleep(0.1) - else: - pytest.fail("no client got in the queue") - - [task.cancel() for task in reversed(tasks)] - gather(*tasks, return_exceptions=True, timeout=1.0) - - stats = p.get_stats() - assert stats["pool_available"] == min_size(pool_cls, nconns) - assert stats.get("requests_waiting", 0) == 0 - - with p.connection() as conn: - cur = conn.execute("select 1") - assert cur.fetchone() == (1,) - - -@skip_sync -def test_cancel_on_check(pool_cls, dsn): - do_cancel = True - - def check(conn): - nonlocal do_cancel - if do_cancel: - do_cancel = False - raise CancelledError() - - pool_cls.check_connection(conn) - - with pool_cls(dsn, min_size=min_size(pool_cls, 1), check=check, timeout=1.0) as p: - try: - with p.connection() as conn: - conn.execute("select 1") - except CancelledError: - pass - - with p.connection() as conn: - conn.execute("select 1") - - -@skip_sync -def test_cancel_on_rollback(pool_cls, dsn, monkeypatch): - do_cancel = False - - with pool_cls(dsn, min_size=min_size(pool_cls, 1), timeout=1.0) as p: - with p.connection() as conn: - - def rollback(self): - if do_cancel: - raise CancelledError() - else: - type(self).rollback(self) - - monkeypatch.setattr(type(conn), "rollback", rollback) - conn.execute("select 1") - - do_cancel = True - with pytest.raises((psycopg.errors.SyntaxError, CancelledError)): - with p.connection() as conn: - conn.execute("selexx 2") - - do_cancel = False - with p.connection() as conn: - cur = conn.execute("select 3") - assert cur.fetchone() == (3,) - - @pytest.mark.crdb_skip("backend pid") def test_drain(pool_cls, dsn): pids1 = set() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/pool/test_pool_common_async.py new/psycopg-3.3.4/tests/pool/test_pool_common_async.py --- old/psycopg-3.3.3/tests/pool/test_pool_common_async.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/pool/test_pool_common_async.py 2026-05-01 22:41:15.000000000 +0200 @@ -3,7 +3,6 @@ import logging from time import time from typing import Any -from asyncio import CancelledError import pytest @@ -505,7 +504,7 @@ assert p._sched_runner is None assert not p._workers - with pytest.raises(psycopg.OperationalError, match="cannot be reused"): + with pytest.raises(pool.PoolClosed, match="cannot be reused"): await p.open() @@ -709,6 +708,8 @@ @skip_sync async def test_cancel_on_check(pool_cls, dsn): + from asyncio import CancelledError + do_cancel = True async def check(conn): @@ -734,6 +735,8 @@ @skip_sync async def test_cancel_on_rollback(pool_cls, dsn, monkeypatch): + from asyncio import CancelledError + do_cancel = False async with pool_cls(dsn, min_size=min_size(pool_cls, 1), timeout=1.0) as p: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/pool/test_pool_null.py new/psycopg-3.3.4/tests/pool/test_pool_null.py --- old/psycopg-3.3.3/tests/pool/test_pool_null.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/pool/test_pool_null.py 2026-05-01 22:41:15.000000000 +0200 @@ -14,7 +14,7 @@ from psycopg.rows import Row, TupleRow, class_row from ..utils import assert_type, set_autocommit -from ..acompat import Event, gather, skip_sync, sleep, spawn +from ..acompat import gather, sleep, spawn from .test_pool_common import delay_connection, ensure_waiting try: @@ -446,59 +446,6 @@ assert 200 <= stats["connections_ms"] < 300 -@skip_sync -def test_cancellation_in_queue(dsn): - # https://github.com/psycopg/psycopg/issues/509 - - nconns = 3 - - with pool.NullConnectionPool(dsn, min_size=0, max_size=nconns, timeout=1) as p: - p.wait() - - got_conns = [] - ev = Event() - - def worker(i): - try: - logging.info("worker %s started", i) - with p.connection() as conn: - logging.info("worker %s got conn", i) - cur = conn.execute("select 1") - assert cur.fetchone() == (1,) - - got_conns.append(conn) - if len(got_conns) >= nconns: - ev.set() - - sleep(5) - except BaseException as ex: - logging.info("worker %s stopped: %r", i, ex) - raise - - # Start tasks taking up all the connections and getting in the queue - tasks = [spawn(worker, (i,)) for i in range(nconns * 3)] - - # wait until the pool has served all the connections and clients are queued. - assert ev.wait(3.0) - for i in range(10): - if p.get_stats().get("requests_queued", 0): - break - else: - sleep(0.1) - else: - pytest.fail("no client got in the queue") - - [task.cancel() for task in reversed(tasks)] - gather(*tasks, return_exceptions=True, timeout=1.0) - - stats = p.get_stats() - assert stats.get("requests_waiting", 0) == 0 - - with p.connection() as conn: - cur = conn.execute("select 1") - assert cur.fetchone() == (1,) - - def test_close_returns(dsn): # Mostly test the interface; close is close even if it goes via putconn(). with pool.NullConnectionPool(dsn, close_returns=True) as p: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/pool/test_pool_null_async.py new/psycopg-3.3.4/tests/pool/test_pool_null_async.py --- old/psycopg-3.3.3/tests/pool/test_pool_null_async.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/pool/test_pool_null_async.py 2026-05-01 22:41:15.000000000 +0200 @@ -11,7 +11,7 @@ from psycopg.rows import Row, TupleRow, class_row from ..utils import assert_type, set_autocommit -from ..acompat import AEvent, asleep, gather, skip_sync, spawn +from ..acompat import asleep, gather, skip_sync, spawn from .test_pool_common_async import delay_connection, ensure_waiting try: @@ -447,6 +447,8 @@ @skip_sync async def test_cancellation_in_queue(dsn): + from ..acompat import AEvent + # https://github.com/psycopg/psycopg/issues/509 nconns = 3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/test_connection.py new/psycopg-3.3.4/tests/test_connection.py --- old/psycopg-3.3.3/tests/test_connection.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/test_connection.py 2026-05-01 22:41:15.000000000 +0200 @@ -19,7 +19,7 @@ from psycopg.conninfo import conninfo_to_dict, make_conninfo, timeout_from_conninfo from psycopg._conninfo_utils import get_param -from .acompat import skip_async, skip_sync, sleep +from .acompat import skip_async, sleep from .test_adapt import make_bin_dumper, make_dumper from ._test_cursor import my_row_factory from ._test_connection import testctx # noqa: F401 # fixture @@ -122,6 +122,20 @@ assert elapsed == pytest.approx(2.0, 0.1) [email protected](pq.__impl__ == "python", reason="only affects C extension") +def test_connect_timeout_large_monotonic(conn_cls, dsn, monkeypatch): + # Regression: deadline was stored as C float (32-bit). At ~777 days of + # process uptime the float32 ULP reaches 8 s, so a 2-second timeout is + # silently rounded away, causing ConnectionTimeout to fire immediately. + # 2^26 = 67_108_864 s ≈ 777 days is the minimum value where this occurs + # with psycopg's enforced 2-second minimum connect_timeout. + from psycopg._cmodule import _psycopg + + monkeypatch.setattr(_psycopg, "monotonic", lambda: 67108864.5) + with conn_cls.connect(dsn, connect_timeout=2): + pass + + @pytest.mark.slow @pytest.mark.timing def test_multi_hosts(conn_cls, proxy, dsn, monkeypatch): @@ -412,13 +426,6 @@ assert conn.pgconn.transaction_status == pq.TransactionStatus.INTRANS -@skip_sync -def test_autocommit_readonly_property(conn): - with pytest.raises(AttributeError): - conn.autocommit = True - assert not conn.autocommit - - def test_autocommit(conn): assert conn.autocommit is False conn.set_autocommit(True) @@ -702,13 +709,6 @@ assert current == default -@skip_sync [email protected]("param", tx_params) -def test_transaction_param_readonly_property(conn, param): - with pytest.raises(AttributeError): - setattr(conn, param.name, None) - - @pytest.mark.parametrize("autocommit", [True, False]) @pytest.mark.parametrize("param", tx_params_isolation) def test_set_transaction_param_implicit(conn, param, autocommit): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/test_connection_async.py new/psycopg-3.3.4/tests/test_connection_async.py --- old/psycopg-3.3.3/tests/test_connection_async.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/test_connection_async.py 2026-05-01 22:41:15.000000000 +0200 @@ -117,6 +117,20 @@ assert elapsed == pytest.approx(2.0, 0.1) [email protected](pq.__impl__ == "python", reason="only affects C extension") +async def test_connect_timeout_large_monotonic(aconn_cls, dsn, monkeypatch): + # Regression: deadline was stored as C float (32-bit). At ~777 days of + # process uptime the float32 ULP reaches 8 s, so a 2-second timeout is + # silently rounded away, causing ConnectionTimeout to fire immediately. + # 2^26 = 67_108_864 s ≈ 777 days is the minimum value where this occurs + # with psycopg's enforced 2-second minimum connect_timeout. + from psycopg._cmodule import _psycopg + + monkeypatch.setattr(_psycopg, "monotonic", lambda: 67108864.5) + async with await aconn_cls.connect(dsn, connect_timeout=2): + pass + + @pytest.mark.slow @pytest.mark.timing async def test_multi_hosts(aconn_cls, proxy, dsn, monkeypatch): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/test_cursor_common.py new/psycopg-3.3.4/tests/test_cursor_common.py --- old/psycopg-3.3.3/tests/test_cursor_common.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/test_cursor_common.py 2026-05-01 22:41:15.000000000 +0200 @@ -150,6 +150,9 @@ cur.execute("select generate_series(1, 10)") assert cur.statusmessage == "SELECT 10" + cur.execute("") + assert cur.statusmessage is None + cur.execute("create table statusmessage ()") assert cur.statusmessage == "CREATE TABLE" @@ -398,6 +401,26 @@ assert cur.rowcount == 2 [email protected]("returning", [False, True]) +def test_executemany_statusmessage(conn, execmany, returning): + cur = conn.cursor() + cur.executemany( + ph(cur, "insert into execmany(num, data) values (%s, %s)"), + [(10, "hello"), (20, "world")], + returning=returning, + ) + assert cur.rowcount == (1 if returning else 2) + assert cur.statusmessage is not None + assert cur.statusmessage.startswith("INSERT") + + cur.executemany( + ph(cur, "insert into execmany(num, data) values (%s, %s)"), + [], + returning=returning, + ) + assert cur.statusmessage is None + + def test_executemany_returning(conn, execmany): cur = conn.cursor() cur.executemany( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/test_cursor_common_async.py new/psycopg-3.3.4/tests/test_cursor_common_async.py --- old/psycopg-3.3.3/tests/test_cursor_common_async.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/test_cursor_common_async.py 2026-05-01 22:41:15.000000000 +0200 @@ -148,6 +148,9 @@ await cur.execute("select generate_series(1, 10)") assert cur.statusmessage == "SELECT 10" + await cur.execute("") + assert cur.statusmessage is None + await cur.execute("create table statusmessage ()") assert cur.statusmessage == "CREATE TABLE" @@ -400,6 +403,26 @@ assert cur.rowcount == 2 [email protected]("returning", [False, True]) +async def test_executemany_statusmessage(aconn, execmany, returning): + cur = aconn.cursor() + await cur.executemany( + ph(cur, "insert into execmany(num, data) values (%s, %s)"), + [(10, "hello"), (20, "world")], + returning=returning, + ) + assert cur.rowcount == (1 if returning else 2) + assert cur.statusmessage is not None + assert cur.statusmessage.startswith("INSERT") + + await cur.executemany( + ph(cur, "insert into execmany(num, data) values (%s, %s)"), + [], + returning=returning, + ) + assert cur.statusmessage is None + + async def test_executemany_returning(aconn, execmany): cur = aconn.cursor() await cur.executemany( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/test_sql.py new/psycopg-3.3.4/tests/test_sql.py --- old/psycopg-3.3.3/tests/test_sql.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/test_sql.py 2026-05-01 22:41:15.000000000 +0200 @@ -401,7 +401,7 @@ assert sql.Literal("foo").as_string(conn) == "'foo'" @pytest.mark.crdb_skip("composite") # create type, actually - @pytest.mark.parametrize("name", ["a-b", f"{eur}", "order", "foo bar"]) + @pytest.mark.parametrize("name", ["a-b", f"{eur}", "order", "foo bar", "FooBar"]) def test_invalid_name(self, conn, name): if conn.info.parameter_status("is_superuser") != "on": pytest.skip("not a superuser") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/test_tstring.py new/psycopg-3.3.4/tests/test_tstring.py --- old/psycopg-3.3.3/tests/test_tstring.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/test_tstring.py 2026-05-01 22:41:15.000000000 +0200 @@ -16,7 +16,7 @@ async def test_connection_no_params(aconn): with pytest.raises(TypeError): - await aconn.execute(t"select 1", []) # noqa: F542 + await aconn.execute(t"select 1", []) # noqa: F542 async def test_cursor_no_params(aconn): @@ -170,7 +170,7 @@ lit = sql.Literal(42) cur = await aconn.execute(t"select {lit:l} as foo") assert await cur.fetchone() == (42,) - assert cur._query.query == b'select 42 as foo' + assert cur._query.query == b"select 42 as foo" with pytest.raises(psycopg.ProgrammingError, match=r"sql\.Literal.*':l'"): await aconn.execute(t"select {lit} as foo") @@ -185,7 +185,7 @@ @pytest.mark.xfail(reason="Template.join() needed") async def test_template_join(aconn): ts = [t"{i} as {name:i}" for i, name in enumerate(("foo", "bar", "baz"))] - fields = t','.join(ts) # noqa: F542 + fields = t",".join(ts) # noqa: F542 cur = await aconn.execute(t"select {fields}") assert await cur.fetchone() == (0, 1, 2) assert cur.description[0].name == "foo" @@ -194,7 +194,7 @@ async def test_sql_join(aconn): ts = [t"{i} as {name:i}" for i, name in enumerate(("foo", "bar", "baz"))] - fields = sql.SQL(',').join(ts) + fields = sql.SQL(",").join(ts) cur = await aconn.execute(t"select {fields:q}") assert await cur.fetchone() == (0, 1, 2) assert cur.description[0].name == "foo" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tests/types/test_enum.py new/psycopg-3.3.4/tests/types/test_enum.py --- old/psycopg-3.3.3/tests/types/test_enum.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tests/types/test_enum.py 2026-05-01 22:41:15.000000000 +0200 @@ -2,6 +2,7 @@ import pytest +from psycopg import ClientCursor from psycopg import errors as e from psycopg import pq, sql from psycopg.adapt import PyFormat @@ -36,6 +37,12 @@ THREE = 3 +class CamelCaseEnum(int, Enum): + one = 1 + TWO = 2 + Three = 3 + + enum_cases = [PureTestEnum, StrTestEnum, IntTestEnum] encodings = ["utf8", crdb_encoding("latin1")] @@ -44,10 +51,12 @@ def make_test_enums(request, svcconn): for enum in enum_cases + [NonAsciiEnum]: ensure_enum(enum, svcconn) + ensure_enum(CamelCaseEnum, svcconn, name="CamelCaseEnum") -def ensure_enum(enum, conn): - name = enum.__name__.lower() +def ensure_enum(enum, conn, name=""): + if not name: + name = enum.__name__.lower() labels = list(enum.__members__) conn.execute(sql.SQL(""" drop type if exists {name}; @@ -114,6 +123,20 @@ assert cur.fetchone()[0] == enum[label] [email protected]_skip("broken regtype") [email protected]("fmt_in", PyFormat) +def test_enum_quoted_name(conn, fmt_in): + enum = CamelCaseEnum + + info = EnumInfo.fetch(conn, sql.Identifier(enum.__name__)) + register_enum(info, conn, enum=enum) + + cur = ClientCursor(conn) + for value in enum: + cur.execute(f"select %{fmt_in.value}", [value]) + assert next(cur)[0] is value + + @pytest.mark.crdb_skip("encoding") @pytest.mark.parametrize("enum", enum_cases) @pytest.mark.parametrize("fmt_in", PyFormat) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psycopg-3.3.3/tools/async_to_sync.py new/psycopg-3.3.4/tools/async_to_sync.py --- old/psycopg-3.3.3/tools/async_to_sync.py 2026-02-18 13:12:21.000000000 +0100 +++ new/psycopg-3.3.4/tools/async_to_sync.py 2026-05-01 22:41:15.000000000 +0200 @@ -27,7 +27,7 @@ # The version of Python officially used for the conversion. # Output may differ in other versions. # Should be consistent with the Python version used in lint.yml -PYVER = "3.11" +PYVER = "3.14" ALL_INPUTS = """ psycopg/psycopg/_conninfo_attempts_async.py @@ -183,7 +183,8 @@ ENTRYPOINT ["tools/async_to_sync.py"] """ - cmdline = [engine, "build", "--tag", tag, "-f", "-", str(PROJECT_DIR)] + cmdline = [engine, "build", "--network=host", "--tag", tag, "-f", "-"] + cmdline += [str(PROJECT_DIR)] sp.run(cmdline, check=True, text=True, input=containerfile) cmdline = sys.argv[1:] @@ -347,7 +348,7 @@ "wait_timeout": "wait", } _skip_imports = { - "acompat": {"alist", "anext"}, + "acompat": {"alist", "anext", "skip_sync"}, "_acompat": {"ensure_async"}, } @@ -357,6 +358,10 @@ return node def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST: + for deco in node.decorator_list: + match deco: + case ast.Name(id="skip_sync"): + return None self._fix_docstring(node.body) node.name = self.names_map.get(node.name, node.name) for arg in node.args.args:
