testtools-dev team mailing list archive
-
testtools-dev team
-
Mailing list archive
-
Message #00265
[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