Deprecating code in Basilisk
Motivation
The nature of a fast evolving software such as Basilisk is that systems are consistently improving, many times making older functionality obsolete. Thus, we face the challenge of handling older code while we move towards the new systems. We cannot simply remove old functionality, as we don’t want user code to break overnight. Instead, we enter a phase of deprecation, when we warn users about the use of deprecated code, but otherwise continue to allow it and support it. After enough time has passed for our users to update their code, the deprecated functionality can be removed.
This support page explains the different mechanisms we have available in Basilisk to mark code as deprecated. Deprecated code will cause deprecation warnings to pop-up in the user’s console, but otherwise it should work as expected.
Deprecating Python Code
For code that is entirely defined in Python, we can make use of the utility decorators and descriptors
defined in Basilisk.utilities.deprecated. This section shows how to deprecate functions, classes, attributes,
and properties.
To illustrate this functionality, let’s imagine the following code:
def standaloneFun(arg):
...
class MyClass:
def __init__(self):
self.myAttribute = 0
self._myPropertyInner = 0
def myFun1(self):
...
@property
def myProperty(self):
return self._myPropertyInner * 2
@myProperty.setter
def myProperty(self, value: int):
self._myPropertyInner = value / 2
There is a standalone function standaloneFun, a class MyClass with two attributes
myAttribute and _myPropertyInner, a class method myFun1, and a property myProperty
with a getter and setter (which makes use of the _myPropertyInner private attribute).
If we want to deprecate the standalone function and the class method, the syntax is as follows:
from Basilisk.utilities import deprecated
@deprecated.deprecated("2099/05/05", "Use standaloneFun_new() instead!")
def standaloneFun(arg):
...
class MyClass:
...
@deprecated.deprecated("2000/05/05", "myFun1 is super old!")
def myFun1(self):
...
...
The first argument to @deprecated.deprecated must be a string with the date when the function is expected
to be removed (as a rule of thumb, between 6 to 12 months after the release of
the deprecated code). The second argument is a message that is shown directly
to users. Here, you may explain why the function is deprecated, alternative functions,
links to documentation or scenarios that show how to translate deprecated code…
If you want to deprecate a class, then use:
from Basilisk.utilities import deprecated
@deprecated.deprecated("2099/05/05", "This entire class is replaced by MyOtherClass")
class MyClass:
...
This is the same syntax as deprecating functions, and the arguments behave in the same way.
If you want to deprecate an attribute, that is, a class variable, then do:
from Basilisk.utilities import deprecated
class MyClass:
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 input arguments to deprecated.DeprecatedAttribute are the same as the arguments
for deprecated.deprecated. Note that if you want to initialize the attribute to
some variable (or otherwise manipulate it in any way) without raising deprecation
warnings, you should use the deprecated.ignore context manager.
Finally, if you need to deprecate a property, then use:
from Basilisk.utilities import deprecated
class MyClass:
@property
def myProperty(self):
return self.myPropertyInner * 2
@myProperty.setter
def myProperty(self, value: int):
self.myPropertyInner = value / 2
myProperty = deprecated.DeprecatedProperty(
"2099/05/05", "myProperty is no longer used in the simulation", myProperty
)
The first two arguments to deprecated.DeprecatedProperty are the same as the
arguments to deprecated.deprecated or deprecated.DeprecatedAttribute.
The third argument, however, shold be the name of the property to deprecate.
Deprecating C++ Code Wrapped by SWIG
This section explains how to deprecate code that is written in C++ and exposed to Python through a SWIG interface. Note that deprecation warnings will be raised only when the Python wrappers to C++ functionality are invoked. Currently, it is not possible to emit deprecation warnings when the deprecated functionality is called from C++.
In order to deprecate functions, classes, or variables in C++, we use special macros in the SWIG file that is exposing the deprecated functionality. For example, let’s consider we have this C++ code:
// example.h
void standaloneFun(int, double) {};
struct MyClass
{
void myFun() {};
int myAttribute;
};
with the following SWIG interface file:
// example.i
%module example
%{
#include "example.h"
%}
%include "example.h"
If we want to deprecate the standalone function and class function, then we would change the SWIG file to:
// example.i
%module example
%{
#include "example.h"
%}
%include "swig_deprecated.i"
%deprecated_function(standaloneFun, "2023/01/01", "You should use standaloneFunNew")
%deprecated_function(MyClass::myFun, "2023/01/01", "myFun has no effects.")
%include "example.h"
In the code above, we have included "swig_deprecated.i", which makes the
%deprecated_function macro available. Then, we have called this macro before we included the header file
"example.h". The first input to the macro is the SWIG identifier for the function.
For standalone functions this is simple the function name, but for class functions this is
[CLASS_NAME]::[FUNCTION_NAME]. The next two arguments are the expected removal date
and message, as covered in the previous section.
If we want to deprecate an entire class, then the SWIG file ought to change to:
// example.i
%module example
%{
#include "example.h"
%}
%include "swig_deprecated.i"
%deprecated_function(MyClass::MyClass, "2023/01/01", "Use MyNewClass.")
%include "example.h"
Again, we use %deprecated_function before %include "example.h". This time, however,
we need to target [CLASS_NAME]::[CLASS_NAME].
Finally, to deprecate a class variable, the SWIG file would change to:
// example.i
%module example
%{
#include "example.h"
%}
%include "swig_deprecated.i"
%deprecated_variable(MyClass, myAttribute, "2023/01/01", "Use MyNewClass.")
%include "example.h"
This time, we call the macro %deprecated_variable, although always
before %include "example.h". In this case, the two first arguments to %deprecated_variable
are the name of the class that contains the variable, and then the name of the varible.
The final two arguments are the expected removal date and the message.
If a C++ structure or one of its fields are renamed, an alias can be used to deprecate the entity while retaining support for the old name. This is done using the _DeprecatedWrapper class defined in "swig_deprecated.i". Its implementation is detailed below.
// example.i
%module example
%{
#include "example.h"
%}
%include "swig_deprecated.i"
%include example.h
%pythoncode %{
import sys
mod = sys.modules[__name__]
mod.ExampleStruct = _DeprecatedWrapper(
mod.ExampleStruct,
targetName="ExampleStruct",
deprecatedFields={"oldField": "newField"},
typeConversion="scalarTo3D",
removalDate="YYYY/MM/DD"
)
mod.OldStructure = _DeprecatedWrapper(
mod.NewStructure,
aliasName="OldStructure",
targetName="NewStructure",
removalDate="YYYY/MM/DD"
)
protectAllClasses(sys.modules[__name__])
%}
If a field is renamed, the top chunk creates a wrapper that contains the old field name and handles deprecation warnings and getter/setter behavior. The typeConversion parameter can be set to "scalarTo3D" to deprecate a scalar variable and alias to a new 3D variable with repeated values.
If a structure is renamed, the second chunk creates a wrapper that generates an alias using the old structure name for continued support.
Deprecating Rust Code
Native Rust Basilisk modules have two deprecation mechanisms with different audiences:
Basilisk’s dated module and field annotations generate runtime warnings for Python users.
Rust’s standard
#[deprecated]attribute generates a compiler warning for Rust code that uses a deprecated Rust item.
The standard Rust attribute does not generate a warning when a user imports or
uses the generated Python module, and it does not implement Basilisk’s dated
urgent-warning behavior. It is therefore not a substitute for
#[bsk(deprecated(...))] on a Python-visible interface. Choose the mechanism
based on whether the deprecated interface is visible to Python users or only
to other Rust code.
Python-Visible Rust Modules
Deprecate construction of an entire native Rust Basilisk module by adding deprecation metadata to its module annotation:
#[bsk_build::module(
deprecated(
removal_date = "2099/05/05",
message = "Use replacementModule instead."
)
)]
#[repr(C)]
pub struct oldModuleConfig {
// Configuration fields and message ports
}
Constructing oldModule.oldModule() from Python then issues the standard
Basilisk deprecation warning. The warning does not prevent the module from
being configured, connected, or scheduled during its deprecation period. It
becomes BSKUrgentDeprecationWarning on or after the removal date.
Python-Visible Configuration Fields
A non-message-port configuration field in a native Rust Basilisk module can be deprecated as follows:
#[bsk(deprecated(
removal_date = "2099/05/05",
message = "Use newGain instead."
))]
pub oldGain: f64,
Reading or assigning oldGain from Python issues Basilisk’s standard dated
deprecation warning. The generated getOldGain() and setOldGain() methods
issue the same warning. The field remains functional during the deprecation
period, so existing scripts do not immediately break.
The generated wrapper calls the same deprecated.deprecationWarn() function
used by the C++ deprecation wrappers. Before the removal date it emits
BSKDeprecationWarning. On or after that date it automatically emits the
bold-red BSKUrgentDeprecationWarning.
The removal date must use YYYY/MM/DD. As with Python and C++ deprecations,
the message should name the preferred replacement and provide enough
information for users to migrate. A deprecated field may also retain a
validator:
#[bsk(
validate = validate_old_gain,
deprecated(
removal_date = "2099/05/05",
message = "Use newGain instead."
)
)]
pub oldGain: f64,
The #[bsk(deprecated(...))] annotation currently applies only to
Python-visible, non-port configuration fields. Direct access to the field from
within Rust does not issue this Python runtime warning.
Rust-Only Modules, Methods, and Other Items
Use Rust’s standard deprecated attribute for an API consumed by other Rust code:
#[deprecated(
since = "2.13.0",
note = "Use calculate_new_control instead."
)]
pub fn calculate_old_control() {
// Retained implementation
}
Rust emits a compiler warning when Rust code uses this function, and
rustdoc identifies it as deprecated. The attribute can also be applied to
an inherent method, type, structure field, or Rust module. Deprecating a Rust
module causes its child items to inherit the deprecation.
Do not apply #[deprecated] to the init, reset, or update
methods in a BskModule trait implementation. These are Basilisk framework
callbacks rather than user-facing methods, and Rust deprecation attributes
cannot usefully deprecate individual trait implementation methods.
Current Support Summary
Interface |
Current mechanism |
Warning audience |
|---|---|---|
Python-visible Rust configuration field |
|
Python property, getter, and setter users |
Rust helper function, inherent method, type, or Rust module |
|
Rust callers during compilation |
Entire Python-visible Rust Basilisk module |
|
Python users constructing the module |
Python-visible Rust message port |
Not currently generated |
Document the deprecation until port-level wrapper support is added |
Python-visible custom Rust method |
Not currently generated |
Generated configuration getters and setters are covered by their field |
Thus, a user-settable Rust module parameter can be deprecated today with a
Basilisk runtime warning, as can construction of an entire Rust Basilisk
module. Rust-only modules and methods can be deprecated for Rust callers.
Message ports and future custom methods exposed through the generated Python
wrapper require additional generator support. Adding #[deprecated] to the
Rust configuration structure alone is not a substitute because Python users
will not see that compiler warning.