From 03b22b032bac32233864c06ebb178377e1a51293 Mon Sep 17 00:00:00 2001 From: Cody Fincher Date: Mon, 27 Jul 2026 20:18:29 +0000 Subject: [PATCH] fix: correct isinstance and issubclass results in compiled builds Compiled builds returned wrong answers for isinstance and issubclass on SQLSpec's class hierarchies, and the answer depended on which check ran first in the process. An ordinary type check was enough to break query execution: isinstance(select, CreateIndex) # False, an ordinary check isinstance(select, QueryBuilder) # False -- wrong, poisoned by the above session.execute(select) # AttributeError: 'Select' object has # no attribute 'sql' mypyc builds compiled classes without running ABCMeta.__new__ for each class, so every class in a hierarchy inherited a single shared cache object. The first isinstance or issubclass result was written into that shared cache and returned for every later check against any class in the hierarchy. All 54 abstract classes across builder, core, migrations, storage, and serializer modules shared one cache. Abstract bases now use plain inheritance. abstractmethod is retained, so mypy still reports "Cannot instantiate abstract class" and the type-check gate keeps enforcing implementations. The runtime instantiation guard is unaffected in practice because mypyc had already disabled it: with ABC in place, QueryBuilder() constructed successfully and __abstractmethods__ was absent. Nothing in the codebase relies on virtual subclass registration. StatementResult no longer inherits collections.abc.Iterable, which carries the same metaclass. It defines __iter__, so isinstance(result, Iterable) still succeeds through the structural check. Also corrects a cast in the migration runner that raised TypeError in compiled builds whenever a runner had no configuration attached. cast is a runtime type check under mypyc, and the value is legitimately None. Reproduces on CPython 3.10 through 3.14 and is not fixed upstream. Claude-Session: https://claude.ai/code/session_01CKBtk2CVvc3nNRAx4NJ56K --- sqlspec/builder/_base.py | 4 ++-- sqlspec/core/filters.py | 8 ++++---- sqlspec/core/result/_base.py | 7 ++++--- sqlspec/core/splitter.py | 4 ++-- sqlspec/migrations/base.py | 6 +++--- sqlspec/migrations/loaders.py | 2 +- sqlspec/migrations/runner.py | 6 +++--- sqlspec/storage/backends/base.py | 4 ++-- sqlspec/utils/serializers/_json.py | 4 ++-- 9 files changed, 23 insertions(+), 22 deletions(-) diff --git a/sqlspec/builder/_base.py b/sqlspec/builder/_base.py index 65bee2ab6..b4a711191 100644 --- a/sqlspec/builder/_base.py +++ b/sqlspec/builder/_base.py @@ -4,7 +4,7 @@ """ import re -from abc import ABC, abstractmethod +from abc import abstractmethod from collections.abc import Callable, Iterable, Mapping from typing import Any, NoReturn, cast @@ -115,7 +115,7 @@ def __hash__(self) -> int: return hash((self.sql, frozenset(self.parameters.items()), self.dialect)) -class QueryBuilder(ABC): +class QueryBuilder: """Abstract base class for SQL query builders. Provides common functionality for dialect handling, parameter management, diff --git a/sqlspec/core/filters.py b/sqlspec/core/filters.py index 87c1c5f8e..4560fc9d3 100644 --- a/sqlspec/core/filters.py +++ b/sqlspec/core/filters.py @@ -18,7 +18,7 @@ - Cacheable filter configurations """ -from abc import ABC, abstractmethod +from abc import abstractmethod from collections import abc from datetime import datetime from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeAlias @@ -68,7 +68,7 @@ @mypyc_attr(allow_interpreted_subclasses=True) -class StatementFilter(ABC): +class StatementFilter: """Abstract base class for filters that can be appended to a statement.""" __slots__ = () @@ -334,7 +334,7 @@ def on_or_after(self) -> datetime | None: return self._lower_value -class InAnyFilter(StatementFilter, ABC, Generic[T]): +class InAnyFilter(StatementFilter, Generic[T]): """Base class for collection-based filters that support ANY operations.""" __slots__ = ("_field_name", "_values") @@ -473,7 +473,7 @@ class NotAnyCollectionFilter(InAnyFilter[T]): _parameter_suffix: ClassVar[str] = "not_any" -class PaginationFilter(StatementFilter, ABC): +class PaginationFilter(StatementFilter): """Base class for pagination-related filters.""" __slots__ = () diff --git a/sqlspec/core/result/_base.py b/sqlspec/core/result/_base.py index 2258effcc..f2869ebae 100644 --- a/sqlspec/core/result/_base.py +++ b/sqlspec/core/result/_base.py @@ -9,8 +9,7 @@ ArrowResult: Apache Arrow format results for data interchange. """ -from abc import ABC, abstractmethod -from collections.abc import Iterable, Iterator, Sequence +from abc import abstractmethod from typing import TYPE_CHECKING, Any, Final, Literal, cast, overload from mypy_extensions import mypyc_attr @@ -42,6 +41,8 @@ from sqlspec.utils.schema import to_schema if TYPE_CHECKING: + from collections.abc import Iterator, Sequence + from sqlspec.core.compiler import OperationType from sqlspec.typing import ArrowReturnFormat, ArrowTable, PandasDataFrame, PolarsDataFrame, SchemaT @@ -57,7 +58,7 @@ @mypyc_attr(allow_interpreted_subclasses=False) -class StatementResult(ABC, Iterable[Any]): +class StatementResult: """Abstract base class for SQL statement execution results. Provides a common interface for handling different types of SQL operation diff --git a/sqlspec/core/splitter.py b/sqlspec/core/splitter.py index f6ab6710f..e27868f6f 100644 --- a/sqlspec/core/splitter.py +++ b/sqlspec/core/splitter.py @@ -16,7 +16,7 @@ import logging import re import threading -from abc import ABC, abstractmethod +from abc import abstractmethod from collections.abc import Callable from enum import Enum from re import Pattern @@ -141,7 +141,7 @@ def __repr__(self) -> str: @mypyc_attr(allow_interpreted_subclasses=False) -class DialectConfig(ABC): +class DialectConfig: """Abstract base class for SQL dialect configurations.""" __slots__ = DIALECT_CONFIG_SLOTS diff --git a/sqlspec/migrations/base.py b/sqlspec/migrations/base.py index 5ec5cff76..a6245aef9 100644 --- a/sqlspec/migrations/base.py +++ b/sqlspec/migrations/base.py @@ -1,7 +1,7 @@ """Base classes for SQLSpec migrations.""" import os -from abc import ABC, abstractmethod +from abc import abstractmethod from pathlib import Path from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast @@ -71,7 +71,7 @@ class AppliedMigrationRecord(TypedDict): @mypyc_attr(allow_interpreted_subclasses=True) -class BaseMigrationTracker(ABC, Generic[DriverT]): +class BaseMigrationTracker(Generic[DriverT]): """Base class for migration version tracking.""" __slots__ = ("_output_policy", "version_table", "version_table_name", "version_table_schema") @@ -414,7 +414,7 @@ def _is_autocommit_error(self, exc: Exception) -> bool: @mypyc_attr(allow_interpreted_subclasses=True) -class BaseMigrationCommands(ABC, Generic[ConfigT, DriverT]): +class BaseMigrationCommands(Generic[ConfigT, DriverT]): """Base class for migration commands.""" extension_configs: "dict[str, dict[str, Any]]" diff --git a/sqlspec/migrations/loaders.py b/sqlspec/migrations/loaders.py index fb1a2f326..7e1738f22 100644 --- a/sqlspec/migrations/loaders.py +++ b/sqlspec/migrations/loaders.py @@ -26,7 +26,7 @@ class MigrationLoadError(Exception): """Exception raised when migration loading fails.""" -class BaseMigrationLoader(abc.ABC): +class BaseMigrationLoader: """Abstract base class for migration loaders.""" __slots__ = () diff --git a/sqlspec/migrations/runner.py b/sqlspec/migrations/runner.py index 2e84388c1..b3c9af134 100644 --- a/sqlspec/migrations/runner.py +++ b/sqlspec/migrations/runner.py @@ -5,7 +5,7 @@ import logging import re import time -from abc import ABC, abstractmethod +from abc import abstractmethod from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, cast, overload @@ -57,7 +57,7 @@ def __init__(self, path: Path, extension_name: "str | None") -> None: self.extension_name = extension_name -class BaseMigrationRunner(ABC): +class BaseMigrationRunner: """Base migration runner with common functionality shared between sync and async implementations.""" def __init__( @@ -485,7 +485,7 @@ def should_use_transaction( def _resolve_default_schema(self) -> str | None: """Return the configured default schema for migration execution.""" config = self.context.config if self.context else None - migration_config = cast("dict[str, Any]", getattr(config, "migration_config", None)) + migration_config = cast("dict[str, Any] | None", getattr(config, "migration_config", None)) return _resolve_default_schema(migration_config) def _resolve_use_transaction(self, migration: "LoadedMigrationMetadata", use_transaction: "bool | None") -> bool: diff --git a/sqlspec/storage/backends/base.py b/sqlspec/storage/backends/base.py index c05143723..55b1eb9cd 100644 --- a/sqlspec/storage/backends/base.py +++ b/sqlspec/storage/backends/base.py @@ -4,7 +4,7 @@ import asyncio import builtins import contextlib -from abc import ABC, abstractmethod +from abc import abstractmethod from collections.abc import AsyncIterator, Iterator from typing import TYPE_CHECKING, Any, cast @@ -137,7 +137,7 @@ def __anext__(self) -> Any: @mypyc_attr(allow_interpreted_subclasses=True) -class ObjectStoreBase(ABC): +class ObjectStoreBase: """Base class for storage backends. All synchronous methods follow the *_sync naming convention for consistency diff --git a/sqlspec/utils/serializers/_json.py b/sqlspec/utils/serializers/_json.py index ec174b92d..ce63f1055 100644 --- a/sqlspec/utils/serializers/_json.py +++ b/sqlspec/utils/serializers/_json.py @@ -6,7 +6,7 @@ import json import threading import uuid as uuid_mod -from abc import ABC, abstractmethod +from abc import abstractmethod from collections.abc import Callable, Iterator, Mapping from decimal import Decimal from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network @@ -326,7 +326,7 @@ def decode(self, data: "str | bytes", *, decode_bytes: bool = True) -> Any: ... -class BaseJSONSerializer(ABC): +class BaseJSONSerializer: """Base class shared by JSON serializer implementations.""" __slots__ = ()