Skip to content

Commit

Permalink
chore: rm docstring_param decorator and duplicate docstring args
Browse files Browse the repository at this point in the history
  • Loading branch information
aidanjgriffiths committed Nov 9, 2023
1 parent 3c28e0b commit 55fa90f
Show file tree
Hide file tree
Showing 3 changed files with 20 additions and 202 deletions.
60 changes: 19 additions & 41 deletions src/scores/continuous/flip_flop.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,23 @@

from scores.functions import angular_difference
from scores.typing import XarrayLike
from scores.utils import DimensionError, check_dims, dims_complement, docstring_param
from scores.utils import DimensionError, check_dims, dims_complement

_FLIPFLOP_DATA = """\
data: Data from which to draw subsets.\
"""
_FLIPFLOP_SAMPLING_DIM = """\
sampling_dim: The name of the dimension along which to calculate
the flip-flop index.\
"""
_FLIPFLOP_SELECTIONS = """\
**selections: Additional keyword arguments specify
subsets to draw from the dimension `sampling_dim` of the supplied `data`
before calculation of the flipflop index. e.g. days123=[1, 2, 3]\
"""
_IS_ANGULAR = """\
is_angular: specifies whether `data` is directional data (e.g. wind
direction).
"""


@docstring_param(
ntabs=3,
data=_FLIPFLOP_DATA,
sampling_dim=_FLIPFLOP_SAMPLING_DIM,
is_angular=_IS_ANGULAR,
)
def _flip_flop_index(data: xr.DataArray, sampling_dim: str, is_angular: bool = False) -> xr.DataArray:
"""
Calculates the flip-flop index by collapsing the dimension specified by
`sampling_dim`.
Args:
{data}
{sampling_dim}
{is_angular}
data: Data from which to draw subsets.
sampling_dim: The name of the dimension along which to calculate
the flip-flop index.
is_angular: specifies whether `data` is directional data (e.g. wind
direction).
Returns:
An xarray.DataArray of the flip-flop index with the dimensions of
A xarray.DataArray of the flip-flop index with the dimensions of
`data`, except for the `sampling_dim` dimension which is collapsed.
See also:
Expand Down Expand Up @@ -91,33 +70,32 @@ def _flip_flop_index(data: xr.DataArray, sampling_dim: str, is_angular: bool = F
@overload
def flip_flop_index(
data: xr.DataArray, sampling_dim: str, is_angular: bool = False, **selections: Iterable[int]
) -> xr.DataArray:
) -> xr.Dataset:
...


@overload
def flip_flop_index(data: xr.DataArray, sampling_dim: str, is_angular: bool = False, **selections: None) -> xr.Dataset:
def flip_flop_index(
data: xr.DataArray, sampling_dim: str, is_angular: bool = False, **selections: None
) -> xr.DataArray:
...


@docstring_param(
ntabs=3,
data=_FLIPFLOP_DATA,
sampling_dim=_FLIPFLOP_SAMPLING_DIM,
is_angular=_IS_ANGULAR,
selections=_FLIPFLOP_SELECTIONS,
)
def flip_flop_index(
data: xr.DataArray, sampling_dim: str, is_angular: bool = False, **selections: Optional[Iterable[int]]
) -> XarrayLike:
"""
Calculates the Flip-flop Index along the dimensions `sampling_dim`.
Args:
{data}
{sampling_dim}
{is_angular}
{selections}
data: Data from which to draw subsets.
sampling_dim: The name of the dimension along which to calculate
the flip-flop index.
is_angular: specifies whether `data` is directional data (e.g. wind
direction).
**selections: Additional keyword arguments specify
subsets to draw from the dimension `sampling_dim` of the supplied `data`
before calculation of the flipflop index. e.g. days123=[1, 2, 3]
Returns:
If `selections` are not supplied: An xarray.DataArray, the Flip-flop
Expand Down
76 changes: 0 additions & 76 deletions src/scores/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,79 +215,3 @@ def check_dims(xr_data: XarrayLike, expected_dims: Sequence[str], mode: Optional
f"Dimensions {list(xr_data[data_var].dims)} of data variable "
f"'{data_var}' are not {mode} to the dimensions {sorted(dims_set)}"
)


def docstring_param(*args, **kwargs):
"""
A function decorator to substitute parameters in the function's docstring.
Args:
ntabs (Optional[int]): optional keyword-only argument, pad all parameters
by the number of tabs specified (except for the first lines, which
are left-stripped).
Other args: all positional args and other keyword args are passed to
function.__doc__.format(...)
Example:
>>> @docstring_param('foo', 'bar')
... def my_function():
... '''My Docstring is {} {}.'''
... # function code
>>> help(my_function)
Help on function my_function:
my_function()
My Docstring is foo bar.
>>> foo = '''
... Foo Bar
... Baz
... '''
... @docstring_param(foo=foo, ntabs=3)
... def my_function():
... '''
... My Docstring is {foo}
... '''
... # function code
>>> help(my_function)
Help on function my_function:
my_function()
My Docstring is Foo Bar
Baz
# note that Baz is shifted 3 tabs to the right of 'My Docstring...'
"""
ntabs = kwargs.get("ntabs", 0)
pad = " " * ntabs

def trim_and_pad(docstring):
"""
Trim then pad tabs for a docstring.
"""
# clean the docstring by removing common white space on the left,
# and any leading/trailing white spaces
docstring = inspect.cleandoc(docstring)

lines = docstring.split("\n")
# since we are substituting into an existing docstring, do not pad
# the first line (or the first non-whitespace character will be
# shifted to the right compared to the position of {..} in the
# existing docstring
padded = [lines[0]] + [(pad + line) if line else "" for line in lines[1:]]

return "\n".join(padded)

def substitute_param(obj):
"""
Substitute parameters in obj.__doc__ using .format(...).
"""
_args = [trim_and_pad(arg) for arg in args]
_kwargs = {key: trim_and_pad(value) for key, value in kwargs.items() if key != "ntabs"}

new_doc = obj.__doc__.format(*_args, **_kwargs)

obj.__doc__ = new_doc
return obj

return substitute_param
86 changes: 1 addition & 85 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest

from scores import utils
from scores.utils import DimensionError, docstring_param
from scores.utils import DimensionError
from scores.utils import gather_dimensions as gd
from tests import utils_test_data

Expand Down Expand Up @@ -500,87 +500,3 @@ def test_gather_dimensions_exceptions():
# Preserve "all" as a string but named dimension present in data
with pytest.warns(UserWarning):
assert gd(fcst_dims_conflict, obs_dims, reduce_dims="all") == fcst_dims_conflict


def test_docstring_param():
"""
Tests the @docstring_param function decorator.
"""

@docstring_param("foo", "bar")
def my_function():
"""My Docstring is {} {}."""

assert my_function.__doc__ == "My Docstring is foo bar."

@docstring_param(foo="foo", bar="bar")
def another_function():
"""My Docstring is {foo} {bar}."""

assert another_function.__doc__ == "My Docstring is foo bar."


def test_docstring_param_multiline():
"""
Tests the @docstring_param function decorator on multiline docstrings.
"""

docstring_elem = """
This line has no leading spaces.
This line is padded.
Relative indentation is preserved.
The final newline is suppressed.
"""

@docstring_param(docstring_elem, ntabs=3)
def my_function():
"""
My Docstring is here.
Heading:
Indented text.
{}
End of the Docstring.
"""

expected_docstring = """
My Docstring is here.
Heading:
Indented text.
This line has no leading spaces.
This line is padded.
Relative indentation is preserved.
The final newline is suppressed.
End of the Docstring.
"""

assert my_function.__doc__ == expected_docstring


def test_docstring_param_class():
"""
Tests the @docstring_param decorator on a class.
"""

# pylint: disable=too-few-public-methods
@docstring_param("Bar")
class Foo:
"""{}"""

def __init__(self):
self.prop = "okay"

@classmethod
def method(cls):
"""dummy method to ensure the class still behaves properly."""
return "okay"

assert Foo.__doc__ == "Bar"
# smoke test that the class still behaves like a class
assert Foo().prop == "okay"
assert Foo.method() == "okay"

0 comments on commit 55fa90f

Please sign in to comment.