Skip to content

Commit

Permalink
Split pgdb package into submodules
Browse files Browse the repository at this point in the history
  • Loading branch information
Cito committed Sep 5, 2023
1 parent 493c6ee commit 3ff98e3
Show file tree
Hide file tree
Showing 18 changed files with 1,917 additions and 1,821 deletions.
7 changes: 7 additions & 0 deletions docs/contents/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ Version 6.0 (to be released)
----------------------------
- Removed support for Python versions older than 3.7 (released June 2017)
and PostgreSQL older than version 10 (released October 2017).
- Converted the standalone modules `pg` and `pgdb` to packages with
several submodules each. The C extension module is now part of the
`pg` package and wrapped into the pure Python module `pg.core`.
- Added type hints and included a stub file for the C extension module.
- Added method `pkeys()` to the `pg.DB` object.
- Removed deprecated function `pg.pgnotify()`.
- Removed deprecated method `ntuples()` of the `pg.Query` object.
- Renamed `pgdb.Type` to `pgdb.DbType` to avoid confusion with `typing.Type`.
- `pg` and `pgdb` now use a shared row factory cache.
- The function `set_row_factory_size()` has been removed. The row cache is now
available as a `RowCache` class with methods `change_size()` and `clear()`.
- Modernized code and tools for development, testing, linting and building.

Version 5.2.5 (2023-08-28)
Expand Down
11 changes: 5 additions & 6 deletions pg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,9 @@
version,
)
from .db import DB
from .helpers import init_core, set_row_factory_size
from .helpers import RowCache, init_core
from .notify import NotificationHandler

__version__ = version

__all__ = [
'DB', 'Adapter',
'NotificationHandler', 'Typecasts',
Expand All @@ -110,7 +108,7 @@
'InvalidResultError', 'MultipleResultsError',
'NoResultError', 'NotSupportedError',
'OperationalError', 'ProgrammingError',
'Connection', 'Query',
'Connection', 'Query', 'RowCache',
'INV_READ', 'INV_WRITE',
'POLLING_OK', 'POLLING_FAILED', 'POLLING_READING', 'POLLING_WRITING',
'RESULT_DDL', 'RESULT_DML', 'RESULT_DQL', 'RESULT_EMPTY',
Expand All @@ -127,9 +125,10 @@
'set_datestyle', 'set_decimal', 'set_decimal_point',
'set_defbase', 'set_defhost', 'set_defopt',
'set_defpasswd', 'set_defport', 'set_defuser',
'set_jsondecode', 'set_query_helpers',
'set_row_factory_size', 'set_typecast',
'set_jsondecode', 'set_query_helpers', 'set_typecast',
'version', '__version__',
]

__version__ = version

init_core()
2 changes: 1 addition & 1 deletion pg/adapt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Adaption of parameters."""
"""Adaptation of parameters."""

from __future__ import annotations

Expand Down
27 changes: 23 additions & 4 deletions pg/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@

from typing import TypeVar

from .core import DatabaseError, Error, InternalError, ProgrammingError

__all__ = ['error', 'db_error', 'int_error', 'prg_error']
from .core import (
DatabaseError,
Error,
InterfaceError,
InternalError,
OperationalError,
ProgrammingError,
)

__all__ = [
'error', 'db_error', 'if_error', 'int_error', 'op_error', 'prg_error'
]

# Error messages

Expand All @@ -32,4 +41,14 @@ def int_error(msg: str) -> InternalError:

def prg_error(msg: str) -> ProgrammingError:
"""Return ProgrammingError."""
return error(msg, ProgrammingError)
return error(msg, ProgrammingError)


def if_error(msg: str) -> InterfaceError:
"""Return InterfaceError."""
return error(msg, InterfaceError)


def op_error(msg: str) -> OperationalError:
"""Return OperationalError."""
return error(msg, OperationalError)
60 changes: 40 additions & 20 deletions pg/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
SomeNamedTuple = Any # alias for accessing arbitrary named tuples

__all__ = [
'quote_if_unqualified', 'oid_key', 'set_row_factory_size',
'quote_if_unqualified', 'oid_key', 'QuoteDict', 'RowCache',
'dictiter', 'namediter', 'namednext', 'scalariter'
]

Expand All @@ -36,30 +36,50 @@ def oid_key(table: str) -> str:
"""Build oid key from a table name."""
return f'oid({table})'

class QuoteDict(dict):
"""Dictionary with auto quoting of its items.
# Row factory
The quote attribute must be set to the desired quote function.
"""

# The result rows for database operations are returned as named tuples
# by default. Since creating namedtuple classes is a somewhat expensive
# operation, we cache up to 1024 of these classes by default.
quote: Callable[[str], str]

@lru_cache(maxsize=1024)
def _row_factory(names: Sequence[str]) -> Callable[[Sequence], NamedTuple]:
"""Get a namedtuple factory for row results with the given names."""
try:
return namedtuple('Row', names, rename=True)._make # type: ignore
except ValueError: # there is still a problem with the field names
names = [f'column_{n}' for n in range(len(names))]
return namedtuple('Row', names)._make # type: ignore
def __getitem__(self, key: str) -> str:
"""Get a quoted value."""
return self.quote(super().__getitem__(key))


def set_row_factory_size(maxsize: int | None) -> None:
"""Change the size of the namedtuple factory cache.
class RowCache:
"""Global cache for the named tuples used for table rows.
If maxsize is set to None, the cache can grow without bound.
The result rows for database operations are returned as named tuples
by default. Since creating namedtuple classes is a somewhat expensive
operation, we cache up to 1024 of these classes by default.
"""
global _row_factory
_row_factory = lru_cache(maxsize)(_row_factory.__wrapped__)

@staticmethod
@lru_cache(maxsize=1024)
def row_factory(names: Sequence[str]) -> Callable[[Sequence], NamedTuple]:
"""Get a namedtuple factory for row results with the given names."""
try:
return namedtuple('Row', names, rename=True)._make # type: ignore
except ValueError: # there is still a problem with the field names
names = [f'column_{n}' for n in range(len(names))]
return namedtuple('Row', names)._make # type: ignore

@classmethod
def clear(cls) -> None:
"""Clear the namedtuple factory cache."""
cls.row_factory.cache_clear()

@classmethod
def change_size(cls, maxsize: int | None) -> None:
"""Change the size of the namedtuple factory cache.
If maxsize is set to None, the cache can grow without bound.
"""
row_factory = cls.row_factory.__wrapped__
cls.row_factory = lru_cache(maxsize)(row_factory) # type: ignore


# Helper functions used by the query object
Expand All @@ -73,14 +93,14 @@ def dictiter(q: Query) -> Generator[dict[str, Any], None, None]:

def namediter(q: Query) -> Generator[SomeNamedTuple, None, None]:
"""Get query result as an iterator of named tuples."""
row = _row_factory(q.listfields())
row = RowCache.row_factory(q.listfields())
for r in q:
yield row(r)


def namednext(q: Query) -> SomeNamedTuple:
"""Get next row from query result as a named tuple."""
return _row_factory(q.listfields())(next(q))
return RowCache.row_factory(q.listfields())(next(q))


def scalariter(q: Query) -> Generator[Any, None, None]:
Expand Down

0 comments on commit 3ff98e3

Please sign in to comment.