Skip to content

Commit 8adcdb0

Browse files
committed
Add "env_patch" argument for subprocess
* add environment variables to subprocess call without provision of full env.
1 parent e168685 commit 8adcdb0

File tree

3 files changed

+64
-4
lines changed

3 files changed

+64
-4
lines changed

doc/source/Subprocess.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ API: Subprocess
5555
.. Note:: Enter and exit main context manager is produced as well.
5656
.. versionadded:: 4.1.0
5757

58-
.. py:method:: execute(command, verbose=False, timeout=1*60*60, *, log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, **kwargs)
58+
.. py:method:: execute(command, verbose=False, timeout=1*60*60, *, log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, env_patch=None, **kwargs)
5959
6060
Execute command and wait for return code.
6161

@@ -78,6 +78,8 @@ API: Subprocess
7878
:type cwd: ``typing.Optional[typing.Union[str, bytes]]``
7979
:param env: Defines the environment variables for the new process.
8080
:type env: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
81+
:param env_patch: Defines the environment variables to ADD for the new process.
82+
:type env_patch: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
8183
:rtype: ExecResult
8284
:raises ExecHelperTimeoutError: Timeout exceeded
8385

@@ -87,7 +89,7 @@ API: Subprocess
8789
.. versionchanged:: 1.2.0 default timeout 1 hour
8890
.. versionchanged:: 1.2.0 stdin data
8991

90-
.. py:method:: __call__(command, verbose=False, timeout=1*60*60, *, log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, **kwargs)
92+
.. py:method:: __call__(command, verbose=False, timeout=1*60*60, *, log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, env_patch=None, **kwargs)
9193
9294
Execute command and wait for return code.
9395

@@ -110,13 +112,15 @@ API: Subprocess
110112
:type cwd: ``typing.Optional[typing.Union[str, bytes]]``
111113
:param env: Defines the environment variables for the new process.
112114
:type env: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
115+
:param env_patch: Defines the environment variables to ADD for the new process.
116+
:type env_patch: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
113117
:rtype: ExecResult
114118
:raises ExecHelperTimeoutError: Timeout exceeded
115119

116120
.. note:: stdin channel is closed after the input processing
117121
.. versionadded:: 3.3.0
118122

119-
.. py:method:: check_call(command, verbose=False, timeout=1*60*60, error_info=None, expected=(0,), raise_on_err=True, *, log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, exception_class=CalledProcessError, **kwargs)
123+
.. py:method:: check_call(command, verbose=False, timeout=1*60*60, error_info=None, expected=(0,), raise_on_err=True, *, log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, env_patch=None, exception_class=CalledProcessError, **kwargs)
120124
121125
Execute command and check for return code.
122126

@@ -145,6 +149,8 @@ API: Subprocess
145149
:type cwd: ``typing.Optional[typing.Union[str, bytes]]``
146150
:param env: Defines the environment variables for the new process.
147151
:type env: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
152+
:param env_patch: Defines the environment variables to ADD for the new process.
153+
:type env_patch: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
148154
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
149155
:type exception_class: typing.Type[CalledProcessError]
150156
:rtype: ExecResult
@@ -156,7 +162,7 @@ API: Subprocess
156162
.. versionchanged:: 3.2.0 Exception class can be substituted
157163
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
158164

159-
.. py:method:: check_stderr(command, verbose=False, timeout=1*60*60, error_info=None, raise_on_err=True, *, expected=(0,), log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, exception_class=CalledProcessError, **kwargs)
165+
.. py:method:: check_stderr(command, verbose=False, timeout=1*60*60, error_info=None, raise_on_err=True, *, expected=(0,), log_mask_re=None, stdin=None, open_stdout=True, open_stderr=True, cwd=None, env=None, env_patch=None, exception_class=CalledProcessError, **kwargs)
160166
161167
Execute command expecting return code 0 and empty STDERR.
162168

@@ -185,6 +191,8 @@ API: Subprocess
185191
:type cwd: ``typing.Optional[typing.Union[str, bytes]]``
186192
:param env: Defines the environment variables for the new process.
187193
:type env: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
194+
:param env_patch: Defines the environment variables to ADD for the new process.
195+
:type env_patch: ``typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]``
188196
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
189197
:type exception_class: typing.Type[CalledProcessError]
190198
:rtype: ExecResult

