Skip to content

Commit 2383901

Browse files
committed
general: add extra tests for new style type annotations
also update readme
1 parent 7da4d40 commit 2383901

3 files changed

Lines changed: 173 additions & 104 deletions

File tree

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Cachew gives the best of two worlds and makes it both **easy and efficient**. Th
125125

126126
# How it works
127127

128-
- first your objects get [converted](src/cachew/marshall/cachew.py#L35) into a simpler JSON-like representation
128+
- first your objects get [converted](src/cachew/marshall/cachew.py#L33) into a simpler JSON-like representation
129129
- after that, they are mapped into byte blobs via [`orjson`](https://github.com/ijl/orjson).
130130

131131
When the function is called, cachew [computes the hash of your function's arguments ](src/cachew/__init__.py:#L592)
@@ -140,18 +140,18 @@ and compares it against the previously stored hash value.
140140

141141

142142

143-
* automatic schema inference: [1](src/cachew/tests/test_cachew.py#L390), [2](src/cachew/tests/test_cachew.py#L404)
143+
* automatic schema inference: [1](src/cachew/tests/test_cachew.py#L371), [2](src/cachew/tests/test_cachew.py#L385)
144144
* supported types:
145145

146146
* primitive: `str`, `int`, `float`, `bool`, `datetime`, `date`, `Exception`
147147

148-
See [tests.test_types](src/cachew/tests/test_cachew.py#L713), [tests.test_primitive](src/cachew/tests/test_cachew.py#L747), [tests.test_dates](src/cachew/tests/test_cachew.py#L667), [tests.test_exceptions](src/cachew/tests/test_cachew.py#L1145)
149-
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L632)
150-
* [Optional](src/cachew/tests/test_cachew.py#L534) types
151-
* [Union](src/cachew/tests/test_cachew.py#L853) types
152-
* [nested datatypes](src/cachew/tests/test_cachew.py#L450)
148+
See [tests.test_types](src/cachew/tests/test_cachew.py#L698), [tests.test_primitive](src/cachew/tests/test_cachew.py#L734), [tests.test_dates](src/cachew/tests/test_cachew.py#L650), [tests.test_exceptions](src/cachew/tests/test_cachew.py#L1133)
149+
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L615)
150+
* [Optional](src/cachew/tests/test_cachew.py#L515) types
151+
* [Union](src/cachew/tests/test_cachew.py#L841) types
152+
* [nested datatypes](src/cachew/tests/test_cachew.py#L431)
153153

154-
* detects [datatype schema changes](src/cachew/tests/test_cachew.py#L480) and discards old data automatically
154+
* detects [datatype schema changes](src/cachew/tests/test_cachew.py#L461) and discards old data automatically
155155

156156

157157
# Performance
@@ -170,15 +170,15 @@ You can also use [extensive unit tests](src/cachew/tests/test_cachew.py) as a re
170170

171171
Some useful (but optional) arguments of `@cachew` decorator:
172172

173-
* `cache_path` can be a directory, or a callable that [returns a path](src/cachew/tests/test_cachew.py#L427) and depends on function's arguments.
173+
* `cache_path` can be a directory, or a callable that [returns a path](src/cachew/tests/test_cachew.py#L408) and depends on function's arguments.
174174

175175
By default, `settings.DEFAULT_CACHEW_DIR` is used.
176176

177177
* `depends_on` is a function which determines whether your inputs have changed, and the cache needs to be invalidated.
178178

179179
By default it just uses string representation of the arguments, you can also specify a custom callable.
180180

181-
For instance, it can be used to [discard cache](src/cachew/tests/test_cachew.py#L122) if the input file was modified.
181+
For instance, it can be used to [discard cache](src/cachew/tests/test_cachew.py#L103) if the input file was modified.
182182

183183
* `cls` is the type that would be serialized.
184184

src/cachew/tests/test_cachew.py

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import hashlib
22
import inspect
3-
import os
43
import string
54
import sys
6-
import textwrap
75
import time
86
import timeit
97
from concurrent.futures import ProcessPoolExecutor
@@ -694,7 +692,6 @@ class AllTypes:
694692
an_opt : Optional[str]
695693
# fmt: on
696694

697-
# TODO test new style list/tuple/union/optional
698695
# TODO support vararg tuples?
699696

700697

@@ -1445,94 +1442,3 @@ def fun_multiple() -> Iterable[int]:
14451442

14461443
assert (tmp_path / callable_name(fun_single)).exists()
14471444
assert (tmp_path / callable_name(fun_multiple)).exists()
1448-
1449-
1450-
@pytest.mark.parametrize('use_future_annotations', [False, True])
1451-
@pytest.mark.parametrize('local', [False, True])
1452-
@pytest.mark.parametrize('throw', [False, True])
1453-
def test_future_annotations(
1454-
*,
1455-
use_future_annotations: bool,
1456-
local: bool,
1457-
throw: bool,
1458-
tmp_path: Path,
1459-
) -> None:
1460-
"""
1461-
Checks handling of postponed evaluation of annotations (from __future__ import annotations)
1462-
"""
1463-
1464-
if sys.version_info[:2] <= (3, 8):
1465-
pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway")
1466-
1467-
# NOTE: to avoid weird interactions with existing interpreter in which pytest is running
1468-
# , we compose a program and running in python directly instead
1469-
# (also not sure if it's even possible to tweak postponed annotations without doing that)
1470-
1471-
if use_future_annotations and local and throw:
1472-
# when annotation is local (like inner class), then they end up as strings
1473-
# so we can't eval it as we don't have access to a class defined inside function
1474-
# keeping this test just to keep track of whether this is fixed at some point
1475-
# possibly relevant:
1476-
# - https://peps.python.org/pep-0563/#keeping-the-ability-to-use-function-local-state-when-defining-annotations
1477-
pytest.skip("local aliases/classses don't work with from __future__ import annotations")
1478-
1479-
_PREAMBLE = f'''
1480-
from pathlib import Path
1481-
import tempfile
1482-
1483-
from cachew import cachew, settings
1484-
settings.THROW_ON_ERROR = {throw}
1485-
1486-
temp_dir = tempfile.TemporaryDirectory()
1487-
td = Path(temp_dir.name)
1488-
1489-
'''
1490-
1491-
_TEST = '''
1492-
T = int
1493-
1494-
@cachew(td)
1495-
def fun() -> list[T]:
1496-
print("called")
1497-
return [1, 2]
1498-
1499-
assert list(fun()) == [1, 2]
1500-
assert list(fun()) == [1, 2]
1501-
'''
1502-
1503-
if use_future_annotations:
1504-
code = '''
1505-
from __future__ import annotations
1506-
'''
1507-
else:
1508-
code = ''
1509-
1510-
code += _PREAMBLE
1511-
1512-
if local:
1513-
code += f'''
1514-
def test() -> None:
1515-
{textwrap.indent(_TEST, prefix=" ")}
1516-
1517-
test()
1518-
'''
1519-
else:
1520-
code += _TEST
1521-
1522-
run_py = tmp_path / 'run.py'
1523-
run_py.write_text(code)
1524-
1525-
cache_dir = tmp_path / 'cache'
1526-
cache_dir.mkdir()
1527-
1528-
res = check_output(
1529-
[sys.executable, run_py],
1530-
env={'TMPDIR': str(cache_dir), **os.environ},
1531-
text=True,
1532-
)
1533-
called = int(res.count('called'))
1534-
if use_future_annotations and local and not throw:
1535-
# cachew fails to set up, so no caching but at least it works otherwise
1536-
assert called == 2
1537-
else:
1538-
assert called == 1
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import sys
5+
import textwrap
6+
from dataclasses import dataclass
7+
from pathlib import Path
8+
from subprocess import check_output
9+
from typing import Any, Iterator
10+
11+
import pytest
12+
from more_itertools import one
13+
14+
from .. import cachew
15+
16+
17+
# fmt: off
18+
@dataclass
19+
class NewStyleTypes1:
20+
a_str : str
21+
a_dict : dict[str, Any]
22+
a_list : list[Any]
23+
a_tuple : tuple[float, str]
24+
# fmt: on
25+
26+
27+
def test_types1(tmp_path: Path) -> None:
28+
if sys.version_info[:2] <= (3, 8):
29+
pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway")
30+
31+
# fmt: off
32+
obj = NewStyleTypes1(
33+
a_str = 'abac',
34+
a_dict = {'a': True, 'x': {'whatever': 3.14}},
35+
a_list = ['aba', 123, None],
36+
a_tuple = (1.23, '3.2.1'),
37+
)
38+
# fmt: on
39+
40+
@cachew(tmp_path)
41+
def get() -> Iterator[NewStyleTypes1]:
42+
yield obj
43+
44+
assert one(get()) == obj
45+
assert one(get()) == obj
46+
47+
48+
# fmt: off
49+
@dataclass
50+
class NewStyleTypes2:
51+
an_opt : str | None
52+
a_union : str | int
53+
# fmt: on
54+
55+
56+
def test_types2(tmp_path: Path) -> None:
57+
if sys.version_info[:2] <= (3, 9):
58+
pytest.skip("can only use new style union types from 3.10")
59+
60+
# fmt: off
61+
obj = NewStyleTypes2(
62+
an_opt = 'hello',
63+
a_union = 999,
64+
)
65+
# fmt: on
66+
67+
@cachew(tmp_path)
68+
def get() -> Iterator[NewStyleTypes2]:
69+
yield obj
70+
71+
assert one(get()) == obj
72+
assert one(get()) == obj
73+
74+
75+
@pytest.mark.parametrize('use_future_annotations', [False, True])
76+
@pytest.mark.parametrize('local', [False, True])
77+
@pytest.mark.parametrize('throw', [False, True])
78+
def test_future_annotations(
79+
*,
80+
use_future_annotations: bool,
81+
local: bool,
82+
throw: bool,
83+
tmp_path: Path,
84+
) -> None:
85+
"""
86+
Checks handling of postponed evaluation of annotations (from __future__ import annotations)
87+
"""
88+
89+
if sys.version_info[:2] <= (3, 8):
90+
pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway")
91+
92+
# NOTE: to avoid weird interactions with existing interpreter in which pytest is running
93+
# , we compose a program and running in python directly instead
94+
# (also not sure if it's even possible to tweak postponed annotations without doing that)
95+
96+
if use_future_annotations and local and throw:
97+
# when annotation is local (like inner class), then they end up as strings
98+
# so we can't eval it as we don't have access to a class defined inside function
99+
# keeping this test just to keep track of whether this is fixed at some point
100+
# possibly relevant:
101+
# - https://peps.python.org/pep-0563/#keeping-the-ability-to-use-function-local-state-when-defining-annotations
102+
pytest.skip("local aliases/classses don't work with from __future__ import annotations")
103+
104+
_PREAMBLE = f'''
105+
from pathlib import Path
106+
import tempfile
107+
108+
from cachew import cachew, settings
109+
settings.THROW_ON_ERROR = {throw}
110+
111+
temp_dir = tempfile.TemporaryDirectory()
112+
td = Path(temp_dir.name)
113+
114+
'''
115+
116+
_TEST = '''
117+
T = int
118+
119+
@cachew(td)
120+
def fun() -> list[T]:
121+
print("called")
122+
return [1, 2]
123+
124+
assert list(fun()) == [1, 2]
125+
assert list(fun()) == [1, 2]
126+
'''
127+
128+
if use_future_annotations:
129+
code = '''
130+
from __future__ import annotations
131+
'''
132+
else:
133+
code = ''
134+
135+
code += _PREAMBLE
136+
137+
if local:
138+
code += f'''
139+
def test() -> None:
140+
{textwrap.indent(_TEST, prefix=" ")}
141+
142+
test()
143+
'''
144+
else:
145+
code += _TEST
146+
147+
run_py = tmp_path / 'run.py'
148+
run_py.write_text(code)
149+
150+
cache_dir = tmp_path / 'cache'
151+
cache_dir.mkdir()
152+
153+
res = check_output(
154+
[sys.executable, run_py],
155+
env={'TMPDIR': str(cache_dir), **os.environ},
156+
text=True,
157+
)
158+
called = int(res.count('called'))
159+
if use_future_annotations and local and not throw:
160+
# cachew fails to set up, so no caching but at least it works otherwise
161+
assert called == 2
162+
else:
163+
assert called == 1

0 commit comments

Comments
 (0)