From c0733dcc73a90f149a863a79545551a72c3fcd8f Mon Sep 17 00:00:00 2001 From: SeaStar Deng <37767638+DSeaStar@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:03:40 +0000 Subject: [PATCH 1/2] BUG: assign altsep normalization in the editable path hook str.replace() returns a new string. The Windows path hook discarded that result, so rpartition(os.sep) never split altsep paths and the hook failed to match __file__. Fixes #868. --- mesonpy/_editable.py | 2 +- tests/test_editable.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/mesonpy/_editable.py b/mesonpy/_editable.py index 7568d069..5e742d11 100644 --- a/mesonpy/_editable.py +++ b/mesonpy/_editable.py @@ -349,7 +349,7 @@ def _rebuild(self) -> Node: def _path_hook(self, path: str) -> MesonpyPathFinder: if os.altsep: - path.replace(os.altsep, os.sep) + path = path.replace(os.altsep, os.sep) path, _, key = path.rpartition(os.sep) if path == __file__: tree = self._rebuild() diff --git a/tests/test_editable.py b/tests/test_editable.py index b8a14695..b7298844 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -349,3 +349,34 @@ def test_editable_rebuild_error(package_purelib_and_platlib, tmp_path, verbose): def test_install_data(venv, editable_install_data, tmp_path): venv.pip('install', os.fspath(editable_install_data)) venv.python('-c', 'import package') + + +def test_path_hook_assigns_altsep_normalization(monkeypatch): + """The Windows path hook must assign the result of str.replace. + + Import machinery can pass paths that use os.altsep. Without assigning + the replace, rpartition(os.sep) does not split the path and the hook + never matches __file__. + """ + monkeypatch.setattr(os, 'sep', '\\') + monkeypatch.setattr(os, 'altsep', '/') + + win_file = _editable.__file__.replace('/', '\\') + monkeypatch.setattr(_editable, '__file__', win_file) + + finder = _editable.MesonpyMetaFinder('pkg', {'pkg'}, '/build', ['true']) + rebuilt = [] + + def fake_rebuild(): + rebuilt.append(True) + tree = _editable.Node() + tree[('subpkg',)] = _editable.Node() + return tree + + finder._rebuild = fake_rebuild # type: ignore[method-assign] + + hook_path = win_file.replace('\\', '/') + '/subpkg' + result = finder._path_hook(hook_path) + assert rebuilt + assert isinstance(result, _editable.MesonpyPathFinder) + From 6de8d4074a6edd42664775b7edee8c4a352aeb03 Mon Sep 17 00:00:00 2001 From: SeaStar Deng <37767638+DSeaStar@users.noreply.github.com> Date: Sat, 15 Aug 2026 00:09:31 +0000 Subject: [PATCH 2/2] TST: exercise the editable path hook with real Windows altsep paths Replace the synthetic sep/__file__ monkeypatch with a Windows-only test that builds package_complex and uses pkgutil.get_importer on a forward-slash search path derived from the real __file__. --- tests/test_editable.py | 57 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/tests/test_editable.py b/tests/test_editable.py index b7298844..9282c18f 100644 --- a/tests/test_editable.py +++ b/tests/test_editable.py @@ -351,32 +351,43 @@ def test_install_data(venv, editable_install_data, tmp_path): venv.python('-c', 'import package') -def test_path_hook_assigns_altsep_normalization(monkeypatch): - """The Windows path hook must assign the result of str.replace. - - Import machinery can pass paths that use os.altsep. Without assigning - the replace, rpartition(os.sep) does not split the path and the hook - never matches __file__. +@pytest.mark.skipif(not os.altsep, reason='os.altsep is only set on Windows') +@pytest.mark.skipif(FREE_THREADED_BUILD and CYTHON_VERSION < (3, 1, 0), + reason='Cython version too old, no free-threaded CPython support') +def test_path_hook_altsep_search_path(package_complex, tmp_path): + """The path hook must match submodule search paths that use os.altsep. + + After a real editable build, importlib/pkgutil look up subpackages via + path hooks. On Windows those paths may use '/' (PYTHONPATH, pathlib, + Meson) even though __file__ uses '\\'. The hook normalizes altsep + before rpartition(os.sep); discarding the replace means the hook + never matches __file__ and subpackage discovery fails. """ - monkeypatch.setattr(os, 'sep', '\\') - monkeypatch.setattr(os, 'altsep', '/') - - win_file = _editable.__file__.replace('/', '\\') - monkeypatch.setattr(_editable, '__file__', win_file) + if os.altsep in _editable.__file__: + pytest.skip('__file__ already uses altsep; hook compares to it as-is') - finder = _editable.MesonpyMetaFinder('pkg', {'pkg'}, '/build', ['true']) - rebuilt = [] + project = mesonpy.Project(package_complex, tmp_path) + finder = _editable.MesonpyMetaFinder( + 'complex', {'complex'}, os.fspath(tmp_path), project._build_command, True + ) - def fake_rebuild(): - rebuilt.append(True) - tree = _editable.Node() - tree[('subpkg',)] = _editable.Node() - return tree + try: + sys.meta_path.insert(0, finder) + sys.path_hooks.insert(0, finder._path_hook) - finder._rebuild = fake_rebuild # type: ignore[method-assign] + import complex + assert complex.__name__ == 'complex' - hook_path = win_file.replace('\\', '/') + '/subpkg' - result = finder._path_hook(hook_path) - assert rebuilt - assert isinstance(result, _editable.MesonpyPathFinder) + # Same location importlib would use, but spelled with os.altsep. + altsep_path = _editable.__file__.replace(os.sep, os.altsep) + os.altsep + 'complex' + importer = pkgutil.get_importer(altsep_path) + assert isinstance(importer, _editable.MesonpyPathFinder) + names = {module.name for module in pkgutil.iter_modules([altsep_path])} + assert 'more' in names + finally: + del sys.meta_path[0] + del sys.path_hooks[0] + for name in list(sys.modules): + if name == 'complex' or name.startswith('complex.'): + del sys.modules[name]