123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665 |
- ##############################################################################
- #
- # 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
|