From 94ca7001cdab7dcf7020ad6fe40a086903b89cad Mon Sep 17 00:00:00 2001 From: "Leon.C" <1169882492@qq.com> Date: Sat, 24 Oct 2026 03:22:12 +0800 Subject: [PATCH] fix(observe): detect methods by first parameter only --- langfuse/_client/observe.py | 6 +-- tests/unit/test_observe.py | 99 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/langfuse/_client/observe.py b/langfuse/_client/observe.py index 5aa88ce2e..8740ad0f1 100644 --- a/langfuse/_client/observe.py +++ b/langfuse/_client/observe.py @@ -437,10 +437,8 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Any: @staticmethod def _is_method(func: Callable) -> bool: - return ( - "self" in inspect.signature(func).parameters - or "cls" in inspect.signature(func).parameters - ) + first_parameter = next(iter(inspect.signature(func).parameters), None) + return first_parameter in ("self", "cls") def _get_input_from_func_args( self, diff --git a/tests/unit/test_observe.py b/tests/unit/test_observe.py index 21ff1418d..7be9c8a30 100644 --- a/tests/unit/test_observe.py +++ b/tests/unit/test_observe.py @@ -33,6 +33,105 @@ def _finished_spans_by_name(memory_exporter: Any, name: str) -> list[Any]: return [span for span in memory_exporter.get_finished_spans() if span.name == name] +def test_capture_input_free_function_keyword_only_cls( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + @observe(capture_input=True) + def classify(value: str, *, cls: str = "news") -> str: + return cls + + assert classify("article", cls="sports") == "sports" + langfuse_memory_client.flush() + + span = _finished_spans_by_name(memory_exporter, "classify")[0] + assert json.loads( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] + ) == { + "args": ["article"], + "kwargs": {"cls": "sports"}, + } + + +@pytest.mark.asyncio +async def test_capture_input_async_free_function_keyword_only_cls( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + @observe(capture_input=True) + async def classify(value: str, *, cls: str = "news") -> str: + return cls + + assert await classify("article", cls="sports") == "sports" + langfuse_memory_client.flush() + + span = _finished_spans_by_name(memory_exporter, "classify")[0] + assert json.loads( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] + ) == { + "args": ["article"], + "kwargs": {"cls": "sports"}, + } + + +def test_capture_input_instance_method_drops_self( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + class Classifier: + @observe(capture_input=True) + def classify(self, value: str) -> str: + return value + + assert Classifier().classify("article") == "article" + langfuse_memory_client.flush() + + span = _finished_spans_by_name(memory_exporter, "classify")[0] + assert json.loads( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] + ) == { + "args": ["article"], + "kwargs": {}, + } + + +def test_capture_input_classmethod_drops_cls( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + class Classifier: + @classmethod + @observe(capture_input=True) + def classify(cls, value: str) -> str: + return value + + assert Classifier.classify("article") == "article" + langfuse_memory_client.flush() + + span = _finished_spans_by_name(memory_exporter, "classify")[0] + assert json.loads( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] + ) == { + "args": ["article"], + "kwargs": {}, + } + + +def test_capture_input_free_function_later_positional_self( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + @observe(capture_input=True) + def classify(value: str, self: str | None = None) -> str | None: + return self + + assert classify("article", "news") == "news" + langfuse_memory_client.flush() + + span = _finished_spans_by_name(memory_exporter, "classify")[0] + assert json.loads( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] + ) == { + "args": ["article", "news"], + "kwargs": {}, + } + + @pytest.mark.asyncio async def test_capture_output_false_preserves_type_when_current_span_is_updated( langfuse_memory_client: Any, memory_exporter: Any