Deprecations#
Guidelines#
What to Deprecate#
All of the following should be given deprecation warnings that give downstream packages enough time to adapt.
Breaking changes#
All breaking changes to public classes and functions.
“Public” is not strictly defined in linkml
, but in general these are things
made available via CLI, or things that are given their own packages/modules.
Breaking changes include (but are not limited to)
Removal of features, classes, functions, and parameters
Changes to defaults
Refactoring that changes import location
Dependencies#
Changes to dependencies typically do not need a deprecation warning, but
for updates that exclude a commonly used version with substantial differences
with the newer version, a warning should be emitted. As an example, the
deprecation
module was initially motivated by
the need to warn about deprecating support for Pydantic v1.
Supported Python Version#
All changes to the minimum supported python versions should be deprecated with at least a patch version of notice.
Duration of Deprecation#
The amount of time (or, the number of versions between when a feature is marked as deprecated and when it is removed) depends on the size of the change and the complexity of adapting to it, but typically they should coincide with a minor or major version.
Process#
There are two phases to a deprecation:
Deprecation - A feature is deprecated when it is marked for removal. No new code should use that feature, and its usage should emit a warning.
Removal - A previously deprecated feature is removed from the package.
All deprecations use the tools in the deprecation
module.
Declare the Deprecation#
To declare a deprecation, create a new Deprecation
class and add it to the
DEPRECATIONS
tuple in linkml.utils.deprecation
. The Deprecation
object contains
the information about what is being deprecated,
after which version it is considered deprecated
(Optional) which version it will be removed in
(Optional) what should be done instead
(Optional) an issue that contains further information.
For example, if we are deprecating a feature for rendering a linkml schema to semaphore, we might create an object like this:
deprecation.py
DEPRECATIONS = (
Deprecation(
name = "semaphore",
message = (
"Flag-based semaphore schemas were considered to not have a high "
"enough information capacity to usefully represent a linkml schema"),
recommendation = "Update to a digital schema representation or morse code",
deprecated_in = SemVer.from_str("1.7.5"),
removed_in = SemVer.from_str("1.8.0"),
issue = -1
),
)
Declaring the deprecation will add it to the Deprecation Log.
Mark the Deprecation#
Everywhere the deprecated feature would be used, add a deprecation_warning()
call
that uses the name
of the Deprecation
object.
This will emit a DeprecationWarning
that looks like this:
>>> deprecation_warning('semaphore')
[semaphore] DEPRECATED
Flag-based semaphore schemas were considered to not have a high enough information capacity to usefully represent a linkml schema
Deprecated In: 1.7.0
Removed In: 1.8.0
Recommendation: Update to a digital schema representation or morse code
See: https://github.com/linkml/linkml/issues/-1
This looks different depending on what is being deprecated, see this guide for further examples of how to deprecate various python language constructs.
Some examples:
Module#
semaphore.py
from linkml.utils import deprecation_warning
deprecation_warning('semaphore')
Function#
def render_semaphore(schema):
deprecation_warning('semaphore')
Class#
class MyClass:
def __init__(self):
deprecation_warning('semaphore')
Dependency Version#
eg. if we were deprecating all versions of pysemaphore<2.0.0
, in every place it is imported:
import pysemaphore
from linkml.utils.deprecation import SemVer
if SemVer.from_package('pysemaphore').major < 2:
deprecation_warning('semaphore')
Python Version#
In linkml/__init__.py
import sys
if sys.version_info.minor <= 8:
deprecation_warning('semaphore')
Default Value#
If we are changing a default value that is None
in the function signature
and filled in the function body:
def my_function(arg:Optional[str] = None):
if arg is None:
deprecation_warning('semaphore')
arg = 'old_default'
Or if we are dropping support for calling a function with a str
in favor of a Path
def my_function(arg: Union[Path, str]):
if isinstance(arg, str):
deprecation_warning('semaphore')
arg = Path(str)
It’s inelegant to check whether a python is called explicitly with a parameter or whether it is using the default, but it is possible:
import inspect
def my_function(arg = 'default'):
frame = inspect.currentframe()
outer = inspect.getouterframes(frame, 1)[1]
if arg not in outer.code_context[0]:
deprecation_warning('semaphore')
Removal#
LinkML’s deprecation warnings are tested: when the version where a deprecation is
marked to be removed is reached, the tests will fail (specifically
tests/test_utils/test_deprecation.py::test_removed_are_removed
).
Remove all deprecated functionality and the calls to deprecation_warning()
, but
leave the Deprecation
class declaration in place as a record.
API#
Utilities for deprecating functionality and dependencies.
Emitting DeprecationWarnings
Tracking deprecated and removed in versions
Fail tests when something marked as removed_in is still present in the specified version
Initial draft for deprecating Pydantic 1, to make more general, needs - function wrapper version - …
To deprecate something:
Create a
Deprecation
object within the DEPRECATIONS tupleUse the
deprecation_warning()
function wherever the deprecated feature would be used to emit the warning
- class linkml.utils.deprecation.Deprecation(name: str, message: str, deprecated_in: SemVer, removed_in: SemVer | None = None, recommendation: str | None = None, issue: int | None = None, **_kwargs)[source]#
Parameterization of a deprecation.
- class linkml.utils.deprecation.SemVer(major: int = 0, minor: int = 0, patch: int = 0, epoch: int | None = None, pre: str | None = None, pre_l: str | None = None, pre_n: str | None = None, post: str | None = None, post_n1: str | None = None, post_l: str | None = None, post_n2: str | None = None, dev: str | None = None, dev_l: str | None = None, dev_n: str | None = None, local: str | None = None, **_kwargs)[source]#
Representation of semantic version that supports inequality comparisons.
Note
The inequality methods _only_ test the numeric major, minor, and patch components of the version - ie. they do not evaluate the prerelease versions as described in the semver spec. This is not intended to be a general SemVer inequality calculator, but used only for testing deprecations
- linkml.utils.deprecation.deprecation_warning(name: str)[source]#
Call this with the name of the deprecation object wherever the deprecated functionality will be used
This function will
emit a warning if the current version is greater than
deprecated_in
log that the deprecated feature was accessed in
EMITTED
for testing deprecations and muting warnings