Skip to content

Fix Windows script path normalization - #874

Merged
craigbarratt merged 5 commits into
custom-components:masterfrom
dajiaohuang:fix-windows-script-paths
Nov 7, 2026
Merged

craigbarratt merged 5 commits into
custom-components:masterfrom
dajiaohuang:fix-windows-script-paths

Conversation

@dajiaohuang

@dajiaohuang dajiaohuang commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

On Windows, load_scripts previously produced backslashes in module names. Normalize relative paths to slash separators for file, apps, modules, and scripts contexts.

Follow-up f42d07d rebases onto current master, replaces process-wide path monkeypatching with an injected path module, adds a nested Windows case and a loader-context regression, skips paths when relpath raises ValueError, and matches requirements-file events by basename. On Linux with Python 3.14, tests/test_init.py passed all 14 tests; Ruff check and format and git diff --check passed. The full pytest run had 182 passes and one failure: test_time_active_hold_off_send_last returned the earlier event instead of the last one. The same test failed identically on unmodified current base in this environment. Windows-style paths are simulated with ntpath; no native Windows Home Assistant integration run is claimed. Earlier pylint and mypy results apply to the prior head only.

@craigbarratt craigbarratt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this! The change to os.path.relpath(...).replace(os.sep, "/") looks right: it gives the same result as before on POSIX, and on Windows it fixes the package/module name matching in glob_read_files. Most of my inline comments are about the test, which currently doesn't exercise the separator replacement.

A related Windows issue you could fix here or in a follow-up: the watchdog handler matches requirements files with "/" + REQUIREMENTS_FILE (around line 223 of __init__.py). That never matches a backslash path on Windows, so editing pyscript\requirements.txt doesn't trigger a reload. Comparing os.path.basename(path) == REQUIREMENTS_FILE would work on any platform.

Also, this now has a merge conflict in tests/test_init.py (master added a restore_state test and changed the imports nearby), so it will need a rebase onto current master.

Comment thread custom_components/pyscript/__init__.py Outdated

def _relative_script_path(path: str, root: str) -> str:
"""Return a slash-separated path relative to the pyscript root."""
return os.path.relpath(path, root).replace(os.sep, "/")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small difference from the old prefix strip: on Windows, os.path.relpath raises ValueError if the two paths are on different drives (e.g. a mapped drive vs. a UNC path for the config dir). Nothing catches it here, so it would abort load_scripts entirely, whereas the old code just produced an odd name. This is probably rare given both paths come from hass.config.path, but it may be worth a try/except ValueError that logs and skips the file.

Comment thread custom_components/pyscript/__init__.py Outdated
rel_path = rel_path[len(pyscript_dir) :]
if rel_path.startswith("/"):
rel_path = rel_path[1:]
rel_path = _relative_script_path(this_path, pyscript_dir)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional suggestion: the code below still assumes / in several places (find("/#"), endswith("/__init__")), and so does global_ctx.module_import (f"{path}/{module_path}.py", path.find("/")). Normalizing once where the paths come from (e.g. pathlib.PurePath(this_path).relative_to(pyscript_dir).as_posix()) would keep every later use portable, so new code doesn't have to remember to normalize. What you have is fine for this fix, though.

Comment thread tests/test_init.py Outdated
return await asyncio.wait_for(notify_q.get(), timeout=4)


def test_relative_script_path_normalizes_windows_paths(monkeypatch):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only covers the helper. It would be good to also test glob_read_files with Windows-style paths, so that the resulting context names (file.X, apps.X, the /__init__ stripping, skipping # paths) are checked too. Otherwise a later change to how rel_path is used could bring back the Windows bug without any test failing.

Comment thread tests/test_init.py Outdated
Comment on lines +77 to +78
monkeypatch.setattr(pyscript_component.os.path, "relpath", ntpath.relpath)
monkeypatch.setattr(pyscript_component.os, "sep", "\\")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These patch os.path.relpath and os.sep for the whole process (they're the shared os/posixpath modules), so any other code running during the test, such as leftover executor threads from earlier hass fixtures, would see Windows behavior. Testing ntpath.relpath(...).replace(ntpath.sep, "/") directly, or having the helper take the path module as a parameter, would avoid changing global state.

Comment thread tests/test_init.py Outdated

assert (
pyscript_component._relative_script_path( # pylint: disable=protected-access
r"C:\config\pyscript\hello.py", r"C:\config\pyscript"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case passes even without the .replace(os.sep, "/"), because a top-level file has no separator in its relative path. Could you add a nested case that actually exercises the fix, e.g.

r"C:\config\pyscript\apps\foo\__init__.py"  ->  "apps/foo/__init__.py"

That way the test fails if the separator replacement is ever dropped.

@dajiaohuang
dajiaohuang force-pushed the fix-windows-script-paths branch from 147458b to 06dc3e4 Compare November 7, 2026 04:26
@craigbarratt
craigbarratt merged commit 32d73d7 into custom-components:master Nov 7, 2026
6 checks passed
@craigbarratt

Copy link
Copy Markdown
Member

Thanks for the PR!

craigbarratt added a commit that referenced this pull request Nov 7, 2026
Since #874 relative script paths come from os.path.relpath, so the
mocked "/hello.py" (outside the pyscript folder) loaded under a
garbled name. Use a path under hass.config.path("pyscript"), as
test_init.py does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants