. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the whole cost. Every # # other case below reads the filesystem or dpkg with shell # # builtins, or greps the output of the second start. # # # # The two are: a bare `import' (one case, so that an import # # failure is legible without reading a transcript) and the # # probe, which takes ONE start for two hundred-odd # # measurements. The work inside is dict operations on # # mappings of at most six items -- microseconds beside an # # interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # NOTHING HERE SLEEPS and nothing here starts a thread, so # # there is no wait to bound and no join to time out. # # # # 1200 is 600s per start, ten times the ~60s a python3 # # start has measured at here and the same per-start budget # # 99-python3-cached-property gives its two. # # # # A suite-timeout red is a batch-wide cost, so if the two # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # 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-bidict 0.23.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-bidict_0.23.0.bb, `inherit pypi # python_setuptools_build_meta', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHAT THAT MAKES THE SUITE ASSERT. # ============================================================================ # # A `bidict' is a one-to-one mapping that exposes the reverse of itself as a # LIVE view. So the forward lookups are the least interesting thing here -- a # plain dict satisfies those. What this suite is actually about is three claims, # and each of them is invisible from a forward read: # # * THE INVERSE IS A VIEW, NOT A COPY. Every `live-*' case below takes the # `inverse' reference FIRST and mutates the forward mapping AFTERWARDS, then # reads the reference it already held. A bidict that rebuilt its inverse on # each access would pass a test that re-read `b.inverse' and fail these. # # * UNIQUENESS IS ENFORCED ON BOTH SIDES, and WHICH exception arrives says # which side was violated. The three `*DuplicationError' cases are asserted # by exception type name, and the hierarchy between them is asserted # separately -- see THE EXCEPTION HIERARCHY IS A TRAP below. # # * `putall' IS ATOMIC. That claim has THREE distinct implementations behind # it, and a test that exercised one would say nothing about the other two. # See THE THREE UPDATE PATHS below. # # ============================================================================ # THE ASYMMETRY THAT MATTERS MOST: `__setitem__' IS NOT `put'. # ============================================================================ # # `bidict.on_dup' is `OnDup(key=DROP_OLD, val=RAISE)', while `put' defaults to # `ON_DUP_RAISE'. So the two entry points disagree on a duplicate KEY: # # b['He'] = 'helium-3' -> REPLACES, exactly as dict does # b.put('He', 'helium-3') -> KeyDuplicationError # # and agree on a duplicate VALUE, which both refuse -- because silently # reassigning a value would drop the key that currently owns it, and that is the # one thing a one-to-one mapping cannot do quietly. # # `setitem-dup-key-replaces' and `put-dup-key-error' are that asymmetry, and # they are asserted as a pair because either one alone reads like the whole rule. # # ============================================================================ # THE EXCEPTION HIERARCHY IS A TRAP, AND IT IS ASSERTED AS ONE. # ============================================================================ # # `KeyAndValueDuplicationError' inherits from BOTH `KeyDuplicationError' and # `ValueDuplicationError'. So a caller who writes # # except KeyDuplicationError: ... # except KeyAndValueDuplicationError: ... # # never reaches the second handler -- the first catches the both-duplicated case # too. `kvde-is-kde' and `kvde-is-vde' pin that, and they are pinned rather than # left to the type names above because a release that flattened the hierarchy # would keep every behavioural case green and silently change which handler runs # in every consumer. # # One more asymmetry inside it: with key AND value each duplicating a DIFFERENT # existing item, `_dedup' consults `on_dup.val' ONLY. So `OnDup(key=RAISE, # val=DROP_OLD)' does NOT raise on that case -- `mixed-dup-both-error' is `none' # and two items collapse into one. Documented on `OnDup', and exactly the kind of # thing a reader assumes the other way round. # # ============================================================================ # THE THREE UPDATE PATHS, AND WHY ATOMICITY IS ASSERTED THREE TIMES. # ============================================================================ # # `BidictBase._update' dispatches on the SHAPE of its argument, and rollback # reaches the caller by a different mechanism in each case: # # 1. `len(arg) > len(self)' and rollback on -> update a COPY, then become it. # The original is never touched, so atomicity is free. # 2. `len(arg) <= len(self)' -> write item by item, accumulating UNWRITES, and # replay them in reverse on a DuplicationError. THIS is the path that can # actually leave a half-applied mapping. # 3. `arg' is a generator, hence not `Sized' -> path 2, because the `len(arg)' # test in path 1 requires `isinstance(arg, Sized)'. This is the path a # caller streaming items takes without knowing it. # # `putall-big-*', `putall-small-*' and `putall-gen-*' are those three, and each # asserts the mapping unchanged FORWARD AND INVERSE -- an unwrite that repaired # only `_fwdm' would leave the two disagreeing and every forward assertion in # this file would still pass. # # `putall-drop-old-*' is the other half of the contract: with # `on_dup=ON_DUP_DROP_OLD' nothing raises, so `rollback' -- which defaults to # `RAISE in on_dup' -- is OFF and the batch applies. That is the documented # behaviour, not a defect, and it is asserted so that reading `putall' as # unconditionally atomic is caught here rather than in a consumer. # # ============================================================================ # WHAT THIS TICKET ASKED FOR AND 0.23.0 DOES NOT HAVE. # ============================================================================ # # BRINTOS-2060 asks for `namedbidict' to be "asserted to expose the named # accessors it generates". THERE IS NO `namedbidict' IN 0.23.0. It was # deprecated in 0.21.3 and REMOVED in this release; `bidict/__init__.py' does not # import it and no module defines it. # # `no-namedbidict' asserts its ABSENCE, which is the only honest form the # request can take. A suite that simply omitted the factory would leave a later # reader unable to tell whether it was overlooked or is gone -- and if a future # release reinstates it, this case goes red and says so. # # ============================================================================ # WHERE THE EXPECTED VALUES COME FROM. # ============================================================================ # # MEASURED ON AN x86-64 CONTROL AT THE SAME VERSION, not read off the source. # The control ran THIS SUITE'S OWN PROBE against THIS BUILD's own .deb payload # (`python3-bidict_0.23.0-r0_wasm32.deb', unpacked with `dpkg-deb -x') under # CPython 3.12 -- the same interpreter minor this image ships. It was then re-run # against the PyPI sdist the recipe fetches (sha256 3959ca59d4d6...c468578) and # produced a BYTE-IDENTICAL transcript apart from the module's path: the wheel # install alters nothing. The packaging cases below could not be measured on the # control and come from that .deb directly (`dpkg-deb -c' and `dpkg-deb -I'). # # Four control results are worth naming because they are not what the source or # this ticket reads like: # # * `values()' RETURNS A `dict_keys', NOT A `ValuesView'. The values of a # bidict ARE the keys of its inverse, so `values()' hands back the inverse's # key view -- which is set-like and has O(1) containment, neither of which a # dict's `values()' offers. `values-view-type' pins the concrete type. # # * `==' IS ORDER-INSENSITIVE EVEN FOR `OrderedBidict', because `__eq__' is # inherited from `BidictBase' and compares items as a set. So two # OrderedBidicts in different orders ARE equal, and a test that used `==' # to check ordering would pass while asserting nothing. # `equals_order_sensitive' is the opt-in, and the two are pinned as a pair. # # * A `frozenbidict' HAS NO `__setitem__' AT ALL -- not one that raises. So # `fb['B'] = x' is a TypeError from the protocol while `fb.__delitem__(k)' is # an AttributeError from the lookup, and the two are pinned separately # because which one a consumer sees depends on how it wrote the mutation. # # * `forceput' ON A KEY AND VALUE THAT DUPLICATE DIFFERENT ITEMS LOSES AN # ITEM. `He'->`helium' and `Li'->`lithium' become the single `He'->`lithium' # and `len' falls from 4 to 3. `force-dup-both-len' is that, and it is the # case a caller reaching for `forceput' to "just make it fit" needs to see. # # ============================================================================ # THE DECLARED DEPENDENCY IS SHORT, AND HERE IT IS ALSO COMPLETE. # ============================================================================ # # The .deb's control stanza reads `Depends: python3-core' and nothing else -- # `pyproject.toml' declares no runtime dependency and the wheel's METADATA # carries ZERO `Requires-Dist' lines. # # On this image python3 is split into sub-packages (BRINTOS-1476), so a short # `Depends' is usually an omission waiting to be found -- it is what # `99-python3-cached-property' pins as `dep-no-threading'. NOT HERE. bidict's # unguarded top-level imports are `typing', `enum', `weakref', `functools', # `itertools', `operator', `types', `abc', `collections.abc' and `contextlib', # and EVERY ONE of them is in `python3-core' on this image -- verified against # the sub-package .debs of this build. So the declared dependency set is # genuinely sufficient, and `dep-line-exact' pins it as such. # # `python3-pickle' is a separate matter: it is THE PROBE's dependency, not # bidict's, needed only for the round-trip cases. The probe guards that import # and `dep-pickle-installed' below is the fact that carries it, so an image that # dropped the sub-package reds one case with a reason rather than truncating the # transcript. P=python3-bidict # ============================================================================ # The suite proper. # ============================================================================ # # `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/bidict DI=$SITE/bidict-0.23.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-bidict.list PROBE=$BST_DATA/python3-bidict-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-bidict.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- payload --- # 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 package misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/is-a-package test -d "$PKG" check $P/init-present test -s "$PKG/__init__.py" # IT IS A PACKAGE, NOT A TOP-LEVEL MODULE. `pyproject.toml' declares a package # rather than a `py-modules' list, so the payload is a directory of twelve # sources. Asserted BOTH ways -- the directory is there and the single-file form # is not -- because a release that collapsed to one module would satisfy # `import' and every behavioural case below while relocating the whole payload. check $P/not-a-single-module test ! -e "$SITE/bidict.py" # THE TWELVE SOURCES, BY NAME. A count alone would pass a release that renamed a # module, and the names are what the `__init__' imports from -- so this is the # list `import bidict' actually depends on. # # BOTH SIDES GO THROUGH THE SAME `sort', deliberately. `sort' does not agree with # itself across implementations on these names: glibc's collation ignores the # leading underscores and puts `_abc.py' first with `metadata.py' in the middle, # while busybox's is byte order and puts `__init__.py' first. Neither is wrong, # and comparing either against a hardcoded order would red on the other libc's # tooling while the payload was identical. Sorting the expected list too makes # this a claim about the SET, which is what it was always meant to be. _want="__init__.py _abc.py _base.py _bidict.py _dup.py _exc.py _frozen.py _iter.py _orderedbase.py _orderedbidict.py _typing.py metadata.py" _got="$(cd "$PKG" 2>/dev/null && ls *.py 2>/dev/null | sort | tr '\n' ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check_eq $P/sources \ "$(printf '%s\n' $_want | sort | tr '\n' ' ')" "$_got" # BST-REDIRECT-OK: forked -- inside $( ) # TWO SIZES ARE PINNED RATHER THAN FLOORED: a truncated or partially written # payload passes `test -s' and fails only here. Both are the sdist's own byte # counts and are what the wheel's RECORD declares. `_base.py' is pinned because # it is the implementation -- `_dedup', `_write' and `_update', i.e. everything # the uniqueness and atomicity sections below assert. check_eq $P/init-size 4398 \ "$(wc -c < "$PKG/__init__.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check_eq $P/base-size 24618 \ "$(wc -c < "$PKG/_base.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # THE PEP 561 MARKER SHIPS. `py.typed' is what makes this package typed for its # consumers; a release that dropped it would keep every runtime case here green # and silently stop type-checking in every downstream project. Pinned by content # as well as presence, because an empty marker is the shape a bad install # produces and `test -s' would not notice a wrong one. check $P/py-typed-present test -s "$PKG/py.typed" check_out $P/py-typed-content "PEP-561 marker." cat "$PKG/py.typed" # NO COMPILED EXTENSION SHIPS, and this is the case that would notice a release # that grew one. On this image a `.so' in site-packages is not a speed-up, it is # an ImportError: python3 here is one static binary with an empty lib-dynload # (BRINTOS-1476). Asserted over the payload rather than over the recipe. check_eq $P/no-so "" "$(find "$PKG" -name '*.so' 2>/dev/null)" # THE BYTE-COMPILED COPIES are part of the payload and are why `import bidict' # on this target is a dozen reads rather than a parse of 62 KB of source. They # land in the PACKAGE's own `__pycache__', which is what a package does -- # contrast `99-python3-cached-property', whose single module byte-compiles into # site-packages' own. Pinned by path for that reason, and counted because ALL # TWELVE sources are compiled at install time, `__init__' included. (Twelve, not # the eleven `submodule-count' reports: that case counts SUBmodules and so # excludes the top-level `bidict' itself.) check $P/pycache-dir test -d "$PKG/__pycache__" check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-base test -s "$PKG/__pycache__/_base.cpython-312.pyc" check_eq $P/pycache-count "12" \ "$(ls "$PKG/__pycache__"/*.cpython-312.pyc 2>/dev/null | wc -l | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # NO EXECUTABLE. `pyproject.toml' declares no `[project.scripts]', so nothing # lands in /usr/bin. Asserted so a future release that started shipping one is # noticed rather than silently unexercised. check $P/no-bin-script test ! -e /usr/bin/bidict # --------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-wheel test -f "$DI/WHEEL" check_out $P/dist-info-version "Version: 0.23.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: bidict" cat "$DI/METADATA" check_out $P/dist-info-license "License: MPL 2.0" cat "$DI/METADATA" # THE LICENCE LANDS AT THE ROOT of dist-info -- not under `licenses/', which is # where hatchling puts it and where `99-python3-annotated-types' pins its own. # This is setuptools' layout, and it is where the recipe's LIC_FILES_CHKSUM md5 # ultimately points. check $P/license-file test -s "$DI/LICENSE" check_out $P/license-file-line "License-File: LICENSE" cat "$DI/METADATA" # `top_level.txt' NAMES THE PACKAGE, and here the distribution name and the # import name COINCIDE -- unlike most ports on this image, where the two differ # by a hyphen. This file is the only place in the payload that states the # mapping, so it is pinned even when it is uneventful. check_out $P/top-level "bidict" cat "$DI/top_level.txt" # NO `INSTALLER' FILE. setuptools' own wheel-install path writes one and pip # writes another; this payload has neither, which says the install came through # the recipe's `python_setuptools_build_meta' path rather than through pip. check $P/no-installer test ! -e "$DI/INSTALLER" # PURELIB, STATED BY THE PACKAGING ITSELF, and the wheel is NOT universal: one # `py3-none-any' tag and no `py2' tag. Pinned as a single tag because a release # that re-added py2 support -- or that grew a platform tag, which is what an # ext_module would produce -- changes this line and nothing else. check_out $P/wheel-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/wheel-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/wheel-generator "Generator: bdist_wheel (0.42.0)" cat "$DI/WHEEL" check_eq $P/wheel-one-tag "1" \ "$(grep -c '^Tag:' "$DI/WHEEL" 2>/dev/null)" # BST-REDIRECT-OK: forked -- inside $( ) # NOTHING IS REQUIRED AT RUNTIME, and the METADATA says so by saying nothing: # `pyproject.toml' declares no runtime dependency, so there is not one # `Requires-Dist' line. Asserted as a COUNT of zero rather than as an absent # string, because a `check_out' cannot express "no line matches". # # This is the wheel's half of the dependency pair; `dep-line-exact' below is the # .deb's. See THE DECLARED DEPENDENCY IS SHORT in the header. check_eq $P/meta-no-requires "0" \ "$(grep -c '^Requires-Dist' "$DI/METADATA" 2>/dev/null)" # BST-REDIRECT-OK: forked -- inside $( ) # THE FLOOR IS 3.8, WHICH THIS IMAGE CLEARS. Pinned because it is the one # declaration in the payload that could refuse this interpreter, and a release # that raised it above 3.12 would install and then fail at import with a message # about neither. check_out $P/meta-requires-python "Requires-Python: >=3.8" cat "$DI/METADATA" # ------------------------------------------------------------ the .deb file --- # The installed file list, read once into $W. The copy is what the loop below # redirects from: a stdin redirect the top-level shell performs on the virtiofs # root makes dup2(2) return 513 and ENDS the suite silently (brintos/linux#73), # and $W is on tmpfs. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # Every payload path is under site-packages and is source, bytecode, the typing # marker or metadata. Anything else means the build produced something this port # did not expect and did not measure. _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) ;; */py.typed) ;; */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE|*/LICENSE.*) ;; */top_level.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-bidict$/,/^$/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" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" check_out $P/pkg-version "Version: 0.23.0-r0" cat "$W/st" # THE DEPENDENCY LINE, PINNED EXACTLY. See THE DECLARED DEPENDENCY IS SHORT in # the header: `python3-core' alone is not an omission here, it is sufficient -- # every module bidict imports unguarded lives in that sub-package on this image. # Asserted as the WHOLE line rather than as a substring, so that a later recipe # which added or dropped a dependency is a visible change rather than a silent # one. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/dep-line test -s "$W/dep" check_out $P/dep-line-exact "Depends: python3-core" cat "$W/dep" # AND THE SUB-PACKAGE THE PROBE NEEDS. `pickle' is not bidict's dependency; it # is this suite's, for the round-trip cases. The probe guards the import, so if # this case is red the three `pickle-*' cases below read `no-pickle' and say why # instead of the probe ending mid-transcript. sed -n '/^Package: python3-pickle$/,/^$/p' /var/lib/dpkg/status > "$W/pk" 2>/dev/null check_out $P/dep-pickle-installed "Status: install ok installed" cat "$W/pk" # ------------------------------------------------------------------ import --- # ONE CASE OF ITS OWN, and the first of two python3 starts. Pure Python, so this # is expected to be uneventful -- it is here so that an import failure is legible # without reading a probe transcript. check $P/import python3 -c 'import bidict' # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" exit 0 fi bst_ok $P/probe-present # THE SECOND START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so a hung probe is reported as a probe overrun with the # last line it printed, rather than as a suite timeout that says nothing about # where it stopped. # # The probe EXITS 0 even when the import fails, so this case passing means "the # probe ran", not "the package works" -- `import-ok' below is what carries that. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' because # a key is emitted once and a duplicate would otherwise concatenate; `cut -f2-' # because a value may legitimately contain `='. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The probe reached its end. Every case below reads a value it printed, so a # probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" _imp=$(pv import_ok) check_eq $P/import-ok "yes" "$_imp" if [ "$_imp" != "yes" ]; then # The package did not import. Skip -- not red -- every behavioural case, each # with the reason attached: they are all one fact, and two hundred reds would # bury it rather than report it. `import' and `import-ok' above are the two # reds that carry the verdict, and the design doc attributes them. _why="bidict did not import: $(pv import_error)" for _c in probe-count package-file is-a-package-import submodule-count \ version-attr author-name author-email author-is-dict \ license-attr url-attr description-attr dist-version \ version-agrees has-all public-names bidict-module frozen-module \ no-namedbidict \ len fwd-H fwd-Be fwd-missing contains-H contains-value \ keys values items repr eq-dict get-default \ values-view-type values-set-like values-isdisjoint \ inv-lookup inv-len inv-is-cached inv-inv-is-original \ inv-alias inv-class inv-is-bidict inv-is-generated \ live-inv-after-fwd-write live-inv-len-after-fwd-write \ live-fwd-after-inv-write live-inv-boron-present \ live-inv-len-after-delete live-fwd-carbon-present \ back-to-fixture inv-shares-fwdm inv-shares-invm \ inverted-items inverted-plain-list has-dunder-inverted \ on-dup-key on-dup-val on-dup-default-is \ setitem-dup-val-error setitem-dup-val-unchanged \ setitem-dup-key-replaces setitem-dup-key-old-val-present \ setitem-dup-key-len setitem-same-item-noop-len \ put-dup-key-error put-dup-val-error put-dup-both-error \ put-unchanged put-same-item-error put-same-item-len \ put-new-len put-new-inv \ kvde-is-kde kvde-is-vde kde-is-dup-error \ dup-error-is-bidict-error bidict-error-is-exception \ vde-args kde-args \ force-is-drop-old force-dup-val-fwd force-dup-val-old-key-present \ force-dup-val-len force-dup-val-inv \ force-dup-key-fwd force-dup-key-len \ force-dup-key-old-val-present \ force-dup-both-fwd force-dup-both-len force-dup-both-li-present \ force-dup-both-helium-present \ drop-new-len drop-new-d-present drop-new-incumbent \ mixed-dup-key-error mixed-dup-val-ok mixed-dup-val-len \ mixed-dup-both-error mixed-dup-both-len \ forceupdate-len forceupdate-items \ putall-big-error putall-big-unchanged putall-big-len \ putall-small-error putall-small-unchanged putall-small-len \ putall-gen-error putall-gen-unchanged putall-gen-len \ putall-internal-dup-error putall-internal-dup-unchanged \ putall-ok-len putall-ok-inv \ putall-drop-old-len putall-drop-old-d putall-drop-old-he-present \ update-dup-val-error update-dup-val-unchanged update-dup-key-ok \ pop-value pop-inv-present pop-missing-error pop-missing-default \ popitem-pair popitem-len clear-len clear-inv-len \ popitem-empty-error \ copy-independent copy-eq-after copy-class \ or-len or-class ior-len ror-len \ pickle-available pickle-eq pickle-inv-works pickle-inv-eq \ reversed-keys \ is-bidirectional is-mutable-bidirectional \ frozen-is-bidirectional frozen-is-mutable abc-abstract-error \ frozen-len frozen-fwd frozen-inv frozen-hashable \ frozen-hash-is-items frozen-hash-order-free frozen-hash-cached \ frozen-hash-stable frozen-as-dict-key \ frozen-setitem-error frozen-has-setitem frozen-has-delitem \ frozen-has-update frozen-has-clear frozen-has-put \ frozen-has-forceput frozen-store-error frozen-del-error \ frozen-unchanged frozen-inv-class frozen-inv-hashable \ bidict-hashable frozen-eq-bidict \ ord-keys ord-values ord-items ord-inv-keys ord-inv-items \ ord-reversed ord-reversed-items ord-class ord-inv-class \ ord-after-append ord-inv-after-append \ ord-after-replace ord-inv-after-replace \ ord-move-last ord-move-first ord-move-missing-error \ ord-popitem-last ord-popitem-first ord-after-popitems \ ord-popitem-empty-error \ ord-eq-order-insensitive ord-eq-order-sensitive \ ord-eq-order-sensitive-self ord-eq-plain-dict \ ord-keys-view-type ord-items-view-type ord-keys-set-op \ ord-keys-eq ord-clear-len ord-after-clear-keys \ ondup-is-tuple ondup-fields ondup-default-repr ondup-raise-repr \ ondup-drop-old-repr ondup-action-names ondup-raise-is-singleton \ ondup-raise-in-default ondup-raise-in-drop-old \ ondup-bare-key ondup-bare-val \ py-version py-maxsize-bits; do bst_skip $P/$_c "$_why" done exit 0 fi # The probe emitted every key it intended to. A probe that returned 0 having # skipped a section would otherwise be indistinguishable from one that ran whole. check_eq $P/probe-count "213" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. The path is the target's, not the control's, so it is # asserted directly rather than compared. check_eq $P/package-file "$PKG/__init__.py" "$(pv package_file)" # A PACKAGE, NOT A MODULE -- the payload half of this is `is-a-package' above; # this is the same claim as the interpreter sees it (`bidict.__path__' exists). check_eq $P/is-a-package-import "yes" "$(pv is_package)" # ELEVEN SUBMODULES ARE IMPORTED BY `import bidict', which is every source but # `__init__' itself. So the import is not lazy and the whole package is resident # after it -- worth pinning on an arm where each of those is a filesystem read. check_eq $P/submodule-count "11" "$(pv submodule_count)" # THE METADATA DUNDERS, which live in `bidict/metadata.py' and are re-exported. # `__author__' IS A DICT, not a string -- unusual, and the kind of thing a # consumer formatting it into a banner would trip over. Asserted as the type # plus its two fields, so a release that flattened it to a string is one legible # red rather than three confusing ones. check_eq $P/version-attr "0.23.0" "$(pv version_attr)" check_eq $P/author-name "Joshua Bronson" "$(pv author_name)" check_eq $P/author-email "jabronson@gmail.com" "$(pv author_email)" check_eq $P/author-is-dict "yes" "$(pv author_is_dict)" check_eq $P/license-attr "MPL 2.0" "$(pv license_attr)" check_eq $P/url-attr "https://bidict.readthedocs.io" "$(pv url_attr)" check_eq $P/description-attr "The bidirectional mapping library for Python." \ "$(pv description_attr)" # THE INSTALLED DISTRIBUTION'S OWN IDEA OF THE VERSION, read through # `importlib.metadata' rather than from the source. `version-agrees' is the case # that says the wheel was cut from the source this package is -- and it is the # one case here that exercises the dist-info the packaging section above only # read as files. check_eq $P/dist-version "0.23.0" "$(pv dist_version)" check_eq $P/version-agrees "yes" "$(pv version_agrees)" # THERE IS NO `__all__'. The package re-exports with the `from ._x import Y as Y' # idiom instead, which makes the names public to a type checker without an # explicit list. Both halves pinned -- the absence, and the surface it leaves -- # because the surface is what `from bidict import *' gets and it is not # obviously the same set. check_eq $P/has-all "no" "$(pv has_all)" check_eq $P/public-names \ "BidictBase,BidictException,BidictKeysView,BidirectionalMapping,DROP_NEW,DROP_OLD,DuplicationError,GeneratedBidictInverse,KeyAndValueDuplicationError,KeyDuplicationError,MutableBidict,MutableBidirectionalMapping,ON_DUP_DEFAULT,ON_DUP_DROP_OLD,ON_DUP_RAISE,OnDup,OnDupAction,OrderedBidict,OrderedBidictBase,RAISE,ValueDuplicationError,bidict,frozenbidict,inverted,metadata" \ "$(pv public_names)" # `__module__' IS REWRITTEN TO `bidict' by the loop at the foot of # `__init__.py', so `bidict.bidict' reprs as `bidict.bidict' rather than # `bidict._bidict.bidict'. Pinned because it is a deliberate act of that loop # rather than a default, and because pickling relies on the name resolving. check_eq $P/bidict-module "bidict" "$(pv bidict_module)" check_eq $P/frozen-module "bidict" "$(pv frozen_module)" # WHAT THIS TICKET ASKED FOR AND THIS RELEASE DOES NOT HAVE. See WHAT THIS # TICKET ASKED FOR in the header: `namedbidict' was removed in 0.23.0, and this # case asserts the absence so that a reinstatement is a visible red rather than # a silent gap in the suite. check_eq $P/no-namedbidict "no" "$(pv has_namedbidict)" # --------------------------------------------------- the forward direction --- # The fixture is four element-symbol pairs, and every literal below is derived # from it. These cases are the least interesting in the file -- a plain dict # satisfies all of them -- and they are here so that a failure in the inverse # sections can be read as being about the inverse. check_eq $P/len "4" "$(pv len)" check_eq $P/fwd-H "hydrogen" "$(pv fwd_H)" check_eq $P/fwd-Be "beryllium" "$(pv fwd_Be)" check_eq $P/fwd-missing "KeyError" "$(pv fwd_missing)" check_eq $P/contains-H "yes" "$(pv contains_H)" # `in' CHECKS KEYS ONLY, both ways round -- `'hydrogen' in b' is FALSE even # though the value is there. Pinned because a one-to-one mapping invites the # assumption that containment is symmetric, and the inverse is where to ask. check_eq $P/contains-value "no" "$(pv contains_hydrogen)" check_eq $P/keys "H,He,Li,Be" "$(pv keys)" check_eq $P/values "hydrogen,helium,lithium,beryllium" "$(pv values)" check_eq $P/items "H:hydrogen,He:helium,Li:lithium,Be:beryllium" \ "$(pv items)" check_eq $P/eq-dict "yes" "$(pv eq_dict)" check_eq $P/get-default "absent" "$(pv get_default)" # THE REPR RENDERS THE ITEMS, so it carries no address and is stable across # runs. That is what makes it assertable at all -- and it is asserted because a # repr is what a consumer logs. check_eq $P/repr \ "bidict({'H': 'hydrogen', 'He': 'helium', 'Li': 'lithium', 'Be': 'beryllium'})" \ "$(pv repr)" # `values()' IS A KEYS VIEW, not a `ValuesView' -- because the values of a # bidict ARE the keys of its inverse. So it is set-like and has O(1) # containment, neither of which a dict's `values()' offers. The concrete type is # the assertion; the two set operations are the consequence a consumer would # actually use. check_eq $P/values-view-type "dict_keys" "$(pv values_view_type)" check_eq $P/values-set-like "yes" "$(pv values_is_set_like)" check_eq $P/values-isdisjoint "yes" "$(pv values_isdisjoint)" # ---------------------------------------------- the inverse is a LIVE view --- # EVERY CASE HERE TOOK THE REFERENCE FIRST AND MUTATED AFTERWARDS. See THE # INVERSE IS A VIEW in the header for why that ordering is the whole assertion. check_eq $P/inv-lookup "H" "$(pv inv_hydrogen)" check_eq $P/inv-len "4" "$(pv inv_len)" # THE INVERSE INSTANCE IS CACHED, and `b.inverse.inverse' IS `b' ITSELF -- not # an equal copy. That identity is held by a weakref in one direction to avoid a # reference cycle, so a release that got the caching wrong would return a fresh # object each time and every `live-*' case below would fail with it. check_eq $P/inv-is-cached "yes" "$(pv inv_is_cached)" check_eq $P/inv-inv-is-original "yes" "$(pv inv_inv_is_original)" check_eq $P/inv-alias "yes" "$(pv inv_alias_is_inverse)" # THE INVERSE OF A `bidict' IS A `bidict' -- the class is its own inverse class, # because its backing forward and inverse mappings are the same type. A subclass # whose backings differ gets a dynamically generated class instead, and # `inv-is-generated' is the case that says this one did not. check_eq $P/inv-class "bidict" "$(pv inv_class)" check_eq $P/inv-is-bidict "yes" "$(pv inv_is_bidict)" check_eq $P/inv-is-generated "no" "$(pv inv_is_generated)" # THE LIVE-VIEW ASSERTION PROPER, in both directions and for both a write and a # delete. The delete is the operation that would survive a copy-based inverse # for longest: the item is gone from one side and still present on the other, # which no equality check on the forward mapping can see. check_eq $P/live-inv-after-fwd-write "B" "$(pv live_inv_after_fwd_write)" check_eq $P/live-inv-len-after-fwd-write "5" "$(pv live_inv_len_after_fwd_write)" check_eq $P/live-fwd-after-inv-write "carbon" "$(pv live_fwd_after_inv_write)" check_eq $P/live-inv-boron-present "no" "$(pv live_inv_boron_present)" check_eq $P/live-inv-len-after-delete "5" "$(pv live_inv_len_after_delete)" check_eq $P/live-fwd-carbon-present "no" "$(pv live_fwd_carbon_present)" # AND THE MAPPING IS BACK WHERE IT STARTED, which is what says the writes and # deletes above were paired rather than merely individually visible. check_eq $P/back-to-fixture "yes" "$(pv back_to_fixture)" # THE INVERSE SHARES THE BACKING MAPPINGS RATHER THAN COPYING THEM -- `inv._fwdm # is b._invm' and vice versa. This is the mechanism behind every case above, and # it is asserted directly so that a release which switched to eager copying is # one red naming the cause rather than six naming symptoms. check_eq $P/inv-shares-fwdm "yes" "$(pv inv_shares_fwdm)" check_eq $P/inv-shares-invm "yes" "$(pv inv_shares_invm)" # `inverted()' PREFERS THE OBJECT'S OWN `__inverted__', which returns an # iterator over the ALREADY inverted items, over swapping each item on the fly. # Both branches are exercised: a bidict takes the fast one, a plain list of # pairs takes the swap. They produce the same sequence here, which is the point # -- the optimisation must not be observable. check_eq $P/inverted-items "hydrogen:H,helium:He,lithium:Li,beryllium:Be" \ "$(pv inverted_items)" check_eq $P/inverted-plain-list "hydrogen:H,helium:He,lithium:Li,beryllium:Be" \ "$(pv inverted_plain_list)" check_eq $P/has-dunder-inverted "yes" "$(pv has_dunder_inverted)" # ------------------------------------------------------------- uniqueness --- # See THE ASYMMETRY THAT MATTERS MOST in the header. The default `on_dup' is # pinned first because every case in this section follows from it. check_eq $P/on-dup-key "DROP_OLD" "$(pv on_dup_key)" check_eq $P/on-dup-val "RAISE" "$(pv on_dup_val)" check_eq $P/on-dup-default-is "yes" "$(pv on_dup_default_is)" # A DUPLICATE VALUE THROUGH `__setitem__' RAISES, and the mapping is untouched. # This is the case that protects against silently dropping the key that owns the # value -- the one thing a one-to-one mapping cannot do quietly. check_eq $P/setitem-dup-val-error "ValueDuplicationError" \ "$(pv setitem_dup_val_error)" check_eq $P/setitem-dup-val-unchanged "yes" "$(pv setitem_dup_val_unchanged)" # A DUPLICATE KEY THROUGH `__setitem__' REPLACES, exactly as dict does, and the # old value leaves the INVERSE with it -- which is the half a dict has no # equivalent of, and the half that would rot if `_write' forgot one side. check_eq $P/setitem-dup-key-replaces "helium-3" "$(pv setitem_dup_key_replaces)" check_eq $P/setitem-dup-key-old-val-present "no" \ "$(pv setitem_dup_key_old_val_present)" check_eq $P/setitem-dup-key-len "4" "$(pv setitem_dup_key_len)" # THE SAME ITEM TWICE IS A NO-OP, not a duplication -- `_dedup' returns None # before either action is consulted. Pinned because it is the reason an # idempotent write does not have to be guarded by the caller. check_eq $P/setitem-same-item-noop-len "4" "$(pv setitem_same_item_noop_len)" # `put' RAISES ON EITHER SIDE, and the three exceptions are distinguished. The # contrast with `setitem-dup-key-replaces' above is the asymmetry the header # names, and the two must be read together. check_eq $P/put-dup-key-error "KeyDuplicationError" "$(pv put_dup_key_error)" check_eq $P/put-dup-val-error "ValueDuplicationError" "$(pv put_dup_val_error)" # KEY AND VALUE EACH DUPLICATING A DIFFERENT ITEM is a THIRD type, not one of # the two above -- `_dedup' checks that case first. check_eq $P/put-dup-both-error "KeyAndValueDuplicationError" \ "$(pv put_dup_both_error)" check_eq $P/put-unchanged "yes" "$(pv put_unchanged)" # THE SAME ITEM AGAIN IS A NO-OP EVEN UNDER `ON_DUP_RAISE' -- `none', not an # exception. So `put' is idempotent for an item already present, which is not # what "raise on any duplication" reads like. check_eq $P/put-same-item-error "none" "$(pv put_same_item_error)" check_eq $P/put-same-item-len "4" "$(pv put_same_item_len)" # AND A GENUINELY NEW ITEM GOES IN, forward and inverse, so that the reds above # are not merely "put never writes anything". check_eq $P/put-new-len "5" "$(pv put_new_len)" check_eq $P/put-new-inv "B" "$(pv put_new_inv)" # THE HIERARCHY. See THE EXCEPTION HIERARCHY IS A TRAP in the header -- these # five cases are one finding, and reading any of them alone gets it wrong. check_eq $P/kvde-is-kde "yes" "$(pv kvde_is_kde)" check_eq $P/kvde-is-vde "yes" "$(pv kvde_is_vde)" check_eq $P/kde-is-dup-error "yes" "$(pv kde_is_dup_error)" check_eq $P/dup-error-is-bidict-error "yes" "$(pv dup_error_is_bidict_error)" check_eq $P/bidict-error-is-exception "yes" "$(pv bidict_error_is_exception)" # THE EXCEPTION CARRIES THE OFFENDING KEY OR VALUE as its single argument, which # is what lets a caller report WHICH item was refused rather than only that one # was. Asserted for both directions, because they carry different halves of the # item: the value for a value duplication, the key for a key one. check_eq $P/vde-args "helium" "$(pv vde_args)" check_eq $P/kde-args "He" "$(pv kde_args)" # ----------------------------------------------- resolving a duplication --- # `forceput' AND `put(on_dup=...)' -- the documented fixes for each case above. # `forceput' IS `put(..., on_dup=ON_DUP_DROP_OLD)', and that identity is # asserted rather than only its behaviour, because everything below follows from # it. check_eq $P/force-is-drop-old "yes" "$(pv force_is_drop_old)" # FORCING A DUPLICATE VALUE DROPS THE OLD KEY ENTIRELY. `He' owned `helium'; # after the force `D' owns it and `He' is gone -- so `len' does NOT move (one # item left, one arrived) while the key set did. That is why the length and the # key presence are separate cases: either alone would miss it. check_eq $P/force-dup-val-fwd "helium" "$(pv force_dup_val_fwd)" check_eq $P/force-dup-val-old-key-present "no" \ "$(pv force_dup_val_old_key_present)" check_eq $P/force-dup-val-len "4" "$(pv force_dup_val_len)" check_eq $P/force-dup-val-inv "D" "$(pv force_dup_val_inv)" # FORCING A DUPLICATE KEY REPLACES THE VALUE, and the old value leaves the # inverse. Same shape as `__setitem__' on a duplicate key -- which is the point: # `forceput' differs from `__setitem__' only on the value side. check_eq $P/force-dup-key-fwd "helium-3" "$(pv force_dup_key_fwd)" check_eq $P/force-dup-key-len "4" "$(pv force_dup_key_len)" check_eq $P/force-dup-key-old-val-present "no" \ "$(pv force_dup_key_old_val_present)" # FORCING BOTH AT ONCE COLLAPSES TWO ITEMS INTO ONE, and `len' FALLS. See WHERE # THE EXPECTED VALUES COME FROM in the header: `He'->`helium' and `Li'->`lithium' # become the single `He'->`lithium'. This is the case a caller reaching for # `forceput' to "just make it fit" needs to see, and the only one in this section # that loses data. check_eq $P/force-dup-both-fwd "lithium" "$(pv force_dup_both_fwd)" check_eq $P/force-dup-both-len "3" "$(pv force_dup_both_len)" check_eq $P/force-dup-both-li-present "no" "$(pv force_dup_both_li_present)" check_eq $P/force-dup-both-helium-present "no" \ "$(pv force_dup_both_helium_present)" # `DROP_NEW' KEEPS THE INCUMBENT AND DISCARDS THE INCOMING ITEM, SILENTLY -- no # exception, and `len' does not move. The opposite resolution to `DROP_OLD', and # the one whose failure mode is a write that appears to have happened. All three # cases are needed to say that: the length, the absent new key, and the # incumbent still owning the value. check_eq $P/drop-new-len "4" "$(pv drop_new_len)" check_eq $P/drop-new-d-present "no" "$(pv drop_new_d_present)" check_eq $P/drop-new-incumbent "He" "$(pv drop_new_incumbent)" # A MIXED `OnDup' -- raise on a duplicate key, drop the old on a duplicate # value. The two fields are read on different branches of `_dedup', so they are # asserted separately. check_eq $P/mixed-dup-key-error "KeyDuplicationError" "$(pv mixed_dup_key_error)" check_eq $P/mixed-dup-val-ok "helium" "$(pv mixed_dup_val_ok)" check_eq $P/mixed-dup-val-len "4" "$(pv mixed_dup_val_len)" # WITH BOTH DUPLICATED, ONLY `val' IS CONSULTED. So `key=RAISE' does NOT raise # here and two items collapse -- see THE EXCEPTION HIERARCHY IS A TRAP in the # header. Documented on `OnDup', and exactly the kind of thing a reader assumes # the other way round. check_eq $P/mixed-dup-both-error "none" "$(pv mixed_dup_both_error)" check_eq $P/mixed-dup-both-len "3" "$(pv mixed_dup_both_len)" # `forceupdate' IS THE BULK FORM, and it is NOT atomic -- nothing raises, so # there is nothing to roll back. Asserted as the resulting mapping rather than as # an absence of exception, because the interesting thing is which items survived: # `D' takes `helium' from `He', and `He' then takes `lithium' from `Li'. check_eq $P/forceupdate-len "4" "$(pv forceupdate_len)" check_eq $P/forceupdate-items "Be:beryllium,D:helium,H:hydrogen,He:lithium" \ "$(pv forceupdate_items)" # --------------------------------------------------------- `putall' IS ATOMIC --- # THREE PATHS, THREE CASES. See THE THREE UPDATE PATHS in the header: the # argument's shape picks the implementation, and only path 2 can actually leave a # half-applied mapping. Each `*-unchanged' case compares the mapping FORWARD AND # INVERSE against the fixture, so an unwrite that repaired only one side fails # here and nowhere else in this file. # # PATH 1 -- five items against a mapping of four, so `_update' works on a copy. check_eq $P/putall-big-error "ValueDuplicationError" "$(pv putall_big_error)" check_eq $P/putall-big-unchanged "yes" "$(pv putall_big_unchanged)" check_eq $P/putall-big-len "4" "$(pv putall_big_len)" # PATH 2 -- two items against a mapping of four, so `_update' writes in place # and must replay its unwrites. The conflicting pair is placed LAST on purpose: a # good write happens first and has to be undone. check_eq $P/putall-small-error "ValueDuplicationError" "$(pv putall_small_error)" check_eq $P/putall-small-unchanged "yes" "$(pv putall_small_unchanged)" check_eq $P/putall-small-len "4" "$(pv putall_small_len)" # PATH 3 -- a GENERATOR, which is not `Sized', so path 1's `len(arg)' test # cannot apply however many items it will yield. Same in-place unwrite path # reached by a different route, and the route a caller streaming items takes # without knowing it. check_eq $P/putall-gen-error "ValueDuplicationError" "$(pv putall_gen_error)" check_eq $P/putall-gen-unchanged "yes" "$(pv putall_gen_unchanged)" check_eq $P/putall-gen-len "4" "$(pv putall_gen_len)" # A DUPLICATE *WITHIN* THE BATCH rather than against the mapping. The batch is # processed in order, so the second item conflicts with the first -- which was # itself only just written and must be unwritten. This is the unwrite path with # nothing pre-existing to restore, which is where an off-by-one in the replay # would show. check_eq $P/putall-internal-dup-error "ValueDuplicationError" \ "$(pv putall_internal_dup_error)" check_eq $P/putall-internal-dup-unchanged "yes" \ "$(pv putall_internal_dup_unchanged)" # AND A BATCH THAT SUCCEEDS, so that "unchanged" above is not merely "putall # never writes anything". check_eq $P/putall-ok-len "6" "$(pv putall_ok_len)" check_eq $P/putall-ok-inv "C" "$(pv putall_ok_inv)" # `putall' WITH `on_dup=ON_DUP_DROP_OLD' DOES NOT ROLL BACK, because `rollback' # defaults to `RAISE in on_dup' and nothing raises. The batch applies: `D' takes # `helium' and `He' is dropped. This is the flag's documented contract, not a # defect, and it is asserted so that reading `putall' as unconditionally atomic # is caught here rather than in a consumer. check_eq $P/putall-drop-old-len "5" "$(pv putall_drop_old_len)" check_eq $P/putall-drop-old-d "helium" "$(pv putall_drop_old_d)" check_eq $P/putall-drop-old-he-present "no" "$(pv putall_drop_old_he_present)" # `update' IS `putall' AT THE INSTANCE'S OWN `on_dup', so a duplicate VALUE # raises and rolls back while a duplicate KEY is absorbed. The same asymmetry as # `__setitem__', in bulk form. check_eq $P/update-dup-val-error "ValueDuplicationError" \ "$(pv update_dup_val_error)" check_eq $P/update-dup-val-unchanged "yes" "$(pv update_dup_val_unchanged)" check_eq $P/update-dup-key-ok "helium-3" "$(pv update_dup_key_ok)" # ------------------------------------------------------- the mutable rest --- # The `MutableMapping' surface. Asserted because `bidict' has to keep TWO # mappings consistent through each of these, and a plain-dict implementation of # any one of them would leave the inverse stale -- which no forward read can see. check_eq $P/pop-value "hydrogen" "$(pv pop_value)" check_eq $P/pop-inv-present "no" "$(pv pop_inv_present)" check_eq $P/pop-missing-error "KeyError" "$(pv pop_missing_error)" check_eq $P/pop-missing-default "absent" "$(pv pop_missing_default)" check_eq $P/popitem-pair "Be:beryllium" "$(pv popitem_pair)" check_eq $P/popitem-len "2" "$(pv popitem_len)" # `clear' EMPTIES BOTH SIDES. A `clear' that emptied only `_fwdm' would leave the # inverse holding every item and `len(b)' reporting zero, which is the worst # available failure: consistent-looking and wrong. check_eq $P/clear-len "0" "$(pv clear_len)" check_eq $P/clear-inv-len "0" "$(pv clear_inv_len)" check_eq $P/popitem-empty-error "KeyError" "$(pv popitem_empty_error)" # `copy' IS SHALLOW AND INDEPENDENT. A copy that shared backing mappings would # pass an equality check and then propagate every later write -- so the # assertion is about the write, not the equality. check_eq $P/copy-independent "yes" "$(pv copy_independent)" check_eq $P/copy-eq-after "yes" "$(pv copy_eq_after)" check_eq $P/copy-class "bidict" "$(pv copy_class)" # THE SET-LIKE OPERATORS. `|' returns a NEW bidict rather than mutating, `|=' # mutates in place, and `|' with a plain dict on the LEFT still yields a bidict # through `__ror__'. The pair a caller reaching for either needs pinned. check_eq $P/or-len "5" "$(pv or_len)" check_eq $P/or-class "bidict" "$(pv or_class)" check_eq $P/ior-len "5" "$(pv ior_len)" check_eq $P/ror-len "5" "$(pv ror_len)" # PICKLING ROUND-TRIPS. Worth asserting because `__reduce__' has a special case: # a dynamically generated inverse class is pickled as its ORIGINAL's inverse, # since the generated class is not importable by name. `pickle-inv-eq' is that # path; `pickle-eq' is the ordinary one. # # `pickle-available' is the probe's own dependency, guarded -- see # `dep-pickle-installed' above. If it reads `no', the three cases below read # `no-pickle' and this is one attributed red rather than a truncated transcript. check_eq $P/pickle-available "yes" "$(pv pickle_available)" check_eq $P/pickle-eq "yes" "$(pv pickle_eq)" check_eq $P/pickle-inv-works "Li" "$(pv pickle_inv_works)" check_eq $P/pickle-inv-eq "yes" "$(pv pickle_inv_eq)" # `reversed()' WORKS because the backing mappings are `dict', which is # reversible. `_set_reversed' decides that per class at definition time and sets # `__reversed__' to None where the backings are not -- so this is a statement # about the class, made at import, and not about this instance. check_eq $P/reversed-keys "Be,Li,He,H" "$(pv reversed_keys)" # THE ABCs. `bidict' is registered as a MUTABLE bidirectional mapping and # `frozenbidict' is NOT -- which is the type-level half of the immutability # asserted behaviourally below, and the half a consumer's `isinstance' check # reads. check_eq $P/is-bidirectional "yes" "$(pv is_bidirectional)" check_eq $P/is-mutable-bidirectional "yes" "$(pv is_mutable_bidirectional)" check_eq $P/frozen-is-bidirectional "yes" "$(pv frozen_is_bidirectional)" check_eq $P/frozen-is-mutable "no" "$(pv frozen_is_mutable)" # THE ABC IS ABSTRACT -- `inverse' is an `abstractmethod', so instantiating # `BidirectionalMapping' itself is a TypeError. Pinned because that is what makes # it an interface rather than a base class with a broken default. check_eq $P/abc-abstract-error "TypeError" "$(pv abc_abstract_error)" # ----------------------------------------------------------- frozenbidict --- # HASHABLE AND IMMUTABLE, and the two are ONE claim: it is hashable BECAUSE it # cannot change. A release that added `__setitem__' while keeping `__hash__' # would be usable as a dict key and then mutate out from under that dict, which # is a silent corruption no forward read would ever show. check_eq $P/frozen-len "4" "$(pv frozen_len)" check_eq $P/frozen-fwd "hydrogen" "$(pv frozen_fwd)" check_eq $P/frozen-inv "He" "$(pv frozen_inv)" check_eq $P/frozen-hashable "yes" "$(pv frozen_hashable)" # THE HASH IS THE ITEMS' -- `t.ItemsView(self)._hash()', documented as "like # `hash(frozenset(self.items()))' but more memory efficient". Asserted against # that equality rather than against a literal, because a hash literal is not # stable across interpreter builds and would be a false red on this arm. check_eq $P/frozen-hash-is-items "yes" "$(pv frozen_hash_is_items)" # ORDER DOES NOT AFFECT THE HASH, which follows from the frozenset and is what # makes two equal frozenbidicts interchangeable as dict keys. check_eq $P/frozen-hash-order-free "yes" "$(pv frozen_hash_order_free)" # THE HASH IS CACHED on `_hash' after the first call -- an implementation # detail, but the one that makes repeated use as a dict key cheap, and the one # that would go wrong if the object were ever mutable. check_eq $P/frozen-hash-cached "yes" "$(pv frozen_hash_cached)" check_eq $P/frozen-hash-stable "yes" "$(pv frozen_hash_stable)" # USABLE AS A DICT KEY, which is the whole point of the four cases above -- and # the round trip a consumer actually performs. check_eq $P/frozen-as-dict-key "ok" "$(pv frozen_as_dict_key)" # IMMUTABLE, ASSERTED METHOD BY METHOD. There is no `__setitem__', # `__delitem__', `update', `clear', `put' or `forceput' on `BidictBase' at all -- # they are ABSENT, not present-and-raising. So `frozen-setitem-error' reads # `no-attr' rather than an exception name, and that is the assertion: a release # that added a raising `__setitem__' would change this value while keeping the # behaviour, which is worth knowing. check_eq $P/frozen-setitem-error "no-attr" "$(pv frozen_setitem_error)" check_eq $P/frozen-has-setitem "no" "$(pv frozen_has_setitem)" check_eq $P/frozen-has-delitem "no" "$(pv frozen_has_delitem)" check_eq $P/frozen-has-update "no" "$(pv frozen_has_update)" check_eq $P/frozen-has-clear "no" "$(pv frozen_has_clear)" check_eq $P/frozen-has-put "no" "$(pv frozen_has_put)" check_eq $P/frozen-has-forceput "no" "$(pv frozen_has_forceput)" # AND THE TWO WAYS A CALLER ACTUALLY HITS IT, which raise DIFFERENT exceptions: # `fb[k] = v' is a TypeError from the store protocol, while `fb.__delitem__(k)' # is an AttributeError from the name lookup. Pinned separately because which one # a consumer sees depends on how it wrote the mutation, and a `except TypeError' # around a `del' would not catch it. check_eq $P/frozen-store-error "TypeError" "$(pv frozen_store_error)" check_eq $P/frozen-del-error "AttributeError" "$(pv frozen_del_error)" check_eq $P/frozen-unchanged "yes" "$(pv frozen_unchanged)" # THE INVERSE OF A frozenbidict IS ALSO FROZEN, and hashable. An inverse that # came back mutable would be a way round every case above -- write through the # inverse and the "immutable" mapping changes. check_eq $P/frozen-inv-class "frozenbidict" "$(pv frozen_inv_class)" check_eq $P/frozen-inv-hashable "yes" "$(pv frozen_inv_hashable)" # A PLAIN `bidict' IS NOT HASHABLE, which is the contrast that makes the above # mean something: `MutableMapping' sets `__hash__' to None. check_eq $P/bidict-hashable "no" "$(pv bidict_hashable)" # EQUALITY CROSSES THE TYPES -- `__eq__' compares items, not classes, so a # frozenbidict equals the bidict with the same items. Equal-but-differently- # hashable is fine here precisely because the mutable one is not hashable at all. check_eq $P/frozen-eq-bidict "yes" "$(pv frozen_eq_bidict)" # ----------------------------------------------------------- OrderedBidict --- # INSERTION ORDER, PRESERVED ACROSS BOTH VIEWS. The forward order is the # insertion order and the INVERSE's order MIRRORS it -- which is not free: both # views read one doubly-linked list, from opposite sides. Every `ord-inv-*' case # is there because a copy-based or independently-ordered inverse would pass the # forward half alone. check_eq $P/ord-keys "H,He,Li,Be" "$(pv ord_keys)" check_eq $P/ord-values "hydrogen,helium,lithium,beryllium" "$(pv ord_values)" check_eq $P/ord-items "H:hydrogen,He:helium,Li:lithium,Be:beryllium" \ "$(pv ord_items)" check_eq $P/ord-inv-keys "hydrogen,helium,lithium,beryllium" \ "$(pv ord_inv_keys)" check_eq $P/ord-inv-items "hydrogen:H,helium:He,lithium:Li,beryllium:Be" \ "$(pv ord_inv_items)" check_eq $P/ord-reversed "Be,Li,He,H" "$(pv ord_reversed)" check_eq $P/ord-reversed-items \ "Be:beryllium,Li:lithium,He:helium,H:hydrogen" "$(pv ord_reversed_items)" check_eq $P/ord-class "OrderedBidict" "$(pv ord_class)" check_eq $P/ord-inv-class "OrderedBidict" "$(pv ord_inv_class)" # A NEW ITEM GOES AT THE END, and it appears at the end of the INVERSE's order # too -- the case that says the two views read one list rather than two. check_eq $P/ord-after-append "H,He,Li,Be,B" "$(pv ord_after_append)" check_eq $P/ord-inv-after-append "hydrogen,helium,lithium,beryllium,boron" \ "$(pv ord_inv_after_append)" # REPLACING A VALUE KEEPS THE KEY'S POSITION. `_bykey' is true here, so the # node is keyed on the KEY and a value replacement does not move it -- while the # inverse's order, which is the same list read by value, shows the NEW value in # the OLD position. Both halves asserted; the inverse half is the one that would # break if the node were re-linked. check_eq $P/ord-after-replace "H,He,Li,Be,B" "$(pv ord_after_replace)" check_eq $P/ord-inv-after-replace \ "hydrogen,helium-3,lithium,beryllium,boron" "$(pv ord_inv_after_replace)" # `move_to_end' BOTH WAYS, and a missing key is a KeyError rather than a no-op. check_eq $P/ord-move-last "He,Li,Be,B,H" "$(pv ord_move_last)" check_eq $P/ord-move-first "H,He,Li,Be,B" "$(pv ord_move_first)" check_eq $P/ord-move-missing-error "KeyError" "$(pv ord_move_missing_error)" # `popitem' TAKES FROM EITHER END -- `last=True' by default, and the parameter # is what an `OrderedDict' user reaches for. `dict.popitem' has no such # parameter, so a consumer swapping one for the other gets a TypeError rather # than the wrong end, which is why the keyword form is asserted. check_eq $P/ord-popitem-last "B:boron" "$(pv ord_popitem_last)" check_eq $P/ord-popitem-first "H:hydrogen" "$(pv ord_popitem_first)" check_eq $P/ord-after-popitems "He,Li,Be" "$(pv ord_after_popitems)" check_eq $P/ord-popitem-empty-error "KeyError" "$(pv ord_popitem_empty_error)" # `==' IS ORDER-INSENSITIVE, INHERITED, and `equals_order_sensitive' is the # opt-in. See WHERE THE EXPECTED VALUES COME FROM in the header: this is the # trap the package documents, and the reason a test that used `==' to check # ordering would pass while asserting nothing. All four cases are one finding. check_eq $P/ord-eq-order-insensitive "yes" "$(pv ord_eq_order_insensitive)" check_eq $P/ord-eq-order-sensitive "no" "$(pv ord_eq_order_sensitive)" check_eq $P/ord-eq-order-sensitive-self "yes" \ "$(pv ord_eq_order_sensitive_self)" check_eq $P/ord-eq-plain-dict "yes" "$(pv ord_eq_plain_dict)" # THE ORDERED VIEWS ARE THE PACKAGE'S OWN, not the backing dict's -- because a # mutated OrderedBidict's order no longer matches its backing mappings, so # delegating would report the wrong order. The concrete type names are the # assertion, and they are private names on purpose: a release that went back to # delegating would change them and quietly reintroduce the bug. check_eq $P/ord-keys-view-type "_OrderedBidictKeysView" \ "$(pv ord_keys_view_type)" check_eq $P/ord-items-view-type "_OrderedBidictItemsView" \ "$(pv ord_items_view_type)" # AND THEY ARE STILL SET-LIKE, which the override had to preserve by proxying # every `Set' method to the backing dict's view. Losing that would be invisible # from any ordering case above. check_eq $P/ord-keys-set-op "H,Li" "$(pv ord_keys_set_op)" check_eq $P/ord-keys-eq "yes" "$(pv ord_keys_eq)" # ORDER SURVIVES `clear' + REFILL. This is the case that catches a `clear' that # emptied the mappings and left the linked list behind -- the refilled mapping # would then report the old order, or a sentinel from the old list, and every # other ordering case here would still pass. check_eq $P/ord-clear-len "0" "$(pv ord_clear_len)" check_eq $P/ord-after-clear-keys "X,Y" "$(pv ord_after_clear_keys)" # ------------------------------------------------------------------- OnDup --- # `OnDup' IS A NamedTuple, so it is comparable by value, iterable, and its # members are the singleton `OnDupAction' enum values -- which is what makes # `RAISE in on_dup' work at all, and that expression is how `_update' decides # whether to roll back. So `ondup-raise-in-default' and # `ondup-raise-in-drop-old' are not trivia: they are the atomicity switch from # the `putall' section, read at its source. check_eq $P/ondup-is-tuple "yes" "$(pv ondup_is_tuple)" check_eq $P/ondup-fields "key,val" "$(pv ondup_fields)" check_eq $P/ondup-default-repr \ "OnDup(key=OnDupAction.DROP_OLD, val=OnDupAction.RAISE)" \ "$(pv ondup_default_repr)" check_eq $P/ondup-raise-repr \ "OnDup(key=OnDupAction.RAISE, val=OnDupAction.RAISE)" "$(pv ondup_raise_repr)" check_eq $P/ondup-drop-old-repr \ "OnDup(key=OnDupAction.DROP_OLD, val=OnDupAction.DROP_OLD)" \ "$(pv ondup_drop_old_repr)" # THE ENUM'S MEMBER ORDER IS PART OF THE SURFACE, since `OnDupAction' is # iterable and a consumer may render it. check_eq $P/ondup-action-names "RAISE,DROP_OLD,DROP_NEW" \ "$(pv ondup_action_names)" check_eq $P/ondup-raise-is-singleton "yes" "$(pv ondup_raise_is_singleton)" check_eq $P/ondup-raise-in-default "yes" "$(pv ondup_raise_in_default)" check_eq $P/ondup-raise-in-drop-old "no" "$(pv ondup_raise_in_drop_old)" # THE DEFAULTS OF `OnDup' ITSELF, which is what a caller constructing a partial # policy gets for the field it left out -- and they are NOT symmetric: an # omitted `key' means DROP_OLD while an omitted `val' means RAISE. check_eq $P/ondup-bare-key "DROP_OLD" "$(pv ondup_bare_key)" check_eq $P/ondup-bare-val "RAISE" "$(pv ondup_bare_val)" # ------------------------------------------------------------- interpreter --- # `py-maxsize-bits' is the ONE value in this file the suite does NOT take from # the control: it reads 64 there and must read 32 here. It is what says the # transcript came from the wasm32 interpreter -- every other value in this file # is arm-independent by construction, so without it a suite run against a HOST # copy of `bidict' would pass in full. check_eq $P/py-version "3.12" "$(pv py_version)" check_eq $P/py-maxsize-bits "32" "$(pv py_maxsize_bits)" rm -rf "$W"