Skip to content
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
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ docs = [

test = [
"pytest",
# coveralls currently requires coverage < 5.0
"coverage < 5.0",
# Python 3.14 deprecates code.co_lnotab; older coverage releases still use it.
"coverage >= 7",
"pytest-cov",
"coveralls",
"coveralls >= 4",
"nbval",
"twine",
"pre-commit",
Expand Down
4 changes: 2 additions & 2 deletions src/obsplus/bank/eventbank.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ def read_index(self, **kwargs) -> pd.DataFrame:
return df

@compose_docstring(
bar_description=bar_parameter_description,
subpaths_description=paths_description,
bar_parameter_description=bar_parameter_description,
paths_description=paths_description,
)
def update_index(
self,
Expand Down
4 changes: 2 additions & 2 deletions src/obsplus/events/get_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def get_events(cat: obspy.Catalog, **kwargs) -> obspy.Catalog:

Parameters
----------
{get_event_parameters}
{get_events_params}
"""
# If not kwargs are passed just return all events
if not kwargs:
Expand All @@ -193,7 +193,7 @@ def get_event_summary(cat: obspy.Catalog | pd.DataFrame, **kwargs) -> pd.DataFra

Parameters
----------
{get_event_parameters}
{get_events_params}
"""
df = obsplus.events_to_df(cat)
event_ids = _get_ids(df, kwargs)
Expand Down
4 changes: 4 additions & 0 deletions src/obsplus/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class DataFrameContentError(ValueError):
"""Raised when something is unexpected in a dataframe's contents."""


class DocstringCompositionError(ValueError):
"""Raised when docstring placeholder substitution fails."""


class AmbiguousResponseError(ValueError):
"""
Raised when trying to get a response for an inventory but more than
Expand Down
17 changes: 17 additions & 0 deletions src/obsplus/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

from __future__ import annotations

import re
import textwrap
from collections.abc import Sequence
from typing import Any

from obsplus.exceptions import DocstringCompositionError

_PLACEHOLDER_RE = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)\}")


def compose_docstring(**kwargs: str | Sequence[str]):
"""
Expand Down Expand Up @@ -40,6 +45,7 @@ def example_function():
def _wrap(func):
docstring = func.__doc__
assert isinstance(docstring, str)
used_keys = set()
# iterate each provided value and look for it in the docstring
for key, value in kwargs.items():
value = value if isinstance(value, str) else "\n".join(value)
Expand All @@ -55,6 +61,17 @@ def _wrap(func):
assert set(spaces) == {" "} or not len(spaces)
new = textwrap.indent(textwrap.dedent(value), spaces)
docstring = docstring.replace(line, new)
used_keys.add(key)

unused_keys = sorted(set(kwargs) - used_keys)
unresolved_placeholders = sorted(set(_PLACEHOLDER_RE.findall(docstring)))
if unused_keys or unresolved_placeholders:
msg = (
f"failed to compose docstring for {func.__qualname__}: "
f"unused keys={unused_keys}, "
f"unresolved placeholders={unresolved_placeholders}"
)
raise DocstringCompositionError(msg)

func.__doc__ = docstring
return func
Expand Down
56 changes: 56 additions & 0 deletions tests/test_utils/test_doc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import textwrap

import pytest
from obsplus.constants import STATION_DTYPES
from obsplus.exceptions import DocstringCompositionError
from obsplus.utils.docs import compose_docstring, format_dtypes


Expand Down Expand Up @@ -59,6 +61,60 @@ def dummy_func():
# all whitespace counts should be the same for the list lines.
assert len(set(white_space_counts)) == 1

def test_raises_on_unused_key(self):
"""Unused compose_docstring keys should fail fast."""
with pytest.raises(DocstringCompositionError, match="unused keys"):

@compose_docstring(params="value")
def testfun2():
"""
A simple test function.
"""

def test_raises_on_unresolved_placeholder(self):
"""Unresolved identifier placeholders should raise."""
with pytest.raises(DocstringCompositionError, match="unresolved placeholders"):

@compose_docstring(params="value")
def testfun3():
"""
A simple test function.

{params}
{missing_value}
"""

def test_ignores_literal_non_placeholder_braces(self):
"""Literal braces that are not placeholders should not raise."""

@compose_docstring(params="value")
def testfun4():
"""
Example dictionary:
{"a": 1}

{params}
"""

assert "value" in testfun4.__doc__

def test_exception_names_function_and_keys(self):
"""The error should identify the function and bad keys."""
with pytest.raises(DocstringCompositionError) as exc:

@compose_docstring(params="value")
def testfun5():
"""
A simple test function.

{missing_value}
"""

msg = str(exc.value)
assert "testfun5" in msg
assert "params" in msg
assert "missing_value" in msg


class TestFormatDtypes:
"""Tests for formatting datatypes to display in docstrings."""
Expand Down
2 changes: 1 addition & 1 deletion tests/test_utils/test_waveforms_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def test_fragmented_stream(self, fragmented_stream):
"""Test with streams that are fragmented"""
with pytest.warns(UserWarning) as w:
st = trim_event_stream(fragmented_stream)
assert "seconds long" in str(w[0].message)
assert any("seconds long" in str(warning.message) for warning in w)
stations = {tr.stats.station for tr in st}
assert "BOB" not in stations

Expand Down
Loading