diff --git a/CHANGES.txt b/CHANGES.txt index d5050f9..05c8daa 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,13 @@ Changes ------- +0.4.1 (2023-10-28) +^^^^^^^^^^^^^^^^^^ +* Implemented cursor setinputsizes. +* Implemented cursor fetchval. +* Added more type annotations. +* Added autocommit setter for cusror. + + 0.4.0 (2023-03-16) ^^^^^^^^^^^^^^^^^^ * Fixed compatibility with python 3.9+. diff --git a/aioodbc/cursor.py b/aioodbc/cursor.py index b525dea..52c5881 100644 --- a/aioodbc/cursor.py +++ b/aioodbc/cursor.py @@ -145,9 +145,21 @@ def executemany(self, sql, *params): def callproc(self, procname, args=()): raise NotImplementedError - async def setinputsizes(self, *args, **kwargs): - """Does nothing, required by DB API.""" - return None + async def setinputsizes(self, sizes=None) -> None: + """Explicitly declare the types and sizes of the parameters in a query. + Set to None to clear any previously registered input sizes. + + :param sizes: A list of tuples, one tuple for each query parameter, + where each tuple contains: + 1. the column datatype + 2. the column size (char length or decimal precision) + 3. the decimal scale. + + For example: + [(pyodbc.SQL_WVARCHAR, 50, 0), (pyodbc.SQL_DECIMAL, 18, 4)] + """ + # sizes: Optional[Iterable[Tuple[int, int, int]]] + await self._run_operation(self._impl.setinputsizes, sizes) async def setoutputsize(self, *args, **kwargs): """Does nothing, required by DB API.""" @@ -163,6 +175,16 @@ def fetchone(self): fut = self._run_operation(self._impl.fetchone) return fut + def fetchval(self): + """Returns the first column of the first row if there are results. + + A ProgrammingError exception is raised if no SQL has been executed + or if it did not return a result set (e.g. was not a SELECT + statement). + """ + fut = self._run_operation(self._impl.fetchval) + return fut + def fetchall(self): """Returns a list of all remaining rows. diff --git a/requirements-dev.txt b/requirements-dev.txt index f5bcdca..fca761c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ -e . -aiodocker==0.21.0 bandit==1.7.5 black==23.3.0 flake8-bugbear==23.3.23 @@ -8,7 +7,7 @@ ipdb==0.13.13 ipython==8.13.2 isort==5.12.0 mypy==1.2.0 -pyodbc>=5.0.0b3 +pyodbc==5.0.1 pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-faulthandler==2.0.1 @@ -17,4 +16,4 @@ pytest==7.3.1 sphinx==7.0.0 sphinxcontrib-asyncio==0.3.0 twine==4.0.2 -uvloop==0.17.0 +uvloop==0.19.0 diff --git a/setup.py b/setup.py index a997419..5c4aaf3 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,8 @@ from setuptools import find_packages, setup -install_requires = ["pyodbc>=5.0.0b3"] + +install_requires = ["pyodbc>=5.0.1"] def read(f): diff --git a/tests/conftest.py b/tests/conftest.py index 7d44d31..431b732 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,12 @@ import asyncio import gc import os -import random -import time import uuid from concurrent.futures import ThreadPoolExecutor -from contextlib import asynccontextmanager -import pyodbc import pytest import pytest_asyncio import uvloop -from aiodocker import Docker import aioodbc @@ -43,163 +38,6 @@ def loop(event_loop): return event_loop -@pytest.fixture(scope="session") -async def docker(): - client = Docker() - - try: - yield client - finally: - await client.close() - - -@pytest.fixture(scope="session") -def host(): - # Alternative: host.docker.internal, however not working on travis - return os.environ.get("DOCKER_MACHINE_IP", "127.0.0.1") - - -@pytest_asyncio.fixture -async def pg_params(pg_server): - server_info = pg_server["pg_params"] - return dict(**server_info) - - -@asynccontextmanager -async def _pg_server_helper(host, docker, session_id): - pg_tag = "9.5" - - await docker.pull(f"postgres:{pg_tag}") - container = await docker.containers.create_or_replace( - name=f"aioodbc-test-server-{pg_tag}-{session_id}", - config={ - "Image": f"postgres:{pg_tag}", - "AttachStdout": False, - "AttachStderr": False, - "HostConfig": { - "PublishAllPorts": True, - }, - }, - ) - await container.start() - container_port = await container.port(5432) - port = container_port[0]["HostPort"] - - pg_params = { - "database": "postgres", - "user": "postgres", - "password": "mysecretpassword", - "host": host, - "port": port, - } - - start = time.time() - dsn = create_pg_dsn(pg_params) - last_error = None - container_info = { - "port": port, - "pg_params": pg_params, - "container": container, - "dsn": dsn, - } - try: - while (time.time() - start) < 40: - try: - conn = pyodbc.connect(dsn) - cur = conn.execute("SELECT 1;") - cur.close() - conn.close() - break - except pyodbc.Error as e: - last_error = e - await asyncio.sleep(random.uniform(0.1, 1)) - else: - pytest.fail(f"Cannot start postgres server: {last_error}") - - yield container_info - finally: - container = container_info["container"] - if container: - await container.kill() - await container.delete(v=True, force=True) - - -@pytest.fixture(scope="session") -async def pg_server(host, docker, session_id): - async with _pg_server_helper(host, docker, session_id) as helper: - yield helper - - -@pytest.fixture -async def pg_server_local(host, docker): - async with _pg_server_helper(host, docker, None) as helper: - yield helper - - -@pytest.fixture -async def mysql_params(mysql_server): - server_info = (mysql_server)["mysql_params"] - return dict(**server_info) - - -@pytest.fixture(scope="session") -async def mysql_server(host, docker, session_id): - mysql_tag = "5.7" - await docker.pull(f"mysql:{mysql_tag}") - container = await docker.containers.create_or_replace( - name=f"aioodbc-test-server-{mysql_tag}-{session_id}", - config={ - "Image": f"mysql:{mysql_tag}", - "AttachStdout": False, - "AttachStderr": False, - "Env": [ - "MYSQL_USER=aioodbc", - "MYSQL_PASSWORD=mysecretpassword", - "MYSQL_DATABASE=aioodbc", - "MYSQL_ROOT_PASSWORD=mysecretpassword", - ], - "HostConfig": { - "PublishAllPorts": True, - }, - }, - ) - await container.start() - port = (await container.port(3306))[0]["HostPort"] - mysql_params = { - "database": "aioodbc", - "user": "aioodbc", - "password": "mysecretpassword", - "host": host, - "port": port, - } - dsn = create_mysql_dsn(mysql_params) - start = time.time() - try: - last_error = None - while (time.time() - start) < 30: - try: - conn = pyodbc.connect(dsn) - cur = conn.execute("SELECT 1;") - cur.close() - conn.close() - break - except pyodbc.Error as e: - last_error = e - await asyncio.sleep(random.uniform(0.1, 1)) - else: - pytest.fail(f"Cannot start mysql server: {last_error}") - - container_info = { - "port": port, - "mysql_params": mysql_params, - } - - yield container_info - finally: - await container.kill() - await container.delete(v=True, force=True) - - @pytest.fixture def executor(): executor = ThreadPoolExecutor(max_workers=1) @@ -215,7 +53,7 @@ def pytest_configure(): @pytest.fixture -def db(request): +def db(): return "sqlite" diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 9b537f5..e653de7 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -56,7 +56,11 @@ async def test_cursor(conn): assert cur.arraysize == 1 assert cur.rowcount == -1 - r = await cur.setinputsizes() + r = await cur.setinputsizes( + [ + (pyodbc.SQL_WVARCHAR, 50, 0), + ] + ) assert r is None await cur.setoutputsize() @@ -172,6 +176,18 @@ async def test_fetchone(conn, table): await cur.close() +@pytest.mark.parametrize("db", pytest.db_list) +@pytest.mark.asyncio +async def test_fetchval(conn, table): + cur = await conn.cursor() + await cur.execute("SELECT * FROM t1;") + resp = await cur.fetchval() + expected = 1 + + assert expected == resp + await cur.close() + + @pytest.mark.parametrize("db", ["sqlite"]) @pytest.mark.asyncio async def test_tables(conn, table):