Exemple #1
0
    def _to_str(self, name=None, file=None, writer_kwargs=None):
        """
        Return the string representation of the collected messages

        Parameters
        ----------
        name
            Title to show at the end
        file
            Text stream to use. If None, uses a temporary StringIO object
        writer_kwargs
            Extra keyword arguments passed to the terminal writer
        """
        writer_kwargs = writer_kwargs or dict()

        if file is None:
            sio = StringIO()
        else:
            sio = file

        sio.write('\n')

        self.tw = TerminalWriter(file=sio)

        if name:
            self.tw.sep('=', title=name, **writer_kwargs)
        else:
            self.tw.sep('=', **writer_kwargs)

        for msg in self.messages:
            self.tw.sep('-', title=msg.header, **writer_kwargs)

            sub_header = msg.sub_header

            if sub_header:
                self.tw.sep('-', title=sub_header, **writer_kwargs)

            self.tw._write_source(msg.message.splitlines(), lexer='pytb')

        n = len(self)
        t = 'task' if n == 1 else 'tasks'
        self.tw.sep('=', title=f'Summary ({n} {t})', **writer_kwargs)

        for msg in self.messages:
            # TODO: include original exception type and error message in
            # summary
            self.tw.write(f'{msg.header}\n')

        if name:
            self.tw.sep('=', title=name, **writer_kwargs)
        else:
            self.tw.sep('=', **writer_kwargs)

        sio.seek(0)
        out = sio.read()

        if file is None:
            sio.close()

        return out
Exemple #2
0
    def wrapper(catch_exception=True, **kwargs):
        if catch_exception:
            try:
                fn(**kwargs)
            # these already color output
            except (DAGBuildError, DAGRenderError):
                error = traceback.format_exc()
                color = False
            except BaseException as e:
                e.show()
                sys.exit(1)
            except Exception:
                error = traceback.format_exc()
                color = True
            else:
                error = None

            if error:
                if color:
                    tw = TerminalWriter(file=sys.stderr)
                    tw._write_source(error.splitlines())
                else:
                    print(error, file=sys.stderr)

                sys.exit(1)
        else:
            fn(**kwargs)
Exemple #3
0
    def __init__(self, workspace=None, templates_path=None):
        self.tw = TerminalWriter()
        self.workspace = None if not workspace else Path(workspace).resolve()
        self._to_delete = []
        self._warnings = []

        self._wd = Path('.').resolve()

        if templates_path:
            self._env = Environment(loader=PackageLoader(*templates_path),
                                    undefined=StrictUndefined)
            self._env.filters['to_pascal_case'] = to_pascal_case
        else:
            self._env = None
Exemple #4
0
class MessageCollector(abc.ABC):
    """Collect messages and display them as a single str with task names
    Utilities for exception handling

    When a DAG is rendered or built, exceptions/warnings might happen, this
    class helps collect them all to show a meaninful error message where each
    one is shown along with the task name it generated it.

    """
    def __init__(self, messages=None):
        self.messages = messages or []

    def append(self, task, message, obj=None):
        self.messages.append(Message(task=task, message=message, obj=obj))

    @abc.abstractmethod
    def __str__(self):
        pass

    def _to_str(self, name=None, file=None, writer_kwargs=None):
        """
        Return the string representation of the collected messages

        Parameters
        ----------
        name
            Title to show at the end
        file
            Text stream to use. If None, uses a temporary StringIO object
        writer_kwargs
            Extra keyword arguments passed to the terminal writer
        """
        writer_kwargs = writer_kwargs or dict()

        if file is None:
            sio = StringIO()
        else:
            sio = file

        sio.write('\n')

        self.tw = TerminalWriter(file=sio)

        if name:
            self.tw.sep('=', title=name, **writer_kwargs)
        else:
            self.tw.sep('=', **writer_kwargs)

        for msg in self.messages:
            self.tw.sep('-', title=msg.header, **writer_kwargs)

            sub_header = msg.sub_header

            if sub_header:
                self.tw.sep('-', title=sub_header, **writer_kwargs)

            self.tw._write_source(msg.message.splitlines(), lexer='pytb')

        n = len(self)
        t = 'task' if n == 1 else 'tasks'
        self.tw.sep('=', title=f'Summary ({n} {t})', **writer_kwargs)

        for msg in self.messages:
            # TODO: include original exception type and error message in
            # summary
            self.tw.write(f'{msg.header}\n')

        if name:
            self.tw.sep('=', title=name, **writer_kwargs)
        else:
            self.tw.sep('=', **writer_kwargs)

        sio.seek(0)
        out = sio.read()

        if file is None:
            sio.close()

        return out

    def __len__(self):
        return len(self.messages)

    def __bool__(self):
        return bool(self.messages)

    def __iter__(self):
        for message in self.messages:
            yield message