exec_helpers/async_api/subprocess_runner.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121

2222
# Standard Library
2323
import asyncio
24+
import copy
2425
import datetime
2526
import errno
2627
import logging
28+
import os
2729
import typing
2830

2931
# Exec-Helpers Implementation
@@ -189,6 +191,7 @@ async def _execute_async( # type: ignore # pylint: disable=arguments-differ
189191
chroot_path: typing.Optional[str] = None,
190192
cwd: typing.Optional[typing.Union[str, bytes]] = None,
191193
env: _EnvT = None,
194+
env_patch: _EnvT = None,
192195
**kwargs: typing.Any,
193196
) -> SubprocessExecuteAsyncResult:
194197
"""Execute command in async mode and return Popen with IO objects.
@@ -207,6 +210,8 @@ async def _execute_async( # type: ignore # pylint: disable=arguments-differ
207210
:type cwd: typing.Optional[typing.Union[str, bytes]]
208211
:param env: Defines the environment variables for the new process.
209212
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
213+
:param env_patch: Defines the environment variables to ADD for the new process.
214+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
210215
:param kwargs: additional parameters for call.
211216
:type kwargs: typing.Any
212217
:return: Tuple with control interface and file-like objects for STDIN/STDERR/STDOUT
@@ -224,6 +229,11 @@ async def _execute_async( # type: ignore # pylint: disable=arguments-differ
224229
"""
225230
started = datetime.datetime.utcnow()
226231

