@@ -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+
49140class 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:
107182class 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:
158212class 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
205234class 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:
261256class 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