Skip to content

Commit

Permalink
pypyr.steps.py add py form input on top of pycode. closes #204.
Browse files Browse the repository at this point in the history
  • Loading branch information
yaythomas committed Nov 25, 2020
1 parent dc95053 commit f1aec85
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 44 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ steps:
cmd: echo ninja shell power | grep '^ninja.*r$'
- name: pypyr.steps.py
in:
pycode: print('any python you like')
py: print('any python you like')
- name: pypyr.steps.cmd
while:
max: 3
Expand Down
12 changes: 7 additions & 5 deletions ops/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ get_version:
in:
defaults:
isConfigSet: false
is_version_module_loaded: False
- name: pypyr.steps.call
comment: set default config & environment values only if not already set.
skip: '{isConfigSet}'
Expand All @@ -123,13 +124,14 @@ get_version:
- name: pypyr.steps.py
description: --> get version
in:
pycode: |
py: |
import importlib
version_module = importlib.import_module(context['version_module_name'])
if context.get('is_version_module_loaded'):
version_module = importlib.import_module(version_module_name)
if is_version_module_loaded:
importlib.reload(version_module)
context['version'] = f'{version_module.__version__}'
context['is_version_module_loaded'] = True
version = f'{version_module.__version__}'
is_version_module_loaded = True
- name: pypyr.steps.echo
in:
echoMe: version is {version}
Expand Down
49 changes: 38 additions & 11 deletions pypyr/steps/py.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,47 @@
def run_step(context):
"""Execute dynamic python code.
Context is a dictionary or dictionary-like.
Context must contain key 'pycode'
Will exec context['pycode'] as dynamically interpreted python statements.
context is mandatory. When you execute the pipeline, it should look
something like this:
pipeline-runner [name here] 'pycode=print(1+1)'.
Takes two forms of input:
py: exec contents as dynamically interpreted python statements, with
contents of context available as vars.
pycode: exec contents as dynamically interpreted python statements,
with the context object itself available as a var.
Args:
context (pypyr.context.Context): Mandatory.
Context is a dictionary or dictionary-like.
Context must contain key 'py' or 'pycode'
"""
logger.debug("started")
context.assert_key_has_value(key='pycode', caller=__name__)

if 'pycode' in context:
exec_pycode(context)
else:
context.assert_key_has_value(key='py', caller=__name__)
exec(context['py'], {}, context)

logger.debug("exec output context merged with pipeline context")

logger.debug("done")


def exec_pycode(context):
"""Exec contents of pycode.
This form of execute means pycode does not have the contents of context in
the locals() namespace, so referencing context needs to do:
a = context['myvar']
Rather than just
a = myvar
Args:
context (pypyr.context.Content): context containing `pycode` key.
Returns:
None. Any mutations to content is on the input arg instance itself.
"""
context.assert_key_has_value(key='pycode', caller=__name__)
logger.debug("Executing python string: %s", context['pycode'])
locals_dictionary = locals()
exec(context['pycode'], globals(), locals_dictionary)
Expand All @@ -30,6 +60,3 @@ def run_step(context):
logger.debug("looking for context update in exec")
exec_context = locals_dictionary['context']
context.update(exec_context)
logger.debug("exec output context merged with pipeline context")

