Files
rio/tests/test_documentation.py

135 lines
4.0 KiB
Python

from collections.abc import Iterable
import imy.docstrings
import pytest
import rio.docs
def _create_tests():
for obj, docs in rio.docs.find_documented_objects().items():
if isinstance(docs, imy.docstrings.FunctionDocs):
test_cls = _create_function_tests(docs)
else:
assert isinstance(obj, type)
test_cls = _create_class_tests(obj, docs)
globals()[test_cls.__name__] = test_cls
def _create_function_tests(docs: imy.docstrings.FunctionDocs) -> type:
# If the function is a decorator, there's no need to document that it takes
# a function/class as an argument
if (
docs.metadata.decorator
and len(docs.parameters) == 1
and docs.parameters[0].name == "handler"
):
parameters = []
else:
parameters = docs.parameters
class Tests: # type: ignore
def test_summary(self) -> None:
assert docs.summary is not None, f"{docs.name} has no summary"
def test_details(self) -> None:
assert docs.details is not None, f"{docs.name} has no details"
@pytest.mark.parametrize(
"param",
parameters,
ids=[param.name for param in parameters],
)
def test_param_description(
self, param: imy.docstrings.FunctionParameter
) -> None:
assert (
param.description is not None
), f"{docs.name}.{param.name} has no description"
Tests.__name__ = f"Test_{docs.name}"
return Tests
def _create_class_tests(cls: type, docs: imy.docstrings.ClassDocs) -> type:
methods = [
func
for func in docs.functions
if func.name != "__init__" or not issubclass(cls, rio.Component)
]
methods_excluding_init = [
func for func in methods if func.name != "__init__"
]
# Components only need their constructor documented, attributes don't matter
attributes = [] if issubclass(cls, rio.Component) else docs.attributes
class Tests:
def test_summary(self) -> None:
assert docs.summary is not None, f"{cls.__name__} has no summary"
def test_details(self) -> None:
assert docs.details is not None, f"{cls.__name__} has no details"
@parametrize_with_name("attr", attributes)
def test_attribute_description(
self, attr: imy.docstrings.ClassField
) -> None:
assert (
attr.description is not None
), f"{cls.__name__}.{attr.name} has no description"
# __init__ methods don't need a summary
@parametrize_with_name("method", methods_excluding_init)
def test_method_summary(
self, method: imy.docstrings.FunctionDocs
) -> None:
assert (
method.summary is not None
), f"{cls.__name__}.{method.name} has no summary"
# __init__ methods don't need details
@parametrize_with_name("method", methods_excluding_init)
def test_method_details(
self, method: imy.docstrings.FunctionDocs
) -> None:
assert (
method.details is not None
), f"{cls.__name__}.{method.name} has no details"
@parametrize_with_name("method", methods)
def test_method_parameters(
self, method: imy.docstrings.FunctionDocs
) -> None:
for param in method.parameters[1:]:
assert (
param.description is not None
), f"Parameter {param.name!r} has no description"
Tests.__name__ = f"Test{docs.name}"
return Tests
def parametrize_with_name(
param_name: str,
docs: Iterable[
imy.docstrings.FunctionDocs
| imy.docstrings.ClassDocs
| imy.docstrings.ClassField
| imy.docstrings.FunctionParameter
],
):
def decorator(func):
pytest.mark.parametrize(
param_name,
docs,
ids=[doc.name for doc in docs],
)(func)
return decorator
_create_tests()