Skip to content

Commit 03dfc2a

Browse files
committed
test(typing): address review comments on PR
1 parent d8a1383 commit 03dfc2a

1 file changed

Lines changed: 114 additions & 136 deletions

File tree

tests/test_typing.py

Lines changed: 114 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -46,36 +46,111 @@ def _run_mypy(code: str) -> tuple[list[str], list[str]]:
4646
return notes, errors
4747

4848

49+
_POSITIVE_SNIPPET = """
50+
import asyncio
51+
from datetime import timedelta
52+
from typing import Optional
53+
54+
from cachier import cachier
55+
56+
# sync_return_type: decorated function's return type preserved
57+
@cachier()
58+
def sync_return(x: int) -> str:
59+
return str(x)
60+
61+
reveal_type(sync_return(5))
62+
reveal_type(sync_return)
63+
64+
# sync_params: multi-arg signature preserved
65+
@cachier()
66+
def sync_params(x: int, y: str) -> list[str]:
67+
return [y] * x
68+
69+
reveal_type(sync_params)
70+
71+
# async_return: awaited return type preserved
72+
@cachier()
73+
async def async_fetch(url: str) -> bytes:
74+
return b"data"
75+
76+
async def _async_caller() -> None:
77+
result = await async_fetch("http://example.com")
78+
reveal_type(result)
79+
80+
reveal_type(async_fetch)
81+
82+
# optional_params: Optional preserved
83+
@cachier()
84+
def optional_greet(name: str, greeting: Optional[str] = None) -> str:
85+
return f"{greeting or 'Hello'}, {name}"
86+
87+
reveal_type(optional_greet)
88+
89+
# generic_return: parametrized dict preserved
90+
@cachier()
91+
def make_mapping(keys: list[str], value: int) -> dict[str, int]:
92+
return {k: value for k in keys}
93+
94+
reveal_type(make_mapping(["a"], 1))
95+
96+
# none_return: None return preserved
97+
@cachier()
98+
def side_effect(x: int) -> None:
99+
return None
100+
101+
reveal_type(side_effect(1))
102+
103+
# decorator_args: typing works with explicit backend
104+
@cachier(backend="memory")
105+
def compute(x: float) -> float:
106+
return x * 2.0
107+
108+
reveal_type(compute(1.0))
109+
110+
# decorator_args: typing works with stale_after
111+
@cachier(stale_after=timedelta(hours=1))
112+
def lookup(key: str) -> list[int]:
113+
return [1, 2, 3]
114+
115+
reveal_type(lookup("x"))
116+
117+
# attributes: cache-management API visible
118+
@cachier()
119+
def attr_fn(x: int) -> int:
120+
return x
121+
122+
attr_fn.clear_cache()
123+
attr_fn.precache_value(1, value_to_cache=42)
124+
_m = attr_fn.metrics
125+
"""
126+
127+
128+
@pytest.fixture(scope="module")
129+
def positive_mypy_output() -> tuple[list[str], list[str]]:
130+
"""Run mypy once on a combined snippet of all positive typing assertions.
131+
132+
Consolidating the positive cases into a single mypy invocation avoids per-test mypy startup cost. Negative tests
133+
(which assert that errors are reported) stay isolated so each one can verify the error originates from its own
134+
snippet.
135+
136+
"""
137+
return _run_mypy(_POSITIVE_SNIPPET)
138+
139+
49140
class TestSyncTyping:
50141
"""Verify that synchronous decorated functions preserve types."""
51142

52-
def test_return_type_preserved(self) -> None:
143+
def test_return_type_preserved(self, positive_mypy_output) -> None:
53144
"""Mypy should infer the original return type through @cachier."""
54-
notes, errors = _run_mypy("""
55-
from cachier import cachier
56-
57-
@cachier()
58-
def my_func(x: int) -> str:
59-
return str(x)
60-
61-
reveal_type(my_func(5))
62-
""")
145+
notes, errors = positive_mypy_output
63146
assert not errors
64147
assert any('"str"' in n for n in notes)
65148

66-
def test_param_types_preserved(self) -> None:
149+
def test_param_types_preserved(self, positive_mypy_output) -> None:
67150
"""Mypy should see the original parameter types through @cachier."""
68-
notes, errors = _run_mypy("""
69-
from cachier import cachier
70-
71-
@cachier()
72-
def my_func(x: int, y: str) -> list[str]:
73-
return [y] * x
74-
75-
reveal_type(my_func)
76-
""")
151+
notes, errors = positive_mypy_output
77152
assert not errors
78-
assert any("int" in n and "str" in n for n in notes)
153+
assert any("[x: int, y: str]" in n and "list[str]" in n for n in notes)
79154

80155
def test_wrong_arg_type_is_error(self) -> None:
81156
"""Mypy should reject calls with wrong argument types."""
@@ -107,36 +182,15 @@ def get_name() -> str:
107182
class TestAsyncTyping:
108183
"""Verify that async decorated functions preserve types."""
109184

110-
def test_async_return_type_preserved(self) -> None:
185+
def test_async_return_type_preserved(self, positive_mypy_output) -> None:
111186
"""Mypy should infer the awaited return type for async functions."""
112-
notes, errors = _run_mypy("""
113-
import asyncio
114-
from cachier import cachier
115-
116-
@cachier()
117-
async def fetch(url: str) -> bytes:
118-
return b"data"
119-
120-
async def main() -> None:
121-
result = await fetch("http://example.com")
122-
reveal_type(result)
123-
124-
asyncio.run(main())
125-
""")
187+
notes, errors = positive_mypy_output
126188
assert not errors
127189
assert any('"bytes"' in n for n in notes)
128190

