Merge branch 'pull-50'
diff --git a/.gitignore b/.gitignore
index e2beb9c..d0531b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,5 @@
 .tox
 build/
 .cache/
+
+\.idea/
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 5140586..c4628f1 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,7 @@
+0.7.0 (2018-12-30)
+    * Added parameterized_class feature, for parameterizing entire test
+      classes.
+
 0.6.2 (2018-03-11)
     * Make sure that `setUp` and `tearDown` methods work correctly (#40)
     * Raise a ValueError when input is empty (thanks @danielbradburn;
diff --git a/README.rst b/README.rst
index 42f295e..a77d3c4 100644
--- a/README.rst
+++ b/README.rst
@@ -11,66 +11,99 @@
 
 .. code:: python
 
-    # test_math.py
-    from nose.tools import assert_equal
-    from parameterized import parameterized
+   # test_math.py
+   from nose.tools import assert_equal
+   from parameterized import parameterized, parameterized_class
 
-    import unittest
-    import math
+   import unittest
+   import math
 
-    @parameterized([
-        (2, 2, 4),
-        (2, 3, 8),
-        (1, 9, 1),
-        (0, 9, 0),
-    ])
-    def test_pow(base, exponent, expected):
-        assert_equal(math.pow(base, exponent), expected)
+   @parameterized([
+       (2, 2, 4),
+       (2, 3, 8),
+       (1, 9, 1),
+       (0, 9, 0),
+   ])
+   def test_pow(base, exponent, expected):
+      assert_equal(math.pow(base, exponent), expected)
 
-    class TestMathUnitTest(unittest.TestCase):
-        @parameterized.expand([
-            ("negative", -1.5, -2.0),
-            ("integer", 1, 1.0),
-            ("large fraction", 1.6, 1),
-        ])
-        def test_floor(self, name, input, expected):
-            assert_equal(math.floor(input), expected)
+   class TestMathUnitTest(unittest.TestCase):
+      @parameterized.expand([
+          ("negative", -1.5, -2.0),
+          ("integer", 1, 1.0),
+          ("large fraction", 1.6, 1),
+      ])
+      def test_floor(self, name, input, expected):
+          assert_equal(math.floor(input), expected)
+
+   @parameterized_class(('a', 'b', 'expected_sum', 'expected_product'), [
+      (1, 2, 3, 2),
+      (5, 5, 10, 25),
+   ])
+   class TestMathClass(unittest.TestCase):
+      def test_add(self):
+         assert_equal(self.a + self.b, self.expected_sum)
+
+      def test_multiply(self):
+         assert_equal(self.a * self.b, self.expected_product)
+
+   @parameterized_class([
+      { "a": 3, "expected": 2 },
+      { "b": 5, "expected": -4 },
+   ])
+   class TestMathClassDict(unittest.TestCase):
+      a = 1
+      b = 1
+
+      def test_subtract(self):
+         assert_equal(self.a - self.b, self.expected)
+
 
 With nose (and nose2)::
 
     $ nosetests -v test_math.py
-    test_math.test_pow(2, 2, 4) ... ok
-    test_math.test_pow(2, 3, 8) ... ok
-    test_math.test_pow(1, 9, 1) ... ok
-    test_math.test_pow(0, 9, 0) ... ok
     test_floor_0_negative (test_math.TestMathUnitTest) ... ok
     test_floor_1_integer (test_math.TestMathUnitTest) ... ok
     test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok
+    test_math.test_pow(2, 2, 4, {}) ... ok
+    test_math.test_pow(2, 3, 8, {}) ... ok
+    test_math.test_pow(1, 9, 1, {}) ... ok
+    test_math.test_pow(0, 9, 0, {}) ... ok
+    test_add (test_math.TestMathClass_0) ... ok
+    test_multiply (test_math.TestMathClass_0) ... ok
+    test_add (test_math.TestMathClass_1) ... ok
+    test_multiply (test_math.TestMathClass_1) ... ok
+    test_subtract (test_math.TestMathClassDict_0) ... ok
 
     ----------------------------------------------------------------------
-    Ran 7 tests in 0.002s
+    Ran 12 tests in 0.015s
 
     OK
 
 As the package name suggests, nose is best supported and will be used for all
 further examples.
 
+
 With py.test (version 2.0 and above)::
 
     $ py.test -v test_math.py
-    ============================== test session starts ==============================
-    platform darwin -- Python 2.7.2 -- py-1.4.30 -- pytest-2.7.1
-    collected 7 items
+    ============================= test session starts ==============================
+    platform darwin -- Python 3.6.1, pytest-3.1.3, py-1.4.34, pluggy-0.4.0
+    collecting ... collected 13 items
 
     test_math.py::test_pow::[0] PASSED
     test_math.py::test_pow::[1] PASSED
     test_math.py::test_pow::[2] PASSED
     test_math.py::test_pow::[3] PASSED
-    test_math.py::TestMathUnitTest::test_floor_0_negative
-    test_math.py::TestMathUnitTest::test_floor_1_integer
-    test_math.py::TestMathUnitTest::test_floor_2_large_fraction
-
-    =========================== 7 passed in 0.10 seconds ============================
+    test_math.py::TestMathUnitTest::test_floor_0_negative PASSED
+    test_math.py::TestMathUnitTest::test_floor_1_integer PASSED
+    test_math.py::TestMathUnitTest::test_floor_2_large_fraction PASSED
+    test_math.py::TestMathClass_0::test_add PASSED
+    test_math.py::TestMathClass_0::test_multiply PASSED
+    test_math.py::TestMathClass_1::test_add PASSED
+    test_math.py::TestMathClass_1::test_multiply PASSED
+    test_math.py::TestMathClassDict_0::test_subtract PASSED
+    ==================== 12 passed, 4 warnings in 0.16 seconds =====================
 
 With unittest (and unittest2)::
 
@@ -78,15 +111,51 @@
     test_floor_0_negative (test_math.TestMathUnitTest) ... ok
     test_floor_1_integer (test_math.TestMathUnitTest) ... ok
     test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok
+    test_add (test_math.TestMathClass_0) ... ok
+    test_multiply (test_math.TestMathClass_0) ... ok
+    test_add (test_math.TestMathClass_1) ... ok
+    test_multiply (test_math.TestMathClass_1) ... ok
+    test_subtract (test_math.TestMathClassDict_0) ... ok
 
     ----------------------------------------------------------------------
-    Ran 3 tests in 0.000s
+    Ran 8 tests in 0.001s
 
     OK
 
 (note: because unittest does not support test decorators, only tests created
 with ``@parameterized.expand`` will be executed)
 
+With green::
+
+    $ green test_math.py -vvv
+    test_math
+      TestMathClass_1
+    .   test_method_a
+    .   test_method_b
+      TestMathClass_2
+    .   test_method_a
+    .   test_method_b
+      TestMathClass_3
+    .   test_method_a
+    .   test_method_b
+      TestMathUnitTest
+    .   test_floor_0_negative
+    .   test_floor_1_integer
+    .   test_floor_2_large_fraction
+      TestMathClass_0
+    .   test_add
+    .   test_multiply
+      TestMathClass_1
+    .   test_add
+    .   test_multiply
+      TestMathClassDict_0
+    .   test_subtract
+
+    Ran 12 tests in 0.121s
+
+    OK (passes=9)
+
+
 Installation
 ------------
 
@@ -109,8 +178,10 @@
    * -
      - Py2.6
      - Py2.7
-     - Py3.3
      - Py3.4
+     - Py3.5
+     - Py3.6
+     - Py3.7
      - PyPy
    * - nose
      - yes
@@ -118,18 +189,24 @@
      - yes
      - yes
      - yes
+     - yes
+     - yes
    * - nose2
      - yes
      - yes
      - yes
      - yes
      - yes
+     - yes
+     - yes
    * - py.test
      - yes
      - yes
      - yes
      - yes
      - yes
+     - yes
+     - yes
    * - | unittest
        | (``@parameterized.expand``)
      - yes
@@ -137,6 +214,8 @@
      - yes
      - yes
      - yes
+     - yes
+     - yes
    * - | unittest2
        | (``@parameterized.expand``)
      - yes
@@ -144,6 +223,8 @@
      - yes
      - yes
      - yes
+     - yes
+     - yes
 
 Dependencies
 ------------
@@ -350,6 +431,48 @@
 
     OK
 
+Finally ``@parameterized.expand_class`` parameterizes an entire class, using
+either a list of attributes, or a list of dicts that will be applied to the
+class:
+
+.. code:: python
+
+   from yourapp.models import User
+   from parameterized import parameterized_class
+
+   @parameterized_class(("username", "access_level", "expected_status_code"), [
+      ("user_1", 1, 200),
+      ("user_2", 2, 404)
+   ])
+   class TestUserAccessLevel(TestCase):
+      def setUp(self):
+         self.client.force_login(User.objects.get(username=self.username)[0])
+
+      def test_url_a(self):
+         response = self.client.get("/url")
+         self.assertEqual(response.status_code, self.expected_status_code)
+
+      def tearDown(self):
+         self.client.logout()
+
+
+   @parameterized_class([
+      { "username": "user_1", "access_level": 1 },
+      { "username": "user_2", "access_level": 2, "expected_status_code": 404 },
+   ])
+   class TestUserAccessLevel(TestCase):
+      expected_status_code = 200
+
+      def setUp(self):
+         self.client.force_login(User.objects.get(username=self.username)[0])
+
+      def test_url_a(self):
+         response = self.client.get('/url')
+         self.assertEqual(response.status_code, self.expected_status_code)
+
+      def tearDown(self):
+         self.client.logout()
+
 
 Migrating from ``nose-parameterized`` to ``parameterized``
 ----------------------------------------------------------
diff --git a/parameterized/parameterized.py b/parameterized/parameterized.py
index 2b6aa78..fae361b 100644
--- a/parameterized/parameterized.py
+++ b/parameterized/parameterized.py
@@ -291,6 +291,7 @@
             _test_runner_guess = None
     return _test_runner_guess
 
+
 class parameterized(object):
     """ Parameterize a test case::
 
@@ -519,3 +520,61 @@
     @classmethod
     def to_safe_name(cls, s):
         return str(re.sub("[^a-zA-Z0-9_]+", "_", s))
+
+
+def parameterized_class(attrs, input_values=None):
+    """ Parameterizes a test class by setting attributes on the class.
+
+        Can be used in two ways:
+
+        1) With a list of dictionaries containing attributes to override::
+
+            @parameterized_class([
+                { "username": "foo" },
+                { "username": "bar", "access_level": 2 },
+            ])
+            class TestUserAccessLevel(TestCase):
+                ...
+
+        2) With a tuple of attributes, then a list of tuples of values:
+
+            @parameterized_class(("username", "access_level"), [
+                ("foo", 1),
+                ("bar", 2)
+            ])
+            class TestUserAccessLevel(TestCase):
+                ...
+
+    """
+
+    if isinstance(attrs, string_types):
+        attrs = [attrs]
+
+    input_dicts = (
+        attrs if input_values is None else
+        [dict(zip(attrs, vals)) for vals in input_values]
+    )
+
+    def decorator(base_class):
+        test_class_module = sys.modules[base_class.__module__].__dict__
+        for idx, input_dict in enumerate(input_dicts):
+            test_class_dict = dict(base_class.__dict__)
+            test_class_dict.update(input_dict)
+
+            name_suffix = input_values and input_values[idx]
+            if isinstance(name_suffix, (list, tuple)) and len(input_values) > 0:
+                name_suffix = name_suffix[0]
+            name_suffix = (
+                "_%s" %(name_suffix, ) if isinstance(name_suffix, string_types) else
+                ""
+            )
+
+            name = "%s_%s%s" %(
+                base_class.__name__,
+                idx,
+                name_suffix,
+            )
+
+            test_class_module[name] = type(name, (base_class, ), test_class_dict)
+
+    return decorator
diff --git a/parameterized/test.py b/parameterized/test.py
index 4193e6e..7bc6b49 100644
--- a/parameterized/test.py
+++ b/parameterized/test.py
@@ -7,7 +7,7 @@
 
 from .parameterized import (
     PY3, PY2, parameterized, param, parameterized_argument_value_pairs,
-    short_repr, detect_runner, SkipTest
+    short_repr, detect_runner, parameterized_class, SkipTest,
 )
 
 def assert_contains(haystack, needle):
@@ -401,3 +401,57 @@
 @parameterized(cases_over_10)
 def test_cases_over_10(input, expected):
     assert_equal(input, expected-1)
+
+
+@parameterized_class(("a", "b", "c"), [
+    ("foo", 1, 2),
+    ("bar", 3, 0),
+    (0, 1, 2),
+])
+class TestParameterizedClass(TestCase):
+    expect([
+        "TestParameterizedClass_0_foo:test_method_a('foo', 1, 2)",
+        "TestParameterizedClass_0_foo:test_method_b('foo', 1, 2)",
+        "TestParameterizedClass_1_bar:test_method_a('bar', 3, 0)",
+        "TestParameterizedClass_1_bar:test_method_b('bar', 3, 0)",
+        "TestParameterizedClass_2:test_method_a(0, 1, 2)",
+        "TestParameterizedClass_2:test_method_b(0, 1, 2)",
+    ])
+
+    def _assertions(self, test_name):
+        assert hasattr(self, "a")
+        assert_equal(self.b + self.c, 3)
+        missing_tests.remove("%s:%s(%r, %r, %r)" %(
+            self.__class__.__name__,
+            test_name,
+            self.a,
+            self.b,
+            self.c,
+        ))
+
+    def test_method_a(self):
+        self._assertions("test_method_a")
+
+    def test_method_b(self):
+        self._assertions("test_method_b")
+
+
+@parameterized_class([
+    {"foo": 1},
+    {"bar": 1},
+])
+class TestParameterizedClassDict(TestCase):
+    expect([
+        "TestParameterizedClassDict_0:test_method(1, 0)",
+        "TestParameterizedClassDict_1:test_method(0, 1)",
+    ])
+
+    foo = 0
+    bar = 0
+
+    def test_method(self):
+        missing_tests.remove("%s:test_method(%r, %r)" %(
+            self.__class__.__name__,
+            self.foo,
+            self.bar,
+        ))
diff --git a/setup.py b/setup.py
index 4eac2e3..396ff94 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@
 
 setup(
     name="parameterized",
-    version="0.6.1",
+    version="0.6.3",
     url="https://github.com/wolever/parameterized",
     license="FreeBSD",
     author="David Wolever",
diff --git a/tox.ini b/tox.ini
index c58219f..8bceac2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,5 @@
 [tox]
-envlist=py{27,35,36,py}-{nose,nose2,pytest,unit,unit2}
-
+envlist=py{27,35,36,37,py}-{nose,nose2,pytest,unit,unit2} 
 [testenv]
 deps=
     nose