Merge lp:~mbp/testtools/try-import-error into lp:~testtools-committers/testtools/trunk

Proposed by Martin Pool
Status: Superseded
Proposed branch: lp:~mbp/testtools/try-import-error
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 155 lines (+69/-6)
2 files modified
testtools/helpers.py (+14/-4)
testtools/tests/test_helpers.py (+55/-2)
To merge this branch: bzr merge lp:~mbp/testtools/try-import-error
Reviewer Review Type Date Requested Status
testtools developers Pending
Review via email: mp+62841@code.launchpad.net

This proposal has been superseded by a proposal from 2011-06-01.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Could we get a test? It looks fine in principle.

-Rob

Revision history for this message
Martin Pool (mbp) wrote :

Yes, it can have a test. Just thought I'd send the api first.

Martin

lp:~mbp/testtools/try-import-error updated
192. By Martin Pool

Add tests for error_callback

193. By Martin Pool

Add myself to the authors list

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testtools/helpers.py'
2--- testtools/helpers.py 2011-03-22 15:25:38 +0000
3+++ testtools/helpers.py 2011-06-01 06:47:29 +0000
4@@ -6,7 +6,7 @@
5 ]
6
7
8-def try_import(name, alternative=None):
9+def try_import(name, alternative=None, error_callback=None):
10 """Attempt to import ``name``. If it fails, return ``alternative``.
11
12 When supporting multiple versions of Python or optional dependencies, it
13@@ -16,29 +16,37 @@
14 ``os.path.join``.
15 :param alternative: The value to return if no module can be imported.
16 Defaults to None.
17+ :param error_callback: If non-None, a callable that is passed the ImportError
18+ when the module cannot be loaded.
19 """
20 module_segments = name.split('.')
21+ last_error = None
22 while module_segments:
23 module_name = '.'.join(module_segments)
24 try:
25 module = __import__(module_name)
26- except ImportError:
27+ except ImportError, e:
28+ last_error = e
29 module_segments.pop()
30 continue
31 else:
32 break
33 else:
34+ if last_error is not None and error_callback is not None:
35+ error_callback(last_error)
36 return alternative
37 nonexistent = object()
38 for segment in name.split('.')[1:]:
39 module = getattr(module, segment, nonexistent)
40 if module is nonexistent:
41+ if last_error is not None and error_callback is not None:
42+ error_callback(last_error)
43 return alternative
44 return module
45
46
47 _RAISE_EXCEPTION = object()
48-def try_imports(module_names, alternative=_RAISE_EXCEPTION):
49+def try_imports(module_names, alternative=_RAISE_EXCEPTION, error_callback=None):
50 """Attempt to import modules.
51
52 Tries to import the first module in ``module_names``. If it can be
53@@ -50,12 +58,14 @@
54 :param module_names: A sequence of module names to try to import.
55 :param alternative: The value to return if no module can be imported.
56 If unspecified, we raise an ImportError.
57+ :param error_callback: If None, called with the ImportError for *each*
58+ module that fails to load.
59 :raises ImportError: If none of the modules can be imported and no
60 alternative value was specified.
61 """
62 module_names = list(module_names)
63 for module_name in module_names:
64- module = try_import(module_name)
65+ module = try_import(module_name, error_callback=error_callback)
66 if module:
67 return module
68 if alternative is _RAISE_EXCEPTION:
69
70=== modified file 'testtools/tests/test_helpers.py'
71--- testtools/tests/test_helpers.py 2011-01-22 17:56:00 +0000
72+++ testtools/tests/test_helpers.py 2011-06-01 06:47:29 +0000
73@@ -1,4 +1,4 @@
74-# Copyright (c) 2010 testtools developers. See LICENSE for details.
75+# Copyright (c) 2010-2011 testtools developers. See LICENSE for details.
76
77 from testtools import TestCase
78 from testtools.helpers import (
79@@ -8,7 +8,35 @@
80 from testtools.matchers import (
81 Equals,
82 Is,
83+ Not,
84 )
85+
86+
87+def check_error_callback(test, function, arg, expected_error_count,
88+ expect_result):
89+ """General test template for error_callback argument.
90+
91+ :param test: Test case instance.
92+ :param function: Either try_import or try_imports.
93+ :param arg: Name or names to import.
94+ :param expected_error_count: Expected number of calls to the callback.
95+ :param expect_result: Boolean for whether a module should
96+ ultimately be returned or not.
97+ """
98+ cb_calls = []
99+ def cb(e):
100+ test.assertIsInstance(e, ImportError)
101+ cb_calls.append(e)
102+ try:
103+ result = function(arg, error_callback=cb)
104+ except ImportError, e:
105+ test.assertFalse(expect_result)
106+ else:
107+ if expect_result:
108+ test.assertThat(result, Not(Is(None)))
109+ else:
110+ test.assertThat(result, Is(None))
111+ test.assertEquals(len(cb_calls), expected_error_count)
112
113
114 class TestTryImport(TestCase):
115@@ -51,7 +79,20 @@
116 result = try_import('os.path.join')
117 import os
118 self.assertThat(result, Is(os.path.join))
119-
120+
121+ def test_error_callback(self):
122+ # the error callback is called on failures.
123+ check_error_callback(self, try_import, 'doesntexist', 1, False)
124+
125+ def test_error_callback_missing_module_member(self):
126+ # the error callback is called on failures to find an object
127+ # inside an existing module.
128+ check_error_callback(self, try_import, 'os.nonexistent', 1, False)
129+
130+ def test_error_callback_not_on_success(self):
131+ # the error callback is not called on success.
132+ check_error_callback(self, try_import, 'os.path', 0, True)
133+
134
135 class TestTryImports(TestCase):
136
137@@ -99,6 +140,18 @@
138 result = try_imports(['os.doesntexist', 'os.path'])
139 import os
140 self.assertThat(result, Is(os.path))
141+
142+ def test_error_callback(self):
143+ # One error for every class that doesn't exist.
144+ check_error_callback(self, try_imports,
145+ ['os.doesntexist', 'os.notthiseither'],
146+ 2, False)
147+ check_error_callback(self, try_imports,
148+ ['os.doesntexist', 'os.notthiseither', 'os'],
149+ 2, True)
150+ check_error_callback(self, try_imports,
151+ ['os.path'],
152+ 0, True)
153
154
155 def test_suite():

Subscribers

People subscribed via source and target branches