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.
This commit is contained in:
Eric Hare
2026-04-21 08:39:46 -07:00
committed by GitHub
parent 9f42c9e707
commit cae2bdfef6
4 changed files with 78 additions and 30 deletions

View File

@ -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:

View File

@ -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
# ---------------------------------------------------------------------------

View File

@ -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)
# ---------------------------------------------------------------------------

View File

@ -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)