Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-subprocrunner for 
openSUSE:Factory checked in at 2021-06-05 23:31:47
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-subprocrunner (Old)
 and      /work/SRC/openSUSE:Factory/.python-subprocrunner.new.1898 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-subprocrunner"

Sat Jun  5 23:31:47 2021 rev:7 rq:897730 version:1.6.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-subprocrunner/python-subprocrunner.changes    
    2021-05-17 18:45:15.356638565 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-subprocrunner.new.1898/python-subprocrunner.changes
      2021-06-05 23:32:17.088516498 +0200
@@ -1,0 +2,27 @@
+Sat Jun  5 13:14:13 UTC 2021 - Martin Hauke <[email protected]>
+
+- Update to version 1.6.0
+  * Add __repr__ method to Retry/SubprocessRunner classes
+  * Fix to properly suppress debug logs when quiet=True
+  * Fixed to save command history at runtime
+
+-------------------------------------------------------------------
+Fri Jun  4 20:34:17 UTC 2021 - Martin Hauke <[email protected]>
+
+- Update to version 1.5.0
+  * Add no_retry_returncodes parameter to Retry class
+- Update to version 1.4.2
+  * Fix retry processing
+  * Modify log messages
+- Update to version 1.4.1
+  * Fix retry processing
+- Update to version 1.4.0
+  * Add quiet mode support for SubprocessRunner/Retry
+  * Modify a retry log message
+- Update to version 1.3.0
+  * Add timeout keyword argument to SubprocessRunner.run:
+  * Add support for retry functionality to SubprocessRunner.run
+  * Add SubprocessRunner.Retry class
+  * Modify type annotation of SubprocessRunner.run return value
+
+-------------------------------------------------------------------

Old:
----
  subprocrunner-1.2.2.tar.gz

New:
----
  subprocrunner-1.6.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-subprocrunner.spec ++++++
--- /var/tmp/diff_new_pack.ipK9HF/_old  2021-06-05 23:32:17.572517340 +0200
+++ /var/tmp/diff_new_pack.ipK9HF/_new  2021-06-05 23:32:17.576517347 +0200
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-subprocrunner
-Version:        1.2.2
+Version:        1.6.0
 Release:        0
 Summary:        A Python wrapper library for subprocess module
 License:        MIT
@@ -37,6 +37,7 @@
 # SECTION test requirements
 BuildRequires:  %{python_module loguru >= 0.4.1}
 BuildRequires:  %{python_module mbstrdecoder >= 1.0.0}
+BuildRequires:  %{python_module pytest-mock}
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module six}
 BuildRequires:  %{python_module typepy}

++++++ subprocrunner-1.2.2.tar.gz -> subprocrunner-1.6.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/PKG-INFO 
new/subprocrunner-1.6.0/PKG-INFO
--- old/subprocrunner-1.2.2/PKG-INFO    2021-05-05 09:10:08.561095700 +0200
+++ new/subprocrunner-1.6.0/PKG-INFO    2021-06-05 13:14:36.910000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: subprocrunner
-Version: 1.2.2
+Version: 1.6.0
 Summary: A Python wrapper library for subprocess module.
 Home-page: https://github.com/thombashi/subprocrunner
 Author: Tsuyoshi Hombashi
@@ -30,13 +30,13 @@
             :target: https://pypi.org/project/subprocrunner
             :alt: Supported Python implementations
         
-        .. image:: 
https://img.shields.io/travis/thombashi/subprocrunner/master.svg?label=Linux/macOS%20CI
-            :target: https://travis-ci.org/thombashi/subprocrunner
-            :alt: Linux/macOS CI status
-        
-        .. image:: 
https://img.shields.io/appveyor/ci/thombashi/subprocrunner/master.svg?label=Windows%20CI
-            :target: 
https://ci.appveyor.com/project/thombashi/subprocrunner/branch/master
-            :alt: Windows CI status
+        .. image:: 
https://github.com/thombashi/subprocrunner/workflows/Tests/badge.svg
+            :target: 
https://github.com/thombashi/subprocrunner/actions/workflows/tests.yml
+            :alt: Test result of Linux/macOS/Windows
+        
+        .. image:: 
https://github.com/thombashi/subprocrunner/actions/workflows/lint.yml/badge.svg
+            :target: 
https://github.com/thombashi/subprocrunner/actions/workflows/lint.yml
+            :alt: Lint result
         
         .. image:: 
https://coveralls.io/repos/github/thombashi/subprocrunner/badge.svg?branch=master
             :target: 
https://coveralls.io/github/thombashi/subprocrunner?branch=master
@@ -52,45 +52,57 @@
         
                 from subprocrunner import SubprocessRunner
         
-                runner = SubprocessRunner("echo test")
-                print("command: {:s}".format(runner.command))
-                print("return code: {:d}".format(runner.run()))
-                print("stdout: {:s}".format(runner.stdout))
-        
-                runner = SubprocessRunner("ls __not_exist_dir__")
-                print("command: {:s}".format(runner.command))
-                print("return code: {:d}".format(runner.run()))
-                print("stderr: {:s}".format(runner.stderr))
-        
+                runner = SubprocessRunner(["echo", "test"])
+                print(runner)
+                print(f"return code: {runner.run()}")
+                print(f"stdout: {runner.stdout}")
+                
+                runner = SubprocessRunner(["ls", "__not_exist_dir__"])
+                print(runner)
+                print(f"return code: {runner.run()}")
+                print(f"stderr: {runner.stderr}")
+                
         :Output:
             .. code::
         
