Merge branch 'master' of github.com:txels/ddt

Conflicts:
	ddt.py
This commit is contained in:
Gary Macindoe
2015-02-04 11:52:34 +00:00
16 changed files with 225 additions and 75 deletions

View File

@@ -4,7 +4,8 @@ python:
- 2.7
#- 3.2
- 3.3
install: pip install -r test-requirements.txt
- 3.4
install: pip install -r requirements/test.txt
script:
- nosetests --with-cov --cover-package=ddt
- flake8 ddt.py test

34
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,34 @@
# Contributing to DDT
We'll be happy if you want to contribute to the improvement of `ddt`.
Code contributions will take the form of pull requests to
[the github repo](https://github.com/txels/ddt).
Your PRs are more likely to be merged quickly if:
- They adhere to coding conventions in the repo (PEP8)
- They include tests
## Building
PRs to `ddt` are always built by Travis-CI on Python 2 and 3.
If you want to build `ddt` locally, the simplest way is to use `tox`:
```
pip install tox
tox
```
This will run tests on various releases of python (2 and 3, as long as they
are installed in your computer), run `flake8` and build the Sphinx
documentation.
Alternatively, if you only want to run tests on your active version of python,
I recommend you make yourself a virtual environment and:
```
pip install -r requirements/build.txt
./build.sh
```

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
Copyright © 2015 Carles Barrobés and additional contributors.
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.
([MIT License](http://mit-license.org/))

View File

@@ -1,4 +1,6 @@
[![Build Status](https://travis-ci.org/txels/ddt.png)](https://travis-ci.org/bulkan/ddt)
[![Code Health](https://landscape.io/github/txels/ddt/master/landscape.svg)](https://landscape.io/github/txels/ddt/master)
[![Can I Use Python 3?](https://caniusepython3.com/project/ddt.svg)](https://caniusepython3.com/project/ddt)
DDT (Data-Driven Tests) allows you to multiply one test case
by running it with different test data, and make it appear as
@@ -10,3 +12,6 @@ Installation
```pip install ddt```
More info at http://ddt.readthedocs.org/
See [Contributing](CONTRIBUTING.md) if you plan to contribute to `ddt`,
and [License](LICENSE.md) if you plan to use it.

164
ddt.py
View File

@@ -1,10 +1,18 @@
# -*- coding: utf-8 -*-
# This file is a part of DDT (https://github.com/txels/ddt)
# Copyright 2012-2015 Carles Barrobés and DDT contributors
# For the exact contribution history, see the git revision log.
# DDT is licensed under the MIT License, included in
# https://github.com/txels/ddt/blob/master/LICENSE.md
import inspect
import json
import os
import re
import sys
from functools import wraps
__version__ = '0.8.0'
__version__ = '1.0.0'
# These attributes will not conflict with any real python attribute
# They are added to the decorated test method and processed later
@@ -62,6 +70,14 @@ def file_data(value):
return wrapper
def is_hash_randomized():
return (((sys.hexversion >= 0x02070300 and
sys.hexversion < 0x03000000) or
(sys.hexversion >= 0x03020300)) and
sys.flags.hash_randomization and
'PYTHONHASHSEED' not in os.environ)
def mk_test_name(name, value, index=0):
"""
Generate a new name for a test case.
@@ -70,7 +86,47 @@ def mk_test_name(name, value, index=0):
string representation of the value, and convert the result into a valid
python identifier by replacing extraneous characters with ``_``.
If hash randomization is enabled (a feature available since 2.7.3/3.2.3
and enabled by default since 3.3) and a "non-trivial" value is passed
this will omit the name argument by default. Set `PYTHONHASHSEED`
to a fixed value before running tests in these cases to get the
names back consistently or use the `__name__` attribute on data values.
A "trivial" value is a plain scalar, or a tuple or list consisting
only of trivial values.
"""
# We avoid doing str(value) if all of the following hold:
#
# * Python version is 2.7.3 or newer (for 2 series) or 3.2.3 or
# newer (for 3 series). Also sys.flags.hash_randomization didn't
# exist before these.
# * sys.flags.hash_randomization is set to True
# * PYTHONHASHSEED is **not** defined in the environment
# * Given `value` argument is not a trivial scalar (None, str,
# int, float).
#
# Trivial scalar values are passed as is in all cases.
trivial_types = (type(None), bool, str, int, float)
try:
trivial_types += (unicode,)
except NameError:
pass
def is_trivial(value):
if isinstance(value, trivial_types):
return True
if isinstance(value, (list, tuple)):
return all(map(is_trivial, value))
return False
if is_hash_randomized() and not is_trivial(value):
return "{0}_{1}".format(name, index + 1)
try:
value = str(value)
except UnicodeEncodeError:
@@ -80,6 +136,55 @@ def mk_test_name(name, value, index=0):
return re.sub('\W|^(?=\d)', '_', test_name)
def feed_data(func, new_name, *args, **kwargs):
"""
This internal method decorator feeds the test data item to the test.
"""
@wraps(func)
def wrapper(self):
return func(self, *args, **kwargs)
wrapper.__name__ = new_name
return wrapper
def add_test(cls, test_name, func, *args, **kwargs):
"""
Add a test case to this class.
The test will be based on an existing function but will give it a new
name.
"""
setattr(cls, test_name, feed_data(func, test_name, *args, **kwargs))
def process_file_data(cls, name, func, file_attr):
"""
Process the parameter in the `file_data` decorator.
"""
cls_path = os.path.abspath(inspect.getsourcefile(cls))
data_file_path = os.path.join(os.path.dirname(cls_path), file_attr)
def _raise_ve(*args): # pylint: disable-msg=W0613
raise ValueError("%s does not exist" % file_attr)
if os.path.exists(data_file_path) is False:
test_name = mk_test_name(name, "error")
add_test(cls, test_name, _raise_ve, None)
else:
data = json.loads(open(data_file_path).read())
for i, elem in enumerate(data):
if isinstance(data, dict):
key, value = elem, data[elem]
test_name = mk_test_name(name, key, i)
elif isinstance(data, list):
value = elem
test_name = mk_test_name(name, value, i)
add_test(cls, test_name, func, value)
def ddt(cls):
"""
Class decorator for subclasses of ``unittest.TestCase``.
@@ -104,70 +209,21 @@ def ddt(cls):
from the ``data`` key.
"""
def feed_data(func, new_name, *args, **kwargs):
"""
This internal method decorator feeds the test data item to the test.
"""
@wraps(func)
def wrapper(self):
return func(self, *args, **kwargs)
wrapper.__name__ = new_name
return wrapper
def add_test(test_name, func, *args, **kwargs):
"""
Add a test case to this class.
The test will be based on an existing function but will give it a new
name.
"""
setattr(cls, test_name, feed_data(func, test_name, *args, **kwargs))
def process_file_data(name, func, file_attr):
"""
Process the parameter in the `file_data` decorator.
"""
cls_path = os.path.abspath(inspect.getsourcefile(cls))
data_file_path = os.path.join(os.path.dirname(cls_path), file_attr)
def _raise_ve(*args):
raise ValueError("%s does not exist" % file_attr)
if os.path.exists(data_file_path) is False:
test_name = mk_test_name(name, "error")
add_test(test_name, _raise_ve, None)
else:
data = json.loads(open(data_file_path).read())
for i, elem in enumerate(data):
if isinstance(data, dict):
key, value = elem, data[elem]
test_name = mk_test_name(name, key, i)
elif isinstance(data, list):
value = elem
test_name = mk_test_name(name, value, i)
if isinstance(value, dict):
add_test(test_name, func, **value)
else:
add_test(test_name, func, value)
for name, func in list(cls.__dict__.items()):
if hasattr(func, DATA_ATTR):
for i, v in enumerate(getattr(func, DATA_ATTR)):
test_name = mk_test_name(name, getattr(v, "__name__", v), i)
if hasattr(func, UNPACK_ATTR):
if isinstance(v, tuple) or isinstance(v, list):
add_test(test_name, func, *v)
add_test(cls, test_name, func, *v)
else:
# unpack dictionary
add_test(test_name, func, **v)
add_test(cls, test_name, func, **v)
else:
add_test(test_name, func, v)
add_test(cls, test_name, func, v)
delattr(cls, name)
elif hasattr(func, FILE_ATTR):
file_attr = getattr(func, FILE_ATTR)
process_file_data(name, func, file_attr)
process_file_data(cls, name, func, file_attr)
delattr(cls, name)
return cls

View File

@@ -48,6 +48,8 @@ master_doc = 'index'
# General information about the project.
project = u'DDT'
# pylint: disable-msg=W0622
# - copyright is a builtin
copyright = u'2012, Carles Barrobés'
# The version info for the project you're documenting, acts as replacement for

View File

@@ -42,3 +42,16 @@ multiplied.
DDT will try to give the new test cases meaningful names by converting the
data values to valid python identifiers.
.. note::
Python 2.7.3 introduced *hash randomization* which is by default
enabled on Python 3.3 and later. DDT's default mechanism to
generate meaningful test names will **not** use the test data value
as part of the name for complex types if hash randomization is
enabled.
You can disable hash randomization by setting the
``PYTHONHASHSEED`` environment variable to a fixed value before
running tests (``export PYTHONHASHSEED=1`` for example).

2
release.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
python setup.py sdist bdist_wheel upload

View File

@@ -1,2 +1,4 @@
-r test.txt
Sphinx
sphinxcontrib-programoutput

1
requirements/release.txt Normal file
View File

@@ -0,0 +1 @@
wheel

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View File

@@ -16,10 +16,14 @@ setup(
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Topic :: Software Development :: Testing',
],
)

View File

@@ -3,12 +3,12 @@ from ddt import ddt, data, file_data, unpack
from test.mycode import larger_than_two, has_three_elements, is_a_greeting
class mylist(list):
class Mylist(list):
pass
def annotated(a, b):
r = mylist([a, b])
r = Mylist([a, b])
setattr(r, "__name__", "test_%d_greater_than_%d" % (a, b))
return r

View File

@@ -3,7 +3,7 @@ import json
import six
from ddt import ddt, data, file_data
from ddt import ddt, data, file_data, is_hash_randomized
from nose.tools import assert_equal, assert_is_not_none, assert_raises
@@ -187,21 +187,21 @@ def test_ddt_data_name_attribute():
def hello():
pass
class myint(int):
class Myint(int):
pass
class mytest(object):
class Mytest(object):
pass
d1 = myint(1)
d1 = Myint(1)
d1.__name__ = 'data1'
d2 = myint(2)
d2 = Myint(2)
data_hello = data(d1, d2)(hello)
setattr(mytest, 'test_hello', data_hello)
setattr(Mytest, 'test_hello', data_hello)
ddt_mytest = ddt(mytest)
ddt_mytest = ddt(Mytest)
assert_is_not_none(getattr(ddt_mytest, 'test_hello_1_data1'))
assert_is_not_none(getattr(ddt_mytest, 'test_hello_2_2'))
@@ -219,26 +219,33 @@ def test_ddt_data_unicode():
if six.PY2:
@ddt
class mytest(object):
class Mytest(object):
@data(u'ascii', u'non-ascii-\N{SNOWMAN}', {u'\N{SNOWMAN}': 'data'})
def test_hello(self, val):
pass
assert_is_not_none(getattr(mytest, 'test_hello_1_ascii'))
assert_is_not_none(getattr(mytest, 'test_hello_2_non_ascii__u2603'))
assert_is_not_none(getattr(mytest, 'test_hello_3__u__u2603____data__'))
assert_is_not_none(getattr(Mytest, 'test_hello_1_ascii'))
assert_is_not_none(getattr(Mytest, 'test_hello_2_non_ascii__u2603'))
if is_hash_randomized():
assert_is_not_none(getattr(Mytest, 'test_hello_3'))
else:
assert_is_not_none(getattr(Mytest,
'test_hello_3__u__u2603____data__'))
elif six.PY3:
@ddt
class mytest(object):
class Mytest(object):
@data('ascii', 'non-ascii-\N{SNOWMAN}', {'\N{SNOWMAN}': 'data'})
def test_hello(self, val):
pass
assert_is_not_none(getattr(mytest, 'test_hello_1_ascii'))
assert_is_not_none(getattr(mytest, 'test_hello_2_non_ascii__'))
assert_is_not_none(getattr(mytest, 'test_hello_3________data__'))
assert_is_not_none(getattr(Mytest, 'test_hello_1_ascii'))
assert_is_not_none(getattr(Mytest, 'test_hello_2_non_ascii__'))
if is_hash_randomized():
assert_is_not_none(getattr(Mytest, 'test_hello_3'))
else:
assert_is_not_none(getattr(Mytest, 'test_hello_3________data__'))
def test_feed_data_with_invalid_identifier():

View File

@@ -1,5 +1,5 @@
[tox]
envlist = py27, py33
envlist = py27, py33, py34
[testenv]
deps =