-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
general: add extra tests for new style type annotations
also update readme
- Loading branch information
Showing
3 changed files
with
147 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |