diff options
author | Massimiliano Culpo <massimiliano.culpo@gmail.com> | 2024-10-29 19:06:26 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-29 19:06:26 +0100 |
commit | b8e3246e8907fc2326473083bc953d11e61bc1e8 (patch) | |
tree | 338c92e71b9981b4b8c03d7d577e3a4b86fbadb2 /lib | |
parent | 60cb628283d51e400b75c502e97f9ef7ba85a59a (diff) | |
download | spack-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.py | 55 | ||||
-rw-r--r-- | lib/spack/spack/test/llnl/util/lang.py | 37 |
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 |