Internal code deprecation API (#8763)

Proposal for an internal API to deprecate LangChain code.

This PR is heavily based on:
https://github.com/matplotlib/matplotlib/blob/main/lib/matplotlib/_api/deprecation.py

This PR only includes deprecation functionality (no renaming etc.). 
Additional functionality can be added on a need basis (e.g., renaming
parameters), but best to roll out as an MVP to test this
out.

DeprecationWarnings are ignored by default. We can change the policy for
the deprecation warnings, but we'll need to make sure we're not creating
noise for users due to internal code invoking deprecated functionality.
pull/8932/head
Eugene Yurtsev 11 months ago committed by GitHub
parent 33a2f58fbf
commit bb12184551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,22 @@
"""Helper functions for managing the LangChain API.
This module is only relevant for LangChain developers, not for users.
.. warning::
This module and its submodules are for internal use only. Do not use them
in your own code. We may change the API at any time with no warning.
"""
from .deprecation import (
LangChainDeprecationWarning,
deprecated,
suppress_langchain_deprecation_warning,
)
__all__ = [
"deprecated",
"LangChainDeprecationWarning",
"suppress_langchain_deprecation_warning",
]

@ -0,0 +1,306 @@
"""Helper functions for deprecating parts of the LangChain API.
This module was adapted from matplotlibs _api/deprecation.py module:
https://github.com/matplotlib/matplotlib/blob/main/lib/matplotlib/_api/deprecation.py
.. warning::
This module is for internal use only. Do not use it in your own code.
We may change the API at any time with no warning.
"""
import contextlib
import functools
import inspect
import warnings
from typing import Any, Callable, Generator, Type, TypeVar
class LangChainDeprecationWarning(DeprecationWarning):
"""A class for issuing deprecation warnings for LangChain users."""
def _warn_deprecated(
since: str,
*,
message: str = "",
name: str = "",
alternative: str = "",
pending: bool = False,
obj_type: str = "",
addendum: str = "",
removal: str = "",
) -> None:
"""Display a standardized deprecation.
Arguments:
since : str
The release at which this API became deprecated.
message : str, optional
Override the default deprecation message. The %(since)s,
%(name)s, %(alternative)s, %(obj_type)s, %(addendum)s,
and %(removal)s format specifiers will be replaced by the
values of the respective arguments passed to this function.
name : str, optional
The name of the deprecated object.
alternative : str, optional
An alternative API that the user may use in place of the
deprecated API. The deprecation warning will tell the user
about this alternative if provided.
pending : bool, optional
If True, uses a PendingDeprecationWarning instead of a
DeprecationWarning. Cannot be used together with removal.
obj_type : str, optional
The object type being deprecated.
addendum : str, optional
Additional text appended directly to the final message.
removal : str, optional
The expected removal version. With the default (an empty
string), a removal version is automatically computed from
since. Set to other Falsy values to not schedule a removal
date. Cannot be used together with pending.
"""
if pending and removal:
raise ValueError("A pending deprecation cannot have a scheduled removal")
if not pending:
if not removal:
removal = f"in {removal}" if removal else "within ?? minor releases"
raise NotImplementedError(
f"Need to determine which default deprecation schedule to use. "
f"{removal}"
)
else:
removal = f"in {removal}"
if not message:
message = ""
if obj_type:
message += f"The {obj_type} `{name}`"
else:
message += f"`{name}`"
if pending:
message += " will be deprecated in a future version"
else:
message += f" was deprecated in LangChain {since}"
if removal:
message += f" and will be removed {removal}"
if alternative:
message += f". Use {alternative} instead."
if addendum:
message += f" {addendum}"
warning_cls = PendingDeprecationWarning if pending else LangChainDeprecationWarning
warning = warning_cls(message)
warnings.warn(warning, category=LangChainDeprecationWarning, stacklevel=2)
# PUBLIC API
T = TypeVar("T", Type, Callable)
def deprecated(
since: str,
*,
message: str = "",
name: str = "",
alternative: str = "",
pending: bool = False,
obj_type: str = "",
addendum: str = "",
removal: str = "",
) -> Callable[[T], T]:
"""Decorator to mark a function, a class, or a property as deprecated.
When deprecating a classmethod, a staticmethod, or a property, the
``@deprecated`` decorator should go *under* ``@classmethod`` and
``@staticmethod`` (i.e., `deprecated` should directly decorate the
underlying callable), but *over* ``@property``.
When deprecating a class ``C`` intended to be used as a base class in a
multiple inheritance hierarchy, ``C`` *must* define an ``__init__`` method
(if ``C`` instead inherited its ``__init__`` from its own base class, then
``@deprecated`` would mess up ``__init__`` inheritance when installing its
own (deprecation-emitting) ``C.__init__``).
Parameters are the same as for `warn_deprecated`, except that *obj_type*
defaults to 'class' if decorating a class, 'attribute' if decorating a
property, and 'function' otherwise.
Arguments:
since : str
The release at which this API became deprecated.
message : str, optional
Override the default deprecation message. The %(since)s,
%(name)s, %(alternative)s, %(obj_type)s, %(addendum)s,
and %(removal)s format specifiers will be replaced by the
values of the respective arguments passed to this function.
name : str, optional
The name of the deprecated object.
alternative : str, optional
An alternative API that the user may use in place of the
deprecated API. The deprecation warning will tell the user
about this alternative if provided.
pending : bool, optional
If True, uses a PendingDeprecationWarning instead of a
DeprecationWarning. Cannot be used together with removal.
obj_type : str, optional
The object type being deprecated.
addendum : str, optional
Additional text appended directly to the final message.
removal : str, optional
The expected removal version. With the default (an empty
string), a removal version is automatically computed from
since. Set to other Falsy values to not schedule a removal
date. Cannot be used together with pending.
Examples
--------
.. code-block:: python
@deprecated('1.4.0')
def the_function_to_deprecate():
pass
"""
def deprecate(
obj: T,
*,
_obj_type: str = obj_type,
_name: str = name,
_message: str = message,
_alternative: str = alternative,
_pending: bool = pending,
_addendum: str = addendum,
) -> T:
"""Implementation of the decorator returned by `deprecated`."""
if isinstance(obj, type):
if not _obj_type:
_obj_type = "class"
wrapped = obj.__init__ # type: ignore
_name = _name or obj.__name__
old_doc = obj.__doc__
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T:
"""Finalize the deprecation of a class."""
try:
obj.__doc__ = new_doc
except AttributeError: # Can't set on some extension objects.
pass
obj.__init__ = functools.wraps(obj.__init__)( # type: ignore[misc]
wrapper
)
return obj
elif isinstance(obj, property):
if not _obj_type:
_obj_type = "attribute"
wrapped = None
_name = _name or obj.fget.__name__
old_doc = obj.__doc__
class _deprecated_property(type(obj)): # type: ignore
"""A deprecated property."""
def __get__(self, instance, owner=None): # type: ignore
if instance is not None or owner is not None:
emit_warning()
return super().__get__(instance, owner)
def __set__(self, instance, value): # type: ignore
if instance is not None:
emit_warning()
return super().__set__(instance, value)
def __delete__(self, instance): # type: ignore
if instance is not None:
emit_warning()
return super().__delete__(instance)
def __set_name__(self, owner, set_name): # type: ignore
nonlocal _name
if _name == "<lambda>":
_name = set_name
def finalize(_: Any, new_doc: str) -> Any: # type: ignore
"""Finalize the property."""
return _deprecated_property(
fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc
)
else:
if not _obj_type:
_obj_type = "function"
wrapped = obj
_name = _name or obj.__name__ # type: ignore
old_doc = wrapped.__doc__
def finalize( # type: ignore
wrapper: Callable[..., Any], new_doc: str
) -> T:
"""Wrap the wrapped function using the wrapper and update the docstring.
Args:
wrapper: The wrapper function.
new_doc: The new docstring.
Returns:
The wrapped function.
"""
wrapper = functools.wraps(wrapped)(wrapper)
wrapper.__doc__ = new_doc
return wrapper
def emit_warning() -> None:
"""Emit the warning."""
_warn_deprecated(
since,
message=_message,
name=_name,
alternative=_alternative,
pending=_pending,
obj_type=_obj_type,
addendum=_addendum,
removal=removal,
)
def warning_emitting_wrapper(*args: Any, **kwargs: Any) -> Any:
"""Wrapper for the original wrapped callable that emits a warning.
Args:
*args: The positional arguments to the function.
**kwargs: The keyword arguments to the function.
Returns:
The return value of the function being wrapped.
"""
emit_warning()
return wrapped(*args, **kwargs)
old_doc = inspect.cleandoc(old_doc or "").strip("\n")
if not old_doc:
new_doc = "[*Deprecated*]"
else:
new_doc = f"[*Deprecated*] {old_doc}"
return finalize(warning_emitting_wrapper, new_doc)
return deprecate
@contextlib.contextmanager
def suppress_langchain_deprecation_warning() -> Generator[None, None, None]:
"""Context manager to suppress LangChainDeprecationWarning."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", LangChainDeprecationWarning)
yield

@ -0,0 +1,252 @@
import warnings
from typing import Any, Dict
import pytest
from pydantic import BaseModel
from langchain._api.deprecation import _warn_deprecated, deprecated
@pytest.mark.parametrize(
"kwargs, expected_message",
[
(
{
"since": "1.0.0",
"name": "OldClass",
"alternative": "NewClass",
"pending": True,
"obj_type": "class",
},
"The class `OldClass` will be deprecated in a future version. Use NewClass "
"instead.",
),
(
{
"since": "2.0.0",
"message": "This is a custom message",
"name": "FunctionA",
"alternative": "",
"pending": True,
"obj_type": "",
"addendum": "",
"removal": "",
},
"This is a custom message",
),
(
{
"since": "1.5.0",
"message": "",
"name": "SomeFunction",
"alternative": "",
"pending": False,
"obj_type": "",
"addendum": "Please migrate your code.",
"removal": "2.5.0",
},
"`SomeFunction` was deprecated in LangChain 1.5.0 and will be "
"removed in 2.5.0 Please migrate your code.",
),
],
)
def test_warn_deprecated(kwargs: Dict[str, Any], expected_message: str) -> None:
"""Test warn deprecated."""
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
_warn_deprecated(**kwargs)
assert len(warning_list) == 1
warning = warning_list[0].message
assert str(warning) == expected_message
def test_undefined_deprecation_schedule() -> None:
"""This test is expected to fail until we defined a deprecation schedule."""
with pytest.raises(NotImplementedError):
_warn_deprecated("1.0.0", pending=False)
@deprecated(since="2.0.0", removal="3.0.0", pending=False)
def deprecated_function() -> str:
"""original doc"""
return "This is a deprecated function."
class ClassWithDeprecatedMethods:
def __init__(self) -> None:
"""original doc"""
pass
@deprecated(since="2.0.0", removal="3.0.0")
def deprecated_method(self) -> str:
"""original doc"""
return "This is a deprecated method."
@classmethod
@deprecated(since="2.0.0", removal="3.0.0")
def deprecated_classmethod(cls) -> str:
"""original doc"""
return "This is a deprecated classmethod."
@staticmethod
@deprecated(since="2.0.0", removal="3.0.0")
def deprecated_staticmethod() -> str:
"""original doc"""
return "This is a deprecated staticmethod."
@property
@deprecated(since="2.0.0", removal="3.0.0")
def deprecated_property(self) -> str:
"""original doc"""
return "This is a deprecated property."
def test_deprecated_function() -> None:
"""Test deprecated function."""
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
assert deprecated_function() == "This is a deprecated function."
assert len(warning_list) == 1
warning = warning_list[0].message
assert str(warning) == (
"The function `deprecated_function` was deprecated in LangChain 2.0.0 "
"and will be removed in 3.0.0"
)
assert deprecated_function.__doc__ == "[*Deprecated*] original doc"
def test_deprecated_method() -> None:
"""Test deprecated method."""
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
obj = ClassWithDeprecatedMethods()
assert obj.deprecated_method() == "This is a deprecated method."
assert len(warning_list) == 1
warning = warning_list[0].message
assert str(warning) == (
"The function `deprecated_method` was deprecated in "
"LangChain 2.0.0 and will be removed in 3.0.0"
)
assert obj.deprecated_method.__doc__ == "[*Deprecated*] original doc"
def test_deprecated_classmethod() -> None:
"""Test deprecated classmethod."""
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
ClassWithDeprecatedMethods.deprecated_classmethod()
assert len(warning_list) == 1
warning = warning_list[0].message
assert str(warning) == (
"The function `deprecated_classmethod` was deprecated in "
"LangChain 2.0.0 and will be removed in 3.0.0"
)
assert (
ClassWithDeprecatedMethods.deprecated_classmethod.__doc__
== "[*Deprecated*] original doc"
)
def test_deprecated_staticmethod() -> None:
"""Test deprecated staticmethod."""
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
assert (
ClassWithDeprecatedMethods.deprecated_staticmethod()
== "This is a deprecated staticmethod."
)
assert len(warning_list) == 1
warning = warning_list[0].message
assert str(warning) == (
"The function `deprecated_staticmethod` was deprecated in "
"LangChain 2.0.0 and will be removed in 3.0.0"
)
assert (
ClassWithDeprecatedMethods.deprecated_staticmethod.__doc__
== "[*Deprecated*] original doc"
)
def test_deprecated_property() -> None:
"""Test deprecated staticmethod."""
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
obj = ClassWithDeprecatedMethods()
assert obj.deprecated_property == "This is a deprecated property."
assert len(warning_list) == 1
warning = warning_list[0].message
assert str(warning) == (
"The function `deprecated_property` was deprecated in "
"LangChain 2.0.0 and will be removed in 3.0.0"
)
assert (
ClassWithDeprecatedMethods.deprecated_property.__doc__
== "[*Deprecated*] original doc"
)
def test_whole_class_deprecation() -> None:
"""Test whole class deprecation."""
# Test whole class deprecation
@deprecated(since="2.0.0", removal="3.0.0")
class DeprecatedClass:
def __init__(self) -> None:
"""original doc"""
pass
@deprecated(since="2.0.0", removal="3.0.0")
def deprecated_method(self) -> str:
"""original doc"""
return "This is a deprecated method."
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
obj = DeprecatedClass()
assert obj.deprecated_method() == "This is a deprecated method."
assert len(warning_list) == 2
warning = warning_list[0].message
assert str(warning) == (
"The class `DeprecatedClass` was deprecated in "
"LangChain 2.0.0 and will be removed in 3.0.0"
)
warning = warning_list[1].message
assert str(warning) == (
"The function `deprecated_method` was deprecated in "
"LangChain 2.0.0 and will be removed in 3.0.0"
)
# Tests with pydantic models
class MyModel(BaseModel):
@deprecated(since="2.0.0", removal="3.0.0")
def deprecated_method(self) -> str:
"""original doc"""
return "This is a deprecated method."
def test_deprecated_method_pydantic() -> None:
"""Test deprecated method."""
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter("always")
obj = MyModel()
assert obj.deprecated_method() == "This is a deprecated method."
assert len(warning_list) == 1
warning = warning_list[0].message
assert str(warning) == (
"The function `deprecated_method` was deprecated in "
"LangChain 2.0.0 and will be removed in 3.0.0"
)
assert obj.deprecated_method.__doc__ == "[*Deprecated*] original doc"
Loading…
Cancel
Save