Ejemplo n.º 1
0
def test_context_error_raises():
    """A ContextError raises with correct message."""
    assert isinstance(ContextError(), PypyrError)

    with pytest.raises(ContextError) as err_info:
        raise ContextError("this is error text right here")

    assert str(err_info.value) == "this is error text right here"
Ejemplo n.º 2
0
def test_context_error_raises():
    """ContextError raises with correct message"""
    assert isinstance(ContextError(), PypyrError)

    with pytest.raises(ContextError) as err_info:
        raise ContextError("this is error text right here")

    assert repr(err_info.value) == ("ContextError('this is error "
                                    "text right here',)")
Ejemplo n.º 3
0
def write_child_context_to_parent(out, parent_context, child_context):
    """Write out keys from child to parent context.

    Args:
        out. str or dict or list. Pass a string for a single
             key to grab from child context, a list of string for a list
             of keys to grab from child context, or a dict where you map
             'parent-key-name': 'child-key-name'.
        parent_context: parent Context. destination context.
        child_context: write from this context to the parent.
    """
    if isinstance(out, str):
        save_me = {out: out}
    elif isinstance(out, list):
        save_me = {k: k for k in out}
    elif isinstance(out, dict):
        save_me = out
    else:
        raise ContextError("pypyr.steps.pype pype.out should be a string, or "
                           f"a list or a dict. Instead, it's a {type(out)}")

    for parent_key, child_key in save_me.items():
        logger.debug(
            "setting parent context %s to value from child context %s",
            parent_key, child_key)

        parent_context[parent_key] = child_context.get_formatted(child_key)
Ejemplo n.º 4
0
    def __init__(self, name, context):
        """Initialize the CmdStep.

        The step config in the context dict looks like this:
            cmd: <<cmd string>>

            OR, as a dict
            cmd:
                run: str. mandatory. command + args to execute.
                save: bool. optional. defaults False. save output to cmdOut.
                cwd: str/path. optional. if specified, change the working
                     directory just for the duration of the command.

        Args:
            name: Unique name for step. Likely __name__ of calling step.
            context: pypyr.context.Context. Look for config in this context
                     instance.

        """
        assert name, ("name parameter must exist for CmdStep.")
        assert context, ("context param must exist for CmdStep.")
        # this way, logs output as the calling step, which makes more sense
        # to end-user than a mystery steps.dsl.blah logging output.
        self.logger = logging.getLogger(name)

        context.assert_key_has_value(key='cmd', caller=name)

        self.context = context
        self.is_save = False

        cmd_config = context['cmd']

        if isinstance(cmd_config, str):
            self.cmd_text = context.get_formatted('cmd')
            self.cwd = None
            self.logger.debug("Processing command string: %s", cmd_config)
        elif isinstance(cmd_config, dict):
            context.assert_child_key_has_value(parent='cmd',
                                               child='run',
                                               caller=name)
            run_string = cmd_config['run']
            self.cmd_text = context.get_formatted_string(run_string)
            is_save = cmd_config.get('save', False)
            self.is_save = context.get_formatted_as_type(is_save,
                                                         out_type=bool)

            cwd_string = cmd_config.get('cwd', None)
            if cwd_string:
                self.cwd = context.get_formatted_string(cwd_string)
                self.logger.debug("Processing command string in dir "
                                  "%s: %s", self.cwd, run_string)
            else:
                self.cwd = None
                self.logger.debug("Processing command string: %s", run_string)

        else:
            raise ContextError(f"{name} cmd config should be either a simple "
                               "string cmd='mycommandhere' or a dictionary "
                               "cmd={'run': 'mycommandhere', 'save': False}.")
Ejemplo n.º 5
0
def run_step(context):
    """Assert that something is True or equal to something else.

    Args:
        context: dictionary-like pypyr.context.Context. context is mandatory.
        Uses the following context keys in context:
            - assertThis. mandatory. Any type. If assertEquals not specified,
              evals as boolean.
            - assertEquals. optional. Any type.

    If assertThis evaluates to False raises error.
    If assertEquals is specified, raises error if assertThis != assertEquals.

    assertThis & assertEquals both support string substitutions.

    Returns:
        None

    Raises:
        ContextError: if assert evaluates to False.
    """
    logger.debug("started")
    assert context, f"context must have value for {__name__}"
    context.assert_key_has_value('assertThis', __name__)

    if 'assertEquals' in context:
        # compare assertThis to assertEquals
        logger.debug("comparing assertThis to assertEquals.")
        assert_result = (context.get_formatted('assertThis') ==
                         context.get_formatted('assertEquals'))
    else:
        # nothing to compare means treat assertThis as a bool.
        logger.debug("Evaluating assertThis as a boolean.")
        assert_result = context.get_formatted_as_type(context['assertThis'],
                                                      out_type=bool)

    logger.info(f"assert evaluated to {assert_result}")

    if not assert_result:
        assert_equals = context.get('assertEquals', None)

        if assert_equals is None:
            # if it's a bool it's presumably not a sensitive value.
            error_text = (
                f"assert {context['assertThis']} evaluated to False.")
        else:
            # emit type to help user, but not the actual field contents.
            error_text = (
                f"assert context['assertThis'] is of type "
                f"{type(context.get_formatted('assertThis')).__name__} "
                f"and does not equal context['assertEquals'] of type "
                f"{type(context.get_formatted('assertEquals')).__name__}.")
        raise ContextError(error_text)

    logger.debug("done")
Ejemplo n.º 6
0
def test_handled_error_raises():
    """A HandledError raises with correct message and with from."""
    assert isinstance(HandledError(), PypyrError)

    try:
        try:
            raise ContextError("this is error text right here")
        except ContextError as e:
            raise HandledError("handled") from e
    except Exception as err_info:
        assert str(err_info) == "handled"
        inner = err_info.__cause__
        assert isinstance(inner, ContextError)
        assert str(inner) == "this is error text right here"
Ejemplo n.º 7
0
def test_run_failure_step_group_swallows():
    """Failure step group runner swallows errors."""
    logger = pypyr.log.logger.get_logger('pypyr.stepsrunner')

    with patch('pypyr.stepsrunner.run_step_group') as mock_run_group:
        with patch.object(logger, 'error') as mock_logger_error:
            mock_run_group.side_effect = ContextError('arb error')
            pypyr.stepsrunner.run_failure_step_group({'pipe': 'val'},
                                                     Context())

        mock_logger_error.assert_any_call(
            "Failure handler also failed. Swallowing.")

    mock_run_group.assert_called_once_with(pipeline_definition={'pipe': 'val'},
                                           step_group_name='on_failure',
                                           context=Context())
Ejemplo n.º 8
0
def get_args(get_item):
    """Parse env, key, default out of input dict.

    Args:
        get_item: dict. contains keys env/key/default

    Returns:
        (env, key, has_default, default) tuple, where
            env: str. env var name.
            key: str. save env value to this context key.
            has_default: bool. True if default specified.
            default: the value of default, if specified.

    Raises:
        ContextError: envGet is not a list of dicts.
        KeyNotInContextError: If env or key not found in get_config.

    """
    if not isinstance(get_item, dict):
        raise ContextError('envGet must contain a list of dicts.')

    env = get_item.get('env', None)

    if not env:
        raise KeyNotInContextError(
            'context envGet[env] must exist in context for envGet.')

    key = get_item.get('key', None)

    if not key:
        raise KeyNotInContextError(
            'context envGet[key] must exist in context for envGet.')

    if 'default' in get_item:
        has_default = True
        default = get_item['default']
    else:
        has_default = False
        default = None

    return (env, key, has_default, default)
Ejemplo n.º 9
0
def assert_key_exists(obj, key, caller, parent=None):
    """Assert that object contains key.

    Error messages are structured as if obj is a pypyr Context.

    Args:
        obj (mapping): object to check for key.
        key (any valid key type): validates that this key exists in context
        caller: string. calling function or module name - this used to
                construct error messages. Tip: use .__name__
        parent (any valid key type): parent key name. Used to construct error
                                     messages to indicate the name of missing
                                     obj in context.

    Raises:
        KeyNotInContextError: When key doesn't exist in context.

    """
    try:
        if key not in obj:
            if parent:
                msg = (f"context[{parent!r}][{key!r}] doesn't "
                       f"exist. It must exist for {caller}.")
            else:
                msg = (f"context[{key!r}] doesn't exist. "
                       f"It must exist for {caller}.")

            raise KeyNotInContextError(msg)
    except TypeError as err:
        # catches None on obj or obj not iterable
        if parent:
            msg = (f"context[{parent!r}] must exist, be iterable and contain "
                   f"{key!r} for {caller}. {err}")
        else:
            msg = (f"context[{key!r}] must exist and be iterable for "
                   f"{caller}. {err}")
        raise ContextError(msg) from err
