From 7bd6c49f8a58d02ae5a9567d4303f9a8d17b513e Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Tue, 27 Sep 2022 21:57:03 +0200 Subject: [PATCH 01/40] Add arm64 mac and linux wheels (#954) --- .github/workflows/install-postgres.sh | 9 +++-- .github/workflows/release.yml | 52 +++++++++++++++++++-------- tests/test_introspection.py | 2 +- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/.github/workflows/install-postgres.sh b/.github/workflows/install-postgres.sh index c3f27186..4ffbb4d6 100755 --- a/.github/workflows/install-postgres.sh +++ b/.github/workflows/install-postgres.sh @@ -27,11 +27,16 @@ if [ "${ID}" = "debian" -o "${ID}" = "ubuntu" ]; then apt-get install -y --no-install-recommends \ "postgresql-${PGVERSION}" \ "postgresql-contrib-${PGVERSION}" +elif [ "${ID}" = "almalinux" ]; then + yum install -y \ + "postgresql-server" \ + "postgresql-devel" \ + "postgresql-contrib" elif [ "${ID}" = "centos" ]; then - el="EL-${VERSION_ID}-$(arch)" + el="EL-${VERSION_ID%.*}-$(arch)" baseurl="https://download.postgresql.org/pub/repos/yum/reporpms" yum install -y "${baseurl}/${el}/pgdg-redhat-repo-latest.noarch.rpm" - if [ ${VERSION_ID} -ge 8 ]; then + if [ ${VERSION_ID%.*} -ge 8 ]; then dnf -qy module disable postgresql fi yum install -y \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e984a351..01b97a84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: submodules: true - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 - name: Build source distribution run: | @@ -68,19 +68,35 @@ jobs: name: dist path: dist/*.tar.* - build-wheels: + build-wheels-matrix: needs: validate-release-request + runs-on: ubuntu-latest + outputs: + include: ${{ steps.set-matrix.outputs.include }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + - run: pip install cibuildwheel==2.10.2 + - id: set-matrix + run: | + MATRIX_INCLUDE=$( + { + cibuildwheel --print-build-identifiers --platform linux --arch x86_64,aarch64 | grep cp | jq -Rc '{"only": inputs, "os": "ubuntu-latest"}' \ + && cibuildwheel --print-build-identifiers --platform macos --arch x86_64,arm64 | grep cp | jq -Rc '{"only": inputs, "os": "macos-latest"}' \ + && cibuildwheel --print-build-identifiers --platform windows --arch x86,AMD64 | grep cp | jq -Rc '{"only": inputs, "os": "windows-latest"}' + } | jq -sc + ) + echo ::set-output name=include::"$MATRIX_INCLUDE" + + build-wheels: + needs: build-wheels-matrix runs-on: ${{ matrix.os }} + continue-on-error: true + name: Build ${{ matrix.only }} + strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - cibw_python: ["cp36-*", "cp37-*", "cp38-*", "cp39-*", "cp310-*"] - cibw_arch: ["auto64", "auto32"] - exclude: - - os: macos-latest - cibw_arch: "auto32" - - os: ubuntu-latest - cibw_arch: "auto32" + include: ${{ fromJson(needs.build-wheels-matrix.outputs.include) }} defaults: run: @@ -94,12 +110,18 @@ jobs: with: fetch-depth: 50 submodules: true + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v2 - - uses: pypa/cibuildwheel@v2.8.0 + - uses: pypa/cibuildwheel@v2.10.2 + with: + only: ${{ matrix.only }} env: CIBW_BUILD_VERBOSITY: 1 - CIBW_BUILD: ${{ matrix.cibw_python }} - CIBW_ARCHS: ${{ matrix.cibw_arch }} + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 - uses: actions/upload-artifact@v2 with: @@ -107,7 +129,7 @@ jobs: path: wheelhouse/*.whl publish-docs: - needs: validate-release-request + needs: [build-sdist, build-wheels] runs-on: ubuntu-latest env: @@ -121,7 +143,7 @@ jobs: submodules: true - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/tests/test_introspection.py b/tests/test_introspection.py index 56f1d7a3..78561dd0 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -12,7 +12,7 @@ from asyncpg import connection as apg_con -MAX_RUNTIME = 0.1 +MAX_RUNTIME = 0.25 class SlowIntrospectionConnection(apg_con.Connection): From 5f908e679a6264c5fcf8a92895a2f34a9387e4da Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Tue, 27 Sep 2022 13:44:08 -0700 Subject: [PATCH 02/40] Add Python 3.11 to the test matrix (#948) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d61573db..3a95aaf3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: # job. strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2"] os: [ubuntu-latest, macos-latest, windows-latest] loop: [asyncio, uvloop] exclude: From 40b16ea65f8b634a392e7e5b6509ee7dda45c4cd Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 4 Oct 2022 10:28:31 -0700 Subject: [PATCH 03/40] Exclude .venv from flake8 (#958) Virtual environment directories are often named `.venv` by convention. --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 9697fc96..3a8b87a8 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] ignore = E402,E731,W503,W504,E252 -exclude = .git,__pycache__,build,dist,.eggs,.github,.local +exclude = .git,__pycache__,build,dist,.eggs,.github,.local,.venv From 0e73fec27884d94d8205f2d0a71dc74b5ec6fc49 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 11 Oct 2022 11:29:23 -0700 Subject: [PATCH 04/40] Upgrade to flake8 5.0.4 (from 3.9.2) (#961) This moves the project to a more modern version of flake8 (and its dependencies). No new lint issues were identified by this upgrade. --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 332bad3f..78cc0fd3 100644 --- a/setup.py +++ b/setup.py @@ -29,11 +29,7 @@ # Minimal dependencies required to test asyncpg. TEST_DEPENDENCIES = [ - # pycodestyle is a dependency of flake8, but it must be frozen because - # their combination breaks too often - # (example breakage: https://gitlab.com/pycqa/flake8/issues/427) - 'pycodestyle~=2.7.0', - 'flake8~=3.9.2', + 'flake8~=5.0.4', 'uvloop>=0.15.3; platform_system != "Windows" and python_version >= "3.7"', ] From 84c99bfda3885bbbf952e861bc315e9fb4443454 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 11 Oct 2022 14:10:27 -0700 Subject: [PATCH 05/40] Show an example of a custom Record class (#960) This demonstrates a dot-notation implementation as suggested by this FAQ item. --- docs/faq.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 664e49bd..52e5f9e3 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -34,6 +34,12 @@ class that implements dot-notation via the ``record_class`` argument to :func:`connect() ` or any of the Record-returning methods. +.. code-block:: python + + class MyRecord(asyncpg.Record): + def __getattr__(self, name): + return self[name] + Why can't I use a :ref:`cursor ` outside of a transaction? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From eccdf61afb0116f9500f6fb2f832058ba8eb463e Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 11 Oct 2022 15:35:48 -0700 Subject: [PATCH 06/40] Use the exact type name in Record.__repr__ (#959) We support Record subclasses, so include the exact type name (rather than just 'Record') in the repr() string. --- asyncpg/protocol/record/recordobj.c | 29 +++++++++++++++++++++++++---- tests/test_record.py | 1 + 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/asyncpg/protocol/record/recordobj.c b/asyncpg/protocol/record/recordobj.c index 4bf34c8a..c0049217 100644 --- a/asyncpg/protocol/record/recordobj.c +++ b/asyncpg/protocol/record/recordobj.c @@ -451,16 +451,31 @@ record_subscript(ApgRecordObject* o, PyObject* item) } +static const char * +get_typename(PyTypeObject *type) +{ + assert(type->tp_name != NULL); + const char *s = strrchr(type->tp_name, '.'); + if (s == NULL) { + s = type->tp_name; + } + else { + s++; + } + return s; +} + + static PyObject * record_repr(ApgRecordObject *v) { Py_ssize_t i, n; - PyObject *keys_iter; + PyObject *keys_iter, *type_prefix; _PyUnicodeWriter writer; n = Py_SIZE(v); if (n == 0) { - return PyUnicode_FromString(""); + return PyUnicode_FromFormat("<%s>", get_typename(Py_TYPE(v))); } keys_iter = PyObject_GetIter(v->desc->keys); @@ -471,16 +486,22 @@ record_repr(ApgRecordObject *v) i = Py_ReprEnter((PyObject *)v); if (i != 0) { Py_DECREF(keys_iter); - return i > 0 ? PyUnicode_FromString("") : NULL; + if (i > 0) { + return PyUnicode_FromFormat("<%s ...>", get_typename(Py_TYPE(v))); + } + return NULL; } _PyUnicodeWriter_Init(&writer); writer.overallocate = 1; writer.min_length = 12; /* */ - if (_PyUnicodeWriter_WriteASCIIString(&writer, "") self.assertEqual(list(r.items()), [('a', 1), ('b', '2')]) self.assertEqual(list(r.keys()), ['a', 'b']) From bb0cb39de43964599f944c51a35edc4df5cbd6fb Mon Sep 17 00:00:00 2001 From: Bryan Forbes Date: Wed, 26 Oct 2022 14:34:12 -0500 Subject: [PATCH 07/40] Drop Python 3.6 support (#940) --- .github/workflows/tests.yml | 5 +---- README.rst | 2 +- asyncpg/compat.py | 10 ---------- asyncpg/connect_utils.py | 10 +--------- asyncpg/connection.py | 3 +-- asyncpg/pool.py | 4 ---- asyncpg/protocol/scram.pyx | 11 ++--------- docs/index.rst | 2 +- setup.py | 9 ++++----- tests/test_connect.py | 5 ----- tests/test_pool.py | 4 ---- 11 files changed, 11 insertions(+), 54 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3a95aaf3..f451cff8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,13 +17,10 @@ jobs: # job. strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2"] os: [ubuntu-latest, macos-latest, windows-latest] loop: [asyncio, uvloop] exclude: - # uvloop does not support Python 3.6 - - loop: uvloop - python-version: "3.6" # uvloop does not support windows - loop: uvloop os: windows-latest diff --git a/README.rst b/README.rst index 2ed14726..01a28c00 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ of PostgreSQL server binary protocol for use with Python's ``asyncio`` framework. You can read more about asyncpg in an introductory `blog post `_. -asyncpg requires Python 3.6 or later and is supported for PostgreSQL +asyncpg requires Python 3.7 or later and is supported for PostgreSQL versions 9.5 to 14. Older PostgreSQL versions or other databases implementing the PostgreSQL protocol *may* work, but are not being actively tested. diff --git a/asyncpg/compat.py b/asyncpg/compat.py index 348b8caa..29b8e16e 100644 --- a/asyncpg/compat.py +++ b/asyncpg/compat.py @@ -8,10 +8,8 @@ import asyncio import pathlib import platform -import sys -PY_37 = sys.version_info >= (3, 7) SYSTEM = platform.uname().system @@ -36,14 +34,6 @@ def get_pg_home_directory() -> pathlib.Path: return pathlib.Path.home() -if PY_37: - def current_asyncio_task(loop): - return asyncio.current_task(loop) -else: - def current_asyncio_task(loop): - return asyncio.Task.current_task(loop) - - async def wait_closed(stream): # Not all asyncio versions have StreamWriter.wait_closed(). if hasattr(stream, 'wait_closed'): diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 90a61503..40905edf 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -237,10 +237,6 @@ def _parse_hostlist(hostlist, port, *, unquote=False): def _parse_tls_version(tls_version): - if not hasattr(ssl_module, 'TLSVersion'): - raise ValueError( - "TLSVersion is not supported in this version of Python" - ) if tls_version.startswith('SSL'): raise ValueError( f"Unsupported TLS version: {tls_version}" @@ -573,11 +569,7 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl_min_protocol_version ) else: - try: - ssl.minimum_version = _parse_tls_version('TLSv1.2') - except ValueError: - # Python 3.6 does not have ssl.TLSVersion - pass + ssl.minimum_version = _parse_tls_version('TLSv1.2') if ssl_max_protocol_version is None: ssl_max_protocol_version = os.getenv('PGSSLMAXPROTOCOLVERSION') diff --git a/asyncpg/connection.py b/asyncpg/connection.py index ea128aab..365ab416 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -20,7 +20,6 @@ import warnings import weakref -from . import compat from . import connect_utils from . import cursor from . import exceptions @@ -1468,7 +1467,7 @@ async def _cancel(self, waiter): waiter.set_exception(ex) finally: self._cancellations.discard( - compat.current_asyncio_task(self._loop)) + asyncio.current_task(self._loop)) if not waiter.done(): waiter.set_result(None) diff --git a/asyncpg/pool.py b/asyncpg/pool.py index 14e4be7e..9bd2a3e3 100644 --- a/asyncpg/pool.py +++ b/asyncpg/pool.py @@ -43,10 +43,6 @@ def __new__(mcls, name, bases, dct, *, wrap=False): return super().__new__(mcls, name, bases, dct) - def __init__(cls, name, bases, dct, *, wrap=False): - # Needed for Python 3.5 to handle `wrap` class keyword argument. - super().__init__(name, bases, dct) - @staticmethod def _wrap_connection_method(meth_name): def call_con_method(self, *args, **kwargs): diff --git a/asyncpg/protocol/scram.pyx b/asyncpg/protocol/scram.pyx index bfb82f73..765ddd46 100644 --- a/asyncpg/protocol/scram.pyx +++ b/asyncpg/protocol/scram.pyx @@ -9,18 +9,11 @@ import base64 import hashlib import hmac import re +import secrets import stringprep import unicodedata -# try to import the secrets library from Python 3.6+ for the -# cryptographic token generator for generating nonces as part of SCRAM -# Otherwise fall back on os.urandom -try: - from secrets import token_bytes as generate_token_bytes -except ImportError: - from os import urandom as generate_token_bytes - @cython.final cdef class SCRAMAuthentication: """Contains the protocol for generating and a SCRAM hashed password. @@ -198,7 +191,7 @@ cdef class SCRAMAuthentication: cdef: bytes token - token = generate_token_bytes(num_bytes) + token = secrets.token_bytes(num_bytes) return base64.b64encode(token) diff --git a/docs/index.rst b/docs/index.rst index 87c43aa8..ee9f85d4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ PostgreSQL and Python/asyncio. asyncpg is an efficient, clean implementation of PostgreSQL server binary protocol for use with Python's ``asyncio`` framework. -**asyncpg** requires Python 3.6 or later and is supported for PostgreSQL +**asyncpg** requires Python 3.7 or later and is supported for PostgreSQL versions 9.5 to 14. Older PostgreSQL versions or other databases implementing the PostgreSQL protocol *may* work, but are not being actively tested. diff --git a/setup.py b/setup.py index 78cc0fd3..af0bcdc3 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ import sys -if sys.version_info < (3, 6): - raise RuntimeError('asyncpg requires Python 3.6 or greater') +if sys.version_info < (3, 7): + raise RuntimeError('asyncpg requires Python 3.7 or greater') import os import os.path @@ -30,7 +30,7 @@ # Minimal dependencies required to test asyncpg. TEST_DEPENDENCIES = [ 'flake8~=5.0.4', - 'uvloop>=0.15.3; platform_system != "Windows" and python_version >= "3.7"', + 'uvloop>=0.15.3; platform_system != "Windows"', ] # Dependencies required to build documentation. @@ -255,7 +255,6 @@ def finalize_options(self): 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -264,7 +263,7 @@ def finalize_options(self): 'Topic :: Database :: Front-Ends', ], platforms=['macOS', 'POSIX', 'Windows'], - python_requires='>=3.6.0', + python_requires='>=3.7.0', zip_safe=False, author='MagicStack Inc', author_email='hello@magic.io', diff --git a/tests/test_connect.py b/tests/test_connect.py index db7817f6..c8da7e29 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -14,7 +14,6 @@ import shutil import ssl import stat -import sys import tempfile import textwrap import unittest @@ -1466,10 +1465,6 @@ async def test_executemany_uvloop_ssl_issue_700(self): finally: await con.close() - @unittest.skipIf( - sys.version_info < (3, 7), - "Python < 3.7 doesn't have ssl.TLSVersion" - ) async def test_tls_version(self): if self.cluster.get_pg_version() < (12, 0): self.skipTest("PostgreSQL < 12 cannot set ssl protocol version") diff --git a/tests/test_pool.py b/tests/test_pool.py index e2c99efc..b77783e9 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -10,7 +10,6 @@ import os import platform import random -import sys import textwrap import time import unittest @@ -741,7 +740,6 @@ async def test_pool_size_and_capacity(self): self.assertEqual(pool.get_size(), 3) self.assertEqual(pool.get_idle_size(), 0) - @unittest.skipIf(sys.version_info[:2] < (3, 6), 'no asyncgen support') async def test_pool_handles_transaction_exit_in_asyncgen_1(self): pool = await self.create_pool(database='postgres', min_size=1, max_size=1) @@ -763,7 +761,6 @@ class MyException(Exception): async for _ in iterate(con): # noqa raise MyException() - @unittest.skipIf(sys.version_info[:2] < (3, 6), 'no asyncgen support') async def test_pool_handles_transaction_exit_in_asyncgen_2(self): pool = await self.create_pool(database='postgres', min_size=1, max_size=1) @@ -788,7 +785,6 @@ class MyException(Exception): del iterator - @unittest.skipIf(sys.version_info[:2] < (3, 6), 'no asyncgen support') async def test_pool_handles_asyncgen_finalization(self): pool = await self.create_pool(database='postgres', min_size=1, max_size=1) From eab7fdf2f014785cdc2245c8bcb6bc086763b702 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Wed, 26 Oct 2022 14:35:44 -0700 Subject: [PATCH 08/40] Test on Python 3.11 and PostgreSQL 15, fix workflow deprecations (#968) --- .github/workflows/release.yml | 28 ++++++++++++++-------------- .github/workflows/tests.yml | 12 ++++++------ README.rst | 2 +- docs/index.rst | 2 +- tests/test_connect.py | 4 +++- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01b97a84..7f89128e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: mkdir -p dist/ echo "${VERSION}" > dist/VERSION - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: dist path: dist/ @@ -50,7 +50,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 50 submodules: true @@ -63,7 +63,7 @@ jobs: pip install -U setuptools wheel pip python setup.py sdist - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: dist path: dist/*.tar.* @@ -74,7 +74,7 @@ jobs: outputs: include: ${{ steps.set-matrix.outputs.include }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-python@v4 - run: pip install cibuildwheel==2.10.2 - id: set-matrix @@ -86,7 +86,7 @@ jobs: && cibuildwheel --print-build-identifiers --platform windows --arch x86,AMD64 | grep cp | jq -Rc '{"only": inputs, "os": "windows-latest"}' } | jq -sc ) - echo ::set-output name=include::"$MATRIX_INCLUDE" + echo "include=$MATRIX_INCLUDE" >> $GITHUB_OUTPUT build-wheels: needs: build-wheels-matrix @@ -106,11 +106,11 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 50 submodules: true - + - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v2 @@ -123,7 +123,7 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: dist path: wheelhouse/*.whl @@ -137,7 +137,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 5 submodules: true @@ -153,7 +153,7 @@ jobs: make htmldocs - name: Checkout gh-pages - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 5 ref: gh-pages @@ -179,12 +179,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 5 submodules: false - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: name: dist path: dist/ @@ -193,7 +193,7 @@ jobs: id: relver run: | set -e - echo ::set-output name=version::$(cat dist/VERSION) + echo "version=$(cat dist/VERSION)" >> $GITHUB_OUTPUT rm dist/VERSION - name: Merge and tag the PR @@ -219,7 +219,7 @@ jobs: ls -al dist/ - name: Upload to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f451cff8..f2340b5c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: # job. strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, macos-latest, windows-latest] loop: [asyncio, uvloop] exclude: @@ -35,7 +35,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 50 submodules: true @@ -51,7 +51,7 @@ jobs: __version__\s*=\s*(?:['"])([[:PEP440:]])(?:['"]) - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 if: steps.release.outputs.version == 0 with: python-version: ${{ matrix.python-version }} @@ -76,7 +76,7 @@ jobs: test-postgres: strategy: matrix: - postgres-version: ["9.5", "9.6", "10", "11", "12", "13", "14"] + postgres-version: ["9.5", "9.6", "10", "11", "12", "13", "14", "15"] runs-on: ubuntu-latest @@ -84,7 +84,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 50 submodules: true @@ -111,7 +111,7 @@ jobs: >> "${GITHUB_ENV}" - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 if: steps.release.outputs.version == 0 - name: Install Python Deps diff --git a/README.rst b/README.rst index 01a28c00..e5212156 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ framework. You can read more about asyncpg in an introductory `blog post `_. asyncpg requires Python 3.7 or later and is supported for PostgreSQL -versions 9.5 to 14. Older PostgreSQL versions or other databases implementing +versions 9.5 to 15. Older PostgreSQL versions or other databases implementing the PostgreSQL protocol *may* work, but are not being actively tested. diff --git a/docs/index.rst b/docs/index.rst index ee9f85d4..93671abc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,7 +15,7 @@ of PostgreSQL server binary protocol for use with Python's ``asyncio`` framework. **asyncpg** requires Python 3.7 or later and is supported for PostgreSQL -versions 9.5 to 14. Older PostgreSQL versions or other databases implementing +versions 9.5 to 15. Older PostgreSQL versions or other databases implementing the PostgreSQL protocol *may* work, but are not being actively tested. Contents diff --git a/tests/test_connect.py b/tests/test_connect.py index c8da7e29..4903fc03 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1287,6 +1287,7 @@ def setUp(self): create_script = [] create_script.append('CREATE ROLE ssl_user WITH LOGIN;') + create_script.append('GRANT ALL ON SCHEMA public TO ssl_user;') self._add_hba_entry() @@ -1301,6 +1302,7 @@ def tearDown(self): self.cluster.trust_local_connections() drop_script = [] + drop_script.append('REVOKE ALL ON SCHEMA public FROM ssl_user;') drop_script.append('DROP ROLE ssl_user;') drop_script = '\n'.join(drop_script) self.loop.run_until_complete(self.con.execute(drop_script)) @@ -1461,7 +1463,7 @@ async def test_executemany_uvloop_ssl_issue_700(self): ) finally: try: - await con.execute('DROP TABLE test_many') + await con.execute('DROP TABLE IF EXISTS test_many') finally: await con.close() From 925cfe15be00776cb9b1a7d76bca74e0b5b2f4f1 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Wed, 26 Oct 2022 14:37:20 -0700 Subject: [PATCH 09/40] asyncpg v0.27.0 Support Python 3.11 and PostgreSQL 15. This release also drops support for Python 3.6. Changes ======= * Add arm64 mac and linux wheels (by @ddelange in 7bd6c49f for #954) * Add Python 3.11 to the test matrix (by @elprans in 5f908e67 for #948) * Exclude .venv from flake8 (#958) (by @jparise in 40b16ea6 for #958) * Upgrade to flake8 5.0.4 (from 3.9.2) (#961) (by @jparise in 0e73fec2 for #961) * Show an example of a custom Record class (#960) (by @jparise in 84c99bfd for #960) * Use the exact type name in Record.__repr__ (#959) (by @jparise in eccdf61a for #959) * Drop Python 3.6 support (#940) (by @bryanforbes in bb0cb39d for #940) * Test on Python 3.11 and PostgreSQL 15, fix workflow deprecations (#968) (by @elprans in eab7fdf2 for #968) --- asyncpg/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncpg/_version.py b/asyncpg/_version.py index 7e897c90..263da2e2 100644 --- a/asyncpg/_version.py +++ b/asyncpg/_version.py @@ -10,4 +10,4 @@ # supported platforms, publish the packages on PyPI, merge the PR # to the target branch, create a Git tag pointing to the commit. -__version__ = '0.26.0' +__version__ = '0.27.0' From 95cf254c3c0d86b278b702a901ca9e1b2b2d0902 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Wed, 26 Oct 2022 15:37:04 -0700 Subject: [PATCH 10/40] workflows/release: Don't ignore errors in individual wheel jobs --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f89128e..c042ce4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,7 +91,6 @@ jobs: build-wheels: needs: build-wheels-matrix runs-on: ${{ matrix.os }} - continue-on-error: true name: Build ${{ matrix.only }} strategy: From a6e2f183507e774aa75de5b6f325fdb2574a544e Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Wed, 26 Oct 2022 15:37:54 -0700 Subject: [PATCH 11/40] Post-release version bump --- asyncpg/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncpg/_version.py b/asyncpg/_version.py index 263da2e2..15958dc7 100644 --- a/asyncpg/_version.py +++ b/asyncpg/_version.py @@ -10,4 +10,4 @@ # supported platforms, publish the packages on PyPI, merge the PR # to the target branch, create a Git tag pointing to the commit. -__version__ = '0.27.0' +__version__ = '0.27.0.dev0' From 8f6cc98f3f0f159d185e2fd5846b826edfa98048 Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Wed, 26 Oct 2022 15:54:09 -0700 Subject: [PATCH 12/40] workflows: Use python-verion in setup-python actions explicitly Avoids a warning --- .github/workflows/release.yml | 6 +++++- .github/workflows/tests.yml | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c042ce4e..31f844ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,8 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 + with: + python-version: "3.x" - name: Build source distribution run: | @@ -76,6 +78,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + with: + python-version: "3.x" - run: pip install cibuildwheel==2.10.2 - id: set-matrix run: | @@ -144,7 +148,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: "3.x" - name: Build docs run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f2340b5c..a120e9a6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -113,6 +113,8 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 if: steps.release.outputs.version == 0 + with: + python-version: "3.x" - name: Install Python Deps if: steps.release.outputs.version == 0 From 43bd82c54e29352ae08b1982c23b9cb0cc33717d Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Wed, 26 Oct 2022 15:54:38 -0700 Subject: [PATCH 13/40] Correct the development version --- asyncpg/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncpg/_version.py b/asyncpg/_version.py index 15958dc7..693e3bed 100644 --- a/asyncpg/_version.py +++ b/asyncpg/_version.py @@ -10,4 +10,4 @@ # supported platforms, publish the packages on PyPI, merge the PR # to the target branch, create a Git tag pointing to the commit. -__version__ = '0.27.0.dev0' +__version__ = '0.28.0.dev0' From d2e710fe296febb3c370319f87ef8c5b32152002 Mon Sep 17 00:00:00 2001 From: Floris van Nee Date: Mon, 28 Nov 2022 18:29:36 +0100 Subject: [PATCH 14/40] Do not try to cleanup statements (#981) This supports a case where we prepare an unnamed statement to inspect the return types. The statement should not be cleaned up afterwards because it is automatically cleaned up by Postgres --- asyncpg/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 365ab416..73cb6e66 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -1416,6 +1416,7 @@ def _mark_stmts_as_closed(self): def _maybe_gc_stmt(self, stmt): if ( stmt.refs == 0 + and stmt.name and not self._stmt_cache.has( (stmt.query, stmt.record_class, stmt.ignore_custom_codec) ) From 9cb2c1ce044768ef7c396976b8abf896fad901f7 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 2 Dec 2022 23:41:35 -0600 Subject: [PATCH 15/40] Add Pool.is_closing() method (#973) --- asyncpg/pool.py | 7 +++++++ tests/test_pool.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/asyncpg/pool.py b/asyncpg/pool.py index 9bd2a3e3..eaf501f4 100644 --- a/asyncpg/pool.py +++ b/asyncpg/pool.py @@ -446,6 +446,13 @@ async def _initialize(self): await asyncio.gather(*connect_tasks) + def is_closing(self): + """Return ``True`` if the pool is closing or is closed. + + .. versionadded:: 0.28.0 + """ + return self._closed or self._closing + def get_size(self): """Return the current number of connections in this pool. diff --git a/tests/test_pool.py b/tests/test_pool.py index b77783e9..5577632c 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -740,6 +740,17 @@ async def test_pool_size_and_capacity(self): self.assertEqual(pool.get_size(), 3) self.assertEqual(pool.get_idle_size(), 0) + async def test_pool_closing(self): + async with self.create_pool() as pool: + self.assertFalse(pool.is_closing()) + await pool.close() + self.assertTrue(pool.is_closing()) + + async with self.create_pool() as pool: + self.assertFalse(pool.is_closing()) + pool.terminate() + self.assertTrue(pool.is_closing()) + async def test_pool_handles_transaction_exit_in_asyncgen_1(self): pool = await self.create_pool(database='postgres', min_size=1, max_size=1) From 7df9812a068c95e5dd4aa1d0270db8f177ee1e50 Mon Sep 17 00:00:00 2001 From: Anna Date: Sat, 3 Dec 2022 10:42:18 +0500 Subject: [PATCH 16/40] Fix test_tls_version for LibreSSL (#974) Context: https://github.com/python/cpython/issues/78182 --- tests/test_connect.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index 4903fc03..7707a1c9 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1497,13 +1497,14 @@ async def test_tls_version(self): '&ssl_min_protocol_version=TLSv1.1' '&ssl_max_protocol_version=TLSv1.1' ) - with self.assertRaisesRegex(ssl.SSLError, 'no protocols'): - await self.connect( - dsn='postgresql://ssl_user@localhost/postgres' - '?sslmode=require' - '&ssl_min_protocol_version=TLSv1.2' - '&ssl_max_protocol_version=TLSv1.1' - ) + if not ssl.OPENSSL_VERSION.startswith('LibreSSL'): + with self.assertRaisesRegex(ssl.SSLError, 'no protocols'): + await self.connect( + dsn='postgresql://ssl_user@localhost/postgres' + '?sslmode=require' + '&ssl_min_protocol_version=TLSv1.2' + '&ssl_max_protocol_version=TLSv1.1' + ) con = await self.connect( dsn='postgresql://ssl_user@localhost/postgres' '?sslmode=require' From bee17cb8415c76204ec21d25399d39faa5333fd7 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Wed, 7 Dec 2022 22:23:28 +0100 Subject: [PATCH 17/40] Fixed tests so they pass on windows. Are the hosts unique for the replica clusters ? I find it hard to tell, but at least on Windows all clusters are on localhost, so the test was not actually verifying the code. --- tests/test_connect.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index f905e3cd..e8145a80 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -25,7 +25,7 @@ import asyncpg from asyncpg import _testbase as tb -from asyncpg import connection +from asyncpg import connection as pg_connection from asyncpg import connect_utils from asyncpg import cluster as pg_cluster from asyncpg import exceptions @@ -1167,7 +1167,7 @@ async def test_connect_args_validation(self): class TestConnection(tb.ConnectedTestCase): async def test_connection_isinstance(self): - self.assertTrue(isinstance(self.con, connection.Connection)) + self.assertTrue(isinstance(self.con, pg_connection.Connection)) self.assertTrue(isinstance(self.con, object)) self.assertFalse(isinstance(self.con, list)) @@ -1750,23 +1750,23 @@ async def test_no_explicit_close_with_debug(self): class TestConnectionAttributes(tb.HotStandbyTestCase): async def _run_connection_test( - self, connect, target_attribute, expected_host + self, connect, target_attribute, expected_port ): conn = await connect(target_session_attribute=target_attribute) - self.assertTrue(_get_connected_host(conn).startswith(expected_host)) + self.assertTrue(_get_connected_host(conn).endswith(expected_port)) await conn.close() - async def test_target_server_attribute_host(self): - master_host = self.master_cluster.get_connection_spec()['host'] - standby_host = self.standby_cluster.get_connection_spec()['host'] + async def test_target_server_attribute_port(self): + master_port = self.master_cluster.get_connection_spec()['port'] + standby_port = self.standby_cluster.get_connection_spec()['port'] tests = [ - (self.connect_primary, 'primary', master_host), - (self.connect_standby, 'standby', standby_host), + (self.connect_primary, 'primary', master_port), + (self.connect_standby, 'standby', standby_port), ] - for connect, target_attr, expected_host in tests: + for connect, target_attr, expected_port in tests: await self._run_connection_test( - connect, target_attr, expected_host + connect, target_attr, expected_port ) async def test_target_attribute_not_matched(self): @@ -1781,9 +1781,9 @@ async def test_target_attribute_not_matched(self): async def test_prefer_standby_when_standby_is_up(self): con = await self.connect(target_session_attribute='prefer-standby') - standby_host = self.standby_cluster.get_connection_spec()['host'] + standby_port = self.standby_cluster.get_connection_spec()['port'] connected_host = _get_connected_host(con) - self.assertTrue(connected_host.startswith(standby_host)) + self.assertTrue(connected_host.endswith(standby_port)) await con.close() async def test_prefer_standby_picks_master_when_standby_is_down(self): @@ -1791,20 +1791,23 @@ async def test_prefer_standby_picks_master_when_standby_is_down(self): connection_spec = { 'host': [ primary_spec['host'], - '/var/test/a/cluster/that/does/not/exist', + 'unlocalhost', ], - 'port': [primary_spec['port'], 12345], + 'port': [primary_spec['port'], 15345], 'database': primary_spec['database'], 'user': primary_spec['user'], 'target_session_attribute': 'prefer-standby' } - con = await connection.connect(**connection_spec, loop=self.loop) - master_host = self.master_cluster.get_connection_spec()['host'] + con = await self.connect(**connection_spec) + master_port = self.master_cluster.get_connection_spec()['port'] connected_host = _get_connected_host(con) - self.assertTrue(connected_host.startswith(master_host)) + self.assertTrue(connected_host.endswith(master_port)) await con.close() def _get_connected_host(con): - return con._transport.get_extra_info('peername') + peername = con._transport.get_extra_info('peername') + if isinstance(peername, tuple): + peername = "".join((str(s) for s in peername if s)) + return peername From 1c675119fca1c55ce0b331a4133c461510f745a9 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Thu, 8 Dec 2022 08:10:04 +0100 Subject: [PATCH 18/40] push for workflows --- tests/test_connect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_connect.py b/tests/test_connect.py index e8145a80..c766f00e 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1806,6 +1806,7 @@ async def test_prefer_standby_picks_master_when_standby_is_down(self): await con.close() + def _get_connected_host(con): peername = con._transport.get_extra_info('peername') if isinstance(peername, tuple): From fae9d9cf930c2dcfd75c749a78719f81455ba0f2 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Thu, 8 Dec 2022 08:43:43 +0100 Subject: [PATCH 19/40] push for workflows --- tests/test_connect.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index c766f00e..e8145a80 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1806,7 +1806,6 @@ async def test_prefer_standby_picks_master_when_standby_is_down(self): await con.close() - def _get_connected_host(con): peername = con._transport.get_extra_info('peername') if isinstance(peername, tuple): From 7a847ce9c74e7117e9c6b5ec926a524da888f2e3 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Thu, 8 Dec 2022 08:53:09 +0100 Subject: [PATCH 20/40] no image for python 3.6 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d61573db..28e38e4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: # job. strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] os: [ubuntu-latest, macos-latest, windows-latest] loop: [asyncio, uvloop] exclude: From a3d7342e29d9a9e2813ecd81de97f386c817f2f1 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Thu, 8 Dec 2022 09:21:17 +0100 Subject: [PATCH 21/40] merge ci changes from master --- .github/workflows/tests.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 28e38e4e..a120e9a6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,13 +17,10 @@ jobs: # job. strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, macos-latest, windows-latest] loop: [asyncio, uvloop] exclude: - # uvloop does not support Python 3.6 - - loop: uvloop - python-version: "3.6" # uvloop does not support windows - loop: uvloop os: windows-latest @@ -38,7 +35,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 50 submodules: true @@ -54,7 +51,7 @@ jobs: __version__\s*=\s*(?:['"])([[:PEP440:]])(?:['"]) - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 if: steps.release.outputs.version == 0 with: python-version: ${{ matrix.python-version }} @@ -79,7 +76,7 @@ jobs: test-postgres: strategy: matrix: - postgres-version: ["9.5", "9.6", "10", "11", "12", "13", "14"] + postgres-version: ["9.5", "9.6", "10", "11", "12", "13", "14", "15"] runs-on: ubuntu-latest @@ -87,7 +84,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 50 submodules: true @@ -114,8 +111,10 @@ jobs: >> "${GITHUB_ENV}" - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 if: steps.release.outputs.version == 0 + with: + python-version: "3.x" - name: Install Python Deps if: steps.release.outputs.version == 0 From 7d9234f87aff97728f05237b79af25328dd80b70 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Sun, 11 Dec 2022 11:06:26 +0100 Subject: [PATCH 22/40] merge setup.py changes from master --- setup.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 332bad3f..af0bcdc3 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ import sys -if sys.version_info < (3, 6): - raise RuntimeError('asyncpg requires Python 3.6 or greater') +if sys.version_info < (3, 7): + raise RuntimeError('asyncpg requires Python 3.7 or greater') import os import os.path @@ -29,12 +29,8 @@ # Minimal dependencies required to test asyncpg. TEST_DEPENDENCIES = [ - # pycodestyle is a dependency of flake8, but it must be frozen because - # their combination breaks too often - # (example breakage: https://gitlab.com/pycqa/flake8/issues/427) - 'pycodestyle~=2.7.0', - 'flake8~=3.9.2', - 'uvloop>=0.15.3; platform_system != "Windows" and python_version >= "3.7"', + 'flake8~=5.0.4', + 'uvloop>=0.15.3; platform_system != "Windows"', ] # Dependencies required to build documentation. @@ -259,7 +255,6 @@ def finalize_options(self): 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -268,7 +263,7 @@ def finalize_options(self): 'Topic :: Database :: Front-Ends', ], platforms=['macOS', 'POSIX', 'Windows'], - python_requires='>=3.6.0', + python_requires='>=3.7.0', zip_safe=False, author='MagicStack Inc', author_email='hello@magic.io', From a43b0646fdf08777bb92b327d3393ab96dcb0c74 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Mon, 12 Dec 2022 08:33:26 +0100 Subject: [PATCH 23/40] Add logging to server selection procedure --- asyncpg/connect_utils.py | 12 +++++++++--- tests/test_connect.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index a51eb789..97dd8065 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -10,6 +10,7 @@ import enum import functools import getpass +import logging import os import pathlib import platform @@ -30,6 +31,7 @@ from . import exceptions from . import protocol +logger = logging.getLogger(__name__) class SSLMode(enum.IntEnum): disable = 0 @@ -898,11 +900,15 @@ async def can_be_used(connection): if hot_standby_status is not None: is_in_hot_standby = hot_standby_status == 'on' else: - is_in_hot_standby = await connection.fetchval( + is_in_recovery = await connection.fetchval( "SELECT pg_catalog.pg_is_in_recovery()" ) - - return is_in_hot_standby == should_be_in_hot_standby + if is_in_recovery: + logger.warning("Connection {!r} is still in recovery mode".format(connection)) + is_in_hot_standby = not is_in_recovery + connection_eligible = is_in_hot_standby == should_be_in_hot_standby + logger.debug("Connection {!r} is eligible ({!r}). Allow".format(connection, connection_eligible)) + return connection_eligible return can_be_used diff --git a/tests/test_connect.py b/tests/test_connect.py index e8145a80..161b6f0c 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1740,7 +1740,7 @@ async def test_no_explicit_close_with_debug(self): r'unclosed connection') as rw: await self._run_no_explicit_close_test() - msg = rw.warning.args[0] + msg = " ".join(rw.warning.args) self.assertIn(' created at:\n', msg) self.assertIn('in test_no_explicit_close_with_debug', msg) finally: From 24ea4e58e84a8a27092a15b26ee4e561f3d26dac Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Mon, 12 Dec 2022 08:38:37 +0100 Subject: [PATCH 24/40] formatting --- asyncpg/connect_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 97dd8065..e37ce310 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -33,6 +33,7 @@ logger = logging.getLogger(__name__) + class SSLMode(enum.IntEnum): disable = 0 allow = 1 @@ -904,10 +905,14 @@ async def can_be_used(connection): "SELECT pg_catalog.pg_is_in_recovery()" ) if is_in_recovery: - logger.warning("Connection {!r} is still in recovery mode".format(connection)) + logger.warning("Connection {!r} is still in recovery mode" + .format(connection)) is_in_hot_standby = not is_in_recovery connection_eligible = is_in_hot_standby == should_be_in_hot_standby - logger.debug("Connection {!r} is eligible ({!r}). Allow".format(connection, connection_eligible)) + logger.debug( + "Connection {!r} eligible=({!r}). Allow hot standby={!r}". + format(connection, connection_eligible, should_be_in_hot_standby) + ) return connection_eligible return can_be_used From 3df816dca375f6c3ade07b3bce7cdcd4b74edd80 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Mon, 12 Dec 2022 08:51:55 +0100 Subject: [PATCH 25/40] undo mistake --- asyncpg/connect_utils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index e37ce310..57edfac0 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -894,6 +894,8 @@ def _accept_in_hot_standby(should_be_in_hot_standby: bool): """ If the server didn't report "in_hot_standby" at startup, we must determine the state by checking "SELECT pg_catalog.pg_is_in_recovery()". + If the server allows a connection and states it is in recovery it must + be a replica/standby server. """ async def can_be_used(connection): settings = connection.get_settings() @@ -901,13 +903,9 @@ async def can_be_used(connection): if hot_standby_status is not None: is_in_hot_standby = hot_standby_status == 'on' else: - is_in_recovery = await connection.fetchval( + is_in_hot_standby = await connection.fetchval( "SELECT pg_catalog.pg_is_in_recovery()" ) - if is_in_recovery: - logger.warning("Connection {!r} is still in recovery mode" - .format(connection)) - is_in_hot_standby = not is_in_recovery connection_eligible = is_in_hot_standby == should_be_in_hot_standby logger.debug( "Connection {!r} eligible=({!r}). Allow hot standby={!r}". From e24a091d7900e57c34f3757260af8d05f0e747aa Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Mon, 12 Dec 2022 08:58:47 +0100 Subject: [PATCH 26/40] fix test? --- tests/test_connect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index 161b6f0c..a18a22e4 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1464,7 +1464,7 @@ async def test_executemany_uvloop_ssl_issue_700(self): ) finally: try: - await con.execute('DROP TABLE test_many') + await con.execute('DROP TABLE IF EXISTS test_many') finally: await con.close() From 38984a68895697f013316311203bc9ca27b1dc4e Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Mon, 12 Dec 2022 09:27:59 +0100 Subject: [PATCH 27/40] disable tests for pg11 to see if all the rest of the test cases pass --- asyncpg/connect_utils.py | 7 ------- tests/test_connect.py | 6 ++++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 57edfac0..50b49d0a 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -10,7 +10,6 @@ import enum import functools import getpass -import logging import os import pathlib import platform @@ -31,8 +30,6 @@ from . import exceptions from . import protocol -logger = logging.getLogger(__name__) - class SSLMode(enum.IntEnum): disable = 0 @@ -907,10 +904,6 @@ async def can_be_used(connection): "SELECT pg_catalog.pg_is_in_recovery()" ) connection_eligible = is_in_hot_standby == should_be_in_hot_standby - logger.debug( - "Connection {!r} eligible=({!r}). Allow hot standby={!r}". - format(connection, connection_eligible, should_be_in_hot_standby) - ) return connection_eligible return can_be_used diff --git a/tests/test_connect.py b/tests/test_connect.py index a18a22e4..4935f776 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1757,6 +1757,8 @@ async def _run_connection_test( await conn.close() async def test_target_server_attribute_port(self): + if self.cluster.get_pg_version()[0] == 11: + self.skipTest("PostgreSQL 11 seems to have issues with this test") master_port = self.master_cluster.get_connection_spec()['port'] standby_port = self.standby_cluster.get_connection_spec()['port'] tests = [ @@ -1770,6 +1772,8 @@ async def test_target_server_attribute_port(self): ) async def test_target_attribute_not_matched(self): + if self.cluster.get_pg_version()[0] == 11: + self.skipTest("PostgreSQL 11 seems to have issues with this test") tests = [ (self.connect_standby, 'primary'), (self.connect_primary, 'standby'), @@ -1780,6 +1784,8 @@ async def test_target_attribute_not_matched(self): await connect(target_session_attribute=target_attr) async def test_prefer_standby_when_standby_is_up(self): + if self.cluster.get_pg_version()[0] == 11: + self.skipTest("PostgreSQL 11 seems to have issues with this test") con = await self.connect(target_session_attribute='prefer-standby') standby_port = self.standby_cluster.get_connection_spec()['port'] connected_host = _get_connected_host(con) From 86423c31e6118e68daf7965811cf44b3c64cf047 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Thu, 15 Dec 2022 08:21:39 +0100 Subject: [PATCH 28/40] disable tests for pg11 to see if all the rest of the test cases pass --- tests/test_connect.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index 4935f776..18236f64 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1757,7 +1757,7 @@ async def _run_connection_test( await conn.close() async def test_target_server_attribute_port(self): - if self.cluster.get_pg_version()[0] == 11: + if self.master_cluster.get_pg_version()[0] == 11: self.skipTest("PostgreSQL 11 seems to have issues with this test") master_port = self.master_cluster.get_connection_spec()['port'] standby_port = self.standby_cluster.get_connection_spec()['port'] @@ -1772,7 +1772,7 @@ async def test_target_server_attribute_port(self): ) async def test_target_attribute_not_matched(self): - if self.cluster.get_pg_version()[0] == 11: + if self.master_cluster.get_pg_version()[0] == 11: self.skipTest("PostgreSQL 11 seems to have issues with this test") tests = [ (self.connect_standby, 'primary'), @@ -1784,7 +1784,7 @@ async def test_target_attribute_not_matched(self): await connect(target_session_attribute=target_attr) async def test_prefer_standby_when_standby_is_up(self): - if self.cluster.get_pg_version()[0] == 11: + if self.master_cluster.get_pg_version()[0] == 11: self.skipTest("PostgreSQL 11 seems to have issues with this test") con = await self.connect(target_session_attribute='prefer-standby') standby_port = self.standby_cluster.get_connection_spec()['port'] @@ -1793,6 +1793,8 @@ async def test_prefer_standby_when_standby_is_up(self): await con.close() async def test_prefer_standby_picks_master_when_standby_is_down(self): + if self.master_cluster.get_pg_version()[0] == 11: + self.skipTest("PostgreSQL 11 seems to have issues with this test") primary_spec = self.get_cluster_connection_spec(self.master_cluster) connection_spec = { 'host': [ From 277ed968a3c672a24fb192ba1500f49af2f258d0 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Fri, 16 Dec 2022 09:02:40 +0100 Subject: [PATCH 29/40] add some more fixes that were already implemented --- tests/test_connect.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_connect.py b/tests/test_connect.py index 18236f64..51e92391 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1290,6 +1290,7 @@ def setUp(self): create_script = [] create_script.append('CREATE ROLE ssl_user WITH LOGIN;') + create_script.append('GRANT ALL ON SCHEMA public TO ssl_user;') self._add_hba_entry() @@ -1304,6 +1305,7 @@ def tearDown(self): self.cluster.trust_local_connections() drop_script = [] + drop_script.append('REVOKE ALL ON SCHEMA public FROM ssl_user;') drop_script.append('DROP ROLE ssl_user;') drop_script = '\n'.join(drop_script) self.loop.run_until_complete(self.con.execute(drop_script)) From 5737f767f7da5ddb687f61328ba8d948e674cc6a Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Fri, 16 Dec 2022 21:11:05 +0100 Subject: [PATCH 30/40] Add support for read-write and read-only target_session_attribute options --- asyncpg/_testbase/__init__.py | 1 + asyncpg/connect_utils.py | 25 +++++++++++++++++++++++++ tests/test_connect.py | 22 ++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/asyncpg/_testbase/__init__.py b/asyncpg/_testbase/__init__.py index 3dd8a314..7aca834f 100644 --- a/asyncpg/_testbase/__init__.py +++ b/asyncpg/_testbase/__init__.py @@ -438,6 +438,7 @@ def tearDown(self): class HotStandbyTestCase(ClusterTestCase): + @classmethod def setup_cluster(cls): cls.master_cluster = cls.new_cluster(pg_cluster.TempCluster) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 50b49d0a..93fb0973 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -885,6 +885,8 @@ class SessionAttribute(str, enum.Enum): primary = 'primary' standby = 'standby' prefer_standby = 'prefer-standby' + read_write = "read-write" + read_only = "read-only" def _accept_in_hot_standby(should_be_in_hot_standby: bool): @@ -909,6 +911,27 @@ async def can_be_used(connection): return can_be_used +def _accept_read_only(should_be_read_only: bool): + """ + Verify the server has not set default_transaction_read_only=True + """ + async def can_be_used(connection): + settings = connection.get_settings() + is_read_only = getattr(settings, 'default_transaction_read_only', None) + if is_read_only is not None: + is_read_only = is_read_only == "on" + else: + is_read_only = False + if should_be_read_only: + if is_read_only: + return True + elif await _accept_in_hot_standby(True)(connection): + return True + return False + return _accept_in_hot_standby(False)(connection) + return can_be_used + + async def _accept_any(_): return True @@ -918,6 +941,8 @@ async def _accept_any(_): SessionAttribute.primary: _accept_in_hot_standby(False), SessionAttribute.standby: _accept_in_hot_standby(True), SessionAttribute.prefer_standby: _accept_in_hot_standby(True), + SessionAttribute.read_write: _accept_read_only(False), + SessionAttribute.read_only: _accept_read_only(True), } diff --git a/tests/test_connect.py b/tests/test_connect.py index 51e92391..5ccfd4a9 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1768,6 +1768,17 @@ async def test_target_server_attribute_port(self): (self.connect_standby, 'standby', standby_port), ] + for connect, target_attr, expected_port in tests: + await self._run_connection_test( + connect, target_attr, expected_port + ) + if self.master_cluster.get_pg_version()[0] < 14: + self.skipTest("PostgreSQL<14 does not support these features") + tests = [ + (self.connect_primary, 'read-write', master_port), + (self.connect_standby, 'read-only', standby_port), + ] + for connect, target_attr, expected_port in tests: await self._run_connection_test( connect, target_attr, expected_port @@ -1785,6 +1796,17 @@ async def test_target_attribute_not_matched(self): with self.assertRaises(exceptions.TargetServerAttributeNotMatched): await connect(target_session_attribute=target_attr) + if self.master_cluster.get_pg_version()[0] < 14: + self.skipTest("PostgreSQL<14 does not support these features") + tests = [ + (self.connect_standby, 'read-write'), + (self.connect_primary, 'read-only'), + ] + + for connect, target_attr in tests: + with self.assertRaises(exceptions.TargetServerAttributeNotMatched): + await connect(target_session_attribute=target_attr) + async def test_prefer_standby_when_standby_is_up(self): if self.master_cluster.get_pg_version()[0] == 11: self.skipTest("PostgreSQL 11 seems to have issues with this test") From c3133c04c9cc1b0cbe0805a42331f56eddc98f0c Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Fri, 16 Dec 2022 21:27:16 +0100 Subject: [PATCH 31/40] fix little logical error --- asyncpg/connect_utils.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 93fb0973..e5329982 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -917,18 +917,11 @@ def _accept_read_only(should_be_read_only: bool): """ async def can_be_used(connection): settings = connection.get_settings() - is_read_only = getattr(settings, 'default_transaction_read_only', None) - if is_read_only is not None: - is_read_only = is_read_only == "on" - else: - is_read_only = False - if should_be_read_only: - if is_read_only: - return True - elif await _accept_in_hot_standby(True)(connection): - return True - return False - return _accept_in_hot_standby(False)(connection) + is_read_only = getattr(settings, 'default_transaction_read_only', 'off') + + if should_be_read_only and is_read_only == "on": + return True + return await _accept_in_hot_standby(should_be_read_only)(connection) return can_be_used From ae325ba83c075ef11dfaa3cef86f427ab0ab17dc Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Fri, 16 Dec 2022 21:31:12 +0100 Subject: [PATCH 32/40] linter --- asyncpg/connect_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index e5329982..c966556b 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -917,9 +917,9 @@ def _accept_read_only(should_be_read_only: bool): """ async def can_be_used(connection): settings = connection.get_settings() - is_read_only = getattr(settings, 'default_transaction_read_only', 'off') + is_readonly = getattr(settings, 'default_transaction_read_only', 'off') - if should_be_read_only and is_read_only == "on": + if should_be_read_only and is_readonly == "on": return True return await _accept_in_hot_standby(should_be_read_only)(connection) return can_be_used From 3bc832245e5a49a0766dfa29469497688df4c2b0 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Sun, 18 Dec 2022 10:25:05 +0100 Subject: [PATCH 33/40] fix logic issue --- asyncpg/connect_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index c966556b..5b78f27d 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -905,8 +905,7 @@ async def can_be_used(connection): is_in_hot_standby = await connection.fetchval( "SELECT pg_catalog.pg_is_in_recovery()" ) - connection_eligible = is_in_hot_standby == should_be_in_hot_standby - return connection_eligible + return is_in_hot_standby == should_be_in_hot_standby return can_be_used @@ -919,8 +918,9 @@ async def can_be_used(connection): settings = connection.get_settings() is_readonly = getattr(settings, 'default_transaction_read_only', 'off') - if should_be_read_only and is_readonly == "on": - return True + if is_readonly == "on": + return should_be_read_only + return await _accept_in_hot_standby(should_be_read_only)(connection) return can_be_used From d824333212e807178d4a40abaadcac5cb1848de2 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Sun, 29 Jan 2023 10:54:07 +0100 Subject: [PATCH 34/40] Update based on review. --- asyncpg/connect_utils.py | 25 +++++++++++++++++++------ asyncpg/connection.py | 20 ++++++++------------ tests/test_connect.py | 14 +++++++------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index ccc19fa2..89c6c939 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -57,7 +57,7 @@ def parse(cls, sslmode): 'direct_tls', 'connect_timeout', 'server_settings', - 'target_session_attribute', + 'target_session_attrs', ]) @@ -258,7 +258,7 @@ def _dot_postgresql_path(filename) -> pathlib.Path: def _parse_connect_dsn_and_args(*, dsn, host, port, user, password, passfile, database, ssl, direct_tls, connect_timeout, server_settings, - target_session_attribute): + target_session_attrs): # `auth_hosts` is the version of host information for the purposes # of reading the pgpass file. auth_hosts = None @@ -595,11 +595,24 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, 'server_settings is expected to be None or ' 'a Dict[str, str]') + if target_session_attrs is None: + + target_session_attrs = os.getenv("PGTARGETSESSIONATTRS", SessionAttribute.any) + try: + + target_session_attrs = SessionAttribute(target_session_attrs) + except ValueError as exc: + raise exceptions.InterfaceError( + "target_session_attrs is expected to be one of " + "{!r}" + ", got {!r}".format(SessionAttribute.__members__.values, target_session_attrs) + ) from exc + params = _ConnectionParameters( user=user, password=password, database=database, ssl=ssl, sslmode=sslmode, direct_tls=direct_tls, connect_timeout=connect_timeout, server_settings=server_settings, - target_session_attribute=target_session_attribute) + target_session_attrs=target_session_attrs) return addrs, params @@ -610,7 +623,7 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile, max_cached_statement_lifetime, max_cacheable_statement_size, ssl, direct_tls, server_settings, - target_session_attribute): + target_session_attrs): local_vars = locals() for var_name in {'max_cacheable_statement_size', 'max_cached_statement_lifetime', @@ -639,7 +652,7 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile, password=password, passfile=passfile, ssl=ssl, direct_tls=direct_tls, database=database, connect_timeout=timeout, server_settings=server_settings, - target_session_attribute=target_session_attribute) + target_session_attrs=target_session_attrs) config = _ClientConfiguration( command_timeout=command_timeout, @@ -941,7 +954,7 @@ async def _connect(*, loop, timeout, connection_class, record_class, **kwargs): loop = asyncio.get_event_loop() addrs, params, config = _parse_connect_arguments(timeout=timeout, **kwargs) - target_attr = params.target_session_attribute + target_attr = params.target_session_attrs candidates = [] chosen_connection = None diff --git a/asyncpg/connection.py b/asyncpg/connection.py index cec576f0..095ad398 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -1794,7 +1794,7 @@ async def connect(dsn=None, *, connection_class=Connection, record_class=protocol.Record, server_settings=None, - target_session_attribute=SessionAttribute.any): + target_session_attrs=None): r"""A coroutine to establish a connection to a PostgreSQL server. The connection parameters may be specified either as a connection @@ -2005,16 +2005,21 @@ async def connect(dsn=None, *, this connection object. Must be a subclass of :class:`~asyncpg.Record`. - :param SessionAttribute target_session_attribute: + :param SessionAttribute target_session_attrs: If specified, check that the host has the correct attribute. Can be one of: "any": the first successfully connected host "primary": the host must NOT be in hot standby mode "standby": the host must be in hot standby mode + "read-write": the host must allow writes + "read-only": the host most NOT allow writes "prefer-standby": first try to find a standby host, but if none of the listed hosts is a standby server, return any of them. + If not specified will try to use PGTARGETSESSIONATTRS from the environment. + Defaults to "any" if no value is set. + :return: A :class:`~asyncpg.connection.Connection` instance. Example: @@ -2099,15 +2104,6 @@ async def connect(dsn=None, *, if record_class is not protocol.Record: _check_record_class(record_class) - try: - target_session_attribute = SessionAttribute(target_session_attribute) - except ValueError as exc: - raise exceptions.InterfaceError( - "target_session_attribute is expected to be one of " - "'any', 'primary', 'standby' or 'prefer-standby'" - ", got {!r}".format(target_session_attribute) - ) from exc - if loop is None: loop = asyncio.get_event_loop() @@ -2130,7 +2126,7 @@ async def connect(dsn=None, *, statement_cache_size=statement_cache_size, max_cached_statement_lifetime=max_cached_statement_lifetime, max_cacheable_statement_size=max_cacheable_statement_size, - target_session_attribute=target_session_attribute + target_session_attrs=target_session_attrs ) diff --git a/tests/test_connect.py b/tests/test_connect.py index 3b8f69ff..baf2ebe5 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -788,7 +788,7 @@ def run_testcase(self, testcase): database = testcase.get('database') sslmode = testcase.get('ssl') server_settings = testcase.get('server_settings') - target_session_attribute = testcase.get('target_session_attribute') + target_session_attrs = testcase.get('target_session_attrs') expected = testcase.get('result') expected_error = testcase.get('error') @@ -813,7 +813,7 @@ def run_testcase(self, testcase): passfile=passfile, database=database, ssl=sslmode, direct_tls=False, connect_timeout=None, server_settings=server_settings, - target_session_attribute=target_session_attribute) + target_session_attrs=target_session_attrs) params = { k: v for k, v in params._asdict().items() @@ -1750,7 +1750,7 @@ class TestConnectionAttributes(tb.HotStandbyTestCase): async def _run_connection_test( self, connect, target_attribute, expected_port ): - conn = await connect(target_session_attribute=target_attribute) + conn = await connect(target_session_attrs=target_attribute) self.assertTrue(_get_connected_host(conn).endswith(expected_port)) await conn.close() @@ -1790,7 +1790,7 @@ async def test_target_attribute_not_matched(self): for connect, target_attr in tests: with self.assertRaises(exceptions.TargetServerAttributeNotMatched): - await connect(target_session_attribute=target_attr) + await connect(target_session_attrs=target_attr) if self.master_cluster.get_pg_version()[0] < 14: self.skipTest("PostgreSQL<14 does not support these features") @@ -1801,12 +1801,12 @@ async def test_target_attribute_not_matched(self): for connect, target_attr in tests: with self.assertRaises(exceptions.TargetServerAttributeNotMatched): - await connect(target_session_attribute=target_attr) + await connect(target_session_attrs=target_attr) async def test_prefer_standby_when_standby_is_up(self): if self.master_cluster.get_pg_version()[0] == 11: self.skipTest("PostgreSQL 11 seems to have issues with this test") - con = await self.connect(target_session_attribute='prefer-standby') + con = await self.connect(target_session_attrs='prefer-standby') standby_port = self.standby_cluster.get_connection_spec()['port'] connected_host = _get_connected_host(con) self.assertTrue(connected_host.endswith(standby_port)) @@ -1824,7 +1824,7 @@ async def test_prefer_standby_picks_master_when_standby_is_down(self): 'port': [primary_spec['port'], 15345], 'database': primary_spec['database'], 'user': primary_spec['user'], - 'target_session_attribute': 'prefer-standby' + 'target_session_attrs': 'prefer-standby' } con = await self.connect(**connection_spec) From 08ad1f84e5c5deaf6a6a5a2274fd4dd1b13eafc8 Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Sun, 29 Jan 2023 11:15:55 +0100 Subject: [PATCH 35/40] fix tests --- asyncpg/connect_utils.py | 8 +++-- asyncpg/connection.py | 4 +-- tests/test_connect.py | 66 ++++++++++++++++++++++++++++++++-------- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 89c6c939..e0c10442 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -597,7 +597,9 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, if target_session_attrs is None: - target_session_attrs = os.getenv("PGTARGETSESSIONATTRS", SessionAttribute.any) + target_session_attrs = os.getenv( + "PGTARGETSESSIONATTRS", SessionAttribute.any + ) try: target_session_attrs = SessionAttribute(target_session_attrs) @@ -605,7 +607,9 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, raise exceptions.InterfaceError( "target_session_attrs is expected to be one of " "{!r}" - ", got {!r}".format(SessionAttribute.__members__.values, target_session_attrs) + ", got {!r}".format( + SessionAttribute.__members__.values, target_session_attrs + ) ) from exc params = _ConnectionParameters( diff --git a/asyncpg/connection.py b/asyncpg/connection.py index 095ad398..432fcef6 100644 --- a/asyncpg/connection.py +++ b/asyncpg/connection.py @@ -29,7 +29,6 @@ from . import serverversion from . import transaction from . import utils -from .connect_utils import SessionAttribute class ConnectionMeta(type): @@ -2017,7 +2016,8 @@ async def connect(dsn=None, *, none of the listed hosts is a standby server, return any of them. - If not specified will try to use PGTARGETSESSIONATTRS from the environment. + If not specified will try to use PGTARGETSESSIONATTRS + from the environment. Defaults to "any" if no value is set. :return: A :class:`~asyncpg.connection.Connection` instance. diff --git a/tests/test_connect.py b/tests/test_connect.py index baf2ebe5..b38963ad 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -384,7 +384,8 @@ class TestConnectParams(tb.TestCase): 'password': 'passw', 'database': 'testdb', 'ssl': True, - 'sslmode': SSLMode.prefer}) + 'sslmode': SSLMode.prefer, + 'target_session_attrs': 'any'}) }, { @@ -406,7 +407,8 @@ class TestConnectParams(tb.TestCase): 'result': ([('host2', 456)], { 'user': 'user2', 'password': 'passw2', - 'database': 'db2'}) + 'database': 'db2', + 'target_session_attrs': 'any'}) }, { @@ -434,7 +436,8 @@ class TestConnectParams(tb.TestCase): 'password': 'passw2', 'database': 'db2', 'sslmode': SSLMode.disable, - 'ssl': False}) + 'ssl': False, + 'target_session_attrs': 'any'}) }, { @@ -455,7 +458,8 @@ class TestConnectParams(tb.TestCase): 'password': '123123', 'database': 'abcdef', 'ssl': True, - 'sslmode': SSLMode.allow}) + 'sslmode': SSLMode.allow, + 'target_session_attrs': 'any'}) }, { @@ -483,7 +487,8 @@ class TestConnectParams(tb.TestCase): 'password': 'passw2', 'database': 'db2', 'sslmode': SSLMode.disable, - 'ssl': False}) + 'ssl': False, + 'target_session_attrs': 'any'}) }, { @@ -504,7 +509,8 @@ class TestConnectParams(tb.TestCase): 'password': '123123', 'database': 'abcdef', 'ssl': True, - 'sslmode': SSLMode.prefer}) + 'sslmode': SSLMode.prefer, + 'target_session_attrs': 'any'}) }, { @@ -513,7 +519,8 @@ class TestConnectParams(tb.TestCase): 'result': ([('localhost', 5555)], { 'user': 'user3', 'password': '123123', - 'database': 'abcdef'}) + 'database': 'abcdef', + 'target_session_attrs': 'any'}) }, { @@ -522,6 +529,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('host1', 5432), ('host2', 5432)], { 'database': 'db', 'user': 'user', + 'target_session_attrs': 'any', }) }, @@ -531,6 +539,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('host1', 1111), ('host2', 2222)], { 'database': 'db', 'user': 'user', + 'target_session_attrs': 'any', }) }, @@ -540,6 +549,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('2001:db8::1234%eth0', 5432), ('::1', 5432)], { 'database': 'db', 'user': 'user', + 'target_session_attrs': 'any', }) }, @@ -549,6 +559,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('2001:db8::1234', 1111), ('::1', 2222)], { 'database': 'db', 'user': 'user', + 'target_session_attrs': 'any', }) }, @@ -558,6 +569,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('2001:db8::1234', 5432), ('::1', 5432)], { 'database': 'db', 'user': 'user', + 'target_session_attrs': 'any', }) }, @@ -572,6 +584,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('host1', 1111), ('host2', 2222)], { 'database': 'db', 'user': 'foo', + 'target_session_attrs': 'any', }) }, @@ -584,6 +597,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('host1', 1111), ('host2', 2222)], { 'database': 'db', 'user': 'foo', + 'target_session_attrs': 'any', }) }, @@ -597,6 +611,7 @@ class TestConnectParams(tb.TestCase): 'result': ([('host1', 5432), ('host2', 5432)], { 'database': 'db', 'user': 'foo', + 'target_session_attrs': 'any', }) }, @@ -616,7 +631,8 @@ class TestConnectParams(tb.TestCase): 'password': 'ask', 'database': 'db', 'ssl': True, - 'sslmode': SSLMode.require}) + 'sslmode': SSLMode.require, + 'target_session_attrs': 'any'}) }, { @@ -637,7 +653,8 @@ class TestConnectParams(tb.TestCase): 'password': 'ask', 'database': 'db', 'sslmode': SSLMode.verify_full, - 'ssl': True}) + 'ssl': True, + 'target_session_attrs': 'any'}) }, { @@ -645,7 +662,8 @@ class TestConnectParams(tb.TestCase): 'dsn': 'postgresql:///dbname?host=/unix_sock/test&user=spam', 'result': ([os.path.join('/unix_sock/test', '.s.PGSQL.5432')], { 'user': 'spam', - 'database': 'dbname'}) + 'database': 'dbname', + 'target_session_attrs': 'any'}) }, { @@ -657,6 +675,7 @@ class TestConnectParams(tb.TestCase): 'user': 'us@r', 'password': 'p@ss', 'database': 'db', + 'target_session_attrs': 'any', } ) }, @@ -670,6 +689,7 @@ class TestConnectParams(tb.TestCase): 'user': 'user', 'password': 'p', 'database': 'db', + 'target_session_attrs': 'any', } ) }, @@ -682,6 +702,7 @@ class TestConnectParams(tb.TestCase): { 'user': 'us@r', 'database': 'db', + 'target_session_attrs': 'any', } ) }, @@ -709,7 +730,8 @@ class TestConnectParams(tb.TestCase): 'user': 'user', 'database': 'user', 'sslmode': SSLMode.disable, - 'ssl': None + 'ssl': None, + 'target_session_attrs': 'any', } ) }, @@ -723,7 +745,8 @@ class TestConnectParams(tb.TestCase): '.s.PGSQL.5432' )], { 'user': 'spam', - 'database': 'db' + 'database': 'db', + 'target_session_attrs': 'any', } ) }, @@ -744,6 +767,7 @@ class TestConnectParams(tb.TestCase): 'database': 'db', 'ssl': True, 'sslmode': SSLMode.prefer, + 'target_session_attrs': 'any', } ) }, @@ -874,7 +898,9 @@ def test_test_connect_params_run_testcase(self): 'host': 'abc', 'result': ( [('abc', 5432)], - {'user': '__test__', 'database': '__test__'} + {'user': '__test__', + 'database': '__test__', + 'target_session_attrs': 'any'} ) }) @@ -912,6 +938,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for user@abc', 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -928,6 +955,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for user@abc', 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -942,6 +970,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for user@abc', 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -957,6 +986,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for localhost', 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -974,6 +1004,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for localhost', 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -991,6 +1022,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for cde:5433', 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -1007,6 +1039,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for testuser', 'user': 'testuser', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -1023,6 +1056,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass for testdb', 'user': 'user', 'database': 'testdb', + 'target_session_attrs': 'any', } ) }) @@ -1039,6 +1073,7 @@ def test_connect_pgpass_regular(self): 'password': 'password from pgpass with escapes', 'user': R'test\\', 'database': R'test\:db', + 'target_session_attrs': 'any', } ) }) @@ -1066,6 +1101,7 @@ def test_connect_pgpass_badness_mode(self): { 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -1086,6 +1122,7 @@ def test_connect_pgpass_badness_non_file(self): { 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -1102,6 +1139,7 @@ def test_connect_pgpass_nonexistent(self): { 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -1122,6 +1160,7 @@ def test_connect_pgpass_inaccessible_file(self): { 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) @@ -1144,6 +1183,7 @@ def test_connect_pgpass_inaccessible_directory(self): { 'user': 'user', 'database': 'db', + 'target_session_attrs': 'any', } ) }) From 7cdd2ba2212e78b2f65fe89f6bf9d9e076325d5c Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Sun, 29 Jan 2023 11:19:44 +0100 Subject: [PATCH 36/40] See the results on pg11 --- tests/test_connect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index b38963ad..3b56241b 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1795,8 +1795,8 @@ async def _run_connection_test( await conn.close() async def test_target_server_attribute_port(self): - if self.master_cluster.get_pg_version()[0] == 11: - self.skipTest("PostgreSQL 11 seems to have issues with this test") + #if self.master_cluster.get_pg_version()[0] == 11: + # self.skipTest("PostgreSQL 11 seems to have issues with this test") master_port = self.master_cluster.get_connection_spec()['port'] standby_port = self.standby_cluster.get_connection_spec()['port'] tests = [ From 19b7a17c8a3be0181027c1e03a224bfc60254a8d Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Mon, 30 Jan 2023 12:39:06 +0100 Subject: [PATCH 37/40] ... --- tests/test_connect.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_connect.py b/tests/test_connect.py index 3b56241b..02a6a50b 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1795,8 +1795,6 @@ async def _run_connection_test( await conn.close() async def test_target_server_attribute_port(self): - #if self.master_cluster.get_pg_version()[0] == 11: - # self.skipTest("PostgreSQL 11 seems to have issues with this test") master_port = self.master_cluster.get_connection_spec()['port'] standby_port = self.standby_cluster.get_connection_spec()['port'] tests = [ From 247b1a53ac3d460d101320e4ef602b4ca86aab79 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Mon, 13 Feb 2023 21:50:12 +0100 Subject: [PATCH 38/40] Fix missing PyPI wheels (#993) Fix for jq skipping first line `input` Outputs one new input. `inputs` Outputs all remaining inputs, one by one. https://github.com/pypa/cibuildwheel/discussions/1261#discussioncomment-4734804 --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31f844ba..1eba94a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,9 +85,9 @@ jobs: run: | MATRIX_INCLUDE=$( { - cibuildwheel --print-build-identifiers --platform linux --arch x86_64,aarch64 | grep cp | jq -Rc '{"only": inputs, "os": "ubuntu-latest"}' \ - && cibuildwheel --print-build-identifiers --platform macos --arch x86_64,arm64 | grep cp | jq -Rc '{"only": inputs, "os": "macos-latest"}' \ - && cibuildwheel --print-build-identifiers --platform windows --arch x86,AMD64 | grep cp | jq -Rc '{"only": inputs, "os": "windows-latest"}' + cibuildwheel --print-build-identifiers --platform linux --arch x86_64,aarch64 | grep cp | jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' \ + && cibuildwheel --print-build-identifiers --platform macos --arch x86_64,arm64 | grep cp | jq -nRc '{"only": inputs, "os": "macos-latest"}' \ + && cibuildwheel --print-build-identifiers --platform windows --arch x86,AMD64 | grep cp | jq -nRc '{"only": inputs, "os": "windows-latest"}' } | jq -sc ) echo "include=$MATRIX_INCLUDE" >> $GITHUB_OUTPUT From 172b8f693f4b6885bec60001964fee13fc8de644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9onard=20Besson?= Date: Sun, 12 Mar 2023 01:21:14 +0100 Subject: [PATCH 39/40] Handle environments without home dir (#1011) --- asyncpg/compat.py | 10 +++++--- asyncpg/connect_utils.py | 49 ++++++++++++++++++++++++++-------------- tests/test_connect.py | 29 ++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/asyncpg/compat.py b/asyncpg/compat.py index 29b8e16e..b9b13fa5 100644 --- a/asyncpg/compat.py +++ b/asyncpg/compat.py @@ -8,6 +8,7 @@ import asyncio import pathlib import platform +import typing SYSTEM = platform.uname().system @@ -18,7 +19,7 @@ CSIDL_APPDATA = 0x001a - def get_pg_home_directory() -> pathlib.Path: + def get_pg_home_directory() -> typing.Optional[pathlib.Path]: # We cannot simply use expanduser() as that returns the user's # home directory, whereas Postgres stores its config in # %AppData% on Windows. @@ -30,8 +31,11 @@ def get_pg_home_directory() -> pathlib.Path: return pathlib.Path(buf.value) / 'postgresql' else: - def get_pg_home_directory() -> pathlib.Path: - return pathlib.Path.home() + def get_pg_home_directory() -> typing.Optional[pathlib.Path]: + try: + return pathlib.Path.home() + except (RuntimeError, KeyError): + return None async def wait_closed(stream): diff --git a/asyncpg/connect_utils.py b/asyncpg/connect_utils.py index 40905edf..4d1a3f7d 100644 --- a/asyncpg/connect_utils.py +++ b/asyncpg/connect_utils.py @@ -249,8 +249,13 @@ def _parse_tls_version(tls_version): ) -def _dot_postgresql_path(filename) -> pathlib.Path: - return (pathlib.Path.home() / '.postgresql' / filename).resolve() +def _dot_postgresql_path(filename) -> typing.Optional[pathlib.Path]: + try: + homedir = pathlib.Path.home() + except (RuntimeError, KeyError): + return None + + return (homedir / '.postgresql' / filename).resolve() def _parse_connect_dsn_and_args(*, dsn, host, port, user, @@ -501,11 +506,16 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl.load_verify_locations(cafile=sslrootcert) ssl.verify_mode = ssl_module.CERT_REQUIRED else: - sslrootcert = _dot_postgresql_path('root.crt') try: + sslrootcert = _dot_postgresql_path('root.crt') + assert sslrootcert is not None ssl.load_verify_locations(cafile=sslrootcert) - except FileNotFoundError: + except (AssertionError, FileNotFoundError): if sslmode > SSLMode.require: + if sslrootcert is None: + raise RuntimeError( + 'Cannot determine home directory' + ) raise ValueError( f'root certificate file "{sslrootcert}" does ' f'not exist\nEither provide the file or ' @@ -526,18 +536,20 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN else: sslcrl = _dot_postgresql_path('root.crl') - try: - ssl.load_verify_locations(cafile=sslcrl) - except FileNotFoundError: - pass - else: - ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN + if sslcrl is not None: + try: + ssl.load_verify_locations(cafile=sslcrl) + except FileNotFoundError: + pass + else: + ssl.verify_flags |= \ + ssl_module.VERIFY_CRL_CHECK_CHAIN if sslkey is None: sslkey = os.getenv('PGSSLKEY') if not sslkey: sslkey = _dot_postgresql_path('postgresql.key') - if not sslkey.exists(): + if sslkey is not None and not sslkey.exists(): sslkey = None if not sslpassword: sslpassword = '' @@ -549,12 +561,15 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user, ) else: sslcert = _dot_postgresql_path('postgresql.crt') - try: - ssl.load_cert_chain( - sslcert, keyfile=sslkey, password=lambda: sslpassword - ) - except FileNotFoundError: - pass + if sslcert is not None: + try: + ssl.load_cert_chain( + sslcert, + keyfile=sslkey, + password=lambda: sslpassword + ) + except FileNotFoundError: + pass # OpenSSL 1.1.1 keylog file, copied from create_default_context() if hasattr(ssl, 'keylog_filename'): diff --git a/tests/test_connect.py b/tests/test_connect.py index 7707a1c9..a939db50 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -71,6 +71,14 @@ def mock_dot_postgresql(*, ca=True, crl=False, client=False, protected=False): yield +@contextlib.contextmanager +def mock_no_home_dir(): + with unittest.mock.patch( + 'pathlib.Path.home', unittest.mock.Mock(side_effect=RuntimeError) + ): + yield + + class TestSettings(tb.ConnectedTestCase): async def test_get_settings_01(self): @@ -1257,6 +1265,27 @@ async def test_connection_implicit_host(self): user=conn_spec.get('user')) await con.close() + @unittest.skipIf(os.environ.get('PGHOST'), 'unmanaged cluster') + async def test_connection_no_home_dir(self): + with mock_no_home_dir(): + con = await self.connect( + dsn='postgresql://foo/', + user='postgres', + database='postgres', + host='localhost') + await con.fetchval('SELECT 42') + await con.close() + + with self.assertRaisesRegex( + RuntimeError, + 'Cannot determine home directory' + ): + with mock_no_home_dir(): + await self.connect( + host='localhost', + user='ssl_user', + ssl='verify-full') + class BaseTestSSLConnection(tb.ConnectedTestCase): @classmethod From ea002ed903dff5a15d8a0647ffccb8cdf70f546d Mon Sep 17 00:00:00 2001 From: Jesse De Loore Date: Thu, 4 May 2023 19:47:31 +0200 Subject: [PATCH 40/40] Apply fix --- asyncpg/cluster.py | 2 +- tests/test_connect.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/asyncpg/cluster.py b/asyncpg/cluster.py index 0999e41c..4467cc2a 100644 --- a/asyncpg/cluster.py +++ b/asyncpg/cluster.py @@ -626,7 +626,7 @@ def init(self, **settings): 'pg_basebackup init exited with status {:d}:\n{}'.format( process.returncode, output.decode())) - if self._pg_version <= (11, 0): + if self._pg_version < (12, 0): with open(os.path.join(self._data_dir, 'recovery.conf'), 'w') as f: f.write(textwrap.dedent("""\ standby_mode = 'on' diff --git a/tests/test_connect.py b/tests/test_connect.py index 02a6a50b..3701b5e2 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1819,8 +1819,6 @@ async def test_target_server_attribute_port(self): ) async def test_target_attribute_not_matched(self): - if self.master_cluster.get_pg_version()[0] == 11: - self.skipTest("PostgreSQL 11 seems to have issues with this test") tests = [ (self.connect_standby, 'primary'), (self.connect_primary, 'standby'), @@ -1842,8 +1840,6 @@ async def test_target_attribute_not_matched(self): await connect(target_session_attrs=target_attr) async def test_prefer_standby_when_standby_is_up(self): - if self.master_cluster.get_pg_version()[0] == 11: - self.skipTest("PostgreSQL 11 seems to have issues with this test") con = await self.connect(target_session_attrs='prefer-standby') standby_port = self.standby_cluster.get_connection_spec()['port'] connected_host = _get_connected_host(con) @@ -1851,8 +1847,6 @@ async def test_prefer_standby_when_standby_is_up(self): await con.close() async def test_prefer_standby_picks_master_when_standby_is_down(self): - if self.master_cluster.get_pg_version()[0] == 11: - self.skipTest("PostgreSQL 11 seems to have issues with this test") primary_spec = self.get_cluster_connection_spec(self.master_cluster) connection_spec = { 'host': [