1from functools import update_wrapper, wraps
2from types import MethodType
3
4
5class _AvailableIfDescriptor:
6 """Implements a conditional property using the descriptor protocol.
7
8 Using this class to create a decorator will raise an ``AttributeError``
9 if check(self) returns a falsey value. Note that if check raises an error
10 this will also result in hasattr returning false.
11
12 See https://docs.python.org/3/howto/descriptor.html for an explanation of
13 descriptors.
14 """
15
16 def __init__(self, fn, check, attribute_name):
17 self.fn = fn
18 self.check = check
19 self.attribute_name = attribute_name
20
21 # update the docstring of the descriptor
22 update_wrapper(self, fn)
23
24 def __get__(self, obj, owner=None):
25 attr_err = AttributeError(
26 f"This {repr(owner.__name__)} has no attribute {repr(self.attribute_name)}"
27 )
28 if obj is not None:
29 # delegate only on instances, not the classes.
30 # this is to allow access to the docstrings.
31 if not self.check(obj):
32 raise attr_err
33 out = MethodType(self.fn, obj)
34
35 else:
36 # This makes it possible to use the decorated method as an unbound method,
37 # for instance when monkeypatching.
38 @wraps(self.fn)
39 def out(*args, **kwargs):
40 if not self.check(args[0]):
41 raise attr_err
42 return self.fn(*args, **kwargs)
43
44 return out
45
46
47def available_if(check):
48 """An attribute that is available only if check returns a truthy value.
49
50 Parameters
51 ----------
52 check : callable
53 When passed the object with the decorated method, this should return
54 a truthy value if the attribute is available, and either return False
55 or raise an AttributeError if not available.
56
57 Returns
58 -------
59 callable
60 Callable makes the decorated method available if `check` returns
61 a truthy value, otherwise the decorated method is unavailable.
62
63 Examples
64 --------
65 >>> from sklearn.utils.metaestimators import available_if
66 >>> class HelloIfEven:
67 ... def __init__(self, x):
68 ... self.x = x
69 ...
70 ... def _x_is_even(self):
71 ... return self.x % 2 == 0
72 ...
73 ... @available_if(_x_is_even)
74 ... def say_hello(self):
75 ... print("Hello")
76 ...
77 >>> obj = HelloIfEven(1)
78 >>> hasattr(obj, "say_hello")
79 False
80 >>> obj.x = 2
81 >>> hasattr(obj, "say_hello")
82 True
83 >>> obj.say_hello()
84 Hello
85 """
86 return lambda fn: _AvailableIfDescriptor(fn, check, attribute_name=fn.__name__)