-                command: echo test
+                SubprocessRunner(command='echo test', returncode='not yet 
executed')
                 return code: 0
                 stdout: test
-        
-                command: ls __not_exist_dir__
+                
+                SubprocessRunner(command='ls __not_exist_dir__', 
returncode='not yet executed')
                 return code: 2
                 stderr: ls: cannot access '__not_exist_dir__': No such file or 
directory
         
+        Execute a command with retry
+        --------------------------------------------------------
+        
+        :Sample Code:
+            .. code:: python
+        
+                from subprocrunner import Retry, SubprocessRunner
+        
+                SubprocessRunner(command).run(retry=Retry(total=3, 
backoff_factor=0.2, jitter=0.2))
+        
         dry run
         ----------------------------
+        Commands are not actually run when passing ``dry_run=True`` to 
``SubprocessRunner`` class constructor.
+        
         :Sample Code:
             .. code:: python
         
                 from subprocrunner import SubprocessRunner
         
                 runner = SubprocessRunner("echo test", dry_run=True)
-                print("command: {:s}".format(runner.command))
-                print("return code: {:d}".format(runner.run()))
-                print("stdout: {:s}".format(runner.stdout))
-        
+                print(runner)
+                print(f"return code: {runner.run()}")
+                print(f"stdout: {runner.stdout}")
+                
         :Output:
             .. code::
         
-                command: echo test
+                SubprocessRunner(command='echo test', returncode='not yet 
executed', dryrun=True)
                 return code: 0
-                stdout:
+                stdout: 
         
         Get execution command history
         --------------------------------------------------------
@@ -101,10 +113,10 @@
         
                 SubprocessRunner.clear_history()
                 SubprocessRunner.is_save_history = True
-        
-                SubprocessRunner("echo hoge").run()
-                SubprocessRunner("echo foo").run()
-        
+                
+                SubprocessRunner(["echo", "hoge"]).run()
+                SubprocessRunner(["echo", "foo"]).run()
+                
                 print("\n".join(SubprocessRunner.get_history()))
         
         :Output:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/README.rst 
new/subprocrunner-1.6.0/README.rst
--- old/subprocrunner-1.2.2/README.rst  2021-03-20 05:14:16.000000000 +0100
+++ new/subprocrunner-1.6.0/README.rst  2021-06-05 13:14:22.000000000 +0200
@@ -20,13 +20,13 @@
     :target: https://pypi.org/project/subprocrunner
     :alt: Supported Python implementations
 
-.. image:: 
https://img.shields.io/travis/thombashi/subprocrunner/master.svg?label=Linux/macOS%20CI
-    :target: https://travis-ci.org/thombashi/subprocrunner
-    :alt: Linux/macOS CI status
-
-.. image:: 
https://img.shields.io/appveyor/ci/thombashi/subprocrunner/master.svg?label=Windows%20CI
-    :target: 
https://ci.appveyor.com/project/thombashi/subprocrunner/branch/master
-    :alt: Windows CI status
+.. image:: https://github.com/thombashi/subprocrunner/workflows/Tests/badge.svg
+    :target: 
https://github.com/thombashi/subprocrunner/actions/workflows/tests.yml
+    :alt: Test result of Linux/macOS/Windows
+
+.. image:: 
https://github.com/thombashi/subprocrunner/actions/workflows/lint.yml/badge.svg
+    :target: 
https://github.com/thombashi/subprocrunner/actions/workflows/lint.yml
+    :alt: Lint result
 
 .. image:: 
https://coveralls.io/repos/github/thombashi/subprocrunner/badge.svg?branch=master
     :target: https://coveralls.io/github/thombashi/subprocrunner?branch=master
@@ -42,45 +42,57 @@
 
         from subprocrunner import SubprocessRunner
 
-        runner = SubprocessRunner("echo test")
-        print("command: {:s}".format(runner.command))
-        print("return code: {:d}".format(runner.run()))
-        print("stdout: {:s}".format(runner.stdout))
-
-        runner = SubprocessRunner("ls __not_exist_dir__")
-        print("command: {:s}".format(runner.command))
-        print("return code: {:d}".format(runner.run()))
-        print("stderr: {:s}".format(runner.stderr))
-
+        runner = SubprocessRunner(["echo", "test"])
+        print(runner)
+        print(f"return code: {runner.run()}")
+        print(f"stdout: {runner.stdout}")
+        
+        runner = SubprocessRunner(["ls", "__not_exist_dir__"])
+        print(runner)
+        print(f"return code: {runner.run()}")
+        print(f"stderr: {runner.stderr}")
+        
 :Output:
     .. code::
 
-        command: echo test
+        SubprocessRunner(command='echo test', returncode='not yet executed')
         return code: 0
         stdout: test
-
-        command: ls __not_exist_dir__
+        
+        SubprocessRunner(command='ls __not_exist_dir__', returncode='not yet 
executed')
         return code: 2
         stderr: ls: cannot access '__not_exist_dir__': No such file or 
directory
 
+Execute a command with retry
+--------------------------------------------------------
+
+:Sample Code:
+    .. code:: python
+
+        from subprocrunner import Retry, SubprocessRunner
+
+        SubprocessRunner(command).run(retry=Retry(total=3, backoff_factor=0.2, 
jitter=0.2))
+
 dry run
 ----------------------------
+Commands are not actually run when passing ``dry_run=True`` to 
``SubprocessRunner`` class constructor.
+
 :Sample Code:
     .. code:: python
 
         from subprocrunner import SubprocessRunner
 
         runner = SubprocessRunner("echo test", dry_run=True)
-        print("command: {:s}".format(runner.command))
-        print("return code: {:d}".format(runner.run()))
-        print("stdout: {:s}".format(runner.stdout))
-
+        print(runner)
+        print(f"return code: {runner.run()}")
+        print(f"stdout: {runner.stdout}")
+        
 :Output:
     .. code::
 
-        command: echo test
+        SubprocessRunner(command='echo test', returncode='not yet executed', 
dryrun=True)
         return code: 0
-        stdout:
+        stdout: 
 
 Get execution command history
 --------------------------------------------------------
@@ -91,10 +103,10 @@
 
         SubprocessRunner.clear_history()
         SubprocessRunner.is_save_history = True
-
-        SubprocessRunner("echo hoge").run()
-        SubprocessRunner("echo foo").run()
-
+        
+        SubprocessRunner(["echo", "hoge"]).run()
+        SubprocessRunner(["echo", "foo"]).run()
+        
         print("\n".join(SubprocessRunner.get_history()))
 
 :Output:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/subprocrunner-1.2.2/requirements/test_requirements.txt 
new/subprocrunner-1.6.0/requirements/test_requirements.txt
--- old/subprocrunner-1.2.2/requirements/test_requirements.txt  2021-03-20 
05:14:16.000000000 +0100
+++ new/subprocrunner-1.6.0/requirements/test_requirements.txt  2021-06-05 
13:14:22.000000000 +0200
@@ -1,2 +1,3 @@
 pytest
+pytest-mock
 typepy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/subprocrunner/__init__.py 
new/subprocrunner-1.6.0/subprocrunner/__init__.py
--- old/subprocrunner-1.2.2/subprocrunner/__init__.py   2021-03-20 
05:14:16.000000000 +0100
+++ new/subprocrunner-1.6.0/subprocrunner/__init__.py   2021-06-05 
13:14:22.000000000 +0200
@@ -8,3 +8,4 @@
 from ._subprocess_runner import SubprocessRunner
 from ._which import Which
 from .error import CalledProcessError, CommandError
+from .retry import Retry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/subprocrunner/__version__.py 
new/subprocrunner-1.6.0/subprocrunner/__version__.py
--- old/subprocrunner-1.2.2/subprocrunner/__version__.py        2021-05-05 
08:33:26.000000000 +0200
+++ new/subprocrunner-1.6.0/subprocrunner/__version__.py        2021-06-05 
13:14:22.000000000 +0200
@@ -1,6 +1,6 @@
 __author__ = "Tsuyoshi Hombashi"
 __copyright__ = "Copyright 2016, {}".format(__author__)
 __license__ = "MIT License"
-__version__ = "1.2.2"
+__version__ = "1.6.0"
 __maintainer__ = __author__
 __email__ = "[email protected]"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/subprocrunner/_logger/_logger.py 
new/subprocrunner-1.6.0/subprocrunner/_logger/_logger.py
--- old/subprocrunner-1.2.2/subprocrunner/_logger/_logger.py    2021-05-05 
07:06:17.000000000 +0200
+++ new/subprocrunner-1.6.0/subprocrunner/_logger/_logger.py    2021-06-05 
13:14:22.000000000 +0200
@@ -40,6 +40,7 @@
         "CRITICAL": logger.critical,
     }
 
+    log_level = log_level.strip().upper()
     method = method_table.get(log_level)
     if method is None:
         raise ValueError("unknown log level: {}".format(log_level))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/subprocrunner-1.2.2/subprocrunner/_subprocess_runner.py 
new/subprocrunner-1.6.0/subprocrunner/_subprocess_runner.py
--- old/subprocrunner-1.2.2/subprocrunner/_subprocess_runner.py 2021-05-05 
07:59:09.000000000 +0200
+++ new/subprocrunner-1.6.0/subprocrunner/_subprocess_runner.py 2021-06-05 
13:14:22.000000000 +0200
@@ -9,13 +9,14 @@
 import subprocess
 import traceback
 from subprocess import PIPE
-from typing import Dict, List, Optional, Pattern, Sequence, Union, cast
+from typing import Dict, List, Optional, Pattern, Sequence, Union, cast  # noqa
 
 from mbstrdecoder import MultiByteStrDecoder
 
 from ._logger import DEFAULT_ERROR_LOG_LEVEL, get_logging_method
 from ._which import Which
 from .error import CalledProcessError, CommandError
+from .retry import Retry
 from .typing import Command
 
 
@@ -32,6 +33,7 @@
     """
 
     _DRY_RUN_OUTPUT = ""
+    _RETRY_ATTEMPT_KEY = "__retry_attempt__"
 
     default_error_log_level = DEFAULT_ERROR_LOG_LEVEL
     default_is_dry_run = False
@@ -57,6 +59,7 @@
         error_log_level: Optional[str] = None,
         ignore_stderr_regexp: Optional[Pattern] = None,
         dry_run: Optional[bool] = None,
+        quiet: bool = False,
     ) -> None:
         self.__command = []  # type: Union[str, Sequence[str]]
 
@@ -74,23 +77,33 @@
             self.__dry_run = dry_run
         else:
             self.__dry_run = self.default_is_dry_run
-        self.__stdout = None  # type: Union[str, bytes, None]
-        self.__stderr = None  # type: Union[str, bytes, None]
-        self.__returncode = None
+        self.__stdout = None  # type: Optional[str]
+        self.__stderr = None  # type: Optional[str]
+        self.__returncode = None  # type: Optional[int]
 
         self.__ignore_stderr_regexp = ignore_stderr_regexp
-        self.__debug_logging_method = get_logging_method()
+        self.__debug_logging_method = get_logging_method("QUIET" if quiet else 
"DEBUG")
 
-        if error_log_level is not None:
+        if quiet:
+            self.error_log_level = "QUIET"
+        elif error_log_level is not None:
             self.error_log_level = error_log_level
         else:
             self.error_log_level = self.default_error_log_level
 
-        if self.is_save_history:
-            if len(self.__command_history) >= self.history_size:
-                self.__command_history.pop(0)
+        self.__quiet = quiet
 
-            self.__command_history.append(command)
+    def __repr__(self) -> str:
+        params = [
+            "command='{}'".format(self.command_str),
+            "returncode={}".format(
+                self.returncode if self.returncode is not None else "'not yet 
executed'"
+            ),
+        ]
+        if self.dry_run:
+            params.append("dry_run={}".format(self.dry_run))
+
+        return "SubprocessRunner({})".format(", ".join(params))
 
     @property
     def dry_run(self) -> bool:
@@ -108,11 +121,11 @@
         return " ".join(self.__command)
 
     @property
-    def stdout(self) -> Union[str, bytes, None]:
+    def stdout(self) -> Optional[str]:
         return self.__stdout
 
     @property
-    def stderr(self) -> Union[str, bytes, None]:
+    def stderr(self) -> Optional[str]:
         return self.__stderr
 
     @property
@@ -127,28 +140,18 @@
     def error_log_level(self, log_level: Optional[str]):
         self.__error_logging_method = get_logging_method(log_level)
 
-    def run(self, **kwargs) -> Optional[int]:
-        self.__verify_command()
-
-        check = kwargs.pop("check", None)
-        env = kwargs.pop("env", None)
+    def _run(self, env, check: bool, timeout: Optional[float] = None, 
**kwargs) -> int:
+        self.__save_command()
+        
self.__debug_print_command(retry_attept=kwargs.get(self._RETRY_ATTEMPT_KEY))
 
-        if self.dry_run:
-            self.__stdout = self._DRY_RUN_OUTPUT
-            self.__stderr = self._DRY_RUN_OUTPUT
-            self.__returncode = 0
-
-            self.__debug_logging_method("dry-run: {}".format(self.command))
-
-            return self.__returncode
-
-        self.__debug_print_command()
+        if self._RETRY_ATTEMPT_KEY in kwargs:
+            kwargs.pop(self._RETRY_ATTEMPT_KEY)
 
         try:
             proc = subprocess.Popen(
                 self.command,
                 shell=self.__is_shell,
-                env=self.__get_env(env),
+                env=env,
                 stdin=PIPE,
                 stdout=PIPE,
                 stderr=PIPE,
@@ -158,11 +161,11 @@
                 self.command, shell=self.__is_shell, stdin=PIPE, stdout=PIPE, 
stderr=PIPE
             )
 
-        self.__stdout, self.__stderr = proc.communicate(**kwargs)
+        stdout, stderr = proc.communicate(timeout=timeout, **kwargs)
         self.__returncode = proc.returncode
 
-        self.__stdout = MultiByteStrDecoder(self.__stdout).unicode_str
-        self.__stderr = MultiByteStrDecoder(self.__stderr).unicode_str
+        self.__stdout = MultiByteStrDecoder(stdout).unicode_str
+        self.__stderr = MultiByteStrDecoder(stderr).unicode_str
 
         if self.returncode == 0:
             return 0
@@ -172,45 +175,83 @@
                 self.__ignore_stderr_regexp
                 and self.__ignore_stderr_regexp.search(self.stderr) is not None
             ):
-                return self.returncode
+                return self.__returncode
         except AttributeError:
             pass
 
+        self.__error_logging_method(
+            "command='{}', returncode={}, stderr={!r}".format(
+                self.command_str, self.returncode, self.stderr
+            )
+        )
+
         if check is True:
-            # stdout and stderr attributes added since Python 3.5
             raise CalledProcessError(
-                returncode=self.returncode,
+                returncode=self.__returncode,
                 cmd=self.command_str,
                 output=self.stdout,
                 stderr=self.stderr,
             )
 
-        # pytype: disable=attribute-error
-        self.__error_logging_method(
-            "command='{}', returncode={}, stderr={!r}".format(
-                self.command, self.returncode, self.stderr
-            )
+        return self.__returncode
+
+    def run(self, timeout: Optional[float] = None, retry: Retry = None, 
**kwargs) -> int:
+        self.__verify_command()
+
+        check = kwargs.pop("check", False)
+        env = self.__get_env(kwargs.pop("env", None))
+
+        if self.dry_run:
+            self.__stdout = self._DRY_RUN_OUTPUT
+            self.__stderr = self._DRY_RUN_OUTPUT
+            self.__returncode = 0
+
+            self.__save_command()
+            self.__debug_print_command()
+
+            return self.__returncode
+
+        returncode = self._run(
+            env=env, check=check if retry is None else False, timeout=timeout, 
**kwargs
         )
-        # pytype: enable=attribute-error
+        if retry is None or returncode in [0] + retry.no_retry_returncodes:
+            return returncode
+
+        for i in range(retry.total):
+            retry.sleep_before_retry(
+                attempt=i + 1,
+                logging_method=self.__debug_logging_method,
+                retry_target=self.command_str,
+            )
+            kwargs[self._RETRY_ATTEMPT_KEY] = i + 1
+
+            returncode = self._run(env=env, check=False, timeout=timeout, 
**kwargs)
+            if returncode in [0] + retry.no_retry_returncodes:
+                return returncode
+
+        if check is True:
+            raise CalledProcessError(
+                returncode=self.__returncode,  # type: ignore
+                cmd=self.command_str,
+                output=self.stdout,
+                stderr=self.stderr,
+            )
 
-        return self.returncode
+        return self.__returncode  # type: ignore
 
     def popen(self, std_in: Optional[int] = None, env: Optional[Dict[str, 
str]] = None):
         self.__verify_command()
+        self.__debug_print_command()
 
         if self.dry_run:
             self.__stdout = self._DRY_RUN_OUTPUT
             self.__stderr = self._DRY_RUN_OUTPUT
             self.__returncode = 0
 
-            self.__debug_logging_method("dry-run: {}".format(self.command))
-
             return subprocess.CompletedProcess(
                 args=[], returncode=self.__returncode, stdout=self.__stdout, 
stderr=self.__stderr
             )
 
-        self.__debug_print_command()
-
         try:
             process = subprocess.Popen(
                 self.command,
@@ -243,6 +284,15 @@
 
         Which(base_command).verify()
 
+    def __save_command(self):
+        if not self.is_save_history:
+            return
+
+        if len(self.__command_history) >= self.history_size:
+            self.__command_history.pop(0)
+
+        self.__command_history.append(self.command_str)
+
     @staticmethod
     def __get_env(env=None):
         if env is not None:
@@ -253,8 +303,19 @@
 
         return os.environ
 
-    def __debug_print_command(self) -> None:
-        message_list = [self.command_str]
+    def __debug_print_command(self, retry_attept: Optional[int] = None) -> 
None:
+        if self.__quiet:
+            return
+
+        message_list = []
+
+        if self.dry_run:
+            message_list.append("dryrun: ")
+
+        if retry_attept is not None:
+            message_list.append("retry-attempt={}: {}".format(retry_attept, 
self.command_str))
+        else:
+            message_list.append(self.command_str)
 
         if self.is_output_stacktrace:
             message_list.append("".join(traceback.format_stack()[:-2]))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/subprocrunner/error.py 
new/subprocrunner-1.6.0/subprocrunner/error.py
--- old/subprocrunner-1.2.2/subprocrunner/error.py      2021-05-05 
07:59:36.000000000 +0200
+++ new/subprocrunner-1.6.0/subprocrunner/error.py      2021-06-05 
13:14:22.000000000 +0200
@@ -3,8 +3,8 @@
 """
 
 
-import subprocess
-import sys
+# keep the following line for backward compatibility
+from subprocess import CalledProcessError  # noqa
 from typing import Optional
 
 from .typing import Command
@@ -24,13 +24,3 @@
         self.__errno = kwargs.pop("errno", None)
 
         super().__init__(*args)
-
-
-class CalledProcessError(subprocess.CalledProcessError):
-    def __init__(self, *args, **kwargs) -> None:
-        if sys.version_info[0:2] <= (3, 4):
-            # stdout and stderr attribute added to 
subprocess.CalledProcessError since Python 3.5
-            self.stdout = kwargs.pop("stdout", None)
-            self.stderr = kwargs.pop("stderr", None)
-
-        super().__init__(*args, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/subprocrunner/retry.py 
new/subprocrunner-1.6.0/subprocrunner/retry.py
--- old/subprocrunner-1.2.2/subprocrunner/retry.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/subprocrunner-1.6.0/subprocrunner/retry.py      2021-06-05 
13:14:22.000000000 +0200
@@ -0,0 +1,72 @@
+import time
+from random import uniform
+from typing import Callable, List, Optional
+
+
+class Retry:
+    def __init__(
+        self,
+        total: int = 3,
+        backoff_factor: float = 0.2,
+        jitter: float = 0.2,
+        no_retry_returncodes: Optional[List[int]] = None,
+        quiet: bool = False,
+    ) -> None:
+        self.total = total
+        self.__backoff_factor = backoff_factor
+        self.__jitter = jitter
+        self.__quiet = quiet
+
+        if self.total <= 0:
+            raise ValueError("total must be greater than zero")
+
+        if self.__backoff_factor <= 0:
+            raise ValueError("backoff_factor must be greater than zero")
+
+        if self.__jitter <= 0:
+            raise ValueError("jitter must be greater than zero")
+
+        if no_retry_returncodes:
+            self.no_retry_returncodes = no_retry_returncodes
+        else:
+            self.no_retry_returncodes = []
+
+    def __repr__(self) -> str:
+        msgs = [
+            "total={}".format(self.total),
+            "backoff-factor={}".format(self.__backoff_factor),
+            "jitter={}".format(self.__jitter),
+        ]
+
+        if self.no_retry_returncodes:
+            
msgs.append("no-retry-returncodes={}".format(self.no_retry_returncodes))
+
+        return "Retry({})".format(", ".join(msgs))
+
+    def calc_backoff_time(self, attempt: int) -> float:
+        sleep_duration = self.__backoff_factor * (2 ** max(0, attempt - 1))
+        sleep_duration += uniform(0.5 * self.__jitter, 1.5 * self.__jitter)
+
+        return sleep_duration
+
+    def sleep_before_retry(
+        self,
+        attempt: int,
+        logging_method: Optional[Callable] = None,
+        retry_target: Optional[str] = None,
+    ) -> float:
+        sleep_duration = self.calc_backoff_time(attempt)
+
+        if logging_method and not self.__quiet:
+            if retry_target:
+                msg = "Retrying '{}' in ".format(retry_target)
+            else:
+                msg = "Retrying in "
+
+            msg += "{:.2f} seconds ... (attempt={}/{})".format(sleep_duration, 
attempt, self.total)
+
+            logging_method(msg)
+
+        time.sleep(sleep_duration)
+
+        return sleep_duration
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/subprocrunner.egg-info/PKG-INFO 
new/subprocrunner-1.6.0/subprocrunner.egg-info/PKG-INFO
--- old/subprocrunner-1.2.2/subprocrunner.egg-info/PKG-INFO     2021-05-05 
09:10:08.000000000 +0200
+++ new/subprocrunner-1.6.0/subprocrunner.egg-info/PKG-INFO     2021-06-05 
13:14:36.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: subprocrunner
-Version: 1.2.2
+Version: 1.6.0
 Summary: A Python wrapper library for subprocess module.
 Home-page: https://github.com/thombashi/subprocrunner
 Author: Tsuyoshi Hombashi
@@ -30,13 +30,13 @@
             :target: https://pypi.org/project/subprocrunner
             :alt: Supported Python implementations
         
-        .. image:: 
https://img.shields.io/travis/thombashi/subprocrunner/master.svg?label=Linux/macOS%20CI
-            :target: https://travis-ci.org/thombashi/subprocrunner
-            :alt: Linux/macOS CI status
-        
-        .. image:: 
https://img.shields.io/appveyor/ci/thombashi/subprocrunner/master.svg?label=Windows%20CI
-            :target: 
https://ci.appveyor.com/project/thombashi/subprocrunner/branch/master
-            :alt: Windows CI status
+        .. image:: 
https://github.com/thombashi/subprocrunner/workflows/Tests/badge.svg
+            :target: 
https://github.com/thombashi/subprocrunner/actions/workflows/tests.yml
+            :alt: Test result of Linux/macOS/Windows
+        
+        .. image:: 
https://github.com/thombashi/subprocrunner/actions/workflows/lint.yml/badge.svg
+            :target: 
https://github.com/thombashi/subprocrunner/actions/workflows/lint.yml
+            :alt: Lint result
         
         .. image:: 
https://coveralls.io/repos/github/thombashi/subprocrunner/badge.svg?branch=master
             :target: 
https://coveralls.io/github/thombashi/subprocrunner?branch=master
@@ -52,45 +52,57 @@
         
                 from subprocrunner import SubprocessRunner
         
-                runner = SubprocessRunner("echo test")
-                print("command: {:s}".format(runner.command))
-                print("return code: {:d}".format(runner.run()))
-                print("stdout: {:s}".format(runner.stdout))
-        
-                runner = SubprocessRunner("ls __not_exist_dir__")
-                print("command: {:s}".format(runner.command))
-                print("return code: {:d}".format(runner.run()))
-                print("stderr: {:s}".format(runner.stderr))
-        
+                runner = SubprocessRunner(["echo", "test"])
+                print(runner)
+                print(f"return code: {runner.run()}")
+                print(f"stdout: {runner.stdout}")
+                
+                runner = SubprocessRunner(["ls", "__not_exist_dir__"])
+                print(runner)
+                print(f"return code: {runner.run()}")
+                print(f"stderr: {runner.stderr}")
+                
         :Output:
             .. code::
         
-                command: echo test
+                SubprocessRunner(command='echo test', returncode='not yet 
executed')
                 return code: 0
                 stdout: test
-        
-                command: ls __not_exist_dir__
+                
+                SubprocessRunner(command='ls __not_exist_dir__', 
returncode='not yet executed')
                 return code: 2
                 stderr: ls: cannot access '__not_exist_dir__': No such file or 
directory
         
+        Execute a command with retry
+        --------------------------------------------------------
+        
+        :Sample Code:
+            .. code:: python
+        
+                from subprocrunner import Retry, SubprocessRunner
+        
+                SubprocessRunner(command).run(retry=Retry(total=3, 
backoff_factor=0.2, jitter=0.2))
+        
         dry run
         ----------------------------
+        Commands are not actually run when passing ``dry_run=True`` to 
``SubprocessRunner`` class constructor.
+        
         :Sample Code:
             .. code:: python
         
                 from subprocrunner import SubprocessRunner
         
                 runner = SubprocessRunner("echo test", dry_run=True)
-                print("command: {:s}".format(runner.command))
-                print("return code: {:d}".format(runner.run()))
-                print("stdout: {:s}".format(runner.stdout))
-        
+                print(runner)
+                print(f"return code: {runner.run()}")
+                print(f"stdout: {runner.stdout}")
+                
         :Output:
             .. code::
         
-                command: echo test
+                SubprocessRunner(command='echo test', returncode='not yet 
executed', dryrun=True)
                 return code: 0
-                stdout:
+                stdout: 
         
         Get execution command history
         --------------------------------------------------------
@@ -101,10 +113,10 @@
         
                 SubprocessRunner.clear_history()
                 SubprocessRunner.is_save_history = True
-        
-                SubprocessRunner("echo hoge").run()
-                SubprocessRunner("echo foo").run()
-        
+                
+                SubprocessRunner(["echo", "hoge"]).run()
+                SubprocessRunner(["echo", "foo"]).run()
+                
                 print("\n".join(SubprocessRunner.get_history()))
         
         :Output:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/subprocrunner-1.2.2/subprocrunner.egg-info/SOURCES.txt 
new/subprocrunner-1.6.0/subprocrunner.egg-info/SOURCES.txt
--- old/subprocrunner-1.2.2/subprocrunner.egg-info/SOURCES.txt  2021-05-05 
09:10:08.000000000 +0200
+++ new/subprocrunner-1.6.0/subprocrunner.egg-info/SOURCES.txt  2021-06-05 
13:14:36.000000000 +0200
@@ -12,6 +12,7 @@
 subprocrunner/_which.py
 subprocrunner/error.py
 subprocrunner/py.typed
+subprocrunner/retry.py
 subprocrunner/typing.py
 subprocrunner.egg-info/PKG-INFO
 subprocrunner.egg-info/SOURCES.txt
@@ -22,5 +23,6 @@
 subprocrunner/_logger/_logger.py
 subprocrunner/_logger/_null_logger.py
 test/test_logger.py
+test/test_retry.py
 test/test_subproc_runner.py
 test/test_which.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/subprocrunner-1.2.2/subprocrunner.egg-info/requires.txt 
new/subprocrunner-1.6.0/subprocrunner.egg-info/requires.txt
--- old/subprocrunner-1.2.2/subprocrunner.egg-info/requires.txt 2021-05-05 
09:10:08.000000000 +0200
+++ new/subprocrunner-1.6.0/subprocrunner.egg-info/requires.txt 2021-06-05 
13:14:36.000000000 +0200
@@ -5,5 +5,6 @@
 
 [test]
 pytest
+pytest-mock
 typepy
 loguru<1,>=0.4.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/test/test_retry.py 
new/subprocrunner-1.6.0/test/test_retry.py
--- old/subprocrunner-1.2.2/test/test_retry.py  1970-01-01 01:00:00.000000000 
+0100
+++ new/subprocrunner-1.6.0/test/test_retry.py  2021-06-05 13:14:22.000000000 
+0200
@@ -0,0 +1,54 @@
+import pytest
+
+from subprocrunner.retry import Retry
+
+
+class Test_Retry_repr:
+    def test_normal(self):
+        assert (
+            str(Retry(backoff_factor=0.5, jitter=0.5))
+            == "Retry(total=3, backoff-factor=0.5, jitter=0.5)"
+        )
+
+
+class Test_Retry_calc_backoff_time:
+    @pytest.mark.parametrize(
+        ["attempt"],
+        [
+            [0],
+            [1],
+            [2],
+            [3],
+            [4],
+        ],
+    )
+    def test_normal(self, attempt):
+        LOOP = 100
+        coef = 2 ** max(0, attempt - 1)
+        backoff_factor = 1
+        jitter = 0.5
+        base_time = backoff_factor * coef
+        retry = Retry(backoff_factor=backoff_factor, jitter=jitter)
+
+        for _i in range(LOOP):
+            assert (
+                (base_time - jitter * 0.5)
+                <= retry.calc_backoff_time(attempt=attempt)
+                <= (base_time + jitter * 1.5)
+            )
+
+
+class Test_Retry_sleep_before_retry:
+    def test_normal(self):
+        attempt = 1
+        coef = 2 ** max(0, attempt - 1)
+        backoff_factor = 0.1
+        jitter = 0.1
+        base_time = backoff_factor * coef
+        retry = Retry(backoff_factor=backoff_factor, jitter=jitter)
+
+        assert (
+            (base_time - jitter * 0.5)
+            <= retry.sleep_before_retry(attempt=attempt)
+            <= (base_time + jitter * 1.5)
+        )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/test/test_subproc_runner.py 
new/subprocrunner-1.6.0/test/test_subproc_runner.py
--- old/subprocrunner-1.2.2/test/test_subproc_runner.py 2021-05-05 
07:06:24.000000000 +0200
+++ new/subprocrunner-1.6.0/test/test_subproc_runner.py 2021-06-05 
13:14:22.000000000 +0200
@@ -9,13 +9,15 @@
 import re
 import subprocess
 import sys
-from subprocess import PIPE, CalledProcessError
+from subprocess import PIPE
 
 import pytest
 from typepy import is_not_null_string, is_null_string
 
 from subprocrunner import SubprocessRunner
 from subprocrunner._logger._null_logger import NullLogger
+from subprocrunner.error import CalledProcessError
+from subprocrunner.retry import Retry
 
 
 os_type = platform.system()
@@ -32,6 +34,30 @@
     raise NotImplementedError(os_type)
 
 
+BACKOFF_FACTOR = 0.01
+JITTER = 0.01
+
+
+class Test_SubprocessRunner_repr:
+    @pytest.mark.parametrize(
+        ["command", "dry_run", "expected"],
+        [
+            [
+                ["ls", "hoge"],
+                False,
+                "SubprocessRunner(command='ls hoge', returncode='not yet 
executed')",
+            ],
+            [
+                ["ls", "hoge"],
+                True,
+                "SubprocessRunner(command='ls hoge', returncode='not yet 
executed', dry_run=True)",
+            ],
+        ],
+    )
+    def test_normal(self, command, dry_run, expected):
+        assert str(SubprocessRunner(command=["ls", "hoge"], dry_run=dry_run)) 
== expected
+
+
 class Test_SubprocessRunner_run:
     @pytest.mark.parametrize(
         ["command", "dry_run", "expected"],
@@ -62,7 +88,11 @@
         assert SubprocessRunner(command).run() == expected
 
     @pytest.mark.parametrize(
-        ["command", "expected"], [["echo test", "test"], [["echo", "test"], 
"test"]]
+        ["command", "expected"],
+        [
+            ["echo test", "test"],
+            [["echo", "test"], "test"],
+        ],
     )
     def test_stdout(self, command, expected):
         runner = SubprocessRunner(command)
@@ -130,8 +160,18 @@
             with pytest.raises(expected):
                 runner.run(check=True)
 
+    def test_timeout_kwarg(self, mocker):
+        mocked_communicate = mocker.patch("subprocess.Popen.communicate")
+        mocked_communicate.return_value = ("", "")
+
+        mocker.patch("subprocrunner.Which.verify")
+        runner = SubprocessRunner("dummy")
+        runner.run(timeout=1)
+
+        mocked_communicate.assert_called_with(timeout=1)
+
     def test_unicode(self, monkeypatch):
-        def monkey_communicate(input=None):
+        def monkey_communicate(input=None, timeout=None):
             return ("", "'dummy' 
??????????????????????????????????????????????????????" 
"???????????????????????????????????????????????? 
????????????????????????????????????????????????")
 
         monkeypatch.setattr(subprocess.Popen, "communicate", 
monkey_communicate)
@@ -139,6 +179,81 @@
         runner = SubprocessRunner(list_command)
         runner.run()
 
+    def test_retry(self, mocker):
+        mocker.patch("subprocrunner.Which.verify")
+
+        runner = SubprocessRunner("always-failed-command")
+        retry_ct = 3
+
+        # w/ retry: check=False
+        mocked_run = mocker.patch("subprocrunner.SubprocessRunner._run")
+        mocked_run.return_value = 1
+        runner.run(
+            check=False,
+            retry=Retry(total=retry_ct, backoff_factor=BACKOFF_FACTOR, 
jitter=JITTER),
+        )
+        assert mocked_run.call_count == retry_ct + 1
+
+        # w/ retry: check=True
+        mocked_run = mocker.patch("subprocrunner.SubprocessRunner._run")
+        mocked_run.return_value = 1
+        try:
+            runner.run(
+                check=True,
+                retry=Retry(total=retry_ct, backoff_factor=BACKOFF_FACTOR, 
jitter=JITTER),
+            )
+        except CalledProcessError:
+            pass
+        assert mocked_run.call_count == retry_ct + 1
+
+        # w/o retry: check=False
+        mocked_run = mocker.patch("subprocrunner.SubprocessRunner._run")
+        mocked_run.return_value = 1
+        runner.run(check=False, retry=None)
+        assert mocked_run.call_count == 1
+
+        # w/o retry: check=True
+        mocked_run = mocker.patch("subprocrunner.SubprocessRunner._run")
+        mocked_run.return_value = 1
+        try:
+            runner.run(check=True, retry=None)
+        except CalledProcessError:
+            pass
+        assert mocked_run.call_count == 1
+
+    def test_no_retry_returncodes(self, mocker):
+        mocker.patch("subprocrunner.Which.verify")
+
+        runner = SubprocessRunner("always-failed-command")
+        mocked_run = mocker.patch("subprocrunner.SubprocessRunner._run")
+        mocked_run.return_value = 2
+        runner.run(
+            check=True,
+            retry=Retry(
+                total=3,
+                backoff_factor=BACKOFF_FACTOR,
+                jitter=JITTER,
+                no_retry_returncodes=[2],
+            ),
+        )
+        assert mocked_run.call_count == 1
+
+    def test_retry_success_ater_failed(self, mocker):
+        mocker.patch("subprocrunner.Which.verify")
+
+        def failed_first_call(*args, **kwargs):
+            attempt = kwargs.get(SubprocessRunner._RETRY_ATTEMPT_KEY)
+            if attempt is None:
+                return 1
+
+            return 0
+
+        runner = SubprocessRunner("always-failed-command")
+        mocked_run = mocker.patch("subprocrunner.SubprocessRunner._run")
+        mocked_run.side_effect = failed_first_call
+        runner.run(check=True, retry=Retry(total=3, 
backoff_factor=BACKOFF_FACTOR, jitter=JITTER))
+        assert mocked_run.call_count == 2
+
 
 class Test_SubprocessRunner_popen:
     @pytest.mark.parametrize(
@@ -165,18 +280,33 @@
 
 class Test_SubprocessRunner_command_history:
     @pytest.mark.parametrize(
-        ["command", "dry_run", "expected"], [[list_command, False, 0], 
[list_command, True, 0]]
+        ["command", "dry_run"],
+        [
+            [list_command, False],
+            [list_command, True],
+        ],
     )
-    def test_normal(self, command, dry_run, expected):
+    def test_normal(self, command, dry_run):
+        loop_count = 3
+
         SubprocessRunner.is_save_history = False
         SubprocessRunner.clear_history()
-
-        loop_count = 3
         for _i in range(loop_count):
             SubprocessRunner(command, dry_run=dry_run).run()
-        assert len(SubprocessRunner.get_history()) == 0
+        assert SubprocessRunner.get_history() == []
 
         SubprocessRunner.is_save_history = True
+        SubprocessRunner.clear_history()
         for _i in range(loop_count):
             SubprocessRunner(command, dry_run=dry_run).run()
-        assert len(SubprocessRunner.get_history()) == loop_count
+        assert SubprocessRunner.get_history() == [list_command] * loop_count
+
+    def test_normal_retry(self, mocker):
+        SubprocessRunner.clear_history()
+        SubprocessRunner.is_save_history = True
+        command = [list_command, "not_exist_dir"]
+        runner = SubprocessRunner(command)
+        retry_ct = 3
+
+        runner.run(retry=Retry(total=retry_ct, backoff_factor=BACKOFF_FACTOR, 
jitter=JITTER))
+        assert runner.get_history() == [" ".join(command)] * (retry_ct + 1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/subprocrunner-1.2.2/tox.ini 
new/subprocrunner-1.6.0/tox.ini
--- old/subprocrunner-1.2.2/tox.ini     2021-05-05 08:29:55.000000000 +0200
+++ new/subprocrunner-1.6.0/tox.ini     2021-06-05 13:14:22.000000000 +0200
@@ -47,7 +47,7 @@
     black
     isort>=5
 commands =
-    autoflake --in-place --recursive --remove-all-unused-imports 
--ignore-init-module-imports --exclude ".pytype" .
+    autoflake --in-place --recursive --remove-all-unused-imports 
--ignore-init-module-imports .
     isort .
     black setup.py examples test subprocrunner
 
@@ -58,10 +58,8 @@
     codespell
     mypy
     pylama
-    pytype
 commands =
     python setup.py check
     mypy subprocrunner
-    pytype --keep-going --jobs 2 --disable import-error subprocrunner
     codespell subprocrunner examples test README.rst -q 2 --check-filenames
     pylama

Reply via email to