diff --git a/libs/core/langchain_core/_api/deprecation.py b/libs/core/langchain_core/_api/deprecation.py index 5a3e90b53983d..ede02915fd804 100644 --- a/libs/core/langchain_core/_api/deprecation.py +++ b/libs/core/langchain_core/_api/deprecation.py @@ -28,6 +28,27 @@ from langchain_core._api.internal import is_caller_internal +def _build_deprecation_message( + *, + alternative: str = "", + alternative_import: str = "", +) -> str: + """Build a simple deprecation message for `__deprecated__` attribute. + + Args: + alternative: An alternative API name. + alternative_import: A fully qualified import path for the alternative. + + Returns: + A deprecation message string for IDE/type checker display. + """ + if alternative_import: + return f"Use {alternative_import} instead." + if alternative: + return f"Use {alternative} instead." + return "Deprecated." + + class LangChainDeprecationWarning(DeprecationWarning): """A class for issuing deprecation warnings for LangChain users.""" @@ -81,60 +102,57 @@ def deprecated( ) -> 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 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__`). + 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. + 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. Args: - since: - The release at which this API became deprecated. - message: - 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 + since: The release at which this API became deprecated. + message: 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: - The name of the deprecated object. - alternative: - 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. - alternative_import: - An alternative import that the user may use instead. - pending: - If `True`, uses a `PendingDeprecationWarning` instead of a - DeprecationWarning. Cannot be used together with removal. - obj_type: - The object type being deprecated. - addendum: - Additional text appended directly to the final message. - removal: - 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. - package: - The package of the deprecated object. + name: The name of the deprecated object. + alternative: 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. + alternative_import: An alternative import that the user may use instead. + pending: If `True`, uses a `PendingDeprecationWarning` instead of a + `DeprecationWarning`. + + Cannot be used together with removal. + obj_type: The object type being deprecated. + addendum: Additional text appended directly to the final message. + removal: 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. + package: The package of the deprecated object. Returns: A decorator to mark a function or class as deprecated. - ```python - @deprecated("1.4.0") - def the_function_to_deprecate(): - pass - ``` + Example: + ```python + @deprecated("1.4.0") + def the_function_to_deprecate(): + pass + ``` """ _validate_deprecation_params( removal, alternative, alternative_import, pending=pending @@ -223,6 +241,11 @@ def warn_if_direct_instance( obj.__init__ = functools.wraps(obj.__init__)( # type: ignore[misc] warn_if_direct_instance ) + # Set __deprecated__ for PEP 702 (IDE/type checker support) + obj.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined] + alternative=alternative, + alternative_import=alternative_import, + ) return obj elif isinstance(obj, FieldInfoV1): @@ -315,12 +338,15 @@ def __set_name__(self, owner: type | None, set_name: str) -> None: def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001 """Finalize the property.""" - return cast( - "T", - _DeprecatedProperty( - fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc - ), + prop = _DeprecatedProperty( + fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc ) + # Set __deprecated__ for PEP 702 (IDE/type checker support) + prop.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined] + alternative=alternative, + alternative_import=alternative_import, + ) + return cast("T", prop) else: _name = _name or cast("type | Callable", obj).__qualname__ @@ -343,6 +369,11 @@ def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: """ wrapper = functools.wraps(wrapped)(wrapper) wrapper.__doc__ = new_doc + # Set __deprecated__ for PEP 702 (IDE/type checker support) + wrapper.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined] + alternative=alternative, + alternative_import=alternative_import, + ) return cast("T", wrapper) old_doc = inspect.cleandoc(old_doc or "").strip("\n") @@ -398,7 +429,7 @@ def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: @contextlib.contextmanager def suppress_langchain_deprecation_warning() -> Generator[None, None, None]: - """Context manager to suppress LangChainDeprecationWarning.""" + """Context manager to suppress `LangChainDeprecationWarning`.""" with warnings.catch_warnings(): warnings.simplefilter("ignore", LangChainDeprecationWarning) warnings.simplefilter("ignore", LangChainPendingDeprecationWarning) @@ -421,35 +452,33 @@ def warn_deprecated( """Display a standardized deprecation. Args: - since: - The release at which this API became deprecated. - message: - 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 + since: The release at which this API became deprecated. + message: 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: - The name of the deprecated object. - alternative: - 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. - alternative_import: - An alternative import that the user may use instead. - pending: - If `True`, uses a `PendingDeprecationWarning` instead of a - DeprecationWarning. Cannot be used together with removal. - obj_type: - The object type being deprecated. - addendum: - Additional text appended directly to the final message. - removal: - 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. - package: - The package of the deprecated object. + name: The name of the deprecated object. + alternative: 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. + alternative_import: An alternative import that the user may use instead. + pending: If `True`, uses a `PendingDeprecationWarning` instead of a + `DeprecationWarning`. + + Cannot be used together with removal. + obj_type: The object type being deprecated. + addendum: Additional text appended directly to the final message. + removal: 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. + package: The package of the deprecated object. """ if not pending: if not removal: @@ -534,8 +563,8 @@ def rename_parameter( """Decorator indicating that parameter *old* of *func* is renamed to *new*. The actual implementation of *func* should use *new*, not *old*. If *old* is passed - to *func*, a DeprecationWarning is emitted, and its value is used, even if *new* is - also passed by keyword. + to *func*, a `DeprecationWarning` is emitted, and its value is used, even if *new* + is also passed by keyword. Args: since: The version in which the parameter was renamed. diff --git a/libs/core/tests/unit_tests/_api/test_deprecation.py b/libs/core/tests/unit_tests/_api/test_deprecation.py index 3826da5980613..66895eeeae843 100644 --- a/libs/core/tests/unit_tests/_api/test_deprecation.py +++ b/libs/core/tests/unit_tests/_api/test_deprecation.py @@ -493,3 +493,88 @@ def a(self, new_name: str) -> str: with pytest.raises(TypeError): assert foo.a("hello", old_name="hello") # type: ignore[call-arg] + + +# Tests for PEP 702 __deprecated__ attribute + + +def test_deprecated_function_has_pep702_attribute() -> None: + """Test that deprecated functions have `__deprecated__` attribute.""" + + @deprecated(since="2.0.0", removal="3.0.0", alternative="new_function") + def old_function() -> str: + """Original doc.""" + return "old" + + assert hasattr(old_function, "__deprecated__") + assert old_function.__deprecated__ == "Use new_function instead." + + +def test_deprecated_function_with_alternative_import_has_pep702_attribute() -> None: + """Test `__deprecated__` with `alternative_import`.""" + + @deprecated( + since="2.0.0", removal="3.0.0", alternative_import="new_module.new_function" + ) + def old_function() -> str: + """Original doc.""" + return "old" + + assert hasattr(old_function, "__deprecated__") + assert old_function.__deprecated__ == "Use new_module.new_function instead." + + +def test_deprecated_function_without_alternative_has_pep702_attribute() -> None: + """Test `__deprecated__` without alternative shows `'Deprecated.'`.""" + + @deprecated(since="2.0.0", removal="3.0.0") + def old_function() -> str: + """Original doc.""" + return "old" + + assert hasattr(old_function, "__deprecated__") + assert old_function.__deprecated__ == "Deprecated." + + +def test_deprecated_class_has_pep702_attribute() -> None: + """Test that deprecated classes have `__deprecated__` attribute (PEP 702).""" + + @deprecated(since="2.0.0", removal="3.0.0", alternative="NewClass") + class OldClass: + def __init__(self) -> None: + """Original doc.""" + + assert hasattr(OldClass, "__deprecated__") + assert OldClass.__deprecated__ == "Use NewClass instead." + + +def test_deprecated_class_without_alternative_has_pep702_attribute() -> None: + """Test `__deprecated__` on class without alternative.""" + + @deprecated(since="2.0.0", removal="3.0.0") + class OldClass: + def __init__(self) -> None: + """Original doc.""" + + assert hasattr(OldClass, "__deprecated__") + assert OldClass.__deprecated__ == "Deprecated." + + +def test_deprecated_property_has_pep702_attribute() -> None: + """Test that deprecated properties have `__deprecated__` attribute (PEP 702). + + Note: When using @property over @deprecated (which is what works in practice), + the `__deprecated__` attribute is set on the property's underlying `fget` function. + """ + + class MyClass: + @property + @deprecated(since="2.0.0", removal="3.0.0", alternative="new_property") + def old_property(self) -> str: + """Original doc.""" + return "old" + + prop = MyClass.__dict__["old_property"] + # The __deprecated__ attribute is on the underlying fget function + assert hasattr(prop.fget, "__deprecated__") + assert prop.fget.__deprecated__ == "Use new_property instead."