• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

python-useful-helpers / exec-helpers / 26171872549

20 May 2026 03:15PM UTC coverage: 53.557% (-0.9%) from 54.425%
26171872549

push

github

penguinolog
Adjust github CI

95 of 198 branches covered (47.98%)

Branch coverage included in aggregate %.

959 of 1770 relevant lines covered (54.18%)

4.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/exec_helpers/async_api/api.py
1
#    Copyright 2018 - 2023 Aleksei Stepanov aka penguinolog.
2

3
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
#    not use this file except in compliance with the License. You may obtain
5
#    a copy of the License at
6

7
#         http://www.apache.org/licenses/LICENSE-2.0
8

9
#    Unless required by applicable law or agreed to in writing, software
10
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
#    License for the specific language governing permissions and limitations
13
#    under the License.
14

15
"""Async API.
16

17
.. versionadded:: 3.0.0
18
"""
19

20
from __future__ import annotations
21

22
import abc
23
import asyncio
24
import contextlib
25
import logging
26
import pathlib
27
import typing
28
from collections.abc import Awaitable
29
from collections.abc import Callable
30

31
from exec_helpers import api
32
from exec_helpers import constants
33
from exec_helpers import exceptions
34
from exec_helpers import proc_enums
35
from exec_helpers.api import CalledProcessErrorSubClassT
36
from exec_helpers.api import ChRootPathSetT
37
from exec_helpers.api import CommandT
38
from exec_helpers.api import ErrorInfoT
39
from exec_helpers.api import ExpectedExitCodesT
40
from exec_helpers.api import LogMaskReT
41
from exec_helpers.api import OptionalStdinT
42
from exec_helpers.api import OptionalTimeoutT
43

44
from .. import _helpers
45

46
if typing.TYPE_CHECKING:
47
    import types
48
    from collections.abc import Sequence
49
    from typing import Self
50

51
    from exec_helpers.async_api import exec_result
52
    from exec_helpers.proc_enums import ExitCodeT
53

54
__all__ = (
55
    "CalledProcessErrorSubClassT",
56
    "ChRootPathSetT",
57
    "CommandT",
58
    "ErrorInfoT",
59
    "ExecHelper",
60
    "ExpectedExitCodesT",
61
    "LogMaskReT",
62
    "OptionalStdinT",
63
    "OptionalTimeoutT",
64
)
65

66

67
class ExecuteContext(contextlib.AbstractAsyncContextManager[api.ExecuteAsyncResult], abc.ABC):
×
68
    """Execute context manager."""
69

70
    __slots__ = (
×
71
        "__command",
72
        "__logger",
73
        "__open_stderr",
74
        "__open_stdout",
75
        "__stdin",
76
    )
77

78
    def __init__(
×
79
        self,
80
        *,
81
        command: str,
82
        stdin: bytes | None = None,
83
        open_stdout: bool = True,
84
        open_stderr: bool = True,
85
        logger: logging.Logger,
86
        **kwargs: typing.Any,
87
    ) -> None:
88
        """Execute async context manager.
89

90
        :param command: Command for execution (fully formatted).
91
        :type command: str
92
        :param stdin: Pass STDIN text to the process (fully formatted).
93
        :type stdin: bytes
94
        :param open_stdout: Open STDOUT stream for read.
95
        :type open_stdout: bool
96
        :param open_stderr: Open STDERR stream for read.
97
        :type open_stderr: bool
98
        :param logger: Logger instance.
99
        :type logger: logging.Logger
100
        :param kwargs: Additional parameters for call.
101
        :type kwargs: typing.Any
102
        """
103
        self.__command = command
×
104
        self.__stdin = stdin
×
105
        self.__open_stdout = open_stdout
×
106
        self.__open_stderr = open_stderr
×
107
        self.__logger = logger
×
108
        if kwargs:
×
109
            self.__logger.warning(f"Unexpected arguments: {kwargs!r}.", stack_info=True)
×
110

111
    @property
×
112
    def logger(self) -> logging.Logger:
