. "$BST_LIB" # BST-TIMEOUT: 1800 # THREE python3 starts, and they are the whole cost. # # Everything else below reads the filesystem or dpkg with # # shell builtins, or greps the probe's output. # # # # The three are: a bare `import'; a second import whose # # STDERR is the assertion (`import-quiet'); and the probe, # # which takes ONE start for fifty-seven measurements. # # # # 1800 is 600s per start, against the ~60s a python3 start # # has measured at on this arm and the ~45s a whole # # one-start suite (99-python3-typing-extensions) measured # # at on an idle boot. The margin is for a LOADED boot, # # which is the number a batch verify actually produces -- # # fourteen suites sharing one guest. # # # # NOTHING HERE IS QUADRATIC IN ANYTHING. The deepest # # structure the probe builds is a three-callback ExitStack # # and the largest string it moves is a few dozen bytes. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed # # replaces this note in the test plan rather than in this # # comment, which would cost an image build to keep the # # shipped suite byte-identical. # # python3-contextlib2 21.6.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-contextlib2_21.6.0.bb, `inherit pypi # setuptools3', PyPI sdist, no patches) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # contextlib2 is the backport-and-extension of the standard library's # contextlib: ExitStack, AsyncExitStack, nullcontext, closing, aclosing, # suppress, redirect_stdout, redirect_stderr, the @contextmanager and # @asynccontextmanager decorators, the ContextDecorator base, and the # AbstractContextManager / AbstractAsyncContextManager ABCs. 21.6.0 is the # backport of CPython 3.10's contextlib. # # ============================================================================ # THE ONE THING THAT MAKES THIS PORT POSSIBLE. # ============================================================================ # # IT IS PURE PYTHON, and on this arm that is a hard precondition rather than a # nicety. 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 never had one # -- the whole package is contextlib2/__init__.py, 27653 bytes -- and the cases # below assert that negative from three independent directions: the wheel's own # RECORD manifest, the .deb's file list, and the LOADED module set at run time. # So a version bump that grew a compiled speed-up fails a case here rather than # at `import' on target. # # THE RECIPE NEEDED NO BBAPPEND. It has no target DEPENDS: setuptools3 builds # the wheel with native tooling and `installer' unpacks it, so the whole of # do_compile and do_install runs on the build host and the target sees .py # files. The `-dev' and `-dbg' packages are PKGSIZE 0 -- there is no header, no # archive and no debug info on an arm that compiles nothing -- so naming either # in IMAGE_INSTALL would fail do_rootfs with no installation candidate. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every behavioural expectation below is the OUTPUT of a control run, not a # reading of the source: the SAME probe script, over the SAME payload this # build packaged for the target, under the NATIVE CPython 3.12.13 from this # build's own sysroot. For a pure-Python package that comparison is exactly the # right claim -- there is nothing in contextlib2 that can know the pointer # width, so any difference between the arms is a defect and not a property. # Byte-identical over three runs and three PYTHONHASHSEED values. # # ============================================================================ # THE TWO THINGS MOST LIKELY TO BE MISREAD. # ============================================================================ # # THERE IS NO EVENT LOOP IN THIS SUITE, AND THAT IS DELIBERATE. contextlib2 # ships an async half and the obvious way to drive it is asyncio.run(). The # probe does not, because none of contextlib2's coroutines ever awaits anything # that suspends: a two-line trampoline that send(None)s once and reads the # StopIteration IS the complete semantics. What that buys 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. The trampoline fails loudly (`coroutine suspended') if a future # release starts suspending, so the shortcut is self-policing. # # `callback_no_args' PINS AN UPSTREAM MESSAGE WITH AN UNBALANCED PAREN -- # "Not enough positional arguments expected at least 2, got 1)". That is # contextlib2's own text, built by partitioning CPython's TypeError on "(" and # keeping the tail; the opening paren is what the partition consumed. It is # not a transcription slip here and it is not a wasm32 artifact -- the x86-64 # control produces the same bytes. # # ============================================================================ # WHAT IS DELIBERATELY NOT COVERED. # ============================================================================ # # Upstream ships test/test_contextlib.py and test/test_contextlib_async.py in # the sdist, but the recipe does not inherit ptest and neither file is # packaged, so there is no ptest case here. The suite drives the API directly # instead, which is what a consumer of this package does. P=python3-contextlib2 # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not run". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/contextlib2 DI=$SITE/contextlib2-21.6.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-contextlib2.list PROBE=$BST_DATA/python3-contextlib2-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-contextlib2.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE MODULE LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the library misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" # It is a PACKAGE, not a single module -- upstream's setup.py declares # `packages=['contextlib2']' and the whole implementation is inside # __init__.py. A release that flattened it to contextlib2.py would still import # and still pass every behavioural case, so the shape is asserted. check $P/no-flat-module test ! -f "$SITE/contextlib2.py" # The installed size is pinned rather than floored: a truncated or partially # written payload passes `test -s' and fails only here. 27653 bytes is # contextlib2/__init__.py byte for byte as the 21.6.0 sdist ships it, and it is # the whole of the implementation. check_eq $P/module-size 27653 \ "$(wc -c < "$PKG/__init__.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # THE TYPE STUB AND THE PEP 561 MARKER ARE PART OF THE PAYLOAD, and they are # what `include_package_data' in upstream's setup.py is there for. py.typed is # ZERO BYTES by design -- PEP 561 defines it as a marker whose presence is the # whole signal -- so it is checked with -f and never with -s. A suite that used # -s here would red on a correct install. check $P/type-stub test -s "$PKG/__init__.pyi" check_eq $P/type-stub-size 5181 \ "$(wc -c < "$PKG/__init__.pyi" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check $P/py-typed test -f "$PKG/py.typed" check_eq $P/py-typed-empty 0 \ "$(wc -c < "$PKG/py.typed" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # The byte-compiled copy is part of the payload and is why `import' on this # target is a read rather than a parse of 27 KB of source. Measured at 31151 # bytes in the .deb this build produced; asserted as present and non-empty # rather than pinned, since the .pyc header carries build-time state the source # does not. check $P/pycache-present test -s "$PKG/__pycache__/__init__.cpython-312.pyc" # --------------------------------------------------------- the dist-info ------ # .dist-info is what `importlib.metadata' reads, and it is the only place the # version is legible at run time -- contextlib2 defines no __version__, which # `has-dunder-version' below asserts directly. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 21.6.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: contextlib2" cat "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" # Root-Is-Purelib: true IS THE STATEMENT, from the packaging side rather than # from a file check, that this build produced a pure-Python distribution. The # TWO tags are upstream's `universal = 1' in setup.cfg: a py2/py3 universal # wheel, which this recipe's setuptools3 build preserves. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-tag-py2 "Tag: py2-none-any" cat "$DI/WHEEL" # top_level.txt names ONLY `contextlib2'. One word, and it is the shortest # statement that nothing else was packaged under this distribution's name. check_eq $P/dist-info-top-level "contextlib2" "$(cat "$DI/top_level.txt" 2>/dev/null)" # RECORD lists every file the wheel wrote, so the two cases below read the # package's OWN manifest rather than guessing at the tree. Eight lines: the # three payload files, the four .dist-info members, and RECORD's own # self-reference. Pinned, so a wheel that grew a ninth artifact is read here # rather than discovered by the no-.so case failing to mention it. check $P/dist-info-record test -s "$DI/RECORD" check_eq $P/record-entries 8 "$(grep -c . "$DI/RECORD" 2>/dev/null)" check_eq $P/record-no-so "" "$(grep '\.so' "$DI/RECORD" 2>/dev/null)" # ------------------------------------------------------------ the .deb list --- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO SHARED OBJECT ANYWHERE IN THE PACKAGE -- the same claim as `record-no-so', # against the filesystem manifest instead of the wheel's, for the case where # the two disagree. check_eq $P/pkg-no-so "" \ "$(grep -c '\.so$' "$W/pkgf" 2>/dev/null | grep -v '^0$')" # Everything shipped is Python, a type stub, the PEP 561 marker or metadata. A # file that is none of those is a packaging change nobody described. _bad="" while IFS= read -r _f; do case "$_f" in ''|/usr/lib/python3.12/site-packages) continue ;; */) continue ;; esac [ -d "$_f" ] && continue case "$_f" in *.py|*.pyc|*.pyi|*/py.typed) ;; */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE.txt) ;; */top_level.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" # BST-REDIRECT-OK: $W is under $TMPDIR, not the virtiofs root check_eq $P/pkg-only-python "" "$_bad" # THE PACKAGE SHIPS NO EXECUTABLE. contextlib2 declares no [project.scripts] # and installs nothing under /usr/bin, so unlike python3-cbor2 there is no # console-script case in this suite -- and this assertion is what says that is # a property of the package rather than a case somebody forgot to write. check_eq $P/pkg-no-bin "" "$(grep '^/usr/bin/' "$W/pkgf" 2>/dev/null)" # ------------------------------------------------------------- dpkg status ---- sed -n '/^Package: python3-contextlib2$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE DEPENDENCY SET, AND IT IS ONE PACKAGE. RDEPENDS:python3-contextlib2 out of # this build's pkgdata is exactly `python3-core'. The module imports abc, # _collections_abc, collections.deque, functools.wraps, sys, types.MethodType, # types.GenericAlias and warnings -- all inside the one interpreter image the # distro already carries -- so nothing was added to the image on its account, # and in particular NOT python3-asyncio: the async half of contextlib2 defines # coroutines but never imports a scheduler. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_eq $P/pkg-no-asyncio-dep "" "$(grep 'python3-asyncio' "$W/dep" 2>/dev/null)" # ----------------------------------------------------------------- import ----- check $P/import python3 -c 'import contextlib2' # AND IT MUST BE SILENT. A DeprecationWarning at import time, or a loader # diagnostic from a shared object that should not exist, would land on stderr # and be swallowed by every caller who only checks the exit status. check_eq $P/import-quiet "" \ "$(python3 -c 'import contextlib2' 2>&1 >/dev/null)" # ------------------------------------------------------------------ probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # 900s is half the suite budget for the one start that does all fifty-seven # measurements. On overrun bst_probe names the last line the probe printed, # which names the case it stopped at. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # One `grep' per value, split on the FIRST `=' so a value containing one is # safe. Every key the probe emits is [a-z0-9_]+. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE PROBE RAN TO THE END AND EMITTED EVERYTHING. Without these two, a probe # that died half way would report every case after the death as a mismatch # against an empty string, which reads as fifty behavioural failures instead of # one truncated transcript. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "57" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. contextlib2 21.6.0 IS a backport of CPython 3.10's # contextlib, so a suite that had accidentally measured the stdlib module would # agree with most of the behavioural expectations below. This case is what says # which file was read, and it is the one that makes the rest attributable. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/not-stdlib "ok" "$(pv not_stdlib_contextlib)" check_eq $P/dist-version "21.6.0" "$(pv dist_version)" # `__all__' IS THE PACKAGE'S OWN STATEMENT OF ITS SURFACE, pinned whole and # sorted, so a release that quietly adds or drops a public name is one diff to # read rather than a case nobody wrote. Fourteen names: the thirteen in the # main list plus the deprecated `ContextStack' the module appends afterwards. check_eq $P/all-names \ "AbstractAsyncContextManager,AbstractContextManager,AsyncExitStack,ContextDecorator,ContextStack,ExitStack,aclosing,asynccontextmanager,closing,contextmanager,nullcontext,redirect_stderr,redirect_stdout,suppress" \ "$(pv all_names)" check_eq $P/all-count "14" "$(pv all_count)" # THERE IS NO contextlib2.__version__ and there never has been. The version a # caller can read at run time is the installed distribution's, out of the # .dist-info above. Pinned as a NEGATIVE so that a future release growing the # attribute is a diff to read rather than a silent change of meaning. check_eq $P/has-dunder-version "no" "$(pv has_dunder_version)" # ------------------------------------------------- ExitStack: unwind order ---- # THE CASES THE PORT TURNS ON. An ExitStack that ran its callbacks FIFO would # still close every resource and 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. Each trace below is ONE string, so "all three ran" and # "they ran in this order" cannot be half-asserted. check_eq $P/exitstack-enter-self "yes" "$(pv exitstack_enter_self)" check_eq $P/unwind-order "c,b,a" "$(pv unwind_order)" # EVERY CALLBACK RUNS EVEN WHEN THE BLOCK RAISES, and the ORIGINAL exception is # what reaches the caller. 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 both are in one value. check_eq $P/unwind-on-raise "c,b,a|ValueError: boom" "$(pv unwind_on_raise)" # pop_all() MOVES the callbacks; it does not copy them. The failure that matters # is a pop_all() that copied -- every callback then runs twice, which for a # close() is an error and for a refund is worse. Both sides are in the value: # the original unwinds NOTHING (`'), the new stack unwinds everything, # LIFO. check_eq $P/pop-all "|b,a" "$(pv pop_all)" # push() CAN SUPPRESS AND callback() CANNOT, and that asymmetry is documented # rather than incidental: push registers something with the __exit__ signature, # whose truthy return suppresses, while callback wraps a plain callable and # discards what it returns. A port that blurred the two would silently start # eating exceptions. check_eq $P/push-suppresses "suppressed" "$(pv push_suppresses)" check_eq $P/callback-cannot-suppress "propagated: not swallowed" \ "$(pv callback_cannot_suppress)" # enter_context() DRIVES THE FULL PROTOCOL -- __enter__ in, the value it # returned handed to the caller, __exit__ out -- and two entered managers unwind # LIFO. The second is the case a caller actually depends on: # `stack.enter_context(open(...))' twice, closed in the reverse of opening. check_eq $P/enter-context "enter:one,body:42,exit:one" "$(pv enter_context)" check_eq $P/enter-context-order \ "enter:outer,enter:inner,exit:inner,exit:outer" "$(pv enter_context_order)" # A CALLBACK THAT RAISES DURING UNWIND DOES NOT ABANDON THE REST OF THE STACK. # This is what makes an ExitStack safe for teardown at all: one broken close() # must not leak every resource below it. The trace shows the two survivors # running after the raiser, and the raiser's exception reaching the caller. check_eq $P/raise-during-unwind "b,a|RuntimeError: bad-callback" \ "$(pv raise_during_unwind)" # THE __context__ CHAIN. 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 started the unwind. contextlib2 goes to real # trouble for this (_fix_exception_context), so it is measured, not assumed. check_eq $P/context-chain "RuntimeError <- ValueError" "$(pv context_chain)" # close() IS __exit__(None, None, None) BY ANOTHER NAME. A stack unwound twice # must be EMPTY the second time rather than running anything again -- hence one # `a' in the trace and not two. check_eq $P/close-then-close "a" "$(pv close_then_close)" # 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 far from here. check_eq $P/decorator-returns "callback:True push:True" "$(pv decorator_returns)" # callback() WITH NO CALLABLE. The message is contextlib2's own -- it cannot use # positional-only syntax, for the 3.6 compatibility its source explains, so it # rebuilds the TypeError text by partitioning CPython's on "(". THE UNBALANCED # PAREN IS UPSTREAM'S and reproduces byte-for-byte on the x86-64 control; see # the header. check_eq $P/callback-no-args \ "TypeError: Not enough positional arguments expected at least 2, got 1)" \ "$(pv callback_no_args)" # ------------------------------------------------------------ nullcontext ---- # The stand-in: it must 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. check_eq $P/nullcontext-yields "payload" "$(pv nullcontext_yields)" check_eq $P/nullcontext-default "None" "$(pv nullcontext_default)" check_eq $P/nullcontext-reraises "KeyError: 'k'" "$(pv nullcontext_reraises)" # 21.6.0 gave nullcontext __aenter__/__aexit__, so the SAME stand-in serves an # `async with'. Driven through the probe's trampoline; see the header. check_eq $P/nullcontext-async "async-payload" "$(pv 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. check_eq $P/closing-once "closes:1 same:True" "$(pv closing_once)" check_eq $P/closing-on-raise "closes:1" "$(pv closing_on_raise)" # aclosing is the same promise over aclose(), added in 21.6.0 for async # generators. check_eq $P/aclosing-once "closes:1 same:True" "$(pv aclosing_once)" # --------------------------------------------------------------- suppress ---- # ONLY the named types, and their SUBCLASSES. The four arms each catch a # different mistake: a suppress that swallowed everything, one that swallowed # nothing, one that compared types by identity rather than by issubclass, and # one that treated an empty type list as a wildcard. The subclass arm is the # subtle one -- `suppress(LookupError)' must swallow a KeyError -- and it is # what a reimplementation is most likely to get wrong. check_eq $P/suppress-named "suppressed" "$(pv suppress_named)" check_eq $P/suppress-other "propagated: ValueError: v" "$(pv suppress_other)" check_eq $P/suppress-subclass "suppressed" "$(pv suppress_subclass)" check_eq $P/suppress-empty "propagated" "$(pv suppress_empty)" # suppress has no payload: `with suppress(E) as x' binds None, and binding # anything else would be a change of surface. check_eq $P/suppress-enter-none "None" "$(pv suppress_enter_none)" # --------------------------------------------------------------- redirect ---- # THE RE-ENTRANCY IS THE CASE THAT MATTERS. 21.6.0 saves the displaced stream 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 dead buffer -- which on this image would take the # guest's console with it. Restoration is asserted by identity against the # object the probe saved before it started. check_eq $P/redirect-stdout "captured:captured restored:True" \ "$(pv redirect_stdout)" check_eq $P/redirect-stderr "captured:err-captured restored:True" \ "$(pv redirect_stderr)" check_eq $P/redirect-reentrant "outer:one,three inner:two restored:True" \ "$(pv redirect_reentrant)" # -------------------------------------------------------- ContextDecorator --- # THE SAME OBJECT AS A DECORATOR AND AS A `with' BLOCK, which is the ticket's # wording and 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. The trace is one instance used # three times -- decorated call, with-block, decorated call again. check_eq $P/contextdecorator-both "in,work,out,in,block,out,in,work,out" \ "$(pv contextdecorator_both)" # @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". check_eq $P/contextmanager-decorator "in,work,out,in,work,out" \ "$(pv contextmanager_decorator)" # @contextmanager's TWO ERROR PATHS, both RuntimeError with messages that are # part of the surface -- these are what a caller meets when they write the # generator wrong, so the wording is pinned and not just the type. check_eq $P/cm-no-yield "RuntimeError: generator didn't yield" \ "$(pv cm_no_yield)" check_eq $P/cm-two-yields "RuntimeError: generator didn't stop" \ "$(pv cm_two_yields)" # The generator SEES the exception at its yield, which is what makes try/finally # inside it work, and may suppress by swallowing it. check_eq $P/cm-sees-exception "caught:inner,finally" "$(pv cm_sees_exception)" # ------------------------------------------------------------------ ABCs ----- # 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. A hook that # regressed to nominal checking would break every duck-typed caller while # passing every case above, so both the positive and the NEGATIVE are asserted. check_eq $P/abc-sync-duck "yes" "$(pv abc_sync_duck)" check_eq $P/abc-sync-rejects "yes" "$(pv abc_sync_rejects)" check_eq $P/abc-async-duck "yes" "$(pv abc_async_duck)" check_eq $P/abc-exitstack "yes" "$(pv abc_exitstack)" check_eq $P/abc-asyncexitstack "yes" "$(pv abc_asyncexitstack)" # The ABCs are SUBSCRIPTABLE -- __class_getitem__ returns a GenericAlias, so # `AbstractContextManager[int]' is legal in an annotation evaluated at run time. # A release that dropped it would break any caller not using # `from __future__ import annotations'. check_eq $P/abc-subscriptable "GenericAlias" "$(pv abc_subscriptable)" # ----------------------------------------------------------- AsyncExitStack -- # A SEPARATE IMPLEMENTATION, not a wrapper, so LIFO order and # run-everything-on-raise are RE-MEASURED rather than inferred from the sync # stack. `async-mixed-order' is the case only this class can get wrong: sync and # async callbacks on ONE stack, unwound LIFO across both kinds, which is what a # caller writing `async with AsyncExitStack()' actually hits. check_eq $P/async-unwind-order "c,b,a" "$(pv async_unwind_order)" check_eq $P/async-mixed-order "sync-c,async-b,sync-a" "$(pv async_mixed_order)" check_eq $P/async-enter-context "aenter,body:payload,aexit" \ "$(pv async_enter_context)" check_eq $P/async-unwind-on-raise "b,a|ValueError: async-boom" \ "$(pv async_unwind_on_raise)" check_eq $P/async-pop-all "|a" "$(pv async_pop_all)" check_eq $P/asynccontextmanager "in,body:v,out" "$(pv asynccontextmanager)" # -------------------------------------------------- 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. # # ONE warning, not three: only ContextStack.__init__ warns. `register' and # `register_exit' are silent forwarders to callback/push, which is why the # value names a single DeprecationWarning after a two-callback unwind. check_eq $P/contextstack-alias \ "unwound:b,a warned:DeprecationWarning subclass:True" \ "$(pv contextstack_alias)" check_eq $P/contextstack-preserve "|a" "$(pv contextstack_preserve)" check_eq $P/refresh-cm "same:True warned:DeprecationWarning" "$(pv refresh_cm)" # ------------------------------------------------------ the platform negative - # NOTHING THE WHEEL INSTALLED LOADED AS A SHARED OBJECT, asserted against the # LOADED module set at run time -- the thing that would actually fail on this # image, rather than against the tree. It is SCOPED to site-packages on purpose: # an unscoped sweep would also name the interpreter's own extension modules, # which on the x86-64 control are fourteen .so files and on this static target are # zero, so the unscoped form would measure how the interpreter was linked and # would differ between the arms for a reason that has nothing to do with this # package. check_eq $P/no-ext-loaded "" "$(pv no_ext_loaded_from_sitepkg)" # The interpreter that answered. It is the same 3.12.13 the control ran under, # which is what makes every comparison above a statement about the ARM rather # than about two different Pythons. check_eq $P/interpreter-version "3.12.13" "$(pv interpreter_version)" rm -rf "$W"