Files
geMoldInsight/venv/lib/python3.11/site-packages/pyvista/_deprecate_positional_args.py
T

252 lines
9.8 KiB
Python
Raw Normal View History

2026-02-12 23:22:11 +08:00
from __future__ import annotations
from functools import wraps
import inspect
from inspect import Parameter
from inspect import Signature
import os
from pathlib import Path
from typing import Callable
from typing import TypeVar
from typing import overload
import warnings
from typing_extensions import ParamSpec
from pyvista._version import version_info
_MAX_POSITIONAL_ARGS = 3 # Should match value in pyproject.toml
P = ParamSpec('P')
T = TypeVar('T')
@overload
def _deprecate_positional_args(
func: Callable[P, T],
*,
version: tuple[int, int] = ...,
allowed: list[str] | None = ...,
n_allowed: int = ...,
) -> Callable[P, T]: ...
@overload
def _deprecate_positional_args(
*, version: tuple[int, int] = ..., allowed: list[str] | None = ..., n_allowed: int = ...
) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
def _deprecate_positional_args(
func: Callable[..., T] | None = None,
*,
version: tuple[int, int] = (0, 50),
allowed: list[str] | None = None,
n_allowed: int | None = None,
) -> Callable[..., T] | Callable[[Callable[P, T]], Callable[P, T]]:
"""Use a decorator to deprecate positional arguments.
Parameters
----------
func : callable, default=None
Function to check arguments on.
version : tuple[int, int], default: (0, 50)
The version (major, minor) when positional arguments will result in RuntimeError.
allowed : list[str], optional
List of argument names which are allowed to be positional. This value is limited
based on rule PLR0917.
n_allowed : int, optional
Override the number of allowed positional arguments to this value.
"""
def _inner_deprecate_positional_args(f: Callable[P, T]) -> Callable[P, T]:
def qualified_name() -> str:
return f.__qualname__ if hasattr(f, '__qualname__') else f.__name__
decorator_name = _deprecate_positional_args.__name__
sig = inspect.signature(f)
param_names = list(sig.parameters)
# Validate n_allowed itself
if n_allowed:
if n_allowed <= _MAX_POSITIONAL_ARGS:
msg = (
f'In decorator {decorator_name!r} for function {qualified_name()!r}:\n'
f'`n_allowed` must be greater than {_MAX_POSITIONAL_ARGS} for it to be useful.'
)
raise ValueError(msg)
n_allowed_ = n_allowed
else:
n_allowed_ = _MAX_POSITIONAL_ARGS
if allowed is not None:
# Validate input type
if not isinstance(allowed, list):
msg = ( # type: ignore[unreachable]
f'In decorator {decorator_name!r} for function {qualified_name()!r}:\n'
f'Allowed arguments must be a list, got {type(allowed)}.'
)
raise TypeError(msg)
# Validate number of allowed args
if len(allowed) > n_allowed_:
msg = (
f'In decorator {decorator_name!r} for function {qualified_name()!r}:\n'
f'A maximum of {n_allowed_} positional arguments are allowed.\n'
f'Got {len(allowed)}: {allowed}'
)
raise ValueError(msg)
# Validate allowed against actual parameter names
for name in allowed:
if name not in param_names:
msg = (
f'Allowed positional argument {name!r} in decorator '
f'{decorator_name!r}\n'
f'is not a parameter of function {qualified_name()!r}.'
)
raise ValueError(msg)
# Check that allowed args appears in the same order as in the signature
sig_allowed = [name for name in param_names if name in allowed]
if sig_allowed != allowed:
msg = (
f'The `allowed` list {allowed} in decorator {decorator_name!r} is not in the\n'
f'same order as the parameters in {qualified_name()!r}.\n'
f'Expected order: {sig_allowed}.'
)
raise ValueError(msg)
# Check that allowed args are not already kwonly
for name in allowed:
if sig.parameters[name].kind == Parameter.KEYWORD_ONLY:
msg = (
f'Parameter {name!r} in decorator {decorator_name!r} is already '
f'keyword-only\nand should be removed from the allowed list.'
)
raise ValueError(msg)
# Check if the decorator is even needed at all
n_positional = 0
for name in param_names:
if name not in ['cls', 'self'] and sig.parameters[name].kind in [
Parameter.POSITIONAL_ONLY,
Parameter.POSITIONAL_OR_KEYWORD,
]:
n_positional += 1
actual_n_allowed = len(allowed) if allowed else 0
if n_positional <= actual_n_allowed:
msg = (
f'Function {qualified_name()!r} has {actual_n_allowed} positional arguments, '
f'which is less than or equal to the\nmaximum number of allowed positional '
f'arguments ({n_allowed_}).\nThis decorator is not necessary and can be removed.'
)
raise RuntimeError(msg)
# Raise error post-deprecation
if version_info >= version:
# Construct expected positional args and signature
new_parameters = []
max_args_to_print = actual_n_allowed + 2
cls_or_self = 'cls' in param_names or 'self' in param_names
max_args_to_print = (max_args_to_print + 1) if cls_or_self else max_args_to_print
has_too_many_to_print = False
for i, name in enumerate(param_names):
if i > max_args_to_print:
has_too_many_to_print = True
break
if name in ['cls', 'self', *(allowed if allowed else [])]:
current_kind = sig.parameters[name].kind
new_kind = (
current_kind
if current_kind != Parameter.KEYWORD_ONLY
else Parameter.KEYWORD_ONLY
)
new_parameters.append(Parameter(name, kind=new_kind))
else:
new_parameters.append(Parameter(name, kind=Parameter.KEYWORD_ONLY))
signature_string = f'{qualified_name()}{Signature(new_parameters)}'
if has_too_many_to_print:
# Replace ending bracket with ellipses
signature_string = f'{signature_string[:-1]}, ...)'
# Get source file and line number
file = Path(
os.path.relpath(inspect.getfile(f), start=os.getcwd()) # noqa: PTH109 # https://github.com/pyvista/pyvista/pull/7732
).as_posix()
lineno = inspect.getsourcelines(f)[1]
location = f'{file}:{lineno}'
msg = (
f'Positional arguments are no longer allowed in {qualified_name()!r}.\n'
f'Update the function signature at:\n'
f'{location} to enforce keyword-only args:\n'
f' {signature_string}\n'
f'and remove the {decorator_name!r} decorator.'
)
raise RuntimeError(msg)
@wraps(f)
def inner_f(*args: P.args, **kwargs: P.kwargs) -> T:
passed_positional_names = param_names[: len(args)]
# Exclude allowed ones
if allowed:
offending_args = [name for name in passed_positional_names if name not in allowed]
else:
offending_args = passed_positional_names
if 'self' in offending_args:
offending_args.remove('self')
if 'cls' in offending_args:
offending_args.remove('cls')
if offending_args:
# Craft a message to print a warning or raise an error
if len(offending_args) == 1:
a = ' a '
s = ''
this = 'this'
else:
a = ' '
s = 's'
this = 'these'
if version_info < version:
# Print warning
version_str = '.'.join(map(str, version))
arg_list = ', '.join(f'{a!r}' for a in offending_args)
stack_level = 3
def call_site() -> str:
# Get location where the function is called
frame = inspect.stack()[stack_level]
file = Path(
os.path.relpath(frame.filename, start=os.getcwd()) # noqa: PTH109 # https://github.com/pyvista/pyvista/pull/7732
).as_posix()
return f'{file}:{frame.lineno}'
def warn_positional_args() -> None:
from pyvista.core.errors import PyVistaDeprecationWarning # noqa: PLC0415
msg = (
f'\n{call_site()}: '
f'Argument{s} {arg_list} must be passed as{a}keyword argument{s} '
f'to function {qualified_name()!r}.\n'
f'From version {version_str}, passing {this} as{a}positional '
f'argument{s} will result in a TypeError.'
)
warnings.warn(msg, PyVistaDeprecationWarning, stacklevel=stack_level)
warn_positional_args()
return f(*args, **kwargs)
return inner_f
if func is not None:
return _inner_deprecate_positional_args(func)
return _inner_deprecate_positional_args