From b695f97cd393f0be3c498d472cbe6f53af372da4 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 14 May 2026 17:49:56 -0700 Subject: [PATCH] fix: More python 3.14 compatibility cleanup --- .github/workflows/cross-platform-test.yml | 70 +++++++++++++++--- src/backend/base/pyproject.toml | 2 +- .../unit/serialization/test_serialization.py | 2 + .../tracing/test_langfuse_v3_compatibility.py | 71 ++++++++++++++++--- uv.lock | 26 +++---- 5 files changed, 137 insertions(+), 34 deletions(-) diff --git a/.github/workflows/cross-platform-test.yml b/.github/workflows/cross-platform-test.yml index 01d39bb40f..46aa680c8f 100644 --- a/.github/workflows/cross-platform-test.yml +++ b/.github/workflows/cross-platform-test.yml @@ -584,9 +584,26 @@ jobs: uv venv test-env --seed shell: bash - # Wheel installation steps — install langflow-sdk before LFX (sdk is not yet on PyPI) - - name: Install langflow-sdk from wheel (Windows) - if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && (inputs.sdk-artifact-name != '' || needs.build-if-needed.outputs.sdk-artifact-name != '') + # Wheel installation steps — install SDK and LFX together when both are present. + - name: Install SDK and LFX packages from wheel (Windows) + if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && (inputs.sdk-artifact-name != '' || needs.build-if-needed.outputs.sdk-artifact-name != '') && (inputs.lfx-artifact-name != '' || needs.build-if-needed.outputs.lfx-artifact-name != '') + run: | + ls -la ./sdk-dist/ + find ./sdk-dist -name "*.whl" -type f + ls -la ./lfx-dist/ + find ./lfx-dist -name "*.whl" -type f + SDK_WHEEL_FILE=$(find ./sdk-dist -name "*.whl" -type f | head -1) + LFX_WHEEL_FILE=$(find ./lfx-dist -name "*.whl" -type f | head -1) + if [ -n "$SDK_WHEEL_FILE" ] && [ -n "$LFX_WHEEL_FILE" ]; then + uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "$SDK_WHEEL_FILE" "$LFX_WHEEL_FILE" + else + echo "Missing wheel file in ./sdk-dist/ or ./lfx-dist/" + exit 1 + fi + shell: bash + + - name: Install SDK package from wheel (Windows) + if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && (inputs.sdk-artifact-name != '' || needs.build-if-needed.outputs.sdk-artifact-name != '') && (inputs.lfx-artifact-name == '' && needs.build-if-needed.outputs.lfx-artifact-name == '') run: | WHEEL_FILE=$(find ./sdk-dist -name "*.whl" -type f | head -1) if [ -n "$WHEEL_FILE" ]; then @@ -597,8 +614,25 @@ jobs: fi shell: bash - - name: Install langflow-sdk from wheel (Unix) - if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && (inputs.sdk-artifact-name != '' || needs.build-if-needed.outputs.sdk-artifact-name != '') + - name: Install SDK and LFX packages from wheel (Unix) + if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && (inputs.sdk-artifact-name != '' || needs.build-if-needed.outputs.sdk-artifact-name != '') && (inputs.lfx-artifact-name != '' || needs.build-if-needed.outputs.lfx-artifact-name != '') + run: | + ls -la ./sdk-dist/ + find ./sdk-dist -name "*.whl" -type f + ls -la ./lfx-dist/ + find ./lfx-dist -name "*.whl" -type f + SDK_WHEEL_FILE=$(find ./sdk-dist -name "*.whl" -type f | head -1) + LFX_WHEEL_FILE=$(find ./lfx-dist -name "*.whl" -type f | head -1) + if [ -n "$SDK_WHEEL_FILE" ] && [ -n "$LFX_WHEEL_FILE" ]; then + uv pip install --prerelease=allow --python ./test-env/bin/python "$SDK_WHEEL_FILE" "$LFX_WHEEL_FILE" + else + echo "Missing wheel file in ./sdk-dist/ or ./lfx-dist/" + exit 1 + fi + shell: bash + + - name: Install SDK package from wheel (Unix) + if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && (inputs.sdk-artifact-name != '' || needs.build-if-needed.outputs.sdk-artifact-name != '') && (inputs.lfx-artifact-name == '' && needs.build-if-needed.outputs.lfx-artifact-name == '') run: | WHEEL_FILE=$(find ./sdk-dist -name "*.whl" -type f | head -1) if [ -n "$WHEEL_FILE" ]; then @@ -610,7 +644,7 @@ jobs: shell: bash - name: Install LFX package from wheel (Windows) - if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && (inputs.lfx-artifact-name != '' || needs.build-if-needed.outputs.lfx-artifact-name != '') + if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && (inputs.lfx-artifact-name != '' || needs.build-if-needed.outputs.lfx-artifact-name != '') && (inputs.sdk-artifact-name == '' && needs.build-if-needed.outputs.sdk-artifact-name == '') run: | ls -la ./lfx-dist/ find ./lfx-dist -name "*.whl" -type f @@ -652,7 +686,7 @@ jobs: shell: bash - name: Install LFX package from wheel (Unix) - if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && (inputs.lfx-artifact-name != '' || needs.build-if-needed.outputs.lfx-artifact-name != '') + if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && (inputs.lfx-artifact-name != '' || needs.build-if-needed.outputs.lfx-artifact-name != '') && (inputs.sdk-artifact-name == '' && needs.build-if-needed.outputs.sdk-artifact-name == '') run: | ls -la ./lfx-dist/ find ./lfx-dist -name "*.whl" -type f @@ -701,11 +735,20 @@ jobs: LFX_WHEEL=$(find ./lfx-dist -name "*.whl" -type f | head -1) if [ -n "$LFX_WHEEL" ]; then echo "Force reinstalling LFX: $LFX_WHEEL" + INSTALL_WHEELS=("$LFX_WHEEL") + SDK_WHEEL="" + if [ -d ./sdk-dist ]; then + SDK_WHEEL=$(find ./sdk-dist -name "*.whl" -type f | head -1) + fi + if [ -n "$SDK_WHEEL" ]; then + echo "Force reinstalling langflow-sdk with LFX: $SDK_WHEEL" + INSTALL_WHEELS=("$SDK_WHEEL" "$LFX_WHEEL") + fi NO_DEPS="--no-deps" if [ "${{ inputs.pre_release }}" = "true" ]; then NO_DEPS="" fi - uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/Scripts/python.exe "$LFX_WHEEL" + uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/Scripts/python.exe "${INSTALL_WHEELS[@]}" fi fi @@ -729,11 +772,20 @@ jobs: LFX_WHEEL=$(find ./lfx-dist -name "*.whl" -type f | head -1) if [ -n "$LFX_WHEEL" ]; then echo "Force reinstalling LFX: $LFX_WHEEL" + INSTALL_WHEELS=("$LFX_WHEEL") + SDK_WHEEL="" + if [ -d ./sdk-dist ]; then + SDK_WHEEL=$(find ./sdk-dist -name "*.whl" -type f | head -1) + fi + if [ -n "$SDK_WHEEL" ]; then + echo "Force reinstalling langflow-sdk with LFX: $SDK_WHEEL" + INSTALL_WHEELS=("$SDK_WHEEL" "$LFX_WHEEL") + fi NO_DEPS="--no-deps" if [ "${{ inputs.pre_release }}" = "true" ]; then NO_DEPS="" fi - uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/bin/python "$LFX_WHEEL" + uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/bin/python "${INSTALL_WHEELS[@]}" fi fi diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index d4a2431ac3..d67cfa5c1c 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -234,7 +234,7 @@ sentence-transformers = ["sentence-transformers>=2.3.1"] ctransformers = ["ctransformers>=0.2.10"] # Individual monitoring providers -langfuse = ["langfuse~=3.8"] +langfuse = ["langfuse~=3.8; python_version < '3.14'"] langwatch = ["langwatch~=0.10.0; python_version < '3.14'"] langsmith = ["langsmith>=0.3.42,<1.0.0"] arize = ["arize-phoenix-otel>=0.6.1"] diff --git a/src/backend/tests/unit/serialization/test_serialization.py b/src/backend/tests/unit/serialization/test_serialization.py index 8e2f1cc654..6e03845be3 100644 --- a/src/backend/tests/unit/serialization/test_serialization.py +++ b/src/backend/tests/unit/serialization/test_serialization.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math from datetime import datetime, timezone from typing import Any diff --git a/src/backend/tests/unit/services/tracing/test_langfuse_v3_compatibility.py b/src/backend/tests/unit/services/tracing/test_langfuse_v3_compatibility.py index d9e134396b..8312383e71 100644 --- a/src/backend/tests/unit/services/tracing/test_langfuse_v3_compatibility.py +++ b/src/backend/tests/unit/services/tracing/test_langfuse_v3_compatibility.py @@ -8,6 +8,8 @@ See: https://langfuse.com/docs/observability/sdk/upgrade-path """ import os +import sys +import types import uuid from unittest.mock import MagicMock, patch @@ -28,41 +30,66 @@ def langfuse_env_vars(): yield +def _clear_failed_langfuse_import() -> None: + """Remove partially imported Langfuse modules after SDK import failures.""" + for module_name in list(sys.modules): + if module_name == "langfuse" or module_name.startswith("langfuse."): + sys.modules.pop(module_name, None) + + +def _import_langfuse_or_skip(): + try: + from langfuse import Langfuse + except Exception as exc: + _clear_failed_langfuse_import() + pytest.skip(f"langfuse SDK is not importable: {exc}") + return Langfuse + + +def _import_callback_handler_or_skip(): + try: + from langfuse.langchain import CallbackHandler + except Exception as exc: + _clear_failed_langfuse_import() + pytest.skip(f"langfuse LangChain callback handler is not importable: {exc}") + return CallbackHandler + + class TestLangfuseV3ApiExists: """Verify that the langfuse v3 API methods we need actually exist.""" def test_langfuse_client_has_start_span(self): """Verify start_span method exists (v3 API).""" - from langfuse import Langfuse + langfuse_class = _import_langfuse_or_skip() - assert hasattr(Langfuse, "start_span"), "Langfuse.start_span() should exist in v3" + assert hasattr(langfuse_class, "start_span"), "Langfuse.start_span() should exist in v3" def test_langfuse_client_has_start_as_current_span(self): """Verify start_as_current_span method exists (v3 API).""" - from langfuse import Langfuse + langfuse_class = _import_langfuse_or_skip() - assert hasattr(Langfuse, "start_as_current_span"), "Langfuse.start_as_current_span() should exist in v3" + assert hasattr(langfuse_class, "start_as_current_span"), "Langfuse.start_as_current_span() should exist in v3" def test_langfuse_client_has_create_trace_id(self): """Verify create_trace_id method exists (v3 API).""" - from langfuse import Langfuse + langfuse_class = _import_langfuse_or_skip() - assert hasattr(Langfuse, "create_trace_id"), "Langfuse.create_trace_id() should exist in v3" + assert hasattr(langfuse_class, "create_trace_id"), "Langfuse.create_trace_id() should exist in v3" def test_langfuse_client_does_not_have_trace(self): """Verify trace() method was removed in v3.""" - from langfuse import Langfuse + langfuse_class = _import_langfuse_or_skip() # This test documents that trace() no longer exists # If this fails, langfuse may have restored backward compatibility - assert not hasattr(Langfuse, "trace"), "Langfuse.trace() should NOT exist in v3 (removed)" + assert not hasattr(langfuse_class, "trace"), "Langfuse.trace() should NOT exist in v3 (removed)" def test_callback_handler_import_path(self): """Verify the v3 callback handler import path works.""" # v3 path - from langfuse.langchain import CallbackHandler + callback_handler = _import_callback_handler_or_skip() - assert CallbackHandler is not None + assert callback_handler is not None class TestLangfuseTracerV3Compatibility: @@ -91,7 +118,29 @@ class TestLangfuseTracerFunctionality: @pytest.fixture def mock_langfuse(self): """Create a mock langfuse client that simulates v3 API.""" - with patch("langfuse.Langfuse") as mock_langfuse_class: + + class TraceContext(dict): + pass + + mock_langfuse_module = types.ModuleType("langfuse") + mock_langfuse_types_module = types.ModuleType("langfuse.types") + mock_langfuse_langchain_module = types.ModuleType("langfuse.langchain") + + mock_langfuse_class = MagicMock() + mock_langfuse_types_module.TraceContext = TraceContext + mock_langfuse_langchain_module.CallbackHandler = MagicMock() + mock_langfuse_module.Langfuse = mock_langfuse_class + mock_langfuse_module.types = mock_langfuse_types_module + mock_langfuse_module.langchain = mock_langfuse_langchain_module + + with patch.dict( + sys.modules, + { + "langfuse": mock_langfuse_module, + "langfuse.types": mock_langfuse_types_module, + "langfuse.langchain": mock_langfuse_langchain_module, + }, + ): mock_client = MagicMock() mock_langfuse_class.return_value = mock_client mock_langfuse_class.create_trace_id = MagicMock(return_value="a" * 32) diff --git a/uv.lock b/uv.lock index a08d359414..16455dacc8 100644 --- a/uv.lock +++ b/uv.lock @@ -7140,7 +7140,7 @@ all = [ { name = "langchain-pinecone", marker = "python_full_version < '3.14'" }, { name = "langchain-sambanova" }, { name = "langchain-unstructured" }, - { name = "langfuse" }, + { name = "langfuse", marker = "python_full_version < '3.14'" }, { name = "langsmith" }, { name = "langwatch", marker = "python_full_version < '3.14'" }, { name = "lark" }, @@ -7307,7 +7307,7 @@ complete = [ { name = "langchain-pinecone", marker = "python_full_version < '3.14'" }, { name = "langchain-sambanova" }, { name = "langchain-unstructured" }, - { name = "langfuse" }, + { name = "langfuse", marker = "python_full_version < '3.14'" }, { name = "langsmith" }, { name = "langwatch", marker = "python_full_version < '3.14'" }, { name = "lark" }, @@ -7459,7 +7459,7 @@ langchain-unstructured = [ { name = "langchain-unstructured" }, ] langfuse = [ - { name = "langfuse" }, + { name = "langfuse", marker = "python_full_version < '3.14'" }, ] langsmith = [ { name = "langsmith" }, @@ -7875,7 +7875,7 @@ requires-dist = [ { name = "langflow-base", extras = ["yfinance"], marker = "extra == 'complete'", editable = "src/backend/base" }, { name = "langflow-base", extras = ["youtube"], marker = "extra == 'complete'", editable = "src/backend/base" }, { name = "langflow-base", extras = ["zep"], marker = "extra == 'complete'", editable = "src/backend/base" }, - { name = "langfuse", marker = "extra == 'langfuse'", specifier = "~=3.8" }, + { name = "langfuse", marker = "python_full_version < '3.14' and extra == 'langfuse'", specifier = "~=3.8" }, { name = "langgraph-checkpoint", specifier = ">4.0.0,<5.0.0" }, { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.3.42,<1.0.0" }, { name = "langwatch", marker = "python_full_version < '3.14' and extra == 'langwatch'", specifier = "~=0.10.0" }, @@ -8058,15 +8058,15 @@ name = "langfuse" version = "3.14.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backoff" }, - { name = "httpx" }, - { name = "openai" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-sdk" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "wrapt" }, + { name = "backoff", marker = "python_full_version < '3.14'" }, + { name = "httpx", marker = "python_full_version < '3.14'" }, + { name = "openai", marker = "python_full_version < '3.14'" }, + { name = "opentelemetry-api", marker = "python_full_version < '3.14'" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version < '3.14'" }, + { name = "opentelemetry-sdk", marker = "python_full_version < '3.14'" }, + { name = "packaging", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "wrapt", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/e4/5bb96bc5f95f476c8f7c4e654a6420250938cc21645b914a1309c2dc3d49/langfuse-3.14.6.tar.gz", hash = "sha256:058f7b7caa9284b964feaee81c223542d78e9fb4daabf312269b9f903471b9c4", size = 236530, upload-time = "2026-04-01T17:27:55.057Z" } wheels = [