Ejemplo n.º 10
0
    def assert_child_key_has_value(self, parent, child, caller):
        """Assert that context contains key that has child which has a value.

        Args:
            parent: parent key
            child: validate this sub-key of parent exists AND isn't None.
            caller: string. calling function name - this used to construct
                    error messages

        Raises:
            KeyNotInContextError: Key doesn't exist
            KeyInContextHasNoValueError: context[key] is None
            AssertionError: if key is None

        """
        assert parent, ("parent parameter must be specified.")
        assert child, ("child parameter must be specified.")
        self.assert_key_has_value(parent, caller)

        try:
            child_exists = child in self[parent]
        except TypeError as err:
            # This happens if parent isn't iterable
            raise ContextError(
                f"context['{parent}'] must be iterable and contain '{child}' "
                f"for {caller}. {err}") from err

        if child_exists:
            if self[parent][child] is None:
                raise KeyInContextHasNoValueError(
                    f"context['{parent}']['{child}'] must have a value for "
                    f"{caller}.")
        else:
            raise KeyNotInContextError(
                f"context['{parent}']['{child}'] doesn't "
                f"exist. It must exist for {caller}.")
Ejemplo n.º 11
0
def get_arguments(context):
    """Parse arguments for pype from context and assign default values.

    Args:
        context: pypyr.context.Context. context is mandatory.

    Returns:
        tuple (pipeline_name, #str
               args, #dict
               out, #str or dict or list
               use_parent_context, #bool
               pipe_arg, #str
               skip_parse, #bool
               raise_error #bool
               groups #list of str
               success_group #str
               failure_group #str
               )

    Raises:
       pypyr.errors.KeyNotInContextError: if ['pype']['name'] is missing.
       pypyr.errors.KeyInContextHasNoValueError: if ['pype']['name'] exists but
                                                 is None.
    """
    context.assert_key_has_value(key='pype', caller=__name__)
    pype = context.get_formatted('pype')

    try:
        pipeline_name = pype['name']

        if pipeline_name is None:
            raise KeyInContextHasNoValueError(
                "pypyr.steps.pype ['pype']['name'] exists but is empty.")
    except KeyError as err:
        raise KeyNotInContextError(
            "pypyr.steps.pype missing 'name' in the 'pype' context item. "
            "You need to specify the pipeline name to run another "
            "pipeline.") from err

    args = pype.get('args', None)

    if args is not None and not isinstance(args, dict):
        raise ContextError(
            "pypyr.steps.pype 'args' in the 'pype' context item "
            "must be a dict.")

    if args and 'useParentContext' not in pype:
        use_parent_context = False
    else:
        use_parent_context = pype.get('useParentContext', True)

    out = pype.get('out', None)
    if out and use_parent_context:
        raise ContextError(
            "pypyr.steps.pype pype.out is only relevant if useParentContext "
            "= False. If you're using the parent context, no need to have out "
            "args since their values will already be in context. If you're "
            "NOT using parent context and you've specified pype.args, just "
            "leave off the useParentContext key and it'll default to False "
            "under the hood, or set it to False yourself if you keep it in.")

    pipe_arg = pype.get('pipeArg', None)
    skip_parse = pype.get('skipParse', True)
    raise_error = pype.get('raiseError', True)
    loader = pype.get('loader', None)
    groups = pype.get('groups', None)
    if isinstance(groups, str):
        groups = [groups]

    success_group = pype.get('success', None)
    failure_group = pype.get('failure', None)

    return (pipeline_name, args, out, use_parent_context, pipe_arg, skip_parse,
            raise_error, loader, groups, success_group, failure_group)
Ejemplo n.º 12
0
                              groups=['g'],
                              success_group='sg',
                              failure_group='fg')

    mocked_set_up_notify.assert_called_once()
    mocked_set_work_dir.assert_called_once_with('arb/dir')
    mocked_run_pipeline.assert_called_once_with(
        pipeline_name='arb pipe',
        pipeline_context_input='arb context input',
        groups=['g'],
        success_group='sg',
        failure_group='fg')


@patch('pypyr.pipelinerunner.load_and_run_pipeline',
       side_effect=ContextError('arb'))
