diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml index 39984e8..1ee75b5 100644 --- a/.github/workflows/actions.yaml +++ b/.github/workflows/actions.yaml @@ -7,17 +7,17 @@ jobs: strategy: matrix: os: [ ubuntu-latest, macos-latest ] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t', 'pypy3.10', 'pypy3.11'] tarantool: ['1.10', '2', '3'] exclude: - os: macos-latest tarantool: '1.10' - os: macos-latest tarantool: '2' - - os: macos-latest - python-version: '3.7' - python-version: 'pypy3.10' tarantool: '1.10' + - python-version: 'pypy3.11' + tarantool: '1.10' runs-on: ${{ matrix.os }} @@ -25,6 +25,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -40,24 +41,41 @@ jobs: if: matrix.os == 'macos-latest' run: brew install tarantool + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Install dependencies - run: | - python -m pip install --upgrade pip setuptools wheel coveralls + run: uv sync --extra test + + - name: Run ruff check + run: uv run ruff check . + + - name: Run ruff format check + run: uv run ruff format --check . + - name: Run tests - run: | - if [[ "$RUNNER_OS" == "Linux" && ${{ matrix.python-version }} == "3.12" && ${{ matrix.tarantool }} == "3" ]]; then - make build && make test - make clean && make debug && make coverage - # coveralls - else - make build && make lint && make quicktest - fi + run: uv run pytest . + + - name: Run tests with uvloop + if: "!contains(matrix.python-version, 'pypy')" + env: + USE_UVLOOP: "1" + run: uv run pytest . + + - name: Run coverage tests + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14' && matrix.tarantool == '3' env: + ASYNCTNT_DEBUG: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}" + run: | + make clean + uv pip install -e '.[test]' + uv run pytest --cov + ./scripts/run_until_success.sh uv run coverage report -m + ./scripts/run_until_success.sh uv run coverage html build-wheels: name: Build wheels on ${{ matrix.os }} - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ${{ matrix.os }} strategy: matrix: @@ -71,19 +89,33 @@ jobs: submodules: recursive - uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Install cibuildwheel - run: python -m pip install --upgrade cibuildwheel + run: uv tool install cibuildwheel + + - name: Install pyproject-build + run: uv tool install build + + - name: Build source archive + if: matrix.os == 'ubuntu-latest' + run: | + pyproject-build -s . - name: Build wheels - run: python -m cibuildwheel --output-dir wheelhouse + run: cibuildwheel --output-dir dist env: - CIBW_BUILD: "cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* pp310-*" + CIBW_ENABLE: pypy pypy-eol + CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-* cp314t-* pp310-* pp311-*" - uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.os }} - path: ./wheelhouse/*.whl + path: ./dist/* publish: name: Publish wheels @@ -91,55 +123,66 @@ jobs: needs: - build-wheels runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/asynctnt + permissions: + id-token: write # Required for trusted publishing + contents: write # Required for releases steps: - name: Get tag id: get_tag run: echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//} + - run: echo "Current tag is ${{ steps.get_tag.outputs.TAG }}" + - uses: actions/checkout@v4 with: submodules: recursive + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.14' + + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools wheel twine build + uv pip install --upgrade build - uses: actions/download-artifact@v4 with: name: wheels-ubuntu-latest - path: wheels-ubuntu + path: dist - uses: actions/download-artifact@v4 with: name: wheels-macos-latest - path: wheels-macos + path: dist - uses: actions/download-artifact@v4 with: name: wheels-windows-latest - path: wheels-windows + path: dist - - name: Publish dist + - name: Build source archive run: | python -m build . -s - tree dist wheels-ubuntu wheels-macos wheels-windows - twine upload dist/* wheels-ubuntu/*.whl wheels-macos/*.whl wheels-windows/*.whl - env: - TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + tree dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + print-hash: true + - uses: marvinpinto/action-automatic-releases@latest with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false title: ${{ steps.get_tag.outputs.TAG }} files: | - wheels-ubuntu/*.whl - wheels-macos/*.whl - wheels-windows/*.whl dist/* docs: @@ -156,12 +199,10 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.14' - - name: Install dependencies - run: | - python -m pip install --upgrade pip setuptools wheel build - make build + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Build docs run: make docs diff --git a/.gitignore b/.gitignore index 0033cf3..3b9b434 100644 --- a/.gitignore +++ b/.gitignore @@ -81,7 +81,7 @@ celerybeat-schedule .env # virtualenv -.venv/ +.venv*/ venv/ ENV/ @@ -113,3 +113,10 @@ deploy_key* cython_debug temp +bin +include +instances.enabled +modules +templates +uv.lock +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 7493646..2765cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v2.5.0 +**Breaking changes** +* Dropped support for Python 3.7 and 3.8 (minimum required version is now 3.9) + +**New features** +* Added support for Python 3.14, including free-threaded (no-GIL) builds +* Added support for PyPy 3.11 + +**Other changes** +* Upgraded Cython to 3.2.4 +* Declared Cython module as `freethreading_compatible` for Python 3.13+ +* Disabled C freelist in free-threaded builds to ensure thread safety + ## v2.4.0 **New features** * Added support for Python 3.13 [#37](https://github.com/igorcoding/asynctnt/issues/37) diff --git a/Makefile b/Makefile index 90e8798..6cc9c93 100644 --- a/Makefile +++ b/Makefile @@ -1,67 +1,62 @@ .PHONY: clean build local debug annotate dist docs style mypy ruff style-check lint test quicktest coverage -PYTHON?=python - all: local -clean: - pip uninstall -y asynctnt - rm -rf asynctnt/*.c asynctnt/*.h asynctnt/*.cpp - rm -rf asynctnt/*.so asynctnt/*.html - rm -rf asynctnt/iproto/*.c asynctnt/iproto/*.h - rm -rf asynctnt/iproto/*.so asynctnt/iproto/*.html asynctnt/iproto/requests/*.html - rm -rf build *.egg-info .eggs dist - find . -name '__pycache__' | xargs rm -rf - rm -rf htmlcov - rm -rf __tnt* - rm -rf tests/__tnt* - - build: - $(PYTHON) -m pip install -e '.[test,docs]' + uv pip install -e '.[test,docs]' local: - $(PYTHON) -m pip install -e . - + uv pip install -e . debug: clean - ASYNCTNT_DEBUG=1 $(PYTHON) -m pip install -e '.[test]' - + ASYNCTNT_DEBUG=1 uv pip install -e '.[test]' annotate: cython -3 -a asynctnt/iproto/protocol.pyx -dist: - $(PYTHON) -m build . - -docs: build - $(MAKE) -C docs html +lint: style-check ruff style: - $(PYTHON) -m black . - $(PYTHON) -m isort . + uv run --active ruff format . + uv run --active ruff check --select I,F401 --fix . -mypy: - $(PYTHON) -m mypy --enable-error-code ignore-without-code . +style-check: + uv run --active ruff format --check . ruff: - $(PYTHON) -m ruff check . - -style-check: - $(PYTHON) -m black --check --diff . - $(PYTHON) -m isort --check --diff . + uv run --active ruff check . -lint: style-check ruff +mypy: + uv run --active mypy --enable-error-code ignore-without-code . -test: lint - PYTHONASYNCIODEBUG=1 $(PYTHON) -m pytest - $(PYTHON) -m pytest - USE_UVLOOP=1 $(PYTHON) -m pytest +test: + PYTHONASYNCIODEBUG=1 uv run --active pytest + uv run --active pytest + USE_UVLOOP=1 uv run --active pytest quicktest: - $(PYTHON) -m pytest + uv run --active pytest coverage: - $(PYTHON) -m pytest --cov - ./scripts/run_until_success.sh $(PYTHON) -m coverage report -m - ./scripts/run_until_success.sh $(PYTHON) -m coverage html + uv run --active pytest --cov + ./scripts/run_until_success.sh uv run --active coverage report -m + ./scripts/run_until_success.sh uv run --active coverage html + +dist: + uv pip install build + uv run --active python -m build . + +docs: build + $(MAKE) -C docs html + +clean: + uv pip uninstall asynctnt + rm -rf asynctnt/*.c asynctnt/*.h asynctnt/*.cpp + rm -rf asynctnt/*.so asynctnt/*.html + rm -rf asynctnt/iproto/*.c asynctnt/iproto/*.h + rm -rf asynctnt/iproto/*.so asynctnt/iproto/*.html asynctnt/iproto/requests/*.html + rm -rf build *.egg-info .eggs dist + find . -name '__pycache__' | xargs rm -rf + rm -rf htmlcov + rm -rf __tnt* + rm -rf tests/__tnt* diff --git a/README.md b/README.md index 1f4a24b..dec8044 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build](https://github.com/igorcoding/asynctnt/actions/workflows/actions.yaml/badge.svg?branch=master)](https://github.com/igorcoding/asynctnt/actions) [![PyPI](https://img.shields.io/pypi/v/asynctnt.svg)](https://pypi.python.org/pypi/asynctnt) [![Maintainability](https://api.codeclimate.com/v1/badges/6cec8adae280cda3e161/maintainability)](https://codeclimate.com/github/igorcoding/asynctnt/maintainability) - + @@ -11,7 +11,7 @@ asynctnt is a high-performance [Tarantool](https://tarantool.org/) database connector library for Python/asyncio. It was highly inspired by [asyncpg](https://github.com/MagicStack/asyncpg) module. -asynctnt requires Python 3.7 or later and is supported for Tarantool +asynctnt requires Python 3.9 or later and is supported for Tarantool versions 1.10+. diff --git a/asynctnt/__init__.py b/asynctnt/__init__.py index 27e6b8a..3bcfa95 100644 --- a/asynctnt/__init__.py +++ b/asynctnt/__init__.py @@ -18,4 +18,4 @@ TarantoolTuple, ) -__version__ = "2.4.0" +__version__ = "2.5.0" diff --git a/asynctnt/connection.py b/asynctnt/connection.py index 4fd8e3c..0dea13b 100644 --- a/asynctnt/connection.py +++ b/asynctnt/connection.py @@ -134,7 +134,7 @@ def __init__( self._auto_refetch_schema = True if not self._fetch_schema: logger.warning( - "Setting fetch_schema to True as " "auto_refetch_schema is True" + "Setting fetch_schema to True as auto_refetch_schema is True" ) self._fetch_schema = True else: @@ -257,12 +257,12 @@ async def full_connect(): if self._host.startswith("unix/"): unix_path = self._port assert isinstance(unix_path, str), ( - "port must be a str instance for " "unix socket" + "port must be a str instance for unix socket" ) assert unix_path, "No unix file path specified" - assert os.path.exists( - unix_path - ), "Unix socket `{}` not found".format(unix_path) + assert os.path.exists(unix_path), ( + "Unix socket `{}` not found".format(unix_path) + ) conn = loop.create_unix_connection( functools.partial( diff --git a/asynctnt/iproto/ext/datetime.pyx b/asynctnt/iproto/ext/datetime.pyx index 0694490..667677a 100644 --- a/asynctnt/iproto/ext/datetime.pyx +++ b/asynctnt/iproto/ext/datetime.pyx @@ -1,5 +1,10 @@ -cimport cpython.datetime -from cpython.datetime cimport PyDateTimeAPI, datetime, datetime_tzinfo, timedelta_new +from cpython.datetime cimport ( + datetime, + datetime_from_timestamp, + datetime_tzinfo, + timedelta_new, + timezone_new, +) from libc.stdint cimport uint32_t from libc.string cimport memcpy @@ -80,8 +85,4 @@ cdef object datetime_to_py(IProtoDateTime *dt): tz = timezone_new(delta) timestamp = dt.seconds + ( dt.nsec) / 1e9 - return PyDateTimeAPI.DateTime_FromTimestamp( - PyDateTimeAPI.DateTimeType, - (timestamp,) if tz is None else (timestamp, tz), - NULL, - ) + return datetime_from_timestamp(timestamp, tz) diff --git a/asynctnt/iproto/protocol.pxd b/asynctnt/iproto/protocol.pxd index f7e5045..e5e350d 100644 --- a/asynctnt/iproto/protocol.pxd +++ b/asynctnt/iproto/protocol.pxd @@ -5,7 +5,6 @@ include "const.pxi" include "cmsgpuck.pxd" include "xd.pxd" -include "python.pxd" include "bit.pxd" include "unicodeutil.pxd" diff --git a/asynctnt/iproto/protocol.pyx b/asynctnt/iproto/protocol.pyx index 0e8601c..4e4a059 100644 --- a/asynctnt/iproto/protocol.pyx +++ b/asynctnt/iproto/protocol.pyx @@ -1,4 +1,5 @@ # cython: language_level=3 +# cython: freethreading_compatible=True cimport cpython.dict from cpython.datetime cimport import_datetime diff --git a/asynctnt/iproto/python.pxd b/asynctnt/iproto/python.pxd deleted file mode 100644 index 8446aaa..0000000 --- a/asynctnt/iproto/python.pxd +++ /dev/null @@ -1,77 +0,0 @@ -from cpython.version cimport PY_VERSION_HEX - - -cdef extern from "Python.h": - char *PyByteArray_AS_STRING(object obj) - int Py_REFCNT(object obj) - - -cdef extern from "datetime.h": - """ - /* Backport for Python 2.x */ - #if PY_MAJOR_VERSION < 3 - #ifndef PyDateTime_DELTA_GET_DAYS - #define PyDateTime_DELTA_GET_DAYS(o) (((PyDateTime_Delta*)o)->days) - #endif - #ifndef PyDateTime_DELTA_GET_SECONDS - #define PyDateTime_DELTA_GET_SECONDS(o) (((PyDateTime_Delta*)o)->seconds) - #endif - #ifndef PyDateTime_DELTA_GET_MICROSECONDS - #define PyDateTime_DELTA_GET_MICROSECONDS(o) (((PyDateTime_Delta*)o)->microseconds) - #endif - #endif - /* Backport for Python < 3.6 */ - #if PY_VERSION_HEX < 0x030600a4 - #ifndef PyDateTime_TIME_GET_FOLD - #define PyDateTime_TIME_GET_FOLD(o) ((void)(o), 0) - #endif - #ifndef PyDateTime_DATE_GET_FOLD - #define PyDateTime_DATE_GET_FOLD(o) ((void)(o), 0) - #endif - #endif - /* Backport for Python < 3.6 */ - #if PY_VERSION_HEX < 0x030600a4 - #define __Pyx_DateTime_DateTimeWithFold(year, month, day, hour, minute, second, microsecond, tz, fold) \ - ((void)(fold), PyDateTimeAPI->DateTime_FromDateAndTime(year, month, day, hour, minute, second, \ - microsecond, tz, PyDateTimeAPI->DateTimeType)) - #define __Pyx_DateTime_TimeWithFold(hour, minute, second, microsecond, tz, fold) \ - ((void)(fold), PyDateTimeAPI->Time_FromTime(hour, minute, second, microsecond, tz, PyDateTimeAPI->TimeType)) - #else /* For Python 3.6+ so that we can pass tz */ - #define __Pyx_DateTime_DateTimeWithFold(year, month, day, hour, minute, second, microsecond, tz, fold) \ - PyDateTimeAPI->DateTime_FromDateAndTimeAndFold(year, month, day, hour, minute, second, \ - microsecond, tz, fold, PyDateTimeAPI->DateTimeType) - #define __Pyx_DateTime_TimeWithFold(hour, minute, second, microsecond, tz, fold) \ - PyDateTimeAPI->Time_FromTimeAndFold(hour, minute, second, microsecond, tz, fold, PyDateTimeAPI->TimeType) - #endif - /* Backport for Python < 3.7 */ - #if PY_VERSION_HEX < 0x030700b1 - #define __Pyx_TimeZone_UTC NULL - #define __Pyx_TimeZone_FromOffset(offset) ((void)(offset), (PyObject*)NULL) - #define __Pyx_TimeZone_FromOffsetAndName(offset, name) ((void)(offset), (void)(name), (PyObject*)NULL) - #else - #define __Pyx_TimeZone_UTC PyDateTime_TimeZone_UTC - #define __Pyx_TimeZone_FromOffset(offset) PyTimeZone_FromOffset(offset) - #define __Pyx_TimeZone_FromOffsetAndName(offset, name) PyTimeZone_FromOffsetAndName(offset, name) - #endif - /* Backport for Python < 3.10 */ - #if PY_VERSION_HEX < 0x030a00a1 - #ifndef PyDateTime_TIME_GET_TZINFO - #define PyDateTime_TIME_GET_TZINFO(o) \ - ((((PyDateTime_Time*)o)->hastzinfo) ? ((PyDateTime_Time*)o)->tzinfo : Py_None) - #endif - #ifndef PyDateTime_DATE_GET_TZINFO - #define PyDateTime_DATE_GET_TZINFO(o) \ - ((((PyDateTime_DateTime*)o)->hastzinfo) ? ((PyDateTime_DateTime*)o)->tzinfo : Py_None) - #endif - #endif - """ - - # The above macros is Python 3.7+ so we use these instead - object __Pyx_TimeZone_FromOffset(object offset) - - -cdef inline object timezone_new(object offset): - if PY_VERSION_HEX < 0x030700b1: - from datetime import timezone - return timezone(offset) - return __Pyx_TimeZone_FromOffset(offset) diff --git a/asynctnt/iproto/tupleobj/tupleobj.c b/asynctnt/iproto/tupleobj/tupleobj.c index bb92690..4a85384 100644 --- a/asynctnt/iproto/tupleobj/tupleobj.c +++ b/asynctnt/iproto/tupleobj/tupleobj.c @@ -12,8 +12,10 @@ static PyObject *ttuple_iter(PyObject *); static PyObject *ttuple_new_items_iter(PyObject *); +#ifndef Py_GIL_DISABLED static AtntTupleObject *free_list[AtntTuple_MAXSAVESIZE]; static int numfree[AtntTuple_MAXSAVESIZE]; +#endif PyObject * @@ -31,12 +33,15 @@ AtntTuple_New(PyObject *metadata, Py_ssize_t size) return NULL; } +#ifndef Py_GIL_DISABLED if (size < AtntTuple_MAXSAVESIZE && (o = free_list[size]) != NULL) { free_list[size] = (AtntTupleObject *) o->ob_item[0]; numfree[size]--; _Py_NewReference((PyObject *)o); } - else { + else +#endif + { /* Check for overflow */ if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - sizeof(AtntTupleObject) - sizeof(PyObject *)) / sizeof(PyObject *)) { @@ -79,6 +84,7 @@ ttuple_dealloc(AtntTupleObject *o) Py_CLEAR(o->ob_item[i]); } +#ifndef Py_GIL_DISABLED if (len < AtntTuple_MAXSAVESIZE && numfree[len] < AtntTuple_MAXFREELIST && AtntTuple_CheckExact(o)) @@ -88,10 +94,11 @@ ttuple_dealloc(AtntTupleObject *o) free_list[len] = o; goto done; /* return */ } +#endif } Py_TYPE(o)->tp_free((PyObject *)o); done: - CPy_TRASHCAN_END(o) + CPy_TRASHCAN_END(o); } @@ -133,7 +140,7 @@ ttuple_hash(AtntTupleObject *v) } len = Py_SIZE(v); - mult = _PyHASH_MULTIPLIER; + mult = ATNT_HASH_MULTIPLIER; x = 0x345678UL; p = v->ob_item; @@ -404,12 +411,13 @@ ttuple_repr(AtntTupleObject *v) } #else - _PyUnicodeWriter writer; - _PyUnicodeWriter_Init(&writer); - writer.overallocate = 1; - writer.min_length = 12; /* */ + ATNT_UW_DECL(writer); + ATNT_UW_CREATE(writer, 12); /* */ + if (ATNT_UW_CREATE_FAILED(writer)) { + goto error; + } - if (_PyUnicodeWriter_WriteASCIIString(&writer, " 0) { - if (_PyUnicodeWriter_WriteChar(&writer, ' ') < 0) { + if (ATNT_UW_WRITE_CHAR(ATNT_UW_REF(writer), ' ') < 0) { Py_DECREF(key_repr); Py_DECREF(val_repr); goto error; } } - if (_PyUnicodeWriter_WriteStr(&writer, key_repr) < 0) { + if (ATNT_UW_WRITE_STR(ATNT_UW_REF(writer), key_repr) < 0) { Py_DECREF(key_repr); Py_DECREF(val_repr); goto error; } Py_DECREF(key_repr); - if (_PyUnicodeWriter_WriteChar(&writer, '=') < 0) { + if (ATNT_UW_WRITE_CHAR(ATNT_UW_REF(writer), '=') < 0) { Py_DECREF(val_repr); goto error; } - if (_PyUnicodeWriter_WriteStr(&writer, val_repr) < 0) { + if (ATNT_UW_WRITE_STR(ATNT_UW_REF(writer), val_repr) < 0) { Py_DECREF(val_repr); goto error; } @@ -514,17 +522,16 @@ ttuple_repr(AtntTupleObject *v) } Py_XDECREF(parts_joined); #else - writer.overallocate = 0; if (oversize) { - if (_PyUnicodeWriter_WriteASCIIString(&writer, " ...>", 5) < 0) { + if (ATNT_UW_WRITE_ASCII(ATNT_UW_REF(writer), " ...>", 5) < 0) { goto error; } } else { - if (_PyUnicodeWriter_WriteChar(&writer, '>') < 0) { + if (ATNT_UW_WRITE_CHAR(ATNT_UW_REF(writer), '>') < 0) { goto error; } } - result = _PyUnicodeWriter_Finish(&writer); + result = ATNT_UW_FINISH(ATNT_UW_REF(writer)); #endif Py_XDECREF(keys_iter); @@ -536,7 +543,7 @@ ttuple_repr(AtntTupleObject *v) #if defined(PYPY_VERSION) Py_XDECREF(parts); #else - _PyUnicodeWriter_Dealloc(&writer); + ATNT_UW_DISCARD(ATNT_UW_REF(writer)); #endif Py_ReprLeave((PyObject *)v); return NULL; diff --git a/asynctnt/iproto/tupleobj/tupleobj.h b/asynctnt/iproto/tupleobj/tupleobj.h index cf26667..5435ac7 100644 --- a/asynctnt/iproto/tupleobj/tupleobj.h +++ b/asynctnt/iproto/tupleobj/tupleobj.h @@ -12,15 +12,46 @@ extern "C" { # define CPy_TRASHCAN_BEGIN(op, dealloc) do {} while(0); # define CPy_TRASHCAN_END(op) do {} while(0); #else - -#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 8 # define CPy_TRASHCAN_BEGIN(op, dealloc) Py_TRASHCAN_BEGIN(op, dealloc) # define CPy_TRASHCAN_END(op) Py_TRASHCAN_END -#else -# define CPy_TRASHCAN_BEGIN(op, dealloc) Py_TRASHCAN_SAFE_BEGIN(op) -# define CPy_TRASHCAN_END(op) Py_TRASHCAN_SAFE_END(op) + +/* + * PyUnicodeWriter compatibility macros for Python 3.14+ + * Python 3.14 introduced public PyUnicodeWriter API, deprecating private _PyUnicodeWriter. + * These macros are only defined for CPython (not PyPy). + */ +#if PY_VERSION_HEX >= 0x030E0000 /* Python 3.14+ */ +# define ATNT_UW_DECL(name) PyUnicodeWriter *name +# define ATNT_UW_CREATE(name, len) name = PyUnicodeWriter_Create(len) +# define ATNT_UW_CREATE_FAILED(name) (name == NULL) +# define ATNT_UW_REF(name) name +# define ATNT_UW_WRITE_ASCII PyUnicodeWriter_WriteASCII +# define ATNT_UW_WRITE_CHAR PyUnicodeWriter_WriteChar +# define ATNT_UW_WRITE_STR PyUnicodeWriter_WriteStr +# define ATNT_UW_FINISH PyUnicodeWriter_Finish +# define ATNT_UW_DISCARD PyUnicodeWriter_Discard +#else /* Python < 3.14 */ +# define ATNT_UW_DECL(name) _PyUnicodeWriter name +# define ATNT_UW_CREATE(name, len) do { _PyUnicodeWriter_Init(&name); name.overallocate = 1; name.min_length = len; } while(0) +# define ATNT_UW_CREATE_FAILED(name) (0) /* _PyUnicodeWriter_Init doesn't fail */ +# define ATNT_UW_REF(name) &name +# define ATNT_UW_WRITE_ASCII _PyUnicodeWriter_WriteASCIIString +# define ATNT_UW_WRITE_CHAR _PyUnicodeWriter_WriteChar +# define ATNT_UW_WRITE_STR _PyUnicodeWriter_WriteStr +# define ATNT_UW_FINISH _PyUnicodeWriter_Finish +# define ATNT_UW_DISCARD _PyUnicodeWriter_Dealloc #endif +#endif /* !PYPY_VERSION */ + +/* + * PyHASH_MULTIPLIER compatibility macro for Python 3.13+ + * Python 3.13 introduced public PyHASH_MULTIPLIER, replacing private _PyHASH_MULTIPLIER. + */ +#if PY_VERSION_HEX >= 0x030D0000 /* Python 3.13+ */ +# define ATNT_HASH_MULTIPLIER PyHASH_MULTIPLIER +#else +# define ATNT_HASH_MULTIPLIER _PyHASH_MULTIPLIER #endif /* Largest ttuple to save on free list */ diff --git a/bench/benchmark.py b/bench/benchmark.py index 1346ac2..2125a0b 100644 --- a/bench/benchmark.py +++ b/bench/benchmark.py @@ -20,7 +20,7 @@ def main(): parser.add_argument("-b", type=int, default=300, help="number of bulks") args = parser.parse_args() - print("Running {} requests in {} batches. ".format(args.n, args.b)) + print("Running {} requests in {} batches. ".format(args.n, args.b)) # noqa: T201 scenarios = [ ["ping", []], @@ -34,30 +34,27 @@ def main(): ] for use_uvloop in [True]: + run_func = asyncio.run if use_uvloop: try: import uvloop except ImportError: - print("No uvloop installed. Skipping.") + print("No uvloop installed. Skipping.") # noqa: T201 continue - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - else: - asyncio.set_event_loop_policy(None) - asyncio.set_event_loop(None) - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + run_func = uvloop.run - print("--------- uvloop: {} --------- ".format(use_uvloop)) + print("--------- uvloop: {} --------- ".format(use_uvloop)) # noqa: T201 for name, conn_creator in [ ("asynctnt", create_asynctnt), # ('aiotarantool', create_aiotarantool), ]: - conn = loop.run_until_complete(conn_creator()) - for scenario in scenarios: - loop.run_until_complete( - async_bench( + + async def main(name=name, conn_creator=conn_creator): + conn = await conn_creator() + for scenario in scenarios: + await async_bench( name, conn, args.n, @@ -66,7 +63,8 @@ def main(): args=scenario[1], kwargs=scenario[2] if len(scenario) > 2 else {}, ) - ) + + run_func(main()) async def async_bench(name, conn, n, b, method, args=None, kwargs=None): @@ -87,7 +85,7 @@ async def bulk_f(): end = datetime.datetime.now() elapsed = end - start - print( + print( # noqa: T201 "{} [{}] Elapsed: {}, RPS: {}".format( name, method, elapsed, n / elapsed.total_seconds() ) diff --git a/pyproject.toml b/pyproject.toml index 0a91beb..e74c413 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ description = "A fast Tarantool Database connector for Python/asyncio." authors = [ { name = "igorcoding", email = "igorcoding@gmail.com" } ] -license = {text = "Apache License, Version 2.0"} +license-files = ["LICENSE.txt"] dynamic = ["version"] classifiers=[ "Development Status :: 5 - Production/Stable", @@ -14,19 +14,18 @@ classifiers=[ "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", 'Programming Language :: Python :: Implementation :: CPython', "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Database :: Front-Ends" ] -requires-python = '>=3.7.0' +requires-python = '>=3.9.0' readme = "README.md" dependencies = [ 'PyYAML >= 5.0', @@ -37,16 +36,13 @@ github = "https://github.com/igorcoding/asynctnt" [project.optional-dependencies] test = [ - 'isort', - 'black', 'ruff', 'uvloop>=0.12.3; platform_system != "Windows" and platform.python_implementation != "PyPy"', 'pytest', 'pytest-cov', 'coverage[toml]', 'pytz', - 'python-dateutil', - "Cython==3.0.11", # for coverage + "Cython==3.2.4", # for coverage ] docs = [ @@ -60,10 +56,9 @@ docs = [ [build-system] requires = [ - "setuptools>=60", + "setuptools>=77", "wheel", - - "Cython==3.0.11", + "Cython==3.2.4", ] build-backend = "setuptools.build_meta" @@ -107,33 +102,66 @@ show_missing = true [tool.coverage.html] directory = "htmlcov" -[tool.black] -extend-exclude = '(env|.env|venv|.venv).*' - -[tool.isort] -profile = "black" -multi_line_output = 3 -skip_glob = [ - "env*", - "venv*", -] - - [tool.ruff] -lint.select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - # "I", # isort - "C", # flake8-comprehensions - "B", # flake8-bugbear +extend-exclude = [".venv*", "env*", "venv*"] + +[tool.ruff.format] +# Аналогично black, двойные кавычки +quote-style = "double" + +# Аналогично black, пробелы вместо табов +indent-style = "space" + +# Аналогично black, уважаем trailing commas +skip-magic-trailing-comma = false + +# Аналогично black, автоматически определяем подходящее окончание строки. +line-ending = "auto" + +[tool.ruff.lint] +# Список кодов или префиксов правил, которые следует считать исправляемыми. (https://docs.astral.sh/ruff/settings/#fixable) +# По умолчанию все правила считаются исправляемыми. +fixable = ["I", "RUF022", "RUF023", "F401"] +preview = true + +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear + "T20", # flake8-print ] -lint.ignore = [ - "E501", # line too long, handled by black - "B008", # do not perform function calls in argument defaults - "C901", # too complex +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex ] -extend-exclude = [ - "app/store/migrations", +[tool.ruff.lint.isort] +# Позволяет использовать as в комбинации с группировкой (https://docs.astral.sh/ruff/settings/#isort-combine-as-imports) +#from package import ( +# func1 as foo, +# func2 as boo, +#) +combine-as-imports = true + +# Воспринимать следующие пакеты в качестве stdlib (https://docs.astral.sh/ruff/settings/#isort-extra-standard-library) +extra-standard-library = ["typing_extensions"] + +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder" ] + +# Не добавлять пустую строку перед данными секциям (https://docs.astral.sh/ruff/settings/#isort-no-lines-before) +no-lines-before = [] + +[tool.ruff.lint.pep8-naming] +# если навесить данные декораторы, то можно использовать cls (https://docs.astral.sh/ruff/settings/#pep8-naming-classmethod-decorators) +# в качестве первого аргумента. +classmethod-decorators = ["cached_classproperty", "classproperty"] diff --git a/setup.py b/setup.py index 2c4244c..d343767 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def find_version(): return re.match(r"""__version__\s*=\s*(['"])([^'"]+)\1""", line).group(2) -CYTHON_VERSION = "3.0.11" +CYTHON_VERSION = "3.2.4" class build_ext(setuptools_build_ext.build_ext): diff --git a/tests/_testbase.py b/tests/_testbase.py index 66c6e1a..a12b056 100644 --- a/tests/_testbase.py +++ b/tests/_testbase.py @@ -172,7 +172,7 @@ def _make_instance(cls, **kwargs): tarantool_docker_tag = os.getenv("TARANTOOL_DOCKER_VERSION") in_docker = False if tarantool_docker_tag: - print( + print( # noqa: T201 "Running tarantool in docker: {}:{}".format( tarantool_docker_image or "tarantool/tarantool", tarantool_docker_tag, @@ -269,6 +269,7 @@ async def tnt_reconnect(self, **kwargs): await self.tnt_connect(**kwargs) def assertResponseEqual(self, resp, target, *args): + self.assertEqual(len(resp), len(target)) tuples = [] for item in resp: if isinstance(item, TarantoolTuple): diff --git a/tests/test_common.py b/tests/test_common.py index e57deb5..c91447e 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -55,7 +55,7 @@ async def test__schema_refetch_on_schema_change(self): self.assertIn("new_space", self.conn.schema.spaces) finally: await self.conn.eval( - "local s = box.space.new_space;" "if s ~= nil then s:drop(); end" + "local s = box.space.new_space;if s ~= nil then s:drop(); end" ) async def test__schema_refetch_manual(self): @@ -93,7 +93,7 @@ async def test__schema_no_fetch_and_refetch(self): self.assertEqual(self.conn.schema_id, -1) # Changing scheme - await self.conn.eval("s = box.schema.create_space('new_space');" "s:drop();") + await self.conn.eval("s = box.schema.create_space('new_space');s:drop();") try: await self.conn.ping() @@ -105,16 +105,14 @@ async def test__schema_no_fetch_and_refetch(self): self.assertEqual(self.conn.schema_id, -1) async def test__parse_numeric_map_keys(self): - res = await self.conn.eval( - """return { + res = await self.conn.eval("""return { [1] = 1, [2] = 2, hello = 3, world = 4, [-3] = 5, [4.5] = 6 - }""" - ) + }""") d = {1: 1, 2: 2, "hello": 3, "world": 4, -3: 5, 4.5: 6} @@ -185,7 +183,7 @@ async def test__schema_refetch_next_byte(self): try: for _ in range(251): await self.conn.eval( - "s = box.schema.create_space('new_space');" "s:drop();" + "s = box.schema.create_space('new_space');s:drop();" ) except TarantoolDatabaseError as e: self.fail(e) @@ -221,8 +219,7 @@ async def func(): ) async with conn: await conn.eval( - "s = box.schema.create_space('spacex');" - "s:create_index('primary');" + "s = box.schema.create_space('spacex');s:create_index('primary');" ) except TarantoolDatabaseError as e: self.fail(e) diff --git a/tests/test_mp_ext.py b/tests/test_mp_ext.py index bb28552..def3cdc 100644 --- a/tests/test_mp_ext.py +++ b/tests/test_mp_ext.py @@ -1,10 +1,8 @@ import datetime -import sys import uuid from dataclasses import dataclass from decimal import Decimal -import dateutil.parser import pytz import asynctnt @@ -117,11 +115,9 @@ class MpExtErrorTestCase(BaseTarantoolTestCase): @ensure_version(min=(2, 4, 1)) async def test__ext_error(self): try: - await self.conn.eval( - """ + await self.conn.eval(""" box.schema.space.create('_space') - """ - ) + """) except TarantoolDatabaseError as e: self.assertIsNotNone(e.error) self.assertGreater(len(e.error.trace), 0) @@ -136,12 +132,10 @@ async def test__ext_error(self): @ensure_version(min=(2, 4, 1)) async def test__ext_error_custom(self): try: - await self.conn.eval( - """ + await self.conn.eval(""" local e = box.error.new{code=5,reason='A',type='B'} box.error(e) - """ - ) + """) except TarantoolDatabaseError as e: self.assertIsNotNone(e.error) self.assertGreater(len(e.error.trace), 0) @@ -157,12 +151,10 @@ async def test__ext_error_custom(self): @ensure_version(min=(2, 10)) async def test__ext_error_custom_return(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local e = box.error.new{code=5,reason='A',type='B'} return e - """ - ) + """) e = resp[0] self.assertIsInstance(e, IProtoError) self.assertGreater(len(e.trace), 0) @@ -178,82 +170,68 @@ async def test__ext_error_custom_return(self): @ensure_version(min=(2, 10)) async def test__ext_error_custom_return_with_disabled_exterror(self): - await self.conn.eval( - """ + await self.conn.eval(""" require('msgpack').cfg{encode_error_as_ext = false} - """ - ) + """) try: - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local e = box.error.new{code=5,reason='A',type='B'} return e - """ - ) + """) e = resp[0] self.assertIsInstance(e, str) self.assertEqual("A", e) finally: - await self.conn.eval( - """ + await self.conn.eval(""" require('msgpack').cfg{encode_error_as_ext = true} - """ - ) + """) class MpExtDatetimeTestCase(BaseTarantoolTestCase): @ensure_version(min=(2, 10)) async def test__ext_datetime_read(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local date = require('datetime') return date.parse('2000-01-01T02:00:00.23+0300') - """ - ) + """) res = resp[0] - dt = datetime_fromisoformat("2000-01-01T02:00:00.230000+03:00") + dt = datetime.datetime.fromisoformat("2000-01-01T02:00:00.230000+03:00") self.assertEqual(dt, res) @ensure_version(min=(2, 10)) async def test__ext_datetime_tz(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local date = require('datetime') return date.parse('2000-01-01T02:00:00 MSK') - """ - ) + """) res = resp[0] - dt = datetime_fromisoformat("2000-01-01T02:00:00+03:00") + dt = datetime.datetime.fromisoformat("2000-01-01T02:00:00+03:00") self.assertEqual(dt, res) @ensure_version(min=(2, 10)) async def test__ext_datetime_read_neg_tz(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local date = require('datetime') return date.parse('2000-01-01T02:17:43.23-08:00') - """ - ) + """) res = resp[0] - dt = datetime_fromisoformat("2000-01-01T02:17:43.230000-08:00") + dt = datetime.datetime.fromisoformat("2000-01-01T02:17:43.230000-08:00") self.assertEqual(dt, res) @ensure_version(min=(2, 10)) async def test__ext_datetime_read_before_1970(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local date = require('datetime') return date.parse('1930-01-01T02:17:43.23-08:00') - """ - ) + """) res = resp[0] - dt = datetime_fromisoformat("1930-01-01T02:17:43.230000-08:00") + dt = datetime.datetime.fromisoformat("1930-01-01T02:17:43.230000-08:00") self.assertEqual(dt, res) @ensure_version(min=(2, 10)) async def test__ext_datetime_write(self): sp = "tester_ext_datetime" - dt = datetime_fromisoformat("2000-01-01T02:17:43.230000-08:00") + dt = datetime.datetime.fromisoformat("2000-01-01T02:17:43.230000-08:00") resp = await self.conn.insert(sp, [1, dt]) res = resp[0] self.assertEqual(dt, res["dt"]) @@ -261,7 +239,7 @@ async def test__ext_datetime_write(self): @ensure_version(min=(2, 10)) async def test__ext_datetime_write_before_1970(self): sp = "tester_ext_datetime" - dt = datetime_fromisoformat("1004-01-01T02:17:43.230000+04:00") + dt = datetime.datetime.fromisoformat("1004-01-01T02:17:43.230000+04:00") resp = await self.conn.insert(sp, [1, dt]) res = resp[0] self.assertEqual(dt, res["dt"]) @@ -269,7 +247,7 @@ async def test__ext_datetime_write_before_1970(self): @ensure_version(min=(2, 10)) async def test__ext_datetime_write_without_tz(self): sp = "tester_ext_datetime" - dt = datetime_fromisoformat("2022-04-23T02:17:43.450000") + dt = datetime.datetime.fromisoformat("2022-04-23T02:17:43.450000") resp = await self.conn.insert(sp, [1, dt]) res = resp[0] self.assertEqual(dt, res["dt"]) @@ -277,7 +255,7 @@ async def test__ext_datetime_write_without_tz(self): @ensure_version(min=(2, 10)) async def test__ext_datetime_write_without_tz_integer(self): sp = "tester_ext_datetime" - dt = datetime_fromisoformat("2022-04-23T02:17:43") + dt = datetime.datetime.fromisoformat("2022-04-23T02:17:43") resp = await self.conn.insert(sp, [1, dt]) res = resp[0] self.assertEqual(dt, res["dt"]) @@ -285,7 +263,7 @@ async def test__ext_datetime_write_without_tz_integer(self): @ensure_version(min=(2, 10)) async def test__ext_datetime_write_pytz(self): sp = "tester_ext_datetime" - dt = datetime_fromisoformat("2022-04-23T02:17:43") + dt = datetime.datetime.fromisoformat("2022-04-23T02:17:43") dt = pytz.timezone("Europe/Amsterdam").localize(dt) resp = await self.conn.insert(sp, [1, dt]) res = resp[0] @@ -294,7 +272,7 @@ async def test__ext_datetime_write_pytz(self): @ensure_version(min=(2, 10)) async def test__ext_datetime_write_pytz_america(self): sp = "tester_ext_datetime" - dt = datetime_fromisoformat("2022-04-23T02:17:43") + dt = datetime.datetime.fromisoformat("2022-04-23T02:17:43") dt = pytz.timezone("America/New_York").localize(dt) resp = await self.conn.insert(sp, [1, dt]) res = resp[0] @@ -304,8 +282,7 @@ async def test__ext_datetime_write_pytz_america(self): class MpExtIntervalTestCase(BaseTarantoolTestCase): @ensure_version(min=(2, 10)) async def test__ext_interval_read(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local datetime = require('datetime') return datetime.interval.new({ year=1, @@ -317,8 +294,7 @@ async def test__ext_interval_read(self): sec=7, nsec=8, }) - """ - ) + """) self.assertEqual( asynctnt.MPInterval( year=1, @@ -335,8 +311,7 @@ async def test__ext_interval_read(self): @ensure_version(min=(2, 10)) async def test__ext_interval_read_adjust_last(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local datetime = require('datetime') return datetime.interval.new({ year=1, @@ -349,8 +324,7 @@ async def test__ext_interval_read_adjust_last(self): nsec=8, adjust='last' }) - """ - ) + """) self.assertEqual( asynctnt.MPInterval( year=1, @@ -368,8 +342,7 @@ async def test__ext_interval_read_adjust_last(self): @ensure_version(min=(2, 10)) async def test__ext_interval_read_adjust_excess(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local datetime = require('datetime') return datetime.interval.new({ year=1, @@ -382,8 +355,7 @@ async def test__ext_interval_read_adjust_excess(self): nsec=8, adjust='excess' }) - """ - ) + """) self.assertEqual( asynctnt.MPInterval( year=1, @@ -401,8 +373,7 @@ async def test__ext_interval_read_adjust_excess(self): @ensure_version(min=(2, 10)) async def test__ext_interval_read_all_negative(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local datetime = require('datetime') return datetime.interval.new({ year=-1, @@ -415,8 +386,7 @@ async def test__ext_interval_read_all_negative(self): nsec=-8, adjust='excess' }) - """ - ) + """) self.assertEqual( asynctnt.MPInterval( year=-1, @@ -434,8 +404,7 @@ async def test__ext_interval_read_all_negative(self): @ensure_version(min=(2, 10)) async def test__ext_interval_read_all_mixed(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local datetime = require('datetime') return datetime.interval.new({ year=1, @@ -448,8 +417,7 @@ async def test__ext_interval_read_all_mixed(self): nsec=-8, adjust='excess' }) - """ - ) + """) self.assertEqual( asynctnt.MPInterval( year=1, @@ -467,12 +435,10 @@ async def test__ext_interval_read_all_mixed(self): @ensure_version(min=(2, 10)) async def test__ext_interval_read_zeros(self): - resp = await self.conn.eval( - """ + resp = await self.conn.eval(""" local datetime = require('datetime') return datetime.interval.new({}) - """ - ) + """) self.assertEqual( asynctnt.MPInterval(), resp[0], @@ -564,9 +530,3 @@ async def test__ext_interval_send_with_zeros(self): ], ) self.assertTrue(resp[0]) - - -def datetime_fromisoformat(s): - if sys.version_info < (3, 7, 0): - return dateutil.parser.isoparse(s) - return datetime.datetime.fromisoformat(s) diff --git a/tests/test_op_ping.py b/tests/test_op_ping.py index c4516a9..dfff1fc 100644 --- a/tests/test_op_ping.py +++ b/tests/test_op_ping.py @@ -36,15 +36,11 @@ async def test__ping_connection_lost(self): try: os.kill(self.tnt.pid, 0) - running = True except Exception: - running = False + pass with self.assertRaises(TarantoolNotConnectedError): - res = await self.conn.ping() - print(res) - print("running", running) - print(os.system("ps aux | grep tarantool")) + await self.conn.ping() self.tnt.start() await self.sleep(1) diff --git a/tests/test_op_push.py b/tests/test_op_push.py index cce71c7..7c24a20 100644 --- a/tests/test_op_push.py +++ b/tests/test_op_push.py @@ -228,7 +228,7 @@ async def test__push_read_all_disconnect(self): @ensure_version(min=(1, 10)) async def test__push_read_all_multiple_iterators(self): fut = self.conn.eval( - "box.session.push(1);" "box.session.push(2);" "box.session.push(3);", + "box.session.push(1);box.session.push(2);box.session.push(3);", push_subscribe=True, ) it1 = PushIterator(fut) diff --git a/tests/test_op_select.py b/tests/test_op_select.py index 8a6bebe..76a5d36 100644 --- a/tests/test_op_select.py +++ b/tests/test_op_select.py @@ -190,7 +190,9 @@ async def test__select_all_by_hash_index(self): data = await self._fill_data(4, space="no_schema_space") res = await self.conn.select("no_schema_space", index="primary_hash") - self.assertResponseEqual(res, data, "Body ok") + self.assertResponseEqual( + sorted(res, key=lambda t: t[0]), sorted(data, key=lambda t: t[0]), "Body ok" + ) async def test__select_key_tuple(self): try: diff --git a/tests/test_op_sql_execute.py b/tests/test_op_sql_execute.py index 574e7a6..bab0cd7 100644 --- a/tests/test_op_sql_execute.py +++ b/tests/test_op_sql_execute.py @@ -130,7 +130,7 @@ async def test__sql_insert_autoincrement_multiple(self): @ensure_version(min=(2, 0)) async def test__sql_insert_multiple(self): res = await self.conn.execute( - "insert into sql_space (id, name) " "values (1, 'one'), (2, 'two')" + "insert into sql_space (id, name) values (1, 'one'), (2, 'two')" ) self.assertEqual(2, res.rowcount, "rowcount ok") diff --git a/tests/test_response.py b/tests/test_response.py index 5575aa9..0e781b5 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -183,7 +183,7 @@ async def test__response_repr(self): ) self.assertEqual( - "", + "", repr(res[0]), "repr ok", )