less dot-separated string in the Machinery
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from wf_authoring import WorkflowBuilder
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from wf_authoring import WorkflowBuilder, state_field
|
||||
from wf_core.paths import StatePath
|
||||
|
||||
from tests.authoring.helpers import (
|
||||
AppendState,
|
||||
@@ -13,6 +18,14 @@ from tests.authoring.helpers import (
|
||||
)
|
||||
|
||||
|
||||
class DotAliasState(BaseModel):
|
||||
person_tags: Annotated[
|
||||
list[str],
|
||||
Field(alias="person.name"),
|
||||
state_field(reducer="wf.std.append"),
|
||||
]
|
||||
|
||||
|
||||
def test_builder_accepts_basemodel_classes_for_workflow_schemas() -> None:
|
||||
builder = WorkflowBuilder(
|
||||
name="model_schema_demo",
|
||||
@@ -102,3 +115,19 @@ def test_nested_state_basemodel_projects_parent_and_child_paths() -> None:
|
||||
assert fields["person.tags"].type == "array"
|
||||
assert fields["person"].reducer.name == "wf.std.replace"
|
||||
assert fields["person.tags"].reducer.name == "wf.std.append"
|
||||
|
||||
|
||||
def test_state_schema_from_preserves_literal_dotted_alias_paths() -> None:
|
||||
builder = WorkflowBuilder(
|
||||
name="dotted_alias_state_schema_demo",
|
||||
input_schema=WorkflowInput,
|
||||
state_schema=DotAliasState,
|
||||
output_schema=WorkflowOutput,
|
||||
start="start",
|
||||
)
|
||||
|
||||
workflow = builder.compile()
|
||||
fields = workflow.state_schema.field_index()
|
||||
|
||||
assert StatePath(("person.name",)) in fields
|
||||
assert fields[StatePath(("person.name",))].reducer.name == "wf.std.append"
|
||||
|
||||
@@ -31,16 +31,18 @@ def test_exact_nested_state_path_uses_declared_reducer() -> None:
|
||||
|
||||
|
||||
def test_state_schema_accepts_legacy_field_list_and_dumps_json_schema() -> None:
|
||||
schema = StateSchema.model_validate({
|
||||
"fields": [
|
||||
{"path": "state.person", "type": "object"},
|
||||
{
|
||||
"path": "state.person.name",
|
||||
"type": "string",
|
||||
"reducer": "wf.std.replace",
|
||||
},
|
||||
]
|
||||
})
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"fields": [
|
||||
{"path": "state.person", "type": "object"},
|
||||
{
|
||||
"path": "state.person.name",
|
||||
"type": "string",
|
||||
"reducer": "wf.std.replace",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert schema.fields[0].path == StatePath.of("person")
|
||||
assert schema.field_map()["person.name"].type == "string"
|
||||
@@ -50,22 +52,24 @@ def test_state_schema_accepts_legacy_field_list_and_dumps_json_schema() -> None:
|
||||
|
||||
|
||||
def test_state_schema_uses_json_schema_properties_as_canonical_shape() -> None:
|
||||
schema = StateSchema.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name",
|
||||
"reducer": "wf.std.replace",
|
||||
}
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name",
|
||||
"reducer": "wf.std.replace",
|
||||
}
|
||||
},
|
||||
},
|
||||
"count": {"type": "integer", "reducer": "wf.std.add"},
|
||||
},
|
||||
"count": {"type": "integer", "reducer": "wf.std.add"},
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
fields = schema.field_map()
|
||||
assert fields["person.name"].validation_schema.type == "string"
|
||||
@@ -73,14 +77,48 @@ def test_state_schema_uses_json_schema_properties_as_canonical_shape() -> None:
|
||||
assert fields["count"].reducer == ReducerRef(name="wf.std.add")
|
||||
|
||||
|
||||
def test_state_schema_rejects_invalid_reducer_extension_keyword() -> None:
|
||||
try:
|
||||
StateSchema.model_validate({
|
||||
def test_state_schema_preserves_literal_dotted_property_names() -> None:
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {"type": "integer", "reducer": {"bad": True}},
|
||||
"person.name": {"type": "string", "reducer": "wf.std.replace"}
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
fields = schema.field_index()
|
||||
|
||||
assert set(fields) == {StatePath(("person.name",))}
|
||||
assert fields[StatePath(("person.name",))].path == StatePath(("person.name",))
|
||||
|
||||
|
||||
def test_state_schema_field_map_keeps_display_key_for_literal_dotted_property() -> None:
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person.name": {"type": "string", "reducer": "wf.std.replace"}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
fields = schema.field_map()
|
||||
|
||||
assert set(fields) == {"person.name"}
|
||||
assert fields["person.name"].path == StatePath(("person.name",))
|
||||
|
||||
|
||||
def test_state_schema_rejects_invalid_reducer_extension_keyword() -> None:
|
||||
try:
|
||||
StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {"type": "integer", "reducer": {"bad": True}},
|
||||
},
|
||||
}
|
||||
)
|
||||
except ValueError as exc:
|
||||
assert "invalid reducer for state field 'count'" in str(exc)
|
||||
else:
|
||||
@@ -88,14 +126,16 @@ def test_state_schema_rejects_invalid_reducer_extension_keyword() -> None:
|
||||
|
||||
|
||||
def test_state_schema_accepts_canonical_schema_field() -> None:
|
||||
schema = StateSchema.model_validate({
|
||||
"fields": [
|
||||
{
|
||||
"path": "state.person.name",
|
||||
"schema": {"type": "string", "title": "Person Name"},
|
||||
}
|
||||
]
|
||||
})
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"path": "state.person.name",
|
||||
"schema": {"type": "string", "title": "Person Name"},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
field = schema.field_map()["person.name"]
|
||||
assert field.validation_schema.type == "string"
|
||||
@@ -111,13 +151,15 @@ def test_state_schema_accepts_deprecated_dict_shape_and_dumps_list() -> None:
|
||||
|
||||
|
||||
def test_state_schema_accepts_deprecated_dict_value_with_schema_key() -> None:
|
||||
schema = StateSchema.model_validate({
|
||||
"fields": {
|
||||
"person.name": {
|
||||
"schema": {"type": "string", "description": "Display name"},
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"fields": {
|
||||
"person.name": {
|
||||
"schema": {"type": "string", "description": "Display name"},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
assert schema.field_map()["person.name"].validation_schema.type == "string"
|
||||
|
||||
@@ -129,27 +171,32 @@ def test_state_schema_accepts_json_schema_field_without_type() -> None:
|
||||
|
||||
|
||||
def test_state_schema_accepts_deprecated_state_prefixed_dict_keys() -> None:
|
||||
schema = StateSchema.model_validate({
|
||||
"fields": {"state.person.name": {"type": "string"}}
|
||||
})
|
||||
schema = StateSchema.model_validate(
|
||||
{"fields": {"state.person.name": {"type": "string"}}}
|
||||
)
|
||||
|
||||
assert schema.field_map()["person.name"].path == StatePath.of("person.name")
|
||||
|
||||
|
||||
def test_state_field_decl_model_dump_serializes_path_as_string() -> None:
|
||||
field = StateFieldDecl.model_validate({
|
||||
"path": "state.person.name",
|
||||
"type": "string",
|
||||
})
|
||||
def test_state_field_decl_model_dump_serializes_path_structurally() -> None:
|
||||
field = StateFieldDecl.model_validate(
|
||||
{
|
||||
"path": "state.person.name",
|
||||
"type": "string",
|
||||
}
|
||||
)
|
||||
|
||||
assert field.model_dump()["path"] == "state.person.name"
|
||||
assert field.model_dump(mode="json")["path"] == "state.person.name"
|
||||
assert field.model_dump()["path"] == {"root": "state", "parts": ["person", "name"]}
|
||||
assert field.model_dump(mode="json")["path"] == {
|
||||
"root": "state",
|
||||
"parts": ["person", "name"],
|
||||
}
|
||||
|
||||
|
||||
def test_state_schema_model_dump_serializes_paths_as_strings() -> None:
|
||||
schema = StateSchema.model_validate({
|
||||
"fields": [{"path": "state.person.name", "type": "string"}]
|
||||
})
|
||||
schema = StateSchema.model_validate(
|
||||
{"fields": [{"path": "state.person.name", "type": "string"}]}
|
||||
)
|
||||
|
||||
dumped = schema.model_dump(mode="json")
|
||||
assert dumped["properties"]["person"]["properties"]["name"]["type"] == "string"
|
||||
@@ -158,12 +205,14 @@ def test_state_schema_model_dump_serializes_paths_as_strings() -> None:
|
||||
|
||||
def test_state_schema_rejects_duplicate_field_paths() -> None:
|
||||
try:
|
||||
StateSchema.model_validate({
|
||||
"fields": [
|
||||
{"path": "state.person.name", "type": "string"},
|
||||
{"path": "state.person.name", "type": "string"},
|
||||
]
|
||||
})
|
||||
StateSchema.model_validate(
|
||||
{
|
||||
"fields": [
|
||||
{"path": "state.person.name", "type": "string"},
|
||||
{"path": "state.person.name", "type": "string"},
|
||||
]
|
||||
}
|
||||
)
|
||||
except ValueError as exc:
|
||||
assert "duplicate state field path 'person.name'" in str(exc)
|
||||
else:
|
||||
@@ -172,17 +221,19 @@ def test_state_schema_rejects_duplicate_field_paths() -> None:
|
||||
|
||||
def test_exact_nested_state_path_uses_reducer_from_json_schema_property() -> None:
|
||||
workflow = _workflow_from_state_schema(
|
||||
StateSchema.model_validate({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {"type": "array", "reducer": "wf.std.append"}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
StateSchema.model_validate(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {"type": "array", "reducer": "wf.std.append"}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
state = {"person": {"tags": ["seed"]}}
|
||||
|
||||
@@ -192,12 +243,14 @@ def test_exact_nested_state_path_uses_reducer_from_json_schema_property() -> Non
|
||||
|
||||
|
||||
def test_state_schema_field_map_uses_rootless_keys() -> None:
|
||||
schema = StateSchema.model_validate({
|
||||
"fields": [
|
||||
{"path": "state.person.name", "type": "string"},
|
||||
{"path": "state.person.tags", "type": "array"},
|
||||
]
|
||||
})
|
||||
schema = StateSchema.model_validate(
|
||||
{
|
||||
"fields": [
|
||||
{"path": "state.person.name", "type": "string"},
|
||||
{"path": "state.person.tags", "type": "array"},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
fields = schema.field_map()
|
||||
assert fields["person.name"].path == StatePath.of("person.name")
|
||||
|
||||
Reference in New Issue
Block a user