xml-pipeline/third_party/xmlable/_errors.py
dullfig 82b5fcdd78 Replace MessageBus with aiostream-based StreamPump
Major refactor of the message pump architecture:

- Replace bus.py with stream_pump.py using aiostream for composable
  stream processing with natural fan-out via flatmap
- Add to_id field to MessageState for explicit routing
- Fix routing to use to_id.class format (e.g., "greeter.greeting")
- Generate XSD schemas from xmlified payload classes
- Fix xmlable imports (absolute -> relative) and parse_element ctx

New features:
- handlers/hello.py: Sample Greeting/GreetingResponse handler
- config/organism.yaml: Sample organism configuration
- 41 tests (31 unit + 10 integration) all passing

Schema changes:
- envelope.xsd: Allow any namespace payloads (##other -> ##any)

Dependencies added to pyproject.toml:
- aiostream>=0.5 (core dependency)
- pyhumps, termcolor (for xmlable)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 10:41:17 -08:00

261 lines
8.8 KiB
Python

"""
Colourful & descriptive errors for xmlable
- Clear messages
- Trace for parsing
"""
from dataclasses import dataclass
from typing import Any, Iterable
from termcolor import colored
from termcolor.termcolor import Color
from ._utils import typename, AnyType
def trace_note(trace: list[str], arrow_c: Color, node_c: Color):
return colored(" > ", arrow_c).join(
map(lambda x: colored(x, node_c), trace)
)
@dataclass
class XErrorCtx:
trace: list[str]
def next(self, node: str):
return XErrorCtx(trace=self.trace + [node])
# TODO: Custom backtrace to point to location in the file
class XError(Exception):
def __init__(
self,
short: str,
what: str,
why: str,
ctx: XErrorCtx | None = None,
notes: Iterable[str] = [],
):
super().__init__(colored(short, "red", attrs=["blink"]))
self.add_note(colored("What: " + what, "blue"))
self.add_note(colored("Why: " + why, "yellow"))
if ctx is not None:
self.add_note(
colored("Where: ", "magenta")
+ trace_note(ctx.trace, "light_magenta", "light_cyan")
)
for note in notes:
self.add_note(note)
class ErrorTypes:
@staticmethod
def NonXMlifiedType(t_name: str) -> XError:
return XError(
short="Non XMlified Type",
what=f"You attempted to use {t_name} in an xmlified class, but {t_name} is not xmlified",
why=f"All types used in an xmlified class must be xmlified",
)
@staticmethod
def InvalidData(ctx: XErrorCtx, val: Any, t_name: str) -> XError:
return XError(
short="Invalid Data",
what=f"Could not validate {val} as a valid {t_name}",
why=f"Produced xml must be valid",
ctx=ctx,
)
@staticmethod
def ParseFailure(
ctx: XErrorCtx, text: str | None, t_name: str, caught: Exception
) -> XError:
return XError(
short="Parse Failure",
what=f"Failed to parse {text} as a {t_name} with error: \n {caught}",
why=f"This error implies the xml is not validated against the current xsd, or there is a bug in this type's parser",
ctx=ctx,
)
@staticmethod
def UnexpectedTag(
ctx: XErrorCtx, expected_name: str, struct_name: str, tag_found: str
) -> XError:
return XError(
short="Unexpected Tag",
what=f"Expected {expected_name} but found {tag_found}",
why=f"This is a {struct_name} that contains 0..n elements of {expected_name} and no other elements",
ctx=ctx,
)
@staticmethod
def IncorrectType(
ctx: XErrorCtx, expected_len: int, struct_name: str, val: Any, name: str
) -> XError:
return XError(
short="Incorrect Type",
what=f"You have provided {len(val)} values {val} for {name}, but {name} is a {struct_name} that takes only {expected_len} values",
why=f"In order to generate xml, the values provided need to be the correct types",
ctx=ctx,
)
@staticmethod
def IncorrectElementTag(
ctx: XErrorCtx,
struct_name: str,
tag_name: str,
elem_index: int,
tag_expected: str,
tag_found: str,
) -> XError:
return XError(
short="Incorrect Element Tag",
what=f"While parsing {struct_name} {tag_name} we expected element {elem_index} to be {tag_expected}, but found {tag_found}",
why=f"The xml representation for {struct_name} requires the correct names in the correct order",
ctx=ctx,
)
@staticmethod
def DuplicateItem(
ctx: XErrorCtx, struct_name: str, tag: str, item: str
) -> XError:
return XError(
short=f"Duplicate item in {struct_name}",
what=f"In {tag} the item {item} is present more than once",
why=f"A set can only contain unique items",
ctx=ctx,
)
@staticmethod
def InvalidDictionaryItem(
ctx: XErrorCtx,
expected_tag: str,
expected_key: str,
expected_val: str,
dict_tag: str,
item_tag: str,
) -> XError:
return XError(
short="Invalid item in dictionary",
what=f"An unexpected item with {dict_tag} is in dictionary {item_tag}",
why=f"Each item must have tag {expected_tag} with children {expected_key} and {expected_val}",
ctx=ctx,
)
@staticmethod
def InvalidVariant(
ctx: XErrorCtx,
name: str,
expected_types: list[AnyType],
found_type: AnyType | None,
found_value: Any,
) -> XError:
types = " | ".join(map(str, expected_types))
return XError(
short=f"Datatype not in Union",
what=f"{name} is a union of {types}, which does not contain {found_type} (you provided: {found_value})",
why=f"... uuuh, its a union?",
ctx=ctx,
)
@staticmethod
def MultipleVariants(ctx: XErrorCtx, variant_names: list[str]) -> XError:
return XError(
short="Multiple union variants present",
what=f"variants {', '.join(variant_names)} are present",
why=f"A union can only be one variant at a time",
ctx=ctx,
)
@staticmethod
def ParseInvalidVariant(
ctx: XErrorCtx, tag: str, named_variants: list[str], found_variant: str
) -> XError:
return XError(
short="Invalid Variant",
what=f"The union {tag} can contain variants {', '.join(named_variants)}, but you have used {found_variant}",
why=f"Only valid variants can be parsed",
ctx=ctx,
)
@staticmethod
def NoneIsSome(ctx: XErrorCtx, name: str, val: Any) -> XError:
return XError(
short="None object is not None",
what=f"{name} contains value {val} which is not None",
why="A None type object can only contain none",
ctx=ctx,
)
@staticmethod
def NotADataclass(cls: AnyType) -> XError:
cls_name: str = typename(cls)
return XError(
short="Non-Dataclass",
what=f"{cls_name} is not a dataclass",
why=f"xmlify uses dataclasses to get fields",
ctx=XErrorCtx([cls_name]),
notes=[f"\nTry:\n@xmlify\n@dataclass\nclass {cls_name}:"],
)
@staticmethod
def ReservedAttribute(cls: AnyType, attr_name: str) -> XError:
cls_name: str = typename(cls)
return XError(
short=f"Reserved Attribute",
what=f"{cls_name}.{attr_name} is used by xmlify, so it cannot be a field of the class",
why=f"xmlify aguments {cls_name} by adding methods it can then use for xsd, xml generation and parsing",
ctx=XErrorCtx([cls_name]),
)
@staticmethod
def CommentAttribute(cls: AnyType) -> XError:
cls_name: str = typename(cls)
return XError(
short=f"Comment Attribute",
what=f"xmlifed classes cannot use comment as an attribute",
why=f"comment is used as a tag name for comments by lxml, so comments inserted on xml generation could conflict",
ctx=XErrorCtx([cls_name]),
)
@staticmethod
def NonMemberTag(
ctx: XErrorCtx, cls: AnyType, tag: str, name: str
) -> XError:
cls_name: str = typename(cls)
return XError(
short="Non member tag",
what=f"In {tag} {cls_name}.{name} could not be found.",
why=f"All members, including {cls_name}.{name} must be present",
ctx=ctx,
)
@staticmethod
def MissingAttribute(
cls: AnyType, required_attrs: set[str], missing_attr: str
) -> XError:
cls_name: str = typename(cls)
return XError(
short="Missing Attribute",
what=f"The attribute {missing_attr} is missing from {cls_name}",
why=f"To be manual_xmlified the attributes: {', '.join(required_attrs)} are required. Try using help(IXmlify)",
ctx=XErrorCtx([cls_name]),
)
@staticmethod
def DependencyCycle(cycle: list[AnyType]) -> XError:
return XError(
short="Dependency Cycle in XSD",
what=f"There is a cycle: {'<-'.join(map(str, cycle))}",
why="The XSDs for classes are written to the .xsd file in dependency order",
)
@staticmethod
def NotXmlified(cls: AnyType) -> XError:
cls_name: str = typename(cls)
return XError(
short="Not Xmlified",
what=f"{cls_name} is not xmlified, and hence cannot have an associated parser",
why=f"the .xsd(...) method is required to write_xsd",
notes=[f"To fix, try:\n@xmlify\n@dataclass\nclass {cls_name}: ..."],
)