@patch('pypyr.moduleloader.set_working_directory')
def test_main_fail(mocked_work_dir, mocked_run_pipeline):
    """Main raise unhandled error on pipeline failure."""
    pipeline_cache.clear()
    with pytest.raises(ContextError) as err_info:
        pypyr.pipelinerunner.main(pipeline_name='arb pipe',
                                  pipeline_context_input='arb context input',
                                  working_dir='arb/dir')

    assert str(err_info.value) == "arb"

    mocked_work_dir.assert_called_once_with('arb/dir')
    mocked_run_pipeline.assert_called_once_with(
        pipeline_name='arb pipe',
        pipeline_context_input='arb context input',
Ejemplo n.º 13
0
@patch('pypyr.moduleloader.set_working_directory')
def test_main_pass(mocked_work_dir, mocked_run_pipeline):
    """main initializes and runs pipelines."""
    pypyr.pipelinerunner.main(pipeline_name='arb pipe',
                              pipeline_context_input='arb context input',
                              working_dir='arb/dir',
                              log_level=77)

    mocked_work_dir.assert_called_once_with('arb/dir')
    mocked_run_pipeline.assert_called_once_with(
        pipeline_name='arb pipe',
        pipeline_context_input='arb context input',
        working_dir='arb/dir')


@patch('pypyr.pipelinerunner.run_pipeline', side_effect=ContextError('arb'))
@patch('pypyr.moduleloader.set_working_directory')
def test_main_fail(mocked_work_dir, mocked_run_pipeline):
    """main raises unhandled error on pipeline failure."""

    with pytest.raises(ContextError) as err_info:
        pypyr.pipelinerunner.main(pipeline_name='arb pipe',
                                  pipeline_context_input='arb context input',
                                  working_dir='arb/dir',
                                  log_level=77)

    assert repr(err_info.value) == ("ContextError('arb',)")

    mocked_work_dir.assert_called_once_with('arb/dir')
    mocked_run_pipeline.assert_called_once_with(
        pipeline_name='arb pipe',
Ejemplo n.º 14
0
def control_of_flow_instruction(name, instruction_type, context, context_key):
    """Run a control of flow instruction.

    The step config in the context dict looks like this:
        <<instruction-name>>: <<cmd string>>. Mandatory.

        OR, as a dict
        <<instruction-name:
            groups: <<str>> or <<list of str>> - mandatory.
            success: <<str>>
            failure: <<str>>

    Args:
        name: Unique name for step. Likely __name__ of calling step.
        instruction_type: Type - must inherit from
                          pypyr.errors.ControlOfFlowInstruction
        context: pypyr.context.Context. Look for config in this context
                 instance.
        context_key: str name of step config in context.

    """
    assert name, ("name parameter must exist for a ControlOfFlowStep.")
    assert context, ("context param must exist for ControlOfFlowStep.")
    # this way, logs output as the calling step, which makes more sense
    # to end-user than a mystery steps.dsl.blah logging output.
    logger = logging.getLogger(name)
    logger.debug("starting")

    context.assert_key_has_value(key=context_key, caller=name)
    original_config = (context_key, context[context_key])

    config = context.get_formatted(context_key)

    if isinstance(config, str):
        groups = [config]
        success_group = None
        failure_group = None
    elif isinstance(config, list):
        groups = config
        success_group = None
        failure_group = None
    elif isinstance(config, dict):
        if 'groups' not in config:
            raise KeyNotInContextError(
                f"{context_key} needs a child key 'groups', which should "
                "be a list or a str with the step-group name(s) you want "
                f"to run. This is for step {name}.")
        groups = config['groups']
        if not groups:
            raise KeyInContextHasNoValueError(
                f"{context_key}.groups must have a value for step {name}")

        if isinstance(groups, str):
            groups = [groups]

        success_group = config.get('success', None)
        failure_group = config.get('failure', None)
    else:
        raise ContextError(
            f"{context_key} needs a child key 'groups', which should "
            "be a list or a str with the step-group name(s) you want "
            f"to run. This is for step {name}. Instead, you've got {config}")

    if success_group is not None and not isinstance(success_group, str):
        raise ContextError(
            f"{context_key}.success must be a string for {name}.")

    if failure_group is not None and not isinstance(failure_group, str):
        raise ContextError(
            f"{context_key}.failure must be a string for {name}.")

    logger.info(
        ("step %s about to hand over control with %s: Will run groups: %s "
         " with success %s and failure %s"), name, context_key, groups,
        success_group, failure_group)
    raise instruction_type(groups=groups,
                           success_group=success_group,
                           failure_group=failure_group,
                           original_config=original_config)
Ejemplo n.º 15
0
def test_get_error_name_canonical():
    """Other error returns modulename.name on get_error_name."""
    assert get_error_name(ContextError('blah')) == 'pypyr.errors.ContextError'