diff --git a/libs/core/langchain_core/_api/deprecation.py b/libs/core/langchain_core/_api/deprecation.py index b4d56b70cb4..970cfa56946 100644 --- a/libs/core/langchain_core/_api/deprecation.py +++ b/libs/core/langchain_core/_api/deprecation.py @@ -245,10 +245,9 @@ def deprecated( old_doc = inspect.cleandoc(old_doc or "").strip("\n") + # old_doc can be None if not old_doc: - new_doc = "[*Deprecated*]" - else: - new_doc = f"[*Deprecated*] {old_doc}" + old_doc = "" # Modify the docstring to include a deprecation notice. notes_header = "\nNotes\n-----" @@ -258,7 +257,7 @@ def deprecated( addendum, ] details = " ".join([component.strip() for component in components if component]) - new_doc += ( + new_doc = ( f"[*Deprecated*] {old_doc}\n" f"{notes_header if notes_header not in old_doc else ''}\n" f".. deprecated:: {since}\n" diff --git a/libs/core/tests/unit_tests/_api/test_deprecation.py b/libs/core/tests/unit_tests/_api/test_deprecation.py index d26b18c3ad4..8573d64b379 100644 --- a/libs/core/tests/unit_tests/_api/test_deprecation.py +++ b/libs/core/tests/unit_tests/_api/test_deprecation.py @@ -129,7 +129,7 @@ def test_deprecated_function() -> None: doc = deprecated_function.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc") assert not inspect.iscoroutinefunction(deprecated_function) @@ -151,7 +151,7 @@ async def test_deprecated_async_function() -> None: doc = deprecated_function.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc") assert inspect.iscoroutinefunction(deprecated_async_function) @@ -171,7 +171,7 @@ def test_deprecated_method() -> None: doc = obj.deprecated_method.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc") assert not inspect.iscoroutinefunction(obj.deprecated_method) @@ -194,7 +194,7 @@ async def test_deprecated_async_method() -> None: doc = obj.deprecated_method.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc") assert inspect.iscoroutinefunction(obj.deprecated_async_method) @@ -213,7 +213,7 @@ def test_deprecated_classmethod() -> None: doc = ClassWithDeprecatedMethods.deprecated_classmethod.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc") def test_deprecated_staticmethod() -> None: @@ -233,7 +233,7 @@ def test_deprecated_staticmethod() -> None: ) doc = ClassWithDeprecatedMethods.deprecated_staticmethod.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc") def test_deprecated_property() -> None: @@ -253,7 +253,7 @@ def test_deprecated_property() -> None: ) doc = ClassWithDeprecatedMethods.deprecated_property.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc") def test_whole_class_deprecation() -> None: @@ -289,6 +289,88 @@ def test_whole_class_deprecation() -> None: "The function `deprecated_method` was deprecated in " "LangChain 2.0.0 and will be removed in 3.0.0" ) + # [*Deprecated*] should be inserted only once: + if obj.__doc__ is not None: + assert obj.__doc__.count("[*Deprecated*]") == 1 + + +def test_whole_class_inherited_deprecation() -> None: + """Test whole class deprecation for inherited class. + + The original version of deprecation decorator created duplicates with + '[*Deprecated*]'. + """ + + # 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." + + @deprecated(since="2.2.0", removal="3.2.0") + class InheritedDeprecatedClass(DeprecatedClass): + """Inherited deprecated class.""" + + def __init__(self) -> None: + """original doc""" + pass + + @deprecated(since="2.2.0", removal="3.2.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 `tests.unit_tests._api.test_deprecation.DeprecatedClass` was " + "deprecated in tests 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" + ) + # if [*Deprecated*] was inserted only once: + if obj.__doc__ is not None: + assert obj.__doc__.count("[*Deprecated*]") == 1 + + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + + obj = InheritedDeprecatedClass() + assert obj.deprecated_method() == "This is a deprecated method." + + assert len(warning_list) == 2 + warning = warning_list[0].message + assert str(warning) == ( + "The class " + "`tests.unit_tests._api.test_deprecation.InheritedDeprecatedClass` " + "was deprecated in tests 2.2.0 and will be removed in 3.2.0" + ) + + warning = warning_list[1].message + assert str(warning) == ( + "The function `deprecated_method` was deprecated in " + "LangChain 2.2.0 and will be removed in 3.2.0" + ) + # if [*Deprecated*] was inserted only once: + if obj.__doc__ is not None: + assert obj.__doc__.count("[*Deprecated*]") == 1 + assert "[*Deprecated*] Inherited deprecated class." in obj.__doc__ # Tests with pydantic models @@ -314,4 +396,4 @@ def test_deprecated_method_pydantic() -> None: doc = obj.deprecated_method.__doc__ assert isinstance(doc, str) - assert doc.startswith("[*Deprecated*] original doc") + assert doc.startswith("[*Deprecated*] original doc")