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

python-useful-helpers / exec-helpers / 6879920120

15 Nov 2023 04:10PM CUT coverage: 55.86%. Remained the same
6879920120

push

github

penguinolog
Fix README.rst: drop old versions from readme

399 of 706 branches covered (0.0%)

Branch coverage included in aggregate %.

1088 of 1956 relevant lines covered (55.62%)

5.56 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 logging
×
25
import pathlib
×
26
import typing
×
27

28
from exec_helpers import api
×
29
from exec_helpers import constants
×
30
from exec_helpers import exceptions
×
31
from exec_helpers import proc_enums
×
32
from exec_helpers.api import CalledProcessErrorSubClassT
×
33
from exec_helpers.api import ChRootPathSetT
×
34
from exec_helpers.api import CommandT
×
35
from exec_helpers.api import ErrorInfoT
×
36
from exec_helpers.api import ExpectedExitCodesT
×
37
from exec_helpers.api import LogMaskReT
×
38
from exec_helpers.api import OptionalStdinT
×
39
from exec_helpers.api import OptionalTimeoutT
×
40

41
from .. import _helpers
×
42

43
if typing.TYPE_CHECKING:
44
    import types
45
    from collections.abc import Sequence
46

47
    from typing_extensions import Self
48

49
    from exec_helpers.async_api import exec_result
50
    from exec_helpers.proc_enums import ExitCodeT
51

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

64

65
class ExecuteContext(typing.AsyncContextManager[api.ExecuteAsyncResult], abc.ABC):
×
66
    """Execute context manager."""
67

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

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

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

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

113
        :return: logger
114
        :rtype: logging.Logger
115
        """
116
        return self.__logger
×
117

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

122
        :return: command as string
123
        :rtype: str
124
        """
125
        return self.__command
×
126

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

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

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

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

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

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

154

155
# noinspection PyProtectedMember
156
class _ChRootContext(typing.AsyncContextManager[None]):
×
157
    """Async extension for chroot.
158

159
    :param conn: connection instance
160
    :type conn: ExecHelper
161
    :param path: chroot path or None for no chroot
162
    :type path: str | pathlib.Path | None
163
    :raises TypeError: incorrect type of path variable
164
    """
165

166
    def __init__(self, conn: ExecHelper, path: ChRootPathSetT = None) -> None:
×
167
        """Context manager for call commands with sudo.
168

169
        :raises TypeError: incorrect type of path variable
170
        """
171
        self._conn: ExecHelper = conn
×
172
        self._chroot_status: str | None = conn._chroot_path
×
173
        if path is None or isinstance(path, str):
×
174
            self._path: str | None = path
×
175
        elif isinstance(path, pathlib.Path):
×
176
            self._path = path.as_posix()  # get absolute path
×
177
        else:
178
            raise TypeError(f"path={path!r} is not instance of {ChRootPathSetT}")
×
179

180
    async def __aenter__(self) -> None:
×
181
        await self._conn.__aenter__()
×
182
        self._chroot_status = self._conn._chroot_path
×
183
        self._conn._chroot_path = self._path
×
184

185
    async def __aexit__(
×
186
        self,
187
        exc_type: type[BaseException] | None,
188
        exc_val: BaseException | None,
189
        exc_tb: types.TracebackType | None,
190
    ) -> None:
191
        self._conn._chroot_path = self._chroot_status
×
192
        await self._conn.__aexit__(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)
×
193

194

195
class ExecHelper(
×
196
    typing.Callable[..., typing.Awaitable["ExecHelper"]],  # type: ignore[misc]
197
    typing.AsyncContextManager["ExecHelper"],
198
    abc.ABC,
199
):
200
    """Subprocess helper with timeouts and lock-free FIFO.
201

202
    :param logger: logger instance to use
203
    :type logger: logging.Logger
204
    :param log_mask_re: regex lookup rule to mask command for logger.
205
                        all MATCHED groups will be replaced by '<*masked*>'
206
    :type log_mask_re: str | re.Pattern[str] | None
207
    """
208

209
    __slots__ = ("__alock", "__logger", "log_mask_re", "__chroot_path")
×
210

211
    def __init__(self, log_mask_re: LogMaskReT = None, *, logger: logging.Logger) -> None:
×
212
        """Subprocess helper with timeouts and lock-free FIFO."""
213
        self.__alock: asyncio.Lock | None = None
×
214
        self.__logger: logging.Logger = logger
×
215
        self.log_mask_re: LogMaskReT = log_mask_re
×
216
        self.__chroot_path: str | None = None
×
217

218
    @property
×
219
    def logger(self) -> logging.Logger:
×
220
        """Instance logger access.
221

222
        :return: logger instance
223
        :rtype: logging.Logger
224
        """
225
        return self.__logger
×
226

227
    @property
×
228
    def _chroot_path(self) -> str | None:
×
229
        """Path for chroot if set.
230

231
        :rtype: str | None
232
        .. versionadded:: 4.1.0
233
        """
234
        return self.__chroot_path
×
235

236
    @_chroot_path.setter
×
237
    def _chroot_path(self, new_state: ChRootPathSetT) -> None:
×
238
        """Path for chroot if set.
239

240
        :param new_state: new path
241
        :type new_state: str | None
242
        :raises TypeError: Not supported path information
243
        .. versionadded:: 4.1.0
244
        """
245
        if new_state is None or isinstance(new_state, str):
×
246
            self.__chroot_path = new_state
×
247
        elif isinstance(new_state, pathlib.Path):
×
248
            self.__chroot_path = new_state.as_posix()
×
249
        else:
250
            raise TypeError(f"chroot_path is expected to be string, but set {new_state!r}")
×
251

252
    @_chroot_path.deleter
×
253
    def _chroot_path(self) -> None:
×
254
        """Remove Path for chroot.
255

256
        .. versionadded:: 4.1.0
257
        """
258
        self.__chroot_path = None
×
259

260
    def chroot(self, path: ChRootPathSetT) -> _ChRootContext:
×
261
        """Context manager for changing chroot rules.
262

263
        :param path: chroot path or none for working without chroot.
264
        :type path: str | pathlib.Path | None
265
        :return: context manager with selected chroot state inside
266
        :rtype: typing.ContextManager
267

268
        .. Note:: Enter and exit main context manager is produced as well.
269
        .. versionadded:: 4.1.0
270
        """
271
        return _ChRootContext(conn=self, path=path)
×
272

273
    async def __aenter__(self) -> Self:
×
274
        """Async context manager.
275

276
        :return: exec helper instance with async entered context manager
277
        :rtype: ExecHelper
278
        """
279
        if self.__alock is None:
×
280
            self.__alock = asyncio.Lock()
×
281
        await self.__alock.acquire()
×
282
        return self
×
283

284
    async def __aexit__(
×
285
        self,
286
        exc_type: type[BaseException] | None,
287
        exc_val: BaseException | None,
288
        exc_tb: types.TracebackType | None,
289
    ) -> None:
290
        """Async context manager."""
291
        if self.__alock is not None:
×
292
            self.__alock.release()
×
293

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

297
        :param cmd: command
298
        :type cmd: str
299
        :param log_mask_re: regex lookup rule to mask command for logger.
300
                            all MATCHED groups will be replaced by '<*masked*>'
301
        :type log_mask_re: str | re.Pattern[str] | None
302
        :return: masked command
303
        :rtype: str
304

305
        .. versionadded:: 1.2.0
306
        """
307

308
        return _helpers.mask_command(cmd.rstrip(), self.log_mask_re, log_mask_re)
×
309

310
    def _prepare_command(self, cmd: str, chroot_path: str | None = None) -> str:
×
311
        """Prepare command: cower chroot and other cases.
312

313
        :param cmd: main command
314
        :type cmd: str
315
        :param chroot_path: path to make chroot for execution
316
        :type chroot_path: str | None
317
        :return: final command, includes chroot, if required
318
        :rtype: str
319
        """
320
        return _helpers.chroot_command(cmd, chroot_path=chroot_path or self._chroot_path)
×
321

322
    @abc.abstractmethod
×
323
    async def _exec_command(
×
324
        self,
325
        command: str,
326
        async_result: api.ExecuteAsyncResult,
327
        timeout: OptionalTimeoutT,
328
        *,
329
        verbose: bool = False,
330
        log_mask_re: LogMaskReT = None,
331
        stdin: OptionalStdinT = None,
332
        log_stdout: bool = True,
333
        log_stderr: bool = True,
334
        **kwargs: typing.Any,
335
    ) -> exec_result.ExecResult:
336
        """Get exit status from channel with timeout.
337

338
        :param command: Command for execution
339
        :type command: str
340
        :param async_result: execute_async result
341
        :type async_result: ExecuteAsyncResult