×
113
        """Instance logger.
114

115
        :return: Logger.
116
        :rtype: logging.Logger
117
        """
118
        return self.__logger
×
119

120
    @property
×
121
    def command(self) -> str:
×
122
        """Command for execution (fully formatted).
123

124
        :return: Command as string.
125
        :rtype: str
126
        """
127
        return self.__command
×
128

129
    @property
×
130
    def stdin(self) -> bytes | None:
×
131
        """Pass STDIN text to the process (fully formatted).
132

133
        :return: pass STDIN text to the process
134
        :rtype: str | None
135
        """
136
        return self.__stdin
×
137

138
    @property
×
139
    def open_stdout(self) -> bool:
×
140
        """Open STDOUT stream for read.
141

142
        :return: Open STDOUT for handling.
143
        :rtype: bool
144
        """
145
        return self.__open_stdout
×
146

147
    @property
×
148
    def open_stderr(self) -> bool:
×
149
        """Open STDERR stream for read.
150

151
        :return: Open STDERR for handling.
152
        :rtype: bool
153
        """
154
        return self.__open_stderr
×
155

156

157
# noinspection PyProtectedMember
158
class _ChRootContext(contextlib.AbstractAsyncContextManager[None]):
×
159
    """Async extension for chroot.
160

161
    :param conn: Connection instance.
162
    :type conn: ExecHelper
163
    :param path: chroot path or None for no chroot.
164
    :type path: str | pathlib.Path | None
165
    :param chroot_exe: chroot executable.
166
    :type chroot_exe: str | None
167
    :raises TypeError: incorrect type of path or chroot_exe variable.
168
    """
169

170
    def __init__(self, conn: ExecHelper, path: ChRootPathSetT = None, chroot_exe: str | None = None) -> None:
×
171
        """Context manager for call commands with sudo.
172

173
        :raises TypeError: incorrect type of path or chroot_exe variable
174
        """
175
        self._conn: ExecHelper = conn
×
176
        self._chroot_status: str | None = conn._chroot_path
×
177
        self._chroot_exe_status: str | None = conn._chroot_exe
×
178
        if path is None or isinstance(path, str):
×
179
            self._path: str | None = path
×
180
        elif isinstance(path, pathlib.Path):
×
181
            self._path = path.as_posix()  # get an absolute path
×
182
        else:
183
            raise TypeError(f"path={path!r} is not instance of {ChRootPathSetT}")
×
184
        if chroot_exe is None or isinstance(chroot_exe, str):
×
185
            self._exe: str | None = chroot_exe
×
186
        else:
187
            raise TypeError(f"chroot_exe={chroot_exe!r} is not None or instance of str")
×
188

189
    async def __aenter__(self) -> None:
×
190
        await self._conn.__aenter__()
×
191
        self._chroot_status = self._conn._chroot_path
×
192
        self._conn._chroot_path = self._path
×
193
        self._chroot_exe_status = self._conn._chroot_exe
×
194
        self._conn._chroot_exe = self._exe
×
195

196
    async def __aexit__(
×
197
        self,
198
        exc_type: type[BaseException] | None,
199
        exc_val: BaseException | None,
200
        exc_tb: types.TracebackType | None,
201
    ) -> None:
202
        self._conn._chroot_path = self._chroot_status
×
203
        self._conn._chroot_exe = self._chroot_exe_status
×
204
        await self._conn.__aexit__(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)
×
205

206

207
class ExecHelper(
×
208
    Callable[..., Awaitable["ExecHelper"]],  # type: ignore[misc]
209
    contextlib.AbstractAsyncContextManager["ExecHelper"],
210
    abc.ABC,
211
):
212
    """Subprocess helper with timeouts and lock-free FIFO.
213

214
    :param logger: Logger instance to use.
215
    :type logger: logging.Logger
216
    :param log_mask_re: Regex lookup rule to mask command for logger.
217
                        All MATCHED groups will be replaced by '<*masked*>'.
218
    :type log_mask_re: str | re.Pattern[str] | None
219
    """
