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

zopefoundation / zope.interface / 16098775349

04 Mar 2025 10:16PM UTC coverage: 99.073% (+0.05%) from 99.022%
16098775349

push

github

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

Apply latest meta templates, drop support for Python 3.8

2436 of 2464 branches covered (98.86%)

Branch coverage included in aggregate %.

12209 of 12318 relevant lines covered (99.12%)

6.93 hits per line

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

99.28
/src/zope/interface/ro.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
"""
7✔
15
Compute a resolution order for an object and its bases.
16

17
.. versionchanged:: 5.0
18
   The resolution order is now based on the same C3 order that Python
19
   uses for classes. In complex instances of multiple inheritance, this
20
   may result in a different ordering.
21

22
   In older versions, the ordering wasn't required to be C3 compliant,
23
   and for backwards compatibility, it still isn't. If the ordering
24
   isn't C3 compliant (if it is *inconsistent*), zope.interface will
25
   make a best guess to try to produce a reasonable resolution order.
26
   Still (just as before), the results in such cases may be
27
   surprising.
28

29
.. rubric:: Environment Variables
30

31
Due to the change in 5.0, certain environment variables can be used to control
32
errors and warnings about inconsistent resolution orders. They are listed in
33
priority order, with variables at the bottom generally overriding variables
34
above them.
35

36
ZOPE_INTERFACE_WARN_BAD_IRO
37
    If this is set to "1", then if there is at least one inconsistent
38
    resolution order discovered, a warning
39
    (:class:`InconsistentResolutionOrderWarning`) will be issued. Use the
40
    usual warning mechanisms to control this behaviour. The warning text will
41
    contain additional information on debugging.
42

43
ZOPE_INTERFACE_TRACK_BAD_IRO
44
    If this is set to "1", then zope.interface will log information about each
45
    inconsistent resolution order discovered, and keep those details in memory
46
    in this module for later inspection.
47

48
ZOPE_INTERFACE_STRICT_IRO
49
    If this is set to "1", any attempt to use :func:`ro` that would produce a
50
    non-C3 ordering will fail by raising
51
    :class:`InconsistentResolutionOrderError`.
52

53
.. important::
54

55
   ``ZOPE_INTERFACE_STRICT_IRO`` is intended to become the default in the
56
   future.
57

58
There are two environment variables that are independent.
59

60
ZOPE_INTERFACE_LOG_CHANGED_IRO
61
    If this is set to "1", then if the C3 resolution order is different from
62
    the legacy resolution order for any given object, a message explaining the
63
    differences will be logged. This is intended to be used for debugging
64
    complicated IROs.
65

66
ZOPE_INTERFACE_USE_LEGACY_IRO
67
    If this is set to "1", then the C3 resolution order will *not* be used.
68
    The legacy IRO will be used instead. This is a temporary measure and will
69
    be removed in the future. It is intended to help during the transition.
70
    It implies ``ZOPE_INTERFACE_LOG_CHANGED_IRO``.
71

72
.. rubric:: Debugging Behaviour Changes in zope.interface 5
73

74
Most behaviour changes from zope.interface 4 to 5 are related to
75
inconsistent resolution orders. ``ZOPE_INTERFACE_STRICT_IRO`` is the
76
most effective tool to find such inconsistent resolution orders, and
77
we recommend running your code with this variable set if at all
78
possible. Doing so will ensure that all interface resolution orders
79
are consistent, and if they're not, will immediately point the way to
80
where this is violated.
81

82
Occasionally, however, this may not be enough. This is because in some
83
cases, a C3 ordering can be found (the resolution order is fully
84
consistent) that is substantially different from the ad-hoc legacy
85
ordering. In such cases, you may find that you get an unexpected value
86
returned when adapting one or more objects to an interface. To debug
87
this, *also* enable ``ZOPE_INTERFACE_LOG_CHANGED_IRO`` and examine the
88
output. The main thing to look for is changes in the relative
89
positions of interfaces for which there are registered adapters.
90
"""
91
__docformat__ = 'restructuredtext'
7✔
92

93
import warnings
7✔
94

95

