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"
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',)")
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)
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}.")
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")
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"
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())
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)
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
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}.")
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)
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',
@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',
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)
def test_get_error_name_canonical(): """Other error returns modulename.name on get_error_name.""" assert get_error_name(ContextError('blah')) == 'pypyr.errors.ContextError'