aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/zope.interface/py3/zope/interface/ro.py
blob: 52986483c2781ab0d5941c523e98d82c7a91f7de (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
##############################################################################
#
# Copyright (c) 2003 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
Compute a resolution order for an object and its bases.

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

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

.. rubric:: Environment Variables

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

ZOPE_INTERFACE_WARN_BAD_IRO
    If this is set to "1", then if there is at least one inconsistent resolution
    order discovered, a warning (:class:`InconsistentResolutionOrderWarning`) will
    be issued. Use the usual warning mechanisms to control this behaviour. The warning
    text will contain additional information on debugging.
ZOPE_INTERFACE_TRACK_BAD_IRO
    If this is set to "1", then zope.interface will log information about each
    inconsistent resolution order discovered, and keep those details in memory in this module
    for later inspection.
ZOPE_INTERFACE_STRICT_IRO
    If this is set to "1", any attempt to use :func:`ro` that would produce a non-C3
    ordering will fail by raising :class:`InconsistentResolutionOrderError`.

.. important::

   ``ZOPE_INTERFACE_STRICT_IRO`` is intended to become the default in the future.

There are two environment variables that are independent.

ZOPE_INTERFACE_LOG_CHANGED_IRO
    If this is set to "1", then if the C3 resolution order is different from
    the legacy resolution order for any given object, a message explaining the differences
    will be logged. This is intended to be used for debugging complicated IROs.
ZOPE_INTERFACE_USE_LEGACY_IRO
    If this is set to "1", then the C3 resolution order will *not* be used. The
    legacy IRO will be used instead. This is a temporary measure and will be removed in the
    future. It is intended to help during the transition.
    It implies ``ZOPE_INTERFACE_LOG_CHANGED_IRO``.

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

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

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

__all__ = [
    'ro',
    'InconsistentResolutionOrderError',
    'InconsistentResolutionOrderWarning',
]

__logger = None

def _logger():
    global __logger # pylint:disable=global-statement
    if __logger is None:
        import logging
        __logger = logging.getLogger(__name__)
    return __logger

def _legacy_mergeOrderings(orderings):
    """Merge multiple orderings so that within-ordering order is preserved

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

    For example:

    >>> _mergeOrderings([
    ... ['x', 'y', 'z'],
    ... ['q', 'z'],
    ... [1, 3, 5],
    ... ['z']
    ... ])
    ['x', 'y', 'q', 1, 3, 5, 'z']

    """

    seen = set()
    result = []
    for ordering in reversed(orderings):
        for o in reversed(ordering):
            if o not in seen:
                seen.add(o)
                result.insert(0, o)

    return result

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

def _legacy_ro(ob):
    return _legacy_mergeOrderings([_legacy_flatten(ob)])

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


class InconsistentResolutionOrderWarning(PendingDeprecationWarning):
    """
    The warning issued when an invalid IRO is requested.
    """

class InconsistentResolutionOrderError(TypeError):
    """
    The error raised when an invalid IRO is requested in strict mode.
    """

    def __init__(self, c3, base_tree_remaining):
        self.C = c3.leaf
        base_tree = c3.base_tree
        self.base_ros = {
            base: base_tree[i + 1]
            for i, base in enumerate(self.C.__bases__)
        }
        # Unfortunately, this doesn't necessarily directly match
        # up to any transformation on C.__bases__, because
        # if any were fully used up, they were removed already.
        self.base_tree_remaining = base_tree_remaining

        TypeError.__init__(self)

    def __str__(self):
        import pprint
        return "{}: For object {!r}.\nBase ROs:\n{}\nConflict Location:\n{}".format(
            self.__class__.__name__,
            self.C,
            pprint.pformat(self.base_ros),
            pprint.pformat(self.base_tree_remaining),
        )


class _NamedBool(int): # cannot actually inherit bool

    def __new__(cls, val, name):
        inst = super(cls, _NamedBool).__new__(cls, val)
        inst.__name__ = name
        return inst


class _ClassBoolFromEnv:
    """
    Non-data descriptor that reads a transformed environment variable
    as a boolean, and caches the result in the class.
    """

    def __get__(self, inst, klass):
        import os
        for cls in klass.__mro__:
            my_name = None
            for k in dir(klass):
                if k in cls.__dict__ and cls.__dict__[k] is self:
                    my_name = k
                    break
            if my_name is not None:
                break
        else: # pragma: no cover
            raise RuntimeError("Unable to find self")

        env_name = 'ZOPE_INTERFACE_' + my_name
        val = os.environ.get(env_name, '') == '1'
        val = _NamedBool(val, my_name)
        setattr(klass, my_name, val)
        setattr(klass, 'ORIG_' + my_name, self)
        return val


class _StaticMRO:
    # A previously resolved MRO, supplied by the caller.
    # Used in place of calculating it.

    had_inconsistency = None # We don't know...

    def __init__(self, C, mro):
        self.leaf = C
        self.__mro = tuple(mro)

    def mro(self):
        return list(self.__mro)


class C3:
    # Holds the shared state during computation of an MRO.

    @staticmethod
    def resolver(C, strict, base_mros):
        strict = strict if strict is not None else C3.STRICT_IRO
        factory = C3
        if strict:
            factory = _StrictC3
        elif C3.TRACK_BAD_IRO:
            factory = _TrackingC3

        memo = {}
        base_mros = base_mros or {}
        for base, mro in base_mros.items():
            assert base in C.__bases__
            memo[base] = _StaticMRO(base, mro)

        return factory(C, memo)

    __mro = None
    __legacy_ro = None
    direct_inconsistency = False

    def __init__(self, C, memo):
        self.leaf = C
        self.memo = memo
        kind = self.__class__

        base_resolvers = []
        for base in C.__bases__:
            if base not in memo:
                resolver = kind(base, memo)
                memo[base] = resolver
            base_resolvers.append(memo[base])

        self.base_tree = [
            [C]
        ] + [
            memo[base].mro() for base in C.__bases__
        ] + [
            list(C.__bases__)
        ]

        self.bases_had_inconsistency = any(base.had_inconsistency for base in base_resolvers)

        if len(C.__bases__) == 1:
            self.__mro = [C] + memo[C.__bases__[0]].mro()

    @property
    def had_inconsistency(self):
        return self.direct_inconsistency or self.bases_had_inconsistency

    @property
    def legacy_ro(self):
        if self.__legacy_ro is None:
            self.__legacy_ro = tuple(_legacy_ro(self.leaf))
        return list(self.__legacy_ro)

    TRACK_BAD_IRO = _ClassBoolFromEnv()
    STRICT_IRO = _ClassBoolFromEnv()
    WARN_BAD_IRO = _ClassBoolFromEnv()
    LOG_CHANGED_IRO = _ClassBoolFromEnv()
    USE_LEGACY_IRO = _ClassBoolFromEnv()
    BAD_IROS = ()

    def _warn_iro(self):
        if not self.WARN_BAD_IRO:
            # For the initial release, one must opt-in to see the warning.
            # In the future (2021?) seeing at least the first warning will
            # be the default
            return
        import warnings
        warnings.warn(
            "An inconsistent resolution order is being requested. "
            "(Interfaces should follow the Python class rules known as C3.) "
            "For backwards compatibility, zope.interface will allow this, "
            "making the best guess it can to produce as meaningful an order as possible. "
            "In the future this might be an error. Set the warning filter to error, or set "
            "the environment variable 'ZOPE_INTERFACE_TRACK_BAD_IRO' to '1' and examine "
            "ro.C3.BAD_IROS to debug, or set 'ZOPE_INTERFACE_STRICT_IRO' to raise exceptions.",
            InconsistentResolutionOrderWarning,
        )

    @staticmethod
    def _can_choose_base(base, base_tree_remaining):
        # From C3:
        # nothead = [s for s in nonemptyseqs if cand in s[1:]]
        for bases in base_tree_remaining:
            if not bases or bases[0] is base:
                continue

            for b in bases:
                if b is base:
                    return False
        return True

    @staticmethod
    def _nonempty_bases_ignoring(base_tree, ignoring):
        return list(filter(None, [
            [b for b in bases if b is not ignoring]
            for bases
            in base_tree
        ]))

    def _choose_next_base(self, base_tree_remaining):
        """
        Return the next base.

        The return value will either fit the C3 constraints or be our best
        guess about what to do. If we cannot guess, this may raise an exception.
        """
        base = self._find_next_C3_base(base_tree_remaining)
        if base is not None:
            return base
        return self._guess_next_base(base_tree_remaining)

    def _find_next_C3_base(self, base_tree_remaining):
        """
        Return the next base that fits the constraints, or ``None`` if there isn't one.
        """
        for bases in base_tree_remaining:
            base = bases[0]
            if self._can_choose_base(base, base_tree_remaining):
                return base
        return None

    class _UseLegacyRO(Exception):
        pass

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

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

    def _merge(self):
        # Returns a merged *list*.
        result = self.__mro = []
        base_tree_remaining = self.base_tree
        base = None
        while 1:
            # Take last picked base out of the base tree wherever it is.
            # This differs slightly from the standard Python MRO and is needed
            # because we have no other step that prevents duplicates
            # from coming in (e.g., in the inconsistent fallback path)
            base_tree_remaining = self._nonempty_bases_ignoring(base_tree_remaining, base)

            if not base_tree_remaining:
                return result
            try:
                base = self._choose_next_base(base_tree_remaining)
            except self._UseLegacyRO:
                self.__mro = self.legacy_ro
                return self.legacy_ro

            result.append(base)

    def mro(self):
        if self.__mro is None:
            self.__mro = tuple(self._merge())
        return list(self.__mro)


class _StrictC3(C3):
    __slots__ = ()
    def _guess_next_base(self, base_tree_remaining):
        raise InconsistentResolutionOrderError(self, base_tree_remaining)


class _TrackingC3(C3):
    __slots__ = ()
    def _guess_next_base(self, base_tree_remaining):
        import traceback
        bad_iros = C3.BAD_IROS
        if self.leaf not in bad_iros:
            if bad_iros == ():
                import weakref

                # This is a race condition, but it doesn't matter much.
                bad_iros = C3.BAD_IROS = weakref.WeakKeyDictionary()
            bad_iros[self.leaf] = t = (
                InconsistentResolutionOrderError(self, base_tree_remaining),
                traceback.format_stack()
            )
            _logger().warning("Tracking inconsistent IRO: %s", t[0])
        return C3._guess_next_base(self, base_tree_remaining)


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

    # Components we use to build up the comparison report
    class Item:
        prefix = '  '
        def __init__(self, item):
            self.item = item
        def __str__(self):
            return "{}{}".format(
                self.prefix,
                self.item,
            )

    class Deleted(Item):
        prefix = '- '

    class Inserted(Item):
        prefix = '+ '

    Empty = str

    class ReplacedBy: # pragma: no cover
        prefix = '- '
        suffix = ''
        def __init__(self, chunk, total_count):
            self.chunk = chunk
            self.total_count = total_count

        def __iter__(self):
            lines = [
                self.prefix + str(item) + self.suffix
                for item in self.chunk
            ]
            while len(lines) < self.total_count:
                lines.append('')

            return iter(lines)

    class Replacing(ReplacedBy):
        prefix = "+ "
        suffix = ''


    _c3_report = None
    _legacy_report = None

    def __init__(self, c3, c3_ro, legacy_ro):
        self.c3 = c3
        self.c3_ro = c3_ro
        self.legacy_ro = legacy_ro

    def __move(self, from_, to_, chunk, operation):
        for x in chunk:
            to_.append(operation(x))
            from_.append(self.Empty())

    def _generate_report(self):
        if self._c3_report is None:
            import difflib

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

                if opcode == 'equal':
                    # Guaranteed same length
                    c3_report.extend(self.Item(x) for x in c3_chunk)
                    legacy_report.extend(self.Item(x) for x in legacy_chunk)
                if opcode == 'delete':
                    # Guaranteed same length
                    assert not c3_chunk
                    self.__move(c3_report, legacy_report, legacy_chunk, self.Deleted)
                if opcode == 'insert':
                    # Guaranteed same length
                    assert not legacy_chunk
                    self.__move(legacy_report, c3_report, c3_chunk, self.Inserted)
                if opcode == 'replace': # pragma: no cover (How do you make it output this?)
                    # Either side could be longer.
                    chunk_size = max(len(c3_chunk), len(legacy_chunk))
                    c3_report.extend(self.Replacing(c3_chunk, chunk_size))
                    legacy_report.extend(self.ReplacedBy(legacy_chunk, chunk_size))

        return self._c3_report, self._legacy_report

    @property
    def _inconsistent_label(self):
        inconsistent = []
        if self.c3.direct_inconsistency:
            inconsistent.append('direct')
        if self.c3.bases_had_inconsistency:
            inconsistent.append('bases')
        return '+'.join(inconsistent) if inconsistent else 'no'

    def __str__(self):
        c3_report, legacy_report = self._generate_report()
        assert len(c3_report) == len(legacy_report)

        left_lines = [str(x) for x in legacy_report]
        right_lines = [str(x) for x in c3_report]

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

        padding = ' ' * 2
        max_left = max(len(x) for x in left_lines)
        max_right = max(len(x) for x in right_lines)

        left_title = 'Legacy RO (len={})'.format(len(self.legacy_ro))

        right_title = 'C3 RO (len={}; inconsistent={})'.format(
            len(self.c3_ro),
            self._inconsistent_label,
        )
        lines = [
            (padding + left_title.ljust(max_left) + padding + right_title.ljust(max_right)),
            padding + '=' * (max_left + len(padding) + max_right)
        ]
        lines += [
            padding + left.ljust(max_left) + padding + right
            for left, right in zip(left_lines, right_lines)
        ]

        return '\n'.join(lines)


# Set to `Interface` once it is defined. This is used to
# avoid logging false positives about changed ROs.
_ROOT = None

def ro(C, strict=None, base_mros=None, log_changed_ro=None, use_legacy_ro=None):
    """
    ro(C) -> list

    Compute the precedence list (mro) according to C3.

    :return: A fresh `list` object.

    .. versionchanged:: 5.0.0
       Add the *strict*, *log_changed_ro* and *use_legacy_ro*
       keyword arguments. These are provisional and likely to be
       removed in the future. They are most useful for testing.
    """
    # The ``base_mros`` argument is for internal optimization and
    # not documented.
    resolver = C3.resolver(C, strict, base_mros)
    mro = resolver.mro()

    log_changed = log_changed_ro if log_changed_ro is not None else resolver.LOG_CHANGED_IRO
    use_legacy = use_legacy_ro if use_legacy_ro is not None else resolver.USE_LEGACY_IRO

    if log_changed or use_legacy:
        legacy_ro = resolver.legacy_ro
        assert isinstance(legacy_ro, list)
        assert isinstance(mro, list)
        changed = legacy_ro != mro
        if changed:
            # Did only Interface move? The fix for issue #8 made that
            # somewhat common. It's almost certainly not a problem, though,
            # so allow ignoring it.
            legacy_without_root = [x for x in legacy_ro if x is not _ROOT]
            mro_without_root = [x for x in mro if x is not _ROOT]
            changed = legacy_without_root != mro_without_root

        if changed:
            comparison = _ROComparison(resolver, mro, legacy_ro)
            _logger().warning(
                "Object %r has different legacy and C3 MROs:\n%s",
                C, comparison
            )
        if resolver.had_inconsistency and legacy_ro == mro:
            comparison = _ROComparison(resolver, mro, legacy_ro)
            _logger().warning(
                "Object %r had inconsistent IRO and used the legacy RO:\n%s"
                "\nInconsistency entered at:\n%s",
                C, comparison, resolver.direct_inconsistency
            )
        if use_legacy:
            return legacy_ro

    return mro


def is_consistent(C):
    """
    Check if the resolution order for *C*, as computed by :func:`ro`, is consistent
    according to C3.
    """
    return not C3.resolver(C, False, None).had_inconsistency