96
__all__ = [
7✔
97
    'ro',
98
    'InconsistentResolutionOrderError',
99
    'InconsistentResolutionOrderWarning',
100
]
101

102
__logger = None
7✔
103

104

105
def _logger():
7✔
106
    global __logger  # pylint:disable=global-statement
107
    if __logger is None:
7✔
108
        import logging
7✔
109
        __logger = logging.getLogger(__name__)
7✔
110
    return __logger
7✔
111

112

113
def _legacy_mergeOrderings(orderings):
7✔
114
    """Merge multiple orderings so that within-ordering order is preserved
115

116
    Orderings are constrained in such a way that if an object appears
117
    in two or more orderings, then the suffix that begins with the
118
    object must be in both orderings.
119

120
    For example:
121

122
    >>> _legacy_mergeOrderings([
123
    ... ['x', 'y', 'z'],
124
    ... ['q', 'z'],
125
    ... [1, 3, 5],
126
    ... ['z']
127
    ... ])
128
    ['x', 'y', 'q', 1, 3, 5, 'z']
129

130
    """
131

132
    seen = set()
7✔
133
    result = []
7✔
134
    for ordering in reversed(orderings):
7✔
135
        for o in reversed(ordering):
7✔
136
            if o not in seen:
7✔
137
                seen.add(o)
7✔
138
                result.insert(0, o)
7✔
139

140
    return result
7✔
141

142

143
def _legacy_flatten(begin):
7✔
144
    result = [begin]
7✔
145
    i = 0
7✔
146
    for ob in iter(result):
7✔
147
        i += 1
7✔
148
        # The recursive calls can be avoided by inserting the base classes
149
        # into the dynamically growing list directly after the currently
150
        # considered object;  the iterator makes sure this will keep working
151
        # in the future, since it cannot rely on the length of the list
152
        # by definition.
153
        result[i:i] = ob.__bases__
7✔
154
    return result
7✔
155

156

157
def _legacy_ro(ob):
7✔
158
    return _legacy_mergeOrderings([_legacy_flatten(ob)])
7✔
159

160
###
161
# Compare base objects using identity, not equality. This matches what
162
# the CPython MRO algorithm does, and is *much* faster to boot: that,
163
# plus some other small tweaks makes the difference between 25s and 6s
164
# in loading 446 plone/zope interface.py modules (1925 InterfaceClass,
165
# 1200 Implements, 1100 ClassProvides objects)
166
###
167

168

169
class InconsistentResolutionOrderWarning(PendingDeprecationWarning):
7✔
170
    """
171
    The warning issued when an invalid IRO is requested.
172
    """
173

174

175
class InconsistentResolutionOrderError(TypeError):
7✔
176
    """
177
    The error raised when an invalid IRO is requested in strict mode.
178
    """
179

180
    def __init__(self, c3, base_tree_remaining):
7✔
181
        self.C = c3.leaf
7✔
182
        base_tree = c3.base_tree
7✔
183
        self.base_ros = {
7✔
184
            base: base_tree[i + 1]
185
            for i, base in enumerate(self.C.__bases__)
186
        }
187
        # Unfortunately, this doesn't necessarily directly match
188
        # up to any transformation on C.__bases__, because
189
        # if any were fully used up, they were removed already.
190
        self.base_tree_remaining = base_tree_remaining
7✔
191

192
        TypeError.__init__(self)
7✔
193

194
    def __str__(self):
7✔
195
        import pprint
7✔
196
        return (
7✔
197
            "{}: For object {!r}.\nBase ROs:\n{}\nConflict Location:\n{}"
198
        ).format(
199
            self.__class__.__name__,
200
            self.C,
201
            pprint.pformat(self.base_ros),
202
            pprint.pformat(self.base_tree_remaining),
203
        )
204

205

206
class _NamedBool(int):  # cannot actually inherit bool
7✔
207

208
    def __new__(cls, val, name):
7✔
209
        inst = super(cls, _NamedBool).__new__(cls, val)
7✔
210
        inst.__name__ = name
7✔
211
        return inst
7✔
212

213

