diff --git a/test_settings.json b/test_settings.json index edfbccb03..bee01abd1 100644 --- a/test_settings.json +++ b/test_settings.json @@ -19,7 +19,7 @@ "expression": "rate(1 minute)" }, { - "function": "test.test_app.method", + "function": "tests.test_app.method", "event_source": { "arn": "arn:aws:sns:::1", "events": [ diff --git a/tests/test_app.py b/tests/test_app.py index ec1fe7648..ddf3c660b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -21,3 +21,5 @@ def callback(self): def prebuild_me(): print("this is a prebuild script") +def method(): + print("this is a method") diff --git a/tests/test_bad_circular_extends_settings.json b/tests/test_bad_circular_extends_settings.json index 8b8416afe..28ec20bab 100644 --- a/tests/test_bad_circular_extends_settings.json +++ b/tests/test_bad_circular_extends_settings.json @@ -19,7 +19,7 @@ "expression": "rate(1 minute)" }, { - "function": "test.test_app.method", + "function": "tests.test_app.method", "event_source": { "arn": "arn:aws:sns:::1", "events": [ diff --git a/tests/test_bad_module_paths.json b/tests/test_bad_module_paths.json new file mode 100644 index 000000000..6f194531a --- /dev/null +++ b/tests/test_bad_module_paths.json @@ -0,0 +1,16 @@ +{ + "ttt888": { + "s3_bucket": "lmbda", + "app_function": "tests.test_app.hello_world", + "delete_local_zip": true, + "debug": true, + "parameter_depth": 2, + "prebuild_script": "tests.test_app.prebuild_me", + "events": [ + { + "function": "tests.test_app.not_a_function", + "expression": "rate(1 minute)" + } + ] + } +} \ No newline at end of file diff --git a/tests/test_bad_settings.json b/tests/test_bad_settings.json index a064a783d..40114e4d1 100644 --- a/tests/test_bad_settings.json +++ b/tests/test_bad_settings.json @@ -19,7 +19,7 @@ "expression": "rate(1 minute)" }, { - "function": "test.test_app.method", + "function": "tests.test_app.method", "event_source": { "arn": "arn:aws:sns:::1", "events": [ diff --git a/tests/test_bad_stage_name_settings.json b/tests/test_bad_stage_name_settings.json index 65f1aa723..e7ff890a7 100644 --- a/tests/test_bad_stage_name_settings.json +++ b/tests/test_bad_stage_name_settings.json @@ -18,7 +18,7 @@ "expression": "rate(1 minute)" }, { - "function": "test.test_app.method", + "function": "tests.test_app.method", "event_source": { "arn": "arn:aws:sns:::1", "events": [ diff --git a/tests/test_settings.yml b/tests/test_settings.yml index 9f6afbe27..37fe6271d 100644 --- a/tests/test_settings.yml +++ b/tests/test_settings.yml @@ -14,7 +14,7 @@ ttt888: events: - function: tests.test_app.schedule_me expression: rate(1 minute) - - function: test.test_app.method + - function: tests.test_app.method event_source: arn: arn:aws:sns:::1 events: diff --git a/tests/tests.py b/tests/tests.py index 1c9a5f622..87782aac9 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -899,6 +899,12 @@ def test_bad_environment_vars_catch(self): zappa_cli.api_stage = 'ttt888' self.assertRaises(ValueError, zappa_cli.load_settings, 'tests/test_bad_environment_vars.json') + def test_function_sanity_check(self): + zappa_cli = ZappaCLI() + self.assertRaises(ClickException, zappa_cli.function_sanity_check, 'not_a_module.foo') + self.assertRaises(ClickException, zappa_cli.function_sanity_check, 'tests.test_app.not_a_function') + self.assertRaises(ClickException, zappa_cli.load_settings, 'test/test_bad_module_paths.json') + # @mock.patch('botocore.session.Session.full_config', new_callable=mock.PropertyMock) # def test_cli_init(self, mock_config): diff --git a/zappa/cli.py b/zappa/cli.py index 4169471e0..437e5ec4a 100644 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -912,7 +912,9 @@ def schedule(self): return for event in events: - self.collision_warning(event.get('function')) + if event.get('name') == 'zappa-keep-warm': + continue + self.function_sanity_check(event.get('function')) if self.stage_config.get('keep_warm', True): if not events: @@ -1724,12 +1726,15 @@ def load_settings(self, settings_file=None, session=None): setattr(self.zappa, setting, setting_val) if self.app_function: - self.collision_warning(self.app_function) + self.function_sanity_check(self.app_function) if self.app_function[-3:] == '.py': click.echo(click.style("Warning!", fg="red", bold=True) + " Your app_function is pointing to a " + click.style("file and not a function", bold=True) + "! It should probably be something like 'my_file.app', not 'my_file.py'!") + if self.exception_handler: + self.function_sanity_check(self.exception_handler) + return self.zappa def get_json_or_yaml_settings(self, settings_name="zappa_settings"): @@ -2184,12 +2189,13 @@ def execute_prebuild_script(self): prebuild_function = getattr(module_, pb_func) prebuild_function() # Call the function - def collision_warning(self, item): + def function_sanity_check(self, item): """ Given a string, print a warning if this could - collide with a Zappa core package module. + collide with a Zappa core package module, or if the + given function could not be correctly imported. - Use for app functions and events. + Use for app functions, event functions, and exception handlers. """ namespace_collisions = [ @@ -2201,6 +2207,23 @@ def collision_warning(self, item): " You may have a namespace collision with " + click.style(item, bold=True) + "! You may want to rename that file.") + module_path, function_path = item.rsplit('.', 1) + try: + module = importlib.import_module(module_path) + except ImportError: # pragma: no cover + raise ClickException( + "You have tried to reference the module " + click.style(module_path, bold=True) + + " when importing " + click.style(item, bold=True) + ", but this module cannot be " + + "imported. Please check for typos and ensure that this module is reachable.") + + try: + getattr(module, function_path) + except AttributeError: # pragma: no cover + raise ClickException( + "You have tried to reference the function " + click.style(function_path, bold=True) + + " when importing " + click.style(item, bold=True) + ", but this function cannot be " + + "imported. Please check for typos and ensure that this function is reachable.") + def deploy_api_gateway(self, api_id): cache_cluster_enabled = self.stage_config.get('cache_cluster_enabled', False) cache_cluster_size = str(self.stage_config.get('cache_cluster_size', .5))