typeid-python 0.3 returns uuid_utils.UUID from .uuid: psycopg2 can't adapt it, uuid6 disappears, and TypeID is now slotted
Context: SQLAlchemy TypeDecorator storing TypeIDs as a Postgres UUID column (impl = sqlalchemy.types.UUID) via psycopg2, plus an ObjID(TypeID) subclass.
What changed in typeid-python 0.3.x (checked 0.3.10, typeid/core/typeid.py):
- Dependency
uuid6replaced byuuid_utils. Any code that didimport uuid6because it came along transitively now fails at import, including alembic migrations that minteduuid6.uuid7(). TypeID.uuidreturnsuuid_utils.UUIDwhen parsed from a string (lazy decode).isinstance(u, uuid.UUID)is False. With psycopg2,sqlalchemy.types.UUID().bind_processor(psycopg2 dialect)is None, so the object goes straight to psycopg2:ProgrammingError: can't adapt type 'uuid_utils.UUID'. (from_uuid(x)stores whatever object you pass, so the type of.uuiddepends on how the TypeID was built.)TypeIDdeclares__slots__. A subclass without__slots__gets a__dict__, soplain_typeid.__class__ = MySubclassraisesTypeError: object layout differs.
Fixes:
class ObjID(TypeID):
__slots__ = () # same layout as TypeID; __class__ swaps stay legal
def to_dt(self): # UUIDv7: first 48 bits are Unix ms; no uuid6 needed
ms = int.from_bytes(self.uuid_bytes[:6], "big")
return datetime.datetime.fromtimestamp(ms / 1000, tz=datetime.timezone.utc)
@classmethod
def new(cls, prefix):
return cls(prefix=prefix) # TypeID mints a UUIDv7 when no suffix
# TypeDecorator.process_bind_param
return uuid.UUID(bytes=value.uuid_bytes) # stdlib UUID psycopg2 can adaptIn migrations, replace uuid6.uuid7() with uuid_utils.compat.uuid7() (returns a stdlib uuid.UUID, same as uuid6's subclass did) and declare uuid-utils as a direct dependency.
Detect before upgrading: grep for uuid6, .uuid passed to a DB driver, and __class__ = on TypeIDs. A test suite that writes rows through the TypeDecorator catches #2 immediately; #3 only shows if a test passes a bare TypeID through the swap path.
Context: SQLAlchemy TypeDecorator storing TypeIDs as a Postgres UUID column (impl = sqlalchemy.types.UUID) via psycopg2, plus an ObjID(TypeID) subclass.
What changed in typeid-python 0.3.x (checked 0.3.10, typeid/core/typeid.py):
- Dependency
uuid6replaced byuuid_utils. Any code that didimport uuid6because it came along transitively now fails at import, including alembic migrations that minteduuid6.uuid7(). TypeID.uuidreturnsuuid_utils.UUIDwhen parsed from a string (lazy decode).isinstance(u, uuid.UUID)is False. With psycopg2,sqlalchemy.types.UUID().bind_processor(psycopg2 dialect)is None, so the object goes straight to psycopg2:ProgrammingError: can't adapt type 'uuid_utils.UUID'. (from_uuid(x)stores whatever object you pass, so the type of.uuiddepends on how the TypeID was built.)TypeIDdeclares__slots__. A subclass without__slots__gets a__dict__, soplain_typeid.__class__ = MySubclassraisesTypeError: object layout differs.
Fixes:
class ObjID(TypeID):
__slots__ = () # same layout as TypeID; __class__ swaps stay legal
def to_dt(self): # UUIDv7: first 48 bits are Unix ms; no uuid6 needed
ms = int.from_bytes(self.uuid_bytes[:6], "big")
return datetime.datetime.fromtimestamp(ms / 1000, tz=datetime.timezone.utc)
@classmethod
def new(cls, prefix):
return cls(prefix=prefix) # TypeID mints a UUIDv7 when no suffix
# TypeDecorator.process_bind_param
return uuid.UUID(bytes=value.uuid_bytes) # stdlib UUID psycopg2 can adaptIn migrations, replace uuid6.uuid7() with uuid_utils.compat.uuid7() (returns a stdlib uuid.UUID, same as uuid6's subclass did) and declare uuid-utils as a direct dependency.
Detect before upgrading: grep for uuid6, .uuid passed to a DB driver, and __class__ = on TypeIDs. A test suite that writes rows through the TypeDecorator catches #2 immediately; #3 only shows if a test passes a bare TypeID through the swap path.