← Back to team overview

testtools-dev team mailing list archive

[Merge] lp:~jml/testtools/try-import into lp:testtools

 

Jonathan Lange has proposed merging lp:~jml/testtools/try-import into lp:testtools.

Requested reviews:
  testtools developers (testtools-dev)


This branch adds a couple of new helpers to testtools: try_import and try_imports.  They are both there to make conditional imports easier.

Arguably, they aren't exactly on-topic for testtools. However, they are useful and I'd like to re-use them in other code and they aren't quite big enough for the overhead of managing yet another project.

Keen to know your thoughts.
-- 
https://code.launchpad.net/~jml/testtools/try-import/+merge/39719
Your team testtools developers is requested to review the proposed merge of lp:~jml/testtools/try-import into lp:testtools.
=== modified file 'MANUAL'
--- MANUAL	2010-10-26 18:59:12 +0000
+++ MANUAL	2010-10-31 20:57:46 +0000
@@ -303,3 +303,33 @@
  * Don't expect setting .todo, .timeout or .skip attributes to do anything
  * flushLoggedErrors is not there for you.  Sorry.
  * assertFailure is not there for you.  Even more sorry.
+
+
+General helpers
+---------------
+
+Lots of the time we would like to conditionally import modules.  testtools
+needs to do this itself, and graciously extends the ability to its users.
+
+Instead of::
+
+    try:
+        from twisted.internet import defer
+    except ImportError:
+        defer = None
+
+You can do::
+
+    defer = try_import('twisted.internet.defer')
+
+
+Instead of::
+
+    try:
+        from StringIO import StringIO
+    except ImportError:
+        from io import StringIO
+
+You can do::
+
+    StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])

=== modified file 'NEWS'
--- NEWS	2010-10-31 16:42:36 +0000
+++ NEWS	2010-10-31 20:57:46 +0000
@@ -33,7 +33,10 @@
 * ``text_content`` conveniently converts a Python string to a Content object.
   (Jonathan Lange, James Westby)
 
-* `New `KeysEqual`` matcher.  (Jonathan Lange)
+* New ``KeysEqual`` matcher.  (Jonathan Lange)
+
+* New helpers for conditionally importing modules, ``try_import`` and
+  ``try_imports``.  (Jonathan Lange)
 
 
 0.9.7

=== modified file 'testtools/__init__.py'
--- testtools/__init__.py	2010-10-17 15:53:21 +0000
+++ testtools/__init__.py	2010-10-31 20:57:46 +0000
@@ -20,8 +20,14 @@
     'skipIf',
     'skipUnless',
     'ThreadsafeForwardingResult',
+    'try_import',
+    'try_imports',
     ]
 
+from testtools.helpers import (
+    try_import,
+    try_imports,
+    )
 from testtools.matchers import (
     Matcher,
     )

=== modified file 'testtools/deferredruntest.py'
--- testtools/deferredruntest.py	2010-10-31 16:33:44 +0000
+++ testtools/deferredruntest.py	2010-10-31 20:57:46 +0000
@@ -12,12 +12,9 @@
     'SynchronousDeferredRunTest',
     ]
 
-try:
-    from StringIO import StringIO
-except ImportError:
-    from io import StringIO
 import sys
 
