• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / ExtensionClass / 16098792070

04 Mar 2025 10:13PM UTC coverage: 100.0%. Remained the same
16098792070

push

github

web-flow
Merge pull request #82 from zopefoundation/config-with-c-code-template-3c1c588c

Apply latest meta templates, drop support for Python 3.8

110 of 110 branches covered (100.0%)

Branch coverage included in aggregate %.

366 of 366 relevant lines covered (100.0%)

7.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/ExtensionClass/tests.py
1
##############################################################################
2
#
3
# Copyright (c) 2003 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14

15

16
import sys
7✔
17
import unittest
7✔
18
from doctest import DocTestSuite
7✔
19

20
from ExtensionClass import Base
7✔
21
from ExtensionClass import ExtensionClass
7✔
22

23

24
def print_dict(d):
7✔
25
    d = d.items()
7✔
26
    print('{%s}' % (', '.join(
7✔
27
        [(f'{k!r}: {v!r}') for (k, v) in sorted(d)]
28
    )))
29

30

31
def test_mixing():
7✔
32
    """Test working with a classic class
33

34
    >>> class Classic:
35
    ...   def x(self):
36
    ...     return 42
37

38
    >>> class O(Base):
39
    ...   def __of__(*a):
40
    ...      return a
41

42
    >>> class O2(Classic, O):
43
    ...   def __of__(*a):
44
    ...      return (O2.inheritedAttribute('__of__')(*a),
45
    ...              O2.inheritedAttribute('x')(a[0]))
46

47
    >>> class C(Base):
48
    ...   def __class_init__(cls):
49
    ...      print('class init called')
50
    ...      print(cls.__name__)
51
    ...   def bar(self):
52
    ...      return 'bar called'
53
    class init called
54
    C
55

56
    >>> c = C()
57
    >>> o2 = O2()
58
    >>> c.o2 = o2
59
    >>> int(c.o2 == ((o2, c), 42))
60
    1
61

62
    Test working with a new style
63

64
    >>> class Modern(object):
65
    ...   def x(self):
66
    ...     return 42
67

68
    >>> class O2(Modern, O):
69
    ...   def __of__(*a):
70
    ...      return (O2.inheritedAttribute('__of__')(*a),
71
    ...              O2.inheritedAttribute('x')(a[0]))
72

73
    >>> o2 = O2()
74
    >>> c.o2 = o2
75
    >>> int(c.o2 == ((o2, c), 42))
76
    1
77
    """
78

79

80
def test_class_creation_under_stress():
7✔
81
    """
82
    >>> numbers = []
83
    >>> for i in range(100):
84
    ...     class B(Base):
85
    ...         numbers.append(i)
86
    >>> numbers == list(range(100))
87
    True
88

89
    >>> import gc
90
    >>> x = gc.collect()
91
    """
92

93

94
def old_test_add():
7✔
95
    """test_add.py from old EC
96

97
    >>> class foo(Base):
98
    ...     def __add__(self,other):
99
    ...         print('add called')
100

101
    >>> foo()+foo()
102
    add called
103
    """
104

105

106
def proper_error_on_deleattr():
7✔
107
    """
108
    Florent Guillaume wrote:
109

110
    ...
111

112
    Excellent.
113
    Will it also fix this particularity of ExtensionClass:
114

115
    >>> class A(Base):
116
    ...   def foo(self):
117
    ...     self.gee
118
    ...   def bar(self):
119
    ...     del self.gee
120

121
    >>> a=A()
122
    >>> a.foo()  # doctest: +IGNORE_EXCEPTION_DETAIL
123
    Traceback (most recent call last):
124
    ...
125
    AttributeError: 'A' object has no attribute 'gee'
126

127
    >>> a.bar()  # doctest: +IGNORE_EXCEPTION_DETAIL
128
    Traceback (most recent call last):
129
    ...
130
    AttributeError: 'A' object has no attribute 'gee'
131

132
    I.e., the fact that KeyError is raised whereas a normal class would
133
    raise AttributeError.
134
    """
135

136

137
class TestNoInstanceDictionaryBase(unittest.TestCase):
7✔
138

139
    def _getTargetClass(self):
7✔
140
        from ExtensionClass import NoInstanceDictionaryBase
7✔
141
        return NoInstanceDictionaryBase
7✔
142

143
    def _subclass_has_no_dict(self):
7✔
144
        class B(self._getTargetClass()):
7✔
145
            pass
7✔
146
        b = B()
7✔
147
        with self.assertRaises(AttributeError):
7✔
148
            getattr(b, '__dict__')
7✔
149
        return B
7✔
150

151
    def test_slots_and_grandchild(self):
7✔
152
        class B(self._getTargetClass()):
7✔
153
            __slots__ = ('a', 'b')
7✔
154

155
        class BB(B):
7✔
156
            pass
7✔
157

158
        b = BB()
7✔
159
        with self.assertRaises(AttributeError):
7✔
160
            getattr(b, '__dict__')
7✔
161

162
        b.a = 1
7✔
163
        b.b = 2
7✔
164
        self.assertEqual(b.a, 1)
7✔
165
        self.assertEqual(b.b, 2)
7✔
166
        with self.assertRaises(AttributeError):
7✔
167
            getattr(b, '__dict__')
7✔
168

169

170
class TestNoInstanceDictionaryBasePy(TestNoInstanceDictionaryBase):
7✔
171

172
    def _getTargetClass(self):
7✔
173
        from ExtensionClass import NoInstanceDictionaryBasePy
7✔
174
        return NoInstanceDictionaryBasePy
7✔
175

176
    def test_subclass_has_no_dict(self):
7✔
177
        B = self._subclass_has_no_dict()
7✔
178
        # In Python, we implement this by adding an empty __slots__
179
        self.assertEqual(B.__slots__, ())
7✔
180

181

182
def test__basicnew__():
7✔
183
    """
184
    >>> x = Simple.__basicnew__()
185
    >>> x.__dict__
186
    {}
187
    """
188

189

190
def eqattrs(self, other, *attrs):
7✔
191
    self_data = [getattr(self, a, None) for a in attrs]
7✔
192
    other_data = [getattr(other, a, None) for a in attrs]
7✔
193
    return self_data == other_data
7✔
194

195

196
class Simple(Base):
7✔
197
    def __init__(self, name, **kw):
7✔
198
        self.__name__ = name
7✔
199
        self.__dict__.update(kw)
7✔
200
        self._v_favorite_color = 'blue'
7✔
201
        self._p_foo = 'bar'
7✔
202

203
    def __eq__(self, other):
7✔
204
        return eqattrs(self, other, '__class__', *(self.__dict__.keys()))
7✔
205

206

207
def test_basic_pickling():
7✔
208
    """
209
    >>> x = Simple('x', aaa=1, bbb='foo')
210

211
    >>> x.__getnewargs__()  # doctest: +IGNORE_EXCEPTION_DETAIL
212
    Traceback (most recent call last):
213
    ...
214
    AttributeError: ...
215

216
    >>> print_dict(x.__getstate__())
217
    {'__name__': 'x', 'aaa': 1, 'bbb': 'foo'}
218

219
    >>> f, (c,), state = x.__reduce__()
220
    >>> f.__name__
221
    '__newobj__'
222
    >>> f.__module__ in ('copyreg', 'copy_reg')
223
    True
224
    >>> c.__name__
225
    'Simple'
226

227
    >>> print_dict(state)
228
    {'__name__': 'x', 'aaa': 1, 'bbb': 'foo'}
229

230
    >>> import pickle
231
    >>> pickle.loads(pickle.dumps(x)) == x
232
    1
233
    >>> pickle.loads(pickle.dumps(x, 0)) == x
234
    1
235
    >>> pickle.loads(pickle.dumps(x, 1)) == x
236
    1
237
    >>> pickle.loads(pickle.dumps(x, 2)) == x
238
    1
239

240
    >>> x.__setstate__({'z': 1})
241
    >>> x.__dict__
242
    {'z': 1}
243
    """
244

245

246
class Custom(Simple):
7✔
247

248
    def __new__(cls, x, y):
7✔
249
        r = Base.__new__(cls)
7✔
250
        r.x, r.y = x, y
7✔
251
        return r
7✔
252

253
    def __init__(self, x, y):
7✔
254
        self.a = 42
7✔
255

256
    def __getnewargs__(self):
7✔
257
        return self.x, self.y
7✔
258

259
    def __getstate__(self):
7✔
260
        return self.a
7✔
261

262
    def __setstate__(self, a):
7✔
263
        self.a = a
7✔
264

265

266
def test_pickling_w_overrides():
7✔
267
    """
268
    >>> x = Custom('x', 'y')
269
    >>> x.a = 99
270

271
    >>> (f, (c, ax, ay), a) = x.__reduce__()
272
    >>> f.__name__
273
    '__newobj__'
274
    >>> f.__module__ in ('copy_reg', 'copyreg')
275
    True
276
    >>> c.__name__
277
    'Custom'
278
    >>> ax, ay, a
279
    ('x', 'y', 99)
280

281
    >>> import pickle
282
    >>> pickle.loads(pickle.dumps(x)) == x
283
    1
284
    >>> pickle.loads(pickle.dumps(x, 0)) == x
285
    1
286
    >>> pickle.loads(pickle.dumps(x, 1)) == x
287
    1
288
    >>> pickle.loads(pickle.dumps(x, 2)) == x
289
    1
290
    """
291

292

293
class Slotted(Base):
7✔
294
    __slots__ = 's1', 's2', '_p_splat', '_v_eek'
7✔
295

296
    def __init__(self, s1, s2):
7✔
297
        self.s1, self.s2 = s1, s2
7✔
298
        self._v_eek = 1
7✔
299
        self._p_splat = 2
7✔
300

301

302
class SubSlotted(Slotted):
7✔
303
    __slots__ = 's3', 's4'
7✔
304

305
    def __init__(self, s1, s2, s3):
7✔
306
        Slotted.__init__(self, s1, s2)
7✔
307
        self.s3 = s3
7✔
308

309
    def __eq__(self, other):
7✔
310
        return eqattrs(self, other, '__class__', 's1', 's2', 's3', 's4')
7✔
311

312

313
def test_pickling_w_slots_only():
7✔
314
    """
315
    >>> x = SubSlotted('x', 'y', 'z')
316

317
    >>> x.__getnewargs__()  # doctest: +IGNORE_EXCEPTION_DETAIL
318
    Traceback (most recent call last):
319
    ...
320
    AttributeError: ...
321

322
    >>> d, s = x.__getstate__()
323
    >>> d
324
    >>> print_dict(s)
325
    {'s1': 'x', 's2': 'y', 's3': 'z'}
326

327
    >>> import pickle
328
    >>> pickle.loads(pickle.dumps(x)) == x
329
    1
330
    >>> pickle.loads(pickle.dumps(x, 0)) == x
331
    1
332
    >>> pickle.loads(pickle.dumps(x, 1)) == x
333
    1
334
    >>> pickle.loads(pickle.dumps(x, 2)) == x
335
    1
336

337
    >>> x.s4 = 'spam'
338

339
    >>> d, s = x.__getstate__()
340
    >>> d
341
    >>> print_dict(s)
342
    {'s1': 'x', 's2': 'y', 's3': 'z', 's4': 'spam'}
343

344
    >>> pickle.loads(pickle.dumps(x)) == x
345
    1
346
    >>> pickle.loads(pickle.dumps(x, 0)) == x
347
    1
348
    >>> pickle.loads(pickle.dumps(x, 1)) == x
349
    1
350
    >>> pickle.loads(pickle.dumps(x, 2)) == x
351
    1
352
    """
353

354

355
class SubSubSlotted(SubSlotted):
7✔
356

357
    def __init__(self, s1, s2, s3, **kw):
7✔
358
        SubSlotted.__init__(self, s1, s2, s3)
7✔
359
        self.__dict__.update(kw)
7✔
360
        self._v_favorite_color = 'blue'
7✔
361
        self._p_foo = 'bar'
7✔
362

363
    def __eq__(self, other):
7✔
364
        return eqattrs(self, other,
7✔
365
                       '__class__', 's1', 's2', 's3', 's4',
366
                       *(self.__dict__.keys()))
367

368

369
def test_pickling_w_slots():
7✔
370
    """
371
    >>> x = SubSubSlotted('x', 'y', 'z', aaa=1, bbb='foo')
372

373
    >>> x.__getnewargs__()  # doctest: +IGNORE_EXCEPTION_DETAIL
374
    Traceback (most recent call last):
375
    ...
376
    AttributeError: ...
377

378
    >>> d, s = x.__getstate__()
379
    >>> print_dict(d)
380
    {'aaa': 1, 'bbb': 'foo'}
381
    >>> print_dict(s)
382
    {'s1': 'x', 's2': 'y', 's3': 'z'}
383

384
    >>> import pickle
385
    >>> pickle.loads(pickle.dumps(x)) == x
386
    1
387
    >>> pickle.loads(pickle.dumps(x, 0)) == x
388
    1
389
    >>> pickle.loads(pickle.dumps(x, 1)) == x
390
    1
391
    >>> pickle.loads(pickle.dumps(x, 2)) == x
392
    1
393

394
    >>> x.s4 = 'spam'
395

396
    >>> d, s = x.__getstate__()
397
    >>> print_dict(d)
398
    {'aaa': 1, 'bbb': 'foo'}
399
    >>> print_dict(s)
400
    {'s1': 'x', 's2': 'y', 's3': 'z', 's4': 'spam'}
401

402
    >>> pickle.loads(pickle.dumps(x)) == x
403
    1
404
    >>> pickle.loads(pickle.dumps(x, 0)) == x
405
    1
406
    >>> pickle.loads(pickle.dumps(x, 1)) == x
407
    1
408
    >>> pickle.loads(pickle.dumps(x, 2)) == x
409
    1
410

411
    """
412

413

414
def test_pickling_w_slots_w_empty_dict():
7✔
415
    """
416
    >>> x = SubSubSlotted('x', 'y', 'z')
417

418
    >>> x.__getnewargs__()  # doctest: +IGNORE_EXCEPTION_DETAIL
419
    Traceback (most recent call last):
420
    ...
421
    AttributeError: ...
422

423
    >>> d, s = x.__getstate__()
424
    >>> print_dict(d)
425
    {}
426
    >>> print_dict(s)
427
    {'s1': 'x', 's2': 'y', 's3': 'z'}
428

429
    >>> import pickle
430
    >>> pickle.loads(pickle.dumps(x)) == x
431
    1
432
    >>> pickle.loads(pickle.dumps(x, 0)) == x
433
    1
434
    >>> pickle.loads(pickle.dumps(x, 1)) == x
435
    1
436
    >>> pickle.loads(pickle.dumps(x, 2)) == x
437
    1
438

439
    >>> x.s4 = 'spam'
440

441
    >>> d, s = x.__getstate__()
442
    >>> print_dict(d)
443
    {}
444
    >>> print_dict(s)
445
    {'s1': 'x', 's2': 'y', 's3': 'z', 's4': 'spam'}
446

447
    >>> pickle.loads(pickle.dumps(x)) == x
448
    1
449
    >>> pickle.loads(pickle.dumps(x, 0)) == x
450
    1
451
    >>> pickle.loads(pickle.dumps(x, 1)) == x
452
    1
453
    >>> pickle.loads(pickle.dumps(x, 2)) == x
454
    1
455
    """
456

457

458
def test_setattr_on_extension_type():
7✔
459
    """
460
    >>> for name in 'x', '_x', 'x_', '__x_y__', '___x__', '__x___', '_x_':
461
    ...     setattr(Base, name, 1)
462
    ...     print(getattr(Base, name))
463
    ...     delattr(Base, name)
464
    ...     print(getattr(Base, name, 0))
465
    1
466
    0
467
    1
468
    0
469
    1
470
    0
471
    1
472
    0
473
    1
474
    0
475
    1
476
    0
477
    1
478
    0
479

480
    >>> Base.__foo__ = 1  # doctest: +IGNORE_EXCEPTION_DETAIL
481
    Traceback (most recent call last):
482
    ...
483
    TypeError: can't set attributes of built-in/extension type """ \
484
        """'ExtensionClass.Base' if the attribute name begins """ \
485
        """and ends with __ and contains only 4 _ characters
486

487
    >>> Base.__foo__  # doctest: +IGNORE_EXCEPTION_DETAIL
488
    Traceback (most recent call last):
489
    ...
490
    AttributeError: ...
491

492
    >>> try:
493
    ...     del Base.__foo__
494
    ... except (AttributeError, TypeError):  # different on pypy
495
    ...     print('error')
496
    error
497
    """
498

499

500
def test_mro():
7✔
501
    """ExtensionClass method-resolution order
502

503
    The EC MRO is chosen to maximize backward compatibility and
504
    provide a model that is easy to reason about.  The basic idea is:
505

506
    I'll call this the "encapsulated base"  scheme.
507

508
    Consider:
509

510
      >>> class X(Base):
511
      ...    pass
512
      >>> class Y(Base):
513
      ...    pass
514
      >>> class Z(Base):
515
      ...    pass
516

517
      >>> class C(X, Y, Z):
518
      ...    def foo(self):
519
      ...       return 42
520

521
    When we look up an attribute, we do the following:
522

523
    - Look in C's dictionary first.
524

525
    - Look up the attribute in X.  We don't care how we get the
526
      attribute from X. If X is a new-style-class, we use the new
527
      algorithm. If X is a classic class, we use left-to-right
528
      depth-first. If X is an nsEC, use the "encapsulated base"
529
      algorithm.
530

531
      If we don't find the attribute in X, look in Y and then in Z,
532
      using the same approach.
533

534
      This algorithm will produce backward compatible results, providing
535
      the equivalent of left-to-right depth-first for nsECs and classic
536
      classes.
537

538
    We'll actually do something less abstract.  We'll use a simple
539
    algorthm to merge the __mro__ of the base classes, computing an
540
    __mro__ for classic classes using the left-to-right depth-first
541
    algorithm. We'll basically lay the mros end-to-end left-to-right
542
    and remove repeats, keeping the first occurence of each class.
543

544
    >>> [c.__name__ for c in C.__mro__]
545
    ['C', 'X', 'Y', 'Z', 'Base', 'object']
546

547
    For backward-compatability's sake, we actually depart from the
548
    above description a bit. We always put Base and object last in the
549
    mro, as shown in the example above. The primary reason for this is
550
    that object provides a do-nothing __init__ method.  It is common
551
    practice to mix a C-implemented base class that implements a few
552
    methods with a Python class that implements those methods and
553
    others. The idea is that the C implementation overrides selected
554
    methods in C, so the C subclass is listed first. Unfortunately,
555
    because all extension classes are required to subclass Base, and
556
    thus, object, the C subclass brings along the __init__ object
557
    from objects, which would hide any __init__ method provided by the
558
    Python mix-in.
559

560
    Base and object are special in that they are implied by their meta
561
    classes.   For example, a new-style class always has object as an
562
    ancestor, even if it isn't listed as a base:
563

564
    >>> O = type('O', (), {})
565
    >>> [c.__name__ for c in O.__bases__]
566
    ['object']
567
    >>> [c.__name__ for c in O.__mro__]
568
    ['O', 'object']
569

570
    Similarly, Base is always an ancestor of an extension class:
571

572
    >>> E = ExtensionClass('E', (), {})
573
    >>> [c.__name__ for c in E.__bases__]
574
    ['Base']
575
    >>> [c.__name__ for c in E.__mro__]
576
    ['E', 'Base', 'object']
577

578
    Base and object are generally added soley to get a particular meta
579
    class. They aren't used to provide application functionality and
580
    really shouldn't be considered when reasoning about where
581
    attributes come from.  They do provide some useful default
582
    functionality and should be included at the end of the mro.
583

584
    Here are more examples:
585

586
    >>> from ExtensionClass import Base
587

588
    >>> class NA(object):
589
    ...  pass
590
    >>> class NB(NA):
591
    ...  pass
592
    >>> class NC(NA):
593
    ...  pass
594
    >>> class ND(NB, NC):
595
    ...  pass
596
    >>> [c.__name__ for c in ND.__mro__]
597
    ['ND', 'NB', 'NC', 'NA', 'object']
598

599
    >>> class EA(Base):
600
    ...  pass
601
    >>> class EB(EA):
602
    ...  pass
603
    >>> class EC(EA):
604
    ...  pass
605
    >>> class ED(EB, EC):
606
    ...  pass
607
    >>> [c.__name__ for c in ED.__mro__]
608
    ['ED', 'EB', 'EA', 'EC', 'Base', 'object']
609

610
    >>> class EE(ED, ND):
611
    ...  pass
612
    >>> [c.__name__ for c in EE.__mro__]
613
    ['EE', 'ED', 'EB', 'EA', 'EC', 'ND', 'NB', 'NC', 'NA', 'Base', 'object']
614

615
    >>> class EF(ND, ED):
616
    ...  pass
617
    >>> [c.__name__ for c in EF.__mro__]
618
    ['EF', 'ND', 'NB', 'NC', 'NA', 'ED', 'EB', 'EA', 'EC', 'Base', 'object']
619

620
    >>> class CA(object):
621
    ...  pass
622
    >>> class CB(CA):
623
    ...  pass
624
    >>> class CC(CA):
625
    ...  pass
626
    >>> class CD(CB, CC):
627
    ...  pass
628

629
    >>> class ECD(Base, CD):
630
    ...  pass
631
    >>> [c.__name__ for c in ECD.__mro__]
632
    ['ECD', 'CD', 'CB', 'CC', 'CA', 'Base', 'object']
633

634
    >>> class CDE(CD, Base):
635
    ...  pass
636
    >>> [c.__name__ for c in CDE.__mro__]
637
    ['CDE', 'CD', 'CB', 'CC', 'CA', 'Base', 'object']
638

639
    >>> class CEND(CD, ED, ND):
640
    ...  pass
641
    >>> [c.__name__ for c in CEND.__mro__]
642
    ['CEND', 'CD', 'CB', 'CC', 'CA', """ \
643
       """'ED', 'EB', 'EA', 'EC', 'ND', 'NB', 'NC', 'NA', 'Base', 'object']
644
    """
645

646

647
def test_avoiding___init__decoy_w_inheritedAttribute():
7✔
648
    """
649

650
    >>> class Decoy(Base):
651
    ...    pass
652

653
    >>> class B(Base):
654
    ...    def __init__(self, a, b):
655
    ...       print('__init__ %s %s' % (a, b))
656

657
    >>> class C(Decoy, B):
658
    ...    def __init__(self):
659
    ...       print('C init')
660
    ...       C.inheritedAttribute('__init__')(self, 1, 2)
661

662
    >>> x = C()
663
    C init
664
    __init__ 1 2
665
    """
666

667

668
def test_of_not_called_when_not_accessed_through_EC_instance():
7✔
669
    """
670

671
    >>> class Eek(Base):
672
    ...     def __of__(self, parent):
673
    ...         return self, parent
674

675
    If I define an EC instance as an attr of an ordinary class:
676

677
    >>> class O(object):
678
    ...     eek = Eek()
679

680
    >>> class C:
681
    ...     eek = Eek()
682

683
    I get the instance, without calling __of__, when I get it from
684
    either tha class:
685

686
    >>> O.eek is O.__dict__['eek']
687
    True
688

689
    >>> C.eek is C.__dict__['eek']
690
    True
691

692
    or an instance of the class:
693

694
    >>> O().eek is O.__dict__['eek']
695
    True
696

697
    >>> C().eek is C.__dict__['eek']
698
    True
699

700
    If I define an EC instance as an attr of an extension class:
701

702
    >>> class E(Base):
703
    ...     eek = Eek()
704

705

706
    I get the instance, without calling __of__, when I get it from
707
    tha class:
708

709
    >>> E.eek is E.__dict__['eek']
710
    True
711

712
    But __of__ is called if I go through the instance:
713

714
    >>> e = E()
715
    >>> e.eek == (E.__dict__['eek'], e)
716
    True
717
    """
718

719

720
def test_inheriting___doc__():
7✔
721
    """Old-style ExtensionClass inherited __doc__ from base classes.
722

723
    >>> class E(Base):
724
    ...     "eek"
725

726
    >>> class EE(E):
727
    ...     pass
728

729
    >>> EE.__doc__
730
    'eek'
731

732
    >>> EE().__doc__
733
    'eek'
734
    """
735

736

737
def test___of___w_metaclass_instance():
7✔
738
    """When looking for extension class instances, need to handle meta classes
739

740
    >>> class C(Base):
741
    ...     pass
742

743
    >>> class O(Base):
744
    ...     def __of__(self, parent):
745
    ...         print('__of__ called on an O')
746

747
    >>> class M(ExtensionClass):
748
    ...     pass
749

750
    >>> X = M('X', (), {})
751
    >>> class S(X, O):
752
    ...     pass
753

754
    >>> c = C()
755
    >>> c.s = S()
756
    >>> c.s
757
    __of__ called on an O
758
    """
759

760

761
def test___of__set_after_creation():
7✔
762
    """We may need to set __of__ after a class is created.
763

764
    Normally, in a class's __init__, the initialization code checks for
765
    an __of__ method and, if it isn't already set, sets __get__.
766

767
    If a class is persistent and loaded from the database, we want
768
    this to happen in __setstate__.  The pmc_init_of function allws us
769
    to do that.
770

771
    We'll create an extension class without a __of__. We'll also give
772
    it a special meta class, just to make sure that this works with
773
    funny metaclasses too:
774

775
    >>> import ExtensionClass
776
    >>> class M(ExtensionClass.ExtensionClass):
777
    ...     "A meta class"
778
    >>> def B__init__(self, name):
779
    ...     self.name = name
780
    >>> def B__repr__(self):
781
    ...     return self.name
782

783
    >>> B = M('B', (ExtensionClass.Base, ), {
784
    ...     '__init__': B__init__,
785
    ...     '__repr__': B__repr__,
786
    ... })
787

788
    >>> B.__class__ is M
789
    True
790

791
    >>> x = B('x')
792
    >>> x.y = B('y')
793
    >>> x.y
794
    y
795

796
    We define a __of__ method for B after the fact:
797

798
    >>> def __of__(self, other):
799
    ...     print('__of__(%r, %r)' % (self, other))
800
    ...     return self
801

802
    >>> B.__of__ = __of__
803

804
    We see that this has no effect:
805

806
    >>> x.y
807
    y
808

809
    Until we use pmc_init_of:
810

811
    >>> ExtensionClass.pmc_init_of(B)
812
    >>> x.y
813
    __of__(y, x)
814
    y
815

816
    Note that there is no harm in calling pmc_init_of multiple times:
817

818
    >>> ExtensionClass.pmc_init_of(B)
819
    >>> ExtensionClass.pmc_init_of(B)
820
    >>> ExtensionClass.pmc_init_of(B)
821
    >>> x.y
822
    __of__(y, x)
823
    y
824

825
    If we remove __of__, we'll go back to the behavior we had before:
826

827
    >>> del B.__of__
828
    >>> ExtensionClass.pmc_init_of(B)
829
    >>> x.y
830
    y
831
    """
832

833

834
def test_Basic_gc():
7✔
835
    """Test to make sure that EC instances participate in GC
836

837
    >>> from ExtensionClass import Base
838
    >>> import gc
839
    >>> class C1(Base):
840
    ...     pass
841
    ...
842
    >>> class C2(Base):
843
    ...     def __del__(self):
844
    ...         print('removed')
845
    ...
846
    >>> a=C1()
847
    >>> a.b = C1()
848
    >>> a.b.a = a
849
    >>> a.b.c = C2()
850
    >>> ignore = gc.collect()
851
    >>> del a
852
    >>> ignored = gc.collect()
853
    removed
854
    """
855

856

857
def test__init__w_arg():
7✔
858
    """
859
    Traditionally Base's tp_new slot was set to PyType_GenericNew
860
    which doesn't validate its arguments, so we need to support
861
    that.
862

863
    >>> Base('foo', bar='baz')  # doctest: +ELLIPSIS
864
    <ExtensionClass.Base object at ...>
865
    """
866

867

868
def test__parent__does_not_get_wrapped():
7✔
869
    """
870
    The issue at
871
    https://github.com/zopefoundation/ExtensionClass/issues/3
872
    describes how commit afb8488 made the C implementation of
873
    ExtensionClass.Base not wrap __parent__ objects, but the pure
874
    python version was still doing so. Let's make sure that the behaviour
875
    is consistent.
876

877
    >>> import ExtensionClass
878
    >>> class I(ExtensionClass.Base):
879
    ...
880
    ...     def __init__(self, id):
881
    ...         self.id = id
882
    ...
883
    ...     def __of__(self,o):
884
    ...         return 'wrapped'
885
    ...
886
    ...     def __repr__(self):
887
    ...         return self.id
888
    ...
889
    >>> x = I('a')
890
    >>> x.__parent__ = I('b')
891
    >>> x.__parent__
892
    b
893
    """
894

895

896
def test_unbound_function_as___class_init___hook():
7✔
897
    """
898
    Zope patches an unbound function as a `__class_init__` hook onto
899
    `Persistent`; let's make sure that gets called correctly.
900

901
    >>> def InitializeClass(cls):
902
    ...     print('InitializeClass called')
903
    ...     print(cls.__name__)
904
    >>> class A(Base):
905
    ...     pass
906
    >>> A.__class_init__ = InitializeClass
907
    >>> class B(A):
908
    ...     pass
909
    InitializeClass called
910
    B
911
    """
912

913

914
class TestEffectivelyCooperativeBase(unittest.TestCase):
7✔
915

916
    def test___getattribute__cooperative(self):
7✔
917
        # This is similar to test_mro() but covering a specific
918
        # application. The fact that Base and object are *always* moved
919
        # to the end of a given class's mro means that even though
920
        # the actual implementation of Base.__getattribute__ is non-cooperative
921
        # (i.e., in Python, using object.__getattribute__ directly, not
922
        # super()), it doesn't matter: everything else in the hierarchy has
923
        # already been called
924
        class YouShallNotPass(Exception):
7✔
925
            pass
7✔
926

927
        class NoAttributes:
7✔
928
            def __getattribute__(self, name):
7✔
929
                raise YouShallNotPass()
7✔
930

931
        class WithBaseAndNoAttributes(Base, NoAttributes):
7✔
932
            pass
7✔
933

934
        # Even though it's declared this way...
935
        self.assertEqual(WithBaseAndNoAttributes.__bases__,
7✔
936
                         (Base, NoAttributes))
937
        # ... the effective value puts base at the end
938
        self.assertEqual((Base, object), tuple(
7✔
939
            WithBaseAndNoAttributes.mro()[-2:]))
940

941
        # Therefore, we don't get AttributeError, we get our defined exception
942
        with self.assertRaises(YouShallNotPass):
7✔
943
            getattr(WithBaseAndNoAttributes(), 'a')
7✔
944

945

946
class Test_add_classic_mro(unittest.TestCase):
7✔
947

948
    def _callFUT(self, mro, cls):
7✔
949
        from ExtensionClass import _add_classic_mro as FUT
7✔
950
        return FUT(mro, cls)
7✔
951

952
    def test_w_empty_mro_newstyle_class_no_bases(self):
7✔
953

954
        class _Class:
7✔
955
            pass
7✔
956

957
        mro = []
7✔
958
        self._callFUT(mro, _Class)
7✔
959
        self.assertEqual(mro, [_Class, object])
7✔
960

961
    def test_w_empty_mro_newstyle_class_w_bases(self):
7✔
962

963
        class _Base:
7✔
964
            pass
7✔
965

966
        class _Derived(_Base):
7✔
967
            pass
7✔
968

969
        mro = []
7✔
970
        self._callFUT(mro, _Derived)
7✔
971
        self.assertEqual(mro, [_Derived, _Base, object])
7✔
972

973
    def test_w_empty_mro_newstyle_class_w_diamond_inheritance(self):
7✔
974

975
        class _Base:
7✔
976
            pass
7✔
977

978
        class _One(_Base):
7✔
979
            pass
7✔
980

981
        class _Another(_Base):
7✔
982
            pass
7✔
983

984
        class _Derived(_One, _Another):
7✔
985
            pass
7✔
986

987
        mro = []
7✔
988
        self._callFUT(mro, _Derived)
7✔
989
        self.assertEqual(mro, [_Derived, _One, _Base, object, _Another])
7✔
990

991
    def test_w_filled_mro_oldstyle_class_w_bases(self):
7✔
992

993
        class _Base:
7✔
994
            pass
7✔
995

996
        class _Derived(_Base):
7✔
997
            pass
7✔
998

999
        already = object()
7✔
1000
        mro = [already]
7✔
1001
        self._callFUT(mro, _Derived)
7✔
1002
        self.assertEqual(mro, [already, _Derived, _Base] +
7✔
1003
                         ([object] if sys.version_info[0] > 2 else []))
1004

1005

1006
class TestExtensionClass(unittest.TestCase):
7✔
1007

1008
    def _getTargetClass(self):
7✔
1009
        return ExtensionClass
7✔
1010

1011
    def test_compilation(self):
7✔
1012
        from ExtensionClass import _IS_PYPY
7✔
1013
        try:
7✔
1014
            from ExtensionClass import _ExtensionClass
7✔
1015
        except ImportError:  # pragma: no cover
1016
            self.assertTrue(_IS_PYPY)
1017
        else:
1018
            self.assertTrue(hasattr(_ExtensionClass, 'CAPI2'))
6✔
1019

1020
    def test_mro_classic_class(self):
7✔
1021

1022
        class _Base:
7✔
1023
            pass
7✔
1024

1025
        class _Derived(_Base, self._getTargetClass()):
7✔
1026
            pass
7✔
1027

1028
        self.assertEqual(
7✔
1029
            _Derived.__mro__,
1030
            (_Derived, _Base, self._getTargetClass(), type, object))
1031

1032
    def test_class_init(self):
7✔
1033
        class _Derived(self._getTargetClass()):
7✔
1034
            init = 0
7✔
1035

1036
            def __class_init__(cls):
7✔
1037
                cls.init = 1
7✔
1038
        Derived = _Derived('Derived', (), {})
7✔
1039
        self.assertEqual(0, _Derived.init)
7✔
1040
        self.assertEqual(1, Derived.init)
7✔
1041

1042

1043
class TestExtensionClassPy(TestExtensionClass):
7✔
1044

1045
    def _getTargetClass(self):
7✔
1046
        from ExtensionClass import ExtensionClassPy
7✔
1047
        return ExtensionClassPy
7✔
1048

1049

1050
class TestBase(unittest.TestCase):
7✔
1051

1052
    def _getTargetClass(self):
7✔
1053
        return Base
7✔
1054

1055
    def _getExtensionClass(self):
7✔
1056
        return ExtensionClass
7✔
1057

1058
    def test_data_descriptor(self):
7✔
1059
        class Descr:
7✔
1060
            def __get__(self, inst, klass):
7✔
1061
                return (inst, klass)
7✔
1062

1063
            def __set__(self, value):
7✔
1064
                "Does nothing, needed to be a data descriptor"
1065

1066
        class Obj(self._getTargetClass()):
7✔
1067
            attr = Descr()
7✔
1068

1069
        obj = Obj()
7✔
1070
        self.assertEqual(obj.attr, (obj, Obj))
7✔
1071

1072
    def __check_class_attribute(self, name, class_value):
7✔
1073
        cls = type('O', (self._getTargetClass(),), {name: class_value})
7✔
1074
        self.assertEqual(getattr(cls, name), class_value)
7✔
1075
        self.assertIsInstance(cls, self._getExtensionClass())
7✔
1076
        self.assertTrue(issubclass(cls, self._getTargetClass()))
7✔
1077

1078
        inst = cls()
7✔
1079
        self.assertIsInstance(inst, self._getTargetClass())
7✔
1080
        self.assertEqual(getattr(inst, name), class_value)
7✔
1081
        setattr(inst, name, self)
7✔
1082
        self.assertIs(getattr(inst, name), self)
7✔
1083

1084
    def test_class_attributes(self):
7✔
1085
        # parent has some special handling
1086
        self.__check_class_attribute('__parent__', None)
7✔
1087
        self.__check_class_attribute('__parent__', 'not-none')
7✔
1088

1089
        self.__check_class_attribute('nothing_special', None)
7✔
1090
        self.__check_class_attribute('nothing_special', 'not-none')
7✔
1091

1092

1093
class TestBasePy(TestBase):
7✔
1094

1095
    def _getTargetClass(self):
7✔
1096
        from ExtensionClass import BasePy
7✔
1097
        return BasePy
7✔
1098

1099
    def _getExtensionClass(self):
7✔
1100
        from ExtensionClass import ExtensionClassPy
7✔
1101
        return ExtensionClassPy
7✔
1102

1103

1104
def test_suite():
7✔
1105
    return unittest.TestSuite((
7✔
1106
        DocTestSuite('ExtensionClass'),
1107
        DocTestSuite(),
1108
        unittest.defaultTestLoader.loadTestsFromName(__name__)
1109
    ))
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc