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

python-useful-helpers / exec-helpers / 14900591534

08 May 2025 06:58AM CUT coverage: 54.224%. Remained the same
14900591534

push

github

web-flow
Bump actions/download-artifact from 4.1.8 to 4.3.0 (#159)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.8 to 4.3.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.1.8...v4.3.0)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

98 of 201 branches covered (48.76%)

Branch coverage included in aggregate %.

1083 of 1977 relevant lines covered (54.78%)

5.48 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

50
    from typing_extensions import Self
51

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

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

67

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

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

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

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

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

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

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

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

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

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

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

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

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

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

157

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

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

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

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

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

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

207

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

352
        .. versionadded:: 1.2.0
353
        """
354

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc