diff -r bc322469a7a8 Doc/library/collections.rst --- a/Doc/library/collections.rst Mon Dec 10 20:22:55 2012 -0800 +++ b/Doc/library/collections.rst Wed Dec 12 19:12:37 2012 +0200 @@ -731,7 +731,8 @@ self-documenting code. They can be used wherever regular tuples are used, and they add the ability to access fields by name instead of position index. -.. function:: namedtuple(typename, field_names, verbose=False, rename=False) +.. function:: namedtuple(typename, field_names, verbose=False, rename=False, *, + doc=None) Returns a new tuple subclass named *typename*. The new subclass is used to create tuple-like objects that have fields accessible by attribute lookup as @@ -741,7 +742,11 @@ The *field_names* are a single string with each fieldname separated by whitespace and/or commas, for example ``'x y'`` or ``'x, y'``. Alternatively, *field_names* - can be a sequence of strings such as ``['x', 'y']``. + can be a sequence of strings such as ``['x', 'y']`` or a sequence of pairs such + as ``[('x', 'abscissa'), ('y', 'ordinate')]``. In last case the first + element of pair specifies a field name and the second element specifies + a field docstring. If a field docstring is ``None`` or is not specified + then the default field docstring generated. Any valid Python identifier may be used for a fieldname except for names starting with an underscore. Valid identifiers consist of letters, digits, @@ -758,12 +763,17 @@ built. This option is outdated; instead, it is simpler to print the :attr:`_source` attribute. + *doc* argument specifies a class docstring. + Named tuple instances do not have per-instance dictionaries, so they are lightweight and require no more memory than regular tuples. .. versionchanged:: 3.1 Added support for *rename*. + .. versionchanged:: 3.4 + Added support for class and fields docstrings. + .. doctest:: :options: +NORMALIZE_WHITESPACE diff -r bc322469a7a8 Lib/collections/__init__.py --- a/Lib/collections/__init__.py Mon Dec 10 20:22:55 2012 -0800 +++ b/Lib/collections/__init__.py Wed Dec 12 19:12:37 2012 +0200 @@ -242,7 +242,7 @@ from collections import OrderedDict class {typename}(tuple): - '{typename}({arg_list})' + {doc!r} __slots__ = () @@ -284,13 +284,18 @@ {field_defs} ''' +_doc_template = '{typename}({arg_list})' + _repr_template = '{name}=%r' _field_template = '''\ - {name} = _property(_itemgetter({index:d}), doc='Alias for field number {index:d}') + {name} = _property(_itemgetter({index:d}), doc={doc!r}) ''' -def namedtuple(typename, field_names, verbose=False, rename=False): +_field_doc_template = 'Alias for field number {index:d}' + +def namedtuple(typename, field_names, verbose=False, rename=False, *, + doc=None): """Returns a new subclass of tuple with named fields. >>> Point = namedtuple('Point', ['x', 'y']) @@ -318,7 +323,17 @@ # message or automatically replace the field name with a valid name. if isinstance(field_names, str): field_names = field_names.replace(',', ' ').split() - field_names = list(map(str, field_names)) + field_docs = [None] * len(field_names) + else: + for index, field in enumerate(field_names): + if isinstance(field, tuple): + if len(field) != 2: + raise ValueError('Field number %d tuple has length %d; ' + '2 is required' % (index, len(field))) + field_docs = [field[1] if isinstance(field, tuple) else None + for field in field_names] + field_names = [str(field[0] if isinstance(field, tuple) else field) + for field in field_names] if rename: seen = set() for index, name in enumerate(field_names): @@ -350,15 +365,23 @@ seen.add(name) # Fill-in the class template + field_names = tuple(field_names) + arg_list = repr(field_names).replace("'", "")[1:-1] + if doc is None: + doc = _doc_template.format(typename=typename, arg_list=arg_list) class_definition = _class_template.format( typename = typename, - field_names = tuple(field_names), + doc = doc, + field_names = field_names, num_fields = len(field_names), - arg_list = repr(tuple(field_names)).replace("'", "")[1:-1], + arg_list = arg_list, repr_fmt = ', '.join(_repr_template.format(name=name) for name in field_names), - field_defs = '\n'.join(_field_template.format(index=index, name=name) - for index, name in enumerate(field_names)) + field_defs = '\n'.join( + _field_template.format(index=index, name=name, + doc=_field_doc_template.format(index=index, name=name) + if fdoc is None else fdoc) + for index, (name, fdoc) in enumerate(zip(field_names, field_docs))) ) # Execute the template string in a temporary namespace and support diff -r bc322469a7a8 Lib/test/test_collections.py --- a/Lib/test/test_collections.py Mon Dec 10 20:22:55 2012 -0800 +++ b/Lib/test/test_collections.py Wed Dec 12 19:12:37 2012 +0200 @@ -140,6 +140,10 @@ self.assertRaises(ValueError, namedtuple, 'abc', '_efg ghi') # field with leading underscore self.assertRaises(ValueError, namedtuple, 'abc', 'efg efg ghi') # duplicate field + self.assertRaises(ValueError, namedtuple, 'abc', [('efg',)]) # too small field tuple + self.assertRaises(ValueError, + namedtuple, 'abc', [('efg', 'fgh', 'ghi')]) # too large field tuple + namedtuple('Point0', 'x1 y2') # Verify that numbers are allowed in names namedtuple('_', 'a b c') # Test leading underscores in a typename @@ -156,6 +160,14 @@ def test_factory_doc_attr(self): Point = namedtuple('Point', 'x y') self.assertEqual(Point.__doc__, 'Point(x, y)') + self.assertEqual(Point.x.__doc__, 'Alias for field number 0') + self.assertEqual(Point.y.__doc__, 'Alias for field number 1') + + Point = namedtuple('Point', [('x', 'abscissa'), ('y', 'ordinate')], + doc='Point: 2-dimensional coordinate') + self.assertEqual(Point.__doc__, 'Point: 2-dimensional coordinate') + self.assertEqual(Point.x.__doc__, 'abscissa') + self.assertEqual(Point.y.__doc__, 'ordinate') def test_name_fixer(self): for spec, renamed in [