Skip to content
Open
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
168 changes: 151 additions & 17 deletions chainladder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from __future__ import annotations

import copy
import inspect
import re
import numpy as np
import pandas as pd
from importlib.metadata import version

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from re import Match


# Get the default datetime64 data type and precision, extracted from Pandas installation.
# Used for cross-version compatibility between Pandas 2 and Pandas 3.
Expand Down Expand Up @@ -57,34 +65,34 @@ def __init__(self):
# Store initial values as defaults.
self._defaults = copy.deepcopy({k: v for k, v in vars(self).items() if not k.startswith('_')})

def get_option(self, option: str) -> str | bool | list:
def get_option(self, pat: str) -> str | bool | list:
"""
Get the option value for the specified option.

Parameters
----------
option: str
pat: str
The option you wish to get the values for.

Returns
-------
The option value.

"""
self._validate_option(option)
return getattr(self, option)
self._validate_option(pat)
return getattr(self, pat)

def set_option(
self,
option: str,
pat: str,
value: str | bool | list
) -> None:
"""
Set the option value for the specified option.

Parameters
----------
option: str
pat: str
The option you wish to set the value for.
value: str | bool | list
The option value.
Expand All @@ -94,10 +102,10 @@ def set_option(
None

"""
self._validate_option(option)
setattr(self, option, value)
self._validate_option(pat)
setattr(self, pat, value)

def reset_option(self, option: str | None = None) -> None:
def reset_option(self, pat: str | None = None) -> None:
"""
Restores the default value for the specified option. Restores default values for
all options if option is None.
Expand All @@ -108,19 +116,145 @@ def reset_option(self, option: str | None = None) -> None:

"""

if option is not None:
self._validate_option(option)
setattr(self, option, copy.deepcopy(self._defaults[option]))
if pat is not None:
self._validate_option(pat)
setattr(self, pat, copy.deepcopy(self._defaults[pat]))
else:
self.__init__()

def _validate_option(self, option: str) -> None:
def _validate_option(self, pat: str) -> None:
"""
Check whether string assigned to option is one of the configurable options in the Option class.

Parameters
----------
pat: str
The option you want to check.

Returns
-------
None

"""

if pat not in self._defaults:
raise ValueError(f"Invalid option(s): {pat}. Must be one of {list(self._defaults)}.")

def describe_option(self, pat: str = "", _print_desc=True) -> None | str:
"""
Print the description for one or more options.

Call with no arguments to get a listing for all options.

Parameters
----------
pat: str, default ""
The name of the option(s) you want described. Supplying an empty string will describe all options.
For multiple options, separate them with a pipe, |.
_print_desc: bool, default True
If True (default) the description(s) will be printed to stdout.
Otherwise, the description(s) will be returned as a string.

Returns
-------
None

Examples
--------

Describe information on a single option by passing the option name to `pat`.

.. testsetup::

import chainladder as cl

.. testcode::

cl.options.describe_option("AUTO_SPARSE")

.. testoutput::

AUTO_SPARSE : bool
Controls whether chainladder automatically converts a triangle's backing array to a sparse representation
when it would be memory-efficient to do so.
[default: True] [currently: True]

You can use a regexp to look up information on multiple options.

.. testcode::

cl.options.describe_option("AUTO_SPARSE|ARRAY_BACKEND")

.. testoutput::

ARRAY_BACKEND : str
The default array backend for chainladder.
[default: numpy] [currently: numpy]
AUTO_SPARSE : bool
Controls whether chainladder automatically converts a triangle's backing array to a sparse representation
when it would be memory-efficient to do so.
[default: True] [currently: True]

Setting `_print_desc=False` will return a string

.. testcode::

res = cl.options.describe_option("AUTO_SPARSE", _print_desc=False)
print(res)

.. testoutput::

"AUTO_SPARSE : bool\n Controls whether chainladder automatically converts a triangle's backing array
to a sparse representation\n when it would be memory-efficient to do so.\n
[default: True] [currently: True]"
"""
# Match option names against pat as a regex. Empty pattern matches all.
keys: list[str] = [key for key in self._defaults if re.search(pat, key)]

if pat and not keys:
raise ValueError(f"No option matching '{pat}'. Must be one of {list(self._defaults)}.")

# Extract class docstring and clean up indentation.
doc: str = inspect.cleandoc(self.__class__.__doc__)

# Holds the output.
lines: list[str] = []
for key in keys:
# Find a match for the specified option in the docstring.
match: Match[str] | None = re.search(
# Look for pattern matching structure of an attribute. e.g., the attribute name, followed by
# the type name, then the attribute description indented on the next line. Search will be
# split up into groups, specified by parentheses ().
pattern=rf"^{key}:\s*(\S+)\n((?:[ \t]+.+\n?)+)",
string=doc,
flags=re.MULTILINE # Needed to specify '^' as starting line anchor for each line.
)

# If there's a match, extract the attribute type and description.
if match:
type_hint: str = match.group(1) # Type annotation captured by (\S+)
description: str = inspect.cleandoc(match.group(2)) # Description block captured by ((?:[ \t]+.+\n?)+).
else:
type_hint: str = ""
description: str = "No description available."

if option not in self._defaults:
raise ValueError(f"Invalid option(s): {option}. Must be one of {list(self._defaults)}.")
# Indent the description relative to the attribute name.
indented: str = "\n ".join(description.splitlines())
# Extract the default option values.
default: str | bool | list = self._defaults[key]
# Extract the current option values.
current: str | bool | list = getattr(self, key)
# Write the option followed by a type hint.
header: str = f"{key} : {type_hint}" if type_hint else key
# Indent the description relative to the header.
lines.append(f"{header}\n {indented}\n [default: {default}] [currently: {current}]")

def describe_option(self, option: str) -> str:
pass
output: str = "\n".join(lines)
# Print output by default, otherwise return the string.
if _print_desc:
print(output)
return None
return output

options = Options()

Expand Down
124 changes: 123 additions & 1 deletion chainladder/utils/tests/test_utilities.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import pytest

import chainladder as cl
Expand All @@ -9,7 +10,11 @@
)
from chainladder.utils.utility_functions import date_delta_adjustment
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pytest import CaptureFixture
from pytest import MonkeyPatch



Expand Down Expand Up @@ -291,4 +296,121 @@ def test_reset_option_invalid() -> None:
None
"""
with pytest.raises(ValueError):
cl.options.reset_option('NOT_A_REAL_OPTION')
cl.options.reset_option('NOT_A_REAL_OPTION')


def test_describe_option(capsys: CaptureFixture[str]) -> None:
"""
Supply an option to cl.options.describe_option(). Attribute name, type, default/current
settings should be captured in the output.

Parameters
----------
capsys: CaptureFixture[str]
pytest built-in fixture to capture stdout

Returns
-------
None

"""
cl.options.describe_option('ARRAY_BACKEND')
captured = capsys.readouterr()
assert 'ARRAY_BACKEND : str' in captured.out
assert '[default: numpy]' in captured.out
assert '[currently: numpy]' in captured.out

def test_describe_option_multi(capsys) -> None:
"""
Supply two options to cl.options.describe_option(). Attribute names, types, default/current
settings should be captured in the output.

Parameters
----------
capsys: CaptureFixture[str]
pytest built-in fixture to capture stdout

Returns
-------
None

"""
cl.options.describe_option('ARRAY_BACKEND|AUTO_SPARSE')
captured = capsys.readouterr()
assert 'ARRAY_BACKEND : str' in captured.out
assert '[default: numpy]' in captured.out
assert '[currently: numpy]' in captured.out
assert 'AUTO_SPARSE : bool' in captured.out
assert '[default: True]' in captured.out
assert '[currently: True]' in captured.out
assert 'ARRAY_PRIORITY' not in captured.out


def test_describe_option_all(capsys) -> None:
"""
Execute cl.options.describe_option() with default arguments. All attributes
should be captured.

Parameters
----------
capsys: CaptureFixture[str]
pytest built-in fixture to capture stdout

Returns
-------
None

"""
cl.options.describe_option()
captured = capsys.readouterr()
for key in cl.Options()._defaults:
assert key in captured.out


def test_describe_option_return_string() -> None:
"""
Execute cl.options.desribe_option() with _print_desc=False. Should return a string. Check
if attribute info is in the string.

Returns
-------
None

"""
result = cl.options.describe_option('ARRAY_BACKEND', _print_desc=False)
assert isinstance(result, str)
assert 'ARRAY_BACKEND : str' in result
assert '[default: numpy]' in result
assert '[currently: numpy]' in result


def test_describe_option_no_docstring_match(monkeypatch: MonkeyPatch) -> None:
"""
When the class docstring has no entry for an option, describe_option should fall back
to 'No description available.' rather than raising an error.

Parameters
----------
monkeypatch: MonkeyPatch
The pytest built-in monkeypatch fixture.

Returns
-------
None
"""
monkeypatch.setattr(cl.Options, '__doc__', '')
result = cl.options.describe_option('ARRAY_BACKEND', _print_desc=False)
assert 'No description available.' in result


def test_describe_option_invalid() -> None:
"""
Execute cl.options.desribe_option() with an invalid argument. Should raise a ValueError.

Returns
-------
None

"""
with pytest.raises(ValueError):
cl.options.describe_option('NOT_A_REAL_OPTION')
Loading