#
# ISC License
#
# Copyright (c) 2023, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
import warnings
from warnings import catch_warnings as catch_warnings
import datetime
import types
from typing import Union, Literal, TYPE_CHECKING, Any
[docs]
class BSKDeprecationWarning(Warning):
__color__ = "\x1b[33;20m" # Yellow
[docs]
class BSKUrgentDeprecationWarning(Warning):
__color__ = "\x1b[31;1m" # Bold red
[docs]
def filterwarnings(
action: Literal["error", "ignore", "always", "default", "module", "once"],
identifier: str,
**kwargs: Any,
):
"""Use this function to create global deprecation warning filters.
The most common use of this function is suppressing warnings for deprecated systems
that you still intend to use. Imagine you want to ignore deprecation warnings
for the method `myMethod` in class `ValidClass` in module `my_module`. You would add
the following line to your simulation script::
deprecated.filterwarnings("ignore", "my_module.ValidClass.myMethod")
Note that deprecation warnings should not be ignored blindly. Deprecated code WILL
be removed in future releases, so use warning suppression carefully.
Args:
action (Literal["error", "ignore", "always", "default", "module", "once"]):
Controls how the filtered warnings will behave.
identifier (str): The warning message must contain this string. This can be
the function or variable identifier (qualified name) in order to hide
deprecation warnings for specific features.
**kwargs (Any): Any other keyword arguments are passed directly to
`warnings.filterwarnings`.
"""
warnings.filterwarnings(
action, f".*{identifier}.*", category=BSKDeprecationWarning, **kwargs
)
[docs]
class ignore:
"""Use this context manager to ignore warnings in a precise section of code.
The most common use of this function is suppressing warnings for specific calls.
Imagine you want to ignore deprecation warnings for the method `myMethod` in
class `ValidClass` in module `my_module` for a single call to this function.
You would do::
myClass = my_module.ValidClass()
with deprecated.ignore("my_module.ValidClass.myMethod"):
myClass.myMethod() # Does not raise a warning
myClass.myMethod() # Raises a warning
Note that deprecation warnings should not be ignored blindly. Deprecated code WILL
be removed in future releases, so use warning suppression carefully.
"""
def __init__(self, identifier: str) -> None:
self.catch = catch_warnings()
self.identifier = identifier
def __enter__(self):
self.catch.__enter__()
filterwarnings("ignore", self.identifier)
def __exit__(self, *exc_info):
self.catch.__exit__(*exc_info)
[docs]
def deprecationWarn(id: str, removalDate: Union[datetime.date, str], msg: str):
"""Utility function to raise deprecation warnings inside a function body.
This function should rarely be used on its own. For deprecated Python code,
prefer the `@deprecated` decorator. For deprecated C++ code, use the SWIG
macros in `swig_deprecated.i`.
Args:
id (str): An identifier for the deprecated feature (function/variable
qualified name)
removalDate (Union[datetime.date, str]): The date when we expect to remove this
deprecated feature, in the format 'YYYY/MM/DD' or as a ``datetime.date``.
Think of an amount of time that would let users update their code, and then
add that duration to today's date to find a reasonable removal date.
msg (str, optional): a text that is directly shown to the users. Here, you may
explain why the function is deprecated, alternative functions, links to
documentation or scenarios that show how to translate deprecated code...
"""
id = id.replace(")", "").replace("(", "")
if isinstance(removalDate, str):
removalDate = datetime.datetime.strptime(removalDate, r"%Y/%m/%d").date()
if removalDate > datetime.date.today():
warnings.warn(
f"{id} will be removed after {removalDate}: {msg}",
category=BSKDeprecationWarning,
stacklevel=3,
)
else:
warnings.warn(
f"{id} will be removed soon: {msg}",
category=BSKUrgentDeprecationWarning,
stacklevel=3,
)
[docs]
def deprecated(removalDate: str, msg: str):
"""A decorator for functions or classes that are deprecated.
Usage::
@deprecated.deprecated("2024/05/28", "Use MyNewClass")
class MyClass:
...
or::
@deprecated.deprecated("2024/05/28", "myFun is unsafe now")
def myFun(a, b):
...
or::
class ValidClass:
@deprecated.deprecated("2024/05/28", "myMethod is slow, use myBetterMethod")
def myMethod(self, a, b):
...
Args:
removalDate (Union[datetime.date, str]): The date when we expect to remove this
deprecated feature, in the format 'YYYY/MM/DD' or as a ``datetime.date``.
Think of an amount of time that would let users update their code, and then
add that duration to today's date to find a reasonable removal date.
msg (str, optional): a text that is directly shown to the users. Here, you may
explain why the function is deprecated, alternative functions, links to
documentation or scenarios that show how to translate deprecated code...
"""
def wrapper(func):
id = f"{func.__module__}.{func.__qualname__}"
def inner_wrapper(*args, **kwargs):
deprecationWarn(id, removalDate, msg)
return func(*args, **kwargs)
return inner_wrapper
return wrapper
[docs]
class DeprecatedAttribute:
"""Use this descriptor to deprecate class attributes (variables).
If you want to deprecate ``myAttribute`` in the following class::
class MyClass:
def __init__(self) -> None:
self.myAttribute = 0
You can use the following syntax::
class PythonTest:
myAttribute = deprecated.DeprecatedAttribute("2099/05/05", "myAttribute is no longer used in the simulation")
def __init__(self) -> None:
with deprecated.ignore("myAttribute"): # Prevents warnings here
self.myAttribute = 0
The attribute will work as before, but deprecation warnings will be raised
everytime it's used (read or set).
"""
def __init__(self, removalDate: str, msg: str) -> None:
self.removalDate = removalDate
self.msg = msg
def __set_name__(self, owner, name):
self.id = f"{owner.__module__}.{owner.__qualname__}.{name}"
self.name = name
def __get__(self, obj, objtype=None):
deprecationWarn(self.id, self.removalDate, self.msg)
return getattr(obj, f"_{self.name}")
def __set__(self, obj, value):
deprecationWarn(self.id, self.removalDate, self.msg)
setattr(obj, f"_{self.name}", value)
class DeprecatedProperty: # type: ignore
"""Use this descriptor to deprecate class properties.
If you want to deprecate ``myProperty`` in the following class::
class MyClass:
@property
def myProperty(self):
return 0
@myProperty.setter
def myProperty(self, value: int):
...
You can use the following syntax::
class PythonTest:
@property
def myProperty(self):
return 0
@myProperty.setter
def myProperty(self, value: int):
...
myProperty = deprecated.DeprecatedProperty(
"2099/05/05",
"myProperty is no longer used in the simulation",
myProperty)
The property will work as before, but deprecation warnings will be raised
everytime it's used (read or set).
"""
def __init__(self, removalDate: str, msg: str, property: property) -> None:
self.removalDate = removalDate
self.msg = msg
self.property = property
if not hasattr(self.property, "__get__") or not hasattr(
self.property, "__set__"
):
raise ValueError("DeprecatedProperty must be given an existing property")
def __set_name__(self, owner, name):
self.id = f"{owner.__module__}.{owner.__qualname__}.{name}"
def __get__(self, *args, **kwargs):
deprecationWarn(self.id, self.removalDate, self.msg)
return self.property.__get__(*args, **kwargs)
def __set__(self, *args, **kwargs):
deprecationWarn(self.id, self.removalDate, self.msg)
self.property.__set__(*args, **kwargs)
def __delete__(self, *args, **kwargs):
self.property.__delete__(*args, **kwargs)
# Typing hack so that linters don't complain about redefining properties
if TYPE_CHECKING:
[docs]
def DeprecatedProperty(removalDate: str, msg: str) -> Any: # type: ignore
...
# Monkey patching showwarning to modify the behaviour for our warnings
warnings.formatwarning = formatwarning