94 lines
2.8 KiB
Python
94 lines
2.8 KiB
Python
|
from functools import update_wrapper, wraps
|
||
|
from types import MethodType
|
||
|
|
||
|
|
||
|
class _AvailableIfDescriptor:
|
||
|
"""Implements a conditional property using the descriptor protocol.
|
||
|
|
||
|
Using this class to create a decorator will raise an ``AttributeError``
|
||
|
if check(self) returns a falsey value. Note that if check raises an error
|
||
|
this will also result in hasattr returning false.
|
||
|
|
||
|
See https://docs.python.org/3/howto/descriptor.html for an explanation of
|
||
|
descriptors.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, fn, check, attribute_name):
|
||
|
self.fn = fn
|
||
|
self.check = check
|
||
|
self.attribute_name = attribute_name
|
||
|
|
||
|
# update the docstring of the descriptor
|
||
|
update_wrapper(self, fn)
|
||
|
|
||
|
def _check(self, obj, owner):
|
||
|
attr_err_msg = (
|
||
|
f"This {repr(owner.__name__)} has no attribute {repr(self.attribute_name)}"
|
||
|
)
|
||
|
try:
|
||
|
check_result = self.check(obj)
|
||
|
except Exception as e:
|
||
|
raise AttributeError(attr_err_msg) from e
|
||
|
|
||
|
if not check_result:
|
||
|
raise AttributeError(attr_err_msg)
|
||
|
|
||
|
def __get__(self, obj, owner=None):
|
||
|
if obj is not None:
|
||
|
# delegate only on instances, not the classes.
|
||
|
# this is to allow access to the docstrings.
|
||
|
self._check(obj, owner=owner)
|
||
|
out = MethodType(self.fn, obj)
|
||
|
|
||
|
else:
|
||
|
# This makes it possible to use the decorated method as an unbound method,
|
||
|
# for instance when monkeypatching.
|
||
|
@wraps(self.fn)
|
||
|
def out(*args, **kwargs):
|
||
|
self._check(args[0], owner=owner)
|
||
|
return self.fn(*args, **kwargs)
|
||
|
|
||
|
return out
|
||
|
|
||
|
|
||
|
def available_if(check):
|
||
|
"""An attribute that is available only if check returns a truthy value.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
check : callable
|
||
|
When passed the object with the decorated method, this should return
|
||
|
a truthy value if the attribute is available, and either return False
|
||
|
or raise an AttributeError if not available.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
callable
|
||
|
Callable makes the decorated method available if `check` returns
|
||
|
a truthy value, otherwise the decorated method is unavailable.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> from sklearn.utils.metaestimators import available_if
|
||
|
>>> class HelloIfEven:
|
||
|
... def __init__(self, x):
|
||
|
... self.x = x
|
||
|
...
|
||
|
... def _x_is_even(self):
|
||
|
... return self.x % 2 == 0
|
||
|
...
|
||
|
... @available_if(_x_is_even)
|
||
|
... def say_hello(self):
|
||
|
... print("Hello")
|
||
|
...
|
||
|
>>> obj = HelloIfEven(1)
|
||
|
>>> hasattr(obj, "say_hello")
|
||
|
False
|
||
|
>>> obj.x = 2
|
||
|
>>> hasattr(obj, "say_hello")
|
||
|
True
|
||
|
>>> obj.say_hello()
|
||
|
Hello
|
||
|
"""
|
||
|
return lambda fn: _AvailableIfDescriptor(fn, check, attribute_name=fn.__name__)
|