logger.debug("done")
6 changes: 3 additions & 3 deletions tests/integration/pypyr/pipelinerunner_int_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def test_pipeline_runner_main_with_context_relative_working_dir(
assert len(out) == 4
assert out['argList'] == ['A', 'B', 'C', 'raise on success']
assert out['set_in_pipe'] == 456
assert out['pycode'] == "raise ValueError('err from on_success')"
assert out['py'] == "raise ValueError('err from on_success')"

assert len(out['runErrors']) == 1
out_run_error = out['runErrors'][0]
Expand Down Expand Up @@ -238,7 +238,7 @@ def test_pipeline_runner_main_with_context_minimal_with_failure_handled():
assert len(out) == 4
assert out['argList'] == ['A', 'B', 'C', 'raise on success']
assert out['set_in_pipe'] == 456
assert out['pycode'] == "raise ValueError('err from on_success')"
assert out['py'] == "raise ValueError('err from on_success')"

assert len(out['runErrors']) == 1
out_run_error = out['runErrors'][0]
Expand Down Expand Up @@ -270,7 +270,7 @@ def test_pipeline_runner_main_with_context_with_failure_handled():
assert out.working_dir == Path.cwd()

assert len(out) == 2
assert out['pycode'] == "raise ValueError('err from sg3')"
assert out['py'] == "raise ValueError('err from sg3')"

assert len(out['runErrors']) == 1
out_run_error = out['runErrors'][0]
Expand Down
6 changes: 3 additions & 3 deletions tests/pipelines/api/main-all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ sg3:
echoMe: sg3
- name: pypyr.steps.py
in:
pycode: raise ValueError('err from sg3')
py: raise ValueError('err from sg3')

sh:
- name: pypyr.steps.echo
Expand All @@ -58,7 +58,7 @@ sh:
- name: pypyr.steps.py
run: !py argList and 'raise on sh' in argList
in:
pycode: raise ValueError('err from sh')
py: raise ValueError('err from sh')

fh:
- name: pypyr.steps.echo
Expand All @@ -74,7 +74,7 @@ on_success:
- name: pypyr.steps.py
run: !py argList and 'raise on success' in argList
in:
pycode: raise ValueError('err from on_success')
py: raise ValueError('err from on_success')

on_failure:
- name: pypyr.steps.echo
Expand Down
4 changes: 2 additions & 2 deletions tests/pipelines/errors/fail-handler-also-fails.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
steps:
- name: pypyr.steps.py
in:
pycode: raise TypeError('O.G err')
py: raise TypeError('O.G err')
- name: pypyr.steps.echo
in:
echoMe: unreachable
Expand All @@ -12,7 +12,7 @@ on_failure:
echoMe: A
- name: pypyr.steps.py
in:
pycode: raise ValueError('arb')
py: raise ValueError('arb')
- name: pypyr.steps.echo
in:
echoMe: unreachable
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sg1:
echoMe: D
- name: pypyr.steps.py
in:
pycode: raise ValueError('arb')
py: raise ValueError('arb')
- name: pypyr.steps.echo
in:
echoMe: unreachable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sg1:
echoMe: D
- name: pypyr.steps.py
in:
pycode: raise ValueError('arb')
py: raise ValueError('arb')
- name: pypyr.steps.echo
in:
echoMe: unreachable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sg1:
echoMe: D
- name: pypyr.steps.py
in:
pycode: raise ValueError('arb')
py: raise ValueError('arb')
- name: pypyr.steps.echo
in:
echoMe: unreachable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ steps:
echoMe: B.parent
- name: pypyr.steps.py
in:
pycode: raise ValueError('arb')
py: raise ValueError('arb')
- name: pypyr.steps.echo
in:
echoMe: unreachable
Expand Down
2 changes: 1 addition & 1 deletion tests/pipelines/errors/fail-handler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ steps:
echoMe: A
- name: pypyr.steps.py
in:
pycode: raise ValueError('arb')
py: raise ValueError('arb')
- name: pypyr.steps.echo
in:
echoMe: unreachable
Expand Down
2 changes: 1 addition & 1 deletion tests/pipelines/errors/fail-no-handler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ steps:
echoMe: A
- name: pypyr.steps.py
in:
pycode: raise ValueError('arb')
py: raise ValueError('arb')
- name: pypyr.steps.echo
in:
echoMe: unreachable
113 changes: 100 additions & 13 deletions tests/unit/pypyr/steps/py_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,99 @@
import pypyr.steps.py
import pytest

# region py


def test_py_existing_key():
"""Py expression can update existing key."""
context = Context({'x': 123, 'py': 'x = abs(-42)'})
assert context['x'] == 123
pypyr.steps.py.run_step(context)
assert context['x'] == 42


def test_py_with_import():
"""Py expression can use imports."""
context = Context({'y': 4, 'py': 'import math\nx = math.sqrt(y)'})
assert context['y'] == 4
assert 'x' not in context
pypyr.steps.py.run_step(context)
assert context['x'] == 2
assert context['y'] == 4


def test_py_single_code():
"""One word shell command works."""
context = Context({'pycode': 'print(1+1)'})
context = pypyr.steps.py.run_step(context)
"""One word python function works."""
context = Context({'py': 'abs(-1-2)'})
pypyr.steps.py.run_step(context)


def test_py_sequence():
"""Sequence of py code works and touches context."""
context = Context({'py': "test = 1;"})
pypyr.steps.py.run_step(context)

context.update({'py': "test += 2"})
pypyr.steps.py.run_step(context)

context.update({'py': "test += 3"})
pypyr.steps.py.run_step(context)

assert context['test'] == 6, "context should be 6 at this point"


def test_py_sequence_with_semicolons():
"""Single py code string with semi - colons works."""
context = Context({'py':
'x = abs(-1); x+= abs(-2); x += abs(-3);'})
pypyr.steps.py.run_step(context)

assert context['py'] == 'x = abs(-1); x+= abs(-2); x += abs(-3);'
assert context['x'] == 6


def test_py_sequence_with_linefeeds():
"""Single py code string with linefeeds works."""
context = Context({'py':
'abs(-1)\nabs(-2)\nabs(-3)'})
pypyr.steps.py.run_step(context)


def test_py_error_throws():
"""Input pycode error should raise up to caller."""
with pytest.raises(AssertionError):
context = Context({'py': 'assert False'})
pypyr.steps.py.run_step(context)


def test_py_no_context_throw():
"""No pycode in context should throw assert error."""
with pytest.raises(KeyNotInContextError) as err_info:
context = Context({'blah': 'blah blah'})
pypyr.steps.py.run_step(context)

assert str(err_info.value) == ("context['py'] "
"doesn't exist. It must exist for "
"pypyr.steps.py.")


def test_py_none_context_throw():
"""None pycode in context should throw assert error."""
with pytest.raises(KeyInContextHasNoValueError):
context = Context({'py': None})
pypyr.steps.py.run_step(context)
# endregion py

# region pycode


def test_pycode_single_code():
"""One word python function works."""
context = Context({'pycode': 'abs(-1-2)'})
pypyr.steps.py.run_step(context)


def test_pycode_sequence():
"""Sequence of py code works and touches context."""
context = Context({'pycode': "context['test'] = 1;"})
pypyr.steps.py.run_step(context)
Expand All @@ -25,21 +110,21 @@ def test_py_sequence():
assert context['test'] == 6, "context should be 6 at this point"


def test_py_sequence_with_semicolons():
def test_pycode_sequence_with_semicolons():
"""Single py code string with semi - colons works."""
context = Context({'pycode':
'print(1); print(2); print(3);'})
'abs(-1); abs(-2); abs(-3);'})
pypyr.steps.py.run_step(context)

