Skip to content

Commit

Permalink
Add typing for some of the tests (python-trio#2771)
Browse files Browse the repository at this point in the history
* Add typing for tests
`test_channel` had an error, was awaiting a synchronous function

* Use `Union` for runtime eval types

* More WIP tests

* Finish `test_deprecate.py` I think

* Work on `test_dtls`

* Import `TypeAlias` from typing_extensions if type checking

* More work on `test_dtls` and complete `tutil`

* Work on `test_asyncgen`

* Silence some issues from FakeNet (needs another PR) and other type fixes

* Fix return type of `pipe_with_overlapped_read`

* Ignore FakeNet typing issues for the moment

* Help mypy understand we are in 3.10

* 2nd try fixing mypy missing `contextlib.aclosing`

* Use assert instead

* Use type var to indicate input is unchanged

* Make parameter more specific

* Use a version check mypy will understand

* Fix missing `Callable` import

* Attempt inlining `aclosing`

Co-authored-by: TeamSpen210 <spencerb21@live.com>

* It's `aclose` not `close`

* Suggestions by TeamSpen210

Co-authored-by: TeamSpen210 <spencerb21@live.com>

* Ignore incorrect typing for LogCaptureFixture

* Type check tests more strictly

* Remove type alias for `pytest.WarningsRecorder`

* Fix a bunch of mypy errors

* Use `Any` for generic `CaptureFixture`'s argument

* Ignore `skipif` leaves function untyped error

* Use `str` for `CaptureFixture`'s generic and remove skipif ignore

---------

Co-authored-by: TeamSpen210 <spencerb21@live.com>
  • Loading branch information
CoolCat467 and TeamSpen210 authored Sep 7, 2023
1 parent 4a84041 commit be39867
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 227 deletions.
9 changes: 0 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ module = [

# tests
"trio/testing/_fake_net",
"trio/_core/_tests/test_asyncgen",
"trio/_core/_tests/test_guest_mode",
"trio/_core/_tests/test_instrumentation",
"trio/_core/_tests/test_ki",
Expand All @@ -82,15 +81,7 @@ module = [
"trio/_core/_tests/test_multierror_scripts/simple_excepthook",
"trio/_core/_tests/test_parking_lot",
"trio/_core/_tests/test_thread_cache",
"trio/_core/_tests/test_tutil",
"trio/_core/_tests/test_unbounded_queue",
"trio/_core/_tests/test_windows",
"trio/_core/_tests/tutil",
"trio/_tests/pytest_plugin",
"trio/_tests/test_abc",
"trio/_tests/test_channel",
"trio/_tests/test_deprecate",
"trio/_tests/test_dtls",
"trio/_tests/test_exports",
"trio/_tests/test_file_io",
"trio/_tests/test_highlevel_generic",
Expand Down
93 changes: 54 additions & 39 deletions trio/_core/_tests/test_asyncgen.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import contextlib
from __future__ import annotations

import sys
import weakref
from collections.abc import AsyncGenerator
from math import inf
from typing import NoReturn

import pytest

from ... import _core
from .tutil import buggy_pypy_asyncgens, gc_collect_harder, restore_unraisablehook


@pytest.mark.skipif(sys.version_info < (3, 10), reason="no aclosing() in stdlib<3.10")
def test_asyncgen_basics():
def test_asyncgen_basics() -> None:
collected = []

async def example(cause):
async def example(cause: str) -> AsyncGenerator[int, None]:
try:
try:
yield 42
Expand All @@ -37,7 +39,7 @@ async def example(cause):

saved = []

async def async_main():
async def async_main() -> None:
# GC'ed before exhausted
with pytest.warns(
ResourceWarning, match="Async generator.*collected before.*exhausted"
Expand All @@ -47,9 +49,11 @@ async def async_main():
await _core.wait_all_tasks_blocked()
assert collected.pop() == "abandoned"

# aclosing() ensures it's cleaned up at point of use
async with contextlib.aclosing(example("exhausted 1")) as aiter:
aiter = example("exhausted 1")
try:
assert 42 == await aiter.asend(None)
finally:
await aiter.aclose()
assert collected.pop() == "exhausted 1"

# Also fine if you exhaust it at point of use
Expand All @@ -60,9 +64,12 @@ async def async_main():
gc_collect_harder()

# No problems saving the geniter when using either of these patterns
async with contextlib.aclosing(example("exhausted 3")) as aiter:
aiter = example("exhausted 3")
try:
saved.append(aiter)
assert 42 == await aiter.asend(None)
finally:
await aiter.aclose()
assert collected.pop() == "exhausted 3"

# Also fine if you exhaust it at point of use
Expand All @@ -85,10 +92,12 @@ async def async_main():
assert agen.ag_frame is None # all should now be exhausted


async def test_asyncgen_throws_during_finalization(caplog):
async def test_asyncgen_throws_during_finalization(
caplog: pytest.LogCaptureFixture,
) -> None:
record = []

async def agen():
async def agen() -> AsyncGenerator[int, None]:
try:
yield 1
finally:
Expand All @@ -101,18 +110,19 @@ async def agen():
gc_collect_harder()
await _core.wait_all_tasks_blocked()
assert record == ["crashing"]
exc_type, exc_value, exc_traceback = caplog.records[0].exc_info
# Following type ignore is because typing for LogCaptureFixture is wrong
exc_type, exc_value, exc_traceback = caplog.records[0].exc_info # type: ignore[misc]
assert exc_type is ValueError
assert str(exc_value) == "oops"
assert "during finalization of async generator" in caplog.records[0].message


@pytest.mark.skipif(buggy_pypy_asyncgens, reason="pypy 7.2.0 is buggy")
def test_firstiter_after_closing():
def test_firstiter_after_closing() -> None:
saved = []
record = []

async def funky_agen():
async def funky_agen() -> AsyncGenerator[int, None]:
try:
yield 1
except GeneratorExit:
Expand All @@ -124,7 +134,7 @@ async def funky_agen():
record.append("cleanup 2")
await funky_agen().asend(None)

async def async_main():
async def async_main() -> None:
aiter = funky_agen()
saved.append(aiter)
assert 1 == await aiter.asend(None)
Expand All @@ -135,18 +145,20 @@ async def async_main():


@pytest.mark.skipif(buggy_pypy_asyncgens, reason="pypy 7.2.0 is buggy")
def test_interdependent_asyncgen_cleanup_order():
saved = []
record = []
def test_interdependent_asyncgen_cleanup_order() -> None:
saved: list[AsyncGenerator[int, None]] = []
record: list[int | str] = []

async def innermost():
async def innermost() -> AsyncGenerator[int, None]:
try:
yield 1
finally:
await _core.cancel_shielded_checkpoint()
record.append("innermost")

async def agen(label, inner):
async def agen(
label: int, inner: AsyncGenerator[int, None]
) -> AsyncGenerator[int, None]:
try:
yield await inner.asend(None)
finally:
Expand All @@ -158,7 +170,7 @@ async def agen(label, inner):
await inner.asend(None)
record.append(label)

async def async_main():
async def async_main() -> None:
# This makes a chain of 101 interdependent asyncgens:
# agen(99)'s cleanup will iterate agen(98)'s will iterate
# ... agen(0)'s will iterate innermost()'s
Expand All @@ -174,19 +186,20 @@ async def async_main():


@restore_unraisablehook()
def test_last_minute_gc_edge_case():
saved = []
def test_last_minute_gc_edge_case() -> None:
saved: list[AsyncGenerator[int, None]] = []
record = []
needs_retry = True

async def agen():
async def agen() -> AsyncGenerator[int, None]:
try:
yield 1
finally:
record.append("cleaned up")

def collect_at_opportune_moment(token):
def collect_at_opportune_moment(token: _core._entry_queue.TrioToken) -> None:
runner = _core._run.GLOBAL_RUN_CONTEXT.runner
assert runner.system_nursery is not None
if runner.system_nursery._closed and isinstance(
runner.asyncgens.alive, weakref.WeakSet
):
Expand All @@ -201,7 +214,7 @@ def collect_at_opportune_moment(token):
nonlocal needs_retry
needs_retry = True

async def async_main():
async def async_main() -> None:
token = _core.current_trio_token()
token.run_sync_soon(collect_at_opportune_moment, token)
saved.append(agen())
Expand Down Expand Up @@ -231,7 +244,7 @@ async def async_main():
)


async def step_outside_async_context(aiter):
async def step_outside_async_context(aiter: AsyncGenerator[int, None]) -> None:
# abort_fns run outside of task context, at least if they're
# triggered by a deadline expiry rather than a direct
# cancellation. Thus, an asyncgen first iterated inside one
Expand All @@ -242,13 +255,13 @@ async def step_outside_async_context(aiter):
# NB: the strangeness with aiter being an attribute of abort_fn is
# to make it as easy as possible to ensure we don't hang onto a
# reference to aiter inside the guts of the run loop.
def abort_fn(_):
def abort_fn(_: _core.RaiseCancelT) -> _core.Abort:
with pytest.raises(StopIteration, match="42"):
abort_fn.aiter.asend(None).send(None)
del abort_fn.aiter
abort_fn.aiter.asend(None).send(None) # type: ignore[attr-defined] # Callables don't have attribute "aiter"
del abort_fn.aiter # type: ignore[attr-defined]
return _core.Abort.SUCCEEDED

abort_fn.aiter = aiter
abort_fn.aiter = aiter # type: ignore[attr-defined]

async with _core.open_nursery() as nursery:
nursery.start_soon(_core.wait_task_rescheduled, abort_fn)
Expand All @@ -257,16 +270,18 @@ def abort_fn(_):


@pytest.mark.skipif(buggy_pypy_asyncgens, reason="pypy 7.2.0 is buggy")
async def test_fallback_when_no_hook_claims_it(capsys):
async def well_behaved():
async def test_fallback_when_no_hook_claims_it(
capsys: pytest.CaptureFixture[str],
) -> None:
async def well_behaved() -> AsyncGenerator[int, None]:
yield 42

async def yields_after_yield():
async def yields_after_yield() -> AsyncGenerator[int, None]:
with pytest.raises(GeneratorExit):
yield 42
yield 100

async def awaits_after_yield():
async def awaits_after_yield() -> AsyncGenerator[int, None]:
with pytest.raises(GeneratorExit):
yield 42
await _core.cancel_shielded_checkpoint()
Expand All @@ -286,24 +301,24 @@ async def awaits_after_yield():


@pytest.mark.skipif(buggy_pypy_asyncgens, reason="pypy 7.2.0 is buggy")
def test_delegation_to_existing_hooks():
def test_delegation_to_existing_hooks() -> None:
record = []

def my_firstiter(agen):
def my_firstiter(agen: AsyncGenerator[object, NoReturn]) -> None:
record.append("firstiter " + agen.ag_frame.f_locals["arg"])

def my_finalizer(agen):
def my_finalizer(agen: AsyncGenerator[object, NoReturn]) -> None:
record.append("finalizer " + agen.ag_frame.f_locals["arg"])

async def example(arg):
async def example(arg: str) -> AsyncGenerator[int, None]:
try:
yield 42
finally:
with pytest.raises(_core.Cancelled):
await _core.checkpoint()
record.append("trio collected " + arg)

async def async_main():
async def async_main() -> None:
await step_outside_async_context(example("theirs"))
assert 42 == await example("ours").asend(None)
gc_collect_harder()
Expand Down
2 changes: 1 addition & 1 deletion trio/_core/_tests/test_tutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .tutil import check_sequence_matches


def test_check_sequence_matches():
def test_check_sequence_matches() -> None:
check_sequence_matches([1, 2, 3], [1, 2, 3])
with pytest.raises(AssertionError):
check_sequence_matches([1, 3, 2], [1, 2, 3])
Expand Down
Loading

0 comments on commit be39867

Please sign in to comment.