271 lines
6.6 KiB
Python
271 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from wf_sources_python import PythonSourceProvider, load_python_source
|
|
|
|
|
|
class PythonSourceConfigFixture:
|
|
id = "local.ops"
|
|
path = Path(".")
|
|
module = "tests.fixtures.python_source_ops"
|
|
registry = "registry"
|
|
enabled = True
|
|
|
|
|
|
def test_load_python_source_from_sequence_registry() -> None:
|
|
source = load_python_source(
|
|
source_id="local.ops",
|
|
module="tests.fixtures.python_source_ops",
|
|
registry="registry",
|
|
)
|
|
|
|
assert source.id == "local.ops"
|
|
assert source.kind == "python"
|
|
assert set(source.capabilities.node_specs) == {
|
|
"local.ops.echo",
|
|
"local.ops.upper",
|
|
}
|
|
assert source.permissions.safe_for_workflow is True
|
|
|
|
|
|
def test_python_source_provider_loads_configured_sources() -> None:
|
|
sources = PythonSourceProvider([PythonSourceConfigFixture()]).load_sources()
|
|
|
|
assert set(sources) == {"local.ops"}
|
|
assert "local.ops.echo" in sources["local.ops"].capabilities.node_specs
|
|
|
|
|
|
def test_load_python_source_from_callable_registry() -> None:
|
|
source = load_python_source(
|
|
source_id="local.ops",
|
|
module="tests.fixtures.python_source_ops",
|
|
registry="callable_registry",
|
|
)
|
|
|
|
assert set(source.capabilities.node_specs) == {
|
|
"local.ops.echo",
|
|
"local.ops.upper",
|
|
}
|
|
|
|
|
|
def test_load_python_source_uses_configured_import_path(tmp_path: Path) -> None:
|
|
source_root = tmp_path / "source_root"
|
|
source_root.mkdir()
|
|
(source_root / "project_ops.py").write_text(
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel
|
|
from wf_authoring import node
|
|
|
|
class EchoInput(BaseModel):
|
|
text: str
|
|
|
|
class EchoOutput(BaseModel):
|
|
echoed: str
|
|
|
|
@node(name="echo")
|
|
def echo(payload: EchoInput) -> EchoOutput:
|
|
return EchoOutput(echoed=payload.text)
|
|
|
|
registry = [echo]
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
source = load_python_source(
|
|
source_id="local.ops",
|
|
path=source_root,
|
|
module="project_ops",
|
|
registry="registry",
|
|
)
|
|
|
|
assert set(source.capabilities.node_specs) == {"local.ops.echo"}
|
|
|
|
|
|
def test_load_python_source_isolates_same_module_name_across_roots(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
left_root = tmp_path / "left"
|
|
right_root = tmp_path / "right"
|
|
left_root.mkdir()
|
|
right_root.mkdir()
|
|
source_template = """
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel
|
|
from wf_authoring import node
|
|
|
|
class Input(BaseModel):
|
|
text: str
|
|
|
|
class Output(BaseModel):
|
|
value: str
|
|
|
|
@node(name="{name}")
|
|
def operation(payload: Input) -> Output:
|
|
return Output(value=payload.text)
|
|
|
|
registry = [operation]
|
|
"""
|
|
(left_root / "ops.py").write_text(
|
|
source_template.format(name="left"), encoding="utf-8"
|
|
)
|
|
(right_root / "ops.py").write_text(
|
|
source_template.format(name="right"), encoding="utf-8"
|
|
)
|
|
|
|
left = load_python_source(
|
|
source_id="local.left",
|
|
path=left_root,
|
|
module="ops",
|
|
registry="registry",
|
|
)
|
|
right = load_python_source(
|
|
source_id="local.right",
|
|
path=right_root,
|
|
module="ops",
|
|
registry="registry",
|
|
)
|
|
|
|
assert set(left.capabilities.node_specs) == {"local.left.left"}
|
|
assert set(right.capabilities.node_specs) == {"local.right.right"}
|
|
|
|
|
|
def test_load_python_source_supports_relative_imports_under_source_root(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
source_root = tmp_path / "relative_source"
|
|
package_root = source_root / "pkg"
|
|
package_root.mkdir(parents=True)
|
|
(package_root / "__init__.py").write_text("", encoding="utf-8")
|
|
(package_root / "labels.py").write_text(
|
|
'LABEL = "relative"\n',
|
|
encoding="utf-8",
|
|
)
|
|
(package_root / "ops.py").write_text(
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel
|
|
from wf_authoring import node
|
|
|
|
from .labels import LABEL
|
|
|
|
class Input(BaseModel):
|
|
text: str
|
|
|
|
class Output(BaseModel):
|
|
value: str
|
|
|
|
@node(name=LABEL)
|
|
def operation(payload: Input) -> Output:
|
|
return Output(value=payload.text)
|
|
|
|
registry = [operation]
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
source = load_python_source(
|
|
source_id="local.relative",
|
|
path=source_root,
|
|
module="pkg.ops",
|
|
registry="registry",
|
|
)
|
|
|
|
assert set(source.capabilities.node_specs) == {"local.relative.relative"}
|
|
|
|
|
|
def test_load_python_source_supports_delayed_relative_imports(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
source_root = tmp_path / "delayed_relative_source"
|
|
package_root = source_root / "pkg"
|
|
package_root.mkdir(parents=True)
|
|
(package_root / "__init__.py").write_text("", encoding="utf-8")
|
|
(package_root / "labels.py").write_text(
|
|
'LABEL = "delayed"\n',
|
|
encoding="utf-8",
|
|
)
|
|
(package_root / "registry.py").write_text(
|
|
"""
|
|
def build_registry():
|
|
from .ops import operation
|
|
|
|
return [operation]
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(package_root / "ops.py").write_text(
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel
|
|
from wf_authoring import node
|
|
|
|
from .labels import LABEL
|
|
|
|
class Input(BaseModel):
|
|
text: str
|
|
|
|
class Output(BaseModel):
|
|
value: str
|
|
|
|
@node(name=LABEL)
|
|
def operation(payload: Input) -> Output:
|
|
return Output(value=payload.text)
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
source = load_python_source(
|
|
source_id="local.delayed",
|
|
path=source_root,
|
|
module="pkg.registry",
|
|
registry="build_registry",
|
|
)
|
|
|
|
assert set(source.capabilities.node_specs) == {"local.delayed.delayed"}
|
|
|
|
|
|
def test_load_python_source_propagates_enabled_flag() -> None:
|
|
source = load_python_source(
|
|
source_id="local.ops",
|
|
module="tests.fixtures.python_source_ops",
|
|
registry="registry",
|
|
enabled=False,
|
|
)
|
|
|
|
assert source.enabled is False
|
|
|
|
|
|
def test_load_python_source_rejects_duplicate_qualified_names() -> None:
|
|
with pytest.raises(ValueError, match="duplicate NodeSpec names"):
|
|
load_python_source(
|
|
source_id="local.ops",
|
|
module="tests.fixtures.python_source_ops",
|
|
registry="duplicate_registry",
|
|
)
|
|
|
|
|
|
def test_load_python_source_rejects_missing_registry() -> None:
|
|
with pytest.raises(ValueError, match="missing registry object"):
|
|
load_python_source(
|
|
source_id="local.ops",
|
|
module="tests.fixtures.python_source_ops",
|
|
registry="missing",
|
|
)
|
|
|
|
|
|
def test_load_python_source_rejects_non_node_spec() -> None:
|
|
with pytest.raises(TypeError, match="expected NodeSpec"):
|
|
load_python_source(
|
|
source_id="local.ops",
|
|
module="math",
|
|
registry="pi",
|
|
)
|