342
        :param timeout: Timeout for command execution
343
        :type timeout: int | float | None
344
        :param verbose: produce verbose log record on command call
345
        :type verbose: bool
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
        :param stdin: pass STDIN text to the process
350
        :type stdin: bytes | str | bytearray | None
351
        :param log_stdout: log STDOUT during read
352
        :type log_stdout: bool
353
        :param log_stderr: log STDERR during read
354
        :type log_stderr: bool
355
        :param kwargs: additional parameters for call.
356
        :type kwargs: typing.Any
357
        :return: Execution result
358
        :rtype: ExecResult
359
        :raises OSError: exception during process kill (and not regarding already closed process)
360
        :raises ExecHelperTimeoutError: Timeout exceeded
361
        """
362

363
    def _log_command_execute(
×
364
        self,
365
        command: str,
366
        log_mask_re: LogMaskReT,
367
        log_level: int,
368
        chroot_path: str | None = None,
369
        **_: typing.Any,
370
    ) -> None:
371
        """Log command execution."""
372
        cmd_for_log: str = self._mask_command(cmd=command, log_mask_re=log_mask_re)
×
373
        target_path: str | None = chroot_path if chroot_path is not None else self._chroot_path
×
374
        chroot_info: str = "" if not target_path or target_path == "/" else f" (with chroot to: {target_path!r})"
×
375

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

378
    @abc.abstractmethod
×
379
    def open_execute_context(
×
380
        self,
381
        command: str,
382
        *,
383
        stdin: OptionalStdinT = None,
384
        open_stdout: bool = True,
385
        open_stderr: bool = True,
386
        chroot_path: str | None = None,
387
        **kwargs: typing.Any,
388
    ) -> ExecuteContext:
389
        """Get execution context manager.
390

391
        :param command: Command for execution
392
        :type command: str | Iterable[str]
393
        :param stdin: pass STDIN text to the process
394
        :type stdin: bytes | str | bytearray | None
395
        :param open_stdout: open STDOUT stream for read
396
        :type open_stdout: bool
397
        :param open_stderr: open STDERR stream for read
398
        :type open_stderr: bool
399
        :param chroot_path: chroot path override
400
        :type chroot_path: str | None
401
        :param kwargs: additional parameters for call.
402
        :type kwargs: typing.Any
403
        .. versionadded:: 8.0.0
404
        """
405

406
    async def execute(
×
407
        self,
408
        command: CommandT,
409
        verbose: bool = False,
410
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
411
        *,
412
        log_mask_re: LogMaskReT = None,
413
        stdin: OptionalStdinT = None,
414
        open_stdout: bool = True,
415
        log_stdout: bool = True,
416
        open_stderr: bool = True,
417
        log_stderr: bool = True,
418
        chroot_path: str | None = None,
419
        **kwargs: typing.Any,
420
    ) -> exec_result.ExecResult:
421
        """Execute command and wait for return code.
422

423
        :param command: Command for execution
424
        :type command: str | Iterable[str]
425
        :param verbose: Produce log.info records for command call and output
426
        :type verbose: bool
427
        :param timeout: Timeout for command execution.
428
        :type timeout: int | float | None
429
        :param log_mask_re: regex lookup rule to mask command for logger.
430
                            all MATCHED groups will be replaced by '<*masked*>'
431
        :type log_mask_re: str | re.Pattern[str] | None
432
        :param stdin: pass STDIN text to the process
433
        :type stdin: bytes | str | bytearray | None
434
        :param open_stdout: open STDOUT stream for read
435
        :type open_stdout: bool
436
        :param log_stdout: log STDOUT during read
437
        :type log_stdout: bool
438
        :param open_stderr: open STDERR stream for read
439
        :type open_stderr: bool
440
        :param log_stderr: log STDERR during read
441
        :type log_stderr: bool
442
        :param chroot_path: chroot path override
443
        :type chroot_path: str | None
444
        :param kwargs: additional parameters for call.
445
        :type kwargs: typing.Any
446
        :return: Execution result
447
        :rtype: ExecResult
448
        :raises ExecHelperTimeoutError: Timeout exceeded
449

450
        .. versionchanged:: 7.0.0 Allow command as list of arguments. Command will be joined with components escaping.
451
        .. versionchanged:: 8.0.0 chroot path exposed.
452
        """
453
        log_level: int = logging.INFO if verbose else logging.DEBUG
×
454
        cmd = _helpers.cmd_to_string(command)
×
455
        self._log_command_execute(
×
456
            command=cmd,
457
            log_mask_re=log_mask_re,
458
            log_level=log_level,
459
            chroot_path=chroot_path,
460
            **kwargs,
461
        )
462
        async with self.open_execute_context(
×
463
            cmd,
464
            stdin=stdin,
465
            open_stdout=open_stdout,
466
            open_stderr=open_stderr,
467
            chroot_path=chroot_path,
468
            **kwargs,
469
        ) as async_result:
470
            result: exec_result.ExecResult = await self._exec_command(
×
471
                command=cmd,
472
                async_result=async_result,
473
                timeout=timeout,
474
                verbose=verbose,
475
                log_mask_re=log_mask_re,
476
                stdin=stdin,
477
                log_stdout=log_stdout,
478
                log_stderr=log_stderr,
479
                **kwargs,
480
            )
481
        self.logger.log(level=log_level, msg=f"Command {result.cmd!r} exit code: {result.exit_code!s}")
×
482
        return result
×
483

484
    async def __call__(  # pylint: disable=invalid-overridden-method,arguments-differ
×
485
        self,
486
        command: CommandT,
487
        verbose: bool = False,
488
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
489
        *,
490
        log_mask_re: LogMaskReT = None,
491
        stdin: OptionalStdinT = None,
492
        open_stdout: bool = True,
493
        log_stdout: bool = True,
494
        open_stderr: bool = True,
495
        log_stderr: bool = True,
496
        chroot_path: str | None = None,
497
        **kwargs: typing.Any,
498
    ) -> exec_result.ExecResult:
499
        """Execute command and wait for return code.
500

501
        :param command: Command for execution
502
        :type command: str | Iterable[str]
503
        :param verbose: Produce log.info records for command call and output
504
        :type verbose: bool
505
        :param timeout: Timeout for command execution.
506
        :type timeout: int | float | None
507
        :param log_mask_re: regex lookup rule to mask command for logger.
508
                            all MATCHED groups will be replaced by '<*masked*>'
509
        :type log_mask_re: str | re.Pattern[str] | None
510
        :param stdin: pass STDIN text to the process
511
        :type stdin: bytes | str | bytearray | None
512
        :param open_stdout: open STDOUT stream for read
513
        :type open_stdout: bool
514
        :param log_stdout: log STDOUT during read
515
        :type log_stdout: bool
516
        :param open_stderr: open STDERR stream for read
517
        :type open_stderr: bool
518
        :param log_stderr: log STDERR during read
519
        :type log_stderr: bool
520
        :param chroot_path: chroot path override
521
        :type chroot_path: str | None
522
        :param kwargs: additional parameters for call.
523
        :type kwargs: typing.Any
524
        :return: Execution result
525
        :rtype: ExecResult
526
        :raises ExecHelperTimeoutError: Timeout exceeded
527

528
        .. versionadded:: 3.3.0
529
        """
530
        return await self.execute(
×
531
            command=command,
532
            verbose=verbose,
533
            timeout=timeout,
534
            log_mask_re=log_mask_re,
535
            stdin=stdin,
536
            open_stdout=open_stdout,
537
            log_stdout=log_stdout,
538
            open_stderr=open_stderr,
539
            log_stderr=log_stderr,
540
            chroot_path=chroot_path,
541
            **kwargs,
542
        )
543

544
    async def check_call(
×
545
        self,
546
        command: CommandT,
547
        verbose: bool = False,
548
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
549
        error_info: ErrorInfoT = None,
550
        expected: ExpectedExitCodesT = (proc_enums.EXPECTED,),
551
        raise_on_err: bool = True,
552
        *,
553
        log_mask_re: LogMaskReT = None,
554
        stdin: OptionalStdinT = None,
555
        open_stdout: bool = True,
556
        log_stdout: bool = True,
557
        open_stderr: bool = True,
558
        log_stderr: bool = True,
559
        exception_class: CalledProcessErrorSubClassT = exceptions.CalledProcessError,
560
        **kwargs: typing.Any,
561
    ) -> exec_result.ExecResult:
562
        """Execute command and check for return code.
563

564
        :param command: Command for execution
565
        :type command: str | Iterable[str]
566
        :param verbose: Produce log.info records for command call and output
567
        :type verbose: bool
568
        :param timeout: Timeout for command execution.
569
        :type timeout: int | float | None
570
        :param error_info: Text for error details, if fail happens
