llnl.util.lang: add classes to help with deprecations (#47279)
* Add a descriptor to have a class level constant This descriptor helps intercept places where we set a value on instances. It does not really behave like "const" in C-like languages, but is the simplest implementation that might still be useful. * Add a descriptor to deprecate properties/attributes of an object This descriptor is used as a base class. Derived classes may implement a factory to return an adaptor to the attribute being deprecated. The descriptor can either warn, or raise an error, when usage of the deprecated attribute is intercepted. --------- Co-authored-by: Harmen Stoppels <me@harmenstoppels.nl>
This commit is contained in:
parent
60cb628283
commit
b8e3246e89
@ -11,6 +11,7 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
import warnings
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Callable, Iterable, List, Tuple, TypeVar
|
from typing import Callable, Iterable, List, Tuple, TypeVar
|
||||||
|
|
||||||
@ -914,6 +915,21 @@ def ensure_last(lst, *elements):
|
|||||||
lst.append(lst.pop(lst.index(elt)))
|
lst.append(lst.pop(lst.index(elt)))
|
||||||
|
|
||||||
|
|
||||||
|
class Const:
|
||||||
|
"""Class level constant, raises when trying to set the attribute"""
|
||||||
|
|
||||||
|
__slots__ = ["value"]
|
||||||
|
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
raise TypeError(f"Const value does not support assignment [value={self.value}]")
|
||||||
|
|
||||||
|
|
||||||
class TypedMutableSequence(collections.abc.MutableSequence):
|
class TypedMutableSequence(collections.abc.MutableSequence):
|
||||||
"""Base class that behaves like a list, just with a different type.
|
"""Base class that behaves like a list, just with a different type.
|
||||||
|
|
||||||
@ -1018,3 +1034,42 @@ def __init__(self, callback):
|
|||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
return self.callback(owner)
|
return self.callback(owner)
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedProperty:
|
||||||
|
"""Data descriptor to error or warn when a deprecated property is accessed.
|
||||||
|
|
||||||
|
Derived classes must define a factory method to return an adaptor for the deprecated
|
||||||
|
property, if the descriptor is not set to error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ["name"]
|
||||||
|
|
||||||
|
#: 0 - Nothing
|
||||||
|
#: 1 - Warning
|
||||||
|
#: 2 - Error
|
||||||
|
error_lvl = 0
|
||||||
|
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
if self.error_lvl == 1:
|
||||||
|
warnings.warn(
|
||||||
|
f"accessing the '{self.name}' property of '{instance}', which is deprecated"
|
||||||
|
)
|
||||||
|
elif self.error_lvl == 2:
|
||||||
|
raise AttributeError(f"cannot access the '{self.name}' attribute of '{instance}'")
|
||||||
|
|
||||||
|
return self.factory(instance, owner)
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
raise TypeError(
|
||||||
|
f"the deprecated property '{self.name}' of '{instance}' does not support assignment"
|
||||||
|
)
|
||||||
|
|
||||||
|
def factory(self, instance, owner):
|
||||||
|
raise NotImplementedError("must be implemented by derived classes")
|
||||||
|
@ -336,3 +336,40 @@ def test_grouped_exception_base_type():
|
|||||||
message = h.grouped_message(with_tracebacks=False)
|
message = h.grouped_message(with_tracebacks=False)
|
||||||
assert "catch-runtime-error" in message
|
assert "catch-runtime-error" in message
|
||||||
assert "catch-value-error" not in message
|
assert "catch-value-error" not in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_class_level_constant_value():
|
||||||
|
"""Tests that the Const descriptor does not allow overwriting the value from an instance"""
|
||||||
|
|
||||||
|
class _SomeClass:
|
||||||
|
CONST_VALUE = llnl.util.lang.Const(10)
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="not support assignment"):
|
||||||
|
_SomeClass().CONST_VALUE = 11
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated_property():
|
||||||
|
"""Tests the behavior of the DeprecatedProperty descriptor, which is can be used when
|
||||||
|
deprecating an attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class _Deprecated(llnl.util.lang.DeprecatedProperty):
|
||||||
|
def factory(self, instance, owner):
|
||||||
|
return 46
|
||||||
|
|
||||||
|
class _SomeClass:
|
||||||
|
deprecated = _Deprecated("deprecated")
|
||||||
|
|
||||||
|
# Default behavior is to just return the deprecated value
|
||||||
|
s = _SomeClass()
|
||||||
|
assert s.deprecated == 46
|
||||||
|
|
||||||
|
# When setting error_level to 1 the attribute warns
|
||||||
|
_SomeClass.deprecated.error_lvl = 1
|
||||||
|
with pytest.warns(UserWarning):
|
||||||
|
assert s.deprecated == 46
|
||||||
|
|
||||||
|
# When setting error_level to 2 an exception is raised
|
||||||
|
_SomeClass.deprecated.error_lvl = 2
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
_ = s.deprecated
|
||||||
|
Loading…
Reference in New Issue
Block a user