Source code for linkml.utils.deprecation

"""
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 :class:`.Deprecation` object within the `DEPRECATIONS` tuple
- Use the :func:`.deprecation_warning` function wherever the deprecated feature would be used to emit the warning

"""

import re
import warnings
from dataclasses import dataclass
from importlib.metadata import version
from typing import Optional

# Stolen from https://github.com/pypa/packaging/blob/main/src/packaging/version.py
# Updated to include major, minor, and patch versions
PEP440_PATTERN = r"""
    v?
    (?:
        (?:(?P<epoch>[0-9]+)!)?                           # epoch
        (?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)
        (?P<pre>                                          # pre-release
            [-_\.]?
            (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
            [-_\.]?
            (?P<pre_n>[0-9]+)?
        )?
        (?P<post>                                         # post release
            (?:-(?P<post_n1>[0-9]+))
            |
            (?:
                [-_\.]?
                (?P<post_l>post|rev|r)
                [-_\.]?
                (?P<post_n2>[0-9]+)?
            )
        )?
        (?P<dev>                                          # dev release
            [-_\.]?
            (?P<dev_l>dev)
            [-_\.]?
            (?P<dev_n>[0-9]+)?
        )?
    )
    (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
"""
PEP440 = re.compile(r"^\s*" + PEP440_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)


[docs]@dataclass class SemVer: """ 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 """ major: int = 0 minor: int = 0 patch: int = 0 epoch: Optional[int] = None pre: Optional[str] = None pre_l: Optional[str] = None pre_n: Optional[str] = None post: Optional[str] = None post_n1: Optional[str] = None post_l: Optional[str] = None post_n2: Optional[str] = None dev: Optional[str] = None dev_l: Optional[str] = None dev_n: Optional[str] = None local: Optional[str] = None def __post_init__(self): self.major = int(self.major) self.minor = int(self.minor) self.patch = int(self.patch)
[docs] @classmethod def from_str(cls, v: str) -> Optional["SemVer"]: """ Create a SemVer from a string using `PEP 440 <https://peps.python.org/pep-0440/>`_ syntax. Examples: .. code-block:: python >>> version = SemVer.from_str("v0.1.0") >>> print(version) 0.1.0 """ match = PEP440.search(v) if match is None: return None return SemVer(**match.groupdict())
[docs] @classmethod def from_package(cls, package: str) -> "SemVer": """Get semver from package name""" v = version(package) return SemVer.from_str(v)
def __eq__(self, other: "SemVer"): return self.major == other.major and self.minor == other.minor and self.patch == other.patch def __lt__(self, other: "SemVer"): # fall through each if elif only if version component is equal for field in ("major", "minor", "patch"): if getattr(self, field) < getattr(other, field): return True elif getattr(self, field) > getattr(other, field): return False # otherwise, equal (which is False) return False def __gt__(self, other: "SemVer"): return not (self < other) and not (self == other) def __le__(self, other: "SemVer"): return (self < other) or (self == other) def __ge__(self, other: "SemVer"): return (self > other) or (self == other) def __str__(self) -> str: return ".".join([str(item) for item in [self.major, self.minor, self.patch]])
[docs]@dataclass class Deprecation: """ Parameterization of a deprecation. """ name: str """Shorthand, unique name used to refer to this deprecation""" message: str """Message to be displayed explaining the deprecation""" deprecated_in: SemVer """Version that the feature was deprecated in""" removed_in: Optional[SemVer] = None """Version that the feature will be removed in""" recommendation: Optional[str] = None """Recommendation about what to do to replace the deprecated behavior""" issue: Optional[int] = None """GitHub version describing deprecation""" def __post_init__(self): if self.deprecated_in is not None and isinstance(self.deprecated_in, str): self.deprecated_in = SemVer.from_str(self.deprecated_in) if self.removed_in is not None and isinstance(self.removed_in, str): self.removed_in = SemVer.from_str(self.removed_in) def __str__(self) -> str: msg = f"[{self.name}] " if self.removed: msg += "REMOVED" elif self.deprecated: msg += "DEPRECATED" msg += f"\n{self.message}" msg += f"\nDeprecated In: {str(self.deprecated_in)}" if self.removed_in is not None: msg += f"\nRemoved In: {str(self.removed_in)}" if self.recommendation is not None: msg += f"\nRecommendation: {self.recommendation}" if self.issue is not None: msg += f"\nSee: https://github.com/linkml/linkml/issues/{self.issue}" return msg @property def deprecated(self) -> bool: return SemVer.from_package("linkml") >= self.deprecated_in @property def removed(self) -> bool: if self.removed_in is None: return False return SemVer.from_package("linkml") >= self.removed_in
[docs] def warn(self, **kwargs): if self.deprecated: warnings.warn(message=str(self), category=DeprecationWarning, stacklevel=3, **kwargs)
DEPRECATIONS = ( Deprecation( name="pydanticgen-v1", deprecated_in=SemVer.from_str("1.7.5"), removed_in=SemVer.from_str("1.8.0"), message="Support for generating Pydantic v1.*.* models with pydanticgen is deprecated", recommendation="Migrate any existing models to Pydantic v2", issue=1925, ), Deprecation( name="pydantic-v1", deprecated_in=SemVer.from_str("1.7.5"), removed_in=SemVer.from_str("1.9.0"), message=( "LinkML will set a dependency of pydantic>=2 and become incompatible " "with packages with pydantic<2 as a runtime dependency" ), recommendation="Update dependent packages to use pydantic>=2", issue=1925, ), ) # type: tuple[Deprecation, ...] EMITTED = set() # type: set[str]
[docs]def deprecation_warning(name: str): """ 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 """ global DEPRECATIONS global EMITTED dep = [dep for dep in DEPRECATIONS if dep.name == name] if len(dep) == 1: dep = dep[0] elif len(dep) > 1: raise RuntimeError(f"Duplicate deprecations found with name {name}") else: EMITTED.add(name) return if dep.name not in EMITTED: dep.warn() EMITTED.add(name)