@@ -23,9 +23,12 @@ def __init__(
2323 stream_stdout : bool = False ,
2424 stream_stderr : bool = False ,
2525 raise_execution : bool = True ,
26+ raise_timeout : bool = True ,
2627 raise_exit_code : bool = True ,
2728 raise_stderr : bool = False ,
2829 text_output : bool = True ,
30+ timeout : float | None = None ,
31+ subprocess_kwargs : dict | None = None ,
2932 logger = None ,
3033 log_title : str = "Shell Process" ,
3134 log_level_execution : LogLevel = "critical" ,
@@ -39,9 +42,12 @@ def __init__(
3942 self .stream_stdout = stream_stdout
4043 self .stream_stderr = stream_stderr
4144 self .raise_execution = raise_execution
45+ self .raise_timeout = raise_timeout
4246 self .raise_exit_code = raise_exit_code
4347 self .raise_stderr = raise_stderr
4448 self .text_output = text_output
49+ self .timeout = timeout
50+ self .subprocess_kwargs = subprocess_kwargs or {}
4551 self .logger = logger
4652 self .log_title = log_title
4753 self .log_level_execution = log_level_execution
@@ -59,9 +65,12 @@ def run(
5965 stream_stdout : bool | None = None ,
6066 stream_stderr : bool | None = None ,
6167 raise_execution : bool | None = None ,
68+ raise_timeout : bool | None = None ,
6269 raise_exit_code : bool | None = None ,
6370 raise_stderr : bool | None = None ,
6471 text_output : bool | None = None ,
72+ timeout : float | None = None ,
73+ subprocess_kwargs : dict | None = None ,
6574 log_title : str | None = None ,
6675 log_level_execution : LogLevel | None = None ,
6776 log_level_exit_code : LogLevel | None = None ,
@@ -108,6 +117,8 @@ def run(
108117 )
109118 if not output .executed and args .raise_execution :
110119 raise _exception .PyShellManExecutionError (output = output )
120+ if output .timeout and args .raise_timeout :
121+ raise _exception .PyShellManTimeoutError (output = output )
111122 if not output .succeeded and args .raise_exit_code :
112123 raise _exception .PyShellManNonZeroExitCodeError (output = output )
113124 if stderr and args .raise_stderr :
@@ -127,9 +138,18 @@ def _run_nostream(
127138 args : _SimpleNamespace
128139 ) -> tuple [str | bytes | None , str | bytes | None , int | None ]:
129140 try :
130- process = _subprocess .run (command , text = args .text_output , cwd = args .cwd , capture_output = True )
141+ process = _subprocess .run (
142+ command ,
143+ text = args .text_output ,
144+ cwd = args .cwd ,
145+ capture_output = True ,
146+ timeout = args .timeout ,
147+ ** args .subprocess_kwargs ,
148+ )
131149 except FileNotFoundError :
132150 return None , None , None
151+ except _subprocess .TimeoutExpired as e :
152+ return e .stdout , e .stderr , "TIMEOUT"
133153 stdout = (process .stdout .strip () if args .text_output else process .stdout ) or None
134154 stderr = (process .stderr .strip () if args .text_output else process .stderr ) or None
135155 code = process .returncode
@@ -151,6 +171,7 @@ def _run_stream(
151171 stdout = _subprocess .PIPE ,
152172 stderr = _subprocess .PIPE ,
153173 bufsize = 1 if args .text_output else 0 ,
174+ ** args .subprocess_kwargs ,
154175 )
155176 except FileNotFoundError :
156177 return None , None , None
@@ -190,16 +211,26 @@ def read_stream(stream, chunks, live: bool):
190211
191212 for t in threads :
192213 t .start ()
193- process .wait ()
214+
215+ try :
216+ process .wait (timeout = args .timeout )
217+ except TimeoutError :
218+ process .kill ()
219+ process .wait () # Allow reader threads to finish draining killed process’s output
220+ returncode = "TIMEOUT"
221+ else :
222+ returncode = process .returncode
223+
194224 for t in threads :
195225 t .join ()
226+
196227 if args .text_output :
197228 stdout = '' .join (stdout_chunks )
198229 stderr = '' .join (stderr_chunks )
199230 else :
200231 stdout = b'' .join (stdout_chunks )
201232 stderr = b'' .join (stderr_chunks )
202- return stdout , stderr , process . returncode
233+ return stdout , stderr , returncode
203234
204235
205236def run (
@@ -209,9 +240,12 @@ def run(
209240 stream_stdout : bool = False ,
210241 stream_stderr : bool = False ,
211242 raise_execution : bool = True ,
243+ raise_timeout : bool | None = None ,
212244 raise_exit_code : bool = True ,
213245 raise_stderr : bool = False ,
214246 text_output : bool = True ,
247+ timeout : float | None = None ,
248+ subprocess_kwargs : dict | None = None ,
215249 logger = None ,
216250 log_title : str = "Shell SubProcess" ,
217251 log_level_execution : LogLevel = "critical" ,
@@ -225,9 +259,12 @@ def run(
225259 stream_stdout = stream_stdout ,
226260 stream_stderr = stream_stderr ,
227261 raise_execution = raise_execution ,
262+ raise_timeout = raise_timeout ,
228263 raise_exit_code = raise_exit_code ,
229264 raise_stderr = raise_stderr ,
230265 text_output = text_output ,
266+ timeout = timeout ,
267+ subprocess_kwargs = subprocess_kwargs ,
231268 logger = logger ,
232269 log_title = log_title ,
233270 log_level_execution = log_level_execution ,
0 commit comments