+from testtools import try_imports
 from testtools.content import (
     Content,
     text_content,
@@ -36,6 +33,8 @@
 from twisted.python import log
 from twisted.trial.unittest import _LogObserver
 
+StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
+
 
 class SynchronousDeferredRunTest(RunTest):
     """Runner for tests that return synchronous Deferreds."""

=== added file 'testtools/helpers.py'
--- testtools/helpers.py	1970-01-01 00:00:00 +0000
+++ testtools/helpers.py	2010-10-31 20:57:46 +0000
@@ -0,0 +1,64 @@
+# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+
+__all__ = [
+    'try_import',
+    'try_imports',
+    ]
+
+
+def try_import(name, alternative=None):
+    """Attempt to import `name`.  If it fails, return `alternative`.
+
+    When supporting multiple versions of Python or optional dependencies, it
+    is useful to be able to try to import a module.
+
+    :param name: The name of the object to import, e.g. 'os.path' or
+        'os.path.join'.
+    :param alternative: The value to return if no module can be imported.
+        Defaults to None.
+    """
+    module_segments = name.split('.')
+    while module_segments:
+        module_name = '.'.join(module_segments)
+        try:
+            module = __import__(module_name)
+        except ImportError:
+            module_segments.pop()
+            continue
+        else:
+            break
+    else:
+        return alternative
+    nonexistent = object()
+    for segment in name.split('.')[1:]:
+        module = getattr(module, segment, nonexistent)
+        if module is nonexistent:
+            return alternative
+    return module
+
+
+_RAISE_EXCEPTION = object()
+def try_imports(module_names, alternative=_RAISE_EXCEPTION):
+    """Attempt to import modules.
+
+    Tries to import the first module in `module_names`.  If it can be
+    imported, we return it.  If not, we go on to the second module and try
+    that.  The process continues until we run out of modules to try.  If none
+    of the modules can be imported, either raise an exception or return the
+    provided `alternative` value.
+
+    :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.
+    :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)
+        if module:
+            return module
+    if alternative is _RAISE_EXCEPTION:
+        raise ImportError(
+            "Could not import any of: %s" % ', '.join(module_names))
+    return alternative

=== modified file 'testtools/testcase.py'
--- testtools/testcase.py	2010-10-26 16:52:16 +0000
+++ testtools/testcase.py	2010-10-31 20:57:46 +0000
@@ -14,16 +14,15 @@
     ]
 
 import copy
-try:
-    from functools import wraps
-except ImportError:
-    wraps = None
 import itertools
 import sys
 import types
 import unittest
 
-from testtools import content
+from testtools import (
+    content,
+    try_import,
+    )
 from testtools.compat import advance_iterator
 from testtools.matchers import (
     Annotate,
@@ -33,33 +32,30 @@
 from testtools.runtest import RunTest
 from testtools.testresult import TestResult
 
-
-try:
-    # Try to use the python2.7 SkipTest exception for signalling skips.
-    from unittest.case import SkipTest as TestSkipped
-except ImportError:
-    class TestSkipped(Exception):
-        """Raised within TestCase.run() when a test is skipped."""
-
-
-try:
-    # Try to use the same exceptions python 2.7 does.
-    from unittest.case import _ExpectedFailure, _UnexpectedSuccess
-except ImportError:
-    # Oops, not available, make our own.
-    class _UnexpectedSuccess(Exception):
-        """An unexpected success was raised.
-
-        Note that this exception is private plumbing in testtools' testcase
-        module.
-        """
-
-    class _ExpectedFailure(Exception):
-        """An expected failure occured.
-
-        Note that this exception is private plumbing in testtools' testcase
-        module.
-        """
+wraps = try_import('functools.wraps')
+
+class TestSkipped(Exception):
+    """Raised within TestCase.run() when a test is skipped."""
+TestSkipped = try_import('unittest.case.SkipTest', TestSkipped)
+
+
+class _UnexpectedSuccess(Exception):
+    """An unexpected success was raised.
+
+    Note that this exception is private plumbing in testtools' testcase
+    module.
+    """
+_UnexpectedSuccess = try_import(
+    'unittest.case._UnexpectedSuccess', _UnexpectedSuccess)
+
+class _ExpectedFailure(Exception):
+    """An expected failure occured.
+
+    Note that this exception is private plumbing in testtools' testcase
+    module.
+    """
+_ExpectedFailure = try_import(
+    'unittest.case._ExpectedFailure', _ExpectedFailure)
 
 
 def run_test_with(test_runner, **kwargs):

=== modified file 'testtools/tests/__init__.py'
--- testtools/tests/__init__.py	2010-10-28 20:18:39 +0000
+++ testtools/tests/__init__.py	2010-10-31 20:57:46 +0000
@@ -10,6 +10,7 @@
         test_compat,
         test_content,
         test_content_type,
+        test_helpers,
         test_matchers,
         test_monkey,
         test_runtest,
@@ -22,6 +23,7 @@
         test_compat,
         test_content,
         test_content_type,
+        test_helpers,
         test_matchers,
         test_monkey,
         test_runtest,

=== added file 'testtools/tests/test_helpers.py'
--- testtools/tests/test_helpers.py	1970-01-01 00:00:00 +0000
+++ testtools/tests/test_helpers.py	2010-10-31 20:57:46 +0000
@@ -0,0 +1,106 @@
+# Copyright (c) 2010 Jonathan M. Lange. See LICENSE for details.
+
+from testtools import TestCase
+from testtools.helpers import (
+    try_import,
+    try_imports,
+    )
+from testtools.matchers import (
+    Equals,
+    Is,
+    )
+
+
+class TestTryImport(TestCase):
+
+    def test_doesnt_exist(self):
+        # try_import('thing', foo) returns foo if 'thing' doesn't exist.
+        marker = object()
+        result = try_import('doesntexist', marker)
+        self.assertThat(result, Is(marker))
+
+    def test_None_is_default_alternative(self):
+        # try_import('thing') returns None if 'thing' doesn't exist.
+        result = try_import('doesntexist')
+        self.assertThat(result, Is(None))
+
+    def test_existing_module(self):
+        # try_import('thing', foo) imports 'thing' and returns it if it's a
+        # module that exists.
+        result = try_import('os', object())
+        import os
+        self.assertThat(result, Is(os))
+
+    def test_existing_submodule(self):
+        # try_import('thing.another', foo) imports 'thing' and returns it if
+        # it's a module that exists.
+        result = try_import('os.path', object())
+        import os
+        self.assertThat(result, Is(os.path))
+
+    def test_nonexistent_submodule(self):
+        # try_import('thing.another', foo) imports 'thing' and returns foo if
+        # 'another' doesn't exist.
+        marker = object()
+        result = try_import('os.doesntexist', marker)
+        self.assertThat(result, Is(marker))
+
+    def test_object_from_module(self):
+        # try_import('thing.object') imports 'thing' and returns
+        # 'thing.object' if 'thing' is a module and 'object' is not.
+        result = try_import('os.path.join')
+        import os
+        self.assertThat(result, Is(os.path.join))
+
+
+class TestTryImports(TestCase):
+
+    def test_doesnt_exist(self):
+        # try_imports('thing', foo) returns foo if 'thing' doesn't exist.
+        marker = object()
+        result = try_imports(['doesntexist'], marker)
+        self.assertThat(result, Is(marker))
+
+    def test_fallback(self):
+        result = try_imports(['doesntexist', 'os'])
+        import os
+        self.assertThat(result, Is(os))
+
+    def test_None_is_default_alternative(self):
+        # try_imports('thing') returns None if 'thing' doesn't exist.
+        e = self.assertRaises(
+            ImportError, try_imports, ['doesntexist', 'noreally'])
+        self.assertThat(
+            str(e),
+            Equals("Could not import any of: doesntexist, noreally"))
+
+    def test_existing_module(self):
+        # try_imports('thing', foo) imports 'thing' and returns it if it's a
+        # module that exists.
+        result = try_imports(['os'], object())
+        import os
+        self.assertThat(result, Is(os))
+
+    def test_existing_submodule(self):
+        # try_imports('thing.another', foo) imports 'thing' and returns it if
+        # it's a module that exists.
+        result = try_imports(['os.path'], object())
+        import os
+        self.assertThat(result, Is(os.path))
+
+    def test_nonexistent_submodule(self):
+        # try_imports('thing.another', foo) imports 'thing' and returns foo if
+        # 'another' doesn't exist.
+        marker = object()
+        result = try_imports(['os.doesntexist'], marker)
+        self.assertThat(result, Is(marker))
+
+    def test_fallback_submodule(self):
+        result = try_imports(['os.doesntexist', 'os.path'])
+        import os
+        self.assertThat(result, Is(os.path))
+
+
+def test_suite():
+    from unittest import TestLoader
+    return TestLoader().loadTestsFromName(__name__)

=== modified file 'testtools/tests/test_testresult.py'
--- testtools/tests/test_testresult.py	2010-10-26 10:19:56 +0000
+++ testtools/tests/test_testresult.py	2010-10-31 20:57:46 +0000
@@ -6,10 +6,6 @@
 
 import codecs
 import datetime
-try:
-    from StringIO import StringIO
-except ImportError:
-    from io import StringIO
 import doctest
 import os
 import shutil
@@ -26,6 +22,7 @@
     TextTestResult,
     ThreadsafeForwardingResult,
     testresult,
+    try_imports,
     )
 from testtools.compat import (
     _b,
@@ -45,6 +42,8 @@
     an_exc_info
     )
 
+StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
+
 
 class TestTestResultContract(TestCase):
     """Tests for the contract of TestResults."""


Follow ups