Skip to content

Commit 770b778

Browse files
Narrow Requirements property return types to compound
The SQLAlchemy dialect requirements file previously annotated every property as ``-> Any``, which matched the inferred type of ``exclusions.open()`` / ``closed()`` helpers (untyped upstream) but hid the actual contract from IDEs and mypy. Narrow each property's return annotation to the real runtime type ``compound`` imported from ``sqlalchemy.testing.exclusions`` and widen the module-level mypy disable to cover the matching ``no-any-return`` that the narrowing surfaces at each call site. A new static-pin test walks every property on the Requirements class and asserts its return annotation resolves to ``compound`` so future additions can't silently slip back to ``Any``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5ea230d commit 770b778

2 files changed

Lines changed: 67 additions & 27 deletions

File tree

src/sqlalchemydqlite/requirements.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
"""SQLAlchemy test suite requirements for dqlite dialect."""
22

3-
# mypy: disable-error-code="no-untyped-call"
4-
5-
from typing import Any
3+
# The ``exclusions.open()`` / ``closed()`` helpers are genuinely untyped
4+
# at their source in ``sqlalchemy.testing.exclusions``, so mypy reports
5+
# ``no-untyped-call`` (for the invocation) and ``no-any-return`` (for
6+
# the return, since the helper's inferred type is ``Any``) on every
7+
# single call below if we don't silence them. Narrowing each property's
8+
# return annotation to ``compound`` (the actual runtime type) is still
9+
# worthwhile — it gives IDE/hover signal and catches typos where a
10+
# property returns a non-``compound`` object — but the two call-site
11+
# silences have to stay until SA adds annotations upstream.
12+
# mypy: disable-error-code="no-untyped-call, no-any-return"
613

714
from sqlalchemy.testing import exclusions
15+
from sqlalchemy.testing.exclusions import compound
816
from sqlalchemy.testing.requirements import SuiteRequirements
917

1018
__all__ = ["Requirements"]
@@ -17,37 +25,37 @@ class Requirements(SuiteRequirements):
1725
"""
1826

1927
@property
20-
def datetime_literals(self) -> Any:
28+
def datetime_literals(self) -> compound:
2129
"""dqlite/SQLite doesn't have native datetime literals."""
2230
return exclusions.closed()
2331

2432
@property
25-
def time_microseconds(self) -> Any:
33+
def time_microseconds(self) -> compound:
2634
"""SQLite stores time as text without microseconds."""
2735
return exclusions.closed()
2836

2937
@property
30-
def datetime_historic(self) -> Any:
38+
def datetime_historic(self) -> compound:
3139
"""SQLite date range limitation."""
3240
return exclusions.closed()
3341

3442
@property
35-
def unicode_ddl(self) -> Any:
43+
def unicode_ddl(self) -> compound:
3644
"""SQLite supports unicode in DDL."""
3745
return exclusions.open()
3846

3947
@property
40-
def savepoints(self) -> Any:
48+
def savepoints(self) -> compound:
4149
"""dqlite supports savepoints."""
4250
return exclusions.open()
4351

4452
@property
45-
def two_phase_transactions(self) -> Any:
53+
def two_phase_transactions(self) -> compound:
4654
"""dqlite doesn't support two-phase transactions."""
4755
return exclusions.closed()
4856

4957
@property
50-
def temp_table_reflection(self) -> Any:
58+
def temp_table_reflection(self) -> compound:
5159
"""SQLite supports temp table reflection."""
5260
return exclusions.open()
5361

@@ -58,90 +66,90 @@ def temp_table_reflection(self) -> Any:
5866
# suite has a single source of truth to adjust.
5967

6068
@property
61-
def cte(self) -> Any:
69+
def cte(self) -> compound:
6270
"""Common Table Expressions (WITH)."""
6371
return exclusions.open()
6472

6573
@property
66-
def window_functions(self) -> Any:
74+
def window_functions(self) -> compound:
6775
"""SQL window functions (OVER / PARTITION BY)."""
6876
return exclusions.open()
6977

7078
@property
71-
def returning(self) -> Any:
79+
def returning(self) -> compound:
7280
"""RETURNING clause on DML."""
7381
return exclusions.open()
7482

7583
@property
76-
def insert_from_select(self) -> Any:
84+
def insert_from_select(self) -> compound:
7785
"""INSERT INTO ... SELECT."""
7886
return exclusions.open()
7987

8088
@property
81-
def on_update_or_delete_cascades(self) -> Any:
89+
def on_update_or_delete_cascades(self) -> compound:
8290
"""ON UPDATE/DELETE CASCADE foreign-key actions."""
8391
return exclusions.open()
8492

8593
@property
86-
def self_referential_foreign_keys(self) -> Any:
94+
def self_referential_foreign_keys(self) -> compound:
8795
"""Table references itself via foreign key."""
8896
return exclusions.open()
8997

9098
@property
91-
def unique_constraint_reflection(self) -> Any:
99+
def unique_constraint_reflection(self) -> compound:
92100
"""Inspector reports UNIQUE constraints."""
93101
return exclusions.open()
94102

95103
@property
96-
def primary_key_constraint_reflection(self) -> Any:
104+
def primary_key_constraint_reflection(self) -> compound:
97105
"""Inspector reports PRIMARY KEY constraints."""
98106
return exclusions.open()
99107

100108
@property
101-
def foreign_key_constraint_reflection(self) -> Any:
109+
def foreign_key_constraint_reflection(self) -> compound:
102110
"""Inspector reports FOREIGN KEY constraints."""
103111
return exclusions.open()
104112

105113
@property
106-
def index_reflection(self) -> Any:
114+
def index_reflection(self) -> compound:
107115
"""Inspector reports indexes."""
108116
return exclusions.open()
109117

110118
@property
111-
def temporary_tables(self) -> Any:
119+
def temporary_tables(self) -> compound:
112120
"""CREATE TEMPORARY TABLE support (same as SQLite)."""
113121
return exclusions.open()
114122

115123
@property
116-
def table_ddl_if_exists(self) -> Any:
124+
def table_ddl_if_exists(self) -> compound:
117125
"""CREATE TABLE IF NOT EXISTS / DROP TABLE IF EXISTS."""
118126
return exclusions.open()
119127

120128
@property
121-
def sane_rowcount(self) -> Any:
129+
def sane_rowcount(self) -> compound:
122130
"""UPDATE / DELETE rowcount is truthful. dqlite forwards the server's
123131
sqlite3_changes() verbatim via ResultResponse.rows_affected."""
124132
return exclusions.open()
125133

126134
@property
127-
def sane_multi_rowcount(self) -> Any:
135+
def sane_multi_rowcount(self) -> compound:
128136
"""executemany aggregates each iteration's rowcount, so multi-row
129137
UPDATE / DELETE totals match the caller's expectation."""
130138
return exclusions.open()
131139

132140
@property
133-
def emulated_lastrowid(self) -> Any:
141+
def emulated_lastrowid(self) -> compound:
134142
"""lastrowid is SQLite's ROWID, forwarded verbatim via
135143
ResultResponse.last_insert_id."""
136144
return exclusions.open()
137145

138146
@property
139-
def supports_empty_inserts(self) -> Any:
147+
def supports_empty_inserts(self) -> compound:
140148
"""INSERT INTO t DEFAULT VALUES. SQLite supports it; dqlite inherits."""
141149
return exclusions.open()
142150

143151
@property
144-
def regexp_match(self) -> Any:
152+
def regexp_match(self) -> compound:
145153
"""The portable ``col.regexp_match(pattern)`` operator compiles
146154
to ``col REGEXP ?``, which SQLite dispatches to a user-defined
147155
``regexp`` function. pysqlite registers that function via

tests/test_requirements.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,35 @@ def test_regexp_match_is_closed(self) -> None:
5151
"""
5252
req = Requirements()
5353
assert req.regexp_match.enabled is False
54+
55+
56+
class TestRequirementsReturnAnnotations:
57+
"""Static pin: every property on the Requirements class advertises
58+
its actual return type as ``compound``. If a future maintainer adds
59+
a property returning ``Any`` (or a helper that yields a bare bool),
60+
this test catches it before the return-type drift makes it into a
61+
release."""
62+
63+
def test_every_property_annotates_compound_return(self) -> None:
64+
import typing
65+
66+
from sqlalchemy.testing.exclusions import compound
67+
68+
from sqlalchemydqlite.requirements import Requirements
69+
70+
skipped = {"_sa_instance_state"}
71+
missing: list[str] = []
72+
for name in vars(Requirements):
73+
if name.startswith("_") or name in skipped:
74+
continue
75+
attr = vars(Requirements)[name]
76+
if not isinstance(attr, property):
77+
continue
78+
fget = attr.fget
79+
assert fget is not None
80+
hints = typing.get_type_hints(fget)
81+
if hints.get("return") is not compound:
82+
missing.append(name)
83+
assert not missing, (
84+
f"Requirements properties must annotate ``-> compound``; missing on: {sorted(missing)}"
85+
)

0 commit comments

Comments
 (0)