Skip to content

Commit

Permalink
RF: Rename and update decorator for version checks
Browse files Browse the repository at this point in the history
- Renamed `keyword-only` decorator to `warn_on_args_to_kwargs` for greater clarity.
- Updated `warn_on_args_to_kwargs` to include version parameters `from_version` and `until_version`.
- Added logic to raise a RuntimeError if the current version of FURY_VERSION is greater than `until_version`.
- Moved `__version__` definition to a new `fury/version.py` module to avoid circular import problems.
- Updated all functions using the decorator to reflect the new name and parameters.
- Adjusted import declarations and ensured compatibility across the code base.
  • Loading branch information
WassCodeur committed Jun 10, 2024
1 parent 6b00d92 commit 8cd737f
Show file tree
Hide file tree
Showing 20 changed files with 269 additions and 216 deletions.
2 changes: 1 addition & 1 deletion fury/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1704,7 +1704,7 @@ def dot(points, colors=None, opacity=None, dot_size=5):
vtk_faces.InsertNextCell(1)
vtk_faces.InsertCellPoint(idd)

color_tuple = color_check(len(points), colors)
color_tuple = color_check(len(points), colors=colors)
color_array, global_opacity = color_tuple

# Create a polydata object
Expand Down
18 changes: 9 additions & 9 deletions fury/colormap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from scipy import linalg

from fury.data import DATA_DIR
from fury.decorators import keyword_only
from fury.decorators import warn_on_args_to_kwargs
from fury.lib import LookupTable

# Allow import, but disable doctests if we don't have matplotlib
Expand All @@ -15,7 +15,7 @@
cm, have_matplotlib, _ = optional_package("matplotlib.cm")


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def colormap_lookup_table(
*,
scale_range=(0, 1),
Expand Down Expand Up @@ -245,7 +245,7 @@ def orient2rgb(v):
return orient


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def line_colors(streamlines, *, cmap="rgb_standard"):
"""Create colors for streamlines to be used in actor.line.
Expand Down Expand Up @@ -312,7 +312,7 @@ def simple_cmap(v):
return simple_cmap


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def create_colormap(v, *, name="plasma", auto=True):
"""Create colors from a specific colormap and return it
as an array of shape (N,3) where every row gives the corresponding
Expand Down Expand Up @@ -516,7 +516,7 @@ def _lab2rgb(lab):
return _xyz2rgb(tmp)


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def distinguishable_colormap(*, bg=(0, 0, 0), exclude=None, nb_colors=None):
"""Generate colors that are maximally perceptually distinct.
Expand Down Expand Up @@ -909,7 +909,7 @@ def get_xyz_coords(illuminant, observer):
) from err


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def xyz2lab(xyz, *, illuminant="D65", observer="2"):
"""XYZ to CIE-LAB color space conversion.
Expand Down Expand Up @@ -957,7 +957,7 @@ def xyz2lab(xyz, *, illuminant="D65", observer="2"):
return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1)


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def lab2xyz(lab, *, illuminant="D65", observer="2"):
"""CIE-LAB to XYZcolor space conversion.
Expand Down Expand Up @@ -1009,7 +1009,7 @@ def lab2xyz(lab, *, illuminant="D65", observer="2"):
return out


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def rgb2lab(rgb, *, illuminant="D65", observer="2"):
"""Conversion from the sRGB color space (IEC 61966-2-1:1999)
to the CIE Lab colorspace under the given illuminant and observer.
Expand Down Expand Up @@ -1040,7 +1040,7 @@ def rgb2lab(rgb, *, illuminant="D65", observer="2"):
return xyz2lab(rgb2xyz(rgb), illuminant=illuminant, observer=observer)


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def lab2rgb(lab, *, illuminant="D65", observer="2"):
"""Lab to RGB color space conversion.
Expand Down
4 changes: 2 additions & 2 deletions fury/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

import numpy as np

from fury.decorators import keyword_only
from fury.decorators import warn_on_args_to_kwargs
from fury.io import load_image


@keyword_only
@warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
def matplotlib_figure_to_numpy(
fig, *, dpi=100, fname=None, flip_up_down=True, transparent=False
):
Expand Down
211 changes: 121 additions & 90 deletions fury/decorators.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Decorators for FURY tests."""

from functools import wraps
from inspect import signature
from functools import wraps
from inspect import signature
import platform
import re
import sys
from warnings import warn

from packaging import version

skip_linux = is_linux = platform.system().lower() == "linux"
skip_osx = is_osx = platform.system().lower() == "darwin"
skip_win = is_win = platform.system().lower() == "windows"
Expand Down Expand Up @@ -50,104 +50,135 @@ def doctest_skip_parser(func):
return func


def keyword_only(func):
def warn_on_args_to_kwargs(from_version=None, until_version=None):
"""Decorator to enforce keyword-only arguments.
This decorator enforces that all arguments after the first one are
keyword-only arguments. It also checks that all keyword arguments are
expected by the function.
Parameters:
-----------
func: function
Function to be decorated.
from_version: str
The version of fury from which the function was supported.
until_version: str
The version of fury until which the function was supported.
Returns:
--------
wrapper: function
Decorated function.
Examples:
---------
>>> @keyword_only
... def f(a, b, *, c, d=1, e=1):
... return a + b + c + d + e
>>> f(1, 2, 3, 4, 5)
15
>>> f(1, 2, c=3, d=4, e=5)
15
>>> f(1, 2, 2, 4, e=5)
14
>>> f(1, 2, c=3, d=4)
11
>>> f(1, 2, d=3, e=5)
Traceback (most recent call last):
...
TypeError: f() missing 1 required keyword-only argument: 'c'
>>> f(1, 2, c=3, d=4, e=5, f=6)
Traceback (most recent call last):
...
TypeError: f() got an unexpected keyword argument 'f'
>>> f(1, c=3, d=4, e=5)
Traceback (most recent call last):
...
TypeError: f() missing 1 required positional argument: 'b'
>>> f(1, 2, 3, 4, 5, 6)
Traceback (most recent call last):
...
TypeError: f() takes 2 positional arguments but 6 were given
decorator: function
Decorator function.
"""

