diff --git a/luigi/parameter.py b/luigi/parameter.py index 3278377f1c..b101640e99 100644 --- a/luigi/parameter.py +++ b/luigi/parameter.py @@ -1379,10 +1379,15 @@ def parse(self, x): # ast.literal_eval(t_str) == t try: # loop required to parse tuple of tuples - return tuple(tuple(x) for x in json.loads(x, object_pairs_hook=FrozenOrderedDict)) + return tuple(self._convert_iterable_to_tuple(x) for x in json.loads(x, object_pairs_hook=FrozenOrderedDict)) except (ValueError, TypeError): return tuple(literal_eval(x)) # if this causes an error, let that error be raised. + def _convert_iterable_to_tuple(self, x): + if isinstance(x, str): + return x + return tuple(x) + class OptionalTupleParameter(OptionalParameterMixin, TupleParameter): """Class to parse optional tuple parameters.""" diff --git a/test/list_parameter_test.py b/test/list_parameter_test.py index 26204e48cf..4210d2a675 100644 --- a/test/list_parameter_test.py +++ b/test/list_parameter_test.py @@ -81,7 +81,7 @@ def test_schema(self): a.normalize(["INVALID_ATTRIBUTE"]) # Check that empty list is not valid - with pytest.raises(ValidationError, match=r"\[\] is too short"): + with pytest.raises(ValidationError): a.normalize([]) # Check that valid lists work diff --git a/test/parameter_test.py b/test/parameter_test.py index 4e1d374d19..155e9b61ac 100644 --- a/test/parameter_test.py +++ b/test/parameter_test.py @@ -533,6 +533,14 @@ class Foo(luigi.Task): self.assertEqual(hash(Foo(args=(('foo', 'bar'), ('doge', 'wow'))).args), hash(p.normalize(p.parse('(("foo", "bar"), ("doge", "wow"))')))) + def test_tuple_string_with_json(self): + class Foo(luigi.Task): + args = luigi.parameter.TupleParameter() + + p = luigi.parameter.TupleParameter() + self.assertEqual(hash(Foo(args=('foo', 'bar')).args), + hash(p.normalize(p.parse('["foo", "bar"]')))) + def test_task(self): class Bar(luigi.Task): pass