summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMassimiliano Culpo <massimiliano.culpo@gmail.com>2024-10-29 19:06:26 +0100
committerGitHub <noreply@github.com>2024-10-29 19:06:26 +0100
commitb8e3246e8907fc2326473083bc953d11e61bc1e8 (patch)
tree338c92e71b9981b4b8c03d7d577e3a4b86fbadb2 /lib
parent60cb628283d51e400b75c502e97f9ef7ba85a59a (diff)
downloadspack-b8e3246e8907fc2326473083bc953d11e61bc1e8.tar.gz
spack-b8e3246e8907fc2326473083bc953d11e61bc1e8.tar.bz2
spack-b8e3246e8907fc2326473083bc953d11e61bc1e8.tar.xz
spack-b8e3246e8907fc2326473083bc953d11e61bc1e8.zip
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>
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/llnl/util/lang.py55
-rw-r--r--lib/spack/spack/test/llnl/util/lang.py37
2 files changed, 92 insertions, 0 deletions
diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py
index 5efe27b8b1..326d25e79b 100644
--- a/lib/spack/llnl/util/lang.py
+++ b/lib/spack/llnl/util/lang.py
@@ -11,6 +11,7 @@ import os
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 @@ class classproperty:
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")
diff --git a/lib/spack/spack/test/llnl/util/lang.py b/lib/spack/spack/test/llnl/util/lang.py
index abf2c5b134..52dcf3950a 100644
--- a/lib/spack/spack/test/llnl/util/lang.py
+++ b/lib/spack/spack/test/llnl/util/lang.py
@@ -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