214
class _ClassBoolFromEnv:
7✔
215
    """
216
    Non-data descriptor that reads a transformed environment variable
217
    as a boolean, and caches the result in the class.
218
    """
219

220
    def __get__(self, inst, klass):
7✔
221
        import os
7✔
222
        for cls in klass.__mro__:
7✔
223
            my_name = None
7✔
224
            for k in dir(klass):
7✔
225
                if k in cls.__dict__ and cls.__dict__[k] is self:
7✔
226
                    my_name = k
7✔
227
                    break
7✔
228
            if my_name is not None:
7✔
229
                break
7✔
230
        else:  # pragma: no cover
231
            raise RuntimeError("Unable to find self")
232

233
        env_name = 'ZOPE_INTERFACE_' + my_name
7✔
234
        val = os.environ.get(env_name, '') == '1'
7✔
235
        val = _NamedBool(val, my_name)
7✔
236
        setattr(klass, my_name, val)
7✔
237
        setattr(klass, 'ORIG_' + my_name, self)
7✔
238
        return val
7✔
239

240

241
class _StaticMRO:
7✔
242
    # A previously resolved MRO, supplied by the caller.
243
    # Used in place of calculating it.
244

245
    had_inconsistency = None  # We don't know...
7✔
246

247
    def __init__(self, C, mro):
7✔
248
        self.leaf = C
7✔
249
        self.__mro = tuple(mro)
7✔
250

251
    def mro(self):
7✔
252
        return list(self.__mro)
7✔
253

254

255
_INCONSISTENT_RESOLUTION_ORDER = """\
7✔
256
An inconsistent resolution order is being requested.  (Interfaces should
257
follow the Python class rules known as C3.) For backwards compatibility,
258
zope.interface will allow this, making the best guess it can to produce as
259
meaningful an order as possible.  In the future this might be an error. Set
260
the warning filter to error, or set the environment variable
261
'ZOPE_INTERFACE_TRACK_BAD_IRO' to '1' and examine ro.C3.BAD_IROS to debug, or
262
set 'ZOPE_INTERFACE_STRICT_IRO' to raise exceptions."""
263

264

265
class C3:
7✔
266
    # Holds the shared state during computation of an MRO.
267

268
    @staticmethod
7✔
269
    def resolver(C, strict, base_mros):
7✔
270
        strict = strict if strict is not None else C3.STRICT_IRO
7✔
271
        factory = C3
7✔
272
        if strict:
7✔
273
            factory = _StrictC3
7✔
274
        elif C3.TRACK_BAD_IRO:
7✔
275
            factory = _TrackingC3
7✔
276

277
        memo = {}
7✔
278
        base_mros = base_mros or {}
7✔
279
        for base, mro in base_mros.items():
7✔
280
            assert base in C.__bases__
7✔
281
            memo[base] = _StaticMRO(base, mro)
7✔
282

283
        return factory(C, memo)
7✔
284

285
    __mro = None
7✔
286
    __legacy_ro = None
7✔
287
    direct_inconsistency = False
7✔
288

289
    def __init__(self, C, memo):
7✔
290
        self.leaf = C
7✔
291
        self.memo = memo
7✔
292
        kind = self.__class__
7✔
293

294
        base_resolvers = []
7✔
295
        for base in C.__bases__:
7✔
296
            if base not in memo:
7✔
297
                resolver = kind(base, memo)
7✔
298
                memo[base] = resolver
7✔
299
            base_resolvers.append(memo[base])
7✔
300

301
        self.base_tree = [
7✔
302
            [C]
303
        ] + [
304
            memo[base].mro() for base in C.__bases__
305
        ] + [
306
            list(C.__bases__)
307
        ]
308

309
        self.bases_had_inconsistency = any(
7✔
310
            base.had_inconsistency for base in base_resolvers
311
        )
312

313
        if len(C.__bases__) == 1:
7✔
314
            self.__mro = [C] + memo[C.__bases__[0]].mro()
7✔
315

316
    @property
7✔
317
    def had_inconsistency(self):
7✔
318
        return self.direct_inconsistency or self.bases_had_inconsistency
7✔
319

320
    @property
7✔
321
    def legacy_ro(self):
