Skip to content

Commit

Permalink
Allow hook to be string of Step instance
Browse files Browse the repository at this point in the history
  • Loading branch information
jdavies-st committed Jan 22, 2024
1 parent 70a263b commit 604842f
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 23 deletions.
13 changes: 13 additions & 0 deletions src/stpipe/hooks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Pre- and post-hooks
"""
import ast
import inspect

from . import function_wrapper, utilities
Expand Down Expand Up @@ -40,6 +41,18 @@ def hook_from_string_or_class(step, hooktype, num, command):
except ImportError:
# String is possibly a subproc, so handle this later
pass
except AttributeError:
# String points to an instance of a Step
# So import the class
class_string, _, params = command.partition("(")
step_class = utilities.import_class(
class_string, subclassof=Step, config_file=step.config_file
)
# Then convert rest of string to args and instantiate the class
kwargs_string = params.strip(")")
expr = ast.parse(f"dict({kwargs_string}\n)", mode="eval")
kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.body.keywords}
return step_class(**kwargs)
except TypeError:
# String points to a function
step_func = utilities.import_func(command)
Expand Down
1 change: 0 additions & 1 deletion src/stpipe/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,6 @@ def run(self, *args):
hook_results = pre_hook.run(*hook_args)
if hook_results is not None:
hook_args = hook_results
self.log.info("hook_args: %s", hook_args)
args = hook_args

self._reference_files_used = []
Expand Down
55 changes: 33 additions & 22 deletions tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ def process(self, input_data):
return input_data


class HookStep(Step):
class_alias = "myhook"

spec = """
param1 = string(default="bar")
param2 = float(default=1)
"""

def process(self, input_data):
self.log.info("Running HookStep with %s and %s", self.param1, self.param2)

return input_data


class MyPipeline(Pipeline):
class_alias = "mypipeline"

Expand All @@ -36,20 +50,6 @@ def process(self, input_data):
return result # noqa: RET504


class HookStep(Step):
class_alias = "myhook"

spec = """
param1 = string(default="bar")
param2 = float(default=1)
"""

def process(self, input_data):
self.log.info("Running HookStep with %s and %s", self.param1, self.param2)

return input_data


def hook_function(input_data):
import logging

Expand All @@ -71,7 +71,6 @@ def test_hook_as_step_class(caplog):
]
}
}

MyPipeline.call(model, steps=steps)

assert "Running HookStep with bar and 1" in caplog.text
Expand All @@ -90,7 +89,6 @@ def test_hook_as_step_instance(caplog):
]
}
}

MyPipeline.call(model, steps=steps)

assert "Running HookStep with foo and 3" in caplog.text
Expand All @@ -108,12 +106,28 @@ def test_hook_as_string_of_importable_step_class(caplog):
]
}
}

MyPipeline.call(model, steps=steps)

assert "Running HookStep" in caplog.text


def test_hook_as_string_of_step_instance(caplog):
"""Test a string of a fully-qualified Step instance w/params"""
datamodels = pytest.importorskip("stdatamodels.jwst.datamodels")
model = datamodels.ImageModel((10, 10))

steps = {
"shovelpixels": {
"post_hooks": [
"test_hooks.HookStep(param1='foo', param2=2)",
]
}
}
MyPipeline.call(model, steps=steps)

assert "Running HookStep with foo and 2" in caplog.text


def test_hook_as_string_of_importable_function(caplog):
"""Test a string of a fully-qualified function path can be a hook"""
datamodels = pytest.importorskip("stdatamodels.jwst.datamodels")
Expand All @@ -126,13 +140,12 @@ def test_hook_as_string_of_importable_function(caplog):
]
}
}

MyPipeline.call(model, steps=steps)

assert "Running hook_function on data array of size (10, 10)" in caplog.text


def test_hook_as_subproccess(caplog, tmp_cwd):
def test_hook_as_systemcall(caplog, tmp_cwd):
"""Test a string of a terminal command"""
datamodels = pytest.importorskip("stdatamodels.jwst.datamodels")
model = datamodels.ImageModel((10, 10))
Expand All @@ -149,13 +162,11 @@ def test_hook_as_subproccess(caplog, tmp_cwd):
]
}
}

MyPipeline.call(model, steps=steps)

# Logs from fitsinfo
assert "shovelpixels.post_hook0" in caplog.text
assert "SystemCall instance created" in caplog.text
assert f"hook_args: (<ImageModel(10, 10) from {filename}>,)" in caplog.text
assert "Spawning 'fitsinfo stpipe.MyPipeline.shovelpixels.post_hook0" in caplog.text

# logs from fitsheader
assert "DATAMODL= 'ImageModel'" in caplog.text

0 comments on commit 604842f

Please sign in to comment.