220

221
    __slots__ = ("__alock", "__chroot_exe", "__chroot_path", "__logger", "log_mask_re")
×
222

223
    def __init__(self, log_mask_re: LogMaskReT = None, *, logger: logging.Logger) -> None:
×
224
        """Subprocess helper with timeouts and lock-free FIFO."""
225
        self.__alock: asyncio.Lock | None = None
×
226
        self.__logger: logging.Logger = logger
×
227
        self.log_mask_re: LogMaskReT = log_mask_re
×
228
        self.__chroot_path: str | None = None
×
229
        self.__chroot_exe: str | None = None
×
230

231
    @property
×
232
    def logger(self) -> logging.Logger:
×
233
        """Instance logger access.
234

235
        :return: Logger instance.
236
        :rtype: logging.Logger
237
        """
238
        return self.__logger
×
239

240
    @property
×
241
    def _chroot_path(self) -> str | None:
×
242
        """Path for chroot if set.
243

244
        :rtype: str | None
245
        .. versionadded:: 4.1.0
246
        """
247
        return self.__chroot_path
×
248

249
    @_chroot_path.setter
×
250
    def _chroot_path(self, new_state: ChRootPathSetT) -> None:
×
251
        """Path for chroot if set.
252

253
        :param new_state: New path.
254
        :type new_state: str | None
255
        :raises TypeError: Not supported path information.
256
        .. versionadded:: 4.1.0
257
        """
258
        if new_state is None or isinstance(new_state, str):
×
259
            self.__chroot_path = new_state
×
260
        elif isinstance(new_state, pathlib.Path):
×
261
            self.__chroot_path = new_state.as_posix()
×
262
        else:
263
            raise TypeError(f"chroot_path is expected to be string, but set {new_state!r}")
×
264

265
    @_chroot_path.deleter
×
266
    def _chroot_path(self) -> None:
×
267
        """Remove Path for chroot.
268

269
        .. versionadded:: 4.1.0
270
        """
271
        self.__chroot_path = None
×
272

273
    @property
×
274
    def _chroot_exe(self) -> str | None:
×
275
        """Exe for chroot
276

277
        :rtype: str | None
278
        .. versionadded:: 8.1.0
279
        """
280
        return self.__chroot_exe
×
281

282
    @_chroot_exe.setter
×
283
    def _chroot_exe(self, new_state: str | None) -> None:
×
284
        """Executable for chroot if set.
285

286
        :param new_state: New exe.
287
        :type new_state: str | None
288
        :raises TypeError: Not supported exe information.
289
        .. versionadded:: 8.1.0
290
        """
291
        if new_state is None or isinstance(new_state, str):
×
292
            self.__chroot_exe = new_state
×
293
        else:
294
            raise TypeError(f"chroot_exe is expected to be None or string, but set {new_state!r}")
×
295

296
    @_chroot_exe.deleter
×
297
    def _chroot_exe(self) -> None:
×
298
        """Restore chroot executable.
299

300
        .. versionadded:: 8.1.0
301
        """
302
        self.__chroot_exe = None
×
303

304
    def chroot(self, path: ChRootPathSetT, chroot_exe: str | None = None) -> _ChRootContext:
×
305
        """Context manager for changing chroot rules.
306

307
        :param path: chroot path or none for working without chroot.
308
        :type path: str | pathlib.Path | None
309
        :param chroot_exe: chroot executable.
310
        :type chroot_exe: str | None
311
        :return: Context manager with selected chroot state inside.
312
        :rtype: typing.ContextManager
313

314
        .. Note:: Enter and exit main context manager is produced as well.
315
        .. versionadded:: 4.1.0
316
        """
317
        return _ChRootContext(conn=self, path=path, chroot_exe=chroot_exe)
×
318

319
    async def __aenter__(self) -> Self:
×
320
        """Async context manager.
321

322
        :return: exec helper Instance with async entered context manager.
323
        :rtype: ExecHelper
324
        """
325
        if self.__alock is None:
×
326
            self.__alock = asyncio.Lock()
×
327
        await self.__alock.acquire()
×
328
        return self
×
329

330
    async def __aexit__(
×
331
        self,
332
        exc_type: type[BaseException] | None,
333
        exc_val: BaseException | None,
334
        exc_tb: types.TracebackType | None,
335
    ) -> None:
336
        """Async context manager."""
337
        if self.__alock is not None:
×
338
            self.__alock.release()
×
339

340
    def _mask_command(self, cmd: str, log_mask_re: LogMaskReT = None) -> str:
×
341
        """Log command with masking and return parsed cmd.
342

343
        :param cmd: Command.
344
        :type cmd: str
345
        :param log_mask_re: Regex lookup rule to mask command for logger.
346
                            All MATCHED groups will be replaced by '<*masked*>'.
347
        :type log_mask_re: str | re.Pattern[str] | None
348
        :return: masked command.
349
        :rtype: str
350

351
        .. versionadded:: 1.2.0
352
        """
353

354
        return _helpers.mask_command(cmd.rstrip(), self.log_mask_re, log_mask_re)
×
355

356
    def _prepare_command(self, cmd: str, chroot_path: str | None = None, chroot_exe: str | None = None) -> str:
×
357
        """Prepare command: cower chroot and other cases.
358

359
        :param cmd: Main command.
360
        :type cmd: str
361
        :param chroot_path: Path to make chroot for execution.
362
        :type chroot_path: str | None
363
        :param chroot_exe: chroot exe override
364
        :type chroot_exe: str | None
365
        :return: Final command, includes chroot, if required.
366
        :rtype: str
367
        """
368
        return _helpers.chroot_command(
×
369
            cmd,
370
            chroot_path=chroot_path or self._chroot_path,
371
            chroot_exe=chroot_exe or self._chroot_exe,
372
        )
373

374
    @abc.abstractmethod
×
375
    async def _exec_command(
×
376
        self,
377
        command: str,
378
        async_result: api.ExecuteAsyncResult,
379
        timeout: OptionalTimeoutT,  # noqa: ASYNC109
380
        *,
381
        verbose: bool = False,
382
        log_mask_re: LogMaskReT = None,
383
        stdin: OptionalStdinT = None,
384
        log_stdout: bool = True,
385
        log_stderr: bool = True,
386
        **kwargs: typing.Any,
387
    ) -> exec_result.ExecResult:
388
        """Get exit status from channel with timeout.
389

390
        :param command: Command for execution.
391
        :type command: str
392
        :param async_result: execute_async result.
393
        :type async_result: ExecuteAsyncResult
394
        :param timeout: Timeout for command execution.
395
        :type timeout: int | float | None
396
        :param verbose: Produce verbose log record on command call.
397
        :type verbose: bool
398
        :param log_mask_re: Regex lookup rule to mask command for logger.
399
                            All MATCHED groups will be replaced by '<*masked*>'.
400
        :type log_mask_re: str | re.Pattern[str] | None
401
        :param stdin: Pass STDIN text to the process.
402
        :type stdin: bytes | str | bytearray | None
403
        :param log_stdout: Log STDOUT during read.
404
        :type log_stdout: bool
405
        :param log_stderr: Log STDERR during read.
406
        :type log_stderr: bool
407
        :param kwargs: Additional parameters for call.
408
        :type kwargs: typing.Any
409
        :return: Execution result.
410
        :rtype: ExecResult
411
        :raises OSError: Exception during process kill (and not regarding already closed process).
412
        :raises ExecHelperTimeoutError: Timeout exceeded.
413
        """
414

415
    def _log_command_execute(
×
416
        self,
417
        command: str,
418
        log_mask_re: LogMaskReT,
419
        log_level: int,
420
        chroot_path: str | None = None,
421
        **_: typing.Any,
422
    ) -> None:
423
        """Log command execution."""
424
        cmd_for_log: str = self._mask_command(cmd=command, log_mask_re=log_mask_re)