7✔
322
        if self.__legacy_ro is None:
7✔
323
            self.__legacy_ro = tuple(_legacy_ro(self.leaf))
7✔
324
        return list(self.__legacy_ro)
7✔
325

326
    TRACK_BAD_IRO = _ClassBoolFromEnv()
7✔
327
    STRICT_IRO = _ClassBoolFromEnv()
7✔
328
    WARN_BAD_IRO = _ClassBoolFromEnv()
7✔
329
    LOG_CHANGED_IRO = _ClassBoolFromEnv()
7✔
330
    USE_LEGACY_IRO = _ClassBoolFromEnv()
7✔
331
    BAD_IROS = ()
7✔
332

333
    def _warn_iro(self):
7✔
334
        if not self.WARN_BAD_IRO:
7✔
335
            # For the initial release, one must opt-in to see the warning.
336
            # In the future (2021?) seeing at least the first warning will
337
            # be the default
338
            return
7✔
339
        warnings.warn(
7✔
340
            _INCONSISTENT_RESOLUTION_ORDER,
341
            InconsistentResolutionOrderWarning,
342
        )
343

344
    @staticmethod
7✔
345
    def _can_choose_base(base, base_tree_remaining):
7✔
346
        # From C3:
347
        # nothead = [s for s in nonemptyseqs if cand in s[1:]]
348
        for bases in base_tree_remaining:
7✔
349
            if not bases or bases[0] is base:
7✔
350
                continue
7✔
351

352
            for b in bases:
7✔
353
                if b is base:
7✔
354
                    return False
7✔
355
        return True
7✔
356

357
    @staticmethod
7✔
358
    def _nonempty_bases_ignoring(base_tree, ignoring):
7✔
359
        return list(filter(None, [
7✔
360
            [b for b in bases if b is not ignoring]
361
            for bases
362
            in base_tree
363
        ]))
364

365
    def _choose_next_base(self, base_tree_remaining):
7✔
366
        """
367
        Return the next base.
368

369
        The return value will either fit the C3 constraints or be our best
370
        guess about what to do. If we cannot guess, this may raise an
371
        exception.
372
        """
373
        base = self._find_next_C3_base(base_tree_remaining)
7✔
374
        if base is not None:
7✔
375
            return base
7✔
376
        return self._guess_next_base(base_tree_remaining)
7✔
377

378
    def _find_next_C3_base(self, base_tree_remaining):
7✔
379
        """Return the next base that fits the constraints
380

381
        Return ``None`` if there isn't one.
382
        """
383
        for bases in base_tree_remaining:
7✔
384
            base = bases[0]
7✔
385
            if self._can_choose_base(base, base_tree_remaining):
7✔
386
                return base
7✔
387
        return None
7✔
388

389
    class _UseLegacyRO(Exception):
7✔
390
        pass
7✔
391

392
    def _guess_next_base(self, base_tree_remaining):
7✔
393
        # Narf. We may have an inconsistent order (we won't know for
394
        # sure until we check all the bases). Python cannot create
395
        # classes like this:
396
        #
397
        # class B1:
398
        #   pass
399
        # class B2(B1):
400
        #   pass
401
        # class C(B1, B2): # -> TypeError; this is like saying C(B1, B2, B1).
402
        #  pass
403
        #
404
        # However, older versions of zope.interface were fine with this order.
405
        # A good example is ``providedBy(IOError())``. Because of the way
406
        # ``classImplements`` works, it winds up with ``__bases__`` ==
407
        # ``[IEnvironmentError, IIOError, IOSError, <implementedBy
408
        # Exception>]`` (on Python 3). But ``IEnvironmentError`` is a base of
409
        # both ``IIOError`` and ``IOSError``. Previously, we would get a
410
        # resolution order of ``[IIOError, IOSError, IEnvironmentError,
411
        # IStandardError, IException, Interface]`` but the standard Python
412
        # algorithm would forbid creating that order entirely.
413

414
        # Unlike Python's MRO, we attempt to resolve the issue. A few
415
        # heuristics have been tried. One was:
416
        #
417
        # Strip off the first (highest priority) base of each direct