@wraps(func)
def wrapper(*args, **kwargs):
sig = signature(func)
params = sig.parameters
# args_names = [param.name for param in params.values()]
KEYWORD_ONLY_ARGS = [
arg.name for arg in params.values() if arg.kind == arg.KEYWORD_ONLY
]
POSITIONAL_ARGS = [
arg.name
for arg in params.values()
if arg.kind in (arg.POSITIONAL_OR_KEYWORD, arg.POSITIONAL_ONLY)
]
missing_kwargs = [
arg
for arg in KEYWORD_ONLY_ARGS
if arg not in kwargs and params[arg].default == params[arg].empty
]
ARG_DEFAULT = [
arg
for arg in KEYWORD_ONLY_ARGS
if arg not in kwargs and params[arg].default != params[arg].empty
]
func_params_sample = []
for arg in params.values():
if arg.kind in (arg.POSITIONAL_OR_KEYWORD, arg.POSITIONAL_ONLY):
func_params_sample.append(f"{arg.name}_value")
elif arg.kind == arg.KEYWORD_ONLY:
func_params_sample.append(f"{arg.name}='value'")
func_params_sample = ", ".join(func_params_sample)
args_kwargs_len = len(args) + len(kwargs)
params_len = len(params)
try:
return func(*args, **kwargs)
except Exception:
if ARG_DEFAULT:
missing_kwargs += ARG_DEFAULT
if missing_kwargs and params_len >= args_kwargs_len:
positional_args_len = len(POSITIONAL_ARGS)
args_k = list(args[positional_args_len:])
args = list(args[:positional_args_len])
kwargs.update(dict(zip(missing_kwargs, args_k)))
warn(
"Here's how to call the Function {}: {}({})".format(
func.__name__, func.__name__, func_params_sample
),
UserWarning,
stacklevel=3,
)
result = func(*args, **kwargs)
return result

return wrapper
def decorator(func):
"""Decorator
Parameters:
-----------
func: function
Function to be decorated.
Returns:
--------
wrapper: function
Decorated function.
Examples:
---------
>>> @warn_on_args_to_kwargs(from_version="0.0.0", until_version="0.10.0")
... def f(a, b, *, c, d=1, e=1):
... return a + b + c + d + e
>>> f(1, 2, 3, 4, 5)
15
>>> f(1, 2, c=3, d=4, e=5)
15
>>> f(1, 2, 2, 4, e=5)
14
>>> f(1, 2, c=3, d=4)
11
>>> f(1, 2, d=3, e=5)
Traceback (most recent call last):
...
TypeError: f() missing 1 required keyword-only argument: 'c'
"""

@wraps(func)
def wrapper(*args, **kwargs):
sig = signature(func)
params = sig.parameters
#
KEYWORD_ONLY_ARGS = [
arg.name for arg in params.values() if arg.kind == arg.KEYWORD_ONLY
]
POSITIONAL_ARGS = [
arg.name
for arg in params.values()
if arg.kind in (arg.POSITIONAL_OR_KEYWORD, arg.POSITIONAL_ONLY)
]

# Keyword-only arguments that do not have default values and not in kwargs
missing_kwargs = [
arg
for arg in KEYWORD_ONLY_ARGS
if arg not in kwargs and params[arg].default == params[arg].empty
]

# Keyword-only arguments that have default values
ARG_DEFAULT = [
arg
for arg in KEYWORD_ONLY_ARGS
if arg not in kwargs and params[arg].default != params[arg].empty
]
func_params_sample = []

# Create a sample of the function parameters
for arg in params.values():
if arg.kind in (arg.POSITIONAL_OR_KEYWORD, arg.POSITIONAL_ONLY):
func_params_sample.append(f"{arg.name}_value")
elif arg.kind == arg.KEYWORD_ONLY:
func_params_sample.append(f"{arg.name}='value'")
func_params_sample = ", ".join(func_params_sample)
args_kwargs_len = len(args) + len(kwargs)
params_len = len(params)
try:
return func(*args, **kwargs)
except TypeError:
# if the version of fury is greater than until_version, an error should
# be displayed to indicate that this way of calling the function func
# was supported by from_version until_version but not by the current
# FURY_VERSION.
if from_version is not None and until_version is not None:
from fury import __version__ as FURY_VERSION

if version.parse(FURY_VERSION) > version.parse(until_version):
raise RuntimeError(
f"Calling the {func.__name__} function in this way "
f"was supported from {from_version} up to {until_version}, "
f"but not in the current version of FURY {FURY_VERSION}. "
f"Here's how you must call the Function {func.__name__}: "
f"{func.__name__}({func_params_sample})"
) from None

if ARG_DEFAULT:
missing_kwargs += ARG_DEFAULT
if missing_kwargs and params_len >= args_kwargs_len:
positional_args_len = len(POSITIONAL_ARGS)
args_k = list(args[positional_args_len:])
args = list(args[:positional_args_len])
kwargs.update(dict(zip(missing_kwargs, args_k)))
result = func(*args, **kwargs)
warn(
f"We'll no longer accept the way you call the {func.__name__} "
f"function in future versions of FURY.\n"
"Here's how to call the Function {}: {}({})".format(
func.__name__, func.__name__, func_params_sample
),
UserWarning,
stacklevel=3,
)
return result

return wrapper

return decorator
Loading

0 comments on commit 8cd737f

Please sign in to comment.