Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

general: add extra tests for new style type annotations #69

Merged
merged 1 commit into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
163 changes: 163 additions & 0 deletions src/cachew/tests/test_future_annotations.py
Original file line number Diff line number Diff line change
@@ -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
Loading