Skip to content

Commit

Permalink
Merge pull request #77 from skeletorXVI/master
Browse files Browse the repository at this point in the history
Adding the ability to specify the yaml loader in the file_data decorator
  • Loading branch information
wswld committed Mar 10, 2020
2 parents fdbaff8 + 371a22e commit 7352f81
Show file tree
Hide file tree
Showing 15 changed files with 169 additions and 23 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: python
python:
- 2.7
- 3.4
- 3.5
install: pip install -r requirements/test.txt

Expand Down
24 changes: 17 additions & 7 deletions ddt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@
else:
_have_yaml = True

__version__ = '1.2.1'
__version__ = '1.3.0'

# These attributes will not conflict with any real python attribute
# They are added to the decorated test method and processed later
# by the `ddt` class decorator.

DATA_ATTR = '%values' # store the data the test must run with
FILE_ATTR = '%file_path' # store the path to JSON file
UNPACK_ATTR = '%unpack' # remember that we have to unpack values
index_len = 5 # default max length of case index
DATA_ATTR = '%values' # store the data the test must run with
FILE_ATTR = '%file_path' # store the path to JSON file
YAML_LOADER_ATTR = '%yaml_loader' # store custom yaml loader for serialization
UNPACK_ATTR = '%unpack' # remember that we have to unpack values
index_len = 5 # default max length of case index


try:
Expand Down Expand Up @@ -79,7 +80,7 @@ def wrapper(func):
return wrapper


def file_data(value):
def file_data(value, yaml_loader=None):
"""
Method decorator to add to your test methods.
Expand All @@ -97,9 +98,14 @@ def file_data(value):
In case of a dict, keys will be used as suffixes to the name of the
test case, and values will be fed as test data.
``yaml_loader`` can be used to customize yaml deserialization.
The default is ``None``, which results in using the ``yaml.safe_load``
method.
"""
def wrapper(func):
setattr(func, FILE_ATTR, value)
if yaml_loader:
setattr(func, YAML_LOADER_ATTR, yaml_loader)
return func
return wrapper

Expand Down Expand Up @@ -212,7 +218,11 @@ def func(*args):
with codecs.open(data_file_path, 'r', 'utf-8') as f:
# Load the data from YAML or JSON
if _is_yaml_file:
data = yaml.safe_load(f)
if hasattr(func, YAML_LOADER_ATTR):
yaml_loader = getattr(func, YAML_LOADER_ATTR)
data = yaml.load(f, Loader=yaml_loader)
else:
data = yaml.safe_load(f)
else:
data = json.load(f)

Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ multiple test cases.
You can find (and fork) the project on Github_.

DDT should work on Python2 and Python3, but we only officially test it for
versions 2.7 and 3.3.
versions 2.7 and 3.5.

Contents:

Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Topic :: Software Development :: Testing',
],
Expand Down
65 changes: 65 additions & 0 deletions test/data/test_custom_yaml_loader.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
bool:
instance: !!bool "false"
expected: false

str:
instance: !!str "test"
expected: test

int:
instance: !!int "32"
expected: 32

float:
instance: !!float "3.123"
expected: 3.123

python_list:
instance: !!python/list [1,2,3,4]
expected:
- 1
- 2
- 3
- 4

python_dict:
instance: !!python/dict
a: 1
b: asd
c: false
expected:
a: 1
b: asd
c: false

my_class:
instance: !!python/object:test.test_example.MyClass
a: 132
b: true
c:
- alpha
- beta
d:
_a: 1
_b: test
expected:
a: 132
b: true
c:
- alpha
- beta
d:
_a: 1
_b: test

python_str:
instance: !!python/str "test"
expected: test

python_int:
instance: !!python/int "32"
expected: 32

python_float:
instance: !!python/float "3.123"
expected: 3.123
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions test/data/test_functional_custom_tags.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
custom_class:
instance: !!python/object:test.test_functional.CustomClass {}
expected: CustomClass
39 changes: 32 additions & 7 deletions test/test_example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest

from ddt import ddt, data, file_data, unpack
from test.mycode import larger_than_two, has_three_elements, is_a_greeting

Expand All @@ -8,7 +9,6 @@
have_yaml_support = False
else:
have_yaml_support = True
del yaml

# A good-looking decorator
needs_yaml = unittest.skipUnless(
Expand All @@ -20,6 +20,19 @@ class Mylist(list):
pass


class MyClass:
def __init__(self, **kwargs):
for field, value in kwargs.items():
setattr(self, field, value)

def __eq__(self, other):
return isinstance(other, dict) and vars(self) == other or \
isinstance(other, MyClass) and vars(self) == vars(other)

def __str__(self):
return "TestObject %s" % vars(self)


def annotated(a, b):
r = Mylist([a, b])
setattr(r, "__name__", "test_%d_greater_than_%d" % (a, b))
Expand Down Expand Up @@ -59,34 +72,34 @@ def test_greater_with_name_docstring(self, value):
self.assertIsNotNone(getattr(value, "__name__"))
self.assertIsNotNone(getattr(value, "__doc__"))

@file_data("test_data_dict_dict.json")
@file_data('data/test_data_dict_dict.json')
def test_file_data_json_dict_dict(self, start, end, value):
self.assertLess(start, end)
self.assertLess(value, end)
self.assertGreater(value, start)

@file_data('test_data_dict.json')
@file_data('data/test_data_dict.json')
def test_file_data_json_dict(self, value):
self.assertTrue(has_three_elements(value))

@file_data('test_data_list.json')
@file_data('data/test_data_list.json')
def test_file_data_json_list(self, value):
self.assertTrue(is_a_greeting(value))

@needs_yaml
@file_data("test_data_dict_dict.yaml")
@file_data('data/test_data_dict_dict.yaml')
def test_file_data_yaml_dict_dict(self, start, end, value):
self.assertLess(start, end)
self.assertLess(value, end)
self.assertGreater(value, start)

@needs_yaml
@file_data('test_data_dict.yaml')
@file_data('data/test_data_dict.yaml')
def test_file_data_yaml_dict(self, value):
self.assertTrue(has_three_elements(value))

@needs_yaml
@file_data('test_data_list.yaml')
@file_data('data/test_data_list.yaml')
def test_file_data_yaml_list(self, value):
self.assertTrue(is_a_greeting(value))

Expand Down Expand Up @@ -130,3 +143,15 @@ def test_doc_missing_kargs(self, value):
def test_list_extracted_with_doc(self, first_value, second_value):
"""Extract into args with first value {} and second value {}"""
self.assertTrue(first_value > second_value)


if have_yaml_support:
# This test will only succeed if the execution context is from the ddt
# directory. pyyaml cannot locate test.test_example.MyClass otherwise!

@ddt
class YamlOnlyTestCase(unittest.TestCase):
@file_data('data/test_custom_yaml_loader.yaml', yaml.FullLoader)
def test_custom_yaml_loader(self, instance, expected):
"""Test with yaml tags to create specific classes to compare"""
self.assertEqual(expected, instance)
55 changes: 50 additions & 5 deletions test/test_functional.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os
import json
from sys import modules

import six

try:
from unittest import mock
except ImportError:
Expand All @@ -15,6 +17,10 @@
from test.mycode import has_three_elements


class CustomClass:
pass


@ddt
class Dummy(object):
"""
Expand Down Expand Up @@ -44,7 +50,7 @@ class FileDataDummy(object):
Dummy class to test the file_data decorator on
"""

@file_data("test_data_dict.json")
@file_data("data/test_data_dict.json")
def test_something_again(self, value):
return value

Expand All @@ -56,7 +62,7 @@ class JSONFileDataMissingDummy(object):
JSON file is missing
"""

@file_data("test_data_dict_missing.json")
@file_data("data/test_data_dict_missing.json")
def test_something_again(self, value):
return value

Expand All @@ -68,7 +74,7 @@ class YAMLFileDataMissingDummy(object):
YAML file is missing
"""

@file_data("test_data_dict_missing.yaml")
@file_data("data/test_data_dict_missing.yaml")
def test_something_again(self, value):
return value

Expand Down Expand Up @@ -148,7 +154,7 @@ def test_file_data_test_names_dict():
tests = set(filter(_is_test, FileDataDummy.__dict__))

tests_dir = os.path.dirname(__file__)
test_data_path = os.path.join(tests_dir, 'test_data_dict.json')
test_data_path = os.path.join(tests_dir, 'data/test_data_dict.json')
test_data = json.loads(open(test_data_path).read())
index_len = len(str(len(test_data)))
created_tests = set([
Expand Down Expand Up @@ -376,7 +382,7 @@ def test_load_yaml_without_yaml_support():
@ddt
class NoYAMLInstalledTest(object):

@file_data('test_data_dict.yaml')
@file_data('data/test_data_dict.yaml')
def test_file_data_yaml_dict(self, value):
assert_true(has_three_elements(value))

Expand All @@ -386,3 +392,42 @@ def test_file_data_yaml_dict(self, value):
for test in tests:
method = getattr(obj, test)
assert_raises(ValueError, method)


def test_load_yaml_with_python_tag():
"""
Test that YAML files containing python tags throw no exception if an
loader allowing python tags is passed.
"""

from yaml import FullLoader
from yaml.constructor import ConstructorError

def str_to_type(class_name):
return getattr(modules[__name__], class_name)

try:
@ddt
class YamlDefaultLoaderTest(object):
@file_data('data/test_functional_custom_tags.yaml')
def test_cls_is_instance(self, cls, expected):
assert_true(isinstance(cls, str_to_type(expected)))
except Exception as e:
if not isinstance(e, ConstructorError):
raise AssertionError()

@ddt
class YamlFullLoaderTest(object):
@file_data('data/test_functional_custom_tags.yaml', FullLoader)
def test_cls_is_instance(self, instance, expected):
assert_true(isinstance(instance, str_to_type(expected)))

tests = list(filter(_is_test, YamlFullLoaderTest.__dict__))
obj = YamlFullLoaderTest()

if not tests:
raise AssertionError('No tests have been found.')

for test in tests:
method = getattr(obj, test)
method()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py27, py34, py35
envlist = py27, py35

[testenv]
deps =
Expand Down

0 comments on commit 7352f81

Please sign in to comment.