×
425
        target_path: str | None = chroot_path if chroot_path is not None else self._chroot_path
×
426
        chroot_info: str = "" if not target_path or target_path == "/" else f" (with chroot to: {target_path!r})"
×
427

428
        self.logger.log(level=log_level, msg=f"Executing command{chroot_info}:\n{cmd_for_log!r}\n")
×
429

430
    @abc.abstractmethod
×
431
    def open_execute_context(
×
432
        self,
433
        command: str,
434
        *,
435
        stdin: OptionalStdinT = None,
436
        open_stdout: bool = True,
437
        open_stderr: bool = True,
438
        chroot_path: str | None = None,
439
        chroot_exe: str | None = None,
440
        **kwargs: typing.Any,
441
    ) -> ExecuteContext:
442
        """Get execution context manager.
443

444
        :param command: Command for execution.
445
        :type command: str | Iterable[str]
446
        :param stdin: Pass STDIN text to the process.
447
        :type stdin: bytes | str | bytearray | None
448
        :param open_stdout: Open STDOUT stream for read.
449
        :type open_stdout: bool
450
        :param open_stderr: Open STDERR stream for read.
451
        :type open_stderr: bool
452
        :param chroot_path: Chroot path override.
453
        :type chroot_path: str | None
454
        :param chroot_exe: Chroot exe override.
455
        :type chroot_exe: str | None
456
        :param kwargs: Additional parameters for call.
457
        :type kwargs: typing.Any
458
        .. versionadded:: 8.0.0
459
        """
460

461
    async def execute(
×
462
        self,
463
        command: CommandT,
464
        verbose: bool = False,
465
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,  # noqa: ASYNC109
466
        *,
467
        log_mask_re: LogMaskReT = None,
468
        stdin: OptionalStdinT = None,
469
        open_stdout: bool = True,
470
        log_stdout: bool = True,
471
        open_stderr: bool = True,
472
        log_stderr: bool = True,
473
        chroot_path: str | None = None,
474
        chroot_exe: str | None = None,
475
        **kwargs: typing.Any,
476
    ) -> exec_result.ExecResult:
477
        """Execute command and wait for return code.
478

479
        :param command: Command for execution.
480
        :type command: str | Iterable[str]
481
        :param verbose: Produce log.info records for command call and output.
482
        :type verbose: bool
483
        :param timeout: Timeout for command execution.
484
        :type timeout: int | float | None
485
        :param log_mask_re: Regex lookup rule to mask command for logger.
486
                            All MATCHED groups will be replaced by '<*masked*>'.
487
        :type log_mask_re: str | re.Pattern[str] | None
488
        :param stdin: Pass STDIN text to the process.
489
        :type stdin: bytes | str | bytearray | None
490
        :param open_stdout: Open STDOUT stream for read.
491
        :type open_stdout: bool
492
        :param log_stdout: Log STDOUT during read.
493
        :type log_stdout: bool
494
        :param open_stderr: Open STDERR stream for read.
495
        :type open_stderr: bool
496
        :param log_stderr: Log STDERR during read.
497
        :type log_stderr: bool
498
        :param chroot_path: chroot path override.
499
        :type chroot_path: str | None
500
        :param chroot_exe: chroot exe override.
501
        :type chroot_exe: str | None
502
        :param kwargs: Additional parameters for call.
503
        :type kwargs: typing.Any
504
        :return: Execution result.
505
        :rtype: ExecResult
506
        :raises ExecHelperTimeoutError: Timeout exceeded.
507

508
        .. versionchanged:: 7.0.0 Allow command as list of arguments. Command will be joined with components escaping.
509
        .. versionchanged:: 8.0.0 chroot path exposed.
510
        .. versionchanged:: 8.1.0 chroot_exe added.
511
        """
512
        log_level: int = logging.INFO if verbose else logging.DEBUG
×
513
        cmd = _helpers.cmd_to_string(command)
×
514
        self._log_command_execute(
×
515
            command=cmd,
516
            log_mask_re=log_mask_re,
517
            log_level=log_level,
518
            chroot_path=chroot_path,
519
            **kwargs,
520
        )
