From b3416e44dad82be43d7a2c91b1823514f0076b61 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Fri, 18 Jan 2013 22:17:19 +1300 Subject: [PATCH] * Testtools now depends on extras, a small library split out from it to contain generally useful non-testing facilities. Since extras has been around for a couple of testtools releases now, we're making this into a hard dependency of testtools. (Robert Collins) * Testtools now uses setuptools rather than distutils so that we can document the extras dependency. (Robert Collins) --- .bzrignore | 1 + NEWS | 11 ++ README | 3 + doc/for-test-authors.rst | 8 +- setup.py | 8 +- testtools/__init__.py | 4 +- testtools/compat.py | 2 +- testtools/content.py | 3 +- testtools/helpers.py | 83 +---------- testtools/testcase.py | 3 +- testtools/testresult/real.py | 3 +- testtools/tests/helpers.py | 5 +- testtools/tests/test_deferredruntest.py | 3 +- testtools/tests/test_distutilscmd.py | 3 +- testtools/tests/test_fixturesupport.py | 3 +- testtools/tests/test_helpers.py | 183 ------------------------ testtools/tests/test_run.py | 3 +- testtools/tests/test_spinner.py | 3 +- testtools/tests/test_testresult.py | 3 +- testtools/tests/test_testsuite.py | 3 +- testtools/testsuite.py | 8 +- 21 files changed, 61 insertions(+), 285 deletions(-) diff --git a/.bzrignore b/.bzrignore index d6aac0d..c01568f 100644 --- a/.bzrignore +++ b/.bzrignore @@ -8,3 +8,4 @@ apidocs _trial_temp doc/_build ./.testrepository +./testtools.egg-info diff --git a/NEWS b/NEWS index 6f3cb8c..42a7f63 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,17 @@ Changes and improvements to testtools_, grouped by release. NEXT ~~~~ +Changes +------- + +* Testtools now depends on extras, a small library split out from it to contain + generally useful non-testing facilities. Since extras has been around for a + couple of testtools releases now, we're making this into a hard dependency of + testtools. (Robert Collins) + +* Testtools now uses setuptools rather than distutils so that we can document + the extras dependency. (Robert Collins) + 0.9.24 ~~~~~~ diff --git a/README b/README index dbc685b..cb08d81 100644 --- a/README +++ b/README @@ -36,6 +36,9 @@ Required Dependencies If you would like to use testtools for earlier Python's, please use testtools 0.9.15. + * extras (helpers that we intend to push into Python itself in the near + future). + Optional Dependencies --------------------- diff --git a/doc/for-test-authors.rst b/doc/for-test-authors.rst index c9e6c6a..57fd9e2 100644 --- a/doc/for-test-authors.rst +++ b/doc/for-test-authors.rst @@ -1288,7 +1288,7 @@ Conditional imports ------------------- 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. +uses the small library extras to do this. This used to be part of testtools. Instead of:: @@ -1317,9 +1317,9 @@ You can do:: Safe attribute testing ---------------------- -``hasattr`` is broken_ on many versions of Python. testtools provides -``safe_hasattr``, which can be used to safely test whether an object has a -particular attribute. +``hasattr`` is broken_ on many versions of Python. The helper ``safe_hasattr`` +can be used to safely test whether an object has a particular attribute. Like +``try_import`` this used to be in testtools but is now in extras. Nullary callables diff --git a/setup.py b/setup.py index 7ecd6d2..fea86b5 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Distutils installer for testtools.""" -from distutils.core import setup +from setuptools import setup import email import os @@ -82,4 +82,8 @@ setup(name='testtools', 'testtools.tests.matchers', ], cmdclass={'test': testtools.TestCommand}, - zip_safe=False) + zip_safe=False, + install_requires=[ + 'extras', + ], + ) diff --git a/testtools/__init__.py b/testtools/__init__.py index d722ce5..7b36344 100644 --- a/testtools/__init__.py +++ b/testtools/__init__.py @@ -30,10 +30,12 @@ __all__ = [ 'try_imports', ] -from testtools.helpers import ( +# Compat - removal announced in 0.9.25. +from extras import ( try_import, try_imports, ) + from testtools.matchers._impl import ( Matcher, ) diff --git a/testtools/compat.py b/testtools/compat.py index 375eca2..bb767ac 100644 --- a/testtools/compat.py +++ b/testtools/compat.py @@ -27,7 +27,7 @@ import sys import traceback import unicodedata -from testtools.helpers import try_imports +from extras import try_imports BytesIO = try_imports(['StringIO.StringIO', 'io.BytesIO']) StringIO = try_imports(['StringIO.StringIO', 'io.StringIO']) diff --git a/testtools/content.py b/testtools/content.py index 8bd4a22..79ff027 100644 --- a/testtools/content.py +++ b/testtools/content.py @@ -17,7 +17,8 @@ import os import sys import traceback -from testtools import try_import +from extras import try_import + from testtools.compat import _b, _format_exc_info, str_is_unicode, _u from testtools.content_type import ContentType, JSON, UTF8_TEXT diff --git a/testtools/helpers.py b/testtools/helpers.py index 2595c1d..401d2cc 100644 --- a/testtools/helpers.py +++ b/testtools/helpers.py @@ -8,83 +8,12 @@ __all__ = [ import sys - -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 - 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. - :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: - last_error = sys.exc_info()[1] - 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, error_callback=None): - """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. - :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, error_callback=error_callback) - if module: - return module - if alternative is _RAISE_EXCEPTION: - raise ImportError( - "Could not import any of: %s" % ', '.join(module_names)) - return alternative - - -def safe_hasattr(obj, attr, _marker=object()): - """Does 'obj' have an attribute 'attr'? - - Use this rather than built-in hasattr, as the built-in swallows exceptions - in some versions of Python and behaves unpredictably with respect to - properties. - """ - return getattr(obj, attr, _marker) is not _marker +# Compat - removal announced in 0.9.25. +from extras import ( + safe_hasattr, + try_import, + try_imports, + ) def map_values(function, dictionary): diff --git a/testtools/testcase.py b/testtools/testcase.py index fc5f863..cda472e 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -20,9 +20,10 @@ import sys import types import unittest +from extras import try_import + from testtools import ( content, - try_import, ) from testtools.compat import ( advance_iterator, diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 0a69872..7112ab6 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -16,12 +16,13 @@ import datetime import sys import unittest +from extras import safe_hasattr + from testtools.compat import all, str_is_unicode, _u from testtools.content import ( text_content, TracebackContent, ) -from testtools.helpers import safe_hasattr from testtools.tags import TagContext # From http://docs.python.org/library/datetime.html diff --git a/testtools/tests/helpers.py b/testtools/tests/helpers.py index ade2d96..4024bac 100644 --- a/testtools/tests/helpers.py +++ b/testtools/tests/helpers.py @@ -8,10 +8,9 @@ __all__ = [ import sys +from extras import safe_hasattr + from testtools import TestResult -from testtools.helpers import ( - safe_hasattr, - ) from testtools.content import TracebackContent from testtools import runtest diff --git a/testtools/tests/test_deferredruntest.py b/testtools/tests/test_deferredruntest.py index 3373c06..aa2d9a6 100644 --- a/testtools/tests/test_deferredruntest.py +++ b/testtools/tests/test_deferredruntest.py @@ -5,6 +5,8 @@ import os import signal +from extras import try_import + from testtools import ( skipIf, TestCase, @@ -13,7 +15,6 @@ from testtools import ( from testtools.content import ( text_content, ) -from testtools.helpers import try_import from testtools.matchers import ( Equals, KeysEqual, diff --git a/testtools/tests/test_distutilscmd.py b/testtools/tests/test_distutilscmd.py index 59762df..7bfc1fa 100644 --- a/testtools/tests/test_distutilscmd.py +++ b/testtools/tests/test_distutilscmd.py @@ -4,12 +4,13 @@ from distutils.dist import Distribution +from extras import try_import + from testtools.compat import ( _b, _u, BytesIO, ) -from testtools.helpers import try_import fixtures = try_import('fixtures') import testtools diff --git a/testtools/tests/test_fixturesupport.py b/testtools/tests/test_fixturesupport.py index cff9eb4..2ccd1e8 100644 --- a/testtools/tests/test_fixturesupport.py +++ b/testtools/tests/test_fixturesupport.py @@ -2,13 +2,14 @@ import unittest +from extras import try_import + from testtools import ( TestCase, content, content_type, ) from testtools.compat import _b, _u -from testtools.helpers import try_import from testtools.testresult.doubles import ( ExtendedTestResult, ) diff --git a/testtools/tests/test_helpers.py b/testtools/tests/test_helpers.py index 98da534..848c2f0 100644 --- a/testtools/tests/test_helpers.py +++ b/testtools/tests/test_helpers.py @@ -1,196 +1,13 @@ # Copyright (c) 2010-2012 testtools developers. See LICENSE for details. from testtools import TestCase -from testtools.helpers import ( - try_import, - try_imports, - ) -from testtools.matchers import ( - Equals, - Is, - Not, - ) from testtools.tests.helpers import ( FullStackRunTest, hide_testtools_stack, is_stack_hidden, - safe_hasattr, ) -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: - 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 TestSafeHasattr(TestCase): - - def test_attribute_not_there(self): - class Foo(object): - pass - self.assertEqual(False, safe_hasattr(Foo(), 'anything')) - - def test_attribute_there(self): - class Foo(object): - pass - foo = Foo() - foo.attribute = None - self.assertEqual(True, safe_hasattr(foo, 'attribute')) - - def test_property_there(self): - class Foo(object): - @property - def attribute(self): - return None - foo = Foo() - self.assertEqual(True, safe_hasattr(foo, 'attribute')) - - def test_property_raises(self): - class Foo(object): - @property - def attribute(self): - 1/0 - foo = Foo() - self.assertRaises(ZeroDivisionError, safe_hasattr, foo, 'attribute') - - -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)) - - 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): - - 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_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) - - class TestStackHiding(TestCase): run_tests_with = FullStackRunTest diff --git a/testtools/tests/test_run.py b/testtools/tests/test_run.py index 5971a4b..f53d59f 100644 --- a/testtools/tests/test_run.py +++ b/testtools/tests/test_run.py @@ -4,11 +4,12 @@ from unittest import TestSuite +from extras import try_import + from testtools.compat import ( _b, StringIO, ) -from testtools.helpers import try_import fixtures = try_import('fixtures') import testtools diff --git a/testtools/tests/test_spinner.py b/testtools/tests/test_spinner.py index 3d677bd..3ba3ce8 100644 --- a/testtools/tests/test_spinner.py +++ b/testtools/tests/test_spinner.py @@ -5,11 +5,12 @@ import os import signal +from extras import try_import + from testtools import ( skipIf, TestCase, ) -from testtools.helpers import try_import from testtools.matchers import ( Equals, Is, diff --git a/testtools/tests/test_testresult.py b/testtools/tests/test_testresult.py index c935b14..64bb743 100644 --- a/testtools/tests/test_testresult.py +++ b/testtools/tests/test_testresult.py @@ -15,6 +15,8 @@ import threading from unittest import TestSuite import warnings +from extras import safe_hasattr + from testtools import ( ExtendedToOriginalDecorator, MultiTestResult, @@ -44,7 +46,6 @@ from testtools.content import ( TracebackContent, ) from testtools.content_type import ContentType, UTF8_TEXT -from testtools.helpers import safe_hasattr from testtools.matchers import ( Contains, DocTestMatches, diff --git a/testtools/tests/test_testsuite.py b/testtools/tests/test_testsuite.py index 3fc837c..d220709 100644 --- a/testtools/tests/test_testsuite.py +++ b/testtools/tests/test_testsuite.py @@ -6,13 +6,14 @@ __metaclass__ = type import unittest +from extras import try_import + from testtools import ( ConcurrentTestSuite, iterate_tests, PlaceHolder, TestCase, ) -from testtools.helpers import try_import from testtools.testsuite import FixtureSuite, iterate_tests, sorted_tests from testtools.tests.helpers import LoggingResult diff --git a/testtools/testsuite.py b/testtools/testsuite.py index 67ace56..668831c 100644 --- a/testtools/testsuite.py +++ b/testtools/testsuite.py @@ -9,13 +9,13 @@ __all__ = [ 'sorted_tests', ] -from testtools.helpers import safe_hasattr, try_imports - -Queue = try_imports(['Queue.Queue', 'queue.Queue']) - import threading import unittest +from extras import safe_hasattr, try_imports + +Queue = try_imports(['Queue.Queue', 'queue.Queue']) + import testtools