From cae2bdfef625fcc5998c9a88ffa8f0cc562ab32b Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Tue, 21 Apr 2026 08:39:46 -0700 Subject: [PATCH] fix(custom): honor asname in `from X import Y as Z` for custom components (#12813) * fix(custom): honor asname in `from X import Y as Z` for custom components `_handle_module_attributes` was keying `exec_globals` on `alias.name`, so `from pkg import Foo as Bar` bound `Foo` instead of `Bar` in a custom component's exec scope. Any reference to `Bar` then raised NameError and the component failed to load. Use `alias.asname or alias.name`, matching the `import X as Y` branch in the same file and standard Python import semantics. Adds regression tests against `prepare_global_scope` and end-to-end through `create_class`. * fix: Make sdk env flag idempotent (release-1.9.1) (#12815) fix: Make sdk env flag idempotent Refactor pytest_addoption to register CLI options via a loop with contextlib.suppress(ValueError), allowing langflow-sdk and lfx to coexist without plugin registration conflicts. Add test_pytest_addoption_is_idempotent to verify behavior. --- src/lfx/src/lfx/custom/validate.py | 3 +- .../unit/custom/component/test_validate.py | 32 ++++++++++ src/sdk/src/langflow_sdk/testing.py | 62 ++++++++++--------- src/sdk/tests/test_testing.py | 11 +++- 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/lfx/src/lfx/custom/validate.py b/src/lfx/src/lfx/custom/validate.py index 41d2154aef..51183931d3 100644 --- a/src/lfx/src/lfx/custom/validate.py +++ b/src/lfx/src/lfx/custom/validate.py @@ -341,7 +341,8 @@ def _resolve_attribute(imported_module, module_name, attr_name): def _handle_module_attributes(imported_module, node, module_name, exec_globals): """Handle importing specific attributes from a module.""" for alias in node.names: - exec_globals[alias.name] = _resolve_attribute(imported_module, module_name, alias.name) + key = alias.asname or alias.name + exec_globals[key] = _resolve_attribute(imported_module, module_name, alias.name) class _MissingModulePlaceholder: diff --git a/src/lfx/tests/unit/custom/component/test_validate.py b/src/lfx/tests/unit/custom/component/test_validate.py index 2abe0d02b0..088cced7ba 100644 --- a/src/lfx/tests/unit/custom/component/test_validate.py +++ b/src/lfx/tests/unit/custom/component/test_validate.py @@ -97,6 +97,38 @@ def to_url(path): assert scope["urllib"].request.pathname2url("folder name/file.txt") == "folder%20name/file.txt" +def test_prepare_global_scope_supports_aliased_from_imports(): + """Regression test: `from X import Y as Z` must bind Z in scope, not Y.""" + module = ast.parse( + dedent(""" +from urllib.request import pathname2url as to_url_path + +def to_url(path): + return to_url_path(path) +""") + ) + scope = prepare_global_scope(module) + + assert "to_url_path" in scope + assert "pathname2url" not in scope + assert scope["to_url_path"]("folder name/file.txt") == "folder%20name/file.txt" + + +def test_create_class_supports_aliased_from_imports(): + """End-to-end: a component using `from X import Y as Z` should load and Z is usable.""" + code = dedent(""" +from urllib.request import pathname2url as to_url_path +from lfx.custom import Component + +class AliasedImportComponent(Component): + def to_url(self, path): + return to_url_path(path) +""") + cls = create_class(code, "AliasedImportComponent") + assert cls.__name__ == "AliasedImportComponent" + assert cls().to_url("folder name/file.txt") == "folder%20name/file.txt" + + # --------------------------------------------------------------------------- # _get_module_fallbacks # --------------------------------------------------------------------------- diff --git a/src/sdk/src/langflow_sdk/testing.py b/src/sdk/src/langflow_sdk/testing.py index e9f00e8c10..5ffe75d785 100644 --- a/src/sdk/src/langflow_sdk/testing.py +++ b/src/sdk/src/langflow_sdk/testing.py @@ -30,6 +30,7 @@ Usage inside a test file:: from __future__ import annotations +import contextlib import os from typing import TYPE_CHECKING, Any @@ -54,34 +55,39 @@ if TYPE_CHECKING: def pytest_addoption(parser: pytest.Parser) -> None: """Register Langflow-specific CLI options.""" group = parser.getgroup("langflow", "Langflow integration testing options") - group.addoption( - "--langflow-env", - dest="langflow_env", - default=None, - metavar="NAME", - help="Environment name from langflow-environments.toml to use for integration tests.", - ) - group.addoption( - "--langflow-url", - dest="langflow_url", - default=None, - metavar="URL", - help="Base URL of the Langflow instance (overrides --langflow-env).", - ) - group.addoption( - "--langflow-api-key", - dest="langflow_api_key", - default=None, - metavar="KEY", - help="API key for the Langflow instance (overrides environment config).", - ) - group.addoption( - "--langflow-environments-file", - dest="langflow_environments_file", - default=None, - metavar="PATH", - help="Path to langflow-environments.toml (overrides default discovery).", - ) + options = { + "--langflow-env": { + "dest": "langflow_env", + "default": None, + "metavar": "NAME", + "help": "Environment name from langflow-environments.toml to use for integration tests.", + }, + "--langflow-url": { + "dest": "langflow_url", + "default": None, + "metavar": "URL", + "help": "Base URL of the Langflow instance (overrides --langflow-env).", + }, + "--langflow-api-key": { + "dest": "langflow_api_key", + "default": None, + "metavar": "KEY", + "help": "API key for the Langflow instance (overrides environment config).", + }, + "--langflow-environments-file": { + "dest": "langflow_environments_file", + "default": None, + "metavar": "PATH", + "help": "Path to langflow-environments.toml (overrides default discovery).", + }, + } + + # langflow-sdk and lfx can both be installed in the same environment and + # expose the same remote-testing flags. Keep registration idempotent so + # pytest plugin auto-discovery can load both entry points safely. + for flag, kwargs in options.items(): + with contextlib.suppress(ValueError): + group.addoption(flag, **kwargs) # --------------------------------------------------------------------------- diff --git a/src/sdk/tests/test_testing.py b/src/sdk/tests/test_testing.py index 0283d61824..bf41198c7e 100644 --- a/src/sdk/tests/test_testing.py +++ b/src/sdk/tests/test_testing.py @@ -8,9 +8,10 @@ from __future__ import annotations import httpx import respx +from _pytest.config.argparsing import Parser from langflow_sdk.client import AsyncLangflowClient, LangflowClient from langflow_sdk.models import RunOutput, RunResponse -from langflow_sdk.testing import AsyncFlowRunner, FlowRunner +from langflow_sdk.testing import AsyncFlowRunner, FlowRunner, pytest_addoption _BASE = "http://langflow.test" @@ -307,3 +308,11 @@ def test_pytest_addoption_registers_options(pytestconfig): "--langflow-environments-file", ): assert pytestconfig.getoption(name) is None, f"Option {name!r} missing or has unexpected default" + + +def test_pytest_addoption_is_idempotent(): + """Registering the plugin twice should not fail when another plugin owns the same flags.""" + parser = Parser(_ispytest=True) + + pytest_addoption(parser) + pytest_addoption(parser)