521
        async with self.open_execute_context(
×
522
            cmd,
523
            stdin=stdin,
524
            open_stdout=open_stdout,
525
            open_stderr=open_stderr,
526
            chroot_path=chroot_path,
527
            chroot_exe=chroot_exe,
528
            **kwargs,
529
        ) as async_result:
530
            result: exec_result.ExecResult = await self._exec_command(
×
531
                command=cmd,
532
                async_result=async_result,
533
                timeout=timeout,
534
                verbose=verbose,
535
                log_mask_re=log_mask_re,
536
                stdin=stdin,
537
                log_stdout=log_stdout,
538
                log_stderr=log_stderr,
539
                **kwargs,
540
            )
541
        self.logger.log(level=log_level, msg=f"Command {result.cmd!r} exit code: {result.exit_code!s}")
×
542
        return result
×
543

544
    async def __call__(  # pylint: disable=invalid-overridden-method
×
545
        self,
546
        command: CommandT,
547
        verbose: bool = False,
548
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,  # noqa: ASYNC109
549
        *,
550
        log_mask_re: LogMaskReT = None,
551
        stdin: OptionalStdinT = None,
552
        open_stdout: bool = True,
553
        log_stdout: bool = True,
554
        open_stderr: bool = True,
555
        log_stderr: bool = True,
556
        chroot_path: str | None = None,
557
        chroot_exe: str | None = None,
558
        **kwargs: typing.Any,
559
    ) -> exec_result.ExecResult:
560
        """Execute command and wait for return code.
561

562
        :param command: Command for execution.
563
        :type command: str | Iterable[str]
564
        :param verbose: Produce log.info records for command call and output.
565
        :type verbose: bool
566
        :param timeout: Timeout for command execution.
567
        :type timeout: int | float | None
568
        :param log_mask_re: Regex lookup rule to mask command for logger.
569
                            All MATCHED groups will be replaced by '<*masked*>'.
570
        :type log_mask_re: str | re.Pattern[str] | None
571
        :param stdin: Pass STDIN text to the process.
572
        :type stdin: bytes | str | bytearray | None
573
        :param open_stdout: Open STDOUT stream for read.
574
        :type open_stdout: bool
575
        :param log_stdout: Log STDOUT during read.
576
        :type log_stdout: bool
577
        :param open_stderr: Open STDERR stream for read.
578
        :type open_stderr: bool
579
        :param log_stderr: Log STDERR during read.
580
        :type log_stderr: bool
581
        :param chroot_path: chroot path override.
582
        :type chroot_path: str | None
583
        :param chroot_exe: chroot exe override.
584
        :type chroot_exe: str | None
585
        :param kwargs: Additional parameters for call.
586
        :type kwargs: typing.Any
587
        :return: Execution result.
588
        :rtype: ExecResult
589
        :raises ExecHelperTimeoutError: Timeout exceeded.
590

591
        .. versionadded:: 3.3.0
592
        """
593
        return await self.execute(
×
594
            command=command,
595
            verbose=verbose,
596
            timeout=timeout,
597
            log_mask_re=log_mask_re,
598
            stdin=stdin,
599
            open_stdout=open_stdout,
600
            log_stdout=log_stdout,
601
            open_stderr=open_stderr,
602
            log_stderr=log_stderr,
603
            chroot_path=chroot_path,
604
            chroot_exe=chroot_exe,
605
            **kwargs,
606
        )
607

608
    async def check_call(
×
609
        self,
610
        command: CommandT,
611
        verbose: bool = False,
612
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,  # noqa: ASYNC109
613
        error_info: ErrorInfoT = None,
614
        expected: ExpectedExitCodesT = (proc_enums.EXPECTED,),
615
        raise_on_err: bool = True,
616
        *,
617
        log_mask_re: LogMaskReT = None,
618
        stdin: OptionalStdinT = None,
619
        open_stdout: bool = True,
620
        log_stdout: bool = True,
621
        open_stderr: bool = True,
622
        log_stderr: bool = True,
623
        exception_class: CalledProcessErrorSubClassT = exceptions.CalledProcessError,
624
        **kwargs: typing.Any,
625
    ) -> exec_result.ExecResult:
