Skip to content

Commit

Permalink
Add pueblo.util.proc.process utility
Browse files Browse the repository at this point in the history
It is a wrapper around `subprocess.Popen` to also terminate child
processes after exiting.
  • Loading branch information
amotl committed Dec 5, 2023
1 parent 692a135 commit a7379fe
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
for contemporary versions of Java
- Add support for Python 3.7
- Add `testbook` to `notebook` subsystem
- Add `pueblo.util.proc.process` utility. It is a wrapper around
`subprocess.Popen` to also terminate child processes after exiting.

## 2023-11-06 v0.0.3
- ngr: Fix `contextlib.chdir` only available on Python 3.11 and newer
Expand Down
26 changes: 26 additions & 0 deletions pueblo/util/proc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import subprocess
import typing as t
from contextlib import contextmanager

import psutil


@contextmanager
def process(*args, **kwargs) -> t.Generator[subprocess.Popen, None, None]:
"""
Wrapper around `subprocess.Popen` to also terminate child processes after exiting.
Implementation by Pedro Cattori. Thanks!
-- https://gist.github.com/jizhilong/6687481#gistcomment-3057122
"""
proc = subprocess.Popen(*args, **kwargs) # noqa: S603
try:
yield proc
finally:
try:
children = psutil.Process(proc.pid).children(recursive=True)
except psutil.NoSuchProcess:
return
for child in children:
child.kill()
proc.kill()
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dependencies = [

[project.optional-dependencies]
all = [
"pueblo[cli,dataframe,fileio,nlp,notebook,testing,web]",
"pueblo[cli,dataframe,fileio,nlp,notebook,proc,testing,web]",
]
cli = [
"click<9",
Expand Down Expand Up @@ -109,6 +109,9 @@ notebook = [
"pytest-notebook<0.11",
"testbook<0.5",
]
proc = [
"psutil<6",
]
release = [
"build<2",
"twine<5",
Expand Down
32 changes: 32 additions & 0 deletions tests/test_proc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest

from pueblo.util.proc import process


def test_process_success(tmp_path):
outfile = tmp_path / "myfile.out.log"
with process(["echo", "Hello, world."], stdout=open(outfile, "w")) as proc:
assert isinstance(proc.pid, int)

with open(outfile, "r") as fp:
assert fp.read() == "Hello, world.\n"


def test_process_noop():
process(["mycommand", "myarg", "--myoption", "myoptionvalue"])


def test_process_failure_command_not_found():
with pytest.raises(FileNotFoundError) as ex:
with process(["mycommand", "myarg", "--myoption", "myoptionvalue"]):
pass
assert ex.match("No such file or directory")


def test_process_failure_in_contextmanager():
with pytest.raises(ZeroDivisionError):
with process(["echo", "Hello, world."]) as proc:
print(proc.pid) # noqa: T201
# Even though this throws an exception, the `process` contextmanager
# will *still* clean up the process correctly.
0 / 0 # noqa: B018

0 comments on commit a7379fe

Please sign in to comment.