Exemple #1
0
async def test_service_exists(hass, caplog):
    """Test importing a pyscript module."""

    conf_dir = hass.config.path(FOLDER)

    file_contents = {
        f"{conf_dir}/hello.py":
        """
import xyz2
from xyz2 import f_minus

@service
def func1():
    pyscript.done = [xyz2.f_add(1, 2), xyz2.f_mult(3, 4), xyz2.f_add(10, 20), f_minus(50, 20)]
""",
        #
        # this will fail to load since import doesn't exist
        #
        f"{conf_dir}/bad_import.py":
        """
import no_such_package

@service
def func10():
    pass
""",
        #
        # this will fail to load since import has a syntax error
        #
        f"{conf_dir}/bad_import2.py":
        """
import bad_module

@service
def func11():
    pass
""",
        #
        # This will load, since there is an apps/world config entry
        #
        f"{conf_dir}/apps/world.py":
        """
from xyz2 import *

@service
def func2():
    pyscript.done = [get_x(), get_name(), other_name(), f_add(1, 5), f_mult(3, 6), f_add(10, 30), f_minus(50, 30)]
""",
        #
        # This will not load, since there is no apps/world2 config entry
        #
        f"{conf_dir}/apps/world2.py":
        """
from xyz2 import *

@service
def func10():
    pass
""",
        f"{conf_dir}/modules/xyz2/__init__.py":
        """
from .other import f_minus, other_name

log.info(f"modules/xyz2 global_ctx={pyscript.get_global_ctx()};")

x = 99

def f_add(a, b):
    return a + b

def f_mult(a, b):
    return a * b

def get_x():
    return x

def get_name():
    return __name__
""",
        f"{conf_dir}/modules/xyz2/other.py":
        """
def f_minus(a, b):
    return a - b

def other_name():
    return __name__
""",
        #
        # this module has a syntax error (missing :)
        #
        f"{conf_dir}/modules/bad_module.py":
        """
def func12()
    pass
""",
        #
        # this script file should auto-load
        #
        f"{conf_dir}/scripts/func13.py":
        """
@service
def func13():
    pass
""",
        #
        # this script file should auto-load
        #
        f"{conf_dir}/scripts/a/b/c/d/func14.py":
        """
@service
def func14():
    pass

log.info(f"func14 global_ctx={pyscript.get_global_ctx()};")

""",
        #
        # this script file should not auto-load
        #
        f"{conf_dir}/scripts/a/b/c/d/#func15.py":
        """
@service
def func15():
    pass
""",
        #
        # this script file should not auto-load
        #
        f"{conf_dir}/scripts/#a/b/c/d/func15.py":
        """
@service
def func15():
    pass
""",
    }

    mock_open = MockOpen()
    for key, value in file_contents.items():
        mock_open[key].read_data = value

    def isfile_side_effect(arg):
        return arg in file_contents

    def glob_side_effect(path, recursive=None):
        result = []
        path_re = path.replace("*", "[^/]*").replace(".", "\\.")
        path_re = path_re.replace("[^/]*[^/]*/", ".*")
        for this_path in file_contents:
            if re.match(path_re, this_path):
                result.append(this_path)
        return result

    conf = {"apps": {"world": {}}}
    with patch(
            "custom_components.pyscript.os.path.isdir", return_value=True
    ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch(
            "custom_components.pyscript.global_ctx.open", mock_open
    ), patch("custom_components.pyscript.open", mock_open), patch(
            "homeassistant.config.load_yaml_config_file",
            return_value={"pyscript": conf}
    ), patch("custom_components.pyscript.watchdog_start",
             return_value=None), patch(
                 "custom_components.pyscript.os.path.getmtime",
                 return_value=1000), patch(
                     "custom_components.pyscript.global_ctx.os.path.getmtime",
                     return_value=1000), patch(
                         "custom_components.pyscript.os.path.isfile"
                     ) as mock_isfile:
        mock_isfile.side_effect = isfile_side_effect
        mock_glob.side_effect = glob_side_effect
        assert await async_setup_component(hass, "pyscript", {DOMAIN: conf})

    notify_q = asyncio.Queue(0)

    async def state_changed(event):
        var_name = event.data["entity_id"]
        if var_name != "pyscript.done":
            return
        value = event.data["new_state"].state
        await notify_q.put(value)

    hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed)

    assert not hass.services.has_service("pyscript", "func10")
    assert not hass.services.has_service("pyscript", "func11")
    assert hass.services.has_service("pyscript", "func13")
    assert hass.services.has_service("pyscript", "func14")
    assert not hass.services.has_service("pyscript", "func15")

    await hass.services.async_call("pyscript", "func1", {})
    ret = await wait_until_done(notify_q)
    assert literal_eval(ret) == [1 + 2, 3 * 4, 10 + 20, 50 - 20]

    await hass.services.async_call("pyscript", "func2", {})
    ret = await wait_until_done(notify_q)
    assert literal_eval(ret) == [
        99, "xyz2", "xyz2.other", 1 + 5, 3 * 6, 10 + 30, 50 - 30
    ]

    assert "modules/xyz2 global_ctx=modules.xyz2;" in caplog.text
    assert "func14 global_ctx=scripts.a.b.c.d.func14;" in caplog.text
    assert "ModuleNotFoundError: import of no_such_package not allowed" in caplog.text
    assert "SyntaxError: invalid syntax (bad_module.py, line 2)" in caplog.text
