ro.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  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. Compute a resolution order for an object and its bases.
  16. .. versionchanged:: 5.0
  17. The resolution order is now based on the same C3 order that Python
  18. uses for classes. In complex instances of multiple inheritance, this
  19. may result in a different ordering.
  20. In older versions, the ordering wasn't required to be C3 compliant,
  21. and for backwards compatibility, it still isn't. If the ordering
  22. isn't C3 compliant (if it is *inconsistent*), zope.interface will
  23. make a best guess to try to produce a reasonable resolution order.
  24. Still (just as before), the results in such cases may be
  25. surprising.
  26. .. rubric:: Environment Variables
  27. Due to the change in 5.0, certain environment variables can be used to control
  28. errors and warnings about inconsistent resolution orders. They are listed in
  29. priority order, with variables at the bottom generally overriding variables
  30. above them.
  31. ZOPE_INTERFACE_WARN_BAD_IRO
  32. If this is set to "1", then if there is at least one inconsistent
  33. resolution order discovered, a warning
  34. (:class:`InconsistentResolutionOrderWarning`) will be issued. Use the
  35. usual warning mechanisms to control this behaviour. The warning text will
  36. contain additional information on debugging.
  37. ZOPE_INTERFACE_TRACK_BAD_IRO
  38. If this is set to "1", then zope.interface will log information about each
  39. inconsistent resolution order discovered, and keep those details in memory
  40. in this module for later inspection.
  41. ZOPE_INTERFACE_STRICT_IRO
  42. If this is set to "1", any attempt to use :func:`ro` that would produce a
  43. non-C3 ordering will fail by raising
  44. :class:`InconsistentResolutionOrderError`.
  45. .. important::
  46. ``ZOPE_INTERFACE_STRICT_IRO`` is intended to become the default in the
  47. future.
  48. There are two environment variables that are independent.
  49. ZOPE_INTERFACE_LOG_CHANGED_IRO
  50. If this is set to "1", then if the C3 resolution order is different from
  51. the legacy resolution order for any given object, a message explaining the
  52. differences will be logged. This is intended to be used for debugging
  53. complicated IROs.
  54. ZOPE_INTERFACE_USE_LEGACY_IRO
  55. If this is set to "1", then the C3 resolution order will *not* be used.
  56. The legacy IRO will be used instead. This is a temporary measure and will
  57. be removed in the future. It is intended to help during the transition.
  58. It implies ``ZOPE_INTERFACE_LOG_CHANGED_IRO``.
  59. .. rubric:: Debugging Behaviour Changes in zope.interface 5
  60. Most behaviour changes from zope.interface 4 to 5 are related to
  61. inconsistent resolution orders. ``ZOPE_INTERFACE_STRICT_IRO`` is the
  62. most effective tool to find such inconsistent resolution orders, and
  63. we recommend running your code with this variable set if at all
  64. possible. Doing so will ensure that all interface resolution orders
  65. are consistent, and if they're not, will immediately point the way to
  66. where this is violated.
  67. Occasionally, however, this may not be enough. This is because in some
  68. cases, a C3 ordering can be found (the resolution order is fully
  69. consistent) that is substantially different from the ad-hoc legacy
  70. ordering. In such cases, you may find that you get an unexpected value
  71. returned when adapting one or more objects to an interface. To debug
  72. this, *also* enable ``ZOPE_INTERFACE_LOG_CHANGED_IRO`` and examine the
  73. output. The main thing to look for is changes in the relative
  74. positions of interfaces for which there are registered adapters.
  75. """
  76. __docformat__ = 'restructuredtext'
  77. import warnings
  78. __all__ = [
  79. 'ro',
  80. 'InconsistentResolutionOrderError',
  81. 'InconsistentResolutionOrderWarning',
  82. ]
  83. __logger = None
  84. def _logger():
  85. global __logger # pylint:disable=global-statement
  86. if __logger is None:
  87. import logging
  88. __logger = logging.getLogger(__name__)
  89. return __logger
  90. def _legacy_mergeOrderings(orderings):
  91. """Merge multiple orderings so that within-ordering order is preserved
  92. Orderings are constrained in such a way that if an object appears
  93. in two or more orderings, then the suffix that begins with the
  94. object must be in both orderings.
  95. For example:
  96. >>> _legacy_mergeOrderings([
  97. ... ['x', 'y', 'z'],
  98. ... ['q', 'z'],
  99. ... [1, 3, 5],
  100. ... ['z']
  101. ... ])
  102. ['x', 'y', 'q', 1, 3, 5, 'z']
  103. """
  104. seen = set()
  105. result = []
  106. for ordering in reversed(orderings):
  107. for o in reversed(ordering):
  108. if o not in seen:
  109. seen.add(o)
  110. result.insert(0, o)
  111. return result
  112. def _legacy_flatten(begin):
  113. result = [begin]
  114. i = 0
  115. for ob in iter(result):
  116. i += 1
  117. # The recursive calls can be avoided by inserting the base classes
  118. # into the dynamically growing list directly after the currently
  119. # considered object; the iterator makes sure this will keep working
  120. # in the future, since it cannot rely on the length of the list
  121. # by definition.
  122. result[i:i] = ob.__bases__
  123. return result
  124. def _legacy_ro(ob):
  125. return _legacy_mergeOrderings([_legacy_flatten(ob)])
  126. ###
  127. # Compare base objects using identity, not equality. This matches what
  128. # the CPython MRO algorithm does, and is *much* faster to boot: that,
  129. # plus some other small tweaks makes the difference between 25s and 6s
  130. # in loading 446 plone/zope interface.py modules (1925 InterfaceClass,
  131. # 1200 Implements, 1100 ClassProvides objects)
  132. ###
  133. class InconsistentResolutionOrderWarning(PendingDeprecationWarning):
  134. """
  135. The warning issued when an invalid IRO is requested.
  136. """
  137. class InconsistentResolutionOrderError(TypeError):
  138. """
  139. The error raised when an invalid IRO is requested in strict mode.
  140. """
  141. def __init__(self, c3, base_tree_remaining):
  142. self.C = c3.leaf
  143. base_tree = c3.base_tree
  144. self.base_ros = {
  145. base: base_tree[i + 1]
  146. for i, base in enumerate(self.C.__bases__)
  147. }
  148. # Unfortunately, this doesn't necessarily directly match
  149. # up to any transformation on C.__bases__, because
  150. # if any were fully used up, they were removed already.
  151. self.base_tree_remaining = base_tree_remaining
  152. TypeError.__init__(self)
  153. def __str__(self):
  154. import pprint
  155. return (
  156. "{}: For object {!r}.\nBase ROs:\n{}\nConflict Location:\n{}"
  157. ).format(
  158. self.__class__.__name__,
  159. self.C,
  160. pprint.pformat(self.base_ros),
  161. pprint.pformat(self.base_tree_remaining),
  162. )
  163. class _NamedBool(int): # cannot actually inherit bool
  164. def __new__(cls, val, name):
  165. inst = super(cls, _NamedBool).__new__(cls, val)
  166. inst.__name__ = name
  167. return inst
  168. class _ClassBoolFromEnv:
  169. """
  170. Non-data descriptor that reads a transformed environment variable
  171. as a boolean, and caches the result in the class.
  172. """
  173. def __get__(self, inst, klass):
  174. import os
  175. for cls in klass.__mro__:
  176. my_name = None
  177. for k in dir(klass):
  178. if k in cls.__dict__ and cls.__dict__[k] is self:
  179. my_name = k
  180. break
  181. if my_name is not None:
  182. break
  183. else: # pragma: no cover
  184. raise RuntimeError("Unable to find self")
  185. env_name = 'ZOPE_INTERFACE_' + my_name
  186. val = os.environ.get(env_name, '') == '1'
  187. val = _NamedBool(val, my_name)
  188. setattr(klass, my_name, val)
  189. setattr(klass, 'ORIG_' + my_name, self)
  190. return val
  191. class _StaticMRO:
  192. # A previously resolved MRO, supplied by the caller.
  193. # Used in place of calculating it.
  194. had_inconsistency = None # We don't know...
  195. def __init__(self, C, mro):
  196. self.leaf = C
  197. self.__mro = tuple(mro)
  198. def mro(self):
  199. return list(self.__mro)
  200. _INCONSISTENT_RESOLUTION_ORDER = """\
  201. An inconsistent resolution order is being requested. (Interfaces should
  202. follow the Python class rules known as C3.) For backwards compatibility,
  203. zope.interface will allow this, making the best guess it can to produce as
  204. meaningful an order as possible. In the future this might be an error. Set
  205. the warning filter to error, or set the environment variable
  206. 'ZOPE_INTERFACE_TRACK_BAD_IRO' to '1' and examine ro.C3.BAD_IROS to debug, or
  207. set 'ZOPE_INTERFACE_STRICT_IRO' to raise exceptions."""
  208. class C3:
  209. # Holds the shared state during computation of an MRO.
  210. @staticmethod
  211. def resolver(C, strict, base_mros):
  212. strict = strict if strict is not None else C3.STRICT_IRO
  213. factory = C3
  214. if strict:
  215. factory = _StrictC3
  216. elif C3.TRACK_BAD_IRO:
  217. factory = _TrackingC3
  218. memo = {}
  219. base_mros = base_mros or {}
  220. for base, mro in base_mros.items():
  221. assert base in C.__bases__
  222. memo[base] = _StaticMRO(base, mro)
  223. return factory(C, memo)
  224. __mro = None
  225. __legacy_ro = None
  226. direct_inconsistency = False
  227. def __init__(self, C, memo):
  228. self.leaf = C
  229. self.memo = memo
  230. kind = self.__class__
  231. base_resolvers = []
  232. for base in C.__bases__:
  233. if base not in memo:
  234. resolver = kind(base, memo)
  235. memo[base] = resolver
  236. base_resolvers.append(memo[base])
  237. self.base_tree = [
  238. [C]
  239. ] + [
  240. memo[base].mro() for base in C.__bases__
  241. ] + [
  242. list(C.__bases__)
  243. ]
  244. self.bases_had_inconsistency = any(
  245. base.had_inconsistency for base in base_resolvers
  246. )
  247. if len(C.__bases__) == 1:
  248. self.__mro = [C] + memo[C.__bases__[0]].mro()
  249. @property
  250. def had_inconsistency(self):
  251. return self.direct_inconsistency or self.bases_had_inconsistency
  252. @property
  253. def legacy_ro(self):
  254. if self.__legacy_ro is None:
  255. self.__legacy_ro = tuple(_legacy_ro(self.leaf))
  256. return list(self.__legacy_ro)
  257. TRACK_BAD_IRO = _ClassBoolFromEnv()
  258. STRICT_IRO = _ClassBoolFromEnv()
  259. WARN_BAD_IRO = _ClassBoolFromEnv()
  260. LOG_CHANGED_IRO = _ClassBoolFromEnv()
  261. USE_LEGACY_IRO = _ClassBoolFromEnv()
  262. BAD_IROS = ()
  263. def _warn_iro(self):
  264. if not self.WARN_BAD_IRO:
  265. # For the initial release, one must opt-in to see the warning.
  266. # In the future (2021?) seeing at least the first warning will
  267. # be the default
  268. return
  269. warnings.warn(
  270. _INCONSISTENT_RESOLUTION_ORDER,
  271. InconsistentResolutionOrderWarning,
  272. )
  273. @staticmethod
  274. def _can_choose_base(base, base_tree_remaining):
  275. # From C3:
  276. # nothead = [s for s in nonemptyseqs if cand in s[1:]]
  277. for bases in base_tree_remaining:
  278. if not bases or bases[0] is base:
  279. continue
  280. for b in bases:
  281. if b is base:
  282. return False
  283. return True
  284. @staticmethod
  285. def _nonempty_bases_ignoring(base_tree, ignoring):
  286. return list(filter(None, [
  287. [b for b in bases if b is not ignoring]
  288. for bases
  289. in base_tree
  290. ]))
  291. def _choose_next_base(self, base_tree_remaining):
  292. """
  293. Return the next base.
  294. The return value will either fit the C3 constraints or be our best
  295. guess about what to do. If we cannot guess, this may raise an
  296. exception.
  297. """
  298. base = self._find_next_C3_base(base_tree_remaining)
  299. if base is not None:
  300. return base
  301. return self._guess_next_base(base_tree_remaining)
  302. def _find_next_C3_base(self, base_tree_remaining):
  303. """Return the next base that fits the constraints
  304. Return ``None`` if there isn't one.
  305. """
  306. for bases in base_tree_remaining:
  307. base = bases[0]
  308. if self._can_choose_base(base, base_tree_remaining):
  309. return base
  310. return None
  311. class _UseLegacyRO(Exception):
  312. pass
  313. def _guess_next_base(self, base_tree_remaining):
  314. # Narf. We may have an inconsistent order (we won't know for
  315. # sure until we check all the bases). Python cannot create
  316. # classes like this:
  317. #
  318. # class B1:
  319. # pass
  320. # class B2(B1):
  321. # pass
  322. # class C(B1, B2): # -> TypeError; this is like saying C(B1, B2, B1).
  323. # pass
  324. #
  325. # However, older versions of zope.interface were fine with this order.
  326. # A good example is ``providedBy(IOError())``. Because of the way
  327. # ``classImplements`` works, it winds up with ``__bases__`` ==
  328. # ``[IEnvironmentError, IIOError, IOSError, <implementedBy
  329. # Exception>]`` (on Python 3). But ``IEnvironmentError`` is a base of
  330. # both ``IIOError`` and ``IOSError``. Previously, we would get a
  331. # resolution order of ``[IIOError, IOSError, IEnvironmentError,
  332. # IStandardError, IException, Interface]`` but the standard Python
  333. # algorithm would forbid creating that order entirely.
  334. # Unlike Python's MRO, we attempt to resolve the issue. A few
  335. # heuristics have been tried. One was:
  336. #
  337. # Strip off the first (highest priority) base of each direct
  338. # base one at a time and seeing if we can come to an agreement
  339. # with the other bases. (We're trying for a partial ordering
  340. # here.) This often resolves cases (such as the IOSError case
  341. # above), and frequently produces the same ordering as the
  342. # legacy MRO did. If we looked at all the highest priority
  343. # bases and couldn't find any partial ordering, then we strip
  344. # them *all* out and begin the C3 step again. We take care not
  345. # to promote a common root over all others.
  346. #
  347. # If we only did the first part, stripped off the first
  348. # element of the first item, we could resolve simple cases.
  349. # But it tended to fail badly. If we did the whole thing, it
  350. # could be extremely painful from a performance perspective
  351. # for deep/wide things like Zope's OFS.SimpleItem.Item. Plus,
  352. # anytime you get ExtensionClass.Base into the mix, you're
  353. # likely to wind up in trouble, because it messes with the MRO
  354. # of classes. Sigh.
  355. #
  356. # So now, we fall back to the old linearization (fast to compute).
  357. self._warn_iro()
  358. self.direct_inconsistency = InconsistentResolutionOrderError(
  359. self, base_tree_remaining,
  360. )
  361. raise self._UseLegacyRO
  362. def _merge(self):
  363. # Returns a merged *list*.
  364. result = self.__mro = []
  365. base_tree_remaining = self.base_tree
  366. base = None
  367. while 1:
  368. # Take last picked base out of the base tree wherever it is.
  369. # This differs slightly from the standard Python MRO and is needed
  370. # because we have no other step that prevents duplicates
  371. # from coming in (e.g., in the inconsistent fallback path)
  372. base_tree_remaining = self._nonempty_bases_ignoring(
  373. base_tree_remaining, base
  374. )
  375. if not base_tree_remaining:
  376. return result
  377. try:
  378. base = self._choose_next_base(base_tree_remaining)
  379. except self._UseLegacyRO:
  380. self.__mro = self.legacy_ro
  381. return self.legacy_ro
  382. result.append(base)
  383. def mro(self):
  384. if self.__mro is None:
  385. self.__mro = tuple(self._merge())
  386. return list(self.__mro)
  387. class _StrictC3(C3):
  388. __slots__ = ()
  389. def _guess_next_base(self, base_tree_remaining):
  390. raise InconsistentResolutionOrderError(self, base_tree_remaining)
  391. class _TrackingC3(C3):
  392. __slots__ = ()
  393. def _guess_next_base(self, base_tree_remaining):
  394. import traceback
  395. bad_iros = C3.BAD_IROS
  396. if self.leaf not in bad_iros:
  397. if bad_iros == ():
  398. import weakref
  399. # This is a race condition, but it doesn't matter much.
  400. bad_iros = C3.BAD_IROS = weakref.WeakKeyDictionary()
  401. bad_iros[self.leaf] = t = (
  402. InconsistentResolutionOrderError(self, base_tree_remaining),
  403. traceback.format_stack()
  404. )
  405. _logger().warning("Tracking inconsistent IRO: %s", t[0])
  406. return C3._guess_next_base(self, base_tree_remaining)
  407. class _ROComparison:
  408. # Exists to compute and print a pretty string comparison
  409. # for differing ROs.
  410. # Since we're used in a logging context, and may actually never be printed,
  411. # this is a class so we can defer computing the diff until asked.
  412. # Components we use to build up the comparison report
  413. class Item:
  414. prefix = ' '
  415. def __init__(self, item):
  416. self.item = item
  417. def __str__(self):
  418. return "{}{}".format(
  419. self.prefix,
  420. self.item,
  421. )
  422. class Deleted(Item):
  423. prefix = '- '
  424. class Inserted(Item):
  425. prefix = '+ '
  426. Empty = str
  427. class ReplacedBy: # pragma: no cover
  428. prefix = '- '
  429. suffix = ''
  430. def __init__(self, chunk, total_count):
  431. self.chunk = chunk
  432. self.total_count = total_count
  433. def __iter__(self):
  434. lines = [
  435. self.prefix + str(item) + self.suffix
  436. for item in self.chunk
  437. ]
  438. while len(lines) < self.total_count:
  439. lines.append('')
  440. return iter(lines)
  441. class Replacing(ReplacedBy):
  442. prefix = "+ "
  443. suffix = ''
  444. _c3_report = None
  445. _legacy_report = None
  446. def __init__(self, c3, c3_ro, legacy_ro):
  447. self.c3 = c3
  448. self.c3_ro = c3_ro
  449. self.legacy_ro = legacy_ro
  450. def __move(self, from_, to_, chunk, operation):
  451. for x in chunk:
  452. to_.append(operation(x))
  453. from_.append(self.Empty())
  454. def _generate_report(self):
  455. if self._c3_report is None:
  456. import difflib
  457. # The opcodes we get describe how to turn 'a' into 'b'. So
  458. # the old one (legacy) needs to be first ('a')
  459. matcher = difflib.SequenceMatcher(None, self.legacy_ro, self.c3_ro)
  460. # The reports are equal length sequences. We're going for a
  461. # side-by-side diff.
  462. self._c3_report = c3_report = []
  463. self._legacy_report = legacy_report = []
  464. for opcode, leg1, leg2, c31, c32 in matcher.get_opcodes():
  465. c3_chunk = self.c3_ro[c31:c32]
  466. legacy_chunk = self.legacy_ro[leg1:leg2]
  467. if opcode == 'equal':
  468. # Guaranteed same length
  469. c3_report.extend(self.Item(x) for x in c3_chunk)
  470. legacy_report.extend(self.Item(x) for x in legacy_chunk)
  471. if opcode == 'delete':
  472. # Guaranteed same length
  473. assert not c3_chunk
  474. self.__move(
  475. c3_report, legacy_report, legacy_chunk, self.Deleted,
  476. )
  477. if opcode == 'insert':
  478. # Guaranteed same length
  479. assert not legacy_chunk
  480. self.__move(
  481. legacy_report, c3_report, c3_chunk, self.Inserted,
  482. )
  483. if opcode == 'replace': # pragma: no cover
  484. # (How do you make it output this?)
  485. # Either side could be longer.
  486. chunk_size = max(len(c3_chunk), len(legacy_chunk))
  487. c3_report.extend(self.Replacing(c3_chunk, chunk_size))
  488. legacy_report.extend(
  489. self.ReplacedBy(legacy_chunk, chunk_size),
  490. )
  491. return self._c3_report, self._legacy_report
  492. @property
  493. def _inconsistent_label(self):
  494. inconsistent = []
  495. if self.c3.direct_inconsistency:
  496. inconsistent.append('direct')
  497. if self.c3.bases_had_inconsistency:
  498. inconsistent.append('bases')
  499. return '+'.join(inconsistent) if inconsistent else 'no'
  500. def __str__(self):
  501. c3_report, legacy_report = self._generate_report()
  502. assert len(c3_report) == len(legacy_report)
  503. left_lines = [str(x) for x in legacy_report]
  504. right_lines = [str(x) for x in c3_report]
  505. # We have the same number of lines in the report; this is not
  506. # necessarily the same as the number of items in either RO.
  507. assert len(left_lines) == len(right_lines)
  508. padding = ' ' * 2
  509. max_left = max(len(x) for x in left_lines)
  510. max_right = max(len(x) for x in right_lines)
  511. left_title = f'Legacy RO (len={len(self.legacy_ro)})'
  512. right_title = 'C3 RO (len={}; inconsistent={})'.format(
  513. len(self.c3_ro),
  514. self._inconsistent_label,
  515. )
  516. lines = [
  517. (
  518. padding +
  519. left_title.ljust(max_left) +
  520. padding +
  521. right_title.ljust(max_right)
  522. ),
  523. padding + '=' * (max_left + len(padding) + max_right)
  524. ]
  525. lines += [
  526. padding + left.ljust(max_left) + padding + right
  527. for left, right in zip(left_lines, right_lines)
  528. ]
  529. return '\n'.join(lines)
  530. # Set to `Interface` once it is defined. This is used to
  531. # avoid logging false positives about changed ROs.
  532. _ROOT = None
  533. def ro(
  534. C, strict=None, base_mros=None, log_changed_ro=None, use_legacy_ro=None,
  535. ):
  536. """
  537. ro(C) -> list
  538. Compute the precedence list (mro) according to C3.
  539. :return: A fresh `list` object.
  540. .. versionchanged:: 5.0.0
  541. Add the *strict*, *log_changed_ro* and *use_legacy_ro*
  542. keyword arguments. These are provisional and likely to be
  543. removed in the future. They are most useful for testing.
  544. """
  545. # The ``base_mros`` argument is for internal optimization and
  546. # not documented.
  547. resolver = C3.resolver(C, strict, base_mros)
  548. mro = resolver.mro()
  549. log_changed = (
  550. log_changed_ro if log_changed_ro is not None
  551. else resolver.LOG_CHANGED_IRO
  552. )
  553. use_legacy = (
  554. use_legacy_ro if use_legacy_ro is not None
  555. else resolver.USE_LEGACY_IRO
  556. )
  557. if log_changed or use_legacy:
  558. legacy_ro = resolver.legacy_ro
  559. assert isinstance(legacy_ro, list)
  560. assert isinstance(mro, list)
  561. changed = legacy_ro != mro
  562. if changed:
  563. # Did only Interface move? The fix for issue #8 made that
  564. # somewhat common. It's almost certainly not a problem, though,
  565. # so allow ignoring it.
  566. legacy_without_root = [x for x in legacy_ro if x is not _ROOT]
  567. mro_without_root = [x for x in mro if x is not _ROOT]
  568. changed = legacy_without_root != mro_without_root
  569. if changed:
  570. comparison = _ROComparison(resolver, mro, legacy_ro)
  571. _logger().warning(
  572. "Object %r has different legacy and C3 MROs:\n%s",
  573. C, comparison
  574. )
  575. if resolver.had_inconsistency and legacy_ro == mro:
  576. comparison = _ROComparison(resolver, mro, legacy_ro)
  577. _logger().warning(
  578. "Object %r had inconsistent IRO and used the legacy RO:\n%s"
  579. "\nInconsistency entered at:\n%s",
  580. C, comparison, resolver.direct_inconsistency
  581. )
  582. if use_legacy:
  583. return legacy_ro
  584. return mro
  585. def is_consistent(C):
  586. """Is the resolution order for *C*, consistent according to C3.
  587. Order as computed by :func:`ro`.
  588. """
  589. return not C3.resolver(C, False, None).had_inconsistency