#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-contextlib2. Emits `key=value' lines, one per line, in file order; the suite reads them back with grep. A python3 start on this arm costs tens of seconds and every case here is a handful of Python-level calls over objects the probe itself builds -- microseconds beside the start -- so taking the start ONCE is the difference between a half-minute suite and a long one. THREE RULES THE VALUES OBEY, ALL ABOUT WHAT SURVIVES THE TRANSPORT. The runner parses its input line by line, so no value may contain a NEWLINE. Unwinding order is reported as a joined string ("c,b,a"), never as a list printed over several lines, and an exception is reported as `: ' with the message flattened. The suite splits each line on the FIRST `=', so a VALUE containing `=' is safe and a KEY containing one is not. Every key emitted here is `[a-z0-9_]+'. And no value may depend on the encoding stdout happens to be in. Everything below is ASCII by construction: the fixtures are single letters and small ints, and nothing in contextlib2's surface carries text the probe did not choose. WHY THERE IS NO EVENT LOOP HERE, AND WHY THAT IS THE STRONGER TEST. contextlib2 ships an async half -- AsyncExitStack, aclosing, asynccontextmanager, and nullcontext's __aenter__/__aexit__ -- and the obvious way to drive it is asyncio.run(). This probe does not, and the reason is not that asyncio is missing: it is that asyncio would add a scheduler, a selector and an event loop to a measurement that is about none of them. None of contextlib2's coroutines ever awaits anything that suspends -- every `await' in the module delegates to another coroutine -- so a two-line trampoline that `send(None)'s once and reads the StopIteration IS the complete semantics, and it fails loudly (`coroutine suspended') rather than silently if a future release starts suspending. What that buys on this image is that the async cases measure the package rather than the platform's timer and poll machinery, which no port of a pure-Python context-manager library should be gated on. WHY THE ORDERING CASES ARE THE CENTRE OF THIS FILE. An ExitStack that ran its callbacks in the wrong order, or skipped one after an earlier one raised, would still pass an import check, a packaging check and most naive round-trips -- the failure is invisible until a real caller leaks a resource. So the unwinding cases append to a list the probe owns and report the whole trace as one string: `unwind_order=c,b,a' says both that all three ran and that they ran LIFO, in a single value that cannot be half-right. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def guard(key, fn): """Emit fn()'s value, or `: ' if it raised. Nothing in this probe may abort the run: one unexpected exception would cost every case after it, and the suite would report those as absent rather than as red -- which reads as a broken port instead of as one broken case. """ try: emit(key, fn()) except Exception as exc: emit(key, '%s: %s' % (type(exc).__name__, exc)) try: import contextlib2 from contextlib2 import (ExitStack, AsyncExitStack, nullcontext, closing, aclosing, suppress, redirect_stdout, redirect_stderr, ContextDecorator, ContextStack, contextmanager, asynccontextmanager, AbstractContextManager, AbstractAsyncContextManager) except Exception as exc: # pragma: no cover print('import_error=%s: %s' % (type(exc).__name__, exc)) sys.exit(1) import io import warnings def one_line(text): return ' '.join(str(text).split()) def run(coro): """Drive a coroutine that never suspends, without an event loop. See the module docstring. A `send(None)' that does NOT raise StopIteration means the coroutine awaited something real, which for this package would be a change of behaviour rather than a scheduling detail -- so it is reported as such instead of being waited on. """ try: coro.send(None) except StopIteration as exc: return exc.value raise RuntimeError('coroutine suspended -- it awaited something real') # ============================================================================ # IDENTITY -- which copy answered, and what it says it is. # ============================================================================ # `module_file' is the one value that makes every case below attributable: a # suite that measured the wrong copy of contextlib2 (a stray sdist on sys.path, # or the stdlib `contextlib' under a shim) would agree with most of the # behavioural expectations here, because contextlib2 21.6.0 IS a backport of # CPython 3.10's contextlib. This case is what says which file was read. guard('module_file', lambda: contextlib2.__file__) guard('is_package', lambda: 'yes' if hasattr(contextlib2, '__path__') else 'no') guard('not_stdlib_contextlib', lambda: 'ok' if 'site-packages' in contextlib2.__file__ else 'suspect: ' + contextlib2.__file__) guard('dist_version', lambda: __import__('importlib.metadata', fromlist=['version']) .version('contextlib2')) # `__all__' IS THE PACKAGE'S OWN STATEMENT OF ITS SURFACE, and pinning it whole # turns a release that quietly adds or drops a public name into one diff to read # rather than into a case that was never written. Reported sorted so that a # reordering upstream is not a red. guard('all_names', lambda: ','.join(sorted(contextlib2.__all__))) guard('all_count', lambda: str(len(contextlib2.__all__))) # THERE IS NO contextlib2.__version__. The module has never defined one; the # version a caller can read at run time is the installed distribution's, out of # the .dist-info the wheel install wrote. Both halves are pinned so that a # future release growing the attribute is a diff rather than a silent change of # meaning. guard('has_dunder_version', lambda: 'yes' if hasattr(contextlib2, '__version__') else 'no') # ============================================================================ # ExitStack -- THE UNWINDING ORDER, which is the whole point of the class. # ============================================================================ # Callbacks are invoked LIFO, to match what nesting the equivalent `with' # statements would have done. A stack that ran them FIFO would still close # every resource and would still pass a leak check; what it would break is any # pair whose teardown is ordered (close the file before removing the directory # it is in). The trace is reported as ONE string so that "all three ran" and # "they ran in this order" cannot be half-asserted. def _unwind_order(): seen = [] with ExitStack() as stack: for name in ('a', 'b', 'c'): stack.callback(seen.append, name) return ','.join(seen) # EVERY CALLBACK RUNS EVEN WHEN THE BLOCK RAISES, and the ORIGINAL exception is # what reaches the caller. These are two distinct promises and a stack can keep # one without the other -- unwinding fully but swallowing the error, or # propagating the error after abandoning the rest of the stack -- so they are # reported as two values off one run. def _unwind_on_raise(): seen = [] caught = None try: with ExitStack() as stack: stack.callback(seen.append, 'a') stack.callback(seen.append, 'b') stack.callback(seen.append, 'c') raise ValueError('boom') except ValueError as exc: caught = exc return '%s|%s' % (','.join(seen), 'ValueError: %s' % caught) # pop_all() TRANSFERS THE CALLBACKS AND LEAVES THE ORIGINAL INERT. This is the # commit/rollback idiom: build a stack, and on success hand it to a caller who # will unwind it later instead of unwinding it here. The failure that matters # is a pop_all() that COPIES rather than moves -- every callback then runs # twice, which for a close() is an error and for a refund is worse. So both # sides are measured: that the original unwinds nothing, and that the new stack # unwinds everything, in order. def _pop_all(): seen = [] with ExitStack() as stack: stack.callback(seen.append, 'a') stack.callback(seen.append, 'b') new = stack.pop_all() after_original = list(seen) new.close() return '%s|%s' % (','.join(after_original) or '', ','.join(seen)) # A pushed callback RETURNING TRUE SUPPRESSES the exception, exactly as an # __exit__ returning true does. `callback()' cannot do this (its wrapper # discards the return value) and `push()' can, and that asymmetry is documented # rather than incidental -- so both arms are pinned here. def _push_suppresses(): def swallow(exc_type, exc, tb): return True with ExitStack() as stack: stack.push(swallow) raise ValueError('swallowed') return 'suppressed' def _callback_cannot_suppress(): try: with ExitStack() as stack: stack.callback(lambda: True) raise ValueError('not swallowed') except ValueError as exc: return 'propagated: %s' % exc return 'suppressed' # enter_context() DRIVES THE FULL PROTOCOL on a context manager the probe # defines -- __enter__ on the way in, __exit__ on the way out, and the value # __enter__ returned handed back to the caller. Looking the two methods up on # the TYPE rather than the instance is what the `with' statement does, and the # tracer below is a plain class so that a lookup that went to the instance # would still work; what this case measures is the calls, not the lookup. class _Tracer(object): def __init__(self, log, name, payload): self.log, self.name, self.payload = log, name, payload def __enter__(self): self.log.append('enter:' + self.name) return self.payload def __exit__(self, *exc): self.log.append('exit:' + self.name) return False def _enter_context(): log = [] with ExitStack() as stack: got = stack.enter_context(_Tracer(log, 'one', 42)) log.append('body:%d' % got) return ','.join(log) # TWO ENTERED CONTEXT MANAGERS UNWIND LIFO TOO, and this is the case a caller # actually depends on: `stack.enter_context(open(...))' twice, closed in the # reverse of the order they were opened. def _enter_context_order(): log = [] with ExitStack() as stack: stack.enter_context(_Tracer(log, 'outer', None)) stack.enter_context(_Tracer(log, 'inner', None)) return ','.join(log) # A CALLBACK THAT RAISES DURING UNWIND DOES NOT ABANDON THE REST OF THE STACK, # and the exception that reaches the caller is the LAST one raised with the # earlier ones on its __context__ chain. This is the behaviour that makes an # ExitStack safe to use for teardown at all: one broken close() must not leak # every resource below it. def _raise_during_unwind(): seen = [] def bad(): raise RuntimeError('bad-callback') caught = None try: with ExitStack() as stack: stack.callback(seen.append, 'a') stack.push(lambda *e: seen.append('b')) stack.callback(bad) except RuntimeError as exc: caught = exc return '%s|%s' % (','.join(seen), 'RuntimeError: %s' % caught) # THE __context__ CHAIN, asserted directly. A callback that raises while an # exception is already in flight must leave the original reachable as the new # exception's context, or a traceback loses the error that actually started the # unwind. contextlib2 goes to real trouble for this (_fix_exception_context), # so it is measured rather than assumed. def _context_chain(): try: with ExitStack() as stack: stack.callback(_raiser) raise ValueError('original') except RuntimeError as exc: ctx = exc.__context__ return '%s <- %s' % (type(exc).__name__, type(ctx).__name__ if ctx is not None else 'None') return 'no-raise' def _raiser(): raise RuntimeError('during-unwind') # close() IS __exit__(None, None, None) BY ANOTHER NAME -- the way to unwind a # stack that was never used as a `with' block. A stack unwound twice must be # empty the second time rather than running anything again. def _close_then_close(): seen = [] stack = ExitStack() stack.callback(seen.append, 'a') stack.close() stack.close() return ','.join(seen) # push() AND callback() BOTH RETURN THEIR ARGUMENT so that either can be used as # a decorator. A release that returned None would break that use silently: the # decorated name would become None and fail at the call site, far from here. def _decorator_returns(): stack = ExitStack() fn = lambda: None r1 = stack.callback(fn) r2 = stack.push(lambda *e: None) stack.close() return 'callback:%s push:%s' % (r1 is fn, callable(r2)) # `with ExitStack() as stack' MUST BIND THE STACK ITSELF. __enter__ returning # anything else -- None most plausibly -- would make every idiom in the docs a # TypeError on the next line. guard('exitstack_enter_self', lambda: (lambda s: 'yes' if s.__enter__() is s else 'no')(ExitStack())) guard('unwind_order', _unwind_order) guard('unwind_on_raise', _unwind_on_raise) guard('pop_all', _pop_all) guard('push_suppresses', _push_suppresses) guard('callback_cannot_suppress', _callback_cannot_suppress) guard('enter_context', _enter_context) guard('enter_context_order', _enter_context_order) guard('raise_during_unwind', _raise_during_unwind) guard('context_chain', _context_chain) guard('close_then_close', _close_then_close) guard('decorator_returns', _decorator_returns) # callback() WITH NO CALLABLE is a TypeError with a message the package writes # itself (it cannot use positional-only syntax, for the 3.6 compatibility the # header of the source explains), so the message is part of its surface. def _expect_type_error(fn): try: fn() except TypeError as exc: return 'TypeError: %s' % one_line(exc) return 'no-raise' guard('callback_no_args', lambda: _expect_type_error(lambda: ExitStack().callback())) # ============================================================================ # nullcontext -- the stand-in. # ============================================================================ # It exists so that `cm = real if cond else nullcontext()' type-checks and runs; # what it must do is yield its argument and do NOTHING on exit, including on the # raising path, where a stand-in that swallowed the exception would turn an # optional context manager into a silent error-eater. def _nullcontext_yields(): with nullcontext('payload') as v: return str(v) def _nullcontext_default(): with nullcontext() as v: return repr(v) guard('nullcontext_yields', _nullcontext_yields) guard('nullcontext_default', _nullcontext_default) def _nullcontext_reraises(): try: with nullcontext('x'): raise KeyError('k') except KeyError as exc: return 'KeyError: %s' % exc return 'suppressed' guard('nullcontext_reraises', _nullcontext_reraises) # nullcontext IS BOTH a sync and an async context manager -- 21.6.0 gave it # __aenter__/__aexit__ so the same stand-in serves an `async with'. Driven # through the trampoline; see the module docstring. async def _nullcontext_async(): async with nullcontext('async-payload') as v: return str(v) guard('nullcontext_async', lambda: run(_nullcontext_async())) # ============================================================================ # closing -- close() exactly once, including on the raising path. # ============================================================================ # The count is what matters and it is why the fixture counts rather than flags: # a `closing' that closed twice would pass a boolean check and would, on a real # file, raise on the second close inside the unwind of an unrelated exception. class _Closeable(object): def __init__(self): self.closes = 0 def close(self): self.closes += 1 def _closing_once(): c = _Closeable() with closing(c) as got: same = got is c return 'closes:%d same:%s' % (c.closes, same) def _closing_on_raise(): c = _Closeable() try: with closing(c): raise ValueError('boom') except ValueError: pass return 'closes:%d' % c.closes guard('closing_once', _closing_once) guard('closing_on_raise', _closing_on_raise) # aclosing IS THE SAME PROMISE OVER aclose(), added in 21.6.0 for async # generators. Same trampoline, same reason. class _AsyncCloseable(object): def __init__(self): self.closes = 0 async def aclose(self): self.closes += 1 async def _aclosing(): c = _AsyncCloseable() async with aclosing(c) as got: same = got is c return 'closes:%d same:%s' % (c.closes, same) guard('aclosing_once', lambda: run(_aclosing())) # ============================================================================ # suppress -- ONLY the named types, and their subclasses. # ============================================================================ # The three arms are the whole contract and each catches a different mistake: a # suppress that swallowed everything, one that swallowed nothing, and one that # compared types by identity rather than by issubclass. The last is the subtle # one -- `suppress(LookupError)' must swallow a KeyError -- and it is the case a # reimplementation is most likely to get wrong. def _suppress_named(): with suppress(KeyError): raise KeyError('k') return 'suppressed' def _suppress_other(): try: with suppress(KeyError): raise ValueError('v') except ValueError as exc: return 'propagated: ValueError: %s' % exc return 'suppressed' def _suppress_subclass(): with suppress(LookupError): raise KeyError('k') return 'suppressed' def _suppress_empty(): try: with suppress(): raise ValueError('v') except ValueError: return 'propagated' return 'suppressed' guard('suppress_named', _suppress_named) guard('suppress_other', _suppress_other) guard('suppress_subclass', _suppress_subclass) guard('suppress_empty', _suppress_empty) # suppress.__enter__ RETURNS None -- it has no payload, and `with suppress(E) as # x' binding anything else would be a change of surface. guard('suppress_enter_none', lambda: repr(suppress(KeyError).__enter__())) # ============================================================================ # redirect_stdout / redirect_stderr -- and the RE-ENTRANCY that 21.6.0 has. # ============================================================================ # The stream is saved on a LIST of old targets rather than in one slot, so the # same instance may be entered twice and each exit restores one level. A # reimplementation with a single slot passes the simple case and corrupts the # nested one, leaving sys.stdout pointing at a closed buffer -- which on this # image would take the guest's console with it. So the nested arm is measured, # and the restoration is asserted by identity against the object the probe saved # before it started. def _redirect_stdout(): real = sys.stdout buf = io.StringIO() with redirect_stdout(buf): print('captured') return 'captured:%s restored:%s' % (buf.getvalue().strip(), sys.stdout is real) def _redirect_stderr(): real = sys.stderr buf = io.StringIO() with redirect_stderr(buf): print('err-captured', file=sys.stderr) return 'captured:%s restored:%s' % (buf.getvalue().strip(), sys.stderr is real) def _redirect_reentrant(): real = sys.stdout outer, inner = io.StringIO(), io.StringIO() cm = redirect_stdout(outer) with cm: print('one') with redirect_stdout(inner): print('two') print('three') return 'outer:%s inner:%s restored:%s' % ( ','.join(outer.getvalue().split()), inner.getvalue().strip(), sys.stdout is real) guard('redirect_stdout', _redirect_stdout) guard('redirect_stderr', _redirect_stderr) guard('redirect_reentrant', _redirect_reentrant) # ============================================================================ # ContextDecorator -- the SAME OBJECT as a decorator and as a with-block. # ============================================================================ # That is the ticket's wording and it is the sharp end of the class: __call__ # wraps the function in `with self._recreate_cm()', and the default # _recreate_cm returns self, so a plain ContextDecorator subclass is reusable # and the trace below shows both uses over ONE instance. class _Deco(ContextDecorator): def __init__(self, log): self.log = log def __enter__(self): self.log.append('in') return self def __exit__(self, *exc): self.log.append('out') return False def _decorator_use(): log = [] d = _Deco(log) @d def work(): log.append('work') work() with d: log.append('block') work() return ','.join(log) guard('contextdecorator_both', _decorator_use) # @contextmanager PRODUCES A ContextDecorator TOO, and a one-shot one: the # generator is consumed by the first use, so _recreate_cm rebuilds it from the # saved func/args. Calling the decorated function twice is what proves the # rebuild happened; without it the second call raises "generator didn't yield". def _contextmanager_decorator(): log = [] @contextmanager def cm(): log.append('in') yield log.append('out') @cm() def work(): log.append('work') work() work() return ','.join(log) guard('contextmanager_decorator', _contextmanager_decorator) # @contextmanager's TWO ERROR PATHS, both RuntimeError with messages that are # part of the surface: a generator that yields nothing, and one that yields # twice. These are what a caller actually meets when they write the generator # wrong, so the wording is pinned rather than just the type. def _cm_no_yield(): @contextmanager def cm(): if False: yield try: with cm(): pass except RuntimeError as exc: return 'RuntimeError: %s' % one_line(exc) return 'no-raise' def _cm_two_yields(): @contextmanager def cm(): yield 1 yield 2 try: with cm(): pass except RuntimeError as exc: return 'RuntimeError: %s' % one_line(exc) return 'no-raise' # A @contextmanager GENERATOR SEES THE EXCEPTION AT ITS YIELD, which is what # makes try/finally inside it work, and it may SUPPRESS by swallowing it. def _cm_sees_exception(): seen = [] @contextmanager def cm(): try: yield except ValueError as exc: seen.append('caught:%s' % exc) finally: seen.append('finally') with cm(): raise ValueError('inner') return ','.join(seen) guard('cm_no_yield', _cm_no_yield) guard('cm_two_yields', _cm_two_yields) guard('cm_sees_exception', _cm_sees_exception) # ============================================================================ # The ABCs and their __subclasshook__ -- structural, not nominal. # ============================================================================ # AbstractContextManager recognises ANY class defining __enter__ and __exit__, # without registration or inheritance. That is what lets `isinstance(open(f), # AbstractContextManager)' be true, and a hook that regressed to nominal # checking would break every duck-typed caller while passing every case above. class _Duck(object): def __enter__(self): return self def __exit__(self, *exc): return False class _AsyncDuck(object): async def __aenter__(self): return self async def __aexit__(self, *exc): return False guard('abc_sync_duck', lambda: 'yes' if issubclass(_Duck, AbstractContextManager) else 'no') guard('abc_sync_rejects', lambda: 'yes' if not issubclass(_Closeable, AbstractContextManager) else 'no') guard('abc_async_duck', lambda: 'yes' if issubclass(_AsyncDuck, AbstractAsyncContextManager) else 'no') # ExitStack IS an AbstractContextManager and AsyncExitStack an # AbstractAsyncContextManager -- by inheritance here, but asserted because a # caller may annotate against the ABC and never touch the concrete type. guard('abc_exitstack', lambda: 'yes' if issubclass(ExitStack, AbstractContextManager) else 'no') guard('abc_asyncexitstack', lambda: 'yes' if issubclass(AsyncExitStack, AbstractAsyncContextManager) else 'no') # THE ABCs ARE SUBSCRIPTABLE -- __class_getitem__ is a classmethod returning a # GenericAlias, so `AbstractContextManager[int]' is legal in an annotation # evaluated at run time. A release that dropped it would break any caller who # does not use `from __future__ import annotations'. guard('abc_subscriptable', lambda: type(AbstractContextManager[int]).__name__) # ============================================================================ # AsyncExitStack -- the same unwinding promises, over coroutines. # ============================================================================ # It is a separate implementation, not a wrapper, so LIFO order and # run-everything-on-raise are re-measured rather than inferred from the sync # stack. It also MIXES sync and async callbacks on one stack, which is the # case a caller writing `async with AsyncExitStack()' actually hits, and which # only this class can get wrong. async def _async_unwind_order(): seen = [] async def acb(name): seen.append(name) async with AsyncExitStack() as stack: stack.push_async_callback(acb, 'a') stack.push_async_callback(acb, 'b') stack.push_async_callback(acb, 'c') return ','.join(seen) async def _async_mixed_order(): seen = [] async def acb(name): seen.append(name) async with AsyncExitStack() as stack: stack.callback(seen.append, 'sync-a') stack.push_async_callback(acb, 'async-b') stack.callback(seen.append, 'sync-c') return ','.join(seen) async def _async_enter_context(): log = [] class _ATracer(object): async def __aenter__(self): log.append('aenter') return 'payload' async def __aexit__(self, *exc): log.append('aexit') return False async with AsyncExitStack() as stack: got = await stack.enter_async_context(_ATracer()) log.append('body:%s' % got) return ','.join(log) async def _async_unwind_on_raise(): seen = [] async def acb(name): seen.append(name) caught = None try: async with AsyncExitStack() as stack: stack.push_async_callback(acb, 'a') stack.push_async_callback(acb, 'b') raise ValueError('async-boom') except ValueError as exc: caught = exc return '%s|ValueError: %s' % (','.join(seen), caught) async def _async_pop_all(): seen = [] async def acb(name): seen.append(name) async with AsyncExitStack() as stack: stack.push_async_callback(acb, 'a') new = stack.pop_all() after_original = list(seen) await new.aclose() return '%s|%s' % (','.join(after_original) or '', ','.join(seen)) guard('async_unwind_order', lambda: run(_async_unwind_order())) guard('async_mixed_order', lambda: run(_async_mixed_order())) guard('async_enter_context', lambda: run(_async_enter_context())) guard('async_unwind_on_raise', lambda: run(_async_unwind_on_raise())) guard('async_pop_all', lambda: run(_async_pop_all())) # @asynccontextmanager, the async half of the decorator, through the same # trampoline. async def _async_cm(): log = [] @asynccontextmanager async def acm(): log.append('in') yield 'v' log.append('out') async with acm() as v: log.append('body:%s' % v) return ','.join(log) guard('asynccontextmanager', lambda: run(_async_cm())) # ============================================================================ # THE DEPRECATED SURFACE -- ContextStack and refresh_cm. # ============================================================================ # Both still work and both warn, and BOTH HALVES ARE THE ASSERTION: a release # that dropped the warning is as much a change as one that dropped the alias. # The warnings are captured with catch_warnings(record=True) and `always', since # a DeprecationWarning is invisible by default and a case that merely "did not # crash" would say nothing. def _contextstack_alias(): with warnings.catch_warnings(record=True) as caught: warnings.simplefilter('always') seen = [] stack = ContextStack() stack.register(seen.append, 'a') stack.register_exit(lambda *e: seen.append('b')) stack.close() names = ','.join(w.category.__name__ for w in caught) return 'unwound:%s warned:%s subclass:%s' % ( ','.join(seen), names or '', issubclass(ContextStack, ExitStack)) def _contextstack_preserve(): with warnings.catch_warnings(record=True): warnings.simplefilter('always') seen = [] stack = ContextStack() stack.register(seen.append, 'a') new = stack.preserve() stack.close() after_original = list(seen) new.close() return '%s|%s' % (','.join(after_original) or '', ','.join(seen)) def _refresh_cm(): with warnings.catch_warnings(record=True) as caught: warnings.simplefilter('always') d = _Deco([]) same = d.refresh_cm() is d names = ','.join(w.category.__name__ for w in caught) return 'same:%s warned:%s' % (same, names or '') guard('contextstack_alias', _contextstack_alias) guard('contextstack_preserve', _contextstack_preserve) guard('refresh_cm', _refresh_cm) # ============================================================================ # THE PLATFORM NEGATIVE -- nothing here loaded a shared object. # ============================================================================ # This image's python3 is one static binary with an empty lib-dynload and no # dynamic loader (BRINTOS-1476), so a module carrying a compiled extension would # build and then be unimportable. contextlib2 has none and never had one; this # case asserts that against the LOADED module set rather than against the tree, # so a future release that grew an accelerator is caught by the thing that would # actually fail. # # IT IS SCOPED TO site-packages ON PURPOSE. An unscoped sweep of sys.modules # would also name the interpreter's OWN extension modules, which on the x86-64 # control are fourteen .so files and on this target are zero -- so the unscoped # form measures how the interpreter was linked and would differ between the arms # for a reason that has nothing to do with this package. Scoped, it asks the # only question this port owns: did anything the WHEEL installed load as a # shared object. The answer is the same on both arms, which is what makes it an # assertion rather than a platform reading. guard('no_ext_loaded_from_sitepkg', lambda: ','.join(sorted( n for n, m in sys.modules.items() if getattr(m, '__file__', None) and str(m.__file__).endswith('.so') and 'site-packages' in str(m.__file__))) or '') guard('interpreter_version', lambda: '%d.%d.%d' % sys.version_info[:3]) # ----------------------------------------------------------------- the end --- emit('emitted_count', str(len(emitted))) emit('probe_done', 'yes')