Initial extraction of code from testtools.

This commit is contained in:
Robert Collins 2012-10-27 22:27:55 +13:00
parent 87a05a447f
commit f8e9322720
12 changed files with 513 additions and 0 deletions

7
.gitignore vendored
View File

@ -25,3 +25,10 @@ pip-log.txt
#Mr Developer
.mr.developer.cfg
# editors
*.swp
*~
# Testrepository
.testrepository

4
.testr.conf Normal file
View File

@ -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

26
LICENSE Normal file
View File

@ -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.

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include LICENSE
include Makefile
include MANIFEST.in
include NEWS
include README.rst
include .gitignore

56
Makefile Normal file
View File

@ -0,0 +1,56 @@
# 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: clean-sphinx
rm -f TAGS tags
find extras -name "*.pyc" -exec rm '{}' \;
prerelease:
# An existing MANIFEST breaks distutils sometimes. Avoid that.
-rm MANIFEST
release:
./setup.py sdist upload --sign
$(PYTHON) scripts/_lp_release.py
snapshot: prerelease
./setup.py sdist
### 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
doc/news.rst:
ln -s ../NEWS doc/news.rst
docs: doc/news.rst docs-sphinx
rm doc/news.rst
docs-sphinx: html-sphinx
# Clean out generated documentation
clean-sphinx:
cd doc && make clean
# Build the html docs using Sphinx.
html-sphinx:
cd doc && make html
.PHONY: apidocs docs-sphinx clean-sphinx html-sphinx docs
.PHONY: check clean prerelease release

15
NEWS Normal file
View File

@ -0,0 +1,15 @@
extras NEWS
+++++++++++
Changes and improvements to extras_, grouped by release.
NEXT
~~~~
0.0.1
~~~~~
Initial extraction from testtools.
.. _extras: http://pypi.python.org/pypi/extras

49
README.rst Normal file
View File

@ -0,0 +1,49 @@
======
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.
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

105
extras/__init__.py Normal file
View File

@ -0,0 +1,105 @@
# 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, 1, 'dev', 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

17
extras/tests/__init__.py Normal file
View File

@ -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)

188
extras/tests/test_extras.py Normal file
View File

@ -0,0 +1,188 @@
# 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)

4
setup.cfg Normal file
View File

@ -0,0 +1,4 @@
[test]
test_module = extras.tests
buffer=1
catch=1

36
setup.py Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python
"""Distutils installer for extras."""
from distutils.core import setup
import os.path
import extras
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(manual_path).read()
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=["License :: OSI Approved :: MIT License"],
packages=[
'extras',
'extras.tests',
],
cmdclass={'test': testtools.TestCommand})