from __future__ import annotations import functools import textwrap from typing import Any from typing import TYPE_CHECKING from typing import TypeVar import warnings from optuna.exceptions import ExperimentalWarning if TYPE_CHECKING: from collections.abc import Callable from typing_extensions import ParamSpec FT = TypeVar("FT") FP = ParamSpec("FP") CT = TypeVar("CT") _EXPERIMENTAL_NOTE_TEMPLATE = """ .. note:: Added in v{ver} as an experimental feature. The interface may change in newer versions without prior notice. See https://github.com/optuna/optuna/releases/tag/v{ver}. """ def warn_experimental_argument(option_name: str) -> None: warnings.warn( f"Argument ``{option_name}`` is an experimental feature." " The interface can change in the future.", ExperimentalWarning, ) def _validate_version(version: str) -> None: if not isinstance(version, str) or len(version.split(".")) != 3: raise ValueError( "Invalid version specification. Must follow `x.y.z` format but `{}` is given".format( version ) ) def _get_docstring_indent(docstring: str) -> str: return docstring.split("\n")[-1] if "\n" in docstring else "" def experimental_func( version: str, name: str | None = None, ) -> Callable[[Callable[FP, FT]], Callable[FP, FT]]: """Decorate function as experimental. Args: version: The first version that supports the target feature. name: The name of the feature. Defaults to fully qualified name of the function, i.e. `f"{func.__module__}.{func.__qualname__}"`. Optional. """ _validate_version(version) def decorator(func: Callable[FP, FT]) -> Callable[FP, FT]: if func.__doc__ is None: func.__doc__ = "" note = _EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version) indent = _get_docstring_indent(func.__doc__) func.__doc__ = func.__doc__.strip() + textwrap.indent(note, indent) + indent _name = name or f"{func.__module__}.{func.__qualname__}" @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> FT: warnings.warn( "{} is experimental (supported from v{}). " "The interface can change in the future.".format(_name, version), ExperimentalWarning, stacklevel=2, ) return func(*args, **kwargs) return wrapper return decorator def experimental_class( version: str, name: str | None = None, ) -> Callable[[CT], CT]: """Decorate class as experimental. Args: version: The first version that supports the target feature. name: The name of the feature. Defaults to the class name. Optional. """ _validate_version(version) def decorator(cls: CT) -> CT: def wrapper(cls: CT) -> CT: """Decorates a class as experimental. This decorator is supposed to be applied to the experimental class. """ _original_init = getattr(cls, "__init__") _original_name = getattr(cls, "__name__") @functools.wraps(_original_init) def wrapped_init(self: Any, *args: Any, **kwargs: Any) -> None: warnings.warn( "{} is experimental (supported from v{}). " "The interface can change in the future.".format( name if name is not None else _original_name, version ), ExperimentalWarning, stacklevel=2, ) _original_init(self, *args, **kwargs) setattr(cls, "__init__", wrapped_init) if cls.__doc__ is None: cls.__doc__ = "" note = _EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version) indent = _get_docstring_indent(cls.__doc__) cls.__doc__ = cls.__doc__.strip() + textwrap.indent(note, indent) + indent return cls return wrapper(cls) return decorator