diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cb6b9146..08a3369a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -5,9 +5,13 @@ ``` chainladder-python/ ├── chainladder/ # Main package -│ ├── __init__.py # Public API, global options, sample data loader +│ ├── __init__.py # Public API, package metadata │ ├── py.typed # PEP 561 marker (ships inline type hints) │ │ +│ ├── _config/ # Package-wide configuration +│ │ ├── options.py # Datetime constants, package options +│ │ └── deprecation.py # Deprecation utilities +│ │ │ ├── core/ # Triangle data structure │ │ ├── triangle.py # Triangle (the public-facing class) │ │ ├── base.py # TriangleBase (assembles all mixins) diff --git a/chainladder/__init__.py b/chainladder/__init__.py index 27b73652..28fbc9a2 100644 --- a/chainladder/__init__.py +++ b/chainladder/__init__.py @@ -7,9 +7,6 @@ This package strives to be minimalistic in needing its own API. The syntax mimics popular packages such as pandas for data manipulation and scikit-learn for model construction. An actuary that is already familiar with these tools will be able to pick up this package with ease. You will be able to save your mental energy for actual actuarial work. - -The __init__.py file governs package configuration, including datetime datatypes and precision, backend and ultimate -valuation defaults, as well as package metadata such as version number. """ # This Source Code Form is subject to the terms of the Mozilla Public @@ -17,446 +14,25 @@ # 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 -import warnings - from importlib.metadata import version -from typing import overload, TYPE_CHECKING -if TYPE_CHECKING: - from re import Match - from types import FrameType - from typing import Any, Literal -del TYPE_CHECKING del annotations -# Get the default datetime64 data type and precision, extracted from Pandas installation. -# Used for cross-version compatibility between Pandas 2 and Pandas 3. -__dt64_dtype__: str = pd.to_datetime(["2000-01-01"]).dtype.name -__dt64_unit__: str = np.datetime_data(__dt64_dtype__)[0] - -# Sentinel pattern used to mark a parameter as required and validate it. -_UNSET: Any = object() - -_option_warning: str = "The parameter 'option' is deprecated and will be removed in a future release. Use 'pat' instead." - -# Array backends slated for removal, mapped to the issue tracking each one. -# Selecting one of these (via set_option, ARRAY_PRIORITY, or set_backend, or by -# passing a Dask dataframe to the Triangle constructor) emits a -# DeprecationWarning. -_DEPRECATED_BACKENDS: dict[str, str] = { - "cupy": "https://github.com/casact/chainladder-python/issues/843", - "dask": "https://github.com/casact/chainladder-python/issues/842", -} - - -def _deprecated_backend_message(backend: str) -> str: - """Build the deprecation message for a soon-to-be-removed array backend.""" - return ( - f"The '{backend}' array backend is deprecated and will be removed in a " - f"future release. See {_DEPRECATED_BACKENDS[backend]}." - ) - - -# Tracks whether the one-time dask parallel-compute deprecation warning has -# already fired this process. The dask 'bag' code paths run automatically -# whenever dask is installed, so they warn at most once instead of on every -# operation. See issue #842. -_dask_parallel_warned: bool = False - - -def _warn_dask_parallel_deprecated(stacklevel: int = 2) -> None: - """ - Emit a one-time DeprecationWarning for dask-accelerated parallel compute. - - The dask ``bag`` scheduler is used as an optional parallel-compute engine - for the sparse backend (groupby aggregation, grouped-triangle arithmetic, - and incremental-to-cumulative conversion). It is deprecated alongside the - dask array backend and will be removed in a future release. Because these - paths run automatically on every qualifying operation, the warning fires at - most once per process to avoid flooding output. - - Parameters - ---------- - stacklevel: int - Forwarded to ``warnings.warn``. Defaults to 2 so the warning points at - the chainladder method that triggered the dask path. - - Returns - ------- - None - - """ - global _dask_parallel_warned - if _dask_parallel_warned: - return - _dask_parallel_warned = True - warnings.warn( - "Using dask for parallel computation is deprecated and will be removed " - f"in a future release. See {_DEPRECATED_BACKENDS['dask']}.", - DeprecationWarning, - stacklevel=stacklevel, - ) - - -@overload -def _resolve_pat( - pat: str | None, option: str | None, required: Literal[True] = ... -) -> str: ... -@overload -def _resolve_pat( - pat: str | None, option: str | None, required: Literal[False] -) -> str | None: ... - - -del overload - - -def _resolve_pat( - pat: str | None, option: str | None, required: bool = True -) -> str | None: - """ - Handles backward compatibility of 'options' parameter in options functions. Checks whether option or pat is - assigned a value and returns it. This value is meant to be assigned to the 'pat' parameter of the calling function. - - Once the 'options' parameter is fully removed, this function can be deleted or generalized as a backwards - compatibility tool to assist in the renaming and deprecation of function parameters. - - Parameters - ---------- - pat: str | None - The 'pat' parameter of the calling function. - option: str | None - The 'option' parameter of the calling function. - required: bool - Whether pat or option are required parameters in the calling function. Defaults to True. - - Returns - ------- - The value to be assigned to the 'pat' parameter of the calling function. - - """ - # Raise an error if the user accidentally assigns a value to both 'pat' and 'option'. - if pat is not None and option is not None: - raise TypeError("Cannot specify both 'pat' and 'option'.") - # Raise the deprecation warning if the user assigns a value to 'option'. - if option is not None: - warnings.warn(_option_warning, FutureWarning, stacklevel=3) - pat: str = option - # Raise an error if neither 'option' nor 'pat' is assigned. - if pat is None and required: - # Determine the name of the calling function. - err: str = "Unable to determine calling function." - frame: FrameType | None = inspect.currentframe() - if frame is None: - raise AttributeError(err) - else: - f_back: FrameType | None = frame.f_back - if f_back is None: - raise AttributeError(err) - else: - caller: str = f_back.f_code.co_name - raise TypeError(f"{caller}() missing required argument: 'pat'.") - return pat - - -class Options: - """ - Used to set defaults for array backend and datetime units. - - Attributes - ---------- - - ARRAY_BACKEND: str - The default array backend for chainladder. - 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. - ARRAY_PRIORITY: list - Determines which backend wins when two triangles with different backends interact, i.e., - when comparing or concatenating them. - ULT_VAL: str - The default ultimate valuation datetime, precision set to default of Pandas installation. - - """ - - def __init__(self): - self.ARRAY_BACKEND = "numpy" - self.AUTO_SPARSE = True - self.ARRAY_PRIORITY = ["dask", "sparse", "cupy", "numpy"] - self.ULT_VAL = str( - pd.Timestamp("2262-01-01") - pd.Timedelta(1, unit=__dt64_unit__) - ) - # 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, pat: str | None = None, *, option: str | None = None - ) -> str | bool | list: - """ - Get the option value for the specified option. - - .. deprecated:: 0.10.0 - The ``option`` parameter is deprecated; use ``pat`` instead. - - Parameters - ---------- - pat: str | None - The option you wish to get the values for. - option: str | None - The option you wish to get the values for. - - Returns - ------- - The option value. - - """ - pat: str = _resolve_pat(pat=pat, option=option) - self._validate_option(pat) - return getattr(self, pat) - - def set_option( - self, - pat: str | None = None, - value: str | bool | list = _UNSET, - *, - option: str | None = None, - ) -> None: - """ - Set the option value for the specified option. - - .. deprecated:: 0.10.0 - The ``option`` parameter is deprecated; use ``pat`` instead. - - Parameters - ---------- - pat: str | None - The option you wish to set the value for. - value: str | bool | list - The option value. - option: str | None - The option you wish to set the values for. - - Returns - ------- - None - - """ - pat: str = _resolve_pat(pat=pat, option=option) - self._validate_option(pat) - if value is _UNSET: - raise TypeError("set_option() missing required argument: 'value'.") - if pat == "ARRAY_BACKEND" and value in _DEPRECATED_BACKENDS: - warnings.warn( - _deprecated_backend_message(value), - DeprecationWarning, - stacklevel=2, - ) - elif pat == "ARRAY_PRIORITY" and isinstance(value, list): - # Only warn when a deprecated backend ('cupy' or 'dask') is - # prioritized ahead of a non-deprecated backend ('numpy' or - # 'sparse'), i.e. it would actually be selected over a supported - # backend. The position in the list determines precedence. - for backend in _DEPRECATED_BACKENDS: - if backend not in value: - continue - backend_index = value.index(backend) - if any( - supported in value and value.index(supported) > backend_index - for supported in ("numpy", "sparse") - ): - warnings.warn( - _deprecated_backend_message(backend), - DeprecationWarning, - stacklevel=2, - ) - setattr(self, pat, value) - - def reset_option( - self, pat: str | None = None, *, option: str | None = None - ) -> None: - """ - Restores the default value for the specified option. Restores default values for - all options if pat is None. - - .. deprecated:: 0.10.0 - The ``option`` parameter is deprecated; use ``pat`` instead. - - Parameters - ---------- - pat: str | None - The option you wish to reset the value for. - option: str | None - The option you wish to reset the value for. - - Returns - ------- - None - - """ - pat = _resolve_pat(pat=pat, option=option, required=False) - if pat is not None: - self._validate_option(pat) - setattr(self, pat, copy.deepcopy(self._defaults[pat])) - else: - self.__init__() - - def _validate_option(self, pat: str) -> None: - """ - Check whether string assigned to option is one of the configurable options in the Options 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: bool = 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 - ------- - The description for the specified option(s) if _print_desc=False, otherwise, `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 - 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] - """ - # Match option names against pat as a regex. Empty pattern matches all. - try: - keys = [key for key in self._defaults if re.search(pat, key)] - except re.error: - raise ValueError(f"'{pat}' is not a valid regular expression.") - - 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"^{re.escape(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." - - # 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}]" - ) - - output: str = "\n".join(lines) - # Print output by default, otherwise return the string. - if _print_desc: - print(output) - return None - return output - - -options = Options() - +from chainladder._config import ( # noqa (API import) + __dt64_dtype__, + __dt64_unit__, + Options, + options, +) +# noinspection PyProtectedMember +from chainladder._config import ( # noqa (API import) + _DEPRECATED_BACKENDS, + _deprecated_backend_message, + _dask_parallel_state, + _warn_dask_parallel_deprecated, +) from chainladder.utils import ( # noqa (API import) WeightedRegression, TriangleWeight, diff --git a/chainladder/_config/__init__.py b/chainladder/_config/__init__.py new file mode 100644 index 00000000..c62b7af4 --- /dev/null +++ b/chainladder/_config/__init__.py @@ -0,0 +1,27 @@ +""" +Governs configuration, options, and deprecations. +""" + +from chainladder._config.deprecation import ( + _DEPRECATED_BACKENDS, + _deprecated_backend_message, + _dask_parallel_state, + _warn_dask_parallel_deprecated, # noqa (API import) +) +from chainladder._config.options import ( + __dt64_dtype__, + __dt64_unit__, + Options, + options, +) + +__all__: list[str] = [ + "__dt64_dtype__", + "__dt64_unit__", + "Options", + "options", + "_DEPRECATED_BACKENDS", + "_deprecated_backend_message", + "_dask_parallel_state", + "_warn_dask_parallel_deprecated", +] diff --git a/chainladder/_config/deprecation.py b/chainladder/_config/deprecation.py new file mode 100644 index 00000000..44843ef3 --- /dev/null +++ b/chainladder/_config/deprecation.py @@ -0,0 +1,149 @@ +""" +Utilities for deprecating chainladder features. +""" + +# 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 inspect +import warnings + +from typing import overload, TYPE_CHECKING + +if TYPE_CHECKING: + from types import FrameType + from typing import Literal +del TYPE_CHECKING +del annotations + + +# Array backends slated for removal, mapped to the issue tracking each one. +# Selecting one of these (via set_option, ARRAY_PRIORITY, or set_backend, or by +# passing a Dask dataframe to the Triangle constructor) emits a +# DeprecationWarning. +_DEPRECATED_BACKENDS: dict[str, str] = { + "cupy": "https://github.com/casact/chainladder-python/issues/843", + "dask": "https://github.com/casact/chainladder-python/issues/842", +} + + +def _deprecated_backend_message(backend: str) -> str: + """Build the deprecation message for a soon-to-be-removed array backend.""" + return ( + f"The '{backend}' array backend is deprecated and will be removed in a " + f"future release. See {_DEPRECATED_BACKENDS[backend]}." + ) + + +class _DaskParallelWarningState: + """ + Tracks whether the one-time dask parallel-compute deprecation warning has + already fired this process. The dask 'bag' code paths run automatically + whenever dask is installed, so they warn at most once instead of on every + operation. See issue #842. + """ + + def __init__(self) -> None: + self.warned: bool = False + + +_dask_parallel_state = _DaskParallelWarningState() + + +def _warn_dask_parallel_deprecated(stacklevel: int = 2) -> None: + """ + Emit a one-time DeprecationWarning for dask-accelerated parallel compute. + + The dask ``bag`` scheduler is used as an optional parallel-compute engine + for the sparse backend (groupby aggregation, grouped-triangle arithmetic, + and incremental-to-cumulative conversion). It is deprecated alongside the + dask array backend and will be removed in a future release. Because these + paths run automatically on every qualifying operation, the warning fires at + most once per process to avoid flooding output. + + Parameters + ---------- + stacklevel: int + Forwarded to ``warnings.warn``. Defaults to 2 so the warning points at + the chainladder method that triggered the dask path. + + Returns + ------- + None + + """ + if _dask_parallel_state.warned: + return + _dask_parallel_state.warned = True + warnings.warn( + "Using dask for parallel computation is deprecated and will be removed " + f"in a future release. See {_DEPRECATED_BACKENDS['dask']}.", + DeprecationWarning, + stacklevel=stacklevel, + ) + + +_option_warning: str = "The parameter 'option' is deprecated and will be removed in a future release. Use 'pat' instead." + + +@overload +def _resolve_pat( + pat: str | None, option: str | None, required: Literal[True] = ... +) -> str: ... +@overload +def _resolve_pat( + pat: str | None, option: str | None, required: Literal[False] +) -> str | None: ... + + +del overload + + +def _resolve_pat( + pat: str | None, option: str | None, required: bool = True +) -> str | None: + """ + Handles backward compatibility of 'options' parameter in options functions. Checks whether option or pat is + assigned a value and returns it. This value is meant to be assigned to the 'pat' parameter of the calling function. + + Once the 'options' parameter is fully removed, this function can be deleted or generalized as a backwards + compatibility tool to assist in the renaming and deprecation of function parameters. + + Parameters + ---------- + pat: str | None + The 'pat' parameter of the calling function. + option: str | None + The 'option' parameter of the calling function. + required: bool + Whether pat or option are required parameters in the calling function. Defaults to True. + + Returns + ------- + The value to be assigned to the 'pat' parameter of the calling function. + + """ + # Raise an error if the user accidentally assigns a value to both 'pat' and 'option'. + if pat is not None and option is not None: + raise TypeError("Cannot specify both 'pat' and 'option'.") + # Raise the deprecation warning if the user assigns a value to 'option'. + if option is not None: + warnings.warn(_option_warning, FutureWarning, stacklevel=3) + pat: str = option + # Raise an error if neither 'option' nor 'pat' is assigned. + if pat is None and required: + # Determine the name of the calling function. + err: str = "Unable to determine calling function." + frame: FrameType | None = inspect.currentframe() + if frame is None: + raise AttributeError(err) + else: + f_back: FrameType | None = frame.f_back + if f_back is None: + raise AttributeError(err) + else: + caller: str = f_back.f_code.co_name + raise TypeError(f"{caller}() missing required argument: 'pat'.") + return pat diff --git a/chainladder/_config/options.py b/chainladder/_config/options.py new file mode 100644 index 00000000..d125a1f8 --- /dev/null +++ b/chainladder/_config/options.py @@ -0,0 +1,334 @@ +""" +Package-wide options. Governs backend behavior and default datetime values. +""" + +# 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 +import warnings + +from typing import TYPE_CHECKING + +from chainladder._config.deprecation import ( + _DEPRECATED_BACKENDS, + _deprecated_backend_message, + _resolve_pat, +) + +if TYPE_CHECKING: + from re import Match + from typing import Any +del TYPE_CHECKING +del annotations + + +# Get the default datetime64 data type and precision, extracted from Pandas installation. +# Used for cross-version compatibility between Pandas 2 and Pandas 3. +__dt64_dtype__: str = pd.to_datetime(["2000-01-01"]).dtype.name +__dt64_unit__: str = np.datetime_data(__dt64_dtype__)[0] + +# Sentinel pattern used to mark a parameter as required and validate it. +_UNSET: Any = object() + + +class Options: + """ + Used to set defaults for array backend and datetime units. + + Attributes + ---------- + + ARRAY_BACKEND: str + The default array backend for chainladder. + 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. + ARRAY_PRIORITY: list + Determines which backend wins when two triangles with different backends interact, i.e., + when comparing or concatenating them. + ULT_VAL: str + The default ultimate valuation datetime, precision set to default of Pandas installation. + + """ + + def __init__(self): + self.ARRAY_BACKEND = "numpy" + self.AUTO_SPARSE = True + self.ARRAY_PRIORITY = ["dask", "sparse", "cupy", "numpy"] + self.ULT_VAL = str( + pd.Timestamp("2262-01-01") - pd.Timedelta(1, unit=__dt64_unit__) # noqa + ) + # 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, pat: str | None = None, *, option: str | None = None + ) -> str | bool | list: + """ + Get the option value for the specified option. + + .. deprecated:: 0.10.0 + The ``option`` parameter is deprecated; use ``pat`` instead. + + Parameters + ---------- + pat: str | None + The option you wish to get the values for. + option: str | None + The option you wish to get the values for. + + Returns + ------- + The option value. + + """ + pat: str = _resolve_pat(pat=pat, option=option) + self._validate_option(pat) + return getattr(self, pat) + + def set_option( + self, + pat: str | None = None, + value: str | bool | list = _UNSET, + *, + option: str | None = None, + ) -> None: + """ + Set the option value for the specified option. + + .. deprecated:: 0.10.0 + The ``option`` parameter is deprecated; use ``pat`` instead. + + Parameters + ---------- + pat: str | None + The option you wish to set the value for. + value: str | bool | list + The option value. + option: str | None + The option you wish to set the values for. + + Returns + ------- + None + + """ + pat: str = _resolve_pat(pat=pat, option=option) + self._validate_option(pat) + if value is _UNSET: + raise TypeError("set_option() missing required argument: 'value'.") + if ( + pat == "ARRAY_BACKEND" + and isinstance(value, str) + and value in _DEPRECATED_BACKENDS + ): + warnings.warn( + _deprecated_backend_message(value), + DeprecationWarning, + stacklevel=2, + ) + elif pat == "ARRAY_PRIORITY" and isinstance(value, list): + # Only warn when a deprecated backend ('cupy' or 'dask') is + # prioritized ahead of a non-deprecated backend ('numpy' or + # 'sparse'), i.e. it would actually be selected over a supported + # backend. The position in the list determines precedence. + for backend in _DEPRECATED_BACKENDS: + if backend not in value: + continue + backend_index = value.index(backend) + if any( + supported in value and value.index(supported) > backend_index + for supported in ("numpy", "sparse") + ): + warnings.warn( + _deprecated_backend_message(backend), + DeprecationWarning, + stacklevel=2, + ) + setattr(self, pat, value) + + def reset_option( + self, pat: str | None = None, *, option: str | None = None + ) -> None: + """ + Restores the default value for the specified option. Restores default values for + all options if pat is None. + + .. deprecated:: 0.10.0 + The ``option`` parameter is deprecated; use ``pat`` instead. + + Parameters + ---------- + pat: str | None + The option you wish to reset the value for. + option: str | None + The option you wish to reset the value for. + + Returns + ------- + None + + """ + pat = _resolve_pat(pat=pat, option=option, required=False) + if pat is not None: + self._validate_option(pat) + setattr(self, pat, copy.deepcopy(self._defaults[pat])) + else: + self.__init__() + + def _validate_option(self, pat: str) -> None: + """ + Check whether string assigned to option is one of the configurable options in the Options 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: bool = 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 + ------- + The description for the specified option(s) if _print_desc=False, otherwise, `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 + 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] + """ + # Match option names against pat as a regex. Empty pattern matches all. + try: + keys = [key for key in self._defaults if re.search(pat, key)] + except re.error: + raise ValueError(f"'{pat}' is not a valid regular expression.") + + 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"^{re.escape(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." + + # 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}]" + ) + + output: str = "\n".join(lines) + # Print output by default, otherwise return the string. + if _print_desc: + print(output) + return None + return output + + +options = Options() diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 5a0b8a42..ed54981a 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -9,16 +9,10 @@ import numpy as np import pandas as pd -from chainladder import ( - __dt64_unit__ -) -from chainladder.utils.utility_functions import date_delta_adjustment +from chainladder import __dt64_unit__ + from chainladder.utils.data._manifest import SAMPLES -from chainladder.utils.utility_functions import ( - date_delta_adjustment, - maximum, - minimum -) +from chainladder.utils.utility_functions import date_delta_adjustment, maximum, minimum from pathlib import Path from typing import TYPE_CHECKING @@ -28,6 +22,7 @@ from pytest import MonkeyPatch from chainladder import Triangle + class _FakeBag: """ Minimal stand-in for a dask bag that runs the mapped function eagerly. @@ -59,7 +54,6 @@ def from_sequence(seq): def test_triangle_json_io(clrd): - xp = clrd.get_array_module() clrd2 = cl.read_json(clrd.to_json(), array_backend=clrd.array_backend) assert clrd == clrd2 assert np.all(clrd.kdims == clrd2.kdims) @@ -153,7 +147,7 @@ def test_concat(clrd): ) -def test_model_diagnostics_erorr(raa,atol): +def test_model_diagnostics_erorr(raa, atol): with pytest.raises(ValueError): cl.model_diagnostics(raa) dev = cl.Development().fit_transform(raa) @@ -161,52 +155,62 @@ def test_model_diagnostics_erorr(raa,atol): emerg = est.full_expectation_.cum_to_incr() md = cl.model_diagnostics(est) assert np.allclose( - md['Run Off 1'].values, - emerg[emerg.valuation.year==1991].latest_diagonal.values, + md["Run Off 1"].values, + emerg[emerg.valuation.year == 1991].latest_diagonal.values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - md['Year Incremental'].values, + md["Year Incremental"].values, raa.cum_to_incr().latest_diagonal.values, atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - md['LDF'].values.flatten()[:0:-1], + md["LDF"].values.flatten()[:0:-1], dev.ldf_.values.flatten(), atol=atol, - equal_nan=True + equal_nan=True, ) assert np.allclose( - md['CDF'].values.flatten()[:0:-1], + md["CDF"].values.flatten()[:0:-1], dev.cdf_.values.flatten(), atol=atol, - equal_nan=True + equal_nan=True, ) -def test_model_diagnostics_groupby(prism,atol): +def test_model_diagnostics_groupby(prism, atol): dev = cl.Development().fit(prism["Incurred"].sum()) est = cl.Chainladder().fit(dev.transform(prism["Incurred"])) - lhs = cl.model_diagnostics(est,groupby=['Line']) - rhs = cl.model_diagnostics(cl.Chainladder().fit(dev.transform(prism["Incurred"].groupby('Line').sum()))) - assert np.allclose(lhs['Ultimate'].values,rhs['Ultimate'].values,atol=atol,equal_nan=True) - assert np.allclose(np.nan_to_num(lhs['IBNR'].values),np.nan_to_num(rhs['IBNR'].values),atol=atol,equal_nan=True) + lhs = cl.model_diagnostics(est, groupby=["Line"]) + rhs = cl.model_diagnostics( + cl.Chainladder().fit(dev.transform(prism["Incurred"].groupby("Line").sum())) + ) + assert np.allclose( + lhs["Ultimate"].values, rhs["Ultimate"].values, atol=atol, equal_nan=True + ) + assert np.allclose( + np.nan_to_num(lhs["IBNR"].values), + np.nan_to_num(rhs["IBNR"].values), + atol=atol, + equal_nan=True, + ) def test_concat_immutability(raa): u = cl.Chainladder().fit(raa).ultimate_ - l = raa.latest_diagonal - u.columns = l.columns + latest = raa.latest_diagonal + u.columns = latest.columns u_new = copy.deepcopy(u) - cl.concat((l, u), axis=3) + cl.concat((latest, u), axis=3) assert u == u_new def test_to_pickle_read_pickle(raa): import tempfile import os + dev = cl.Development(average="simple", n_periods=4).fit(raa) fd, path = tempfile.mkstemp(suffix=".pkl") os.close(fd) @@ -215,20 +219,24 @@ def test_to_pickle_read_pickle(raa): restored = cl.read_pickle(path) assert restored.average == dev.average assert restored.n_periods == dev.n_periods - np.testing.assert_array_almost_equal( - restored.ldf_.values, dev.ldf_.values - ) + np.testing.assert_array_almost_equal(restored.ldf_.values, dev.ldf_.values) finally: os.remove(path) def test_maximum_minimum_1(raa): - ult_vol = cl.Chainladder().fit( - cl.Development(average="volume").fit_transform(raa) - ).ultimate_ - ult_sim = cl.Chainladder().fit( - cl.Development(average="simple").fit_transform(raa) - ).ultimate_ + ult_vol = ( + cl + .Chainladder() + .fit(cl.Development(average="volume").fit_transform(raa)) + .ultimate_ + ) + ult_sim = ( + cl + .Chainladder() + .fit(cl.Development(average="simple").fit_transform(raa)) + .ultimate_ + ) high_side = maximum(ult_vol, ult_sim) low_side = minimum(ult_vol, ult_sim) np.testing.assert_array_almost_equal( @@ -246,6 +254,7 @@ def test_invalid_sample() -> None: with pytest.raises(ValueError): cl.load_sample(key="not_a_real_sample_38473743") + def test_load_sample() -> None: """ Tests whether every sample data set declared in the manifest loads. @@ -282,7 +291,13 @@ def test_list_samples() -> None: # One row per manifest entry, indexed by sample name. assert df.index.name == "name" assert set(df.index) == set(SAMPLES) - assert {"index", "columns", "cumulative", "origin_grain", "development_grain"} <= set(df.columns) + assert { + "index", + "columns", + "cumulative", + "origin_grain", + "development_grain", + } <= set(df.columns) # The fast path skips loading data and therefore omits the grain columns. fast = cl.list_samples(include_grain=False) @@ -372,15 +387,17 @@ def test_load_sample_clrd2025() -> None: tri = cl.load_sample("clrd2025") # Six LOBs in the CAS Schedule P refresh. - expected_lobs = { - "comauto", "medmal", "othliab", "ppauto", "prodliab", "wkcomp" - } + expected_lobs = {"comauto", "medmal", "othliab", "ppauto", "prodliab", "wkcomp"} assert set(tri.index["LOB"].unique()) == expected_lobs # Modern column names (IncurredLosses rather than IncurLoss). expected_columns = { - "IncurredLosses", "CumPaidLoss", "BulkLoss", - "EarnedPremDIR", "EarnedPremCeded", "EarnedPremNet", + "IncurredLosses", + "CumPaidLoss", + "BulkLoss", + "EarnedPremDIR", + "EarnedPremCeded", + "EarnedPremNet", } assert set(str(c) for c in tri.vdims) == expected_columns @@ -388,6 +405,7 @@ def test_load_sample_clrd2025() -> None: assert str(tri.origin.min()) == "1998" assert "2007" in [str(o) for o in tri.origin] + def test_date_delta_adjustment() -> None: """ Tests the date adjustment depending on Pandas default precision, nanosecond for Pandas 2, microsecond for Pandas 3. @@ -401,6 +419,7 @@ def test_date_delta_adjustment() -> None: ) assert result == expected + def test_read_pickle_triangle(raa: Triangle, tmp_path: Path) -> None: """ Create a triangle, dump a pickle of it, and then read it back in. The ingested pickle should result @@ -424,11 +443,7 @@ def test_read_pickle_triangle(raa: Triangle, tmp_path: Path) -> None: assert cl.read_pickle(str(pkl_path)) == raa -def test_triangle_to_pickle( - raa: Triangle, - clrd: Triangle, - tmp_path: Path -) -> None: +def test_triangle_to_pickle(raa: Triangle, clrd: Triangle, tmp_path: Path) -> None: """ Dump a pickle of a triangle and read it back in. The read-in triangle should equal the one that was dumped. @@ -653,10 +668,9 @@ def test_reset_option() -> None: original_array_priority = cl.options.ARRAY_PRIORITY try: - - cl.options.set_option('ARRAY_BACKEND', 'sparse') - cl.options.set_option('AUTO_SPARSE', False) - cl.options.set_option('ARRAY_PRIORITY', ['sparse', 'dask', 'numpy', 'cupy']) + cl.options.set_option("ARRAY_BACKEND", "sparse") + cl.options.set_option("AUTO_SPARSE", False) + cl.options.set_option("ARRAY_PRIORITY", ["sparse", "dask", "numpy", "cupy"]) cl.options.reset_option() @@ -665,10 +679,10 @@ def test_reset_option() -> None: assert cl.options.ARRAY_PRIORITY == original_array_priority finally: - # Manual reset in case of test failure. - cl.options.set_option('ARRAY_BACKEND', original_backend) - cl.options.set_option('AUTO_SPARSE', original_auto_sparse) - cl.options.set_option('ARRAY_PRIORITY', original_array_priority) + # Manual reset in case of test failure. + cl.options.set_option("ARRAY_BACKEND", original_backend) + cl.options.set_option("AUTO_SPARSE", original_auto_sparse) + cl.options.set_option("ARRAY_PRIORITY", original_array_priority) def test_options_defaults() -> None: @@ -682,7 +696,7 @@ def test_options_defaults() -> None: """ options = cl.Options() assert options.ARRAY_BACKEND == "numpy" - assert options.AUTO_SPARSE == True + assert options.AUTO_SPARSE assert options.ARRAY_PRIORITY == ["dask", "sparse", "cupy", "numpy"] assert isinstance(options.ULT_VAL, str) @@ -696,10 +710,10 @@ def test_get_option() -> None: None """ - assert cl.options.get_option('ARRAY_BACKEND') == cl.options.ARRAY_BACKEND - assert cl.options.get_option('AUTO_SPARSE') == cl.options.AUTO_SPARSE - assert cl.options.get_option('ARRAY_PRIORITY') == cl.options.ARRAY_PRIORITY - assert cl.options.get_option('ULT_VAL') == cl.options.ULT_VAL + assert cl.options.get_option("ARRAY_BACKEND") == cl.options.ARRAY_BACKEND + assert cl.options.get_option("AUTO_SPARSE") == cl.options.AUTO_SPARSE + assert cl.options.get_option("ARRAY_PRIORITY") == cl.options.ARRAY_PRIORITY + assert cl.options.get_option("ULT_VAL") == cl.options.ULT_VAL def test_set_option_consistency() -> None: @@ -712,12 +726,13 @@ def test_set_option_consistency() -> None: """ try: - cl.options.set_option('ARRAY_BACKEND', 'sparse') - assert cl.options.ARRAY_BACKEND == 'sparse' - assert cl.options.get_option('ARRAY_BACKEND') == 'sparse' + cl.options.set_option("ARRAY_BACKEND", "sparse") + assert cl.options.ARRAY_BACKEND == "sparse" + assert cl.options.get_option("ARRAY_BACKEND") == "sparse" finally: # Reset the options to default if the test fails. - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") + def test_reset_single_option() -> None: """ @@ -728,11 +743,11 @@ def test_reset_single_option() -> None: None """ - cl.options.set_option('ARRAY_BACKEND', 'sparse') - assert cl.options.ARRAY_BACKEND == 'sparse' + cl.options.set_option("ARRAY_BACKEND", "sparse") + assert cl.options.ARRAY_BACKEND == "sparse" # Return backend to original state. - cl.options.reset_option('ARRAY_BACKEND') - assert cl.options.ARRAY_BACKEND == 'numpy' + cl.options.reset_option("ARRAY_BACKEND") + assert cl.options.ARRAY_BACKEND == "numpy" def test_reset_option_invalid() -> None: @@ -744,7 +759,7 @@ 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_set_option_cupy_backend_deprecated() -> None: @@ -757,9 +772,9 @@ def test_set_option_cupy_backend_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="cupy"): - cl.options.set_option('ARRAY_BACKEND', 'cupy') + cl.options.set_option("ARRAY_BACKEND", "cupy") finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_set_option_dask_backend_deprecated() -> None: @@ -772,9 +787,9 @@ def test_set_option_dask_backend_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="dask"): - cl.options.set_option('ARRAY_BACKEND', 'dask') + cl.options.set_option("ARRAY_BACKEND", "dask") finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_set_option_cupy_priority_deprecated() -> None: @@ -788,9 +803,9 @@ def test_set_option_cupy_priority_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="cupy"): - cl.options.set_option('ARRAY_PRIORITY', ['cupy', 'numpy', 'sparse', 'dask']) + cl.options.set_option("ARRAY_PRIORITY", ["cupy", "numpy", "sparse", "dask"]) finally: - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_PRIORITY") def test_set_option_dask_priority_deprecated() -> None: @@ -804,9 +819,9 @@ def test_set_option_dask_priority_deprecated() -> None: """ try: with pytest.warns(DeprecationWarning, match="dask"): - cl.options.set_option('ARRAY_PRIORITY', ['dask', 'numpy', 'sparse', 'cupy']) + cl.options.set_option("ARRAY_PRIORITY", ["dask", "numpy", "sparse", "cupy"]) finally: - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_PRIORITY") def test_set_option_deprecated_priority_last_no_warning(recwarn) -> None: @@ -820,10 +835,10 @@ def test_set_option_deprecated_priority_last_no_warning(recwarn) -> None: None """ try: - cl.options.set_option('ARRAY_PRIORITY', ['numpy', 'sparse', 'dask', 'cupy']) + cl.options.set_option("ARRAY_PRIORITY", ["numpy", "sparse", "dask", "cupy"]) assert not [w for w in recwarn if issubclass(w.category, DeprecationWarning)] finally: - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_PRIORITY") def test_set_option_supported_backend_no_warning(recwarn) -> None: @@ -837,12 +852,12 @@ def test_set_option_supported_backend_no_warning(recwarn) -> None: None """ try: - cl.options.set_option('ARRAY_BACKEND', 'sparse') - cl.options.set_option('ARRAY_PRIORITY', ['sparse', 'numpy']) + cl.options.set_option("ARRAY_BACKEND", "sparse") + cl.options.set_option("ARRAY_PRIORITY", ["sparse", "numpy"]) assert not [w for w in recwarn if issubclass(w.category, DeprecationWarning)] finally: - cl.options.reset_option('ARRAY_BACKEND') - cl.options.reset_option('ARRAY_PRIORITY') + cl.options.reset_option("ARRAY_BACKEND") + cl.options.reset_option("ARRAY_PRIORITY") def test_set_backend_cupy_deprecated(clrd) -> None: @@ -855,9 +870,10 @@ def test_set_backend_cupy_deprecated(clrd) -> None: None """ with pytest.warns(DeprecationWarning, match="cupy") as record: - clrd.set_backend('cupy', deep=True) + clrd.set_backend("cupy", deep=True) cupy_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "cupy" in str(w.message) ] # A single warning should fire at the user's call site, not once per @@ -877,14 +893,15 @@ def test_set_backend_dask_deprecated(clrd) -> None: """ with pytest.warns(DeprecationWarning, match="dask") as record: try: - clrd.set_backend('dask', deep=True) + clrd.set_backend("dask", deep=True) except Exception: # The actual conversion can fail when the optional 'dask' # dependency is not installed; we only care that the deprecation # warning fired at the public entry point. pass dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 @@ -904,6 +921,7 @@ def test_triangle_dask_input_deprecated() -> None: ------- None """ + class _FakeDaskFrame(pd.DataFrame): @property def _constructor(self): @@ -926,7 +944,8 @@ def _constructor(self): columns="values", ) dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 @@ -944,6 +963,7 @@ def test_triangle_pandas_subclass_no_dask_warning(recwarn) -> None: ------- None """ + class _PandasSubclass(pd.DataFrame): @property def _constructor(self): @@ -961,7 +981,8 @@ def _constructor(self): columns="values", ) dask_warnings = [ - w for w in recwarn + w + for w in recwarn if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert dask_warnings == [] @@ -978,19 +999,20 @@ def test_dask_parallel_deprecated_warns_once() -> None: ------- None """ - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False try: with warnings.catch_warnings(record=True) as record: warnings.simplefilter("always") cl._warn_dask_parallel_deprecated() cl._warn_dask_parallel_deprecated() dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 finally: - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False def test_dask_parallel_groupby_deprecated(monkeypatch: MonkeyPatch) -> None: @@ -1004,19 +1026,20 @@ def test_dask_parallel_groupby_deprecated(monkeypatch: MonkeyPatch) -> None: ------- None """ - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False monkeypatch.setattr("chainladder.core.pandas.db", _FakeDaskBag) sparse_clrd = cl.load_sample("clrd").set_backend("sparse") try: with pytest.warns(DeprecationWarning, match="dask") as record: sparse_clrd.groupby("LOB").sum() dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 finally: - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False def test_dask_parallel_incr_to_cum_deprecated(monkeypatch: MonkeyPatch) -> None: @@ -1030,24 +1053,25 @@ def test_dask_parallel_incr_to_cum_deprecated(monkeypatch: MonkeyPatch) -> None: ------- None """ - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False monkeypatch.setattr("chainladder.core.triangle.db", _FakeDaskBag) incremental_sparse = cl.load_sample("raa").cum_to_incr().set_backend("sparse") try: with pytest.warns(DeprecationWarning, match="dask") as record: incremental_sparse.incr_to_cum() dask_warnings = [ - w for w in record + w + for w in record if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert len(dask_warnings) == 1 finally: - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False def test_dask_parallel_numpy_groupby_no_warning( - monkeypatch: MonkeyPatch, - recwarn, + monkeypatch: MonkeyPatch, + recwarn, ) -> None: """ The dask 'bag' parallel-compute path is gated on the sparse backend, so a @@ -1058,18 +1082,19 @@ def test_dask_parallel_numpy_groupby_no_warning( ------- None """ - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False monkeypatch.setattr("chainladder.core.pandas.db", _FakeDaskBag) numpy_clrd = cl.load_sample("clrd").set_backend("numpy") try: numpy_clrd.groupby("LOB").sum() dask_warnings = [ - w for w in recwarn + w + for w in recwarn if issubclass(w.category, DeprecationWarning) and "dask" in str(w.message) ] assert dask_warnings == [] finally: - cl._dask_parallel_warned = False + cl._dask_parallel_state.warned = False def test_describe_option(capsys: CaptureFixture[str]) -> None: @@ -1087,11 +1112,12 @@ def test_describe_option(capsys: CaptureFixture[str]) -> None: None """ - cl.options.describe_option('ARRAY_BACKEND') + 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 + 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: """ @@ -1108,15 +1134,15 @@ def test_describe_option_multi(capsys) -> None: None """ - cl.options.describe_option('ARRAY_BACKEND|AUTO_SPARSE') + 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 + 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: @@ -1150,11 +1176,11 @@ def test_describe_option_return_string() -> None: None """ - result = cl.options.describe_option('ARRAY_BACKEND', _print_desc=False) + 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 + assert "ARRAY_BACKEND : str" in result + assert "[default: numpy]" in result + assert "[currently: numpy]" in result def test_deprecated_option_kwarg_warns() -> None: @@ -1162,13 +1188,13 @@ def test_deprecated_option_kwarg_warns() -> None: Passing option= to get_option or set_option should emit a FutureWarning. """ with pytest.warns(FutureWarning, match="'option'"): - cl.options.get_option(option='ARRAY_BACKEND') + cl.options.get_option(option="ARRAY_BACKEND") try: with pytest.warns(FutureWarning, match="'option'"): - cl.options.set_option(option='ARRAY_BACKEND', value='numpy') + cl.options.set_option(option="ARRAY_BACKEND", value="numpy") finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_deprecated_option_kwarg_reset_option_warns() -> None: @@ -1176,12 +1202,12 @@ def test_deprecated_option_kwarg_reset_option_warns() -> None: Passing option= to reset_option should emit a FutureWarning. """ try: - cl.options.set_option('ARRAY_BACKEND', 'sparse') + cl.options.set_option("ARRAY_BACKEND", "sparse") with pytest.warns(FutureWarning, match="'option'"): - cl.options.reset_option(option='ARRAY_BACKEND') - assert cl.options.ARRAY_BACKEND == 'numpy' + cl.options.reset_option(option="ARRAY_BACKEND") + assert cl.options.ARRAY_BACKEND == "numpy" finally: - cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option("ARRAY_BACKEND") def test_get_option_missing_pat_raises() -> None: @@ -1206,9 +1232,9 @@ def test_describe_option_no_docstring_match(monkeypatch: MonkeyPatch) -> None: ------- None """ - monkeypatch.setattr(cl.Options, '__doc__', '') - result = cl.options.describe_option('ARRAY_BACKEND', _print_desc=False) - assert 'No description available.' in result + 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: @@ -1221,7 +1247,7 @@ def test_describe_option_invalid() -> None: """ with pytest.raises(ValueError): - cl.options.describe_option('NOT_A_REAL_OPTION') + cl.options.describe_option("NOT_A_REAL_OPTION") def test_both_pat_and_option_raises() -> None: @@ -1229,7 +1255,7 @@ def test_both_pat_and_option_raises() -> None: Passing both pat and option to get_option, set_option, or reset_option should raise TypeError. """ with pytest.raises(TypeError, match="Cannot specify both"): - cl.options.get_option(pat='ARRAY_BACKEND', option='ARRAY_BACKEND') + cl.options.get_option(pat="ARRAY_BACKEND", option="ARRAY_BACKEND") def test_set_option_missing_value_raises() -> None: @@ -1237,7 +1263,7 @@ def test_set_option_missing_value_raises() -> None: Calling set_option with pat but no value should raise TypeError. """ with pytest.raises(TypeError, match="missing required argument"): - cl.options.set_option('ARRAY_BACKEND') + cl.options.set_option("ARRAY_BACKEND") def test_describe_option_invalid_regex() -> None: @@ -1245,4 +1271,4 @@ def test_describe_option_invalid_regex() -> None: Passing a malformed regular expression to describe_option should raise ValueError. """ with pytest.raises(ValueError, match="not a valid regular expression"): - cl.options.describe_option('[') + cl.options.describe_option("[") diff --git a/pyproject.toml b/pyproject.toml index 695bca68..23a1c907 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,7 +157,6 @@ select = ["E2", "E4", "E7", "E9", "F", "B018", "UP034", "N802"] "chainladder/utils/dask.py" = ["E722", "E741"] "chainladder/utils/sparse.py" = ["E231", "E251", "E721"] "chainladder/utils/tests/test_sparse.py" = ["E231", "E251", "N802"] -"chainladder/utils/tests/test_utilities.py" = ["E225", "E231", "E712", "E741", "F811", "F841"] "chainladder/utils/triangle_weight.py" = ["E227", "E231", "E265", "F401"] "chainladder/utils/utility_functions.py" = ["E226", "E227", "E231", "E251", "E252", "E721", "F401", "N802"] "chainladder/utils/weighted_regression.py" = ["E227", "E231", "E252", "N802"]