Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reload hooks between uses #227

Merged
merged 3 commits into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- global variables in hooks are now reloaded between uses to mimic functionality present in `>1.5.0`

## [1.6.0] - 2020-04-07
### Fixed
Expand Down
2 changes: 1 addition & 1 deletion runway/cfngin/hooks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def handle_hooks(stage, hooks, provider, context): # pylint: disable=too-many-s
continue

try:
method = load_object_from_string(hook.path)
method = load_object_from_string(hook.path, try_reload=True)
except (AttributeError, ImportError):
LOGGER.exception("Unable to load method at %s:", hook.path)
if required:
Expand Down
30 changes: 24 additions & 6 deletions runway/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,18 @@ def environ(env=None, **kwargs):
os.environ[key] = val


def load_object_from_string(fqcn):
def load_object_from_string(fqcn, try_reload=False):
"""Convert "." delimited strings to a python object.

Given a "." delimited string representing the full path to an object
(function, class, variable) inside a module, return that object.
Args:
fqcn (str): A "." delimited string representing the full path to an
object (function, class, variable) inside a module
try_reload (bool): Try to reload the module so any global variables
set within the file during import are reloaded. This only applies
to modules that are already imported and are not builtin.

Returns:
Any: Object being imported from the provided path.

Example::

Expand All @@ -359,9 +366,20 @@ def load_object_from_string(fqcn):
"""
module_path = "__main__"
object_name = fqcn
if "." in fqcn:
module_path, object_name = fqcn.rsplit(".", 1)
importlib.import_module(module_path)
if '.' in object_name:
module_path, object_name = fqcn.rsplit('.', 1)
if (
try_reload and
sys.modules.get(module_path) and
module_path.split('.')[0] not in sys.builtin_module_names # skip builtins
):
# TODO remove is next major release; backport circumvents expected functionality
#
# pylint<2.3.1 incorrectly identifies this
# pylint: disable=too-many-function-args
six.moves.reload_module(sys.modules[module_path])
else:
importlib.import_module(module_path)
return getattr(sys.modules[module_path], object_name)


Expand Down
9 changes: 8 additions & 1 deletion tests/cfngin/hooks/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import queue
import unittest

from mock import call, patch

from runway.cfngin.config import Hook
from runway.cfngin.hooks.utils import handle_hooks

Expand Down Expand Up @@ -49,14 +51,19 @@ def test_default_required_hook(self):
with self.assertRaises(AttributeError):
handle_hooks("missing", hooks, self.provider, self.context)

def test_valid_hook(self):
@patch('runway.cfngin.hooks.utils.load_object_from_string')
def test_valid_hook(self, mock_load):
"""Test valid hook."""
mock_load.side_effect = [mock_hook, MockHook]
hooks = [
Hook({"path": "tests.cfngin.hooks.test_utils.mock_hook",
"required": True}),
Hook({'path': 'tests.cfngin.hooks.test_utils.MockHook',
'required': True})]
handle_hooks('pre_build', hooks, self.provider, self.context)
assert mock_load.call_count == 2
mock_load.assert_has_calls([call(hooks[0].path, try_reload=True),
call(hooks[1].path, try_reload=True)])
good = HOOK_QUEUE.get_nowait()
self.assertEqual(good["provider"].region, "us-east-1")
with self.assertRaises(queue.Empty):
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Empty module for python import traversal."""
4 changes: 4 additions & 0 deletions tests/fixtures/mock_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Mock hooks."""
import os

GLOBAL_VALUE = os.getenv('AWS_DEFAULT_REGION')
35 changes: 34 additions & 1 deletion tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import string
import sys

from mock import patch
from mock import MagicMock, patch

from runway.util import MutableMap, argv, environ, load_object_from_string

Expand Down Expand Up @@ -124,3 +124,36 @@ def test_load_object_from_string():
)
for test in tests:
assert load_object_from_string(test[0]) is test[1]

obj_path = 'tests.fixtures.mock_hooks.GLOBAL_VALUE'
# check value from os.environ
assert load_object_from_string(obj_path, try_reload=True) == 'us-east-1'

with environ({'AWS_DEFAULT_REGION': 'us-west-2'}):
# check value from os.environ after changing it to ensure reload
assert load_object_from_string(obj_path, try_reload=True) == 'us-west-2'


@patch('runway.util.six')
def test_load_object_from_string_reload_conditions(mock_six):
"""Test load_object_from_string reload conditions."""
mock_six.moves.reload_module.return_value = MagicMock()
builtin_test = 'sys.version_info'
mock_hook = 'tests.fixtures.mock_hooks.GLOBAL_VALUE'

try:
del sys.modules['tests.fixtures.mock_hooks']
except: # noqa pylint: disable=bare-except
pass

load_object_from_string(builtin_test, try_reload=False)
mock_six.moves.reload_module.assert_not_called()

load_object_from_string(builtin_test, try_reload=True)
mock_six.moves.reload_module.assert_not_called()

load_object_from_string(mock_hook, try_reload=True)
mock_six.moves.reload_module.assert_not_called()

load_object_from_string(mock_hook, try_reload=True)
mock_six.moves.reload_module.assert_called_once()