626
        """Execute command and check for return code.
627

628
        :param command: Command for execution.
629
        :type command: str | Iterable[str]
630
        :param verbose: Produce log.info records for command call and output.
631
        :type verbose: bool
632
        :param timeout: Timeout for command execution.
633
        :type timeout: int | float | None
634
        :param error_info: Text for error details, if fail happens.
635
        :type error_info: str | None
636
        :param expected: Expected return codes (0 by default).
637
        :type expected: Iterable[int | proc_enums.ExitCodes]
638
        :param raise_on_err: Raise exception on unexpected return code.
639
        :type raise_on_err: bool
640
        :param log_mask_re: Regex lookup rule to mask command for logger.
641
                            All MATCHED groups will be replaced by '<*masked*>'.
642
        :type log_mask_re: str | re.Pattern[str] | None
643
        :param stdin: Pass STDIN text to the process.
644
        :type stdin: bytes | str | bytearray | None
645
        :param open_stdout: Open STDOUT stream for read.
646
        :type open_stdout: bool
647
        :param log_stdout: Log STDOUT during read.
648
        :type log_stdout: bool
649
        :param open_stderr: Open STDERR stream for read.
650
        :type open_stderr: bool
651
        :param log_stderr: Log STDERR during read.
652
        :type log_stderr: bool
653
        :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
654
        :type exception_class: type[exceptions.CalledProcessError]
655
        :param kwargs: Additional parameters for call.
656
        :type kwargs: typing.Any
657
        :return: Execution result.
658
        :rtype: ExecResult
659
        :raises ExecHelperTimeoutError: Timeout exceeded.
660
        :raises CalledProcessError: Unexpected exit code.
661

662
        .. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
663
        """
664
        expected_codes: Sequence[ExitCodeT] = proc_enums.exit_codes_to_enums(expected)
×
665
        result: exec_result.ExecResult = await self.execute(
×
666
            command,
667
            verbose=verbose,
668
            timeout=timeout,
669
            log_mask_re=log_mask_re,
670
            stdin=stdin,
671
            open_stdout=open_stdout,
672
            log_stdout=log_stdout,
673
            open_stderr=open_stderr,
674
            log_stderr=log_stderr,
675
            **kwargs,
676
        )
677
        result.check_exit_code(
×
678
            expected_codes,
679
            raise_on_err,
680
            error_info=error_info,
681
            exception_class=exception_class,
682
            logger=self.logger,
683
        )
684
        return result
×
685

686
    async def check_stderr(
×
687
        self,
688
        command: CommandT,
689
        verbose: bool = False,
690
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,  # noqa: ASYNC109
691
        error_info: ErrorInfoT = None,
692
        raise_on_err: bool = True,
693
        *,
694
        expected: ExpectedExitCodesT = (proc_enums.EXPECTED,),
695
        log_mask_re: LogMaskReT = None,
696
        stdin: OptionalStdinT = None,
697
        open_stdout: bool = True,
698
        log_stdout: bool = True,
699
        open_stderr: bool = True,
700
        log_stderr: bool = True,
701
        exception_class: CalledProcessErrorSubClassT = exceptions.CalledProcessError,
702
        **kwargs: typing.Any,
703
    ) -> exec_result.ExecResult:
