diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ab0efb7..f1d4698 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -30,7 +30,7 @@ jobs: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v1" with: - python-version: "3.13" + python-version: "3.14" allow-prereleases: true - run: python3 -m pip install -r tests/requirements_test.txt - run: pytest --cov=custom_components @@ -42,7 +42,7 @@ jobs: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v1" with: - python-version: "3.13" + python-version: "3.14" allow-prereleases: true - run: python3 -m pip install -r tests/requirements_test.txt - run: pylint custom_components/pyscript/*.py tests/*.py @@ -54,7 +54,7 @@ jobs: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v1" with: - python-version: "3.13" + python-version: "3.14" allow-prereleases: true - run: python3 -m pip install -r tests/requirements_test.txt - run: mypy custom_components/pyscript/*.py tests/*.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ccdfd3..bdf8b4d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.12 hooks: - id: ruff-check args: [--fix, --show-fixes] diff --git a/custom_components/pyscript/__init__.py b/custom_components/pyscript/__init__.py index 34a6488..7393854 100644 --- a/custom_components/pyscript/__init__.py +++ b/custom_components/pyscript/__init__.py @@ -6,6 +6,8 @@ import json import logging import os +from os import makedirs as os_makedirs +from os.path import isdir as os_path_isdir import shutil import time import traceback @@ -279,9 +281,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b DecoratorRegistry.init(hass, config_entry) pyscript_folder = hass.config.path(FOLDER) - if not await hass.async_add_executor_job(os.path.isdir, pyscript_folder): + if not await hass.async_add_executor_job(os_path_isdir, pyscript_folder): _LOGGER.debug("Folder %s not found in configuration folder, creating it", FOLDER) - await hass.async_add_executor_job(os.makedirs, pyscript_folder) + await hass.async_add_executor_job(os_makedirs, pyscript_folder) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][CONFIG_ENTRY] = config_entry diff --git a/custom_components/pyscript/decorator_abc.py b/custom_components/pyscript/decorator_abc.py index e978e2c..4775317 100644 --- a/custom_components/pyscript/decorator_abc.py +++ b/custom_components/pyscript/decorator_abc.py @@ -122,7 +122,7 @@ def __init__(self, ast_ctx: AstEval, name: str) -> None: """Initialize the manager.""" self.ast_ctx = ast_ctx self.name = name - self.func_name = name.split(".")[-1] + self.func_name = name.rsplit(".", maxsplit=1)[-1] self.logger = ast_ctx.get_logger() self.status: DecoratorManagerStatus = DecoratorManagerStatus.INIT diff --git a/custom_components/pyscript/eval.py b/custom_components/pyscript/eval.py index 00632b8..07d6310 100644 --- a/custom_components/pyscript/eval.py +++ b/custom_components/pyscript/eval.py @@ -1132,7 +1132,7 @@ async def ast_functiondef(self, arg, async_func=False): func = self.sym_table[arg.name] if dec_name == "pyscript_executor": - if not asyncio.iscoroutinefunction(func): + if not inspect.iscoroutinefunction(func): def executor_wrap_factory(func): async def executor_wrap(*args, **kwargs): @@ -1903,7 +1903,7 @@ async def call_func(self, func, func_name, *args, **kwargs): # await inst.__init__evalfunc_wrap__.call(self, *args, **kwargs) return inst - if asyncio.iscoroutinefunction(func): + if inspect.iscoroutinefunction(func): return await func(*args, **kwargs) if callable(func): if func == time.sleep: # pylint: disable=comparison-with-callable diff --git a/custom_components/pyscript/global_ctx.py b/custom_components/pyscript/global_ctx.py index 2b94204..6470903 100644 --- a/custom_components/pyscript/global_ctx.py +++ b/custom_components/pyscript/global_ctx.py @@ -4,6 +4,7 @@ from collections.abc import Awaitable, Callable import logging import os +from os.path import isfile as os_path_isfile from types import ModuleType from typing import Any, ClassVar @@ -167,7 +168,7 @@ async def module_import(self, module_name: str, import_level: int) -> ModuleType def find_first_file(file_paths: list[list[str]]) -> list[str] | None: for ctx_name, path, rel_path in file_paths: abs_path = os.path.join(pyscript_dir, path) - if os.path.isfile(abs_path): + if os_path_isfile(abs_path): return [ctx_name, abs_path, rel_path] return None diff --git a/custom_components/pyscript/jupyter_kernel.py b/custom_components/pyscript/jupyter_kernel.py index ebb61ad..0c6997d 100644 --- a/custom_components/pyscript/jupyter_kernel.py +++ b/custom_components/pyscript/jupyter_kernel.py @@ -610,7 +610,7 @@ async def control_listen(self, reader, writer): await self.housekeep_q.put(["shutdown"]) except asyncio.CancelledError: raise - except (EOFError, ConnectionResetError): + except EOFError, ConnectionResetError: _LOGGER.debug("control_listen got eof") await self.housekeep_q.put(["unregister", "control", asyncio.current_task()]) control_socket.close() @@ -630,7 +630,7 @@ async def stdin_listen(self, reader, writer): # _LOGGER.debug("stdin_listen received %s", _) except asyncio.CancelledError: raise - except (EOFError, ConnectionResetError): + except EOFError, ConnectionResetError: _LOGGER.debug("stdin_listen got eof") await self.housekeep_q.put(["unregister", "stdin", asyncio.current_task()]) stdin_socket.close() @@ -651,7 +651,7 @@ async def shell_listen(self, reader, writer): except asyncio.CancelledError: shell_socket.close() raise - except (EOFError, ConnectionResetError): + except EOFError, ConnectionResetError: _LOGGER.debug("shell_listen got eof") await self.housekeep_q.put(["unregister", "shell", asyncio.current_task()]) shell_socket.close() @@ -672,7 +672,7 @@ async def heartbeat_listen(self, reader, writer): await heartbeat_socket.send(msg) except asyncio.CancelledError: raise - except (EOFError, ConnectionResetError): + except EOFError, ConnectionResetError: _LOGGER.debug("heartbeat_listen got eof") await self.housekeep_q.put(["unregister", "heartbeat", asyncio.current_task()]) heartbeat_socket.close() @@ -693,7 +693,7 @@ async def iopub_listen(self, reader, writer): # _LOGGER.debug("iopub received %s", _) except asyncio.CancelledError: raise - except (EOFError, ConnectionResetError): + except EOFError, ConnectionResetError: await self.housekeep_q.put(["unregister", "iopub", asyncio.current_task()]) iopub_socket.close() self.iopub_socket.discard(iopub_socket) diff --git a/custom_components/pyscript/state.py b/custom_components/pyscript/state.py index 68bfd3e..9b9e943 100644 --- a/custom_components/pyscript/state.py +++ b/custom_components/pyscript/state.py @@ -9,12 +9,14 @@ from homeassistant.core import Context, HomeAssistant, State as CoreState from homeassistant.helpers.restore_state import DATA_RESTORE_STATE from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers.template import ( +from homeassistant.helpers.template.extensions.math import _SENTINEL as MATH_SENTINEL, MathExtension +from homeassistant.helpers.template.extensions.type_cast import ( + _SENTINEL as TYPECAST_SENTINEL, + TypeCastExtension, +) +from homeassistant.helpers.template.helpers import ( _SENTINEL, forgiving_boolean, - forgiving_float, - forgiving_int, - forgiving_round, raise_no_default, ) from homeassistant.util import dt as dt_util @@ -41,27 +43,27 @@ def __new__(cls, state: CoreState) -> Self: new_var.last_reported = state.last_reported return new_var - def as_float(self, default: float = _SENTINEL) -> float: + def as_float(self, default: float = TYPECAST_SENTINEL) -> float: """Return the state converted to float via the forgiving helper.""" - return forgiving_float(self, default=default) + return TypeCastExtension.forgiving_float(self, default=default) - def as_int(self, default: int = _SENTINEL, base: int = 10) -> int: + def as_int(self, default: int = TYPECAST_SENTINEL, base: int = 10) -> int: """Return the state converted to int via the forgiving helper.""" - return forgiving_int(self, default=default, base=base) + return TypeCastExtension.forgiving_int(self, default=default, base=base) def as_bool(self, default: bool = _SENTINEL) -> bool: """Return the state converted to bool via the forgiving helper.""" return forgiving_boolean(self, default=default) - def as_round(self, precision: int = 0, method: str = "common", default: float = _SENTINEL) -> float: + def as_round(self, precision: int = 0, method: str = "common", default: float = MATH_SENTINEL) -> float: """Return the rounded state value via the forgiving helper.""" - return forgiving_round(self, precision=precision, method=method, default=default) + return MathExtension.forgiving_round(self, precision=precision, method=method, default=default) def as_datetime(self, default: datetime = _SENTINEL) -> datetime: """Return the state converted to a datetime, matching the forgiving template behaviour.""" try: return dt_util.parse_datetime(self, raise_on_error=True) - except (ValueError, TypeError): + except ValueError, TypeError: if default is _SENTINEL: raise_no_default("as_datetime", self) return default diff --git a/custom_components/pyscript/trigger.py b/custom_components/pyscript/trigger.py index 598109f..bf04377 100644 --- a/custom_components/pyscript/trigger.py +++ b/custom_components/pyscript/trigger.py @@ -3,6 +3,7 @@ import asyncio import datetime as dt import functools +import inspect import locale import logging import math @@ -598,7 +599,7 @@ async def wait_until( @classmethod async def user_task_executor(cls, func, *args, **kwargs): """Implement task.executor().""" - if asyncio.iscoroutinefunction(func) or not callable(func): + if inspect.iscoroutinefunction(func) or not callable(func): raise TypeError(f"function {func} is not callable by task.executor") if isinstance(func, EvalFuncVar): raise TypeError( diff --git a/hacs.json b/hacs.json index 3c9d997..7d9b2fa 100644 --- a/hacs.json +++ b/hacs.json @@ -3,5 +3,6 @@ "content_in_root": false, "domains": ["automation", "script", "timer"], "zip_release": true, - "filename": "hass-custom-pyscript.zip" + "filename": "hass-custom-pyscript.zip", + "homeassistant": "2026.5.0" } diff --git a/pyproject.toml b/pyproject.toml index 9b1eff1..6d935e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,5 +83,5 @@ ignore = [ [dependency-groups] dev = [ - "ruff<0.15.0", -] + "ruff==0.15.12", +] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 30a174b..9069827 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,7 @@ addopts = --asyncio-mode=auto [mypy] -python_version = 3.13 +python_version = 3.14 ignore_errors = true follow_imports = silent ignore_missing_imports = true diff --git a/tests/requirements_test.txt b/tests/requirements_test.txt index 6f09c22..5089b25 100644 --- a/tests/requirements_test.txt +++ b/tests/requirements_test.txt @@ -1,12 +1,12 @@ -coverage==7.10.6 +coverage==7.13.5 croniter==6.0.0 watchdog==6.0.0 mock-open==1.4.0 mypy==1.10.1 -pycares<5.0.0 pre-commit==3.7.1 -pytest==9.0.0 -pytest-cov==7.0.0 -pytest-homeassistant-custom-component==0.13.299 +pytest==9.0.3 +pytest-cov==7.1.0 +pytest-homeassistant-custom-component==0.13.326 +ruff==0.15.12 pylint==3.3.4 pylint-strict-informational==0.1 diff --git a/tests/test_apps_modules.py b/tests/test_apps_modules.py index f02761a..2ccf524 100644 --- a/tests/test_apps_modules.py +++ b/tests/test_apps_modules.py @@ -170,7 +170,7 @@ def glob_side_effect(path, recursive=None, root_dir=None, dir_fd=None, include_h 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, + patch("custom_components.pyscript.global_ctx.os_path_isfile") as mock_isfile, ): mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect diff --git a/tests/test_function.py b/tests/test_function.py index e8d2058..f87fbe1 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -143,7 +143,7 @@ def glob_side_effect(path, recursive=None, root_dir=None, dir_fd=None, include_h 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, + patch("custom_components.pyscript.global_ctx.os_path_isfile") as mock_isfile, ): mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect diff --git a/tests/test_init.py b/tests/test_init.py index 8c46232..47995dd 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -29,7 +29,7 @@ async def setup_script(hass, notify_q, now, source, script_name="/hello.py"): Function.hass = None with ( - patch("custom_components.pyscript.os.path.isdir", return_value=True), + 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)), patch("custom_components.pyscript.trigger.dt_now", return_value=now), @@ -74,8 +74,8 @@ async def wait_until_done(notify_q): async def test_setup_makedirs_on_no_dir(hass, caplog): """Test setup calls os.makedirs when no dir found.""" with ( - patch("custom_components.pyscript.os.path.isdir", return_value=False), - patch("custom_components.pyscript.os.makedirs"), + patch("custom_components.pyscript.os_path_isdir", return_value=False), + patch("custom_components.pyscript.os_makedirs"), patch("custom_components.pyscript.watchdog_start", return_value=None) as makedirs_call, patch("homeassistant.config.load_yaml_config_file", return_value={}), ): @@ -463,7 +463,7 @@ def func5(var_name=None, value=None): ] with ( - patch("custom_components.pyscript.os.path.isdir", return_value=True), + 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)), diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index d6284b0..65fef78 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -145,7 +145,7 @@ def glob_side_effect(path, recursive=None, root_dir=None, dir_fd=None, include_h 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, + patch("custom_components.pyscript.global_ctx.os_path_isfile") as mock_isfile, ): mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect diff --git a/tests/test_reload.py b/tests/test_reload.py index 67f5466..3d171d0 100644 --- a/tests/test_reload.py +++ b/tests/test_reload.py @@ -143,7 +143,7 @@ def glob_side_effect(path, recursive=None, root_dir=None, dir_fd=None, include_h 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, + patch("custom_components.pyscript.global_ctx.os_path_isfile") as mock_isfile, ): mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 51ed29a..40f939b 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -145,7 +145,7 @@ def glob_side_effect(path, recursive=None, root_dir=None, dir_fd=None, include_h 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, + patch("custom_components.pyscript.global_ctx.os_path_isfile") as mock_isfile, ): mock_isfile.side_effect = isfile_side_effect mock_glob.side_effect = glob_side_effect