571
        :type error_info: str | None
572
        :param expected: expected return codes (0 by default)
573
        :type expected: Iterable[int | proc_enums.ExitCodes]
574
        :param raise_on_err: Raise exception on unexpected return code
575
        :type raise_on_err: bool
576
        :param log_mask_re: regex lookup rule to mask command for logger.
577
                            all MATCHED groups will be replaced by '<*masked*>'
578
        :type log_mask_re: str | re.Pattern[str] | None
579
        :param stdin: pass STDIN text to the process
580
        :type stdin: bytes | str | bytearray | None
581
        :param open_stdout: open STDOUT stream for read
582
        :type open_stdout: bool
583
        :param log_stdout: log STDOUT during read
584
        :type log_stdout: bool
585
        :param open_stderr: open STDERR stream for read
586
        :type open_stderr: bool
587
        :param log_stderr: log STDERR during read
588
        :type log_stderr: bool
589
        :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
590
        :type exception_class: type[exceptions.CalledProcessError]
591
        :param kwargs: additional parameters for call.
592
        :type kwargs: typing.Any
593
        :return: Execution result
594
        :rtype: ExecResult
595
        :raises ExecHelperTimeoutError: Timeout exceeded
596
        :raises CalledProcessError: Unexpected exit code
597

598
        .. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
599
        """
600
        expected_codes: Sequence[ExitCodeT] = proc_enums.exit_codes_to_enums(expected)
×
601
        result: exec_result.ExecResult = await self.execute(
×
602
            command,
603
            verbose=verbose,
604
            timeout=timeout,
605
            log_mask_re=log_mask_re,
606
            stdin=stdin,
607
            open_stdout=open_stdout,
608
            log_stdout=log_stdout,
609
            open_stderr=open_stderr,
610
            log_stderr=log_stderr,
611
            **kwargs,
612
        )
613
        return self._handle_exit_code(
×
614
            result=result,
615
            error_info=error_info,
616
            expected_codes=expected_codes,
617
            raise_on_err=raise_on_err,
618
            exception_class=exception_class,
619
        )
620

621
    def _handle_exit_code(
×
622
        self,
623
        *,
624
        result: exec_result.ExecResult,
625
        error_info: ErrorInfoT,
626
        expected_codes: ExpectedExitCodesT,
627
        raise_on_err: bool,
628
        exception_class: CalledProcessErrorSubClassT,
629
    ) -> exec_result.ExecResult:
630
        """Internal check_call logic (synchronous).
631

632
        :param result: execution result for validation
633
        :type result: exec_result.ExecResult
634
        :param error_info: optional additional error information
635
        :type error_info: str | None
636
        :param raise_on_err: raise `exception_class` in case of error
637
        :type raise_on_err: bool
638
        :param expected_codes: iterable expected exit codes
639
        :type expected_codes: Iterable[int | ExitCodes]
640
        :param exception_class: exception class for usage in case of errors (subclass of CalledProcessError)
641
        :type exception_class: type[exceptions.CalledProcessError]
642
        :return: execution result
643
        :rtype: exec_result.ExecResult
644
        :raises exceptions.CalledProcessError: stderr presents and raise_on_err enabled
645
        """
646
        append: str = error_info + "\n" if error_info else ""
×
647
        if result.exit_code not in expected_codes:
×
648
            message = (
×
649
                f"{append}Command {result.cmd!r} returned exit code {result.exit_code!s} "
650
                f"while expected {expected_codes!s}"
651
            )
652
            self.logger.error(msg=message)
×
653
            if raise_on_err:
×
654
                raise exception_class(result=result, expected=expected_codes)
×
655
        return result
×
656

657
    async def check_stderr(
×
658
        self,
659
        command: CommandT,
660
        verbose: bool = False,
661
        timeout: OptionalTimeoutT = constants.DEFAULT_TIMEOUT,
662
        error_info: ErrorInfoT = None,
663
        raise_on_err: bool = True,
664
        *,
665
        expected: ExpectedExitCodesT = (proc_enums.EXPECTED,),
666
        log_mask_re: LogMaskReT = None,
667
        stdin: OptionalStdinT = None,
668
        open_stdout: bool = True,
669
        log_stdout: bool = True,
670
        open_stderr: bool = True,
671
        log_stderr: bool = True,
672
        exception_class: CalledProcessErrorSubClassT = exceptions.CalledProcessError,
673
        **kwargs: typing.Any,
674
    ) -> exec_result.ExecResult:
675
        """Execute command expecting return code 0 and empty STDERR.
