Add bundled copy of 'extras' python module used by newer versions of testtools/subunit.
authorJelmer Vernooij <jelmer@samba.org>
Sat, 25 Oct 2014 18:09:10 +0000 (11:09 -0700)
committerJelmer Vernooij <jelmer@samba.org>
Sat, 22 Nov 2014 01:23:10 +0000 (02:23 +0100)
Change-Id: I5ad9222ccb4228a4b16d54a578276d4b9d4e6c4d
Signed-Off-By: Jelmer Vernooij <jelmer@samba.org>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
14 files changed:
lib/extras/.gitignore [new file with mode: 0644]
lib/extras/.testr.conf [new file with mode: 0644]
lib/extras/LICENSE [new file with mode: 0644]
lib/extras/MANIFEST.in [new file with mode: 0644]
lib/extras/Makefile [new file with mode: 0644]
lib/extras/NEWS [new file with mode: 0644]
lib/extras/README.rst [new file with mode: 0644]
lib/extras/extras/__init__.py [new file with mode: 0644]
lib/extras/extras/tests/__init__.py [new file with mode: 0644]
lib/extras/extras/tests/test_extras.py [new file with mode: 0644]
lib/extras/setup.cfg [new file with mode: 0644]
lib/extras/setup.py [new file with mode: 0755]
lib/update-external.sh
lib/wscript_build

diff --git a/lib/extras/.gitignore b/lib/extras/.gitignore
new file mode 100644 (file)
index 0000000..cfc114c
--- /dev/null
@@ -0,0 +1,35 @@
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+MANIFEST
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+
+# editors
+*.swp
+*~
+
+# Testrepository
+.testrepository
diff --git a/lib/extras/.testr.conf b/lib/extras/.testr.conf
new file mode 100644 (file)
index 0000000..8a65628
--- /dev/null
@@ -0,0 +1,4 @@
+[DEFAULT]
+test_command=${PYTHON:-python} -m subunit.run discover . $LISTOPT $IDOPTION
+test_id_option=--load-list $IDFILE
+test_list_option=--list
diff --git a/lib/extras/LICENSE b/lib/extras/LICENSE
new file mode 100644 (file)
index 0000000..4dfca45
--- /dev/null
@@ -0,0 +1,26 @@
+Copyright (c) 2010-2012 the extras authors.
+
+The extras authors are:
+ * Jonathan Lange
+ * Martin Pool
+ * Robert Collins
+
+and are collectively referred to as "extras developers".
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/extras/MANIFEST.in b/lib/extras/MANIFEST.in
new file mode 100644 (file)
index 0000000..da2696e
--- /dev/null
@@ -0,0 +1,6 @@
+include LICENSE
+include Makefile
+include MANIFEST.in
+include NEWS
+include README.rst
+include .gitignore
diff --git a/lib/extras/Makefile b/lib/extras/Makefile
new file mode 100644 (file)
index 0000000..270e8d1
--- /dev/null
@@ -0,0 +1,30 @@
+# See README.rst for copyright and licensing details.
+
+PYTHON=python
+SOURCES=$(shell find extras -name "*.py")
+
+check:
+       PYTHONPATH=$(PWD) $(PYTHON) -m testtools.run extras.tests.test_suite
+
+TAGS: ${SOURCES}
+       ctags -e -R extras/
+
+tags: ${SOURCES}
+       ctags -R extras/
+
+clean:
+       rm -f TAGS tags
+       find extras -name "*.pyc" -exec rm '{}' \;
+
+### Documentation ###
+
+apidocs:
+       # pydoctor emits deprecation warnings under Ubuntu 10.10 LTS
+       PYTHONWARNINGS='ignore::DeprecationWarning' \
+               pydoctor --make-html --add-package extras \
+               --docformat=restructuredtext --project-name=extras \
+               --project-url=https://launchpad.net/extras
+
+
+.PHONY: apidocs
+.PHONY: check clean
diff --git a/lib/extras/NEWS b/lib/extras/NEWS
new file mode 100644 (file)
index 0000000..60713b8
--- /dev/null
@@ -0,0 +1,27 @@
+extras NEWS
++++++++++++
+
+Changes and improvements to extras_, grouped by release.
+
+NEXT
+~~~~
+
+0.0.3
+~~~~~
+
+* Extras setup.py would break on older testtools releases, which could break
+  installs of newer testtools due to extras then failing to install.
+  (Robert Collins)
+
+0.0.2
+~~~~~
+
+* Fix Makefile to not have cruft leftover from testtools.
+
+0.0.1
+~~~~~
+
+* Initial extraction from testtools.
+
+
+.. _extras: http://pypi.python.org/pypi/extras
diff --git a/lib/extras/README.rst b/lib/extras/README.rst
new file mode 100644 (file)
index 0000000..7d3f10b
--- /dev/null
@@ -0,0 +1,57 @@
+======
+extras
+======
+
+extras is a set of extensions to the Python standard library, originally
+written to make the code within testtools cleaner, but now split out for
+general use outside of a testing context.
+
+
+Documentation
+-------------
+
+pydoc extras is your friend. extras currently contains the following functions:
+
+* try_import
+
+* try_imports
+
+* safe_hasattr
+
+Which do what their name suggests.
+
+
+Licensing
+---------
+
+This project is distributed under the MIT license and copyright is owned by
+the extras authors. See LICENSE for details.
+
+
+Required Dependencies
+---------------------
+
+ * Python 2.6+ or 3.0+
+
+
+Bug reports and patches
+-----------------------
+
+Please report bugs using github issues at <https://github.com/testing-cabal/extras>.
+Patches can also be submitted via github.  You can mail the authors directly
+via the mailing list testtools-dev@lists.launchpad.net. (Note that Launchpad
+discards email from unknown addresses - be sure to sign up for a Launchpad
+account before mailing the list, or your mail will be silently discarded).
+
+
+History
+-------
+
+extras used to be testtools.helpers, and was factored out when folk wanted to
+use it separately.
+
+
+Thanks
+------
+
+ * Martin Pool
diff --git a/lib/extras/extras/__init__.py b/lib/extras/extras/__init__.py
new file mode 100644 (file)
index 0000000..5f16625
--- /dev/null
@@ -0,0 +1,103 @@
+# Copyright (c) 2010-2012 extras developers. See LICENSE for details.
+
+"""Extensions to the Python standard library."""
+
+import sys
+
+__all__ = [
+    'safe_hasattr',
+    'try_import',
+    'try_imports',
+    ]
+
+# same format as sys.version_info: "A tuple containing the five components of
+# the version number: major, minor, micro, releaselevel, and serial. All
+# values except releaselevel are integers; the release level is 'alpha',
+# 'beta', 'candidate', or 'final'. The version_info value corresponding to the
+# Python version 2.0 is (2, 0, 0, 'final', 0)."  Additionally we use a
+# releaselevel of 'dev' for unreleased under-development code.
+#
+# If the releaselevel is 'alpha' then the major/minor/micro components are not
+# established at this point, and setup.py will use a version of next-$(revno).
+# If the releaselevel is 'final', then the tarball will be major.minor.micro.
+# Otherwise it is major.minor.micro~$(revno).
+
+__version__ = (0, 0, 3, 'final', 0)
+
+
+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
diff --git a/lib/extras/extras/tests/__init__.py b/lib/extras/extras/tests/__init__.py
new file mode 100644 (file)
index 0000000..e0d7d4a
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright (c) 2010-2012 extras developers. See LICENSE for details.
+
+"""Tests for extras."""
+
+from unittest import TestSuite, TestLoader
+
+
+def test_suite():
+    from extras.tests import (
+        test_extras,
+        )
+    modules = [
+        test_extras,
+        ]
+    loader = TestLoader()
+    suites = map(loader.loadTestsFromModule, modules)
+    return TestSuite(suites)
diff --git a/lib/extras/extras/tests/test_extras.py b/lib/extras/extras/tests/test_extras.py
new file mode 100644 (file)
index 0000000..33dc87c
--- /dev/null
@@ -0,0 +1,186 @@
+# Copyright (c) 2010-2012 extras developers. See LICENSE for details.
+
+from testtools import TestCase
+from testtools.matchers import (
+    Equals,
+    Is,
+    Not,
+    )
+
+from extras import (
+    safe_hasattr,
+    try_import,
+    try_imports,
+    )
+
+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)
diff --git a/lib/extras/setup.cfg b/lib/extras/setup.cfg
new file mode 100644 (file)
index 0000000..c319dc7
--- /dev/null
@@ -0,0 +1,4 @@
+[test]
+test_module = extras.tests
+buffer=1
+catch=1
diff --git a/lib/extras/setup.py b/lib/extras/setup.py
new file mode 100755 (executable)
index 0000000..56612d0
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+"""Distutils installer for extras."""
+
+from setuptools import setup
+import os.path
+
+import extras
+testtools_cmd = extras.try_import('testtools.TestCommand')
+
+
+def get_version():
+    """Return the version of extras that we are building."""
+    version = '.'.join(
+        str(component) for component in extras.__version__[0:3])
+    return version
+
+
+def get_long_description():
+    readme_path = os.path.join(
+        os.path.dirname(__file__), 'README.rst')
+    return open(readme_path).read()
+
+
+cmdclass = {}
+
+if testtools_cmd is not None:
+    cmdclass['test'] = testtools_cmd
+
+
+setup(name='extras',
+      author='Testing cabal',
+      author_email='testtools-dev@lists.launchpad.net',
+      url='https://github.com/testing-cabal/extras',
+      description=('Useful extra bits for Python - things that shold be '
+        'in the standard library'),
+      long_description=get_long_description(),
+      version=get_version(),
+      classifiers=[
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        ],
+      packages=[
+        'extras',
+        'extras.tests',
+        ],
+      cmdclass=cmdclass)
index 95e33bc4b9d263dbc91bdc283eb5c49da2e88c14..1d30721ca83f6d7f1baca894bd9ee152fe19a930 100755 (executable)
@@ -36,4 +36,9 @@ git clone git://github.com/madler/zlib "$WORKDIR/zlib"
 rm -rf "$WORKDIR/zlib/.git"
 rsync --exclude=wscript -avz --delete "$WORKDIR/zlib/" "$THIRD_PARTY_DIR/zlib/"
 
+echo "Updating extra..."
+git clone git://github.com/testing-cabal/extras "$WORKDIR/extras"
+rm -rf "$WORKDIR/extras/.git"
+rsync -avz --delete "$WORKDIR/extras/" "$LIBDIR/extras/"
+
 rm -rf "$WORKDIR"
index c7bf62124bddc5c9300a630c19893543abd03def..002074b546bfaf1b78cc41283e18e3cd9e709919 100644 (file)
@@ -5,7 +5,9 @@ import os, Options
 # work out what python external libraries we need to install
 external_libs = {
     "subunit": "subunit/python/subunit",
-    "testtools": "testtools/testtools"}
+    "testtools": "testtools/testtools",
+    "extras": "extras/extras",
+    }
 
 list = []