129-
def test_async_signature_preserved(self) -> None:
191+
def test_async_signature_preserved(self, positive_mypy_output) -> None:
130192
"""Mypy should see the async function as a coroutine."""
131-
notes, errors = _run_mypy("""
132-
from cachier import cachier
133-
134-
@cachier()
135-
async def fetch(url: str) -> bytes:
136-
return b"data"
137-
138-
reveal_type(fetch)
139-
""")
193+
notes, errors = positive_mypy_output
140194
assert not errors
141195
assert any("Coroutine" in n for n in notes)
142196

@@ -158,90 +212,31 @@ async def main() -> None:
158212
class TestComplexSignatures:
159213
"""Verify preservation of more complex type signatures."""
160214

161-
def test_optional_params(self) -> None:
215+
def test_optional_params(self, positive_mypy_output) -> None:
162216
"""Mypy should preserve Optional parameter types."""
163-
notes, errors = _run_mypy("""
164-
from typing import Optional
165-
from cachier import cachier
166-
167-
@cachier()
168-
def greet(name: str, greeting: Optional[str] = None) -> str:
169-
return f"{greeting or 'Hello'}, {name}"
170-
171-
reveal_type(greet)
172-
""")
217+
notes, errors = positive_mypy_output
173218
assert not errors
174-
assert any("str" in n for n in notes)
219+
assert any("name: str, greeting: str | None" in n for n in notes)
175220

176-
def test_generic_return_type(self) -> None:
221+
def test_generic_return_type(self, positive_mypy_output) -> None:
177222
"""Mypy should preserve generic return types like dict."""
178-
notes, errors = _run_mypy("""
179-
from cachier import cachier
180-
181-
@cachier()
182-
def make_mapping(keys: list[str], value: int) -> dict[str, int]:
183-
return {k: value for k in keys}
184-
185-
reveal_type(make_mapping(["a"], 1))
186-
""")
223+
notes, errors = positive_mypy_output
187224
assert not errors
188225
assert any("dict[str, int]" in n for n in notes)
189226

190-
def test_none_return_type(self) -> None:
227+
def test_none_return_type(self, positive_mypy_output) -> None:
191228
"""Mypy should preserve None return type."""
192-
notes, errors = _run_mypy("""
193-
from cachier import cachier
194-
195-
@cachier()
196-
def side_effect(x: int) -> None:
197-
pass
198-
199-
reveal_type(side_effect(1))
200-
""")
229+
notes, errors = positive_mypy_output
201230
assert not errors
202231
assert any('"None"' in n for n in notes)
203232

204233

205234
class TestDecoratorAttributes:
206235
"""Verify that cache-management attributes are visible to type checkers."""
207236

208-
def test_clear_cache_is_callable(self) -> None:
209-
"""Mypy should see ``.clear_cache()`` as a callable attribute."""
210-
_notes, errors = _run_mypy("""
211-
from cachier import cachier
212-
213-
@cachier()
214-
def f(x: int) -> int:
215-
return x
216-
217-
f.clear_cache()
218-
""")
219-
assert not errors
220-
221-
def test_precache_value_is_callable(self) -> None:
222-
"""Mypy should see ``.precache_value()`` as a callable attribute."""
223-
_notes, errors = _run_mypy("""
224-
from cachier import cachier
225-
226-
@cachier()
227-
def f(x: int) -> int:
228-
return x
229-
230-
f.precache_value(1, value_to_cache=42)
231-
""")
232-
assert not errors
233-
234-
def test_metrics_attribute_accessible(self) -> None:
235-
"""Mypy should see ``.metrics`` as an attribute."""
236-
_notes, errors = _run_mypy("""
237-
from cachier import cachier
238-
239-
@cachier()
240-
def f(x: int) -> int:
241-
return x
242-
243-
m = f.metrics
244-
""")
237+
def test_attributes_do_not_error(self, positive_mypy_output) -> None:
238+
"""Using ``.clear_cache()``, ``.precache_value()``, ``.metrics`` should not raise type errors."""
239+
_notes, errors = positive_mypy_output
245240
assert not errors
246241

247242
def test_undefined_attribute_is_error(self) -> None:
@@ -261,31 +256,14 @@ def f(x: int) -> int:
261256
class TestDecoratorWithArgs:
262257
"""Verify typing works with various decorator arguments."""
263258

264-
def test_with_backend_arg(self) -> None:
259+
def test_with_backend_arg(self, positive_mypy_output) -> None:
265260
"""Type preservation should work with explicit backend selection."""
266-
notes, errors = _run_mypy("""
267-
from cachier import cachier
268-
269-
@cachier(backend="memory")
270-
def compute(x: float) -> float:
271-
return x * 2.0
272-
273-
reveal_type(compute(1.0))
274-
""")
261+
notes, errors = positive_mypy_output
275262
assert not errors
276263
assert any('"float"' in n for n in notes)
277264

278-
def test_with_stale_after_arg(self) -> None:
265+
def test_with_stale_after_arg(self, positive_mypy_output) -> None:
279266
"""Type preservation should work with stale_after parameter."""
280-
notes, errors = _run_mypy("""
281-
from datetime import timedelta
282-
from cachier import cachier
283-
284-
@cachier(stale_after=timedelta(hours=1))
285-
def lookup(key: str) -> list[int]:
286-
return [1, 2, 3]
287-
288-
reveal_type(lookup("x"))
289-
""")
267+
notes, errors = positive_mypy_output
290268
assert not errors
291269
assert any("list[int]" in n for n in notes)

0 commit comments

Comments
 (0)