Skip to content

Commit 5d235d8

Browse files
authored
fix: correction for mypyc and dynamic imports (#249)
Correctly detect optional runtime dependencies for compiled code.
1 parent e3dc909 commit 5d235d8

File tree

17 files changed

+525
-388
lines changed

17 files changed

+525
-388
lines changed

docs/guides/performance/mypyc.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,35 @@ def process() -> None:
633633

634634
## SQLSpec-Specific Guidelines
635635

636+
### Avoid module-level optional dependency constants
637+
638+
- **Problem**: Using ``MY_FEATURE_INSTALLED = module_available("pkg")`` at import time lets mypyc fold the value into the compiled extension. If the optional package is missing during compilation but added later, the compiled module still sees ``False`` forever and removes the guarded code paths entirely.
639+
- **Solution**: Use the runtime detector in ``sqlspec.utils.dependencies``. Wrap guards with ``dependency_flag("pkg")`` (boolean-like object) or call ``module_available("pkg")`` inside ``ensure_*`` helpers. These helpers evaluate availability at runtime, so compiled modules observe the actual environment when they execute.
640+
- **Example**:
641+
642+
```python
643+
# BAD (constant folded during compilation)
644+
FSSPEC_INSTALLED = module_available("fsspec")
645+
if FSSPEC_INSTALLED:
646+
...
647+
648+
# GOOD
649+
from sqlspec.utils.dependencies import dependency_flag
650+
651+
FSSPEC_INSTALLED = dependency_flag("fsspec")
652+
if FSSPEC_INSTALLED:
653+
... # evaluated when the code runs, not when it is compiled
654+
655+
# GOOD (inside guards)
656+
from sqlspec.utils.dependencies import module_available
657+
658+
def ensure_fsspec() -> None:
659+
if not module_available("fsspec"):
660+
raise MissingDependencyError(package="fsspec", install_package="fsspec")
661+
```
662+
663+
- **Testing tip**: call ``reset_dependency_cache()`` in tests that manipulate ``sys.path`` to force the detector to re-check availability after installing or removing temporary packages.
664+
636665
### File Caching Optimization
637666

638667
```python

pyproject.toml

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -139,32 +139,34 @@ exclude = ["/.github", "/docs"]
139139
allow-direct-references = true
140140

141141
[tool.hatch.build.targets.wheel]
142-
include = ["NOTICE"]
143142
packages = ["sqlspec"]
144143

145144

146145
[tool.hatch.build.targets.wheel.hooks.mypyc]
147146
dependencies = ["hatch-mypyc", "hatch-cython"]
148147
enable-by-default = false
149148
exclude = [
150-
"tests/**", # Test files
151-
"sqlspec/__main__.py", # Entry point (can't run directly when compiled)
152-
"sqlspec/cli.py", # CLI module (not performance critical)
153-
"sqlspec/typing.py", # Type aliases
154-
"sqlspec/_typing.py", # Type aliases
155-
"sqlspec/config.py", # Main config
156-
"sqlspec/adapters/**/config.py", # Adapter configurations
157-
"sqlspec/adapters/**/_types.py", # Type definitions (mypyc incompatible)
158-
"sqlspec/extensions/**", # All extensions
159-
"sqlspec/**/__init__.py", # Init files (usually just imports)
160-
"sqlspec/protocols.py", # Protocol definitions
161-
"sqlspec/builder/**/*.py", # Builder (not performance critical)
149+
"tests/**", # Test files
150+
"sqlspec/__main__.py", # Entry point (can't run directly when compiled)
151+
"sqlspec/cli.py", # CLI module (not performance critical)
152+
"sqlspec/typing.py", # Type aliases
153+
"sqlspec/_typing.py", # Type aliases
154+
"sqlspec/config.py", # Main config
155+
"sqlspec/adapters/**/config.py", # Adapter configurations
156+
"sqlspec/adapters/**/_types.py", # Type definitions (mypyc incompatible)
157+
"sqlspec/extensions/**", # All extensions
158+
"sqlspec/**/__init__.py", # Init files (usually just imports)
159+
"sqlspec/protocols.py", # Protocol definitions
160+
"sqlspec/builder/**/*.py", # Builder (not performance critical)
161+
"sqlspec/migrations/commands.py", # Migration command CLI (dynamic imports)
162162

163163
]
164164
include = [
165-
"sqlspec/core/**/*.py", # Core module
166-
"sqlspec/loader.py", # Loader module
167-
165+
"sqlspec/core/**/*.py", # Core module
166+
"sqlspec/loader.py", # Loader module
167+
"sqlspec/storage/**/*.py", # Storage layer
168+
"sqlspec/observability/**/*.py", # Observability utilities
169+
"sqlspec/migrations/**/*.py", # Migrations module
168170
# === ADAPTER TYPE CONVERTERS ===
169171
"sqlspec/adapters/adbc/type_converter.py", # ADBC type converter
170172
"sqlspec/adapters/bigquery/type_converter.py", # BigQuery type converter
@@ -178,22 +180,13 @@ include = [
178180
"sqlspec/utils/type_guards.py", # Type guard utilities
179181
"sqlspec/utils/fixtures.py", # File fixture loading
180182
"sqlspec/utils/data_transformation.py", # Data transformation utilities
183+
"sqlspec/utils/arrow_helpers.py", # Arrow result helpers
184+
"sqlspec/utils/serializers.py", # Serialization helpers
185+
"sqlspec/utils/type_converters.py", # Adapter type converters
186+
"sqlspec/utils/correlation.py", # Correlation context helpers
187+
"sqlspec/utils/portal.py", # Thread portal utilities
188+
"sqlspec/utils/singleton.py", # Lightweight singleton helpers
181189

182-
# === OBSERVABILITY ===
183-
"sqlspec/observability/_config.py",
184-
"sqlspec/observability/_diagnostics.py",
185-
"sqlspec/observability/_dispatcher.py",
186-
"sqlspec/observability/_observer.py",
187-
"sqlspec/observability/_runtime.py",
188-
"sqlspec/observability/_spans.py",
189-
190-
# === STORAGE LAYER ===
191-
# "sqlspec/storage/_utils.py",
192-
# "sqlspec/storage/registry.py",
193-
# "sqlspec/storage/backends/base.py",
194-
# "sqlspec/storage/backends/obstore.py",
195-
# "sqlspec/storage/backends/fsspec.py",
196-
# "sqlspec/storage/backends/local.py",
197190
]
198191
mypy-args = [
199192
"--ignore-missing-imports",

sqlspec/_typing.py

Lines changed: 20 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,11 @@
55
from collections.abc import Iterable, Mapping
66
from dataclasses import dataclass
77
from enum import Enum
8-
from importlib.util import find_spec
98
from typing import Any, ClassVar, Final, Literal, Protocol, cast, runtime_checkable
109

1110
from typing_extensions import TypeVar, dataclass_transform
1211

13-
14-
def module_available(module_name: str) -> bool:
15-
"""Return True if the given module spec can be resolved.
16-
17-
Args:
18-
module_name: Dotted path for the module to locate.
19-
20-
Returns:
21-
True if the module can be resolved, False otherwise.
22-
"""
23-
24-
try:
25-
return find_spec(module_name) is not None
26-
except ModuleNotFoundError:
27-
return False
12+
from sqlspec.utils.dependencies import dependency_flag, module_available
2813

2914

3015
@runtime_checkable
@@ -131,12 +116,10 @@ class FailFastStub:
131116
BaseModel = _RealBaseModel
132117
TypeAdapter = _RealTypeAdapter
133118
FailFast = _RealFailFast
134-
PYDANTIC_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
135119
except ImportError:
136120
BaseModel = BaseModelStub # type: ignore[assignment,misc]
137121
TypeAdapter = TypeAdapterStub # type: ignore[assignment,misc]
138122
FailFast = FailFastStub # type: ignore[assignment,misc]
139-
PYDANTIC_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
140123

141124
# Always define stub types for msgspec
142125

@@ -184,21 +167,17 @@ class UnsetTypeStub(enum.Enum):
184167
UnsetType = _RealUnsetType
185168
UNSET = _REAL_UNSET
186169
convert = _real_convert
187-
MSGSPEC_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
188170
except ImportError:
189171
Struct = StructStub # type: ignore[assignment,misc]
190172
UnsetType = UnsetTypeStub # type: ignore[assignment,misc]
191173
UNSET = UNSET_STUB # type: ignore[assignment] # pyright: ignore[reportConstantRedefinition]
192174
convert = convert_stub
193-
MSGSPEC_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
194175

195176

196177
try:
197178
import orjson # noqa: F401
198-
199-
ORJSON_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
200179
except ImportError:
201-
ORJSON_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
180+
orjson = None # type: ignore[assignment]
202181

203182

204183
# Always define stub type for DTOData
@@ -228,10 +207,8 @@ def as_builtins(self) -> Any:
228207
from litestar.dto.data_structures import DTOData as _RealDTOData # pyright: ignore[reportUnknownVariableType]
229208

230209
DTOData = _RealDTOData
231-
LITESTAR_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
232210
except ImportError:
233211
DTOData = DTODataStub # type: ignore[assignment,misc]
234-
LITESTAR_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
235212

236213

237214
# Always define stub types for attrs
@@ -290,21 +267,17 @@ def attrs_has_stub(*args: Any, **kwargs: Any) -> bool: # noqa: ARG001
290267
attrs_field = _real_attrs_field
291268
attrs_fields = _real_attrs_fields
292269
attrs_has = _real_attrs_has
293-
ATTRS_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
294270
except ImportError:
295271
AttrsInstance = AttrsInstanceStub # type: ignore[misc]
296272
attrs_asdict = attrs_asdict_stub
297273
attrs_define = attrs_define_stub
298274
attrs_field = attrs_field_stub
299275
attrs_fields = attrs_fields_stub
300276
attrs_has = attrs_has_stub # type: ignore[assignment]
301-
ATTRS_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
302277

303278
try:
304279
from cattrs import structure as cattrs_structure
305280
from cattrs import unstructure as cattrs_unstructure
306-
307-
CATTRS_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
308281
except ImportError:
309282

310283
def cattrs_unstructure(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001
@@ -315,8 +288,6 @@ def cattrs_structure(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001
315288
"""Placeholder implementation"""
316289
return {}
317290

318-
CATTRS_INSTALLED = False # pyright: ignore[reportConstantRedefinition] # pyright: ignore[reportConstantRedefinition]
319-
320291

321292
class EmptyEnum(Enum):
322293
"""A sentinel enum used as placeholder."""
@@ -433,16 +404,12 @@ def __iter__(self) -> "Iterable[Any]":
433404
from pyarrow import RecordBatchReader as ArrowRecordBatchReader
434405
from pyarrow import Schema as ArrowSchema
435406
from pyarrow import Table as ArrowTable
436-
437-
PYARROW_INSTALLED = True
438407
except ImportError:
439408
ArrowTable = ArrowTableResult # type: ignore[assignment,misc]
440409
ArrowRecordBatch = ArrowRecordBatchResult # type: ignore[assignment,misc]
441410
ArrowSchema = ArrowSchemaProtocol # type: ignore[assignment,misc]
442411
ArrowRecordBatchReader = ArrowRecordBatchReaderProtocol # type: ignore[assignment,misc]
443412

444-
PYARROW_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
445-
446413

447414
@runtime_checkable
448415
class PandasDataFrameProtocol(Protocol):
@@ -472,20 +439,15 @@ def __getitem__(self, key: Any) -> Any:
472439

473440
try:
474441
from pandas import DataFrame as PandasDataFrame
475-
476-
PANDAS_INSTALLED = True
477442
except ImportError:
478443
PandasDataFrame = PandasDataFrameProtocol # type: ignore[assignment,misc]
479-
PANDAS_INSTALLED = False
480444

481445

482446
try:
483447
from polars import DataFrame as PolarsDataFrame
484448

485-
POLARS_INSTALLED = True
486449
except ImportError:
487450
PolarsDataFrame = PolarsDataFrameProtocol # type: ignore[assignment,misc]
488-
POLARS_INSTALLED = False
489451

490452

491453
@runtime_checkable
@@ -514,8 +476,6 @@ def tolist(self) -> "list[Any]":
514476
StatusCode,
515477
Tracer, # pyright: ignore[reportMissingImports, reportAssignmentType]
516478
)
517-
518-
OPENTELEMETRY_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
519479
except ImportError:
520480
# Define shims for when opentelemetry is not installed
521481

@@ -578,7 +538,6 @@ def get_tracer_provider(self) -> Any: # pragma: no cover
578538
trace = _TraceModule() # type: ignore[assignment]
579539
StatusCode = trace.StatusCode # type: ignore[misc]
580540
Status = trace.Status # type: ignore[misc]
581-
OPENTELEMETRY_INSTALLED = False # pyright: ignore[reportConstantRedefinition] # pyright: ignore[reportConstantRedefinition]
582541

583542

584543
try:
@@ -587,8 +546,6 @@ def get_tracer_provider(self) -> Any: # pragma: no cover
587546
Gauge, # pyright: ignore[reportAssignmentType]
588547
Histogram, # pyright: ignore[reportAssignmentType]
589548
)
590-
591-
PROMETHEUS_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
592549
except ImportError:
593550
# Define shims for when prometheus_client is not installed
594551

@@ -636,8 +593,6 @@ class Histogram(_Metric): # type: ignore[no-redef]
636593
def labels(self, *labelvalues: str, **labelkwargs: str) -> _MetricInstance:
637594
return _MetricInstance() # pragma: no cover
638595

639-
PROMETHEUS_INSTALLED = False # pyright: ignore[reportConstantRedefinition] # pyright: ignore[reportConstantRedefinition]
640-
641596

642597
try:
643598
import aiosql # pyright: ignore[reportMissingImports, reportAssignmentType]
@@ -651,8 +606,6 @@ def labels(self, *labelvalues: str, **labelkwargs: str) -> _MetricInstance:
651606
from aiosql.types import ( # pyright: ignore[reportMissingImports, reportAssignmentType]
652607
SyncDriverAdapterProtocol as AiosqlSyncProtocol, # pyright: ignore[reportMissingImports, reportAssignmentType]
653608
)
654-
655-
AIOSQL_INSTALLED = True # pyright: ignore[reportConstantRedefinition]
656609
except ImportError:
657610
# Define shims for when aiosql is not installed
658611

@@ -723,16 +676,25 @@ async def insert_update_delete(self, conn: Any, query_name: str, sql: str, param
723676
async def insert_update_delete_many(self, conn: Any, query_name: str, sql: str, parameters: Any) -> None: ...
724677
async def insert_returning(self, conn: Any, query_name: str, sql: str, parameters: Any) -> "Any | None": ...
725678

726-
AIOSQL_INSTALLED = False # pyright: ignore[reportConstantRedefinition] # pyright: ignore[reportConstantRedefinition]
727-
728-
729-
FSSPEC_INSTALLED = module_available("fsspec")
730-
NUMPY_INSTALLED = module_available("numpy")
731-
OBSTORE_INSTALLED = module_available("obstore")
732-
PGVECTOR_INSTALLED = module_available("pgvector")
733679

734-
CLOUD_SQL_CONNECTOR_INSTALLED = module_available("google.cloud.sql.connector")
735-
ALLOYDB_CONNECTOR_INSTALLED = module_available("google.cloud.alloydb.connector")
680+
AIOSQL_INSTALLED = dependency_flag("aiosql")
681+
ATTRS_INSTALLED = dependency_flag("attrs")
682+
CATTRS_INSTALLED = dependency_flag("cattrs")
683+
CLOUD_SQL_CONNECTOR_INSTALLED = dependency_flag("google.cloud.sql.connector")
684+
FSSPEC_INSTALLED = dependency_flag("fsspec")
685+
LITESTAR_INSTALLED = dependency_flag("litestar")
686+
MSGSPEC_INSTALLED = dependency_flag("msgspec")
687+
NUMPY_INSTALLED = dependency_flag("numpy")
688+
OBSTORE_INSTALLED = dependency_flag("obstore")
689+
OPENTELEMETRY_INSTALLED = dependency_flag("opentelemetry")
690+
ORJSON_INSTALLED = dependency_flag("orjson")
691+
PANDAS_INSTALLED = dependency_flag("pandas")
692+
PGVECTOR_INSTALLED = dependency_flag("pgvector")
693+
POLARS_INSTALLED = dependency_flag("polars")
694+
PROMETHEUS_INSTALLED = dependency_flag("prometheus_client")
695+
PYARROW_INSTALLED = dependency_flag("pyarrow")
696+
PYDANTIC_INSTALLED = dependency_flag("pydantic")
697+
ALLOYDB_CONNECTOR_INSTALLED = dependency_flag("google.cloud.alloydb.connector")
736698

737699
__all__ = (
738700
"AIOSQL_INSTALLED",

0 commit comments

Comments
 (0)