]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Code review fixes 12594/head
authorThomas Stephenson <ovangle@gmail.com>
Fri, 13 Jun 2025 09:57:15 +0000 (19:57 +1000)
committerThomas Stephenson <ovangle@gmail.com>
Mon, 16 Jun 2025 12:23:26 +0000 (22:23 +1000)
- Removed dprint statements
- Added right-hand versions of commutative operators
- Improved docstrings
- Added changelog entry
- Added dialect documentation

doc/build/changelog/migration_21.rst
doc/build/changelog/unreleased_21/10556.rst [new file with mode: 0644]
doc/build/dialects/postgresql.rst
lib/sqlalchemy/dialects/postgresql/asyncpg.py
lib/sqlalchemy/dialects/postgresql/bitstring.py

index 5dcc9bea09efcdbcf4f63c505cab5ec2f3c3e319..6ad390d1d2115c098ee66b99f2d108ffdafcf34b 100644 (file)
@@ -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 (file)
index 0000000..aed87b1
--- /dev/null
@@ -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.
index 009463e6ee860689d7807eae43cf9a76048fff02..4267f6a965f52e121e876fce93daf4bdfeaca62a 100644 (file)
@@ -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
 ---------------------
 
index 9ca642818939f114952357ca25b1dbf6ef09398a..79dd36b2b9b42523fb385655bfb7498959f0cf7c 100644 (file)
@@ -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
 
index 7b731f667692fc503624c7b8ffc5839dab409bb2..06aae6a9ac65bff79df1045aa625d25ad02e091e 100644 (file)
@@ -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__