diff --git a/src/juv/_run_replace.py b/src/juv/_run_replace.py index c38e838..2f87402 100644 --- a/src/juv/_run_replace.py +++ b/src/juv/_run_replace.py @@ -7,15 +7,26 @@ from uv import find_uv_bin +IS_WINDOWS = sys.platform.startswith("win") + def run(script: str, args: list[str]) -> None: - process = subprocess.Popen( # noqa: S603 - [os.fsdecode(find_uv_bin()), *args], - stdin=subprocess.PIPE, - stdout=sys.stdout, - stderr=sys.stderr, - preexec_fn=os.setsid, # noqa: PLW1509 - ) + if not IS_WINDOWS: + process = subprocess.Popen( # noqa: S603 + [os.fsdecode(find_uv_bin()), *args], + stdin=subprocess.PIPE, + stdout=sys.stdout, + stderr=sys.stderr, + preexec_fn=os.setsid, # noqa: PLW1509 + ) + else: + process = subprocess.Popen( # noqa: S603 + [os.fsdecode(find_uv_bin()), *args], + stdin=subprocess.PIPE, + stdout=sys.stdout, + stderr=sys.stderr, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, + ) assert process.stdin is not None # noqa: S101 process.stdin.write(script.encode()) @@ -25,6 +36,9 @@ def run(script: str, args: list[str]) -> None: try: process.wait() except KeyboardInterrupt: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) + if not IS_WINDOWS: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + else: + os.kill(process.pid, signal.SIGTERM) finally: process.wait() diff --git a/src/juv/_run_template.py b/src/juv/_run_template.py index c123774..bd8afe4 100644 --- a/src/juv/_run_template.py +++ b/src/juv/_run_template.py @@ -75,7 +75,16 @@ def as_with_arg(self) -> str: juv_data_dir = Path(user_data_dir("juv")) juv_data_dir.mkdir(parents=True, exist_ok=True) -temp_dir = tempfile.TemporaryDirectory(dir=juv_data_dir) +# Custom TemporaryDirectory for Python < 3.10 +# TODO: Use `ignore_cleanup_errors=True` in Python 3.10+ +class TemporaryDirectoryIgnoreErrors(tempfile.TemporaryDirectory): + def cleanup(self): + try: + super().cleanup() + except Exception: + pass # Ignore cleanup errors + +temp_dir = TemporaryDirectory(dir=juv_data_dir) merged_dir = Path(temp_dir.name) def handle_termination(signum, frame): @@ -129,7 +138,7 @@ def handle_termination(signum, frame): version = importlib.metadata.version("jupyterlab") print("JUV_MANGED=" + "jupyterlab" + "," + version, file=sys.stderr) -sys.argv = ["jupyter-lab", "{notebook}", *{args}] +sys.argv = ["jupyter-lab", r"{notebook}", *{args}] main() """ @@ -148,7 +157,7 @@ def handle_termination(signum, frame): version = importlib.metadata.version("notebook") print("JUV_MANGED=" + "notebook" + "," + version, file=sys.stderr) -sys.argv = ["jupyter-notebook", "{notebook}", *{args}] +sys.argv = ["jupyter-notebook", r"{notebook}", *{args}] main() """ @@ -167,7 +176,7 @@ def handle_termination(signum, frame): version = importlib.metadata.version("notebook") print("JUV_MANGED=" + "notebook" + "," + version, file=sys.stderr) -sys.argv = ["jupyter-notebook", "{notebook}", *{args}] +sys.argv = ["jupyter-notebook", r"{notebook}", *{args}] main() """ @@ -187,7 +196,7 @@ def handle_termination(signum, frame): print("JUV_MANGED=" + "nbclassic" + "," + version, file=sys.stderr) os.environ["JUPYTER_DATA_DIR"] = str(merged_dir) -sys.argv = ["jupyter-nbclassic", "{notebook}", *{args}] +sys.argv = ["jupyter-nbclassic", r"{notebook}", *{args}] main() """ diff --git a/tests/test_juv.py b/tests/test_juv.py index 84a9464..27a47b6 100644 --- a/tests/test_juv.py +++ b/tests/test_juv.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import os import pathlib import re @@ -21,6 +22,14 @@ SELF_DIR = pathlib.Path(__file__).parent +# Custom TemporaryDirectory for Python < 3.10 +# TODO: Use `ignore_cleanup_errors=True` in Python 3.10+ # noqa: TD002, TD003 +class TemporaryDirectoryIgnoreErrors(tempfile.TemporaryDirectory): + def cleanup(self) -> None: + with contextlib.suppress(Exception): + super().cleanup() + + def invoke(args: list[str], uv_python: str = "3.13") -> Result: return CliRunner().invoke( cli, @@ -810,7 +819,7 @@ def test_stamp( ) -> None: # we need to run these tests in this folder because it uses the git history - with tempfile.TemporaryDirectory(dir=SELF_DIR) as tmpdir: + with TemporaryDirectoryIgnoreErrors(dir=SELF_DIR) as tmpdir: tmp_path = pathlib.Path(tmpdir) monkeypatch.chdir(tmp_path) @@ -840,7 +849,7 @@ def test_stamp_script( ) -> None: # we need to run these tests in this folder because it uses the git history - with tempfile.TemporaryDirectory(dir=SELF_DIR) as tmpdir: + with TemporaryDirectoryIgnoreErrors(dir=SELF_DIR) as tmpdir: tmp_path = pathlib.Path(tmpdir) monkeypatch.chdir(tmp_path) @@ -851,11 +860,11 @@ def test_stamp_script( # /// -def main() -> None: │ - print("Hello from foo.py!") │ - │ - │ -if __name__ == "__main__": │ +def main() -> None: | + print("Hello from foo.py!") | + | + | +if __name__ == "__main__": | main() """) result = invoke(["stamp", "foo.py", "--date", "2006-01-02"]) @@ -874,11 +883,11 @@ def main() -> None: # /// -def main() -> None: │ - print("Hello from foo.py!") │ - │ - │ -if __name__ == "__main__": │ +def main() -> None: | + print("Hello from foo.py!") | + | + | +if __name__ == "__main__": | main() """) @@ -889,7 +898,7 @@ def test_stamp_clear( ) -> None: # we need to run these tests in this folder because it uses the git history - with tempfile.TemporaryDirectory(dir=SELF_DIR) as tmpdir: + with TemporaryDirectoryIgnoreErrors(dir=SELF_DIR) as tmpdir: tmp_path = pathlib.Path(tmpdir) monkeypatch.chdir(tmp_path)