The problem is that on the API boundary of yaml.load
, all representational information of the source has been lost. Validate
gets a Python dict and does not know where it originated from, and moreover the dict does not contain this information.
You can, however, implement this yourself. voluptuous' Invalid
error carries a path
which is a list of keys to follow. Having this path, you can parse the YAML again into nodes (which carry representation information) and discover the position of the item:
import yaml
def line_from(path, yaml_input):
node = yaml.compose(yaml_input)
for item in path:
for entry in node.value:
if entry[0].value == item:
node = entry[1]
break
else: raise ValueError("unknown path element: " + item)
return node.start_mark.line
# demostrating this on more complex input than yours
data = """
spam:
egg:
sausage:
spam
"""
print(line_from(["spam", "egg", "sausage"], data))
# gives 4
Having this, you can then do
try:
data = Validate(yaml.load(StringIO(data)))
except Invalid as e:
line = line_from(e.path, data)
path = "data." + ".".join(e.path)
print(f"Error: validation failed on line {line} ({path}): {e.error_message}")
I'll go this far for this answer as it shows you how to discover the origin line of an error. You will probably need to extend this to:
- handle YAML sequences (my code assumes that every intermediate node is a
MappingNode
, a SequenceNode
will have single nodes in its value
list instead of a key-value tuple)
- handle
MultipleInvalid
to issue a message for each inner error
- rewrite
expected int
to should be an integer
if you really want to (no idea how you'd do that)
- abort after printing the error