Skip to content

Commit

Permalink
[python] Docstring typo, test-assertion msgs, maybe_raises improvem…
Browse files Browse the repository at this point in the history
…ent, `verify_logs` helper (#2892)

* docstring typo, test-assertion msgs, parametrize_cases nits

* `maybe_raises`: optionally accept message match string

* `tests._util`: `verify_logs` helper
  • Loading branch information
ryan-williams authored Aug 15, 2024
1 parent 1e64fdd commit f6c9686
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 13 deletions.
2 changes: 1 addition & 1 deletion apis/python/src/tiledbsoma/io/outgest.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def to_anndata(
If ``uns_keys`` is provided, only the specified top-level ``uns`` keys
are extracted. The default is to extract them all. Use ``uns_keys=[]``
to not ingest any ``uns`` keys.
to not outgest any ``uns`` keys.
Lifecycle:
Maturing.
Expand Down
58 changes: 46 additions & 12 deletions apis/python/tests/_util.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from contextlib import contextmanager, nullcontext
from pathlib import Path
from typing import Any, List, Optional, Tuple, Type, Union
Expand All @@ -6,6 +7,7 @@
import pandas as pd
import pytest
from _pytest._code import ExceptionInfo
from _pytest.logging import LogCaptureFixture
from _pytest.python_api import E, RaisesContext
from anndata import AnnData
from numpy import array_equal
Expand All @@ -15,7 +17,9 @@


def assert_uns_equal(uns0, uns1):
assert uns0.keys() == uns1.keys()
assert (
uns0.keys() == uns1.keys()
), f"extra keys: {uns0.keys() - uns1.keys()}, missing keys: {uns1.keys() - uns0.keys()}"
for k, v0 in uns0.items():
try:
v1 = uns1[k]
Expand All @@ -34,7 +38,7 @@ def assert_uns_equal(uns0, uns1):
elif isinstance(v0, np.ndarray):
assert array_equal(v0, v1)
elif isinstance(v0, (int, float, str, bool)):
assert v0 == v1
assert v0 == v1, f"{v0} != {v1}"
else:
raise ValueError(f"Unsupported type: {type(v0)}")
except AssertionError:
Expand Down Expand Up @@ -115,20 +119,50 @@ def raises_no_typeguard(exc: Type[Exception], *args: Any, **kwargs: Any):
yield


# Alias for several types that can be used to specify expected exceptions in `maybe_raises`
Err = Union[str, Type[E], Tuple[Type[E], str]]


def maybe_raises(
expected_exception: Union[None, Type[E], Tuple[Type[E], ...]],
expected_exception: Optional[Err],
*args: Any,
**kwargs: Any,
) -> Union[RaisesContext[E], ExceptionInfo[E]]:
) -> Union[RaisesContext[E], ExceptionInfo[E], nullcontext]:
"""
Wrapper around ``pytest.raises`` that accepts None (signifying no exception should be raised).
This is only necessary since ``pytest.raises`` does not itself accept None, so we are
decorating.
Wrapper around ``pytest.raises`` that additionally accepts ``None`` (signifying no exception
should be raised), a string (signifying a message match) or a tuple of (exception, message).
Useful in test cases that are parameterized to test both valid and invalid inputs.
"""
return (
nullcontext()
if expected_exception is None
else pytest.raises(expected_exception, *args, **kwargs)
)
if expected_exception is not None:
if isinstance(expected_exception, type):
exc = expected_exception
else:
if isinstance(expected_exception, str):
exc, match = Exception, expected_exception
else:
exc, match = expected_exception
if "match" in kwargs:
raise ValueError(
"Cannot specify 'match' in both kwargs and `expected_exception`"
)
kwargs["match"] = match
return pytest.raises(exc, *args, **kwargs)
else:
return nullcontext()


def verify_logs(caplog: LogCaptureFixture, expected_logs: Optional[List[str]]) -> None:
"""Verify that expected log messages are present in a pytest "caplog" (captured logs) fixture."""
if not expected_logs:
return

for expected_log in expected_logs:
found = False
for record in caplog.records:
log_msg = record.getMessage()
if re.search(expected_log, log_msg):
found = True
break
if not found:
raise AssertionError(f"Expected log message not found: {expected_log}")
2 changes: 2 additions & 0 deletions apis/python/tests/parametrize_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def wrapper(fn):
"""
# Test-case IDs
ids = [case.id for case in cases]

# Convert each case to a "values" array; also filter and reorder to match kwargs expected
# by the wrapped "test_*" function.
spec = getfullargspec(fn)
Expand All @@ -33,6 +34,7 @@ def wrapper(fn):
{name: rt_dict[name] for name in names}.values()
for rt_dict in [asdict(case) for case in cases]
]

# Delegate to PyTest `parametrize`
return pytest.mark.parametrize(
names, # kwarg names
Expand Down

0 comments on commit f6c9686

Please sign in to comment.