Initial extraction of code from testtools.
This commit is contained in:
parent
87a05a447f
commit
f8e9322720
|
@ -25,3 +25,10 @@ pip-log.txt
|
|||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
# editors
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Testrepository
|
||||
.testrepository
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1,6 @@
|
|||
include LICENSE
|
||||
include Makefile
|
||||
include MANIFEST.in
|
||||
include NEWS
|
||||
include README.rst
|
||||
include .gitignore
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
@ -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})
|
Loading…
Reference in New Issue