Merge branch 'master' of github.com:txels/ddt
Conflicts: ddt.py
This commit is contained in:
@@ -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
34
CONTRIBUTING.md
Normal 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
21
LICENSE.md
Normal 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/))
|
||||
@@ -1,4 +1,6 @@
|
||||
[](https://travis-ci.org/bulkan/ddt)
|
||||
[](https://landscape.io/github/txels/ddt/master)
|
||||
[](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
164
ddt.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
release.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
python setup.py sdist bdist_wheel upload
|
||||
@@ -1,2 +1,4 @@
|
||||
-r test.txt
|
||||
|
||||
Sphinx
|
||||
sphinxcontrib-programoutput
|
||||
1
requirements/release.txt
Normal file
1
requirements/release.txt
Normal file
@@ -0,0 +1 @@
|
||||
wheel
|
||||
8
setup.py
8
setup.py
@@ -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',
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user