Skip to content

Commit ea7c102

Browse files
Make tests/ mypy-clean under strict mode with a tests-only override
Configure mypy to also scan tests/ alongside src/ once explicit_package_bases and mypy_path are set so the duplicate-conftest path collision is resolved. A new tests.* override disables four error codes that flag legitimate pytest patterns (method-assign for monkey-patching, no-untyped-def/no-untyped-call for fixture-style helpers, comparison-overlap for IntEnum-equals-int reference pins) — production code keeps strict checks. Concrete fixes in tests/: - Add per-line type-ignore comments for legitimate test patterns (negative-test types, fake conn classes typed as DqliteConnection, monkey-patches that mypy can't follow through dynamic attribute assignment). - Drop redundant per-line type-ignore comments now that the override covers the corresponding error codes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4a80ca5 commit ea7c102

44 files changed

Lines changed: 167 additions & 140 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,33 @@ asyncio_default_fixture_loop_scope = "function"
5656
[tool.mypy]
5757
strict = true
5858
python_version = "3.13"
59+
# Tests live under ``tests/`` with no ``__init__.py``; mypy needs
60+
# ``explicit_package_bases`` (or namespace packages) to avoid the
61+
# duplicate-conftest module-name collision when scanning sibling
62+
# integration/ subdirectories. ``mypy_path`` mirrors the
63+
# ``pythonpath = ["src"]`` declared for pytest so the source files
64+
# resolve under the package name (``dqlitedbapi``), not the layout
65+
# path (``src.dqlitedbapi``).
66+
explicit_package_bases = true
67+
mypy_path = "src"
68+
69+
# Tests use a few patterns that mypy's strict mode flags but are
70+
# correct at runtime. Disabling these codes only inside ``tests.*``
71+
# keeps strict typing on production code while letting the test suite
72+
# use idiomatic patterns without per-line ``# type: ignore`` noise:
73+
#
74+
# * ``method-assign`` — monkey-patching bound methods
75+
# (``conn.execute = fake_execute``) is the standard way to inject a
76+
# fake without subclassing.
77+
# * ``no-untyped-def`` / ``no-untyped-call`` — pytest fixtures and
78+
# small helpers are conventionally written without type annotations.
79+
# * ``comparison-overlap`` — ``assert SomeIntEnum.FOO == 0`` is the
80+
# canonical "this enum matches its int value" pin against a
81+
# reference table; mypy's literal-type narrowing incorrectly flags
82+
# it as non-overlapping even though IntEnum's ``__eq__`` works.
83+
[[tool.mypy.overrides]]
84+
module = "tests.*"
85+
disable_error_code = ["method-assign", "no-untyped-def", "no-untyped-call", "comparison-overlap"]
5986

6087
[tool.ruff]
6188
target-version = "py313"

tests/aio/test_messages_clear_on_closed.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
@pytest.mark.asyncio
2020
async def test_async_commit_clears_messages_when_closed() -> None:
2121
conn = AsyncConnection("localhost:9001")
22-
conn.messages.append(("sentinel", Warning("noop")))
22+
conn.messages.append(("sentinel", Warning("noop"))) # type: ignore[arg-type]
2323
conn._closed = True
2424

2525
with pytest.raises(InterfaceError):
@@ -31,7 +31,7 @@ async def test_async_commit_clears_messages_when_closed() -> None:
3131
@pytest.mark.asyncio
3232
async def test_async_rollback_clears_messages_when_closed() -> None:
3333
conn = AsyncConnection("localhost:9001")
34-
conn.messages.append(("sentinel", Warning("noop")))
34+
conn.messages.append(("sentinel", Warning("noop"))) # type: ignore[arg-type]
3535
conn._closed = True
3636

3737
with pytest.raises(InterfaceError):
@@ -43,7 +43,7 @@ async def test_async_rollback_clears_messages_when_closed() -> None:
4343
@pytest.mark.asyncio
4444
async def test_async_commit_clears_messages_when_never_connected() -> None:
4545
conn = AsyncConnection("localhost:9001")
46-
conn.messages.append(("sentinel", Warning("noop")))
46+
conn.messages.append(("sentinel", Warning("noop"))) # type: ignore[arg-type]
4747
# _async_conn stays None until first use — commit returns silently
4848
# in this branch, but the messages clear must still happen.
4949
assert conn._async_conn is None
@@ -54,7 +54,7 @@ async def test_async_commit_clears_messages_when_never_connected() -> None:
5454
@pytest.mark.asyncio
5555
async def test_async_rollback_clears_messages_when_never_connected() -> None:
5656
conn = AsyncConnection("localhost:9001")
57-
conn.messages.append(("sentinel", Warning("noop")))
57+
conn.messages.append(("sentinel", Warning("noop"))) # type: ignore[arg-type]
5858
assert conn._async_conn is None
5959
await conn.rollback()
6060
assert list(conn.messages) == []

tests/integration/test_datetime_conversion.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_naive_datetime_stays_naive(self, cluster_address: str) -> None:
3535
cursor.execute("DELETE FROM dt_naive")
3636
cursor.execute("INSERT INTO dt_naive (ts) VALUES (?)", [dt])
3737
cursor.execute("SELECT ts FROM dt_naive")
38-
(value,) = cursor.fetchone()
38+
(value,) = cursor.fetchone() # type: ignore[misc]
3939
assert isinstance(value, datetime.datetime)
4040
assert value.tzinfo is None, f"expected naive, got tzinfo={value.tzinfo}"
4141
assert value == dt
@@ -53,7 +53,7 @@ def test_aware_datetime_preserves_offset(self, cluster_address: str) -> None:
5353
cursor.execute("DELETE FROM dt_aware")
5454
cursor.execute("INSERT INTO dt_aware (ts) VALUES (?)", [dt])
5555
cursor.execute("SELECT ts FROM dt_aware")
56-
(value,) = cursor.fetchone()
56+
(value,) = cursor.fetchone() # type: ignore[misc]
5757
assert isinstance(value, datetime.datetime)
5858
assert value == dt
5959
assert value.utcoffset() == datetime.timedelta(hours=5, minutes=30)
@@ -68,7 +68,7 @@ def test_microseconds_preserved(self, cluster_address: str) -> None:
6868
cursor.execute("DELETE FROM dt_us")
6969
cursor.execute("INSERT INTO dt_us (ts) VALUES (?)", [dt])
7070
cursor.execute("SELECT ts FROM dt_us")
71-
(value,) = cursor.fetchone()
71+
(value,) = cursor.fetchone() # type: ignore[misc]
7272
assert value.microsecond == 123456
7373
cursor.execute("DROP TABLE dt_us")
7474

@@ -82,7 +82,7 @@ def test_null_datetime_returns_none(self, cluster_address: str) -> None:
8282
cursor.execute("DELETE FROM dt_null")
8383
cursor.execute("INSERT INTO dt_null (ts) VALUES (NULL)")
8484
cursor.execute("SELECT ts FROM dt_null")
85-
(value,) = cursor.fetchone()
85+
(value,) = cursor.fetchone() # type: ignore[misc]
8686
assert value is None
8787
cursor.execute("DROP TABLE dt_null")
8888

@@ -101,7 +101,7 @@ def test_date_bind_param(self, cluster_address: str) -> None:
101101
cursor.execute("DELETE FROM dt_date")
102102
cursor.execute("INSERT INTO dt_date (d) VALUES (?)", [d])
103103
cursor.execute("SELECT d FROM dt_date")
104-
(value,) = cursor.fetchone()
104+
(value,) = cursor.fetchone() # type: ignore[misc]
105105
assert isinstance(value, datetime.datetime)
106106
assert value.date() == d
107107
cursor.execute("DROP TABLE dt_date")
@@ -125,7 +125,7 @@ def test_naive_time_round_trips_as_string(self, cluster_address: str) -> None:
125125
cursor.execute("DELETE FROM t_naive")
126126
cursor.execute("INSERT INTO t_naive (v) VALUES (?)", [t])
127127
cursor.execute("SELECT v FROM t_naive")
128-
(value,) = cursor.fetchone()
128+
(value,) = cursor.fetchone() # type: ignore[misc]
129129
assert value == "12:30:45"
130130
cursor.execute("DROP TABLE t_naive")
131131

@@ -137,7 +137,7 @@ def test_time_with_microseconds_and_utc(self, cluster_address: str) -> None:
137137
cursor.execute("DELETE FROM t_us")
138138
cursor.execute("INSERT INTO t_us (v) VALUES (?)", [t])
139139
cursor.execute("SELECT v FROM t_us")
140-
(value,) = cursor.fetchone()
140+
(value,) = cursor.fetchone() # type: ignore[misc]
141141
assert value == "12:30:45.123456+00:00"
142142
cursor.execute("DROP TABLE t_us")
143143

@@ -166,7 +166,7 @@ def test_integer_value_in_datetime_column_decodes_as_datetime(
166166
# tags the column as DQLITE_UNIXTIME on readback.
167167
cursor.execute("INSERT INTO ut_test (ts) VALUES (?)", [epoch])
168168
cursor.execute("SELECT ts FROM ut_test")
169-
(value,) = cursor.fetchone()
169+
(value,) = cursor.fetchone() # type: ignore[misc]
170170
assert isinstance(value, datetime.datetime)
171171
assert value == expected
172172
cursor.execute("DROP TABLE ut_test")
@@ -214,7 +214,7 @@ async def scenario() -> datetime.datetime:
214214
row = await cursor.fetchone()
215215
await cursor.execute("DROP TABLE async_dt")
216216
assert row is not None
217-
return row[0]
217+
return row[0] # type: ignore[no-any-return]
218218

219219
value = asyncio.run(scenario())
220220
assert isinstance(value, datetime.datetime)

tests/integration/test_lastrowid_returning.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def test_lastrowid_not_updated_by_insert_returning(self, cluster_address: str) -
3131
# DDL surfaces last_insert_id=0 on its Exec response.
3232
lastrowid_after_ddl = cursor.lastrowid
3333
cursor.execute("INSERT INTO lri_test (v) VALUES (?) RETURNING id", (42,))
34-
returned_id = cursor.fetchone()[0]
34+
returned_id = cursor.fetchone()[0] # type: ignore[index]
3535
# The only authoritative source of the rowid is the returned row.
3636
assert returned_id >= 1
3737
# lastrowid is NOT updated by the RETURNING path — it still

tests/integration/test_misc_coverage.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test_multi_megabyte_blob(self, cluster_address: str) -> None:
9898
c.execute("INSERT INTO big_blob (data) VALUES (?)", [payload])
9999
conn.commit()
100100
c.execute("SELECT data FROM big_blob")
101-
(value,) = c.fetchone()
101+
(value,) = c.fetchone() # type: ignore[misc]
102102
assert value == payload
103103
c.execute("DROP TABLE big_blob")
104104

@@ -119,7 +119,7 @@ def test_binary_constructor_blob_round_trip(self, cluster_address: str) -> None:
119119
c.execute("INSERT INTO bin_ctor (data) VALUES (?)", [dqlitedbapi.Binary(payload)])
120120
conn.commit()
121121
c.execute("SELECT data FROM bin_ctor")
122-
(value,) = c.fetchone()
122+
(value,) = c.fetchone() # type: ignore[misc]
123123
assert value == payload
124124
c.execute("DROP TABLE bin_ctor")
125125

@@ -134,7 +134,7 @@ def test_unicode_identifier_and_emoji_value(self, cluster_address: str) -> None:
134134
c.execute('INSERT INTO "café" ("☕") VALUES (?)', ["hello 🚀 world"])
135135
conn.commit()
136136
c.execute('SELECT "☕" FROM "café"')
137-
(value,) = c.fetchone()
137+
(value,) = c.fetchone() # type: ignore[misc]
138138
assert value == "hello 🚀 world"
139139
assert c.description is not None
140140
assert c.description[0][0] == "☕"
@@ -151,7 +151,7 @@ def test_non_bmp_codepoint_round_trip(self, cluster_address: str) -> None:
151151
c.execute("INSERT INTO nbmp (s) VALUES (?)", [payload])
152152
conn.commit()
153153
c.execute("SELECT s FROM nbmp")
154-
(value,) = c.fetchone()
154+
(value,) = c.fetchone() # type: ignore[misc]
155155
assert value == payload
156156
c.execute("DROP TABLE nbmp")
157157

tests/test_aexit_rollback_debug_log.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ def _connection_with_failing_rollback(rollback_exc: BaseException) -> AsyncConne
3333
conn._op_lock = None
3434
conn._loop_ref = None
3535
conn.messages = []
36-
conn.commit = AsyncMock() # type: ignore[method-assign]
37-
conn.rollback = AsyncMock(side_effect=rollback_exc) # type: ignore[method-assign]
38-
conn.close = AsyncMock() # type: ignore[method-assign]
36+
conn.commit = AsyncMock()
37+
conn.rollback = AsyncMock(side_effect=rollback_exc)
38+
conn.close = AsyncMock()
3939
return conn
4040

4141

tests/test_aio_commit_rollback_no_tx.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,23 @@ class TestAsyncCommitNoTxSwallow:
2929
async def test_commit_swallows_no_transaction_error(self) -> None:
3030
conn = _prime()
3131
assert conn._async_conn is not None
32-
conn._async_conn.execute.side_effect = _client_exc.OperationalError(
32+
conn._async_conn.execute.side_effect = _client_exc.OperationalError( # type: ignore[attr-defined]
3333
1, "cannot commit - no transaction is active"
3434
)
3535
await conn.commit() # silent no-op
3636

3737
async def test_rollback_swallows_no_transaction_error(self) -> None:
3838
conn = _prime()
3939
assert conn._async_conn is not None
40-
conn._async_conn.execute.side_effect = _client_exc.OperationalError(
40+
conn._async_conn.execute.side_effect = _client_exc.OperationalError( # type: ignore[attr-defined]
4141
1, "cannot rollback - no transaction is active"
4242
)
4343
await conn.rollback() # silent no-op
4444

4545
async def test_commit_re_raises_other_operational_errors(self) -> None:
4646
conn = _prime()
4747
assert conn._async_conn is not None
48-
conn._async_conn.execute.side_effect = _client_exc.OperationalError(
48+
conn._async_conn.execute.side_effect = _client_exc.OperationalError( # type: ignore[attr-defined]
4949
10, "some unrelated error"
5050
)
5151
# The client-layer OperationalError is wrapped into the PEP 249
@@ -59,7 +59,7 @@ async def test_commit_re_raises_other_operational_errors(self) -> None:
5959
async def test_rollback_re_raises_other_operational_errors(self) -> None:
6060
conn = _prime()
6161
assert conn._async_conn is not None
62-
conn._async_conn.execute.side_effect = _client_exc.OperationalError(
62+
conn._async_conn.execute.side_effect = _client_exc.OperationalError( # type: ignore[attr-defined]
6363
10, "some unrelated error"
6464
)
6565
with pytest.raises(_dbapi_exc.OperationalError, match="some unrelated error"):

tests/test_aio_commit_rollback_wrapping.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ async def test_commit_wraps_client_exceptions(
8080
) -> None:
8181
conn = _prime()
8282
assert conn._async_conn is not None
83-
conn._async_conn.execute.side_effect = raise_exc
83+
conn._async_conn.execute.side_effect = raise_exc # type: ignore[attr-defined]
8484

8585
with pytest.raises(expect_cls) as exc_info:
8686
await conn.commit()
@@ -101,7 +101,7 @@ async def test_commit_integrity_error_carries_code(self) -> None:
101101
"""
102102
conn = _prime()
103103
assert conn._async_conn is not None
104-
conn._async_conn.execute.side_effect = _client_exc.OperationalError(
104+
conn._async_conn.execute.side_effect = _client_exc.OperationalError( # type: ignore[attr-defined]
105105
19, "UNIQUE constraint failed"
106106
)
107107
with pytest.raises(_dbapi_exc.IntegrityError) as exc_info:
@@ -116,7 +116,7 @@ async def test_rollback_wraps_client_exceptions(
116116
) -> None:
117117
conn = _prime()
118118
assert conn._async_conn is not None
119-
conn._async_conn.execute.side_effect = raise_exc
119+
conn._async_conn.execute.side_effect = raise_exc # type: ignore[attr-defined]
120120

121121
with pytest.raises(expect_cls) as exc_info:
122122
await conn.rollback()
@@ -135,15 +135,15 @@ class TestAsyncNoTxSwallowSurvivesWrapping:
135135
async def test_commit_no_tx_still_silent(self) -> None:
136136
conn = _prime()
137137
assert conn._async_conn is not None
138-
conn._async_conn.execute.side_effect = _client_exc.OperationalError(
138+
conn._async_conn.execute.side_effect = _client_exc.OperationalError( # type: ignore[attr-defined]
139139
1, "cannot commit - no transaction is active"
140140
)
141141
await conn.commit() # silent no-op
142142

143143
async def test_rollback_no_tx_still_silent(self) -> None:
144144
conn = _prime()
145145
assert conn._async_conn is not None
146-
conn._async_conn.execute.side_effect = _client_exc.OperationalError(
146+
conn._async_conn.execute.side_effect = _client_exc.OperationalError( # type: ignore[attr-defined]
147147
1, "cannot rollback - no transaction is active"
148148
)
149149
await conn.rollback() # silent no-op

tests/test_arraysize_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def test_float_rejected_sync(self) -> None:
6666
def test_bool_rejected_sync(self) -> None:
6767
c = self._sync_cursor()
6868
with pytest.raises(ProgrammingError, match="bool"):
69-
c.arraysize = True # type: ignore[assignment]
69+
c.arraysize = True
7070

7171
def test_none_rejected_async(self) -> None:
7272
c = self._async_cursor()
@@ -86,4 +86,4 @@ def test_float_rejected_async(self) -> None:
8686
def test_bool_rejected_async(self) -> None:
8787
c = self._async_cursor()
8888
with pytest.raises(ProgrammingError, match="bool"):
89-
c.arraysize = True # type: ignore[assignment]
89+
c.arraysize = True

tests/test_async_close_race.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async def slow_execute(_sql: str, _params: object) -> tuple[int, int]:
2828
order.append("execute:end")
2929
return (0, 0)
3030

31-
async def fake_query_raw_typed(_sql: str, _params: object) -> tuple[list, list, list, list]:
31+
async def fake_query_raw_typed(_sql: str, _params: object) -> tuple[list, list, list, list]: # type: ignore[type-arg]
3232
return ([], [], [], [])
3333

3434
with patch("dqlitedbapi.connection.DqliteConnection") as MockDqliteConn:
@@ -100,7 +100,7 @@ async def slow_close(*args: object, **kwargs: object) -> None:
100100
await close_release.wait()
101101
await real_close(*args, **kwargs)
102102

103-
inner.close = slow_close # type: ignore[assignment]
103+
inner.close = slow_close
104104

105105
close_task = asyncio.create_task(conn.close())
106106
# Yield so close() acquires op_lock.

0 commit comments

Comments
 (0)