Exemple #2
0
async def test_jupyter_kernel_msgs(hass, caplog):
    """Test Jupyter kernel messages."""
    sock, _ = await setup_script(hass, [dt(2020, 7, 1, 11, 0, 0, 0)], "")

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
    #
    # test the heartbeat loopback with some long and short messages
    # also send messages to stdin and iopub, which ignore them
    #
    for i in range(5):
        if i & 1:
            msg = (f"hello {i} " * 40).encode("utf-8")
        else:
            msg = f"hello {i}".encode("utf-8")
        await sock["hb_port"].send(msg)
        await sock["iopub_port"].send(msg)
        await sock["stdin_port"].send(msg)
        assert await sock["hb_port"].recv() == msg

    #
    # now send some shell messages and check the responses
    #
    reply = await shell_msg(sock, "kernel_info_request", {})
    assert reply["header"]["msg_type"] == "kernel_info_reply"

    reply = await shell_msg(sock, "comm_info_request", {})
    assert reply["header"]["msg_type"] == "comm_info_reply"

    reply = await shell_msg(sock, "history_request", {})
    assert reply["header"]["msg_type"] == "history_reply"

    #
    # test completions
    #
    code = "whi"
    reply = await shell_msg(sock, "complete_request", {
        "code": code,
        "cursor_pos": len(code)
    })
    assert reply["header"]["msg_type"] == "complete_reply"
    assert reply["content"]["matches"] == ["while"]

    #
    # test completions
    #
    code = "1+2\n3+4\nwhi"
    reply = await shell_msg(sock, "complete_request", {
        "code": code,
        "cursor_pos": len(code)
    })
    assert reply["header"]["msg_type"] == "complete_reply"
    assert reply["content"]["matches"] == ["while"]

    code = "pyscr"
    reply = await shell_msg(sock, "complete_request", {
        "code": code,
        "cursor_pos": len(code)
    })
    assert reply["header"]["msg_type"] == "complete_reply"
    assert reply["content"]["matches"] == [
        "pyscript",
        "pyscript.config",
        "pyscript.get_global_ctx",
        "pyscript.list_global_ctx",
        "pyscript.set_global_ctx",
    ]

    hass.states.async_set("pyscript.f1var1", 0)
    reply = await shell_msg(sock, "complete_request", {
        "code": "pyscript.f",
        "cursor_pos": len("pyscript.f")
    })
    assert reply["header"]["msg_type"] == "complete_reply"
    assert reply["content"]["matches"] == ["pyscript.f1var1"]

    hass.states.async_set("pyscript.f1var1", 0, {"attr1": 5, "attr2": 10})
    reply = await shell_msg(sock, "complete_request", {
        "code": "pyscript.f1var1.a",
        "cursor_pos": len("pyscript.f1var1.a")
    })
    assert reply["header"]["msg_type"] == "complete_reply"
    assert reply["content"]["matches"] == [
        "pyscript.f1var1.attr1", "pyscript.f1var1.attr2"
    ]

    #
    # test is_complete
    #
    reply = await shell_msg(sock, "is_complete_request", {"code": "x = 1"})
    assert reply["header"]["msg_type"] == "is_complete_reply"
    assert reply["content"]["status"] == "complete"

    reply = await shell_msg(sock, "is_complete_request",
                            {"code": "def func():\n    pass"})
    assert reply["header"]["msg_type"] == "is_complete_reply"
    assert reply["content"]["status"] == "incomplete"

    reply = await shell_msg(sock, "is_complete_request",
                            {"code": "def func():\n    pass\n"})
    assert reply["header"]["msg_type"] == "is_complete_reply"
    assert reply["content"]["status"] == "complete"

    reply = await shell_msg(sock, "is_complete_request", {"code": "x = "})
    assert reply["header"]["msg_type"] == "is_complete_reply"
    assert reply["content"]["status"] == "invalid"

    reply = await shell_msg(sock, "is_complete_request", {"code": "if 1:\n"})
    assert reply["header"]["msg_type"] == "is_complete_reply"
    assert reply["content"]["status"] == "incomplete"

    #
    # test code execution
    #
    reply = await shell_msg(sock,
                            "execute_request", {"code": "x = 123; x + 1 + 2"},
                            execute=True)
    assert reply["content"]["data"]["text/plain"] == "126"

    reply = await shell_msg(sock,
                            "execute_request", {"code": "import math; x + 5"},
                            execute=True)
    assert reply["content"]["data"]["text/plain"] == "128"

    #
    # do a reload to make sure our global context is preserved
    #
    with patch("homeassistant.config.load_yaml_config_file", return_value={}):
        await hass.services.async_call("pyscript", "reload", {}, blocking=True)

    reply = await shell_msg(sock,
                            "execute_request", {"code": "x + 10"},
                            execute=True)
    assert reply["content"]["data"]["text/plain"] == "133"

    #
    # test completion of object attribute now that we've loaded math above
    #
    code = "import math; math.sq"
    reply = await shell_msg(sock, "complete_request", {
        "code": code,
        "cursor_pos": len(code)
    })
    assert reply["header"]["msg_type"] == "complete_reply"
    assert reply["content"]["matches"] == ["math.sqrt"]

    #
    # run-time error
    #
    reply = await shell_msg(sock,
                            "execute_request", {"code": "xyz"},
                            execute=True)
    assert reply["content"]["evalue"] == "name 'xyz' is not defined"

    #
    # syntax error
    #
    reply = await shell_msg(sock,
                            "execute_request", {"code": "1 + "},
                            execute=True)
    assert reply["content"]["evalue"] == "invalid syntax (jupyter_0, line 1)"

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)

    await shutdown(sock)
Exemple #3
0
async def setup_subaru_integration(
    hass,
    vehicle_list=None,
    vehicle_data=None,
    vehicle_status=None,
    connect_success=True,
):
    """Create Subaru entry."""
    assert await async_setup_component(hass, DOMAIN, {})

    config_entry = MockConfigEntry(
        domain=DOMAIN, data=TEST_CONFIG, options=TEST_OPTIONS, entry_id=1,
    )
    config_entry.add_to_hass(hass)

    with patch(
        "custom_components.subaru.SubaruAPI.connect",
        return_value=connect_success,
        side_effect=None
        if connect_success
        else InvalidCredentials("Invalid Credentials"),
    ), patch(
        "custom_components.subaru.SubaruAPI.get_vehicles", return_value=vehicle_list,
    ), patch(
        "custom_components.subaru.SubaruAPI.vin_to_name",
        return_value=vehicle_data[VEHICLE_NAME],
    ), patch(
        "custom_components.subaru.SubaruAPI.get_api_gen",
        return_value=vehicle_data[VEHICLE_API_GEN],
    ), patch(
        "custom_components.subaru.SubaruAPI.get_ev_status",
        return_value=vehicle_data[VEHICLE_HAS_EV],
    ), patch(
        "custom_components.subaru.SubaruAPI.get_res_status",
        return_value=vehicle_data[VEHICLE_HAS_REMOTE_START],
    ), patch(
        "custom_components.subaru.SubaruAPI.get_remote_status",
        return_value=vehicle_data[VEHICLE_HAS_REMOTE_SERVICE],
    ), patch(
        "custom_components.subaru.SubaruAPI.get_safety_status",
        return_value=vehicle_data[VEHICLE_HAS_SAFETY_SERVICE],
    ), patch(
        "custom_components.subaru.SubaruAPI.get_data", return_value=vehicle_status,
    ), patch(
        "custom_components.subaru.SubaruAPI.update",
    ), patch(
        "custom_components.subaru.SubaruAPI.fetch",
    ):
        success = await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    if success:
        return config_entry
    return None