676

677
        :param command: Command for execution
678
        :type command: str | Iterable[str]
679
        :param verbose: Produce log.info records for command call and output
680
        :type verbose: bool
681
        :param timeout: Timeout for command execution.
682
        :type timeout: int | float | None
683
        :param error_info: Text for error details, if fail happens
684
        :type error_info: str | None
685
        :param raise_on_err: Raise exception on unexpected return code
686
        :type raise_on_err: bool
687
        :param expected: expected return codes (0 by default)
688
        :type expected: Iterable[int | proc_enums.ExitCodes]
689
        :param log_mask_re: regex lookup rule to mask command for logger.
690
                            all MATCHED groups will be replaced by '<*masked*>'
691
        :type log_mask_re: str | re.Pattern[str] | None
692
        :param stdin: pass STDIN text to the process
693
        :type stdin: bytes | str | bytearray | None
694
        :param open_stdout: open STDOUT stream for read
695
        :type open_stdout: bool
696
        :param log_stdout: log STDOUT during read
697
        :type log_stdout: bool
698
        :param open_stderr: open STDERR stream for read
699
        :type open_stderr: bool
700
        :param log_stderr: log STDERR during read
701
        :type log_stderr: bool
702
        :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory.
703
        :type exception_class: type[exceptions.CalledProcessError]
704
        :param kwargs: additional parameters for call.
705
        :type kwargs: typing.Any
706
        :return: Execution result
707
        :rtype: ExecResult
708
        :raises ExecHelperTimeoutError: Timeout exceeded
709
        :raises CalledProcessError: Unexpected exit code or stderr presents
710

711
        .. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
712
        """
713
        result: exec_result.ExecResult = await self.check_call(
×
714
            command,
715
            verbose=verbose,
716
            timeout=timeout,
717
            error_info=error_info,
718
            raise_on_err=raise_on_err,
719
            expected=expected,
720
            exception_class=exception_class,
721
            log_mask_re=log_mask_re,
722
            stdin=stdin,
723
            open_stdout=open_stdout,
724
            log_stdout=log_stdout,
725
            open_stderr=open_stderr,
726
            log_stderr=log_stderr,
727
            **kwargs,
728
        )
729
        return self._handle_stderr(
×
730
            result=result,
731
            error_info=error_info,
732
            raise_on_err=raise_on_err,
733
            expected=expected,
734
            exception_class=exception_class,
735
        )
736

737
    def _handle_stderr(
×
738
        self,
739
        *,
740
        result: exec_result.ExecResult,
741
        error_info: ErrorInfoT,
742
        raise_on_err: bool,
743
        expected: ExpectedExitCodesT,
744
        exception_class: CalledProcessErrorSubClassT,
745
    ) -> exec_result.ExecResult:
746
        """Internal check_stderr logic (synchronous).
747

748
        :param result: execution result for validation
749
        :type result: exec_result.ExecResult
750
        :param error_info: optional additional error information
751
        :type error_info: str | None
752
        :param raise_on_err: raise `exception_class` in case of error
753
        :type raise_on_err: bool
754
        :param expected: iterable expected exit codes
755
        :type expected: Iterable[int | ExitCodes]
756
        :param exception_class: exception class for usage in case of errors (subclass of CalledProcessError)
757
        :type exception_class: type[exceptions.CalledProcessError]
758
        :return: execution result
759
        :rtype: exec_result.ExecResult
760
        :raises exceptions.CalledProcessError: stderr presents and raise_on_err enabled
761
        """
762
        append: str = error_info + "\n" if error_info else ""
×
763
        if result.stderr:
×
764
            message = (
×
765
                f"{append}Command {result.cmd!r} output contains STDERR while not expected\n"
766
                f"\texit code: {result.exit_code!s}"
767
            )
768
            self.logger.error(msg=message)
×
769
            if raise_on_err:
×
770
                raise exception_class(result=result, expected=expected)
×
771
        return result
×
772

773
    @staticmethod
×
774
    def _string_bytes_bytearray_as_bytes(src: str | bytes | bytearray) -> bytes:
×
775
        """Get bytes string from string/bytes/bytearray union.
776

777
        :param src: source string or bytes-like object
778
        :return: Byte string
779
        :rtype: bytes
780
        :raises TypeError: unexpected source type.
781
        """
782
        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