From f92a20d0edd20b96f6edaed2f078d3f87bf07845 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 28 May 2026 13:20:22 -0500 Subject: [PATCH 1/5] FEAT: Implement Options.describe_option. --- chainladder/__init__.py | 164 +++++++++++++++++++--- chainladder/utils/tests/test_utilities.py | 105 +++++++++++++- 2 files changed, 251 insertions(+), 18 deletions(-) diff --git a/chainladder/__init__.py b/chainladder/__init__.py index 4f09953b..ccde6603 100644 --- a/chainladder/__init__.py +++ b/chainladder/__init__.py @@ -16,10 +16,14 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import copy +import inspect +import re import numpy as np import pandas as pd from importlib.metadata import version +from typing 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. @@ -57,13 +61,13 @@ 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 @@ -71,12 +75,12 @@ def get_option(self, option: str) -> str | bool | list: 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: """ @@ -84,7 +88,7 @@ def set_option( Parameters ---------- - option: str + pat: str The option you wish to set the value for. value: str | bool | list The option value. @@ -94,10 +98,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. @@ -108,19 +112,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() diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 0aeffd27..27e4abcf 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -1,3 +1,4 @@ +from __future__ import annotations import pytest import chainladder as cl @@ -9,6 +10,10 @@ ) 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 @@ -291,4 +296,102 @@ def test_reset_option_invalid() -> None: None """ with pytest.raises(ValueError): - cl.options.reset_option('NOT_A_REAL_OPTION') \ No newline at end of file + 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_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') \ No newline at end of file From 0444076d2a9a993b34190bb3e2e4792bb0226f2d Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 28 May 2026 14:03:01 -0500 Subject: [PATCH 2/5] TEST: Add missing test. --- chainladder/utils/tests/test_utilities.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 27e4abcf..aa92443f 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from pytest import CaptureFixture - + from pytest import MonkeyPatch @@ -384,6 +384,25 @@ def test_describe_option_return_string() -> None: 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. From e61f903db5395ee98d31042d84c2b5c0edb2e069 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Thu, 28 May 2026 14:26:11 -0500 Subject: [PATCH 3/5] FIX: Apply Bugbot fix. --- chainladder/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chainladder/__init__.py b/chainladder/__init__.py index ccde6603..59eb7b4a 100644 --- a/chainladder/__init__.py +++ b/chainladder/__init__.py @@ -14,6 +14,7 @@ # 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 @@ -22,7 +23,10 @@ import pandas as pd from importlib.metadata import version -from typing import Match +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from re import Match # Get the default datetime64 data type and precision, extracted from Pandas installation. From 86fbc45d97d0b26293bf213c538169b0f5de3bda Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Sat, 30 May 2026 16:12:20 -0500 Subject: [PATCH 4/5] FEAT: Work on options deprecations. --- chainladder/__init__.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/chainladder/__init__.py b/chainladder/__init__.py index 59eb7b4a..a5649d51 100644 --- a/chainladder/__init__.py +++ b/chainladder/__init__.py @@ -21,8 +21,9 @@ import re import numpy as np import pandas as pd -from importlib.metadata import version +import warnings +from importlib.metadata import version from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -34,6 +35,14 @@ __dt64_dtype__: str = pd.to_datetime(["2000-01-01"]).dtype.name __dt64_unit__: str = np.datetime_data(__dt64_dtype__)[0] +option_warning: str = ("In a future release, the parameter 'option' will be renamed to 'pat' to stay consistent " + "with the Pandas API.") + +def _resolve_pat(pat: str | None, option: str | None): + if option is not None: + warnings.warn(option_warning, FutureWarning, stacklevel=2) + return option + return pat class Options: """ @@ -65,13 +74,19 @@ 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, pat: str) -> str | bool | list: + 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.9.3 + The ``option`` parameter is renamed to ``pat``. Using ``option`` will raise a + ``FutureWarning`` and will be removed in a future release. + Parameters ---------- - pat: str + 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 @@ -79,6 +94,7 @@ def get_option(self, pat: str) -> str | bool | list: The option value. """ + _resolve_pat(pat=pat, option=option) self._validate_option(pat) return getattr(self, pat) @@ -115,7 +131,6 @@ def reset_option(self, pat: str | None = None) -> None: None """ - if pat is not None: self._validate_option(pat) setattr(self, pat, copy.deepcopy(self._defaults[pat])) @@ -136,7 +151,6 @@ def _validate_option(self, pat: str) -> None: None """ - if pat not in self._defaults: raise ValueError(f"Invalid option(s): {pat}. Must be one of {list(self._defaults)}.") From 724a3a01388c8b6db7544c115c7f8852550aa845 Mon Sep 17 00:00:00 2001 From: Gene Dan Date: Wed, 3 Jun 2026 12:15:34 -0500 Subject: [PATCH 5/5] FEAT: Add deprecation logic to pat/option parameters. --- chainladder/__init__.py | 131 ++++++++++++++++++---- chainladder/utils/tests/test_utilities.py | 61 +++++++++- 2 files changed, 168 insertions(+), 24 deletions(-) diff --git a/chainladder/__init__.py b/chainladder/__init__.py index a5649d51..e287e753 100644 --- a/chainladder/__init__.py +++ b/chainladder/__init__.py @@ -24,10 +24,16 @@ import warnings from importlib.metadata import version -from typing import TYPE_CHECKING +from typing import ( + Any, + Literal, + overload, + TYPE_CHECKING +) if TYPE_CHECKING: from re import Match + from types import FrameType # Get the default datetime64 data type and precision, extracted from Pandas installation. @@ -35,13 +41,58 @@ __dt64_dtype__: str = pd.to_datetime(["2000-01-01"]).dtype.name __dt64_unit__: str = np.datetime_data(__dt64_dtype__)[0] -option_warning: str = ("In a future release, the parameter 'option' will be renamed to 'pat' to stay consistent " - "with the Pandas API.") +# Sentinel pattern used to mark a parameter as required and validate it. +_UNSET: Any = object() -def _resolve_pat(pat: str | None, option: str | None): +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: ... +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. + + 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=2) - return option + 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: @@ -74,13 +125,17 @@ 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, pat: str | None = None, *, option: str | None = None) -> str | bool | list: + 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.9.3 - The ``option`` parameter is renamed to ``pat``. Using ``option`` will raise a - ``FutureWarning`` and will be removed in a future release. + The ``option`` parameter is deprecated; use ``pat`` instead. Parameters ---------- @@ -94,43 +149,69 @@ def get_option(self, pat: str | None = None, *, option: str | None = None) -> st The option value. """ - _resolve_pat(pat=pat, option=option) + pat: str = _resolve_pat(pat=pat, option=option) self._validate_option(pat) return getattr(self, pat) def set_option( self, - pat: str, - value: str | bool | list + pat: str | None = None, + value: str | bool | list = _UNSET, + *, + option: str | None = None ) -> None: """ Set the option value for the specified option. + .. deprecated:: 0.9.3 + The ``option`` parameter is deprecated; use ``pat`` instead. + Parameters ---------- - pat: str + 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'.") setattr(self, pat, value) - def reset_option(self, pat: str | None = None) -> None: + 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 option is None. + all options if pat is None. + + .. deprecated:: 0.9.3 + 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])) @@ -139,7 +220,7 @@ def reset_option(self, pat: str | None = None) -> None: def _validate_option(self, pat: str) -> None: """ - Check whether string assigned to option is one of the configurable options in the Option class. + Check whether string assigned to option is one of the configurable options in the Options class. Parameters ---------- @@ -154,7 +235,7 @@ def _validate_option(self, pat: str) -> 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: + def describe_option(self, pat: str = "", _print_desc: bool=True) -> None | str: """ Print the description for one or more options. @@ -171,7 +252,7 @@ def describe_option(self, pat: str = "", _print_desc=True) -> None | str: Returns ------- - None + The description for the specified option(s) if _print_desc=False, otherwise, `None`. Examples -------- @@ -218,12 +299,16 @@ def describe_option(self, pat: str = "", _print_desc=True) -> None | str: .. 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]" + 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. - keys: list[str] = [key for key in self._defaults if re.search(pat, key)] + 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)}.") @@ -239,7 +324,7 @@ def describe_option(self, pat: str = "", _print_desc=True) -> None | str: # 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?)+)", + 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. ) diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index aa92443f..5c6fd6d4 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -384,6 +384,41 @@ def test_describe_option_return_string() -> None: assert '[currently: numpy]' in result +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') + + try: + with pytest.warns(FutureWarning, match="'option'"): + cl.options.set_option(option='ARRAY_BACKEND', value='numpy') + finally: + cl.options.reset_option('ARRAY_BACKEND') + + +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') + with pytest.warns(FutureWarning, match="'option'"): + cl.options.reset_option(option='ARRAY_BACKEND') + assert cl.options.ARRAY_BACKEND == 'numpy' + finally: + cl.options.reset_option('ARRAY_BACKEND') + + +def test_get_option_missing_pat_raises() -> None: + """ + Calling get_option() with neither pat nor option should raise TypeError. + """ + with pytest.raises(TypeError, match="missing required argument"): + cl.options.get_option() + + 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 @@ -413,4 +448,28 @@ def test_describe_option_invalid() -> None: """ with pytest.raises(ValueError): - cl.options.describe_option('NOT_A_REAL_OPTION') \ No newline at end of file + cl.options.describe_option('NOT_A_REAL_OPTION') + + +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') + + +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') + + +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('[') \ No newline at end of file