async def test_api(hass):
    """Test API."""
    MOCK_ENTRY.add_to_hass(hass)
    implementation = ZoomOAuth2Implementation(
        hass,
        DOMAIN,
        MOCK_ENTRY.data[CONF_CLIENT_ID],
        MOCK_ENTRY.data[CONF_CLIENT_SECRET],
        OAUTH2_AUTHORIZE,
        OAUTH2_TOKEN,
        MOCK_ENTRY.data[CONF_VERIFICATION_TOKEN],
    )
    api = ZoomAPI(
        config_entry_oauth2_flow.OAuth2Session(hass, MOCK_ENTRY,
                                               implementation))

    assert await api.async_get_access_token() == MOCK_TOKEN

    with patch(
            "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_request",
            return_value=AiohttpClientMockResponse(
                "get",
                "zoom_url",
                status=HTTPStatus.OK,
                json={
                    "id": "test",
                    "first_name": "test"
                },
            ),
    ):
        await api.async_get_contact_user_profile("test")

    with patch(
            "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_request",
            return_value=AiohttpClientMockResponse(
                "get",
                "zoom_url",
                status=HTTPStatus.OK,
                json={
                    "next_page_token": "",
                    "contacts": [{
                        "id": "test",
                        "first_name": "test"
                    }],
                },
            ),
    ):
        await api.async_get_contacts()

    with patch(
            "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_request",
            return_value=AiohttpClientMockResponse(
                "get",
                "zoom_url",
                status=HTTPStatus.OK,
                json={
                    "id": "test",
                    "first_name": "test"
                },
            ),
    ):
        await api.async_get_my_user_profile()
Exemple #5
0
async def setup_script(hass, now, source, no_connect=False):
    """Initialize and load the given pyscript."""

    scripts = [
        "/some/config/dir/pyscripts/hello.py",
    ]

    with patch(
            "custom_components.pyscript.os.path.isdir",
            return_value=True), patch(
                "custom_components.pyscript.glob.iglob",
                return_value=scripts), patch(
                    "custom_components.pyscript.global_ctx.open",
                    mock_open(read_data=source),
                    create=True,
                ), patch(
                    "custom_components.pyscript.trigger.dt_now",
                    return_value=now), patch(
                        "homeassistant.config.load_yaml_config_file",
                        return_value={}), patch(
                            "custom_components.pyscript.install_requirements",
                            return_value=None,
                        ):
        assert await async_setup_component(hass, "pyscript", {DOMAIN: {}})

    #
    # I'm not sure how to run the mock all the time, so just force the dt_now()
    # trigger function to return the given list of times in now.
    #
    def return_next_time():
        nonlocal now
        if isinstance(now, list):
            if len(now) > 1:
                return now.pop(0)
            return now[0]
        return now

    trigger.__dict__["dt_now"] = return_next_time

    kernel_state_var = "pyscript.jupyter_ports_1234"
    kernel_cfg = {
        "ip": "127.0.0.1",
        "key": SECRET_KEY.decode("utf-8"),
        "signature_scheme": "hmac-sha256",
        "state_var": kernel_state_var,
    }
    if no_connect:
        kernel_cfg["no_connect_timeout"] = 0.0
    await hass.services.async_call("pyscript", "jupyter_kernel_start",
                                   kernel_cfg)

    while True:
        ports_state = hass.states.get(kernel_state_var)
        if ports_state is not None:
            break
        await asyncio.sleep(2e-3)

    port_nums = json.loads(ports_state.state)

    sock = {}

    if no_connect:
        return sock, port_nums

    for name in PORT_NAMES:
        kernel_reader, kernel_writer = await asyncio.open_connection(
            "127.0.0.1", port_nums[name])
        sock[name] = ZmqSocket(kernel_reader, kernel_writer, "ROUTER")
        await sock[name].handshake()

    return sock, port_nums
Exemple #6
0
def bypass_package_install_fixture():
    """Bypass package installation."""
    with patch(
            "custom_components.pyscript.requirements.async_process_requirements"
    ):
        yield
