From: Thomas Stephenson Date: Fri, 13 Jun 2025 09:57:15 +0000 (+1000) Subject: Code review fixes X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F12594%2Fhead;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Code review fixes - Removed dprint statements - Added right-hand versions of commutative operators - Improved docstrings - Added changelog entry - Added dialect documentation --- diff --git a/doc/build/changelog/migration_21.rst b/doc/build/changelog/migration_21.rst index 5dcc9bea09..6ad390d1d2 100644 --- a/doc/build/changelog/migration_21.rst +++ b/doc/build/changelog/migration_21.rst @@ -384,3 +384,25 @@ would appear in a valid ODBC connection string (i.e., the same as would be required if using the connection string directly with ``pyodbc.connect()``). :ticket:`11250` + +.. _change_12594 + +Addition of ``BitString`` subclass for handling postgresql ``BIT`` columns +-------------------------------------------------------------------------- + +Values of :class:`_dialects.postgresl.BIT` columns in the postgresql dialect are +now objects of a new :class:`str` subclass, +:class:`_dialects.postgresql.BitString`. Previously, the value of :class:`BIT` +columns was driver dependent, with most drivers using :class:`str` instances +except asyncpg, which used :class:`asyncpg.BitString`. + +For implementations using the `psycopg`, `psycopg2` or `pg8000` drivers, the new type is +mostly compatible with :class:`str` with some additional methods for manipulating bits and +implementations of the bitwise operators. However, equality and ordering +value will now fail. This can be rectified by wrapping or unwrapping one side +of the comparison so that both sides are similarly typed. + +For implementations using the `asyncpg` driver, the new type is incompatible with +the existing :class:`asyncpg.BitString` type. + +:ticket:`10556` \ No newline at end of file diff --git a/doc/build/changelog/unreleased_21/10556.rst b/doc/build/changelog/unreleased_21/10556.rst new file mode 100644 index 0000000000..aed87b1c47 --- /dev/null +++ b/doc/build/changelog/unreleased_21/10556.rst @@ -0,0 +1,11 @@ +.. change:: + :tags: feature, postgresql + :tickets: 10556 + +Adds the :class:`dialects.postgresql.BitString` representing PostgreSQL bitstrings +in python. This type, which is a subtype of the :class:`str` builtin includes +functionality for converting to/from :class:`int` and :class:`bytes`, in +addition to implementing utility methods and operators for dealing with bits. + +In PostgreSQL drivers, the :class:`postgresql.BIT` type now binds to instances of +this new utility class. diff --git a/doc/build/dialects/postgresql.rst b/doc/build/dialects/postgresql.rst index 009463e6ee..4267f6a965 100644 --- a/doc/build/dialects/postgresql.rst +++ b/doc/build/dialects/postgresql.rst @@ -374,6 +374,21 @@ don't yet fully support, conversion of rows to Python ``ipaddress`` datatypes .. versionadded:: 2.0.18 - added the ``native_inet_types`` parameter. +PostgreSQL BIT type +------------------- + +The PostgreSQL dialect provides a :class:`_postgresql.BitString` type which +represents an ordered sequence of boolean switches. This is exposed in python as a +subclass of :class:`str` which exposes appropriate bitwise methods and operators. + +* :class:`_postgrsql.BIT` - Typing support for PostgreSQL bitstrings. + +* :class:`_postgresql.BitString` - Python implementation of postgresql bitstrings + +.. versionadded:: 2.1 + +PostgreSQL supports + PostgreSQL Data Types --------------------- diff --git a/lib/sqlalchemy/dialects/postgresql/asyncpg.py b/lib/sqlalchemy/dialects/postgresql/asyncpg.py index 9ca6428189..79dd36b2b9 100644 --- a/lib/sqlalchemy/dialects/postgresql/asyncpg.py +++ b/lib/sqlalchemy/dialects/postgresql/asyncpg.py @@ -247,11 +247,9 @@ class AsyncpgBit(BIT): asyncpg_BitString = dialect.dbapi.asyncpg.BitString def to_bind(value): - print(f"processing bound value '{value}'") if isinstance(value, str): value = BitString(value) r = asyncpg_BitString.from_int(int(value), len(value)) - print(f"returning {r}") return r return value @@ -260,7 +258,6 @@ class AsyncpgBit(BIT): def result_processor(self, dialect, coltype): def to_result(value): if value is not None: - print(f"result {value} length {len(value)}") value = BitString.from_int(value.to_int(), length=len(value)) return value diff --git a/lib/sqlalchemy/dialects/postgresql/bitstring.py b/lib/sqlalchemy/dialects/postgresql/bitstring.py index 7b731f6676..06aae6a9ac 100644 --- a/lib/sqlalchemy/dialects/postgresql/bitstring.py +++ b/lib/sqlalchemy/dialects/postgresql/bitstring.py @@ -7,7 +7,6 @@ from __future__ import annotations import functools -import itertools import math from typing import Any from typing import cast @@ -17,29 +16,26 @@ from typing import SupportsIndex @functools.total_ordering class BitString(str): - """Represent a PostgreSQL bit string. + """Represent a PostgreSQL bit string in python""" - e.g. - b = BitString('101') - """ + _DIGITS = frozenset("01") def __new__(cls, _value: str, _check: bool = True) -> BitString: - if not isinstance(_value, BitString) and ( - _check and _value and any(c not in "01" for c in _value) - ): + do_check = not isinstance(_value, BitString) or _check + if do_check and cls._DIGITS.union(_value) > cls._DIGITS: raise ValueError("BitString must only contain '0' and '1' chars") return super().__new__(cls, _value) @classmethod def from_int(cls, value: int, length: int) -> BitString: """ - Returns a BitString consisting of the bits in the little-endian - representation of the given python int ``value``. A ``ValueError`` - is raised if ``value`` is not a non-negative integer. + Returns a BitString consisting of the bits in the integer ``value``. + A ``ValueError`` is raised if ``value`` is not a non-negative integer. If the provided ``value`` can not be represented in a bit string of at most ``length``, a ``ValueError`` will be raised. The bitstring will be padded on the left by ``'0'`` to bits to produce a + bitstring of the desired length. """ if value < 0: raise ValueError("value must be non-negative") @@ -86,7 +82,9 @@ class BitString(str): """ Returns the value of the flag at the given index - e.g. BitString('0101').get_flag(4) == 1 + e.g.:: + + BitString("0101").get_flag(4) == "1" """ return cast(Literal["0", "1"], super().__getitem__(index)) @@ -196,21 +194,7 @@ class BitString(str): return int(self, 2) if self else 0 def to_bytes(self, length: int = -1) -> bytes: - s = str(self) - bs: list[int] = [] - while s: - bs.insert(0, int(s[-8:], 2)) - s = s[:-8] - if length >= 0: - bs = list(itertools.dropwhile(lambda c: c == 0, bs)) - if len(bs) > length: - raise ValueError( - f"Cannot fit a BitString of length {len(self)} into a " - f"bytes instance of length {length}" - ) - # "zfill" the result with 0 bytes - bs = [0] * (length - len(bs)) + bs - return bytes(bs) + return int(self).to_bytes(length if length >= 0 else self.octet_length) def __bytes__(self) -> bytes: return self.to_bytes() @@ -226,25 +210,23 @@ class BitString(str): def __hash__(self) -> int: return hash(BitString) ^ super().__hash__() - def __getitem__(self, key: SupportsIndex | slice[Any, Any, Any]) -> str: + def __getitem__( + self, key: SupportsIndex | slice[Any, Any, Any] + ) -> BitString: return BitString(super().__getitem__(key), False) def __add__(self, o: str) -> BitString: """Return self + o""" if not isinstance(o, str): raise TypeError( - ("Can only concatenate str (not '{0}') to BitString").format( - type(o) - ) + f"Can only concatenate str (not '{type(self)}') to BitString" ) return BitString("".join([self, o])) def __radd__(self, o: str) -> BitString: if not isinstance(o, str): raise TypeError( - (f"Can only concatenate str (not '{0}') to BitString").format( - type(o) - ) + f"Can only concatenate str (not '{type(self)}') to BitString" ) return BitString("".join([o, self])) @@ -253,7 +235,8 @@ class BitString(str): Shifts each the bitstring to the left by the given amount. String length is preserved. - i.e. BitString('000101') << 1 == BitString('001010') + e.g.:: + BitString("000101") << 1 == BitString("001010") """ return BitString( "".join([self, *("0" for _ in range(amount))])[-len(self) :], False @@ -264,7 +247,8 @@ class BitString(str): Shifts each bit in the bitstring to the right by the given amount. String length is preserved. - e.g. BitString('101') >> 1 == BitString('010') + e.g.:: + BitString("101") >> 1 == BitString("010") """ return BitString(self[:-amount], False).zfill(width=len(self)) @@ -272,7 +256,8 @@ class BitString(str): """ Inverts (~) each bit in the bitstring - e.g. ~BitString('01010') == BitString('10101') + e.g.:: + ~BitString("01010") == BitString("10101") """ return BitString("".join("1" if x == "0" else "0" for x in self)) @@ -281,7 +266,8 @@ class BitString(str): Performs a bitwise and (``&``) with the given operand. A ``ValueError`` is raised if the operand is not the same length. - e.g. BitString('011') & BitString('011') == BitString('010') + e.g.:: + BitString("011") & BitString("011") == BitString("010") """ if not isinstance(o, str): @@ -303,7 +289,8 @@ class BitString(str): Performs a bitwise or (``|``) with the given operand. A ``ValueError`` is raised if the operand is not the same length. - e.g. BitString('011') | BitString('010') == BitString('011') + e.g.:: + BitString("011") | BitString("010") == BitString("011") """ if not isinstance(o, str): return NotImplemented @@ -325,7 +312,8 @@ class BitString(str): Performs a bitwise xor (``^``) with the given operand. A ``ValueError`` is raised if the operand is not the same length. - e.g. BitString('011') ^ BitString('010') == BitString('001') + e.g.:: + BitString("011") ^ BitString("010") == BitString("001") """ if not isinstance(o, BitString): @@ -345,3 +333,7 @@ class BitString(str): ), False, ) + + __rand__ = __and__ + __ror__ = __or__ + __rxor__ = __xor__