Exemple #5
0
class Commander:
    """Manage script workflows
    """
    def __init__(self,
                 workspace=None,
                 templates_path=None,
                 environment_kwargs=None):
        self.tw = TerminalWriter()
        self.workspace = None if not workspace else Path(workspace).resolve()
        self._to_delete = []
        self._warnings = []

        self._wd = Path('.').resolve()

        if templates_path:
            self._env = Environment(loader=PackageLoader(*templates_path),
                                    undefined=StrictUndefined,
                                    **(environment_kwargs or {}))
            self._env.filters['to_pascal_case'] = to_pascal_case
        else:
            self._env = None

    def run(self,
            *cmd,
            description=None,
            capture_output=False,
            expected_output=None,
            error_message=None,
            hint=None,
            show_cmd=True):
        """Execute a command in a subprocess

        Parameters
        ----------
        *cmd
            Command to execute

        description: str, default=None
            Label to display before executing the command

        capture_output: bool, default=False
            Captures output, otherwise prints to standard output and standard
            error

        expected_output: str, default=None
            Raises a RuntimeError if the output is different than this value.
            Only valid when capture_output=True

        error_message: str, default=None
            Error to display when expected_output does not match. If None,
            a generic message is shown

        hint: str, default=None
            An optional string to show when at the end of the error when
            the expected_output does not match. Used to hint the user how
            to fix the problem

        show_cmd : bool, default=True
            Whether to display the command next to the description
            (and error message if it fails) or not. Only valid when
            description is not None
        """
        cmd_str = ' '.join(cmd)

        if expected_output is not None and not capture_output:
            raise RuntimeError('capture_output must be True when '
                               'expected_output is not None')

        if description:
            header = f'{description}: {cmd_str}' if show_cmd else description
            self.tw.sep('=', header, blue=True)

        error = None

        # py 3.6 compatibility: cannot use subprocess.run directly
        # because the check_output arg was included until version 3.7
        if not capture_output:
            try:
                result = subprocess.check_call(cmd)
            except Exception as e:
                error = e
        # capture outpuut
        else:
            try:
                result = subprocess.check_output(cmd)
            except Exception as e:
                error = e
            else:
                result = result.decode(sys.stdout.encoding)

            if expected_output is not None:
                error = result != expected_output

        if error:
            lines = []

            if error_message:
                line_first = error_message
            else:
                if show_cmd:
                    cmd_str = ' '.join(cmd)
                    line_first = ('An error occurred when executing '
                                  f'command: {cmd_str}')
                else:
                    line_first = 'An error occurred.'

            lines.append(line_first)

            if not capture_output:
                lines.append(f'Original error message: {error}')

            if hint:
                lines.append(f'Hint: {hint}.')

            raise CommanderException('\n'.join(lines))
        else:
            return result

    def __enter__(self):
        if self.workspace and not Path(self.workspace).exists():
            Path(self.workspace).mkdir()

        return self

    def __exit__(self, exc_type, exc_value, traceback):
        # move to the original working directory
        os.chdir(self._wd)

        self.rm(*self._to_delete)
        supress = isinstance(exc_value, CommanderStop)

        if supress:
            self.info(str(exc_value))

        self._warn_show()

        return supress

    def rm(self, *args):
        """Deletes all files/directories

        Examples
        --------
        >>> cmdr.rm('file', 'directory')
        """
        for f in args:
            _delete(f)

    def rm_on_exit(self, path):
        """Removes file upon exit

        Examples
        --------
        >>> cmdr.rm_on_exit('some_temporary_file')
        """
        self._to_delete.append(Path(path).resolve())

    def copy_template(self, path, **render_kwargs):
        """Copy template to the workspace

        Parameters
        ----------
        path : str
            Path to template (relative to templates path)

        **render_kwargs
            Keyword arguments passed to the template

        Examples
        --------
        >>> # copies template in {templates-path}/directory/template.yaml
        >>> # to {workspace}/template.yaml
        >>> cmdr.copy_template('directory/template.yaml')
        """
        dst = Path(self.workspace, PurePosixPath(path).name)

        # This message is no longer valid since this is only called
        # when there is no env yet
        if dst.exists():
            self.success(f'Using existing {path!s}...')
        else:
            self.info(f'Adding {dst!s}...')
            dst.parent.mkdir(exist_ok=True, parents=True)
            content = self._env.get_template(str(path)).render(**render_kwargs)
            dst.write_text(content)

    def cd(self, dir_):
        """Change current working directory
        """
        os.chdir(dir_)

    def cp(self, src):
        """
        Copies a file or directory to the workspace, replacing it if necessary.
        Deleted on exit.

        Notes
        -----
        Used mainly for preparing Dockerfiles since they can only
        copy from the current working directory

        Examples
        --------
        >>> # copies dir/file to {workspace}/file
        >>> cmdr.cp('dir/file')
        """
        path = Path(src)

        if not path.exists():
            raise CommanderException(
                f'Missing {src} file. Add it and try again.')

        # convert to absolute to ensure we delete the right file on __exit__
        dst = Path(self.workspace, path.name).resolve()
        self._to_delete.append(dst)

        _delete(dst)

        if path.is_file():
            shutil.copy(src, dst)
        else:
            shutil.copytree(src, dst)

    def append_inline(self, line, dst):
        """Append line to a file

        Parameters
        ----------
        line : str
            Line to append

        dst : str
            File to append (can be outside the workspace)

        Examples
        --------
        >>> cmdr.append_inline('*.csv', '.gitignore')
        """
        if not Path(dst).exists():
            Path(dst).touch()

        original = Path(dst).read_text()
        Path(dst).write_text(original + '\n' + line + '\n')

    def print(self, line):
        """Print message (no color)
        """
        self.tw.write(f'{line}\n')

    def success(self, line=None):
        """Print success message (green)
        """
        self.tw.sep('=', line, green=True)

    def info(self, line=None):
        """Print information message (blue)
        """
        self.tw.sep('=', line, blue=True)

    def warn(self, line=None):
        """Print warning (yellow)
        """
        self.tw.sep('=', line, yellow=True)

    def warn_on_exit(self, line):
        """Append a warning message to be displayed on exit
        """
        self._warnings.append(line)

    def _warn_show(self):
        """Display accumulated warning messages (added via .warn_on_exit)
        """
        if self._warnings:
            self.tw.sep('=', 'Warnings', yellow=True)
            self.tw.write('\n\n'.join(self._warnings) + '\n')
            self.tw.sep('=', yellow=True)