Exemple #7
0
async def test_install_requirements(hass, caplog):
    """Test install_requirements function."""
    with patch(
            "custom_components.pyscript.requirements.process_all_requirements"
    ) as process_requirements, patch(
            "custom_components.pyscript.requirements.async_process_requirements"
    ) as ha_install_requirements:
        entry = MockConfigEntry(domain=DOMAIN,
                                data={CONF_ALLOW_ALL_IMPORTS: True})
        entry.add_to_hass(hass)

        # Check that packages get installed correctly
        process_requirements.return_value = {
            "my-package-name": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                "2.0.1",
                ATTR_INSTALLED_VERSION:
                None,
            },
            "my-package-name-alternate": {
                ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"],
                ATTR_VERSION: "2.0.1",
                ATTR_INSTALLED_VERSION: None,
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert ha_install_requirements.called
        assert ha_install_requirements.call_args[0][2] == [
            "my-package-name==2.0.1",
            "my-package-name-alternate==2.0.1",
        ]
        assert CONF_INSTALLED_PACKAGES in entry.data
        assert entry.data[CONF_INSTALLED_PACKAGES] == {
            "my-package-name": "2.0.1",
            "my-package-name-alternate": "2.0.1",
        }

        # Check that we stop tracking packages whose version no longer matches what
        # we have stored (see previous line for what we currently have stored)
        ha_install_requirements.reset_mock()
        caplog.clear()
        process_requirements.return_value = {
            "my-package-name": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                "2.0.1",
                ATTR_INSTALLED_VERSION:
                "2.0.1",
            },
            "my-package-name-alternate": {
                ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"],
                ATTR_VERSION: "2.0.1",
                ATTR_INSTALLED_VERSION: "1.2.1",
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert not ha_install_requirements.called
        assert caplog.record_tuples == [(
            "custom_components.pyscript",
            logging.WARNING,
            ("Version '2.0.1' for package 'my-package-name-alternate' detected "
             "in '['tests/test_data/test_requirements/requirements.txt']' will "
             "be ignored in favor of the version '1.2.1' which was installed "
             "outside of pyscript"),
        )]
        assert entry.data[CONF_INSTALLED_PACKAGES] == {
            "my-package-name": "2.0.1"
        }

        # Check that version upgrades are handled if the version was installed
        # by us before
        ha_install_requirements.reset_mock()
        caplog.clear()
        process_requirements.return_value = {
            "my-package-name": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                "2.2.0",
                ATTR_INSTALLED_VERSION:
                "2.0.1",
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert ha_install_requirements.called
        assert ha_install_requirements.call_args[0][2] == [
            "my-package-name==2.2.0"
        ]
        assert entry.data[CONF_INSTALLED_PACKAGES] == {
            "my-package-name": "2.2.0"
        }

        # Check that we don't install untracked but existing packages
        ha_install_requirements.reset_mock()
        caplog.clear()
        process_requirements.return_value = {
            "my-package-name-alternate": {
                ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"],
                ATTR_VERSION: "2.0.1",
                ATTR_INSTALLED_VERSION: "1.2.1",
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert not ha_install_requirements.called
        assert entry.data[CONF_INSTALLED_PACKAGES] == {
            "my-package-name": "2.2.0"
        }

        # Check that we can downgrade as long as we installed the package
        ha_install_requirements.reset_mock()
        caplog.clear()
        process_requirements.return_value = {
            "my-package-name": {
                ATTR_SOURCES: [f"{PYSCRIPT_FOLDER}/requirements.txt"],
                ATTR_VERSION: "2.0.1",
                ATTR_INSTALLED_VERSION: "2.2.0",
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert ha_install_requirements.called
        assert ha_install_requirements.call_args[0][2] == [
            "my-package-name==2.0.1"
        ]
        assert entry.data[CONF_INSTALLED_PACKAGES] == {
            "my-package-name": "2.0.1"
        }
Exemple #8
0
async def test_reload(hass, caplog):
    """Test reload a pyscript module."""

    conf_dir = hass.config.path(FOLDER)

    file_contents = {
        f"{conf_dir}/hello.py":
        """
import xyz2
from xyz2 import xyz

#
# ensure a regular script doesn't have pyscript.app_config set
#
try:
    x = pyscript.app_config
    assert False
except NameError:
    pass

log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} xyz={xyz} xyz2.xyz={xyz2.xyz}")

@service
def func1():
    pass
""",
        #
        # This will load, since there is an apps/world config entry
        #
        f"{conf_dir}/apps/world.py":
        """
from xyz2 import *

log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} xyz={xyz}")

@service
def func2():
    pass
""",
        #
        # This will load, since there is an apps/world2 config entry
        #
        f"{conf_dir}/apps/world2/__init__.py":
        """
from .other import *

assert pyscript.config['apps']['world2'] == pyscript.app_config

log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} var1={pyscript.config['apps']['world2']['var1']}, other_abc={other_abc}")

@service
def func3():
    pass
""",
        f"{conf_dir}/apps/world2/other.py":
        """
other_abc = 987

log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()}")

#
# ensure a sub file in the app doesn't have pyscript.app_config set
#
try:
    x = pyscript.app_config
    assert False
except NameError:
    pass

@time_trigger("shutdown")
def shutdown(trigger_time=None):
    log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} shutdown trigger_time={trigger_time}")
""",
        f"{conf_dir}/modules/xyz2/__init__.py":
        """
from .other import xyz

#
# ensure a module doesn't have pyscript.app_config set
#
try:
    x = pyscript.app_config
    assert False
except NameError:
    pass

log.info(f"modules/xyz2 global_ctx={pyscript.get_global_ctx()};")
""",
        f"{conf_dir}/modules/xyz2/other.py":
        """
log.info(f"modules/xyz2/other global_ctx={pyscript.get_global_ctx()};")

xyz = 123
""",
        #
        # these shouldn't load since the package takes precedence
        #
        f"{conf_dir}/modules/xyz2.py":
        """
log.info(f"BOTCH shouldn't load {__name__}")
""",
        f"{conf_dir}/apps/world2.py":
        """
log.info(f"BOTCH shouldn't load {__name__}")
""",
    }

    mock_open = MockOpen()
    for key, value in file_contents.items():
        mock_open[key].read_data = value

    def isfile_side_effect(arg):
        return arg in file_contents

    def glob_side_effect(path, recursive=None):
        result = []
        path_re = path.replace("*", "[^/]*").replace(".", "\\.")
        path_re = path_re.replace("[^/]*[^/]*/", ".*")
        for this_path in file_contents:
            if re.match(path_re, this_path):
                result.append(this_path)
        return result

    conf = {"apps": {"world": {}, "world2": {"var1": 100}}}
    with patch(
            "custom_components.pyscript.os.path.isdir", return_value=True
    ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch(
            "custom_components.pyscript.global_ctx.open", mock_open
    ), patch("custom_components.pyscript.open", mock_open), patch(
            "homeassistant.util.yaml.loader.open", mock_open
    ), patch("homeassistant.config.load_yaml_config_file",
             return_value={"pyscript": conf}), patch(
                 "custom_components.pyscript.watchdog_start",
                 return_value=None), patch(
                     "custom_components.pyscript.os.path.getmtime",
                     return_value=1000
                 ), patch(
                     "custom_components.pyscript.global_ctx.os.path.getmtime",
                     return_value=1000), patch(
                         "custom_components.pyscript.os.path.isfile"
                     ) as mock_isfile:
        mock_isfile.side_effect = isfile_side_effect
        mock_glob.side_effect = glob_side_effect
        assert await async_setup_component(hass, "pyscript", {DOMAIN: conf})

        notify_q = asyncio.Queue(0)

        async def state_changed(event):
            var_name = event.data["entity_id"]
            if var_name != "pyscript.done":
                return
            value = event.data["new_state"].state
            await notify_q.put(value)

        hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed)

        assert hass.services.has_service("pyscript", "func1")
        assert hass.services.has_service("pyscript", "func2")
        assert hass.services.has_service("pyscript", "func3")

        assert "modules/xyz2 global_ctx=modules.xyz2;" in caplog.text
        assert "modules/xyz2/other global_ctx=modules.xyz2.other;" in caplog.text
        assert "hello global_ctx=file.hello xyz=123 xyz2.xyz=123" in caplog.text
        assert "world2.other global_ctx=apps.world2.other" in caplog.text
        assert "world2 global_ctx=apps.world2 var1=100, other_abc=987" in caplog.text

        #
        # add a new script file
        #
        file_contents[f"{conf_dir}/hello2.py"] = """
log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()};")

@service
def func20():
    pass
"""
        mock_open[f"{conf_dir}/hello2.py"].read_data = file_contents[
            f"{conf_dir}/hello2.py"]

        #
        # should not load the new script if we reload something else
        #
        await hass.services.async_call("pyscript",
                                       "reload", {"global_ctx": "file.hello"},
                                       blocking=True)
        assert not hass.services.has_service("pyscript", "func20")
        assert "hello2 global_ctx=file.hello2;" not in caplog.text

        #
        # should load new file
        #
        await hass.services.async_call("pyscript", "reload", {}, blocking=True)
        assert hass.services.has_service("pyscript", "func20")
        assert "hello2 global_ctx=file.hello2;" in caplog.text

        #
        # delete the script file
        #
        del file_contents[f"{conf_dir}/hello2.py"]

        #
        # should not delete the script file if we reload something else
        #
        await hass.services.async_call("pyscript",
                                       "reload", {"global_ctx": "file.hello"},
                                       blocking=True)
        assert hass.services.has_service("pyscript", "func20")

        #
        # should delete the script file
        #
        await hass.services.async_call("pyscript", "reload", {}, blocking=True)
        assert not hass.services.has_service("pyscript", "func20")

        #
        # change a module file and confirm the parent script is reloaded too
        #
        file_contents[f"{conf_dir}/modules/xyz2/other.py"] = """
log.info(f"modules/xyz2/other global_ctx={pyscript.get_global_ctx()};")

xyz = 456
"""
        mock_open[
            f"{conf_dir}/modules/xyz2/other.py"].read_data = file_contents[
                f"{conf_dir}/modules/xyz2/other.py"]

        await hass.services.async_call("pyscript", "reload", {}, blocking=True)
        assert "hello global_ctx=file.hello xyz=456 xyz2.xyz=456" in caplog.text

        #
        # change the app config
        #
        conf["apps"]["world2"]["var1"] = 200
        await hass.services.async_call("pyscript", "reload", {}, blocking=True)
        assert "world2 global_ctx=apps.world2 var1=200, other_abc=987" in caplog.text
        assert "world2.other global_ctx=apps.world2.other shutdown trigger_time=shutdown" in caplog.text

        #
        # change a module inside an app
        #
        file_contents[f"{conf_dir}/apps/world2/other.py"] = """
other_abc = 654

log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()}")

@time_trigger("shutdown")
def shutdown(trigger_time=None):
    log.info(f"{__name__} global_ctx={pyscript.get_global_ctx()} shutdown_new trigger_time={trigger_time}")
"""
        mock_open[
            f"{conf_dir}/apps/world2/other.py"].read_data = file_contents[
                f"{conf_dir}/apps/world2/other.py"]
        await hass.services.async_call("pyscript", "reload", {}, blocking=True)
        assert "world2 global_ctx=apps.world2 var1=200, other_abc=654" in caplog.text
        assert "world2.other global_ctx=apps.world2.other shutdown trigger_time=shutdown" in caplog.text

        #
        # now confirm certain files reloaded the correct number of times,
        # and reload everything a few times
        #
        for i in range(3):
            assert caplog.text.count(
                "world global_ctx=apps.world xyz=") == 2 + i
            assert caplog.text.count(
                "world2 global_ctx=apps.world2 var1=") == 3 + i
            assert caplog.text.count(
                "hello global_ctx=file.hello xyz=") == 4 + i
            assert caplog.text.count(
                "modules/xyz2/other global_ctx=modules.xyz2.other") == 2 + i
            assert caplog.text.count(
                "modules/xyz2 global_ctx=modules.xyz2") == 2 + i
            assert (caplog.text.count(
                "world2.other global_ctx=apps.world2.other shutdown trigger_time=shutdown"
            ) == 2)
            assert (caplog.text.count(
                "world2.other global_ctx=apps.world2.other shutdown_new trigger_time=shutdown"
            ) == i)
            if i < 2:
                await hass.services.async_call("pyscript",
                                               "reload", {"global_ctx": "*"},
                                               blocking=True)

        #
        # make sure files that shouldn't load were not loaded
        #
        assert "BOTCH shouldn't load" not in caplog.text
Exemple #9
0
async def test_install_unpinned_requirements(hass, caplog):
    """Test install_requirements function with unpinned versions."""
    with patch(
            "custom_components.pyscript.requirements.process_all_requirements"
    ) as process_requirements, patch(
            "custom_components.pyscript.requirements.async_process_requirements"
    ) as ha_install_requirements:
        entry = MockConfigEntry(domain=DOMAIN,
                                data={CONF_ALLOW_ALL_IMPORTS: True})
        entry.add_to_hass(hass)

        # Check that unpinned version gets skipped because a version is already
        # installed
        process_requirements.return_value = {
            "my-package-name": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                UNPINNED_VERSION,
                ATTR_INSTALLED_VERSION:
                "2.0.1",
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert not ha_install_requirements.called

        # Check that unpinned version gets installed because it isn't already
        # installed
        process_requirements.return_value = {
            "my-package-name": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                UNPINNED_VERSION,
                ATTR_INSTALLED_VERSION:
                None,
            },
            "my-package-name-1": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                "2.0.1",
                ATTR_INSTALLED_VERSION:
                None,
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert ha_install_requirements.called
        assert ha_install_requirements.call_args[0][2] == [
            "my-package-name", "my-package-name-1==2.0.1"
        ]
        # my-package-name will show as not installed and therefore won't be included
        assert entry.data[CONF_INSTALLED_PACKAGES] == {
            "my-package-name-1": "2.0.1"
        }

        # Check that entry.data[CONF_INSTALLED_PACKAGES] gets updated with a version number
        # when unpinned version was requested
        with patch("custom_components.pyscript.requirements.installed_version",
                   return_value="1.1.1"):
            process_requirements.return_value = {
                "my-package-name": {
                    ATTR_SOURCES: [
                        f"{PYSCRIPT_FOLDER}/requirements.txt",
                        f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                    ],
                    ATTR_VERSION:
                    UNPINNED_VERSION,
                    ATTR_INSTALLED_VERSION:
                    None,
                },
                "my-package-name-1": {
                    ATTR_SOURCES: [
                        f"{PYSCRIPT_FOLDER}/requirements.txt",
                        f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                    ],
                    ATTR_VERSION:
                    "2.0.1",
                    ATTR_INSTALLED_VERSION:
                    None,
                },
            }
            await install_requirements(hass, entry, PYSCRIPT_FOLDER)
            await hass.async_block_till_done()
            assert ha_install_requirements.called
            assert ha_install_requirements.call_args[0][2] == [
                "my-package-name", "my-package-name-1==2.0.1"
            ]
            assert entry.data[CONF_INSTALLED_PACKAGES] == {
                "my-package-name": "1.1.1",
                "my-package-name-1": "2.0.1",
            }

        # Check that package gets removed from entry.data[CONF_INSTALLED_PACKAGES] when it was
        # previously installed by pyscript but version was changed presumably by another system
        process_requirements.return_value = {
            "my-package-name": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                UNPINNED_VERSION,
                ATTR_INSTALLED_VERSION:
                "2.0.0",
            },
            "my-package-name-1": {
                ATTR_SOURCES: [
                    f"{PYSCRIPT_FOLDER}/requirements.txt",
                    f"{PYSCRIPT_FOLDER}/apps/app1/requirements.txt",
                ],
                ATTR_VERSION:
                "2.0.1",
                ATTR_INSTALLED_VERSION:
                None,
            },
        }
        await install_requirements(hass, entry, PYSCRIPT_FOLDER)
        await hass.async_block_till_done()
        assert ha_install_requirements.called
        assert ha_install_requirements.call_args[0][2] == [
            "my-package-name-1==2.0.1"
        ]
        assert entry.data[CONF_INSTALLED_PACKAGES] == {
            "my-package-name-1": "2.0.1"
        }
Exemple #10
0
async def test_tasks(hass, caplog):
    """Test starting tasks."""

    conf_dir = hass.config.path(FOLDER)

    file_contents = {
        f"{conf_dir}/hello.py":
        """

#
# check starting multiple tasks, each stopping the prior one
#
def task1(cnt, last):
    task.unique('task1')
    if not last:
        task.sleep(10)
    log.info(f"finished task1, cnt={cnt}")

for cnt in range(10):
    task.create(task1, cnt, cnt == 9)

#
# check the return value after wait
#
def task2(arg):
    return 2 * arg

t2a = task.create(task2, 21)
t2b = task.create(task2, 51)
done, pending = task.wait({t2a, t2b})
log.info(f"task2() results = {[t2a.result(), t2b.result()]}, len(done) = {len(done)};")

#
# check the return value with a regular function
#
@pyscript_compile
def task3(arg):
    return 2 * arg

t3a = task.create(task3, 22)
t3b = task.create(task3, 52)
done, pending = task.wait({t3a, t3b})
log.info(f"task3() results = {[t3a.result(), t3b.result()]}, len(done) = {len(done)};")


#
# check that we can do a done callback
#
def task4(arg):
    task.wait_until(state_trigger="pyscript.var4 == '1'")
    return 2 * arg

def callback4a(arg):
    log.info(f"callback4a arg = {arg}")

def callback4b(arg):
    log.info(f"callback4b arg = {arg}")

def callback4c(arg):
    log.info(f"callback4c arg = {arg}")

t4 = task.create(task4, 23)
task.add_done_callback(t4, callback4a, 26)
task.add_done_callback(t4, callback4b, 101)
task.add_done_callback(t4, callback4c, 200)
task.add_done_callback(t4, callback4a, 25)
task.add_done_callback(t4, callback4c, 201)
task.add_done_callback(t4, callback4b, 100)
task.add_done_callback(t4, callback4a, 24)
task.remove_done_callback(t4, callback4c)
task.remove_done_callback(t4, task4)
pyscript.var4 = 1
done, pending = task.wait({t4})
log.info(f"task4() result = {t4.result()}, len(done) = {len(done)};")

#
# make sure we can't cancel a non-user task
#
try:
    task.cancel()
except TypeError as exc:
    log.info(f"task.cancel: {exc}")

#
# check that we can cancel ourselves
#
def task5(arg=None):
    log.info(f"task5 arg = {arg}")
    task.cancel()
    log.info(f"task5 BOTCH")

t5 = task.create(task5, 83)
done, pending = task.wait({t5})

""",
    }

    mock_open = MockOpen()
    for key, value in file_contents.items():
        mock_open[key].read_data = value

    def isfile_side_effect(arg):
        return arg in file_contents

    def glob_side_effect(path, recursive=None):
        result = []
        path_re = path.replace("*", "[^/]*").replace(".", "\\.")
        path_re = path_re.replace("[^/]*[^/]*/", ".*")
        for this_path in file_contents:
            if re.match(path_re, this_path):
                result.append(this_path)
        return result

    conf = {"apps": {"world": {}}}
    with patch(
            "custom_components.pyscript.os.path.isdir", return_value=True
    ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch(
            "custom_components.pyscript.global_ctx.open", mock_open
    ), patch("custom_components.pyscript.open", mock_open), patch(
            "homeassistant.config.load_yaml_config_file",
            return_value={"pyscript": conf}
    ), patch("custom_components.pyscript.os.path.getmtime",
             return_value=1000), patch(
                 "custom_components.pyscript.watchdog_start",
                 return_value=None), patch(
                     "custom_components.pyscript.global_ctx.os.path.getmtime",
                     return_value=1000), patch(
                         "custom_components.pyscript.os.path.isfile"
                     ) as mock_isfile:
        mock_isfile.side_effect = isfile_side_effect
        mock_glob.side_effect = glob_side_effect
        assert await async_setup_component(hass, "pyscript", {DOMAIN: conf})

    notify_q = asyncio.Queue(0)

    async def state_changed(event):
        var_name = event.data["entity_id"]
        if var_name != "pyscript.done":
            return
        value = event.data["new_state"].state
        await notify_q.put(value)

    hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed)

    assert caplog.text.count("finished task1, cnt=9") == 1
    assert "task2() results = [42, 102], len(done) = 2;" in caplog.text
    assert "task3() results = [44, 104], len(done) = 2;" in caplog.text
    assert "task4() result = 46, len(done) = 1;" in caplog.text
    assert caplog.text.count("callback4a arg =") == 1
    assert "callback4a arg = 24" in caplog.text
    assert caplog.text.count("callback4b arg =") == 1
    assert "callback4b arg = 100" in caplog.text
    assert "callback4c arg =" not in caplog.text
    assert caplog.text.count("is not a user-started task") == 1
    assert "task5 arg = 83" in caplog.text
    assert "task5 BOTCH" not in caplog.text
Exemple #11
0
async def setup_script(hass, notify_q, notify_q2, now, source, config=None):
    """Initialize and load the given pyscript."""

    conf_dir = hass.config.path(FOLDER)

    file_contents = {f"{conf_dir}/hello.py": source}

    mock_open = MockOpen()
    for key, value in file_contents.items():
        mock_open[key].read_data = value

    def isfile_side_effect(arg):
        return arg in file_contents

    def glob_side_effect(path, recursive=None):
        result = []
        path_re = path.replace("*", "[^/]*").replace(".", "\\.")
        path_re = path_re.replace("[^/]*[^/]*/", ".*")
        for this_path in file_contents:
            if re.match(path_re, this_path):
                result.append(this_path)
        return result

    if not config:
        config = {DOMAIN: {CONF_ALLOW_ALL_IMPORTS: True}}
    with patch(
            "custom_components.pyscript.os.path.isdir", return_value=True
    ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch(
            "custom_components.pyscript.global_ctx.open", mock_open
    ), patch(
            "custom_components.pyscript.trigger.dt_now", return_value=now
    ), patch("custom_components.pyscript.open", mock_open), patch(
            "homeassistant.config.load_yaml_config_file", return_value=config
    ), patch(
            "custom_components.pyscript.install_requirements",
            return_value=None,
    ), patch("custom_components.pyscript.watchdog_start",
             return_value=None), patch(
                 "custom_components.pyscript.os.path.getmtime",
                 return_value=1000), patch(
                     "custom_components.pyscript.global_ctx.os.path.getmtime",
                     return_value=1000), patch(
                         "custom_components.pyscript.os.path.isfile"
                     ) as mock_isfile:
        mock_isfile.side_effect = isfile_side_effect
        mock_glob.side_effect = glob_side_effect
        assert await async_setup_component(hass, "pyscript", config)

    #
    # I'm not sure how to run the mock all the time, so just force the dt_now()
    # trigger function to return the given list of times in now.
    #
    def return_next_time():
        nonlocal now
        if isinstance(now, list):
            if len(now) > 1:
                return now.pop(0)
            return now[0]
        return now

    trigger.__dict__["dt_now"] = return_next_time

    if notify_q or notify_q2:

        async def state_changed(event):
            var_name = event.data["entity_id"]
            if var_name == "pyscript.done":
                value = event.data["new_state"].state
                if notify_q:
                    await notify_q.put(value)
            if var_name == "pyscript.done2":
                value = event.data["new_state"].state
                if notify_q2:
                    await notify_q2.put(value)

        hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed)
async def test_reload(hass, caplog):
    """Test reload."""
    notify_q = asyncio.Queue(0)
    now = dt(2020, 7, 1, 11, 59, 59, 999999)
    source0 = """
seq_num = 0

@time_trigger
def func_startup_sync():
    global seq_num

    seq_num += 1
    log.info(f"func_startup_sync setting pyscript.done = {seq_num}")
    pyscript.done = seq_num

@service
@state_trigger("pyscript.f1var1 == '1'")
def func9(var_name=None, value=None):
    global seq_num

    seq_num += 1
    log.info(f"func9 var = {var_name}, value = {value}")
    pyscript.done = [seq_num, var_name, int(value)]

"""
    source1 = """
seq_num = 10

@time_trigger("startup")
def func_startup_sync():
    global seq_num

    seq_num += 1
    log.info(f"func_startup_sync setting pyscript.done = {seq_num}")
    pyscript.done = seq_num

@service
@state_trigger("pyscript.f5var1 == '1'")
def func5(var_name=None, value=None):
    global seq_num

    seq_num += 1
    log.info(f"func5 var = {var_name}, value = {value}")
    pyscript.done = [seq_num, var_name, int(value)]

"""

    await setup_script(hass, notify_q, now, source0)

    #
    # run and reload 6 times with different source files to make sure seqNum
    # gets reset, autostart of func_startup_sync happens and triggers work each time
    #
    # first time: fire event to startup triggers and run func_startup_sync
    #
    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
    for i in range(6):
        if i & 1:
            seq_num = 10

            assert not hass.services.has_service("pyscript", "func9")
            assert hass.services.has_service("pyscript", "reload")
            assert hass.services.has_service("pyscript", "func5")

            seq_num += 1
            assert literal_eval(await wait_until_done(notify_q)) == seq_num

            seq_num += 1
            # initialize the trigger and active variables
            hass.states.async_set("pyscript.f5var1", 0)

            # try some values that shouldn't work, then one that does
            hass.states.async_set("pyscript.f5var1", "string")
            hass.states.async_set("pyscript.f5var1", 1)
            assert literal_eval(await wait_until_done(notify_q)) == [
                seq_num,
                "pyscript.f5var1",
                1,
            ]
            assert "func5 var = pyscript.f5var1, value = 1" in caplog.text
            next_source = source0

        else:
            seq_num = 0

            assert hass.services.has_service("pyscript", "func9")
            assert hass.services.has_service("pyscript", "reload")
            assert not hass.services.has_service("pyscript", "func5")

            seq_num += 1
            assert literal_eval(await wait_until_done(notify_q)) == seq_num

            seq_num += 1
            # initialize the trigger and active variables
            hass.states.async_set("pyscript.f1var1", 0)

            # try some values that shouldn't work, then one that does
            hass.states.async_set("pyscript.f1var1", "string")
            hass.states.async_set("pyscript.f1var1", 1)
            assert literal_eval(await wait_until_done(notify_q)) == [
                seq_num,
                "pyscript.f1var1",
                1,
            ]
            assert "func9 var = pyscript.f1var1, value = 1" in caplog.text
            next_source = source1

        #
        # now reload the other source file
        #
        scripts = [
            "/hello.py",
        ]

        with patch(
                "custom_components.pyscript.os.path.isdir", return_value=True
        ), patch(
                "custom_components.pyscript.glob.iglob", return_value=scripts
        ), patch("custom_components.pyscript.global_ctx.open",
                 mock_open(read_data=next_source)), patch(
                     "custom_components.pyscript.open",
                     mock_open(read_data=next_source)
                 ), patch(
                     "custom_components.pyscript.trigger.dt_now",
                     return_value=now
                 ), patch(
                     "homeassistant.config.load_yaml_config_file",
                     return_value={}
                 ), patch(
                     "custom_components.pyscript.os.path.getmtime",
                     return_value=1000
                 ), patch(
                     "custom_components.pyscript.global_ctx.os.path.getmtime",
                     return_value=1000), patch(
                         "custom_components.pyscript.install_requirements",
                         return_value=None,
                     ):
            reload_param = {}
            if i % 2 == 1:
                #
                # on alternate times, just reload the specific file we are testing with
                #
                reload_param = {"global_ctx": "file.hello"}
            await hass.services.async_call("pyscript",
                                           "reload",
                                           reload_param,
                                           blocking=True)
            if i % 3 == 0:
                #
                # reload a file that doesn't exist; will log error and do nothing
                #
                await hass.services.async_call(
                    "pyscript",
                    "reload", {"global_ctx": "file.nosuchfile"},
                    blocking=True)

    assert "pyscript.reload: no global context 'file.nosuchfile' to reload" in caplog.text
async def test_service_description(hass):
    """Test service description defined in doc_string."""

    await setup_script(
        hass,
        None,
        dt(2020, 7, 1, 11, 59, 59, 999999),
        """
@service
def func_no_doc_string(param1=None):
    pass

@service
def func_simple_doc_string(param2=None, param3=None):
    \"\"\"This is func2_simple_doc_string.\"\"\"
    pass

@service
def func_yaml_doc_string(param2=None, param3=None):
    \"\"\"yaml
description: This is func_yaml_doc_string.
fields:
  param1:
    description: first argument
    example: 12
  param2:
    description: second argument
    example: 34
\"\"\"
    pass
""",
    )

    integration = loader.Integration(
        hass,
        "custom_components.pyscript",
        pathlib.Path("custom_components/pyscript"),
        {
            "name": "pyscript",
            "dependencies": [],
            "requirements": [],
            "domain": "automation"
        },
    )

    with patch(
            "homeassistant.loader.async_get_custom_components",
            return_value={"pyscript": integration},
    ):
        descriptions = (await async_get_all_descriptions(hass))[DOMAIN]

    assert descriptions["func_no_doc_string"][
        "description"] == "pyscript function func_no_doc_string()"
    assert descriptions["func_no_doc_string"]["fields"] == {
        "param1": {
            "description": "argument param1"
        }
    }

    assert descriptions["func_simple_doc_string"][
        "description"] == "This is func2_simple_doc_string."
    assert descriptions["func_simple_doc_string"]["fields"] == {
        "param2": {
            "description": "argument param2"
        },
        "param3": {
            "description": "argument param3"
        },
    }

    assert descriptions["func_yaml_doc_string"][
        "description"] == "This is func_yaml_doc_string."
    assert descriptions["func_yaml_doc_string"]["fields"] == {
        "param1": {
            "description": "first argument",
            "example": "12"
        },
        "param2": {
            "description": "second argument",
            "example": "34"
        },
    }
Exemple #14
0
async def setup_script(hass, now, source, no_connect=False):
    """Initialize and load the given pyscript."""

    conf_dir = hass.config.path(FOLDER)

    file_contents = {f"{conf_dir}/hello.py": source}

    mock_open = MockOpen()
    for key, value in file_contents.items():
        mock_open[key].read_data = value

    def isfile_side_effect(arg):
        return arg in file_contents

    def glob_side_effect(path, recursive=None):
        result = []
        path_re = path.replace("*", "[^/]*").replace(".", "\\.")
        path_re = path_re.replace("[^/]*[^/]*/", ".*")
        for this_path in file_contents:
            if re.match(path_re, this_path):
                result.append(this_path)
        return result

    with patch(
            "custom_components.pyscript.os.path.isdir", return_value=True
    ), patch("custom_components.pyscript.glob.iglob") as mock_glob, patch(
            "custom_components.pyscript.global_ctx.open", mock_open
    ), patch(
            "custom_components.pyscript.trigger.dt_now", return_value=now
    ), patch("custom_components.pyscript.open", mock_open), patch(
            "homeassistant.config.load_yaml_config_file", return_value={}
    ), patch(
            "custom_components.pyscript.install_requirements",
            return_value=None,
    ), patch("custom_components.pyscript.watchdog_start",
             return_value=None), patch(
                 "custom_components.pyscript.os.path.getmtime",
                 return_value=1000), patch(
                     "custom_components.pyscript.global_ctx.os.path.getmtime",
                     return_value=1000), patch(
                         "custom_components.pyscript.os.path.isfile"
                     ) as mock_isfile:
        mock_isfile.side_effect = isfile_side_effect
        mock_glob.side_effect = glob_side_effect
        assert await async_setup_component(hass, "pyscript", {DOMAIN: {}})

    #
    # I'm not sure how to run the mock all the time, so just force the dt_now()
    # trigger function to return the given list of times in now.
    #
    def return_next_time():
        nonlocal now
        if isinstance(now, list):
            if len(now) > 1:
                return now.pop(0)
            return now[0]
        return now

    trigger.__dict__["dt_now"] = return_next_time

    kernel_state_var = "pyscript.jupyter_ports_1234"
    kernel_cfg = {
        "ip": "127.0.0.1",
        "key": SECRET_KEY.decode("utf-8"),
        "signature_scheme": "hmac-sha256",
        "state_var": kernel_state_var,
    }
    if no_connect:
        kernel_cfg["no_connect_timeout"] = 0.0
    await hass.services.async_call("pyscript", "jupyter_kernel_start",
                                   kernel_cfg)

    while True:
        ports_state = hass.states.get(kernel_state_var)
        if ports_state is not None:
            break
        await asyncio.sleep(2e-3)

    port_nums = json.loads(ports_state.state)

    sock = {}

    if no_connect:
        return sock, port_nums

    for name in PORT_NAMES:
        kernel_reader, kernel_writer = await asyncio.open_connection(
            "127.0.0.1", port_nums[name])
        sock[name] = ZmqSocket(kernel_reader, kernel_writer, "ROUTER")
        await sock[name].handshake()

    return sock, port_nums
Exemple #15
0
def pyscript_bypass_setup_fixture():
    """Mock component setup."""
    with patch("custom_components.pyscript.async_setup_entry",
               return_value=True):
        yield
Exemple #16
0
async def test_service_call(hass):
    """Test calling a service using the entity_id as a property."""
    with patch(
            "custom_components.pyscript.state.async_get_all_descriptions",
            return_value={
                "test": {
                    "test": {
                        "description": None,
                        "fields": {
                            "entity_id": "blah",
                            "other_service_data": "blah"
                        }
                    }
                }
            },
    ), patch.object(hass.states,
                    "get",
                    return_value=HassState("test.entity",
                                           "True")), patch.object(
                                               hass.services,
                                               "async_call") as call:
        State.init(hass)
        await State.get_service_params()

        func = await State.get("test.entity.test")
        await func(context=Context(id="test"),
                   blocking=True,
                   limit=1,
                   other_service_data="test")
        assert call.called
        assert call.call_args[0] == (
            "test",
            "test",
            {
                "other_service_data": "test",
                "entity_id": "test.entity"
            },
        )
        assert call.call_args[1] == {
            "context": Context(id="test"),
            "blocking": True,
            "limit": 1
        }
        call.reset_mock()

        func = await State.get("test.entity.test")
        await func(context=Context(id="test"),
                   blocking=False,
                   other_service_data="test")
        assert call.called
        assert call.call_args[0] == (
            "test",
            "test",
            {
                "other_service_data": "test",
                "entity_id": "test.entity"
            },
        )
        assert call.call_args[1] == {
            "context": Context(id="test"),
            "blocking": False
        }