From 6722f5b36196232be546ce1d1ce26c57a130b48d Mon Sep 17 00:00:00 2001 From: igorcoding Date: Sat, 12 Aug 2023 21:56:47 +0300 Subject: [PATCH] Added support for PyPy building --- .github/workflows/actions.yaml | 30 ++++----- CHANGELOG.md | 5 +- Makefile | 2 +- asynctnt/__init__.py | 2 +- asynctnt/iproto/tupleobj/tupleobj.c | 94 ++++++++++++++++++++++++++--- bench/benchmark.py | 10 ++- pyproject.toml | 3 +- tests/__init__.py | 2 +- {asynctnt => tests}/_testbase.py | 0 tests/test_connect.py | 2 +- tests/test_mp_ext.py | 2 +- tests/test_op_push.py | 2 +- tests/test_op_sql_execute.py | 2 +- tests/test_op_sql_prepared.py | 2 +- tests/test_response.py | 23 +++++++ tests/test_stream.py | 2 +- 16 files changed, 144 insertions(+), 39 deletions(-) rename {asynctnt => tests}/_testbase.py (100%) diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml index e081fae..61d6ffc 100644 --- a/.github/workflows/actions.yaml +++ b/.github/workflows/actions.yaml @@ -7,22 +7,22 @@ jobs: strategy: matrix: os: [ ubuntu-latest, macos-latest ] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy3.10'] tarantool: ['1.10', '2'] exclude: - os: macos-latest tarantool: '1.10' - - os: macos-latest - tarantool: '2.7' + - python-version: 'pypy3.10' + tarantool: '1.10' runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: recursive - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install Tarantool ${{ matrix.tarantool }} @@ -62,11 +62,11 @@ jobs: - test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: recursive - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 - name: Install cibuildwheel run: python -m pip install --upgrade cibuildwheel @@ -74,9 +74,9 @@ jobs: - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse env: - CIBW_BUILD: "cp37-* cp38-* cp39-* cp310-* cp311-*" + CIBW_BUILD: "cp37-* cp38-* cp39-* cp310-* cp311-* pp310-*" - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: wheels path: ./wheelhouse/*.whl @@ -92,11 +92,11 @@ jobs: 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@v2 + - uses: actions/checkout@v3 with: submodules: recursive - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.10' @@ -104,7 +104,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools wheel twine build - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: name: wheels path: wheels @@ -133,12 +133,12 @@ jobs: needs: - test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: recursive - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.11' @@ -151,7 +151,7 @@ jobs: run: make docs - name: Deploy - uses: JamesIves/github-pages-deploy-action@4.1.4 + uses: JamesIves/github-pages-deploy-action@v4 with: branch: gh-pages folder: docs/_build/html diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f22381..eb63b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ * Dropped support for Python 3.6 **New features:** -* Added building wheels for Python 3.11 and 3.12 +* Added building wheels for Python 3.11 and initial support for 3.12 +* Added support for PyPy 3.10. It's compiling and working, but there is an obvious performance downgrade compared to CPython. +* Now repr() of TarantoolTuple objects is being truncated to 50 fields **Bug fixes:** * Fixed an issue with encoding of update operations as tuples on PyPy @@ -14,6 +16,7 @@ * Upgraded to Cython 3.0.0 * Using pyproject.toml for building spec * Using black, isort & ruff for linting +* _testbase.py was moved to tests/_testbase.py ## v2.0.1 * Fixed an issue with encoding datetimes less than 01-01-1970 (fixes [#29](https://github.com/igorcoding/asynctnt/issues/29)) diff --git a/Makefile b/Makefile index 41cb4ae..b1acef5 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ local: debug: clean - ASYNCTNT_DEBUG=1 $(PYTHON) -m pip install -e . + ASYNCTNT_DEBUG=1 $(PYTHON) -m pip install -e '.[test]' annotate: diff --git a/asynctnt/__init__.py b/asynctnt/__init__.py index 9804faa..4b1dbee 100644 --- a/asynctnt/__init__.py +++ b/asynctnt/__init__.py @@ -16,4 +16,4 @@ TarantoolTuple, ) -__version__ = "2.1.0" +__version__ = "2.1.0a1" diff --git a/asynctnt/iproto/tupleobj/tupleobj.c b/asynctnt/iproto/tupleobj/tupleobj.c index f720f3f..c891ee3 100644 --- a/asynctnt/iproto/tupleobj/tupleobj.c +++ b/asynctnt/iproto/tupleobj/tupleobj.c @@ -364,13 +364,18 @@ ttuple_repr(AtntTupleObject *v) { Py_ssize_t i, n; PyObject *keys_iter = NULL; - _PyUnicodeWriter writer; + int oversize = 0; n = Py_SIZE(v); if (n == 0) { return PyUnicode_FromString(""); } + if (n >= 50) { + n = 50; + oversize = 1; + } + if (v->metadata != NULL) { keys_iter = PyObject_GetIter(v->metadata->names); if (keys_iter == NULL) { @@ -384,6 +389,22 @@ ttuple_repr(AtntTupleObject *v) return i > 0 ? PyUnicode_FromString("") : NULL; } +#if defined(PYPY_VERSION) + + // we must have a separate implementation for PyPy because _PyUnicodeWriter is a private API + // it may become available in the future, but for now we have to use a workaround + // by building strings using only public API + // but this may change in future and _PyUnicodeWriter will become public: + // https://github.com/python/cpython/issues/107076#issuecomment-1646840936 + + PyObject *part = NULL; + PyObject *parts = PyList_New(n); + if (parts == NULL) { + goto error; + } + +#else + _PyUnicodeWriter writer; _PyUnicodeWriter_Init(&writer); writer.overallocate = 1; writer.min_length = 12; /* */ @@ -392,18 +413,14 @@ ttuple_repr(AtntTupleObject *v) goto error; } +#endif + for (i = 0; i < n; ++i) { PyObject *key = NULL; PyObject *key_repr = NULL; PyObject *val_repr = NULL; PyObject *i_obj = NULL; - if (i > 0) { - if (_PyUnicodeWriter_WriteChar(&writer, ' ') < 0) { - goto error; - } - } - if (Py_EnterRecursiveCall(" while getting the repr of a tarantool tuple")) { goto error; } @@ -433,6 +450,25 @@ ttuple_repr(AtntTupleObject *v) } } +#if defined(PYPY_VERSION) + + part = PyUnicode_FromFormat("%U=%U", key_repr, val_repr); + if (part == NULL) { + Py_DECREF(key_repr); + Py_DECREF(val_repr); + goto error; + } + PyList_SET_ITEM(parts, i, part); + +#else + if (i > 0) { + if (_PyUnicodeWriter_WriteChar(&writer, ' ') < 0) { + Py_DECREF(key_repr); + Py_DECREF(val_repr); + goto error; + } + } + if (_PyUnicodeWriter_WriteStr(&writer, key_repr) < 0) { Py_DECREF(key_repr); Py_DECREF(val_repr); @@ -450,20 +486,58 @@ ttuple_repr(AtntTupleObject *v) goto error; } Py_DECREF(val_repr); +#endif } - writer.overallocate = 0; - if (_PyUnicodeWriter_WriteChar(&writer, '>') < 0) { + PyObject *result = NULL; + +#if defined(PYPY_VERSION) + PyObject *space = NULL; + PyObject *parts_joined = NULL; + + space = PyUnicode_FromString(" "); + if (space == NULL) { goto error; } + parts_joined = PyUnicode_Join(space, parts); + if (parts_joined == NULL) { + Py_DECREF(space); + goto error; + } + Py_DECREF(space); + Py_XDECREF(parts); + + if (oversize) { + result = PyUnicode_FromFormat("", parts_joined); + } else { + result = PyUnicode_FromFormat("", parts_joined); + } + Py_XDECREF(parts_joined); +#else + writer.overallocate = 0; + if (oversize) { + if (_PyUnicodeWriter_WriteASCIIString(&writer, " ...>", 5) < 0) { + goto error; + } + } else { + if (_PyUnicodeWriter_WriteChar(&writer, '>') < 0) { + goto error; + } + } + result = _PyUnicodeWriter_Finish(&writer); +#endif Py_XDECREF(keys_iter); Py_ReprLeave((PyObject *)v); - return _PyUnicodeWriter_Finish(&writer); + return result; error: Py_XDECREF(keys_iter); +#if defined(PYPY_VERSION) + Py_XDECREF(parts); +#else _PyUnicodeWriter_Dealloc(&writer); +#endif Py_ReprLeave((PyObject *)v); return NULL; } diff --git a/bench/benchmark.py b/bench/benchmark.py index 4127030..ccd8769 100644 --- a/bench/benchmark.py +++ b/bench/benchmark.py @@ -33,9 +33,13 @@ def main(): ["execute", ["select 1 as a, 2 as b"], {"parse_metadata": False}], ] - for use_uvloop in [True]: + for use_uvloop in [False, True]: if use_uvloop: - import uvloop + try: + import uvloop + except ImportError: + print("No uvloop installed. Skipping.") + continue asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) else: @@ -77,7 +81,7 @@ async def bulk_f(): await getattr(conn, method)(*args, **kwargs) start = datetime.datetime.now() - coros = [bulk_f() for _ in range(b)] + coros = [asyncio.create_task(bulk_f()) for _ in range(b)] await asyncio.wait(coros) end = datetime.datetime.now() diff --git a/pyproject.toml b/pyproject.toml index 10c1c58..771cc61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,12 +40,13 @@ test = [ 'isort', 'black', 'ruff', - 'uvloop>=0.12.3; platform_system != "Windows"', + 'uvloop>=0.12.3; platform_system != "Windows" and python_version < "3.12" and platform.python_implementation != "PyPy"', 'pytest', 'pytest-cov', 'coverage[toml]', 'pytz', 'python-dateutil', + "Cython(>=3.0.0,<3.1.0)", # for coverage ] docs = [ diff --git a/tests/__init__.py b/tests/__init__.py index 04f967d..3ea4daf 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,7 +3,7 @@ import sys import unittest -from asynctnt._testbase import TarantoolTestCase +from ._testbase import TarantoolTestCase CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/asynctnt/_testbase.py b/tests/_testbase.py similarity index 100% rename from asynctnt/_testbase.py rename to tests/_testbase.py diff --git a/tests/test_connect.py b/tests/test_connect.py index e293a80..8c28445 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -2,7 +2,6 @@ import uuid import asynctnt -from asynctnt._testbase import check_version from asynctnt.connection import ConnectionState from asynctnt.exceptions import ( ErrorCode, @@ -11,6 +10,7 @@ ) from asynctnt.instance import TarantoolSyncInstance from tests import BaseTarantoolTestCase +from tests._testbase import check_version class ConnectTestCase(BaseTarantoolTestCase): diff --git a/tests/test_mp_ext.py b/tests/test_mp_ext.py index e727f9e..c6661d6 100644 --- a/tests/test_mp_ext.py +++ b/tests/test_mp_ext.py @@ -7,9 +7,9 @@ import pytz from asynctnt import IProtoError -from asynctnt._testbase import ensure_version from asynctnt.exceptions import ErrorCode, TarantoolDatabaseError from tests import BaseTarantoolTestCase +from tests._testbase import ensure_version class MpExtTestCase(BaseTarantoolTestCase): diff --git a/tests/test_op_push.py b/tests/test_op_push.py index b8576f6..cce71c7 100644 --- a/tests/test_op_push.py +++ b/tests/test_op_push.py @@ -2,9 +2,9 @@ import asynctnt from asynctnt import PushIterator, Response -from asynctnt._testbase import ensure_version from asynctnt.exceptions import TarantoolDatabaseError, TarantoolNotConnectedError from tests import BaseTarantoolTestCase +from tests._testbase import ensure_version class PushTestCase(BaseTarantoolTestCase): diff --git a/tests/test_op_sql_execute.py b/tests/test_op_sql_execute.py index 010ca92..5479eb8 100644 --- a/tests/test_op_sql_execute.py +++ b/tests/test_op_sql_execute.py @@ -1,6 +1,6 @@ from asynctnt import Response -from asynctnt._testbase import ensure_version from tests import BaseTarantoolTestCase +from tests._testbase import ensure_version class SQLExecuteTestCase(BaseTarantoolTestCase): diff --git a/tests/test_op_sql_prepared.py b/tests/test_op_sql_prepared.py index 6aeb6ea..9bc9e9e 100644 --- a/tests/test_op_sql_prepared.py +++ b/tests/test_op_sql_prepared.py @@ -1,7 +1,7 @@ from asynctnt import Response -from asynctnt._testbase import ensure_version from asynctnt.prepared import PreparedStatement from tests import BaseTarantoolTestCase +from tests._testbase import ensure_version class SQLPreparedStatementTestCase(BaseTarantoolTestCase): diff --git a/tests/test_response.py b/tests/test_response.py index 9a372c3..5575aa9 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -188,6 +188,29 @@ async def test__response_repr(self): "repr ok", ) + async def test__response_repr_trunc(self): + data = [0, "hello", 5, 6, "help", "common", "yo"] + for _ in range(50): + data.append("x") + + await self.conn.insert(self.TESTER_SPACE_ID, data) + + res = await self.conn.select("tester") + self.assertEqual(1, res.rowcount, "count correct") + self.assertTrue( + isinstance(res[0], TarantoolTuple), "expecting a TarantoolTuple" + ) + + tail = [] + for i in range(7, 50): # 50: maximum number of fields to show in repr + tail.append(f"{i}={repr('x')}") + + self.assertEqual( + f"", + repr(res[0]), + "repr ok", + ) + async def test__response_str(self): data = [0, "hello", 5, 6, "help", "common", "yo"] await self.conn.insert(self.TESTER_SPACE_ID, data) diff --git a/tests/test_stream.py b/tests/test_stream.py index c101975..192bfb4 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -1,8 +1,8 @@ import asyncio -from asynctnt._testbase import ensure_bin_version, ensure_version from asynctnt.exceptions import ErrorCode, TarantoolDatabaseError from tests import BaseTarantoolTestCase +from tests._testbase import ensure_bin_version, ensure_version @ensure_bin_version(min=(2, 10))