Skip to content

Commit 052b2df

Browse files
bpo-32492: Tweak _collections._tuplegetter. (GH-11367)
* Replace the docstrings cache with sys.intern(). * Improve tests. * Unify names of tp_descr_get and tp_descr_set functions.
1 parent 5c117dd commit 052b2df

File tree

4 files changed

+75
-37
lines changed

4 files changed

+75
-37
lines changed

Lib/collections/__init__.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,6 @@ def __eq__(self, other):
316316
except ImportError:
317317
_tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc)
318318

319-
_nt_itemgetters = {}
320-
321319
def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
322320
"""Returns a new subclass of tuple with named fields.
323321
@@ -456,16 +454,9 @@ def __getnewargs__(self):
456454
'_asdict': _asdict,
457455
'__getnewargs__': __getnewargs__,
458456
}
459-
cache = _nt_itemgetters
460457
for index, name in enumerate(field_names):
461-
try:
462-
doc = cache[index]
463-
except KeyError:
464-
doc = f'Alias for field number {index}'
465-
cache[index] = doc
466-
467-
tuplegetter_object = _tuplegetter(index, doc)
468-
class_namespace[name] = tuplegetter_object
458+
doc = _sys.intern(f'Alias for field number {index}')
459+
class_namespace[name] = _tuplegetter(index, doc)
469460

470461
result = type(typename, (tuple,), class_namespace)
471462

Lib/test/test_collections.py

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import collections
44
import copy
55
import doctest
6+
import inspect
67
import operator
78
import pickle
89
from random import choice, randrange
@@ -281,20 +282,50 @@ def test_defaults(self):
281282
self.assertEqual(Point(1), (1, 20))
282283
self.assertEqual(Point(), (10, 20))
283284

285+
def test_readonly(self):
286+
Point = namedtuple('Point', 'x y')
287+
p = Point(11, 22)
288+
with self.assertRaises(AttributeError):
289+
p.x = 33
290+
with self.assertRaises(AttributeError):
291+
del p.x
292+
with self.assertRaises(TypeError):
293+
p[0] = 33
294+
with self.assertRaises(TypeError):
295+
del p[0]
296+
self.assertEqual(p.x, 11)
297+
self.assertEqual(p[0], 11)
284298

285299
@unittest.skipIf(sys.flags.optimize >= 2,
286300
"Docstrings are omitted with -O2 and above")
287301
def test_factory_doc_attr(self):
288302
Point = namedtuple('Point', 'x y')
289303
self.assertEqual(Point.__doc__, 'Point(x, y)')
304+
Point.__doc__ = '2D point'
305+
self.assertEqual(Point.__doc__, '2D point')
290306

291307
@unittest.skipIf(sys.flags.optimize >= 2,
292308
"Docstrings are omitted with -O2 and above")
293-
def test_doc_writable(self):
309+
def test_field_doc(self):
294310
Point = namedtuple('Point', 'x y')
295311
self.assertEqual(Point.x.__doc__, 'Alias for field number 0')
312+
self.assertEqual(Point.y.__doc__, 'Alias for field number 1')
296313
Point.x.__doc__ = 'docstring for Point.x'
297314
self.assertEqual(Point.x.__doc__, 'docstring for Point.x')
315+
# namedtuple can mutate doc of descriptors independently
316+
Vector = namedtuple('Vector', 'x y')
317+
self.assertEqual(Vector.x.__doc__, 'Alias for field number 0')
318+
Vector.x.__doc__ = 'docstring for Vector.x'
319+
self.assertEqual(Vector.x.__doc__, 'docstring for Vector.x')
320+
321+
@support.cpython_only
322+
@unittest.skipIf(sys.flags.optimize >= 2,
323+
"Docstrings are omitted with -O2 and above")
324+
def test_field_doc_reuse(self):
325+
P = namedtuple('P', ['m', 'n'])
326+
Q = namedtuple('Q', ['o', 'p'])
327+
self.assertIs(P.m.__doc__, Q.o.__doc__)
328+
self.assertIs(P.n.__doc__, Q.p.__doc__)
298329

299330
def test_name_fixer(self):
300331
for spec, renamed in [
@@ -319,16 +350,18 @@ def test_instance(self):
319350
self.assertEqual(p, Point(y=22, x=11))
320351
self.assertEqual(p, Point(*(11, 22)))
321352
self.assertEqual(p, Point(**dict(x=11, y=22)))
322-
self.assertRaises(TypeError, Point, 1) # too few args
323-
self.assertRaises(TypeError, Point, 1, 2, 3) # too many args
324-
self.assertRaises(TypeError, eval, 'Point(XXX=1, y=2)', locals()) # wrong keyword argument
325-
self.assertRaises(TypeError, eval, 'Point(x=1)', locals()) # missing keyword argument
353+
self.assertRaises(TypeError, Point, 1) # too few args
354+
self.assertRaises(TypeError, Point, 1, 2, 3) # too many args
355+
with self.assertRaises(TypeError): # wrong keyword argument
356+
Point(XXX=1, y=2)
357+
with self.assertRaises(TypeError): # missing keyword argument
358+
Point(x=1)
326359
self.assertEqual(repr(p), 'Point(x=11, y=22)')
327360
self.assertNotIn('__weakref__', dir(p))
328-
self.assertEqual(p, Point._make([11, 22])) # test _make classmethod
329-
self.assertEqual(p._fields, ('x', 'y')) # test _fields attribute
330-
self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method
331-
self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method
361+
self.assertEqual(p, Point._make([11, 22])) # test _make classmethod
362+
self.assertEqual(p._fields, ('x', 'y')) # test _fields attribute
363+
self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method
364+
self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method
332365

333366
try:
334367
p._replace(x=1, error=2)
@@ -360,11 +393,15 @@ def test_tupleness(self):
360393
x, y = p
361394
self.assertEqual(p, (x, y)) # unpacks like a tuple
362395
self.assertEqual((p[0], p[1]), (11, 22)) # indexable like a tuple
363-
self.assertRaises(IndexError, p.__getitem__, 3)
396+
with self.assertRaises(IndexError):
397+
p[3]
398+
self.assertEqual(p[-1], 22)
399+
self.assertEqual(hash(p), hash((11, 22)))
364400

365401
self.assertEqual(p.x, x)
366402
self.assertEqual(p.y, y)
367-
self.assertRaises(AttributeError, eval, 'p.z', locals())
403+
with self.assertRaises(AttributeError):
404+
p.z
368405

369406
def test_odd_sizes(self):
370407
Zero = namedtuple('Zero', '')
@@ -514,13 +551,13 @@ class Point(namedtuple('_Point', ['x', 'y'])):
514551
a.w = 5
515552
self.assertEqual(a.__dict__, {'w': 5})
516553

517-
def test_namedtuple_can_mutate_doc_of_descriptors_independently(self):
518-
A = namedtuple('A', 'x y')
519-
B = namedtuple('B', 'x y')
520-
A.x.__doc__ = 'foo'
521-
B.x.__doc__ = 'bar'
522-
self.assertEqual(A.x.__doc__, 'foo')
523-
self.assertEqual(B.x.__doc__, 'bar')
554+
def test_field_descriptor(self):
555+
Point = namedtuple('Point', 'x y')
556+
p = Point(11, 22)
557+
self.assertTrue(inspect.isdatadescriptor(Point.x))
558+
self.assertEqual(Point.x.__get__(p), 11)
559+
self.assertRaises(AttributeError, Point.x.__set__, p, 33)
560+
self.assertRaises(AttributeError, Point.x.__delete__, p)
524561

525562

526563
################################################################################

Lib/test/test_pydoc.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,16 @@ def test_help_output_redirect(self):
687687
finally:
688688
pydoc.getpager = getpager_old
689689

690+
def test_namedtuple_fields(self):
691+
Person = namedtuple('Person', ['nickname', 'firstname'])
692+
with captured_stdout() as help_io:
693+
pydoc.help(Person)
694+
helptext = help_io.getvalue()
695+
self.assertIn("nickname", helptext)
696+
self.assertIn("firstname", helptext)
697+
self.assertIn("Alias for field number 0", helptext)
698+
self.assertIn("Alias for field number 1", helptext)
699+
690700
def test_namedtuple_public_underscore(self):
691701
NT = namedtuple('NT', ['abc', 'def'], rename=True)
692702
with captured_stdout() as help_io:

Modules/_collectionsmodule.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,7 +2336,7 @@ _count_elements(PyObject *self, PyObject *args)
23362336
Py_RETURN_NONE;
23372337
}
23382338

2339-
/* Helper functions for namedtuples */
2339+
/* Helper function for namedtuple() ************************************/
23402340

23412341
typedef struct {
23422342
PyObject_HEAD
@@ -2369,9 +2369,11 @@ tuplegetter_new_impl(PyTypeObject *type, Py_ssize_t index, PyObject *doc)
23692369
}
23702370

23712371
static PyObject *
2372-
tuplegetterdescr_get(PyObject *self, PyObject *obj, PyObject *type)
2372+
tuplegetter_descr_get(PyObject *self, PyObject *obj, PyObject *type)
23732373
{
2374+
Py_ssize_t index = ((_tuplegetterobject*)self)->index;
23742375
PyObject *result;
2376+
23752377
if (obj == NULL) {
23762378
Py_INCREF(self);
23772379
return self;
@@ -2384,13 +2386,11 @@ tuplegetterdescr_get(PyObject *self, PyObject *obj, PyObject *type)
23842386
PyErr_Format(PyExc_TypeError,
23852387
"descriptor for index '%d' for tuple subclasses "
23862388
"doesn't apply to '%s' object",
2387-
((_tuplegetterobject*)self)->index,
2389+
index,
23882390
obj->ob_type->tp_name);
23892391
return NULL;
23902392
}
23912393

2392-
Py_ssize_t index = ((_tuplegetterobject*)self)->index;
2393-
23942394
if (!valid_index(index, PyTuple_GET_SIZE(obj))) {
23952395
PyErr_SetString(PyExc_IndexError, "tuple index out of range");
23962396
return NULL;
@@ -2402,7 +2402,7 @@ tuplegetterdescr_get(PyObject *self, PyObject *obj, PyObject *type)
24022402
}
24032403

24042404
static int
2405-
tuplegetter_set(PyObject *self, PyObject *obj, PyObject *value)
2405+
tuplegetter_descr_set(PyObject *self, PyObject *obj, PyObject *value)
24062406
{
24072407
if (value == NULL) {
24082408
PyErr_SetString(PyExc_AttributeError, "can't delete attribute");
@@ -2476,8 +2476,8 @@ static PyTypeObject tuplegetter_type = {
24762476
0, /* tp_getset */
24772477
0, /* tp_base */
24782478
0, /* tp_dict */
2479-
tuplegetterdescr_get, /* tp_descr_get */
2480-
tuplegetter_set, /* tp_descr_set */
2479+
tuplegetter_descr_get, /* tp_descr_get */
2480+
tuplegetter_descr_set, /* tp_descr_set */
24812481
0, /* tp_dictoffset */
24822482
0, /* tp_init */
24832483
0, /* tp_alloc */

0 commit comments

Comments
 (0)