bounded_subprocess 2.5.0__tar.gz → 2.7.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.5.0 → bounded_subprocess-2.7.0}/PKG-INFO +1 -1
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/pyproject.toml +1 -1
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/src/bounded_subprocess/bounded_subprocess_async.py +178 -2
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/test_async.py +82 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/uv.lock +2 -2
- bounded_subprocess-2.5.0/.git +0 -1
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/.github/workflows/docs.yaml +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/.github/workflows/test.yml +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/.gitignore +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/AGENTS.md +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/LICENSE.txt +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/Makefile +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/README.md +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/cspell.config.yaml +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/docs/index.md +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/mkdocs.yaml +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/src/bounded_subprocess/__init__.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/src/bounded_subprocess/bounded_subprocess.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/src/bounded_subprocess/interactive.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/src/bounded_subprocess/interactive_async.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/src/bounded_subprocess/util.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/__init__.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/block_on_inputs.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/close_outputs.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/dies_shortly_after_launch.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/dies_while_writing.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/does_not_read.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/echo_stdin.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/fork_bomb.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/fork_once.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/long_stdout.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/read_one_line.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/sleep_forever.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/unbounded_output.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/write_forever_but_no_newline.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/module_test.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/test_interactive.py +0 -0
- {bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/test/test_interactive_async.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bounded_subprocess
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.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
|
|
@@ -22,6 +22,133 @@ from .util import (
|
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _read_process_group_id(pid: int) -> Optional[int]:
|
|
26
|
+
"""
|
|
27
|
+
Read a process's PGID from `/proc/<pid>/stat`.
|
|
28
|
+
|
|
29
|
+
We use PGID to approximate "all related processes" without requiring
|
|
30
|
+
cgroups. Parsing `/proc/<pid>/stat` is Linux-specific and somewhat brittle
|
|
31
|
+
if format assumptions ever change, but it avoids external dependencies and
|
|
32
|
+
works on typical cluster nodes where cgroups may not be configured for us.
|
|
33
|
+
"""
|
|
34
|
+
stat_path = f"/proc/{pid}/stat"
|
|
35
|
+
try:
|
|
36
|
+
with open(stat_path, "r", encoding="utf-8") as f:
|
|
37
|
+
stat = f.read().strip()
|
|
38
|
+
except (FileNotFoundError, ProcessLookupError, PermissionError, OSError):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
right_paren = stat.rfind(")")
|
|
42
|
+
if right_paren == -1:
|
|
43
|
+
return None
|
|
44
|
+
rest = stat[right_paren + 1 :].strip().split()
|
|
45
|
+
# After "comm)", fields are: state, ppid, pgrp, ...
|
|
46
|
+
if len(rest) < 3:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return int(rest[2])
|
|
50
|
+
except ValueError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_proc_status_kb(pid: int, metric_name: str) -> Optional[int]:
|
|
55
|
+
"""
|
|
56
|
+
Read a memory metric (in kB) from `/proc/<pid>/status`.
|
|
57
|
+
|
|
58
|
+
Returns None when the process is gone or the metric is unavailable.
|
|
59
|
+
This is intentionally best-effort because `/proc` is racy for short-lived
|
|
60
|
+
processes and the watchdog tolerates partial visibility.
|
|
61
|
+
"""
|
|
62
|
+
status_path = f"/proc/{pid}/status"
|
|
63
|
+
try:
|
|
64
|
+
with open(status_path, "r", encoding="utf-8") as f:
|
|
65
|
+
for line in f:
|
|
66
|
+
if line.startswith(f"{metric_name}:"):
|
|
67
|
+
parts = line.split()
|
|
68
|
+
if len(parts) >= 2:
|
|
69
|
+
return int(parts[1])
|
|
70
|
+
return None
|
|
71
|
+
except (FileNotFoundError, ProcessLookupError, PermissionError, ValueError):
|
|
72
|
+
return None
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_peak_rss_kb(pid: int) -> Optional[int]:
|
|
77
|
+
"""
|
|
78
|
+
Read per-process peak resident memory in kB.
|
|
79
|
+
|
|
80
|
+
Preferred metric is `VmHWM` (peak RSS); if absent, falls back to `VmRSS`
|
|
81
|
+
(current RSS). The fallback makes behavior degrade gracefully on kernels
|
|
82
|
+
that do not expose `VmHWM` consistently, at the cost of weaker "true peak"
|
|
83
|
+
tracking for those processes.
|
|
84
|
+
"""
|
|
85
|
+
# VmHWM is the process's peak RSS in kB. Fall back to VmRSS if unavailable.
|
|
86
|
+
peak = _read_proc_status_kb(pid, "VmHWM")
|
|
87
|
+
if peak is not None:
|
|
88
|
+
return peak
|
|
89
|
+
return _read_proc_status_kb(pid, "VmRSS")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _sum_process_group_peak_rss_kb(process_group_id: int) -> Optional[int]:
|
|
93
|
+
"""
|
|
94
|
+
Sum per-process peak RSS across all processes in a process group.
|
|
95
|
+
|
|
96
|
+
This is a deliberate non-cgroup approximation for aggregate memory. It
|
|
97
|
+
overcounts shared pages (RSS semantics), can miss processes that start/exit
|
|
98
|
+
between scans, and is O(number of /proc entries) per poll. Those tradeoffs
|
|
99
|
+
are accepted here to keep enforcement portable on cluster environments
|
|
100
|
+
where we cannot assume cgroup control.
|
|
101
|
+
"""
|
|
102
|
+
total_kb = 0
|
|
103
|
+
found_any = False
|
|
104
|
+
for entry in os.scandir("/proc"):
|
|
105
|
+
if not entry.name.isdigit():
|
|
106
|
+
continue
|
|
107
|
+
pid = int(entry.name)
|
|
108
|
+
pgid = _read_process_group_id(pid)
|
|
109
|
+
if pgid != process_group_id:
|
|
110
|
+
continue
|
|
111
|
+
peak_kb = _read_peak_rss_kb(pid)
|
|
112
|
+
if peak_kb is not None:
|
|
113
|
+
total_kb += peak_kb
|
|
114
|
+
found_any = True
|
|
115
|
+
return total_kb if found_any else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def _memory_watchdog(
|
|
119
|
+
p: subprocess.Popen,
|
|
120
|
+
process_group_id: int,
|
|
121
|
+
deadline: float,
|
|
122
|
+
memory_limit_mb: int,
|
|
123
|
+
memory_watchdog_interval_seconds: float,
|
|
124
|
+
) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Enforce `memory_limit_mb` using periodic process-group peak-RSS checks.
|
|
127
|
+
|
|
128
|
+
The watchdog polls aggregate process-group peak RSS and kills the whole
|
|
129
|
+
process group when the limit is exceeded, returning True in that case.
|
|
130
|
+
Polling is interval-based (default 1s, configurable) and intentionally
|
|
131
|
+
approximate: it favors broad compatibility without cgroups over perfect
|
|
132
|
+
accounting precision.
|
|
133
|
+
"""
|
|
134
|
+
limit_kb = memory_limit_mb * 1024
|
|
135
|
+
check_interval = max(0.01, memory_watchdog_interval_seconds)
|
|
136
|
+
while True:
|
|
137
|
+
if p.poll() is not None:
|
|
138
|
+
return False
|
|
139
|
+
aggregate_peak_rss_kb = _sum_process_group_peak_rss_kb(process_group_id)
|
|
140
|
+
if aggregate_peak_rss_kb is not None and aggregate_peak_rss_kb > limit_kb:
|
|
141
|
+
try:
|
|
142
|
+
os.killpg(process_group_id, signal.SIGKILL)
|
|
143
|
+
except ProcessLookupError:
|
|
144
|
+
pass
|
|
145
|
+
return True
|
|
146
|
+
remaining = deadline - time.time()
|
|
147
|
+
if remaining <= 0:
|
|
148
|
+
return False
|
|
149
|
+
await asyncio.sleep(min(check_interval, remaining))
|
|
150
|
+
|
|
151
|
+
|
|
25
152
|
async def run(
|
|
26
153
|
args: List[str],
|
|
27
154
|
timeout_seconds: int = 15,
|
|
@@ -29,6 +156,8 @@ async def run(
|
|
|
29
156
|
env=None,
|
|
30
157
|
stdin_data: Optional[str] = None,
|
|
31
158
|
stdin_write_timeout: Optional[int] = None,
|
|
159
|
+
memory_limit_mb: Optional[int] = None,
|
|
160
|
+
memory_watchdog_interval_seconds: float = 1.0,
|
|
32
161
|
) -> Result:
|
|
33
162
|
"""
|
|
34
163
|
Run a subprocess asynchronously with bounded stdout/stderr capture.
|
|
@@ -38,7 +167,10 @@ async def run(
|
|
|
38
167
|
truncated to `max_output_size` bytes each. If the timeout elapses,
|
|
39
168
|
`Result.timeout` is True and `Result.exit_code` is -1. If `stdin_data`
|
|
40
169
|
cannot be fully written before `stdin_write_timeout`, `Result.exit_code`
|
|
41
|
-
is set to -1 even if the process exits normally.
|
|
170
|
+
is set to -1 even if the process exits normally. If `memory_limit_mb` is
|
|
171
|
+
set, a watchdog checks aggregate peak RSS (`VmHWM`, summed across the
|
|
172
|
+
process group) at a fixed interval and kills the process group when the
|
|
173
|
+
limit is exceeded.
|
|
42
174
|
|
|
43
175
|
Example:
|
|
44
176
|
|
|
@@ -91,6 +223,18 @@ async def run(
|
|
|
91
223
|
except (BrokenPipeError, BlockingIOError):
|
|
92
224
|
pass
|
|
93
225
|
|
|
226
|
+
memory_watchdog_task = None
|
|
227
|
+
if memory_limit_mb is not None:
|
|
228
|
+
memory_watchdog_task = asyncio.create_task(
|
|
229
|
+
_memory_watchdog(
|
|
230
|
+
p=p,
|
|
231
|
+
process_group_id=process_group_id,
|
|
232
|
+
deadline=deadline,
|
|
233
|
+
memory_limit_mb=memory_limit_mb,
|
|
234
|
+
memory_watchdog_interval_seconds=memory_watchdog_interval_seconds,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
94
238
|
bufs = await read_to_eof_async(
|
|
95
239
|
[p.stdout, p.stderr],
|
|
96
240
|
timeout_seconds=timeout_seconds,
|
|
@@ -110,13 +254,28 @@ async def run(
|
|
|
110
254
|
break
|
|
111
255
|
await asyncio.sleep(min(0.05, remaining))
|
|
112
256
|
|
|
257
|
+
memory_limit_exceeded = False
|
|
258
|
+
if memory_watchdog_task is not None:
|
|
259
|
+
if memory_watchdog_task.done():
|
|
260
|
+
memory_limit_exceeded = memory_watchdog_task.result()
|
|
261
|
+
else:
|
|
262
|
+
memory_watchdog_task.cancel()
|
|
263
|
+
try:
|
|
264
|
+
await memory_watchdog_task
|
|
265
|
+
except asyncio.CancelledError:
|
|
266
|
+
pass
|
|
267
|
+
|
|
113
268
|
try:
|
|
114
269
|
os.killpg(process_group_id, signal.SIGKILL)
|
|
115
270
|
except ProcessLookupError:
|
|
116
271
|
pass
|
|
117
272
|
|
|
118
273
|
exit_code = (
|
|
119
|
-
-1
|
|
274
|
+
-1
|
|
275
|
+
if is_timeout
|
|
276
|
+
or (stdin_data is not None and not write_ok)
|
|
277
|
+
or memory_limit_exceeded
|
|
278
|
+
else exit_code
|
|
120
279
|
)
|
|
121
280
|
|
|
122
281
|
return Result(
|
|
@@ -165,6 +324,8 @@ async def podman_run(
|
|
|
165
324
|
stdin_write_timeout: Optional[int] = None,
|
|
166
325
|
volumes: List[str] = [],
|
|
167
326
|
cwd: Optional[str] = None,
|
|
327
|
+
memory_limit_mb: Optional[int] = None,
|
|
328
|
+
entrypoint: Optional[str] = None,
|
|
168
329
|
) -> Result:
|
|
169
330
|
"""
|
|
170
331
|
Run a subprocess in a podman container asynchronously with bounded stdout/stderr capture.
|
|
@@ -184,6 +345,12 @@ async def podman_run(
|
|
|
184
345
|
stdin_write_timeout: Optional timeout for writing stdin data.
|
|
185
346
|
volumes: Optional list of volume mount specifications (e.g., ["/host/path:/container/path"]).
|
|
186
347
|
cwd: Optional working directory path inside the container.
|
|
348
|
+
memory_limit_mb: Optional memory limit in megabytes for the container.
|
|
349
|
+
entrypoint: Optional override for the container image's ENTRYPOINT.
|
|
350
|
+
Passed through to podman's `--entrypoint` flag. Pass `""` to clear
|
|
351
|
+
the image's ENTRYPOINT entirely (so `args` is interpreted as the
|
|
352
|
+
full command). When `None`, the flag is omitted and the image's
|
|
353
|
+
ENTRYPOINT is used unchanged.
|
|
187
354
|
|
|
188
355
|
Example:
|
|
189
356
|
|
|
@@ -228,10 +395,19 @@ async def podman_run(
|
|
|
228
395
|
for volume in volumes:
|
|
229
396
|
podman_args.extend(["-v", volume])
|
|
230
397
|
|
|
398
|
+
# Handle memory limit
|
|
399
|
+
if memory_limit_mb is not None:
|
|
400
|
+
podman_args.extend(["--memory", f"{memory_limit_mb}m", "--memory-swap", f"{memory_limit_mb}m"])
|
|
401
|
+
|
|
231
402
|
# Handle working directory
|
|
232
403
|
if cwd is not None:
|
|
233
404
|
podman_args.extend(["-w", cwd])
|
|
234
405
|
|
|
406
|
+
# Handle entrypoint override. Note: `entrypoint=""` is meaningful — it
|
|
407
|
+
# clears the image's ENTRYPOINT — so we test against None, not falsiness.
|
|
408
|
+
if entrypoint is not None:
|
|
409
|
+
podman_args.extend(["--entrypoint", entrypoint])
|
|
410
|
+
|
|
235
411
|
podman_args.append(image)
|
|
236
412
|
podman_args.extend(args)
|
|
237
413
|
|
|
@@ -126,6 +126,59 @@ async def test_read_one_line():
|
|
|
126
126
|
await assert_no_running_evil()
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
async def test_run_memory_limit_ok():
|
|
131
|
+
result = await run(
|
|
132
|
+
["python3", "-c", "import time; x = bytearray(8 * 1024 * 1024); time.sleep(0.5)"],
|
|
133
|
+
timeout_seconds=5,
|
|
134
|
+
max_output_size=1024,
|
|
135
|
+
memory_limit_mb=256,
|
|
136
|
+
memory_watchdog_interval_seconds=0.05,
|
|
137
|
+
)
|
|
138
|
+
assert result.exit_code == 0
|
|
139
|
+
assert result.timeout is False
|
|
140
|
+
assert len(result.stderr) == 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_run_memory_limit_exceeded():
|
|
145
|
+
result = await run(
|
|
146
|
+
["python3", "-c", "import time; x = bytearray(128 * 1024 * 1024); time.sleep(5)"],
|
|
147
|
+
timeout_seconds=10,
|
|
148
|
+
max_output_size=1024,
|
|
149
|
+
memory_limit_mb=64,
|
|
150
|
+
memory_watchdog_interval_seconds=0.05,
|
|
151
|
+
)
|
|
152
|
+
assert result.exit_code == -1
|
|
153
|
+
assert result.timeout is False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.asyncio
|
|
157
|
+
async def test_run_memory_limit_exceeded_in_child_process():
|
|
158
|
+
result = await run(
|
|
159
|
+
[
|
|
160
|
+
"python3",
|
|
161
|
+
"-c",
|
|
162
|
+
(
|
|
163
|
+
"import os, time\n"
|
|
164
|
+
"pid = os.fork()\n"
|
|
165
|
+
"_ = bytearray(4 * 1024 * 1024)\n"
|
|
166
|
+
"if pid == 0:\n"
|
|
167
|
+
" _ = bytearray(128 * 1024 * 1024)\n"
|
|
168
|
+
" time.sleep(5)\n"
|
|
169
|
+
"else:\n"
|
|
170
|
+
" time.sleep(5)\n"
|
|
171
|
+
),
|
|
172
|
+
],
|
|
173
|
+
timeout_seconds=10,
|
|
174
|
+
max_output_size=1024,
|
|
175
|
+
memory_limit_mb=64,
|
|
176
|
+
memory_watchdog_interval_seconds=0.05,
|
|
177
|
+
)
|
|
178
|
+
assert result.exit_code == -1
|
|
179
|
+
assert result.timeout is False
|
|
180
|
+
|
|
181
|
+
|
|
129
182
|
@pytest.mark.asyncio
|
|
130
183
|
async def test_podman_run_stdin():
|
|
131
184
|
"""Test podman_run with stdin_data input."""
|
|
@@ -216,3 +269,32 @@ async def test_podman_run_cwd():
|
|
|
216
269
|
assert result.exit_code == 0
|
|
217
270
|
assert result.timeout is False
|
|
218
271
|
assert result.stdout.strip() == "/tmp"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.asyncio
|
|
275
|
+
async def test_podman_run_memory_limit_ok():
|
|
276
|
+
"""Test podman_run with memory limit that is sufficient."""
|
|
277
|
+
result = await podman_run(
|
|
278
|
+
["echo", "hello"],
|
|
279
|
+
image="alpine:latest",
|
|
280
|
+
timeout_seconds=10,
|
|
281
|
+
max_output_size=1024,
|
|
282
|
+
memory_limit_mb=64,
|
|
283
|
+
)
|
|
284
|
+
assert result.exit_code == 0
|
|
285
|
+
assert result.timeout is False
|
|
286
|
+
assert result.stdout.strip() == "hello"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@pytest.mark.asyncio
|
|
290
|
+
async def test_podman_run_memory_limit_oom():
|
|
291
|
+
"""Test podman_run where the process exceeds the memory limit."""
|
|
292
|
+
# Allocate ~128MB in a container limited to 32MB
|
|
293
|
+
result = await podman_run(
|
|
294
|
+
["python3", "-c", "x = bytearray(128 * 1024 * 1024)"],
|
|
295
|
+
image="python:3.12-alpine",
|
|
296
|
+
timeout_seconds=30,
|
|
297
|
+
max_output_size=4096,
|
|
298
|
+
memory_limit_mb=32,
|
|
299
|
+
)
|
|
300
|
+
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.7.0"
|
|
35
35
|
source = { editable = "." }
|
|
36
36
|
dependencies = [
|
|
37
37
|
{ name = "typeguard" },
|
bounded_subprocess-2.5.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
|
|
File without changes
|
{bounded_subprocess-2.5.0 → bounded_subprocess-2.7.0}/src/bounded_subprocess/bounded_subprocess.py
RENAMED
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.5.0 → bounded_subprocess-2.7.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.5.0 → bounded_subprocess-2.7.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.5.0 → bounded_subprocess-2.7.0}/test/evil_programs/unbounded_output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|