diff --git a/.travis.yml b/.travis.yml index efeae23..37a31c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - 2.7 - - 3.4 - 3.5 install: pip install -r requirements/test.txt diff --git a/ddt.py b/ddt.py index e71e19a..8a4a955 100644 --- a/ddt.py +++ b/ddt.py @@ -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: @@ -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. @@ -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 @@ -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) diff --git a/docs/index.rst b/docs/index.rst index a1ae40c..e6f3909 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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: diff --git a/setup.py b/setup.py index 57a9741..eefd539 100644 --- a/setup.py +++ b/setup.py @@ -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', ], diff --git a/test/data/test_custom_yaml_loader.yaml b/test/data/test_custom_yaml_loader.yaml new file mode 100644 index 0000000..55b3b68 --- /dev/null +++ b/test/data/test_custom_yaml_loader.yaml @@ -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 diff --git a/test/test_data_dict.json b/test/data/test_data_dict.json similarity index 100% rename from test/test_data_dict.json rename to test/data/test_data_dict.json diff --git a/test/test_data_dict.yaml b/test/data/test_data_dict.yaml similarity index 100% rename from test/test_data_dict.yaml rename to test/data/test_data_dict.yaml diff --git a/test/test_data_dict_dict.json b/test/data/test_data_dict_dict.json similarity index 100% rename from test/test_data_dict_dict.json rename to test/data/test_data_dict_dict.json diff --git a/test/test_data_dict_dict.yaml b/test/data/test_data_dict_dict.yaml similarity index 100% rename from test/test_data_dict_dict.yaml rename to test/data/test_data_dict_dict.yaml diff --git a/test/test_data_list.json b/test/data/test_data_list.json similarity index 100% rename from test/test_data_list.json rename to test/data/test_data_list.json diff --git a/test/test_data_list.yaml b/test/data/test_data_list.yaml similarity index 100% rename from test/test_data_list.yaml rename to test/data/test_data_list.yaml diff --git a/test/data/test_functional_custom_tags.yaml b/test/data/test_functional_custom_tags.yaml new file mode 100644 index 0000000..7ecd43a --- /dev/null +++ b/test/data/test_functional_custom_tags.yaml @@ -0,0 +1,3 @@ +custom_class: + instance: !!python/object:test.test_functional.CustomClass {} + expected: CustomClass diff --git a/test/test_example.py b/test/test_example.py index ac2ce83..db2cf17 100644 --- a/test/test_example.py +++ b/test/test_example.py @@ -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 @@ -8,7 +9,6 @@ have_yaml_support = False else: have_yaml_support = True - del yaml # A good-looking decorator needs_yaml = unittest.skipUnless( @@ -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)) @@ -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)) @@ -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) diff --git a/test/test_functional.py b/test/test_functional.py index 9275ffc..387f4ec 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -1,7 +1,9 @@ import os import json +from sys import modules import six + try: from unittest import mock except ImportError: @@ -15,6 +17,10 @@ from test.mycode import has_three_elements +class CustomClass: + pass + + @ddt class Dummy(object): """ @@ -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 @@ -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 @@ -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 @@ -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([ @@ -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)) @@ -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() diff --git a/tox.ini b/tox.ini index 10b5e0a..d20cbda 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py34, py35 +envlist = py27, py35 [testenv] deps =