From: Daniele Varrazzo Date: Fri, 13 Mar 2020 10:27:19 +0000 (+1300) Subject: Adding a first implementation of the libpq wrapper and tests X-Git-Tag: 3.0.dev0~736 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=aecc520b010bc58f86c90ef9c39bc86a3343aa73;p=thirdparty%2Fpsycopg.git Adding a first implementation of the libpq wrapper and tests --- diff --git a/.gitignore b/.gitignore index 0a764a4de..31577c5a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ env +/psycopg3.egg-info diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..39c79ab60 --- /dev/null +++ b/README.rst @@ -0,0 +1,4 @@ +psycopg3 -- PostgreSQL database adaapter for Python +=================================================== + +psycopg3 is a modern implementation of a PostgreSQL adapter for Python. diff --git a/psycopg3/__init__.py b/psycopg3/__init__.py new file mode 100644 index 000000000..ce7a0b6a1 --- /dev/null +++ b/psycopg3/__init__.py @@ -0,0 +1,9 @@ +""" +psycopg3 -- PostgreSQL database adaapter for Python +""" + +# Copyright (C) 2020 The Psycopg Team + +from .consts import VERSION as __version__ + +__all__ = ["__version__"] diff --git a/psycopg3/_pq_ctypes.py b/psycopg3/_pq_ctypes.py new file mode 100644 index 000000000..bfa0ad974 --- /dev/null +++ b/psycopg3/_pq_ctypes.py @@ -0,0 +1,31 @@ +""" +libpq access using ctypes +""" + +# Copyright (C) 2020 The Psycopg Team + +import ctypes +import ctypes.util +from ctypes import Structure, POINTER +from ctypes import c_char_p, c_int + +pq = ctypes.pydll.LoadLibrary(ctypes.util.find_library("pq")) + + +class PGconn(Structure): + _fields_ = [] + + +PGconn_ptr = POINTER(PGconn) + +PQconnectdb = pq.PQconnectdb +PQconnectdb.argtypes = [c_char_p] +PQconnectdb.restype = PGconn_ptr + +PQstatus = pq.PQstatus +PQstatus.argtypes = [PGconn_ptr] +PQstatus.restype = c_int + +PQerrorMessage = pq.PQerrorMessage +PQerrorMessage.argtypes = [PGconn_ptr] +PQerrorMessage.restype = c_char_p diff --git a/psycopg3/consts.py b/psycopg3/consts.py new file mode 100644 index 000000000..ae4038794 --- /dev/null +++ b/psycopg3/consts.py @@ -0,0 +1,7 @@ +""" +psycopg3 package constants +""" + +# Copyright (C) 2020 The Psycopg Team + +VERSION = "2.99.0" diff --git a/psycopg3/pq.py b/psycopg3/pq.py new file mode 100644 index 000000000..63332fef6 --- /dev/null +++ b/psycopg3/pq.py @@ -0,0 +1,18 @@ +""" +psycopg3 libpq wrapper + +This package exposes the libpq functionalities as Python objects and functions. + +The real implementation (the binding to the C library) is +implementation-dependant but all the implementations share the same interface. +""" + +# Copyright (C) 2020 The Psycopg Team + +from .pq_enums import ConnStatusType + +from . import pq_ctypes as pq_module + +PGconn = pq_module.PGconn + +__all__ = ("ConnStatusType", "PGconn") diff --git a/psycopg3/pq_ctypes.py b/psycopg3/pq_ctypes.py new file mode 100644 index 000000000..6f5105486 --- /dev/null +++ b/psycopg3/pq_ctypes.py @@ -0,0 +1,41 @@ +""" +libpq Python wrapper using ctypes bindings. + +Clients shouldn't use this module directly, unless for testing: they should use +the `pq` module instead, which is in charge of choosing the best +implementation. +""" + +# Copyright (C) 2020 The Psycopg Team + +from .pq_enums import ConnStatusType + +from . import _pq_ctypes as impl + + +class PGconn: + __slots__ = ("pgconn_ptr",) + + def __init__(self, pgconn_ptr): + self.pgconn_ptr = pgconn_ptr + + @classmethod + def connectdb(cls, conninfo): + if isinstance(conninfo, str): + conninfo = conninfo.encode("utf8") + + if not isinstance(conninfo, bytes): + raise TypeError("bytes expected, got %r instead" % conninfo) + + pgconn_ptr = impl.PQconnectdb(conninfo) + return cls(pgconn_ptr) + + @property + def status(self): + rv = impl.PQstatus(self.pgconn_ptr) + return ConnStatusType(rv) + + @property + def error_message(self): + # TODO: decode + return impl.PQerrorMessage(self.pgconn_ptr) diff --git a/psycopg3/pq_enums.py b/psycopg3/pq_enums.py new file mode 100644 index 000000000..338063c6e --- /dev/null +++ b/psycopg3/pq_enums.py @@ -0,0 +1,12 @@ +""" +libpq enum definitions for psycopg3 +""" + +# Copyright (C) 2020 The Psycopg Team + +from enum import IntEnum + + +class ConnStatusType(IntEnum): + CONNECTION_OK = 0 + CONNECTION_BAD = 1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..a8f43fefd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 79 diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..240e70b02 --- /dev/null +++ b/setup.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +psycopg3 -- PostgreSQL database adaapter for Python +""" + +# Copyright (C) 2020 The Psycopg Team + + +import re +import os +from setuptools import setup + +# Grab the version without importing the module +# or we will get import errors on install if prerequisites are still missing +fn = os.path.join(os.path.dirname(__file__), "psycopg3/consts.py") +with open(fn) as f: + m = re.search(r"""(?mi)^VERSION\s*=\s*["']+([^'"]+)["']+""", f.read()) +if m: + version = m.group(1) +else: + raise ValueError("cannot find VERSION in the consts module") + +# Read the description from the README +with open("README.rst") as f: + readme = f.read() + +# TODO: classifiers +classifiers = """ +Programming Language :: Python :: 3 +Topic :: Database +Topic :: Software Development +""" + +setup( + name="psycopg3", + description=readme.splitlines()[0], + long_description="\n".join(readme.splitlines()[2:]).lstrip(), + author="Daniele Varrazzo", + author_email="daniele.varrazzo@gmail.com", + url="https://psycopg.org/psycopg3", + license="TBD", # TODO + python_requires=">=3.6", + packages=["psycopg3"], + classifiers=[x for x in classifiers.split("\n") if x], + zip_safe=False, + version=version, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..16040b121 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ("tests.fix_db",) diff --git a/tests/fix_db.py b/tests/fix_db.py new file mode 100644 index 000000000..44afd3c59 --- /dev/null +++ b/tests/fix_db.py @@ -0,0 +1,29 @@ +import os +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--test-dsn", + metavar="DSN", + default=os.environ.get("PSYCOPG3_TEST_DSN") or None, + help="Connection string to run database tests requiring a connection" + " [you can also use the PSYCOPG3_TEST_DSN env var].", + ) + + +@pytest.fixture +def pq(): + """The libpq module wrapper to test.""" + from psycopg3 import pq + + return pq + + +@pytest.fixture() +def dsn(request): + """Return the dsn used to connect to the `--test-dsn` database.""" + dsn = request.config.getoption("--test-dsn") + if not dsn: + pytest.skip("skipping test as no --test-dsn") + return dsn diff --git a/tests/test_pq.py b/tests/test_pq.py new file mode 100644 index 000000000..fb0bebb15 --- /dev/null +++ b/tests/test_pq.py @@ -0,0 +1,22 @@ +import pytest + + +def test_PQconnectdb(pq, dsn): + conn = pq.PGconn.connectdb(dsn) + assert conn.status == pq.ConnStatusType.CONNECTION_OK, conn.error_message + + +def test_PQconnectdb_bytes(pq, dsn): + conn = pq.PGconn.connectdb(dsn.encode("utf8")) + assert conn.status == pq.ConnStatusType.CONNECTION_OK, conn.error_message + + +def test_PQconnectdb_error(pq): + conn = pq.PGconn.connectdb("dbname=psycopg3_test_not_for_real") + assert conn.status == pq.ConnStatusType.CONNECTION_BAD + + +@pytest.mark.parametrize("baddsn", [None, 42]) +def test_PQconnectdb_badtype(pq, baddsn): + with pytest.raises(TypeError): + pq.PGconn.connectdb(baddsn)