704
        """Execute command expecting return code 0 and empty STDERR.
705

706
        :param command: Command for execution.
707
        :type command: str | Iterable[str]
708
        :param verbose: Produce log.info records for command call and output.
709
        :type verbose: bool
710
        :param timeout: Timeout for command execution.
711
        :type timeout: int | float | None
712
        :param error_info: Text for error details, if fail happens.
713
        :type error_info: str | None
714
        :param raise_on_err: Raise exception on unexpected return code.
715
        :type raise_on_err: bool
716
        :param expected: Expected return codes (0 by default).
717
        :type expected: Iterable[int | proc_enums.ExitCodes]
718
        :param log_mask_re: Regex lookup rule to mask command for logger.
719
                            All MATCHED groups will be replaced by '<*masked*>'.
720
        :type log_mask_re: str | re.Pattern[str] | None
721
        :param stdin: Pass STDIN text to the process.
722
        :type stdin: bytes | str | bytearray | None
723
        :param open_stdout: Open STDOUT stream for read.
724
        :type open_stdout: bool
725
        :param log_stdout: Log STDOUT during read.
726
        :type log_stdout: bool
727
        :param open_stderr: Open STDERR stream for read.
728
        :type open_stderr: bool
729
        :param log_stderr: Log STDERR during read.
730
        :type log_stderr: bool
731
        :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
732
        :type exception_class: type[exceptions.CalledProcessError]
733
        :param kwargs: Additional parameters for call.
734
        :type kwargs: typing.Any
735
        :return: Execution result.
736
        :rtype: ExecResult
737
        :raises ExecHelperTimeoutError: Timeout exceeded.
738
        :raises CalledProcessError: Unexpected exit code or stderr presents.
739

740
        .. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
741
        """
742
        result: exec_result.ExecResult = await self.check_call(
×
743
            command,
744
            verbose=verbose,
745
            timeout=timeout,
746
            error_info=error_info,
747
            raise_on_err=raise_on_err,
748
            expected=expected,
749
            exception_class=exception_class,
750
            log_mask_re=log_mask_re,
751
            stdin=stdin,
752
            open_stdout=open_stdout,
753
            log_stdout=log_stdout,
754
            open_stderr=open_stderr,
755
            log_stderr=log_stderr,
756
            **kwargs,
757
        )
758
        return self._handle_stderr(
×
759
            result=result,
760
            error_info=error_info,
761
            raise_on_err=raise_on_err,
762
            expected=expected,
763
            exception_class=exception_class,
764
        )
765

766
    def _handle_stderr(
×
767
        self,
768
        *,
769
        result: exec_result.ExecResult,
770
        error_info: ErrorInfoT,
771
        raise_on_err: bool,
772
        expected: ExpectedExitCodesT,
773
        exception_class: CalledProcessErrorSubClassT,
774
    ) -> exec_result.ExecResult:
775
        """Internal check_stderr logic (synchronous).
776

777
        :param result: Execution result for validation.
778
        :type result: exec_result.ExecResult
779
        :param error_info: Optional additional error information.
780
        :type error_info: str | None
781
        :param raise_on_err: Raise `exception_class` in case of error.
782
        :type raise_on_err: bool
783
        :param expected: Iterable expected exit codes.
784
        :type expected: Iterable[int | ExitCodes]
785
        :param exception_class: Exception class for usage in case of errors (subclass of CalledProcessError).
786
        :type exception_class: type[exceptions.CalledProcessError]
787
        :return: Execution result.
788
        :rtype: exec_result.ExecResult
789
        :raises exceptions.CalledProcessError: STDERR presents and raise_on_err enabled.
790
        """
791
        append: str = error_info + "\n" if error_info else ""
×
792
        if result.stderr:
×
793
            message = (
×
794
                f"{append}Command {result.cmd!r} output contains STDERR while not expected\n"
795
                f"\texit code: {result.exit_code!s}"
796
            )
797
            self.logger.error(msg=message)
×
798
            if raise_on_err:
×
799
                raise exception_class(result=result, expected=expected)
×
800
        return result
×
801

802
    @staticmethod
×
803
    def _string_bytes_bytearray_as_bytes(src: str | bytes | bytearray) -> bytes:
×
804
        """Get bytes string from string/bytes/bytearray union.
805

806
        :param src: Source string or bytes-like object.
807
        :return: Byte string.
808
        :rtype: bytes
809
        :raises TypeError: unexpected source type.
810
        """
811
        return _helpers.string_bytes_bytearray_as_bytes(src)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc