bounded_subprocess 2.4.0__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bounded_subprocess-2.6.0/.claude/settings.local.json +8 -0
- bounded_subprocess-2.6.0/AGENTS.md +10 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/PKG-INFO +1 -1
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/pyproject.toml +1 -1
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/bounded_subprocess_async.py +31 -2
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/test_async.py +74 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/uv.lock +2 -2
- bounded_subprocess-2.4.0/.git +0 -1
- bounded_subprocess-2.4.0/AGENTS.md +0 -5
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/.github/workflows/docs.yaml +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/.github/workflows/test.yml +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/.gitignore +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/LICENSE.txt +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/Makefile +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/README.md +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/cspell.config.yaml +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/docs/index.md +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/mkdocs.yaml +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/__init__.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/bounded_subprocess.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/interactive.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/interactive_async.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/util.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/__init__.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/block_on_inputs.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/close_outputs.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/dies_shortly_after_launch.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/dies_while_writing.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/does_not_read.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/echo_stdin.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/fork_bomb.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/fork_once.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/long_stdout.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/read_one_line.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/sleep_forever.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/unbounded_output.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/write_forever_but_no_newline.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/module_test.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/test_interactive.py +0 -0
- {bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/test_interactive_async.py +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Use `uv run`to run and not `python`.
|
|
2
|
+
|
|
3
|
+
## Publishing Process
|
|
4
|
+
|
|
5
|
+
Increment the version number as appropriate in pyproject.toml. Delete old
|
|
6
|
+
builds from dist/. Run uv sync to update the lock file. Commit the changes,
|
|
7
|
+
which should have just changes to pyproject.taml and uv.lock. Run `uv build`
|
|
8
|
+
and then `uv publish`. In the interactive prompt, you MUST enter __token__
|
|
9
|
+
for the username. I know it says that, but I don't read what's on the screen.
|
|
10
|
+
The password is in the keychain.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bounded_subprocess
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: A library to facilitate running subprocesses that may misbehave.
|
|
5
5
|
Project-URL: Homepage, https://github.com/arjunguha/bounded_subprocess
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/arjunguha/bounded_subprocess
|
|
@@ -163,6 +163,9 @@ async def podman_run(
|
|
|
163
163
|
env=None,
|
|
164
164
|
stdin_data: Optional[str] = None,
|
|
165
165
|
stdin_write_timeout: Optional[int] = None,
|
|
166
|
+
volumes: List[str] = [],
|
|
167
|
+
cwd: Optional[str] = None,
|
|
168
|
+
memory_limit_mb: Optional[int] = None,
|
|
166
169
|
) -> Result:
|
|
167
170
|
"""
|
|
168
171
|
Run a subprocess in a podman container asynchronously with bounded stdout/stderr capture.
|
|
@@ -172,19 +175,33 @@ async def podman_run(
|
|
|
172
175
|
the same as `run`, except for an additional `image` parameter to specify the
|
|
173
176
|
container image to use.
|
|
174
177
|
|
|
178
|
+
Args:
|
|
179
|
+
args: Command arguments to run in the container.
|
|
180
|
+
image: Container image to use.
|
|
181
|
+
timeout_seconds: Maximum time to wait for the process to complete.
|
|
182
|
+
max_output_size: Maximum size in bytes for stdout/stderr capture.
|
|
183
|
+
env: Optional dictionary of environment variables.
|
|
184
|
+
stdin_data: Optional string data to write to stdin.
|
|
185
|
+
stdin_write_timeout: Optional timeout for writing stdin data.
|
|
186
|
+
volumes: Optional list of volume mount specifications (e.g., ["/host/path:/container/path"]).
|
|
187
|
+
cwd: Optional working directory path inside the container.
|
|
188
|
+
memory_limit_mb: Optional memory limit in megabytes for the container.
|
|
189
|
+
|
|
175
190
|
Example:
|
|
176
191
|
|
|
177
192
|
```python
|
|
178
193
|
import asyncio
|
|
179
|
-
from bounded_subprocess.bounded_subprocess_async import
|
|
194
|
+
from bounded_subprocess.bounded_subprocess_async import podman_run
|
|
180
195
|
|
|
181
196
|
async def main():
|
|
182
|
-
result = await
|
|
197
|
+
result = await podman_run(
|
|
183
198
|
["cat"],
|
|
184
199
|
image="alpine:latest",
|
|
185
200
|
timeout_seconds=5,
|
|
186
201
|
max_output_size=1024,
|
|
187
202
|
stdin_data="hello\n",
|
|
203
|
+
volumes=["/host/data:/container/data"],
|
|
204
|
+
cwd="/container/data",
|
|
188
205
|
)
|
|
189
206
|
print(result.exit_code)
|
|
190
207
|
print(result.stdout.strip())
|
|
@@ -209,6 +226,18 @@ async def podman_run(
|
|
|
209
226
|
for key, value in env.items():
|
|
210
227
|
podman_args.extend(["-e", f"{key}={value}"])
|
|
211
228
|
|
|
229
|
+
# Handle volume mounts
|
|
230
|
+
for volume in volumes:
|
|
231
|
+
podman_args.extend(["-v", volume])
|
|
232
|
+
|
|
233
|
+
# Handle memory limit
|
|
234
|
+
if memory_limit_mb is not None:
|
|
235
|
+
podman_args.extend(["--memory", f"{memory_limit_mb}m", "--memory-swap", f"{memory_limit_mb}m"])
|
|
236
|
+
|
|
237
|
+
# Handle working directory
|
|
238
|
+
if cwd is not None:
|
|
239
|
+
podman_args.extend(["-w", cwd])
|
|
240
|
+
|
|
212
241
|
podman_args.append(image)
|
|
213
242
|
podman_args.extend(args)
|
|
214
243
|
|
|
@@ -171,3 +171,77 @@ async def test_podman_run_unbounded_output():
|
|
|
171
171
|
assert result.timeout is True
|
|
172
172
|
# stderr may contain podman pull messages, which is fine
|
|
173
173
|
assert len(result.stdout) == 1024
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.asyncio
|
|
177
|
+
async def test_podman_run_volumes():
|
|
178
|
+
"""Test podman_run with volume mounts."""
|
|
179
|
+
import tempfile
|
|
180
|
+
|
|
181
|
+
# Create a temporary file with test content
|
|
182
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as tmp_file:
|
|
183
|
+
tmp_file.write("test content\n")
|
|
184
|
+
tmp_path = tmp_file.name
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
# Mount the file and read it from the container
|
|
188
|
+
result = await podman_run(
|
|
189
|
+
["cat", "/mnt/test.txt"],
|
|
190
|
+
image="alpine:latest",
|
|
191
|
+
timeout_seconds=5,
|
|
192
|
+
max_output_size=1024,
|
|
193
|
+
volumes=[f"{tmp_path}:/mnt/test.txt:ro"],
|
|
194
|
+
)
|
|
195
|
+
assert result.exit_code == 0
|
|
196
|
+
assert result.timeout is False
|
|
197
|
+
assert result.stdout == "test content\n"
|
|
198
|
+
finally:
|
|
199
|
+
# Clean up
|
|
200
|
+
try:
|
|
201
|
+
os.unlink(tmp_path)
|
|
202
|
+
except OSError:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.asyncio
|
|
207
|
+
async def test_podman_run_cwd():
|
|
208
|
+
"""Test podman_run with working directory."""
|
|
209
|
+
result = await podman_run(
|
|
210
|
+
["pwd"],
|
|
211
|
+
image="alpine:latest",
|
|
212
|
+
timeout_seconds=5,
|
|
213
|
+
max_output_size=1024,
|
|
214
|
+
cwd="/tmp",
|
|
215
|
+
)
|
|
216
|
+
assert result.exit_code == 0
|
|
217
|
+
assert result.timeout is False
|
|
218
|
+
assert result.stdout.strip() == "/tmp"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_podman_run_memory_limit_ok():
|
|
223
|
+
"""Test podman_run with memory limit that is sufficient."""
|
|
224
|
+
result = await podman_run(
|
|
225
|
+
["echo", "hello"],
|
|
226
|
+
image="alpine:latest",
|
|
227
|
+
timeout_seconds=10,
|
|
228
|
+
max_output_size=1024,
|
|
229
|
+
memory_limit_mb=64,
|
|
230
|
+
)
|
|
231
|
+
assert result.exit_code == 0
|
|
232
|
+
assert result.timeout is False
|
|
233
|
+
assert result.stdout.strip() == "hello"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@pytest.mark.asyncio
|
|
237
|
+
async def test_podman_run_memory_limit_oom():
|
|
238
|
+
"""Test podman_run where the process exceeds the memory limit."""
|
|
239
|
+
# Allocate ~128MB in a container limited to 32MB
|
|
240
|
+
result = await podman_run(
|
|
241
|
+
["python3", "-c", "x = bytearray(128 * 1024 * 1024)"],
|
|
242
|
+
image="python:3.12-alpine",
|
|
243
|
+
timeout_seconds=30,
|
|
244
|
+
max_output_size=4096,
|
|
245
|
+
memory_limit_mb=32,
|
|
246
|
+
)
|
|
247
|
+
assert result.exit_code != 0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
3
3
|
requires-python = ">=3.9"
|
|
4
4
|
resolution-markers = [
|
|
5
5
|
"python_full_version >= '3.10'",
|
|
@@ -31,7 +31,7 @@ wheels = [
|
|
|
31
31
|
|
|
32
32
|
[[package]]
|
|
33
33
|
name = "bounded-subprocess"
|
|
34
|
-
version = "2.
|
|
34
|
+
version = "2.6.0"
|
|
35
35
|
source = { editable = "." }
|
|
36
36
|
dependencies = [
|
|
37
37
|
{ name = "typeguard" },
|
bounded_subprocess-2.4.0/.git
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
gitdir: /media/external0/arjun-nosudo/repos/arjunguha/bounded_subprocess/bare/worktrees/main
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/bounded_subprocess.py
RENAMED
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/src/bounded_subprocess/interactive_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/dies_while_writing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.4.0 → bounded_subprocess-2.6.0}/test/evil_programs/unbounded_output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|