418
        # base one at a time and seeing if we can come to an agreement
419
        # with the other bases. (We're trying for a partial ordering
420
        # here.) This often resolves cases (such as the IOSError case
421
        # above), and frequently produces the same ordering as the
422
        # legacy MRO did. If we looked at all the highest priority
423
        # bases and couldn't find any partial ordering, then we strip
424
        # them *all* out and begin the C3 step again. We take care not
425
        # to promote a common root over all others.
426
        #
427
        # If we only did the first part, stripped off the first
428
        # element of the first item, we could resolve simple cases.
429
        # But it tended to fail badly. If we did the whole thing, it
430
        # could be extremely painful from a performance perspective
431
        # for deep/wide things like Zope's OFS.SimpleItem.Item. Plus,
432
        # anytime you get ExtensionClass.Base into the mix, you're
433
        # likely to wind up in trouble, because it messes with the MRO
434
        # of classes. Sigh.
435
        #
436
        # So now, we fall back to the old linearization (fast to compute).
437
        self._warn_iro()
7✔
438
        self.direct_inconsistency = InconsistentResolutionOrderError(
7✔
439
            self, base_tree_remaining,
440
        )
441
        raise self._UseLegacyRO
7✔
442

443
    def _merge(self):
7✔
444
        # Returns a merged *list*.
445
        result = self.__mro = []
7✔
446
        base_tree_remaining = self.base_tree
7✔
447
        base = None
7✔
448
        while 1:
6✔
449
            # Take last picked base out of the base tree wherever it is.
450
            # This differs slightly from the standard Python MRO and is needed
451
            # because we have no other step that prevents duplicates
452
            # from coming in (e.g., in the inconsistent fallback path)
453
            base_tree_remaining = self._nonempty_bases_ignoring(
7✔
454
                base_tree_remaining, base
455
            )
456

457
            if not base_tree_remaining:
7✔
458
                return result
7✔
459
            try:
7✔
460
                base = self._choose_next_base(base_tree_remaining)
7✔
461
            except self._UseLegacyRO:
7✔
462
                self.__mro = self.legacy_ro
7✔
463
                return self.legacy_ro
7✔
464

465
            result.append(base)
7✔
466

467
    def mro(self):
7✔
468
        if self.__mro is None:
7✔
469
            self.__mro = tuple(self._merge())
7✔
470
        return list(self.__mro)
7✔
471

472

473
class _StrictC3(C3):
7✔
474
    __slots__ = ()
7✔
475

476
    def _guess_next_base(self, base_tree_remaining):
7✔
477
        raise InconsistentResolutionOrderError(self, base_tree_remaining)
7✔
478

479

480
class _TrackingC3(C3):
7✔
481
    __slots__ = ()
7✔
482

483
    def _guess_next_base(self, base_tree_remaining):
7✔
484
        import traceback
7✔
485
        bad_iros = C3.BAD_IROS
7✔
486
        if self.leaf not in bad_iros:
7!
487
            if bad_iros == ():
7!
488
                import weakref
7✔
489

490
                # This is a race condition, but it doesn't matter much.
491
                bad_iros = C3.BAD_IROS = weakref.WeakKeyDictionary()
7✔
492
            bad_iros[self.leaf] = t = (
7✔
493
                InconsistentResolutionOrderError(self, base_tree_remaining),
494
                traceback.format_stack()
495
            )
496
            _logger().warning("Tracking inconsistent IRO: %s", t[0])
7✔
497
        return C3._guess_next_base(self, base_tree_remaining)
7✔
498

499

500
class _ROComparison:
7✔
501
    # Exists to compute and print a pretty string comparison
502
    # for differing ROs.
503
    # Since we're used in a logging context, and may actually never be printed,
504
    # this is a class so we can defer computing the diff until asked.
505

506
    # Components we use to build up the comparison report
507
    class Item:
7✔
508
        prefix = '  '
7✔
509

510
        def __init__(self, item):
7✔
511
            self.item = item
7✔
512

513
        def __str__(self):
7✔
514
            return "{}{}".format(
7✔
515
                self.prefix,
516
                self.item,
517
            )