assert context == {'pycode':
'print(1); print(2); print(3);'}, ("context in and out "
"the same")
'abs(-1); abs(-2); abs(-3);'}, ("context in and out "
"the same")


def test_py_sequence_with_linefeeds():
def test_pycode_sequence_with_linefeeds():
"""Single py code string with linefeeds works."""
context = Context({'pycode':
'print(1)\nprint(2)\nprint(3)'})
'abs(-1)\nabs(-2)\nabs(-3)'})
pypyr.steps.py.run_step(context)


Expand All @@ -50,19 +135,21 @@ def test_pycode_error_throws():
pypyr.steps.py.run_step(context)


def test_no_pycode_context_throw():
def test_pycode_no__context_throw():
"""No pycode in context should throw assert error."""
with pytest.raises(KeyNotInContextError) as err_info:
context = Context({'blah': 'blah blah'})
pypyr.steps.py.run_step(context)

assert str(err_info.value) == ("context['pycode'] "
assert str(err_info.value) == ("context['py'] "
"doesn't exist. It must exist for "
"pypyr.steps.py.")


def test_empty_pycode_context_throw():
"""Empty pycode in context should throw assert error."""
def test_pycode_none_context_throw():
"""None pycode in context should throw assert error."""
with pytest.raises(KeyInContextHasNoValueError):
context = Context({'pycode': None})
pypyr.steps.py.run_step(context)

# endregion pycode

0 comments on commit f1aec85

Please sign in to comment.