From 8b3eabdb992e3b956836479d3542c940950fbba3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 1 Dec 2016 22:09:27 -0800 Subject: [PATCH 1/2] namedtuple: add defaults to the Python 3.6 syntax This lets you write: class Employee(NamedTuple): name: str id: int language: str = 'python' as suggested by Guido on python-ideas. --- src/test_typing.py | 26 ++++++++++++++++++++++++++ src/typing.py | 16 +++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/test_typing.py b/src/test_typing.py index d203ce3d5..1f1e46754 100644 --- a/src/test_typing.py +++ b/src/test_typing.py @@ -1376,6 +1376,10 @@ class G(Generic[T]): class CoolEmployee(NamedTuple): name: str cool: int + +class CoolEmployeeWithDefault(NamedTuple): + name: str + cool: int = 0 """ if PY36: @@ -1920,6 +1924,28 @@ def test_annotation_usage(self): self.assertEqual(CoolEmployee._fields, ('name', 'cool')) self.assertEqual(CoolEmployee._field_types, dict(name=str, cool=int)) + @skipUnless(PY36, 'Python 3.6 required') + def test_annotation_usage_with_default(self): + jelle = CoolEmployeeWithDefault('Jelle') + self.assertIsInstance(jelle, CoolEmployeeWithDefault) + self.assertIsInstance(jelle, tuple) + self.assertEqual(jelle.name, 'Jelle') + self.assertEqual(jelle.cool, 0) + cooler_employee = CoolEmployeeWithDefault('Sjoerd', 1) + self.assertEqual(cooler_employee.cool, 1) + + self.assertEqual(CoolEmployeeWithDefault.__name__, 'CoolEmployeeWithDefault') + self.assertEqual(CoolEmployeeWithDefault._fields, ('name', 'cool')) + self.assertEqual(CoolEmployeeWithDefault._field_types, dict(name=str, cool=int)) + self.assertEqual(CoolEmployeeWithDefault._field_defaults, dict(cool=0)) + + with self.assertRaises(TypeError): + exec(""" +class NonDefaultAfterDefault(NamedTuple): + x: int = 3 + y: int +""") + @skipUnless(PY36, 'Python 3.6 required') def test_namedtuple_keyword_usage(self): LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) diff --git a/src/typing.py b/src/typing.py index 34845b747..29a2e2d35 100644 --- a/src/typing.py +++ b/src/typing.py @@ -1916,7 +1916,21 @@ def __new__(cls, typename, bases, ns): raise TypeError("Class syntax for NamedTuple is only supported" " in Python 3.6+") types = ns.get('__annotations__', {}) - return _make_nmtuple(typename, types.items()) + nm_tpl = _make_nmtuple(typename, types.items()) + saw_default = False + defaults = [] + defaults_dict = {} + for field_name in types: + if field_name in ns: + saw_default = True + default_value = ns[field_name] + defaults.append(default_value) + defaults_dict[field_name] = default_value + elif saw_default: + raise TypeError('Non-default namedtuple field cannot follow default field') + nm_tpl.__new__.__defaults__ = tuple(defaults) + nm_tpl._field_defaults = defaults_dict + return nm_tpl class NamedTuple(metaclass=NamedTupleMeta): """Typed version of namedtuple. From dab309315295ef8a598f9db1a9f39db403f60819 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 3 Dec 2016 08:50:35 -0800 Subject: [PATCH 2/2] namedtuple with defaults: improve error message and remove redundant variable Thanks Ivan for reviewing! --- src/typing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/typing.py b/src/typing.py index 29a2e2d35..43b75652a 100644 --- a/src/typing.py +++ b/src/typing.py @@ -1917,17 +1917,18 @@ def __new__(cls, typename, bases, ns): " in Python 3.6+") types = ns.get('__annotations__', {}) nm_tpl = _make_nmtuple(typename, types.items()) - saw_default = False defaults = [] defaults_dict = {} for field_name in types: if field_name in ns: - saw_default = True default_value = ns[field_name] defaults.append(default_value) defaults_dict[field_name] = default_value - elif saw_default: - raise TypeError('Non-default namedtuple field cannot follow default field') + elif defaults: + raise TypeError("Non-default namedtuple field {field_name} cannot follow default" + " field(s) {default_names}" + .format(field_name=field_name, + default_names=', '.join(defaults_dict.keys()))) nm_tpl.__new__.__defaults__ = tuple(defaults) nm_tpl._field_defaults = defaults_dict return nm_tpl