testtools-dev team mailing list archive
-
testtools-dev team
-
Mailing list archive
-
Message #00664
[Merge] lp:~mbp/testtools/try-import-error into lp:testtools
Martin Pool has proposed merging lp:~mbp/testtools/try-import-error into lp:testtools.
Requested reviews:
testtools developers (testtools-dev)
For more details, see:
https://code.launchpad.net/~mbp/testtools/try-import-error/+merge/63074
jml pointed out try_import and try_imports. I would like to use this in Bazaar to clean up some similar existing code. We want to log the fact that a module isn't available because it might indicate an installation problem. This adds a callback to observe the error.
--
https://code.launchpad.net/~mbp/testtools/try-import-error/+merge/63074
Your team testtools developers is requested to review the proposed merge of lp:~mbp/testtools/try-import-error into lp:testtools.
=== modified file 'testtools/helpers.py'
--- testtools/helpers.py 2011-03-22 15:25:38 +0000
+++ testtools/helpers.py 2011-06-01 06:49:27 +0000
@@ -6,7 +6,7 @@
]
-def try_import(name, alternative=None):
+def try_import(name, alternative=None, error_callback=None):
"""Attempt to import ``name``. If it fails, return ``alternative``.
When supporting multiple versions of Python or optional dependencies, it
@@ -16,29 +16,37 @@
``os.path.join``.
:param alternative: The value to return if no module can be imported.
Defaults to None.
+ :param error_callback: If non-None, a callable that is passed the ImportError
+ when the module cannot be loaded.
"""
module_segments = name.split('.')
+ last_error = None
while module_segments:
module_name = '.'.join(module_segments)
try:
module = __import__(module_name)
- except ImportError:
+ except ImportError, e:
+ last_error = e
module_segments.pop()
continue
else:
break
else:
+ if last_error is not None and error_callback is not None:
+ error_callback(last_error)
return alternative
nonexistent = object()
for segment in name.split('.')[1:]:
module = getattr(module, segment, nonexistent)
if module is nonexistent:
+ if last_error is not None and error_callback is not None:
+ error_callback(last_error)
return alternative
return module
_RAISE_EXCEPTION = object()
-def try_imports(module_names, alternative=_RAISE_EXCEPTION):
+def try_imports(module_names, alternative=_RAISE_EXCEPTION, error_callback=None):
"""Attempt to import modules.
Tries to import the first module in ``module_names``. If it can be
@@ -50,12 +58,14 @@
:param module_names: A sequence of module names to try to import.
:param alternative: The value to return if no module can be imported.
If unspecified, we raise an ImportError.
+ :param error_callback: If None, called with the ImportError for *each*
+ module that fails to load.
:raises ImportError: If none of the modules can be imported and no
alternative value was specified.
"""
module_names = list(module_names)
for module_name in module_names:
- module = try_import(module_name)
+ module = try_import(module_name, error_callback=error_callback)
if module:
return module
if alternative is _RAISE_EXCEPTION:
=== modified file 'testtools/tests/test_helpers.py'
--- testtools/tests/test_helpers.py 2011-01-22 17:56:00 +0000
+++ testtools/tests/test_helpers.py 2011-06-01 06:49:27 +0000
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 testtools developers. See LICENSE for details.
+# Copyright (c) 2010-2011 testtools developers. See LICENSE for details.
from testtools import TestCase
from testtools.helpers import (
@@ -8,7 +8,35 @@
from testtools.matchers import (
Equals,
Is,
+ Not,
)
+
+
+def check_error_callback(test, function, arg, expected_error_count,
+ expect_result):
+ """General test template for error_callback argument.
+
+ :param test: Test case instance.
+ :param function: Either try_import or try_imports.
+ :param arg: Name or names to import.
+ :param expected_error_count: Expected number of calls to the callback.
+ :param expect_result: Boolean for whether a module should
+ ultimately be returned or not.
+ """
+ cb_calls = []
+ def cb(e):
+ test.assertIsInstance(e, ImportError)
+ cb_calls.append(e)
+ try:
+ result = function(arg, error_callback=cb)
+ except ImportError, e:
+ test.assertFalse(expect_result)
+ else:
+ if expect_result:
+ test.assertThat(result, Not(Is(None)))
+ else:
+ test.assertThat(result, Is(None))
+ test.assertEquals(len(cb_calls), expected_error_count)
class TestTryImport(TestCase):
@@ -51,7 +79,20 @@
result = try_import('os.path.join')
import os
self.assertThat(result, Is(os.path.join))
-
+
+ def test_error_callback(self):
+ # the error callback is called on failures.
+ check_error_callback(self, try_import, 'doesntexist', 1, False)
+
+ def test_error_callback_missing_module_member(self):
+ # the error callback is called on failures to find an object
+ # inside an existing module.
+ check_error_callback(self, try_import, 'os.nonexistent', 1, False)
+
+ def test_error_callback_not_on_success(self):
+ # the error callback is not called on success.
+ check_error_callback(self, try_import, 'os.path', 0, True)
+
class TestTryImports(TestCase):
@@ -99,6 +140,18 @@
result = try_imports(['os.doesntexist', 'os.path'])
import os
self.assertThat(result, Is(os.path))
+
+ def test_error_callback(self):
+ # One error for every class that doesn't exist.
+ check_error_callback(self, try_imports,
+ ['os.doesntexist', 'os.notthiseither'],
+ 2, False)
+ check_error_callback(self, try_imports,
+ ['os.doesntexist', 'os.notthiseither', 'os'],
+ 2, True)
+ check_error_callback(self, try_imports,
+ ['os.path'],
+ 0, True)
def test_suite():
Follow ups