Skip to content

Commit f4685a7

Browse files
authored
reload hooks between uses (#227)
* reload hooks between uses * fix test by ensuring that the mock_hooks module is always unimported * reword docstring, add TODO to remove in next major release
1 parent bd854a4 commit f4685a7

File tree

7 files changed

+74
-9
lines changed

7 files changed

+74
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

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

911
## [1.6.0] - 2020-04-07
1012
### Fixed

runway/cfngin/hooks/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def handle_hooks(stage, hooks, provider, context): # pylint: disable=too-many-s
6464
continue
6565

6666
try:
67-
method = load_object_from_string(hook.path)
67+
method = load_object_from_string(hook.path, try_reload=True)
6868
except (AttributeError, ImportError):
6969
LOGGER.exception("Unable to load method at %s:", hook.path)
7070
if required:

runway/util.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,18 @@ def environ(env=None, **kwargs):
344344
os.environ[key] = val
345345

346346

347-
def load_object_from_string(fqcn):
347+
def load_object_from_string(fqcn, try_reload=False):
348348
"""Convert "." delimited strings to a python object.
349349
350-
Given a "." delimited string representing the full path to an object
351-
(function, class, variable) inside a module, return that object.
350+
Args:
351+
fqcn (str): A "." delimited string representing the full path to an
352+
object (function, class, variable) inside a module
353+
try_reload (bool): Try to reload the module so any global variables
354+
set within the file during import are reloaded. This only applies
355+
to modules that are already imported and are not builtin.
356+
357+
Returns:
358+
Any: Object being imported from the provided path.
352359
353360
Example::
354361
@@ -359,9 +366,20 @@ def load_object_from_string(fqcn):
359366
"""
360367
module_path = "__main__"
361368
object_name = fqcn
362-
if "." in fqcn:
363-
module_path, object_name = fqcn.rsplit(".", 1)
364-
importlib.import_module(module_path)
369+
if '.' in object_name:
370+
module_path, object_name = fqcn.rsplit('.', 1)
371+
if (
372+
try_reload and
373+
sys.modules.get(module_path) and
374+
module_path.split('.')[0] not in sys.builtin_module_names # skip builtins
375+
):
376+
# TODO remove is next major release; backport circumvents expected functionality
377+
#
378+
# pylint<2.3.1 incorrectly identifies this
379+
# pylint: disable=too-many-function-args
380+
six.moves.reload_module(sys.modules[module_path])
381+
else:
382+
importlib.import_module(module_path)
365383
return getattr(sys.modules[module_path], object_name)
366384

367385

tests/cfngin/hooks/test_utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import queue
44
import unittest
55

6+
from mock import call, patch
7+
68
from runway.cfngin.config import Hook
79
from runway.cfngin.hooks.utils import handle_hooks
810

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

52-
def test_valid_hook(self):
54+
@patch('runway.cfngin.hooks.utils.load_object_from_string')
55+
def test_valid_hook(self, mock_load):
5356
"""Test valid hook."""
57+
mock_load.side_effect = [mock_hook, MockHook]
5458
hooks = [
5559
Hook({"path": "tests.cfngin.hooks.test_utils.mock_hook",
5660
"required": True}),
5761
Hook({'path': 'tests.cfngin.hooks.test_utils.MockHook',
5862
'required': True})]
5963
handle_hooks('pre_build', hooks, self.provider, self.context)
64+
assert mock_load.call_count == 2
65+
mock_load.assert_has_calls([call(hooks[0].path, try_reload=True),
66+
call(hooks[1].path, try_reload=True)])
6067
good = HOOK_QUEUE.get_nowait()
6168
self.assertEqual(good["provider"].region, "us-east-1")
6269
with self.assertRaises(queue.Empty):

tests/fixtures/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Empty module for python import traversal."""

tests/fixtures/mock_hooks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Mock hooks."""
2+
import os
3+
4+
GLOBAL_VALUE = os.getenv('AWS_DEFAULT_REGION')

tests/test_util.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import string
55
import sys
66

7-
from mock import patch
7+
from mock import MagicMock, patch
88

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

@@ -124,3 +124,36 @@ def test_load_object_from_string():
124124
)
125125
for test in tests:
126126
assert load_object_from_string(test[0]) is test[1]
127+
128+
obj_path = 'tests.fixtures.mock_hooks.GLOBAL_VALUE'
129+
# check value from os.environ
130+
assert load_object_from_string(obj_path, try_reload=True) == 'us-east-1'
131+
132+
with environ({'AWS_DEFAULT_REGION': 'us-west-2'}):
133+
# check value from os.environ after changing it to ensure reload
134+
assert load_object_from_string(obj_path, try_reload=True) == 'us-west-2'
135+
136+
137+
@patch('runway.util.six')
138+
def test_load_object_from_string_reload_conditions(mock_six):
139+
"""Test load_object_from_string reload conditions."""
140+
mock_six.moves.reload_module.return_value = MagicMock()
141+
builtin_test = 'sys.version_info'
142+
mock_hook = 'tests.fixtures.mock_hooks.GLOBAL_VALUE'
143+
144+
try:
145+
del sys.modules['tests.fixtures.mock_hooks']
146+
except: # noqa pylint: disable=bare-except
147+
pass
148+
149+
load_object_from_string(builtin_test, try_reload=False)
150+
mock_six.moves.reload_module.assert_not_called()
151+
152+
load_object_from_string(builtin_test, try_reload=True)
153+
mock_six.moves.reload_module.assert_not_called()
154+
155+
load_object_from_string(mock_hook, try_reload=True)
156+
mock_six.moves.reload_module.assert_not_called()
157+
158+
load_object_from_string(mock_hook, try_reload=True)
159+
mock_six.moves.reload_module.assert_called_once()

0 commit comments

Comments
 (0)