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 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")

View File

@ -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