518

519
    class Deleted(Item):
7✔
520
        prefix = '- '
7✔
521

522
    class Inserted(Item):
7✔
523
        prefix = '+ '
7✔
524

525
    Empty = str
7✔
526

527
    class ReplacedBy:  # pragma: no cover
528
        prefix = '- '
529
        suffix = ''
530

531
        def __init__(self, chunk, total_count):
532
            self.chunk = chunk
533
            self.total_count = total_count
534

535
        def __iter__(self):
536
            lines = [
537
                self.prefix + str(item) + self.suffix
538
                for item in self.chunk
539
            ]
540
            while len(lines) < self.total_count:
541
                lines.append('')
542

543
            return iter(lines)
544

545
    class Replacing(ReplacedBy):
7✔
546
        prefix = "+ "
7✔
547
        suffix = ''
7✔
548

549
    _c3_report = None
7✔
550
    _legacy_report = None
7✔
551

552
    def __init__(self, c3, c3_ro, legacy_ro):
7✔
553
        self.c3 = c3
7✔
554
        self.c3_ro = c3_ro
7✔
555
        self.legacy_ro = legacy_ro
7✔
556

557
    def __move(self, from_, to_, chunk, operation):
7✔
558
        for x in chunk:
7✔
559
            to_.append(operation(x))
7✔
560
            from_.append(self.Empty())
7✔
561

562
    def _generate_report(self):
7✔
563
        if self._c3_report is None:
7!
564
            import difflib
7✔
565

566
            # The opcodes we get describe how to turn 'a' into 'b'. So
567
            # the old one (legacy) needs to be first ('a')
568
            matcher = difflib.SequenceMatcher(None, self.legacy_ro, self.c3_ro)
7✔
569
            # The reports are equal length sequences. We're going for a
570
            # side-by-side diff.
571
            self._c3_report = c3_report = []
7✔
572
            self._legacy_report = legacy_report = []
7✔
573
            for opcode, leg1, leg2, c31, c32 in matcher.get_opcodes():
7✔
574
                c3_chunk = self.c3_ro[c31:c32]
7✔
575
                legacy_chunk = self.legacy_ro[leg1:leg2]
7✔
576

577
                if opcode == 'equal':
7✔
578
                    # Guaranteed same length
579
                    c3_report.extend(self.Item(x) for x in c3_chunk)
7✔
580
                    legacy_report.extend(self.Item(x) for x in legacy_chunk)
7✔
581
                if opcode == 'delete':
7✔
582
                    # Guaranteed same length
583
                    assert not c3_chunk
7✔
584
                    self.__move(
7✔
585
                        c3_report, legacy_report, legacy_chunk, self.Deleted,
586
                    )
587
                if opcode == 'insert':
7✔
588
                    # Guaranteed same length
589
                    assert not legacy_chunk
7✔
590
                    self.__move(
7✔
591
                        legacy_report, c3_report, c3_chunk, self.Inserted,
592
                    )
593
                if opcode == 'replace':  # pragma: no cover
594
                    # (How do you make it output this?)
595
                    # Either side could be longer.
596
                    chunk_size = max(len(c3_chunk), len(legacy_chunk))
597
                    c3_report.extend(self.Replacing(c3_chunk, chunk_size))
598
                    legacy_report.extend(
599
                        self.ReplacedBy(legacy_chunk, chunk_size),
600
                    )
601

602
        return self._c3_report, self._legacy_report
7✔
603

604
    @property
7✔
605
    def _inconsistent_label(self):
7✔
606
        inconsistent = []
7✔
607
        if self.c3.direct_inconsistency:
7✔
608
            inconsistent.append('direct')
7✔
609
        if self.c3.bases_had_inconsistency:
7✔
610
            inconsistent.append('bases')
7✔
611
        return '+'.join(inconsistent) if inconsistent else 'no'
7✔
612

613
    def __str__(self):
7✔
614
        c3_report, legacy_report = self._generate_report()
7✔
615
        assert len(c3_report) == len(legacy_report)
7✔
616

617
        left_lines = [str(x) for x in legacy_report]
7✔
618
        right_lines = [str(x) for x in c3_report]
7✔
619

620
        # We have the same number of lines in the report; this is not
621
        # necessarily the same as the number of items in either RO.
622
        assert len(left_lines) == len(right_lines)
7✔
623

624
        padding = ' ' * 2
7✔
625
        max_left = max(len(x) for x in left_lines)
7✔
626
        max_right = max(len(x) for x in right_lines)
7✔
627

628
        left_title = f'Legacy RO (len={len(self.legacy_ro)})'
7✔
629

630
        right_title = 'C3 RO (len={}; inconsistent={})'.format(
7✔
631
            len(self.c3_ro),
632
            self._inconsistent_label,
633
        )
634
        lines = [
7✔
635
            (
636
                padding +
637
                left_title.ljust(max_left) +
638
                padding +
639
                right_title.ljust(max_right)
640
            ),
641
            padding + '=' * (max_left + len(padding) + max_right)
642
        ]
643
        lines += [
7✔
644
            padding + left.ljust(max_left) + padding + right
645
            for left, right in zip(left_lines, right_lines)
646
        ]
647

648
        return '\n'.join(lines)
7✔
649

650

651
# Set to `Interface` once it is defined. This is used to
652
# avoid logging false positives about changed ROs.
653
_ROOT = None
7✔
654

655

656
def ro(
7✔
657
    C, strict=None, base_mros=None, log_changed_ro=None, use_legacy_ro=None,
658
):
659
    """
660
    ro(C) -> list
661

662
    Compute the precedence list (mro) according to C3.
663

664
    :return: A fresh `list` object.
665

666
    .. versionchanged:: 5.0.0
667
       Add the *strict*, *log_changed_ro* and *use_legacy_ro*
668
       keyword arguments. These are provisional and likely to be
669
       removed in the future. They are most useful for testing.
670
    """
671
    # The ``base_mros`` argument is for internal optimization and
672
    # not documented.
673
    resolver = C3.resolver(C, strict, base_mros)
7✔
674
    mro = resolver.mro()
7✔
675

676
    log_changed = (
7✔
677
        log_changed_ro if log_changed_ro is not None
678
        else resolver.LOG_CHANGED_IRO
679
    )
680
    use_legacy = (
7✔
681
        use_legacy_ro if use_legacy_ro is not None
682
        else resolver.USE_LEGACY_IRO
683
    )
684

685
    if log_changed or use_legacy:
7✔
686
        legacy_ro = resolver.legacy_ro
7✔
687
        assert isinstance(legacy_ro, list)
7✔
688
        assert isinstance(mro, list)
7✔
689
        changed = legacy_ro != mro
7✔
690
        if changed:
7✔
691
            # Did only Interface move? The fix for issue #8 made that
692
            # somewhat common. It's almost certainly not a problem, though,
693
            # so allow ignoring it.
694
            legacy_without_root = [x for x in legacy_ro if x is not _ROOT]
7✔
695
            mro_without_root = [x for x in mro if x is not _ROOT]
7✔
696
            changed = legacy_without_root != mro_without_root
7✔
697

698
        if changed:
7✔
699
            comparison = _ROComparison(resolver, mro, legacy_ro)
7✔
700
            _logger().warning(
7✔
701
                "Object %r has different legacy and C3 MROs:\n%s",
702
                C, comparison
703
            )
704
        if resolver.had_inconsistency and legacy_ro == mro:
7✔
705
            comparison = _ROComparison(resolver, mro, legacy_ro)
7✔
706
            _logger().warning(
7✔
707
                "Object %r had inconsistent IRO and used the legacy RO:\n%s"
708
                "\nInconsistency entered at:\n%s",
709
                C, comparison, resolver.direct_inconsistency
710
            )
711
        if use_legacy:
7✔
712
            return legacy_ro
7✔
713

714
    return mro
7✔
715

716

717
def is_consistent(C):
7✔
718
    """Is the resolution order for *C*, consistent according to C3.
719

720
    Order as computed by :func:`ro`.
721
    """
722
    return not C3.resolver(C, False, None).had_inconsistency
7✔
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