From 9bad50449d39de65b3f2bd510540395ff00f1c9a Mon Sep 17 00:00:00 2001 From: Michael Hidalgo Date: Mon, 6 Oct 2025 12:18:53 +0100 Subject: [PATCH 1/4] Quoting paths on Windows --- jupyter_core/command.py | 4 ++- tests/test_command.py | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/jupyter_core/command.py b/jupyter_core/command.py index 9c9317d..9418fff 100644 --- a/jupyter_core/command.py +++ b/jupyter_core/command.py @@ -123,7 +123,9 @@ def _execvp(cmd: str, argv: list[str]) -> None: if cmd_path is None: msg = f"{cmd!r} not found" raise OSError(msg, errno.ENOENT) - p = Popen([cmd_path] + argv[1:]) # noqa: S603 + # Quoting path in Windows + cmd_line = f'"{cmd_path}"' + "".join(f' "{arg}"' for arg in argv[1:]) + p = Popen(cmd_line) # noqa: S603 # Don't raise KeyboardInterrupt in the parent process. # Set this after spawning, to avoid subprocess inheriting handler. import signal diff --git a/tests/test_command.py b/tests/test_command.py index 30066ed..767b705 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -254,3 +254,65 @@ def test_argv0(tmpdir): def test_version(): assert isinstance(__version__, str) + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +def test_execvp_quotes_path_with_parentheses(): + """Test that _execvp properly quotes paths with parentheses on Windows""" + from jupyter_core.command import _execvp + + # Test path: C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe + test_path = r"C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe" + + with patch("jupyter_core.command.which", return_value=test_path), patch( + "jupyter_core.command.Popen" + ) as mock_popen, patch("jupyter_core.command.signal"), patch.object(sys, "exit"): + mock_process = mock_popen.return_value + mock_process.returncode = 0 + + _execvp("python", ["python"]) + + called_cmd = mock_popen.call_args[0][0] + expected = f'"{test_path}"' + assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +def test_execvp_quotes_simple_path(): + """Test that _execvp properly quotes simple paths on Windows""" + from jupyter_core.command import _execvp + + # Test path: C:\Users\JohnDoe\python.exe + test_path = r"C:\Users\JohnDoe\python.exe" + + with patch("jupyter_core.command.which", return_value=test_path), patch( + "jupyter_core.command.Popen" + ) as mock_popen, patch("jupyter_core.command.signal"), patch.object(sys, "exit"): + mock_process = mock_popen.return_value + mock_process.returncode = 0 + + _execvp("python", ["python"]) + + called_cmd = mock_popen.call_args[0][0] + expected = f'"{test_path}"' + assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +def test_execvp_quotes_path_and_arguments(): + """Test that _execvp quotes both path and arguments with spaces on Windows""" + from jupyter_core.command import _execvp + + test_path = r"C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe" + + with patch("jupyter_core.command.which", return_value=test_path), patch( + "jupyter_core.command.Popen" + ) as mock_popen, patch("jupyter_core.command.signal"), patch.object(sys, "exit"): + mock_process = mock_popen.return_value + mock_process.returncode = 0 + + _execvp("python", ["python", "--config", "my config.py", "--flag"]) + + called_cmd = mock_popen.call_args[0][0] + expected = f'"{test_path}" "--config" "my config.py" "--flag"' + assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" From cf95c623fd2e905e63162dec40a670e7037fcbb8 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 6 Oct 2025 14:24:21 +0000 Subject: [PATCH 2/4] Enhance tests cases for windows paths --- tests/test_command.py | 77 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/tests/test_command.py b/tests/test_command.py index 767b705..d85f017 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -263,36 +263,46 @@ def test_execvp_quotes_path_with_parentheses(): # Test path: C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe test_path = r"C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe" + test_cmd = "python" - with patch("jupyter_core.command.which", return_value=test_path), patch( + with patch("sys.platform", "win32"), patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( "jupyter_core.command.Popen" - ) as mock_popen, patch("jupyter_core.command.signal"), patch.object(sys, "exit"): + ) as mock_popen, patch("signal.signal"), patch.object(sys, "exit"): mock_process = mock_popen.return_value mock_process.returncode = 0 - _execvp("python", ["python"]) + _execvp(test_cmd, [test_cmd]) + # Verify which() was called with the correct command + mock_which.assert_called_once_with(test_cmd) + + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}"' assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" -@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") def test_execvp_quotes_simple_path(): """Test that _execvp properly quotes simple paths on Windows""" from jupyter_core.command import _execvp # Test path: C:\Users\JohnDoe\python.exe test_path = r"C:\Users\JohnDoe\python.exe" + test_cmd = "python" - with patch("jupyter_core.command.which", return_value=test_path), patch( + with patch("sys.platform", "win32"), patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( "jupyter_core.command.Popen" - ) as mock_popen, patch("jupyter_core.command.signal"), patch.object(sys, "exit"): + ) as mock_popen, patch("signal.signal"), patch.object(sys, "exit"): mock_process = mock_popen.return_value mock_process.returncode = 0 - _execvp("python", ["python"]) + _execvp(test_cmd, [test_cmd]) + # Verify which() was called with the correct command + mock_which.assert_called_once_with(test_cmd) + + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}"' assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" @@ -304,15 +314,64 @@ def test_execvp_quotes_path_and_arguments(): from jupyter_core.command import _execvp test_path = r"C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe" + test_cmd = "python" + test_args = [test_cmd, "--config", "my config.py", "--flag"] - with patch("jupyter_core.command.which", return_value=test_path), patch( + with patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( "jupyter_core.command.Popen" ) as mock_popen, patch("jupyter_core.command.signal"), patch.object(sys, "exit"): mock_process = mock_popen.return_value mock_process.returncode = 0 - _execvp("python", ["python", "--config", "my config.py", "--flag"]) + _execvp(test_cmd, test_args) + # Verify which() was called with the correct command + mock_which.assert_called_once_with(test_cmd) + + # Verify Popen was called with properly quoted command and arguments called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}" "--config" "my config.py" "--flag"' assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +def test_execvp_command_not_found(): + """Test that _execvp raises OSError when command is not found on Windows""" + from jupyter_core.command import _execvp + + test_cmd = "nonexistent-command" + + with patch("jupyter_core.command.which", return_value=None): + with pytest.raises(OSError) as exc_info: + _execvp(test_cmd, [test_cmd]) + + assert "not found" in str(exc_info.value) + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +def test_execvp_different_commands(): + """Test that _execvp works with different command names on Windows""" + from jupyter_core.command import _execvp + + test_cases = [ + ("jupyter-lab", r"C:\Program Files\Python\Scripts\jupyter-lab.exe"), + ("jupyter-notebook", r"C:\Users\test\jupyter-notebook.exe"), + ("custom-cmd", r"C:\tools\custom-cmd.exe"), + ] + + for test_cmd, test_path in test_cases: + with patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( + "jupyter_core.command.Popen" + ) as mock_popen, patch("jupyter_core.command.signal"), patch.object(sys, "exit"): + mock_process = mock_popen.return_value + mock_process.returncode = 0 + + _execvp(test_cmd, [test_cmd, "--help"]) + + # Verify which() was called with the correct command + mock_which.assert_called_once_with(test_cmd) + + # Verify Popen was called with properly quoted command + called_cmd = mock_popen.call_args[0][0] + expected = f'"{test_path}" "--help"' + assert called_cmd == expected, f"For cmd {test_cmd}: Expected: {expected!r}, Got: {called_cmd!r}" From 209fa8bba5d7921bf8972842a3bae3e879b469d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:33:53 +0000 Subject: [PATCH 3/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_command.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/test_command.py b/tests/test_command.py index d85f017..449c9c6 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -265,9 +265,11 @@ def test_execvp_quotes_path_with_parentheses(): test_path = r"C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe" test_cmd = "python" - with patch("sys.platform", "win32"), patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( - "jupyter_core.command.Popen" - ) as mock_popen, patch("signal.signal"), patch.object(sys, "exit"): + with patch("sys.platform", "win32"), patch( + "jupyter_core.command.which", return_value=test_path + ) as mock_which, patch("jupyter_core.command.Popen") as mock_popen, patch( + "signal.signal" + ), patch.object(sys, "exit"): mock_process = mock_popen.return_value mock_process.returncode = 0 @@ -275,14 +277,14 @@ def test_execvp_quotes_path_with_parentheses(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}"' assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" -@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") def test_execvp_quotes_simple_path(): """Test that _execvp properly quotes simple paths on Windows""" from jupyter_core.command import _execvp @@ -291,9 +293,11 @@ def test_execvp_quotes_simple_path(): test_path = r"C:\Users\JohnDoe\python.exe" test_cmd = "python" - with patch("sys.platform", "win32"), patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( - "jupyter_core.command.Popen" - ) as mock_popen, patch("signal.signal"), patch.object(sys, "exit"): + with patch("sys.platform", "win32"), patch( + "jupyter_core.command.which", return_value=test_path + ) as mock_which, patch("jupyter_core.command.Popen") as mock_popen, patch( + "signal.signal" + ), patch.object(sys, "exit"): mock_process = mock_popen.return_value mock_process.returncode = 0 @@ -301,7 +305,7 @@ def test_execvp_quotes_simple_path(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}"' @@ -327,7 +331,7 @@ def test_execvp_quotes_path_and_arguments(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command and arguments called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}" "--config" "my config.py" "--flag"' @@ -344,7 +348,7 @@ def test_execvp_command_not_found(): with patch("jupyter_core.command.which", return_value=None): with pytest.raises(OSError) as exc_info: _execvp(test_cmd, [test_cmd]) - + assert "not found" in str(exc_info.value) @@ -370,8 +374,10 @@ def test_execvp_different_commands(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}" "--help"' - assert called_cmd == expected, f"For cmd {test_cmd}: Expected: {expected!r}, Got: {called_cmd!r}" + assert called_cmd == expected, ( + f"For cmd {test_cmd}: Expected: {expected!r}, Got: {called_cmd!r}" + ) From 0f8b4962877a777390231ffda91c4ea8c80d72c2 Mon Sep 17 00:00:00 2001 From: Michael Hidalgo Date: Mon, 6 Oct 2025 15:45:49 +0100 Subject: [PATCH 4/4] Fixing tests and formatting the file --- tests/test_command.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/tests/test_command.py b/tests/test_command.py index d85f017..2f02473 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -265,9 +265,11 @@ def test_execvp_quotes_path_with_parentheses(): test_path = r"C:\Users\JohnDoe(TEST)\AppData\Local\env\tools\python.exe" test_cmd = "python" - with patch("sys.platform", "win32"), patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( - "jupyter_core.command.Popen" - ) as mock_popen, patch("signal.signal"), patch.object(sys, "exit"): + with patch("sys.platform", "win32"), patch( + "jupyter_core.command.which", return_value=test_path + ) as mock_which, patch("jupyter_core.command.Popen") as mock_popen, patch( + "signal.signal" + ), patch.object(sys, "exit"): mock_process = mock_popen.return_value mock_process.returncode = 0 @@ -275,14 +277,14 @@ def test_execvp_quotes_path_with_parentheses(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}"' assert called_cmd == expected, f"Expected: {expected!r}, Got: {called_cmd!r}" -@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") def test_execvp_quotes_simple_path(): """Test that _execvp properly quotes simple paths on Windows""" from jupyter_core.command import _execvp @@ -291,9 +293,11 @@ def test_execvp_quotes_simple_path(): test_path = r"C:\Users\JohnDoe\python.exe" test_cmd = "python" - with patch("sys.platform", "win32"), patch("jupyter_core.command.which", return_value=test_path) as mock_which, patch( - "jupyter_core.command.Popen" - ) as mock_popen, patch("signal.signal"), patch.object(sys, "exit"): + with patch("sys.platform", "win32"), patch( + "jupyter_core.command.which", return_value=test_path + ) as mock_which, patch("jupyter_core.command.Popen") as mock_popen, patch( + "signal.signal" + ), patch.object(sys, "exit"): mock_process = mock_popen.return_value mock_process.returncode = 0 @@ -301,7 +305,7 @@ def test_execvp_quotes_simple_path(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}"' @@ -327,7 +331,7 @@ def test_execvp_quotes_path_and_arguments(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command and arguments called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}" "--config" "my config.py" "--flag"' @@ -341,11 +345,10 @@ def test_execvp_command_not_found(): test_cmd = "nonexistent-command" - with patch("jupyter_core.command.which", return_value=None): - with pytest.raises(OSError) as exc_info: - _execvp(test_cmd, [test_cmd]) - - assert "not found" in str(exc_info.value) + with patch("jupyter_core.command.which", return_value=None), pytest.raises( + OSError, match="not found" + ): + _execvp(test_cmd, [test_cmd]) @pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test") @@ -370,8 +373,10 @@ def test_execvp_different_commands(): # Verify which() was called with the correct command mock_which.assert_called_once_with(test_cmd) - + # Verify Popen was called with properly quoted command called_cmd = mock_popen.call_args[0][0] expected = f'"{test_path}" "--help"' - assert called_cmd == expected, f"For cmd {test_cmd}: Expected: {expected!r}, Got: {called_cmd!r}" + assert called_cmd == expected, ( + f"For cmd {test_cmd}: Expected: {expected!r}, Got: {called_cmd!r}" + )