custom-components / pyscript

Pyscript adds rich Python scripting to HASS
Apache License 2.0
894 stars 47 forks source link

Issues using yaml module in pyscript #593

Closed erkr closed 8 months ago

erkr commented 8 months ago

I'm using the yaml module to parse ha yaml files. Not sure if this is a pyscript issue, a pyyaml issue, or the combination. But I'm stuck.

The issue: HA yaml files contain unsupported tags like !include and !secret. So I added constructors to handle them and ran into the issue that an await is missing somewhere.

I made the most simple example possible to demonstrate the issue.

The code:

class YamlLoader(yaml.SafeLoader):
    pass

def test_constructor(loader: yaml.SafeLoader, node: yaml.nodes.ScalarNode) -> str:
   return f"!Test handled"

YamlLoader.add_constructor('!Test', test_constructor)

yaml_str = """\
example: !Test foo
"""

@service(supports_response="only")
def test():
    data = yaml.load(yaml_str, Loader=YamlLoader)
    return {"data":str(data)}

When I run this test script in the Dev Tools, the response is this:

data: "{'example': <coroutine object EvalFuncVar.__call__ at 0x7f419cf4c0>}"

I expected "!Test handled" in stead of the "<coroutine..."

In the HA log this warning is logged:

This error originated from a custom integration.

Logger: py.warnings
Source: custom_components/pyscript/eval.py:491
Integration: Pyscript Python scripting (documentation, issues)
First occurred: 20:18:43 (1 occurrences)
Last logged: 20:18:43

/config/custom_components/pyscript/eval.py:491: RuntimeWarning: coroutine 'EvalFuncVar.__call__' was never awaited retval = await func.call(ast_ctx, **data)

It works fine when executing the code directly in a Python interpreter. I then remove @service... and adds line to call test()

Best Eric

craigbarratt commented 8 months ago

The issue is that all functions and classes in pyscript are async. yaml.load is expecting Loader to be a regular callback function, not an async function (see the 3rd bullet in the docs).

Here are three potential solutions:

erkr commented 8 months ago

@craigbarratt Thanks for helping out. I needed to combine options 2 and 3 and now it works 🥳

I created this module:

import yaml

@pyscript_compile
class YamlLoader(yaml.SafeLoader):
    pass

@pyscript_compile
def test_constructor(loader: yaml.SafeLoader, node: yaml.nodes.ScalarNode) -> str:
   return f"!Test handled"

def yaml_loader():
   loader = YamlLoader
   loader.add_constructor('!Test', test_constructor)
   return loader

And this service script:

import yaml
from yaml_loader import yaml_loader

yaml_str = """\
example: !Test foo
"""

@service(supports_response="only")
def test():
    data = yaml.load(yaml_str, Loader=yaml_loader())
    return {"data":str(data)}

And that indeed works: data: "{'example': '!Test handled'}

Thank, Eric