232+
if env_patch is not None:
233+
# make mutable copy
234+
env = dict(copy.deepcopy(os.environ) if env is None else copy.deepcopy(env)) # type: ignore
235+
env.update(env_patch) # type: ignore
236+
227237
process: asyncio.subprocess.Process = await asyncio.create_subprocess_shell( # pylint: disable=no-member
228238
cmd=self._prepare_command(cmd=command, chroot_path=chroot_path),
229239
stdout=asyncio.subprocess.PIPE if open_stdout else asyncio.subprocess.DEVNULL,
@@ -282,6 +292,7 @@ async def execute( # type: ignore # pylint: disable=arguments-differ
282292
open_stderr: bool = True,
283293
cwd: typing.Optional[typing.Union[str, bytes]] = None,
284294
env: _EnvT = None,
295+
env_patch: _EnvT = None,
285296
**kwargs: typing.Any,
286297
) -> exec_result.ExecResult:
287298
"""Execute command and wait for return code.
@@ -305,6 +316,8 @@ async def execute( # type: ignore # pylint: disable=arguments-differ
305316
:type cwd: typing.Optional[typing.Union[str, bytes]]
306317
:param env: Defines the environment variables for the new process.
307318
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
319+
:param env_patch: Defines the environment variables to ADD for the new process.
320+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
308321
:param kwargs: additional parameters for call.
309322
:type kwargs: typing.Any
310323
:return: Execution result
@@ -324,6 +337,7 @@ async def execute( # type: ignore # pylint: disable=arguments-differ
324337
open_stderr=open_stderr,
325338
cwd=cwd,
326339
env=env,
340+
env_patch=env_patch,
327341
**kwargs,
328342
)
329343

@@ -339,6 +353,7 @@ async def __call__( # type: ignore
339353
open_stderr: bool = True,
340354
cwd: typing.Optional[typing.Union[str, bytes]] = None,
341355
env: _EnvT = None,
356+
env_patch: _EnvT = None,
342357
**kwargs: typing.Any,
343358
) -> exec_result.ExecResult:
344359
"""Execute command and wait for return code.
@@ -362,6 +377,8 @@ async def __call__( # type: ignore
362377
:type cwd: typing.Optional[typing.Union[str, bytes]]
363378
:param env: Defines the environment variables for the new process.
364379
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
380+
:param env_patch: Defines the environment variables to ADD for the new process.
381+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
365382
:param kwargs: additional parameters for call.
366383
:type kwargs: typing.Any
367384
:return: Execution result
@@ -381,6 +398,7 @@ async def __call__( # type: ignore
381398
open_stderr=open_stderr,
382399
cwd=cwd,
383400
env=env,
401+
env_patch=env_patch,
384402
**kwargs,
385403
)
386404

@@ -399,6 +417,7 @@ async def check_call( # type: ignore # pylint: disable=arguments-differ
399417
open_stderr: bool = True,
400418
cwd: typing.Optional[typing.Union[str, bytes]] = None,
401419
env: _EnvT = None,
420+
env_patch: _EnvT = None,
402421
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
403422
**kwargs: typing.Any,
404423
) -> exec_result.ExecResult:
@@ -429,6 +448,8 @@ async def check_call( # type: ignore # pylint: disable=arguments-differ
429448
:type cwd: typing.Optional[typing.Union[str, bytes]]
430449
:param env: Defines the environment variables for the new process.
431450
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
451+
:param env_patch: Defines the environment variables to ADD for the new process.
452+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
432453
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
433454
:type exception_class: typing.Type[exceptions.CalledProcessError]
434455
:param kwargs: additional parameters for call.
@@ -455,6 +476,7 @@ async def check_call( # type: ignore # pylint: disable=arguments-differ
455476
open_stderr=open_stderr,
456477
cwd=cwd,
457478
env=env,
479+
env_patch=env_patch,
458480
exception_class=exception_class,
459481
**kwargs,
460482
)
@@ -474,6 +496,7 @@ async def check_stderr( # type: ignore # pylint: disable=arguments-differ
474496
open_stderr: bool = True,
475497
cwd: typing.Optional[typing.Union[str, bytes]] = None,
476498
env: _EnvT = None,
499+
env_patch: _EnvT = None,
477500
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
478501
**kwargs: typing.Any,
479502
) -> exec_result.ExecResult:
@@ -504,6 +527,8 @@ async def check_stderr( # type: ignore # pylint: disable=arguments-differ
504527
:type cwd: typing.Optional[typing.Union[str, bytes]]
505528
:param env: Defines the environment variables for the new process.
506529
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
530+
:param env_patch: Defines the environment variables to ADD for the new process.
531+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
507532
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
508533
:type exception_class: typing.Type[exceptions.CalledProcessError]
509534
:param kwargs: additional parameters for call.
@@ -530,6 +555,7 @@ async def check_stderr( # type: ignore # pylint: disable=arguments-differ
530555
open_stderr=open_stderr,
531556
cwd=cwd,
532557
env=env,
558+
env_patch=env_patch,
533559
exception_class=exception_class,
534560
**kwargs,
535561
)

exec_helpers/subprocess_runner.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020

2121
# Standard Library
2222
import concurrent.futures
23+
import copy
2324
import datetime
2425
import errno
2526
import logging
27+
import os
2628
import subprocess # nosec # Expected usage
2729
import typing
2830

@@ -202,6 +204,7 @@ def _execute_async( # pylint: disable=arguments-differ
202204
chroot_path: typing.Optional[str] = None,
203205
cwd: typing.Optional[typing.Union[str, bytes]] = None,
204206
env: _EnvT = None,
207+
env_patch: _EnvT = None,
205208
**kwargs: typing.Any,
206209
) -> SubprocessExecuteAsyncResult:
207210
"""Execute command in async mode and return Popen with IO objects.
@@ -220,6 +223,8 @@ def _execute_async( # pylint: disable=arguments-differ
220223
:type cwd: typing.Optional[typing.Union[str, bytes]]
221224
:param env: Defines the environment variables for the new process.
222225
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
226+
:param env_patch: Defines the environment variables to ADD for the new process.
227+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
223228
:param kwargs: additional parameters for call.
224229
:type kwargs: typing.Any
225230
:return: Tuple with control interface and file-like objects for STDIN/STDERR/STDOUT
@@ -242,6 +247,11 @@ def _execute_async( # pylint: disable=arguments-differ
242247
"""
243248
started = datetime.datetime.utcnow()
244249

250+
if env_patch is not None:
251+
# make mutable copy
252+
env = dict(copy.deepcopy(os.environ) if env is None else copy.deepcopy(env)) # type: ignore
253+
env.update(env_patch) # type: ignore
254+
245255
process: "subprocess.Popen[bytes]" = subprocess.Popen(
246256
args=[self._prepare_command(cmd=command, chroot_path=chroot_path)],
247257
stdout=subprocess.PIPE if open_stdout else subprocess.DEVNULL,
@@ -297,6 +307,7 @@ def execute( # pylint: disable=arguments-differ
297307
open_stderr: bool = True,
298308
cwd: typing.Optional[typing.Union[str, bytes]] = None,
299309
env: _EnvT = None,
310+
env_patch: _EnvT = None,
300311
**kwargs: typing.Any,
301312
) -> exec_result.ExecResult:
302313
"""Execute command and wait for return code.
@@ -320,6 +331,8 @@ def execute( # pylint: disable=arguments-differ
320331
:type cwd: typing.Optional[typing.Union[str, bytes]]
321332
:param env: Defines the environment variables for the new process.
322333
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
334+
:param env_patch: Defines the environment variables to ADD for the new process.
335+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
323336
:param kwargs: additional parameters for call.
324337
:type kwargs: typing.Any
325338
:return: Execution result
@@ -339,6 +352,7 @@ def execute( # pylint: disable=arguments-differ
339352
open_stderr=open_stderr,
340353
cwd=cwd,
341354
env=env,
355+
env_patch=env_patch,
342356
**kwargs,
343357
)
344358

@@ -354,6 +368,7 @@ def __call__(
354368
open_stderr: bool = True,
355369
cwd: typing.Optional[typing.Union[str, bytes]] = None,
356370
env: _EnvT = None,
371+
env_patch: _EnvT = None,
357372
**kwargs: typing.Any,
358373
) -> exec_result.ExecResult:
359374
"""Execute command and wait for return code.
@@ -377,6 +392,8 @@ def __call__(
377392
:type cwd: typing.Optional[typing.Union[str, bytes]]
378393
:param env: Defines the environment variables for the new process.
379394
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
395+
:param env_patch: Defines the environment variables to ADD for the new process.
396+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
380397
:param kwargs: additional parameters for call.
381398
:type kwargs: typing.Any
382399
:return: Execution result
@@ -396,6 +413,7 @@ def __call__(
396413
open_stderr=open_stderr,
397414
cwd=cwd,
398415
env=env,
416+
env_patch=env_patch,
399417
**kwargs,
400418
)
401419

@@ -414,6 +432,7 @@ def check_call( # pylint: disable=arguments-differ
414432
open_stderr: bool = True,
415433
cwd: typing.Optional[typing.Union[str, bytes]] = None,
416434
env: _EnvT = None,
435+
env_patch: _EnvT = None,
417436
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
418437
**kwargs: typing.Any,
419438
) -> exec_result.ExecResult:
@@ -444,6 +463,8 @@ def check_call( # pylint: disable=arguments-differ
444463
:type cwd: typing.Optional[typing.Union[str, bytes]]
445464
:param env: Defines the environment variables for the new process.
446465
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
466+
:param env_patch: Defines the environment variables to ADD for the new process.
467+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
447468
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
448469
:type exception_class: typing.Type[exceptions.CalledProcessError]
449470
:param kwargs: additional parameters for call.
@@ -470,6 +491,7 @@ def check_call( # pylint: disable=arguments-differ
470491
open_stderr=open_stderr,
471492
cwd=cwd,
472493
env=env,
494+
env_patch=env_patch,
473495
exception_class=exception_class,
474496
**kwargs,
475497
)
@@ -489,6 +511,7 @@ def check_stderr( # pylint: disable=arguments-differ
489511
open_stderr: bool = True,
490512
cwd: typing.Optional[typing.Union[str, bytes]] = None,
491513
env: _EnvT = None,
514+
env_patch: _EnvT = None,
492515
exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError,
493516
**kwargs: typing.Any,
494517
) -> exec_result.ExecResult:
@@ -519,6 +542,8 @@ def check_stderr( # pylint: disable=arguments-differ
519542
:type cwd: typing.Optional[typing.Union[str, bytes]]
520543
:param env: Defines the environment variables for the new process.
521544
:type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
545+
:param env_patch: Defines the environment variables to ADD for the new process.
546+
:type env_patch: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]]
522547
:param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
523548
:type exception_class: typing.Type[exceptions.CalledProcessError]
524549
:param kwargs: additional parameters for call.
@@ -545,6 +570,7 @@ def check_stderr( # pylint: disable=arguments-differ
545570
open_stderr=open_stderr,
546571
cwd=cwd,
547572
env=env,
573+
env_patch=env_patch,
548574
exception_class=exception_class,
549575
**kwargs,
550576
)

0 commit comments

Comments
 (0)