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:
Massimiliano Culpo 2024-10-29 19:06:26 +01:00 committed by GitHub
parent 60cb628283
commit b8e3246e89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 92 additions and 0 deletions

View File

@ -11,6 +11,7 @@
import re
import sys
import traceback
import warnings
from datetime import datetime, timedelta
from typing import Callable, Iterable, List, Tuple, TypeVar
@ -914,6 +915,21 @@ def ensure_last(lst, *elements):
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):
"""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):
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")

View File

@ -336,3 +336,40 @@ def test_grouped_exception_base_type():
message = h.grouped_message(with_tracebacks=False)
assert "catch-runtime-error" 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