Skip to content

Commit

Permalink
general: add extra tests for new style type annotations
Browse files Browse the repository at this point in the history
also update readme
  • Loading branch information
karlicoss committed Oct 6, 2024
1 parent 7da4d40 commit e62a6ff
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 104 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Cachew gives the best of two worlds and makes it both **easy and efficient**. Th

# How it works

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

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



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

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

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)
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L632)
* [Optional](src/cachew/tests/test_cachew.py#L534) types
* [Union](src/cachew/tests/test_cachew.py#L853) types
* [nested datatypes](src/cachew/tests/test_cachew.py#L450)
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)
* [@dataclass and NamedTuple](src/cachew/tests/test_cachew.py#L615)
* [Optional](src/cachew/tests/test_cachew.py#L515) types
* [Union](src/cachew/tests/test_cachew.py#L841) types
* [nested datatypes](src/cachew/tests/test_cachew.py#L431)

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


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

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

* `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.
* `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.

By default, `settings.DEFAULT_CACHEW_DIR` is used.

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

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

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

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

Expand Down
94 changes: 0 additions & 94 deletions src/cachew/tests/test_cachew.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import hashlib
import inspect
import os
import string
import sys
import textwrap
import time
import timeit
from concurrent.futures import ProcessPoolExecutor
Expand Down Expand Up @@ -694,7 +692,6 @@ class AllTypes:
an_opt : Optional[str]
# fmt: on

# TODO test new style list/tuple/union/optional
# TODO support vararg tuples?


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

assert (tmp_path / callable_name(fun_single)).exists()
assert (tmp_path / callable_name(fun_multiple)).exists()


@pytest.mark.parametrize('use_future_annotations', [False, True])
@pytest.mark.parametrize('local', [False, True])
@pytest.mark.parametrize('throw', [False, True])
def test_future_annotations(
*,
use_future_annotations: bool,
local: bool,
throw: bool,
tmp_path: Path,
) -> None:
"""
Checks handling of postponed evaluation of annotations (from __future__ import annotations)
"""

if sys.version_info[:2] <= (3, 8):
pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway")

# NOTE: to avoid weird interactions with existing interpreter in which pytest is running
# , we compose a program and running in python directly instead
# (also not sure if it's even possible to tweak postponed annotations without doing that)

if use_future_annotations and local and throw:
# when annotation is local (like inner class), then they end up as strings
# so we can't eval it as we don't have access to a class defined inside function
# keeping this test just to keep track of whether this is fixed at some point
# possibly relevant:
# - https://peps.python.org/pep-0563/#keeping-the-ability-to-use-function-local-state-when-defining-annotations
pytest.skip("local aliases/classses don't work with from __future__ import annotations")

_PREAMBLE = f'''
from pathlib import Path
import tempfile
from cachew import cachew, settings
settings.THROW_ON_ERROR = {throw}
temp_dir = tempfile.TemporaryDirectory()
td = Path(temp_dir.name)
'''

_TEST = '''
T = int
@cachew(td)
def fun() -> list[T]:
print("called")
return [1, 2]
assert list(fun()) == [1, 2]
assert list(fun()) == [1, 2]
'''

if use_future_annotations:
code = '''
from __future__ import annotations
'''
else:
code = ''

code += _PREAMBLE

if local:
code += f'''
def test() -> None:
{textwrap.indent(_TEST, prefix=" ")}
test()
'''
else:
code += _TEST

run_py = tmp_path / 'run.py'
run_py.write_text(code)

cache_dir = tmp_path / 'cache'
cache_dir.mkdir()

res = check_output(
[sys.executable, run_py],
env={'TMPDIR': str(cache_dir), **os.environ},
text=True,
)
called = int(res.count('called'))
if use_future_annotations and local and not throw:
# cachew fails to set up, so no caching but at least it works otherwise
assert called == 2
else:
assert called == 1
137 changes: 137 additions & 0 deletions src/cachew/tests/test_future_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from __future__ import annotations

import os
import sys
import textwrap
from dataclasses import dataclass
from pathlib import Path
from subprocess import check_output
from typing import Any, Iterator

import pytest
from more_itertools import one

from .. import cachew


# fmt: off
@dataclass
class AllTypes:
a_str : str
a_dict : dict[str, Any]
a_list : list[Any]
a_tuple : tuple[float, str]
an_opt : str | None
a_union : str | int
# fmt: on


def test_types(tmp_path: Path) -> None:
# fmt: off
obj = AllTypes(
a_str = 'abac',
a_dict = {'a': True, 'x': {'whatever': 3.14}},
a_list = ['aba', 123, None],
a_tuple = (1.23, '3.2.1'),
an_opt = 'hello',
a_union = 999,
)
# fmt: on

@cachew(tmp_path)
def get() -> Iterator[AllTypes]:
yield obj

assert one(get()) == obj
assert one(get()) == obj


@pytest.mark.parametrize('use_future_annotations', [False, True])
@pytest.mark.parametrize('local', [False, True])
@pytest.mark.parametrize('throw', [False, True])
def test_future_annotations(
*,
use_future_annotations: bool,
local: bool,
throw: bool,
tmp_path: Path,
) -> None:
"""
Checks handling of postponed evaluation of annotations (from __future__ import annotations)
"""

if sys.version_info[:2] <= (3, 8):
pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway")

# NOTE: to avoid weird interactions with existing interpreter in which pytest is running
# , we compose a program and running in python directly instead
# (also not sure if it's even possible to tweak postponed annotations without doing that)

if use_future_annotations and local and throw:
# when annotation is local (like inner class), then they end up as strings
# so we can't eval it as we don't have access to a class defined inside function
# keeping this test just to keep track of whether this is fixed at some point
# possibly relevant:
# - https://peps.python.org/pep-0563/#keeping-the-ability-to-use-function-local-state-when-defining-annotations
pytest.skip("local aliases/classses don't work with from __future__ import annotations")

_PREAMBLE = f'''
from pathlib import Path
import tempfile
from cachew import cachew, settings
settings.THROW_ON_ERROR = {throw}
temp_dir = tempfile.TemporaryDirectory()
td = Path(temp_dir.name)
'''

_TEST = '''
T = int
@cachew(td)
def fun() -> list[T]:
print("called")
return [1, 2]
assert list(fun()) == [1, 2]
assert list(fun()) == [1, 2]
'''

if use_future_annotations:
code = '''
from __future__ import annotations
'''
else:
code = ''

code += _PREAMBLE

if local:
code += f'''
def test() -> None:
{textwrap.indent(_TEST, prefix=" ")}
test()
'''
else:
code += _TEST

run_py = tmp_path / 'run.py'
run_py.write_text(code)

cache_dir = tmp_path / 'cache'
cache_dir.mkdir()

res = check_output(
[sys.executable, run_py],
env={'TMPDIR': str(cache_dir), **os.environ},
text=True,
)
called = int(res.count('called'))
if use_future_annotations and local and not throw:
# cachew fails to set up, so no caching but at least it works otherwise
assert called == 2
else:
assert called == 1

0 comments on commit e62a6ff

Please sign in to comment.