diff --git a/README.md b/README.md index 05fa533..8f99439 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -170,7 +170,7 @@ 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. @@ -178,7 +178,7 @@ Some useful (but optional) arguments of `@cachew` decorator: 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. diff --git a/src/cachew/tests/test_cachew.py b/src/cachew/tests/test_cachew.py index c7ea5f8..a64ee38 100644 --- a/src/cachew/tests/test_cachew.py +++ b/src/cachew/tests/test_cachew.py @@ -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 @@ -694,7 +692,6 @@ class AllTypes: an_opt : Optional[str] # fmt: on -# TODO test new style list/tuple/union/optional # TODO support vararg tuples? @@ -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 diff --git a/src/cachew/tests/test_future_annotations.py b/src/cachew/tests/test_future_annotations.py new file mode 100644 index 0000000..9d688c7 --- /dev/null +++ b/src/cachew/tests/test_future_annotations.py @@ -0,0 +1,163 @@ +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 NewStyleTypes1: + a_str : str + a_dict : dict[str, Any] + a_list : list[Any] + a_tuple : tuple[float, str] +# fmt: on + + +def test_types1(tmp_path: Path) -> None: + if sys.version_info[:2] <= (3, 8): + pytest.skip("too annoying to adjust for 3.8 and it's EOL soon anyway") + + # fmt: off + obj = NewStyleTypes1( + a_str = 'abac', + a_dict = {'a': True, 'x': {'whatever': 3.14}}, + a_list = ['aba', 123, None], + a_tuple = (1.23, '3.2.1'), + ) + # fmt: on + + @cachew(tmp_path) + def get() -> Iterator[NewStyleTypes1]: + yield obj + + assert one(get()) == obj + assert one(get()) == obj + + +# fmt: off +@dataclass +class NewStyleTypes2: + an_opt : str | None + a_union : str | int +# fmt: on + + +def test_types2(tmp_path: Path) -> None: + if sys.version_info[:2] <= (3, 9): + pytest.skip("can only use new style union types from 3.10") + + # fmt: off + obj = NewStyleTypes2( + an_opt = 'hello', + a_union = 999, + ) + # fmt: on + + @cachew(tmp_path) + def get() -> Iterator[NewStyleTypes2]: + 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