Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/lfx/src/lfx/custom/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions src/lfx/tests/unit/custom/component/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
62 changes: 34 additions & 28 deletions src/sdk/src/langflow_sdk/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async def test_my_async_flow(async_flow_runner):

from __future__ import annotations

import contextlib
import os
from typing import TYPE_CHECKING, Any

Expand All @@ -54,34 +55,39 @@ async def test_my_async_flow(async_flow_runner):
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)


# ---------------------------------------------------------------------------
Expand Down
11 changes: 10 additions & 1 deletion src/sdk/tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@

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"

Expand Down Expand Up @@ -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)
Loading