From 4d0b5eb0da718888cda9f6a98630a65a16434cc3 Mon Sep 17 00:00:00 2001 From: Calum Chamberlain Date: Thu, 2 Apr 2026 09:29:05 +1300 Subject: [PATCH 1/2] Docs rendering Auto doc sting composition not working for update_index. --- src/obsplus/bank/eventbank.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/obsplus/bank/eventbank.py b/src/obsplus/bank/eventbank.py index 48956557..7584bfe0 100644 --- a/src/obsplus/bank/eventbank.py +++ b/src/obsplus/bank/eventbank.py @@ -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, From 5ce49a5b5e4e59bf55f35e5e8ff6758986dab414 Mon Sep 17 00:00:00 2001 From: Derrick Chambers Date: Thu, 2 Apr 2026 13:42:16 +0200 Subject: [PATCH 2/2] fix CI issues, make compose_doctest raise --- pyproject.toml | 6 +-- src/obsplus/events/get_events.py | 4 +- src/obsplus/exceptions.py | 4 ++ src/obsplus/utils/docs.py | 17 +++++++ tests/test_utils/test_doc_utils.py | 56 ++++++++++++++++++++++++ tests/test_utils/test_waveforms_utils.py | 2 +- 6 files changed, 83 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3d1c8f1..bac73a84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/obsplus/events/get_events.py b/src/obsplus/events/get_events.py index 1917b3d5..e574bf7d 100644 --- a/src/obsplus/events/get_events.py +++ b/src/obsplus/events/get_events.py @@ -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: @@ -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) diff --git a/src/obsplus/exceptions.py b/src/obsplus/exceptions.py index 51258fb1..d06a8f31 100644 --- a/src/obsplus/exceptions.py +++ b/src/obsplus/exceptions.py @@ -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 diff --git a/src/obsplus/utils/docs.py b/src/obsplus/utils/docs.py index 6b87d84c..834721b8 100644 --- a/src/obsplus/utils/docs.py +++ b/src/obsplus/utils/docs.py @@ -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]): """ @@ -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) @@ -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 diff --git a/tests/test_utils/test_doc_utils.py b/tests/test_utils/test_doc_utils.py index db380b1b..7a7dbb14 100644 --- a/tests/test_utils/test_doc_utils.py +++ b/tests/test_utils/test_doc_utils.py @@ -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 @@ -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.""" diff --git a/tests/test_utils/test_waveforms_utils.py b/tests/test_utils/test_waveforms_utils.py index da2f4d03..1c2d8ca5 100644 --- a/tests/test_utils/test_waveforms_utils.py +++ b/tests/test_utils/test_waveforms_utils.py @@ -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