Skip to content
Snippets Groups Projects
run.py 3.49 KiB
Newer Older
  • Learn to ignore specific revisions
  • """Utility for subprocess calls."""
    import logging
    import os
    import subprocess
    import time
    
    
    
    def run(*args, counter=None, **kwargs):
    
        """
        Run a subprocess and log the call.
    
        Convenience function to call `subprocess.run` and provide some metadata
        about the call.
    
        Parameters
        ----------
        args : tuple
            passed on to subprocess.run(*args). For example
            ("ls -l") or (["ls", "-l"])
        counter : TYPE, optional
            list with one integer, starting from [0].
            This is (a Python hack) to count the number of
            calls of this function, so the different calls of subprocess.run
            are easier to follow in the log files.
        kwargs : dict
            keyword-value arguments to be passed to subprocess.run. For example,
            `shell=True`.
    
        Returns
        -------
        process : subprocess.CompletedProcess
            CompletedProcess object as returned by `subprocess.run` .
    
        Examples
        --------
        >>> run(['date', '+%Y-%m-%d'])
        ##-03 Starting subprocess.run(['date', '+%Y-%m-%d']) with options
        ##-03   getcwd=/Users/fangohr/git/mpsd-software-environments
        ##-03   COMMAND=date +%Y-%m-%d
        2023-05-30
        ##-03   Completed in 0.0054s.
        ##-03
        CompletedProcess(args=['date', '+%Y-%m-%d'], returncode=0)
    
        >>> run(['date +%Y-%m-%d'], shell=True)
        ##-04 Starting subprocess.run(['date +%Y-%m-%d']) with options shell=True
        ##-04   getcwd=/Users/fangohr/git/mpsd-software-environments
        ##-04   COMMAND=date +%Y-%m-%d
        2023-05-30
        ##-04   Completed in 0.0069s.
        ##-04
        CompletedProcess(args=['date +%Y-%m-%d'], returncode=0)
        """
        # token is printed in front of every meta-data line - useful for
        # searching the logs. Starts with "##-00", then "##-01", ...
    
        if counter is None:
            counter = [0]
    
        token = f"##-{counter[0]:02d}"
    
        counter[0] += 1  # increase counter
    
        # make command nicely readable: ["ls", "-l"] -> "ls -l"
        assert isinstance(args, tuple)
        assert len(args) == 1
        arg = args[0]
        # either args is a tuple containing a string | Example: ('ls -1',)
        if isinstance(arg, str):
            command = arg
            # or we have a tuple containing a list of strings.
            # Example: (['ls', '-1'],)
        elif isinstance(arg, list):
            command = " ".join(arg)
        else:
            # we do not expect this to happen
    
            raise NotImplementedError(f"arg={arg}, args={args}")
    
        # Translate `capture_output' for compatibility with older Python
        if kwargs.pop("capture_output", False):
            if kwargs.get("stdout") is not None or kwargs.get("stderr") is not None:
                raise ValueError(
    
    Henri Menke's avatar
    Henri Menke committed
                    "stdout and stderr arguments may not be used with capture_output."
    
                )
            kwargs["stdout"] = subprocess.PIPE
            kwargs["stderr"] = subprocess.PIPE
    
    
        # make options (such as `shell=True`) nicely readable
        options = ", ".join([f"{key}={value}" for key, value in kwargs.items()])
    
        # provide information about upcoming subprocess.run call
        logging.debug(
            f"{token} Starting subprocess.run('{command}') with options {options}"
        )
        logging.debug(f"""{token}   getcwd={os.getcwd()}""")
        logging.debug(f"""{token}   subprocess.run("{arg}")""")
    
        time_start = time.time()
        process = subprocess.run(*args, **kwargs)
        execution_time = time.time() - time_start
    
    
        logging.debug(f"{token}   process={process}")
    
        logging.debug(f"{token}   Completed in {execution_time:.4f}s.")
        logging.debug(f"{token}")  # near-empty line to make reading logs easier
        return process