#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-bidict. Emits `key=value' lines, one per line, in file order; the suite reads them with grep. A python3 start on this arm is tens of seconds and every measurement here is a dict operation on a mapping of at most four items -- microseconds beside the start -- so taking the start ONCE is the difference between a half-minute suite and a twenty-minute one. THE TRANSPORT RULES. The runner parses its input line by line, so no value may contain a NEWLINE; the suite splits on the FIRST `=', so a value containing `=' is safe and a key containing one is not. Nothing emitted below carries a memory address: `BidictBase.__repr__' is defined and renders the items, so a bidict's repr is stable -- but the DESCRIPTORS, views and exception INSTANCES are never repr'd, only their type names and arguments. WHAT THIS PACKAGE IS, AND WHAT THAT MAKES THE PROBE ASSERT. A `bidict' is a one-to-one mapping that exposes the reverse of itself as a LIVE view. So the interesting claims are never the forward lookups -- a plain dict satisfies those -- they are: * THE INVERSE IS A VIEW, NOT A COPY. Every `inverse' case below takes the 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 case 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, because `KeyAndValueDuplicationError' inherits from both of the others and an `except KeyDuplicationError' therefore catches it. * `putall' IS ATOMIC. That is the only claim here whose implementation has THREE distinct code paths, and a test that exercised one would say nothing about the other two. See THE THREE UPDATE PATHS below. THE THREE UPDATE PATHS. `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 if the unwrites are wrong. 3. `arg' is a generator, hence not `Sized' -> path 2, because the `len(arg)' test in path 1 requires `isinstance(arg, Sized)'. All three are exercised below (`putall_big_*', `putall_small_*', `putall_gen_*'), and each is asserted to leave the mapping BYTE-FOR-BYTE as it was -- forward AND inverse, since an unwrite that repaired only `_fwdm' would leave the two disagreeing and every forward assertion would still pass. 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 very release. That is asserted here as an ABSENCE (`has_namedbidict'), which is the only honest form the request can take: a suite that skipped it would leave a later reader unable to tell whether the factory was missed or is gone. WHERE THE EXPECTED VALUES COME FROM. Every value the suite compares against was MEASURED by running THIS FILE on an x86-64 control at the same version -- 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. Nothing in this package declares an ext_module, reads a `sizeof', packs a struct or branches on the platform, so a difference between the two arms is a DEFECT and not a property. ONE VALUE IS EXPECTED TO DIFFER FROM THE CONTROL, and it is what says the transcript came from the wasm32 interpreter rather than from a host copy of the module: `py_maxsize_bits' -- 32 here, 64 on the control. Without it a suite run against a host copy of `bidict' would pass in full, since every other value in this file is arm-independent by construction. THIS PROBE EXITS 0 EVEN WHEN THE IMPORT FAILS. bidict is pure Python, so an import failure would be a platform fact about the interpreter rather than about this package, and turning it into a hundred empty-string mismatches would bury that fact rather than report it. The failure is reported as `import_ok=no' with the exception, the suite reds exactly one case for it, and every behavioural case is SKIPPED with that reason attached. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def err(fn, *args, **kw): """The exception TYPE NAME a call raises, or `none' when it returns. The name only: a message carries quoting and, for some builtins, the repr of an argument, neither of which is worth pinning across two arms. """ try: fn(*args, **kw) except BaseException as e: return type(e).__name__ return 'none' def yn(b): return 'yes' if b else 'no' # ------------------------------------------------------------------ import --- try: import bidict _import_ok = True _import_err = '' except BaseException as e: # pragma: no cover _import_ok = False _import_err = '%s: %s' % (type(e).__name__, e) emit('import_ok', yn(_import_ok)) emit('import_error', _import_err.replace('\n', ' ')) if not _import_ok: emit('emitted_count', str(len(emitted) + 2)) emit('probe_done', 'yes') sys.exit(0) from bidict import OnDup from bidict import bidict as bd from bidict import frozenbidict from bidict import inverted from bidict import OrderedBidict # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. The path is the target's, not the control's, so the suite # asserts it directly against the packaging rather than comparing it here. emit('package_file', bidict.__file__) # A PACKAGE, NOT A MODULE -- twelve source files behind one import name. The # payload half of this is `is-a-package' in the suite; this is the same claim as # the interpreter sees it. emit('is_package', yn(hasattr(bidict, '__path__'))) emit('submodule_count', str(len([n for n in sys.modules if n.startswith('bidict.')]))) # THE METADATA DUNDERS, which live in `bidict/metadata.py' and are re-exported. # `__author__' is a DICT here, not a string -- unusual, and the kind of thing a # consumer formatting it into a banner would trip over. emit('version_attr', bidict.__version__) emit('author_name', bidict.__author__['name']) emit('author_email', bidict.__author__['email']) emit('author_is_dict', yn(isinstance(bidict.__author__, dict))) emit('license_attr', bidict.__license__) emit('url_attr', bidict.__url__) emit('description_attr', bidict.__description__) # THE INSTALLED DISTRIBUTION'S OWN IDEA OF THE VERSION, read from the dist-info # METADATA rather than from the source. `version_agrees' is the case that says # the wheel was cut from the source this package is. try: from importlib.metadata import version as _dist_version _dv = _dist_version('bidict') except BaseException as e: _dv = '<%s>' % type(e).__name__ emit('dist_version', _dv) emit('version_agrees', yn(_dv == bidict.__version__)) # THERE IS NO `__all__'. The package re-exports with the `from ._x import Y as Y' # idiom instead, which is what makes the names public to a type checker without # an explicit list. Both halves pinned: the absence, and the surface it leaves. emit('has_all', yn(hasattr(bidict, '__all__'))) emit('public_names', ','.join(sorted( n for n in vars(bidict) if not n.startswith('_')))) # `__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'. # Asserted because it is a deliberate act of that loop, not a default. emit('bidict_module', bd.__module__) emit('frozen_module', frozenbidict.__module__) # WHAT THIS TICKET ASKED FOR AND THIS RELEASE DOES NOT HAVE. `namedbidict' was # REMOVED in 0.23.0. Asserted as an absence -- see the header. emit('has_namedbidict', yn(hasattr(bidict, 'namedbidict'))) # --------------------------------------------------- the forward direction --- # The fixture, used by everything below that does not need its own. Four items, # fixed, and every literal the suite compares against is derived from these. PAIRS = [('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium'), ('Be', 'beryllium')] b = bd(PAIRS) emit('len', str(len(b))) emit('fwd_H', b['H']) emit('fwd_Be', b['Be']) emit('fwd_missing', err(lambda: b['Xe'])) emit('contains_H', yn('H' in b)) emit('contains_hydrogen', yn('hydrogen' in b)) emit('keys', ','.join(b.keys())) emit('values', ','.join(b.values())) emit('items', ','.join('%s:%s' % kv for kv in b.items())) emit('repr', repr(b)) emit('eq_dict', yn(b == dict(PAIRS))) emit('get_default', b.get('Xe', 'absent')) # `values()' IS A KEYS VIEW, not a `ValuesView' -- because the values of a bidict # ARE the keys of its inverse. So it supports set operations and O(1) # containment, which a plain dict's `values()' does not. The type name is the # assertion; the set operation is the consequence. emit('values_view_type', type(b.values()).__name__) emit('values_is_set_like', yn(b.values() & {'helium', 'neon'} == {'helium'})) emit('values_isdisjoint', yn(b.values().isdisjoint({'neon'}))) # ---------------------------------------------- the inverse is a LIVE view --- # EVERY CASE HERE TAKES THE REFERENCE FIRST AND MUTATES AFTERWARDS. That is the # whole point: a bidict that rebuilt its inverse per access would pass a test # that re-read `b.inverse' and fail this one. inv = b.inverse emit('inv_hydrogen', inv['hydrogen']) emit('inv_len', str(len(inv))) emit('inv_is_cached', yn(b.inverse is b.inverse)) emit('inv_inv_is_original', yn(b.inverse.inverse is b)) emit('inv_alias_is_inverse', yn(b.inv is b.inverse)) emit('inv_class', type(inv).__name__) # 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. Contrast # `GeneratedBidictInverse', which is what a subclass with differing backings gets. emit('inv_is_bidict', yn(type(inv) is bd)) emit('inv_is_generated', yn(isinstance(inv, bidict.GeneratedBidictInverse))) # THE LIVE-VIEW ASSERTION PROPER: write through the FORWARD mapping and read the # inverse reference taken BEFORE the write. b['B'] = 'boron' emit('live_inv_after_fwd_write', inv['boron']) emit('live_inv_len_after_fwd_write', str(len(inv))) # AND THE OTHER WAY ROUND: write through the inverse, read the forward. inv['carbon'] = 'C' emit('live_fwd_after_inv_write', b['C']) # A DELETE PROPAGATES TOO, and it is the operation that would survive a # copy-based inverse for longest -- the item is gone from one side and present # on the other. del b['B'] emit('live_inv_boron_present', yn('boron' in inv)) emit('live_inv_len_after_delete', str(len(inv))) del inv['carbon'] emit('live_fwd_carbon_present', yn('C' in b)) emit('back_to_fixture', yn(dict(b.items()) == dict(PAIRS))) # THE INVERSE SHARES THE BACKING MAPPINGS RATHER THAN COPYING THEM. 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 here rather than six. emit('inv_shares_fwdm', yn(inv._fwdm is b._invm)) emit('inv_shares_invm', yn(inv._invm is b._fwdm)) # `__inverted__' AND THE `inverted()' FUNCTION. `inverted()' prefers the # object's own `__inverted__' -- which returns an iterator over the ALREADY # INVERTED items -- over swapping each item on the fly. emit('inverted_items', ','.join('%s:%s' % kv for kv in inverted(b))) emit('inverted_plain_list', ','.join('%s:%s' % kv for kv in inverted(PAIRS))) emit('has_dunder_inverted', yn(callable(getattr(b, '__inverted__', None)))) # ------------------------------------------------------------ uniqueness --- # WHICH EXCEPTION ARRIVES SAYS WHICH SIDE WAS VIOLATED, and the default `on_dup' # differs between `__setitem__' and `put' -- which is the single most # consequential asymmetry in this package's API. # # `bidict.on_dup' is `OnDup(key=DROP_OLD, val=RAISE)', so `b[existing] = new' # REPLACES and `b[new] = existing_value' RAISES. `put' defaults to # `ON_DUP_RAISE', so it raises on either. emit('on_dup_key', bd.on_dup.key.name) emit('on_dup_val', bd.on_dup.val.name) emit('on_dup_default_is', yn(bd.on_dup is bidict.ON_DUP_DEFAULT)) u = bd(PAIRS) # A DUPLICATE VALUE THROUGH `__setitem__' RAISES -- this is the case that # protects against silently dropping the key that owns the value. emit('setitem_dup_val_error', err(u.__setitem__, 'D', 'helium')) emit('setitem_dup_val_unchanged', yn(dict(u.items()) == dict(PAIRS))) # A DUPLICATE KEY THROUGH `__setitem__' REPLACES, exactly as dict does, and the # old value leaves the inverse with it. u['He'] = 'helium-3' emit('setitem_dup_key_replaces', u['He']) emit('setitem_dup_key_old_val_present', yn('helium' in u.inverse)) emit('setitem_dup_key_len', str(len(u))) # THE SAME ITEM TWICE IS A NO-OP, not a duplication -- `_dedup' returns None. u['He'] = 'helium-3' emit('setitem_same_item_noop_len', str(len(u))) p = bd(PAIRS) # `put' RAISES ON EITHER SIDE, and the three exceptions are distinguished. emit('put_dup_key_error', err(p.put, 'He', 'helium-3')) emit('put_dup_val_error', err(p.put, 'D', 'helium')) # KEY AND VALUE EACH DUPLICATING A DIFFERENT ITEM. `_dedup' checks this case # FIRST and reports `val' rather than `key', so `ON_DUP_RAISE' gives # `KeyAndValueDuplicationError' -- a distinct type, not one of the two above. emit('put_dup_both_error', err(p.put, 'He', 'lithium')) emit('put_unchanged', yn(dict(p.items()) == dict(PAIRS))) # THE SAME ITEM AGAIN IS A NO-OP EVEN UNDER `ON_DUP_RAISE'. emit('put_same_item_error', err(p.put, 'He', 'helium')) emit('put_same_item_len', str(len(p))) # AND A GENUINELY NEW ITEM GOES IN. p.put('B', 'boron') emit('put_new_len', str(len(p))) emit('put_new_inv', p.inverse['boron']) # THE HIERARCHY, and it is not decoration: `KeyAndValueDuplicationError' inherits # from BOTH of the others, so an `except KeyDuplicationError' catches the # both-duplicated case too. A consumer who ordered its handlers the other way # round would never reach the specific one. KDE, VDE, KVDE = (bidict.KeyDuplicationError, bidict.ValueDuplicationError, bidict.KeyAndValueDuplicationError) emit('kvde_is_kde', yn(issubclass(KVDE, KDE))) emit('kvde_is_vde', yn(issubclass(KVDE, VDE))) emit('kde_is_dup_error', yn(issubclass(KDE, bidict.DuplicationError))) emit('dup_error_is_bidict_error', yn(issubclass( bidict.DuplicationError, bidict.BidictException))) emit('bidict_error_is_exception', yn(issubclass(bidict.BidictException, Exception))) # THE EXCEPTION CARRIES THE OFFENDING KEY OR VALUE as its single argument, which # is what lets a caller report which item was refused. try: bd(PAIRS).put('D', 'helium') except VDE as e: emit('vde_args', ','.join(str(a) for a in e.args)) try: bd(PAIRS).put('He', 'helium-3') except KDE as e: emit('kde_args', ','.join(str(a) for a in e.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)', asserted as that identity # rather than only behaviourally, because everything the behavioural cases show # follows from it. emit('force_is_drop_old', yn(bidict.ON_DUP_DROP_OLD == OnDup( key=bidict.DROP_OLD, val=bidict.DROP_OLD))) f = bd(PAIRS) # FORCING A DUPLICATE VALUE DROPS THE OLD KEY. `He' owned `helium'; after the # force `D' owns it and `He' is gone entirely -- len falls by one. f.forceput('D', 'helium') emit('force_dup_val_fwd', f['D']) emit('force_dup_val_old_key_present', yn('He' in f)) emit('force_dup_val_len', str(len(f))) emit('force_dup_val_inv', f.inverse['helium']) f2 = bd(PAIRS) # FORCING A DUPLICATE KEY REPLACES THE VALUE, and len is unchanged. f2.forceput('He', 'helium-3') emit('force_dup_key_fwd', f2['He']) emit('force_dup_key_len', str(len(f2))) emit('force_dup_key_old_val_present', yn('helium' in f2.inverse)) f3 = bd(PAIRS) # FORCING BOTH AT ONCE COLLAPSES TWO ITEMS INTO ONE -- `He'->`helium' and # `Li'->`lithium' become the single `He'->`lithium', so len falls by one. This # is the case that would silently lose data if a caller reached for `forceput' # without meaning to. f3.forceput('He', 'lithium') emit('force_dup_both_fwd', f3['He']) emit('force_dup_both_len', str(len(f3))) emit('force_dup_both_li_present', yn('Li' in f3)) emit('force_dup_both_helium_present', yn('helium' in f3.inverse)) # `DROP_NEW' KEEPS THE EXISTING ITEM AND DISCARDS THE INCOMING ONE, 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. n = bd(PAIRS) n.put('D', 'helium', on_dup=OnDup(key=bidict.DROP_NEW, val=bidict.DROP_NEW)) emit('drop_new_len', str(len(n))) emit('drop_new_d_present', yn('D' in n)) emit('drop_new_incumbent', n.inverse['helium']) # A MIXED `OnDup' -- raise on a duplicate key, drop the old on a duplicate value. # Both halves of one policy object, asserted separately, because `_dedup' reads # the two fields on different branches. mixed = OnDup(key=bidict.RAISE, val=bidict.DROP_OLD) m = bd(PAIRS) emit('mixed_dup_key_error', err(m.put, 'He', 'helium-3', on_dup=mixed)) m.put('D', 'helium', on_dup=mixed) emit('mixed_dup_val_ok', m['D']) emit('mixed_dup_val_len', str(len(m))) # WITH BOTH DUPLICATED, ONLY `val' IS CONSULTED -- documented on `OnDup' and # asserted here, because a reader would expect `key=RAISE' to raise. m2 = bd(PAIRS) emit('mixed_dup_both_error', err(m2.put, 'He', 'lithium', on_dup=mixed)) emit('mixed_dup_both_len', str(len(m2))) # `forceupdate' IS THE BULK FORM, and it is NOT atomic -- nothing raises, so # there is nothing to roll back. Asserted as the resulting mapping. fu = bd(PAIRS) fu.forceupdate([('D', 'helium'), ('He', 'lithium')]) emit('forceupdate_len', str(len(fu))) emit('forceupdate_items', ','.join('%s:%s' % kv for kv in sorted(fu.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 one of the three can # actually leave a half-applied mapping. Each is asserted to leave the mapping # unchanged FORWARD AND INVERSE -- an unwrite that repaired only `_fwdm' would # pass every forward assertion in this file. FIX = dict(PAIRS) FIXINV = {v: k for k, v in PAIRS} def state(x): return dict(x.items()) == FIX and dict(x.inverse.items()) == FIXINV # PATH 1 -- more items than the mapping holds, so `_update' works on a COPY and # becomes it only on success. Five items, the last conflicting. a1 = bd(PAIRS) emit('putall_big_error', err(a1.putall, [ ('B', 'boron'), ('C', 'carbon'), ('N', 'nitrogen'), ('O', 'oxygen'), ('D', 'helium')])) emit('putall_big_unchanged', yn(state(a1))) emit('putall_big_len', str(len(a1))) # PATH 2 -- fewer items than the mapping holds, so `_update' writes in place and # must replay its unwrites. THIS IS THE PATH THAT CAN GO WRONG, and the reason # the conflicting pair is placed LAST: a good write happens first and has to be # undone. a2 = bd(PAIRS) emit('putall_small_error', err(a2.putall, [('B', 'boron'), ('D', 'helium')])) emit('putall_small_unchanged', yn(state(a2))) emit('putall_small_len', str(len(a2))) # PATH 3 -- a GENERATOR, which is not `Sized', so the `len(arg)' test in path 1 # 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. a3 = bd(PAIRS) emit('putall_gen_error', err(a3.putall, ((k, v) for k, v in [('B', 'boron'), ('D', 'helium')]))) emit('putall_gen_unchanged', yn(state(a3))) emit('putall_gen_len', str(len(a3))) # 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. a4 = bd(PAIRS) emit('putall_internal_dup_error', err(a4.putall, [('B', 'boron'), ('C', 'boron')])) emit('putall_internal_dup_unchanged', yn(state(a4))) # AND A BATCH THAT SUCCEEDS, so that "unchanged" above is not merely "putall # never writes anything". a5 = bd(PAIRS) a5.putall([('B', 'boron'), ('C', 'carbon')]) emit('putall_ok_len', str(len(a5))) emit('putall_ok_inv', a5.inverse['carbon']) # `putall' WITH `on_dup=ON_DUP_DROP_OLD' DOES NOT ROLL BACK, because `rollback' # defaults to `RAISE in on_dup' and nothing raises. Asserted as the applied # result -- this is the flag's documented contract, not a defect. a6 = bd(PAIRS) a6.putall([('B', 'boron'), ('D', 'helium')], on_dup=bidict.ON_DUP_DROP_OLD) emit('putall_drop_old_len', str(len(a6))) emit('putall_drop_old_d', a6['D']) emit('putall_drop_old_he_present', yn('He' in a6)) # `update' IS `putall' AT THE INSTANCE'S OWN `on_dup', so a duplicate VALUE # raises and rolls back while a duplicate KEY is absorbed. a7 = bd(PAIRS) emit('update_dup_val_error', err(a7.update, [('B', 'boron'), ('D', 'helium')])) emit('update_dup_val_unchanged', yn(state(a7))) a7.update([('He', 'helium-3')]) emit('update_dup_key_ok', a7['He']) # ------------------------------------------------------- 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. r = bd(PAIRS) emit('pop_value', r.pop('H')) emit('pop_inv_present', yn('hydrogen' in r.inverse)) emit('pop_missing_error', err(r.pop, 'Xe')) emit('pop_missing_default', r.pop('Xe', 'absent')) emit('popitem_pair', '%s:%s' % r.popitem()) emit('popitem_len', str(len(r))) r.clear() emit('clear_len', str(len(r))) emit('clear_inv_len', str(len(r.inverse))) emit('popitem_empty_error', err(r.popitem)) # `copy' IS SHALLOW AND INDEPENDENT -- a copy that shared backing mappings would # pass an equality check and propagate every later write. c = bd(PAIRS) c2 = c.copy() c2['B'] = 'boron' emit('copy_independent', yn('B' not in c)) emit('copy_eq_after', yn(c2 != c)) emit('copy_class', type(c2).__name__) # THE SET-LIKE OPERATORS. `|' returns a NEW bidict rather than mutating, and # `|=' mutates in place -- the pair a caller reaching for either needs pinned. o1 = bd(PAIRS) | {'B': 'boron'} emit('or_len', str(len(o1))) emit('or_class', type(o1).__name__) o2 = bd(PAIRS) o2 |= {'B': 'boron'} emit('ior_len', str(len(o2))) emit('ror_len', str(len({'B': 'boron'} | bd(PAIRS)))) # PICKLING ROUND-TRIPS, and it is 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' IS THE PROBE'S OWN DEPENDENCY, NOT bidict's, and on this image it is a # SEPARATE sub-package of the ported python3 (BRINTOS-1476). So the import is # guarded: an image without `python3-pickle' would otherwise end the probe here # and turn every case below it into an empty-string mismatch, reporting a missing # stdlib sub-package as sixty bidict defects. `pickle_available' is the witness, # and the suite asserts `python3-pickle' installed beside it. try: import pickle _pickle_ok = True except BaseException as e: # pragma: no cover _pickle_ok = False _pickle_why = '%s: %s' % (type(e).__name__, e) emit('pickle_available', yn(_pickle_ok)) if _pickle_ok: pk = pickle.loads(pickle.dumps(bd(PAIRS))) emit('pickle_eq', yn(pk == dict(PAIRS))) emit('pickle_inv_works', pk.inverse['lithium']) pki = pickle.loads(pickle.dumps(bd(PAIRS).inverse)) emit('pickle_inv_eq', yn(pki == {v: k for k, v in PAIRS})) else: for _k in ('pickle_eq', 'pickle_inv_works', 'pickle_inv_eq'): emit(_k, 'no-pickle') # `reversed()' WORKS because the backing mappings are `dict', which is # reversible -- `_set_reversed' decides this per class at definition time. emit('reversed_keys', ','.join(reversed(bd(PAIRS)))) # 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. emit('is_bidirectional', yn(isinstance(b, bidict.BidirectionalMapping))) emit('is_mutable_bidirectional', yn(isinstance(b, bidict.MutableBidirectionalMapping))) emit('frozen_is_bidirectional', yn(isinstance( frozenbidict(PAIRS), bidict.BidirectionalMapping))) emit('frozen_is_mutable', yn(isinstance( frozenbidict(PAIRS), bidict.MutableBidirectionalMapping))) # THE ABC IS ABSTRACT -- `inverse' is an `abstractmethod', so a subclass that # did not override it cannot be instantiated. emit('abc_abstract_error', err(bidict.BidirectionalMapping)) # ----------------------------------------------------------- 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. fb = frozenbidict(PAIRS) emit('frozen_len', str(len(fb))) emit('frozen_fwd', fb['H']) emit('frozen_inv', fb.inverse['helium']) emit('frozen_hashable', yn(err(hash, fb) == 'none')) # THE HASH IS THE ITEMS' -- `t.ItemsView(self)._hash()', which is documented as # "like `hash(frozenset(self.items()))' but more memory efficient". Asserted # against that equality rather than against a literal, since a hash literal is # not stable across interpreter builds. emit('frozen_hash_is_items', yn(hash(fb) == hash(frozenset(fb.items())))) # ORDER DOES NOT AFFECT THE HASH, which follows from the frozenset and is what # makes two equal frozenbidicts interchangeable as dict keys. emit('frozen_hash_order_free', yn( hash(frozenbidict(PAIRS)) == hash(frozenbidict(list(reversed(PAIRS)))))) # 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. emit('frozen_hash_cached', yn(getattr(fb, '_hash', None) == hash(fb))) emit('frozen_hash_stable', yn(hash(fb) == hash(fb))) # USABLE AS A DICT KEY, which is the whole point of the two cases above. emit('frozen_as_dict_key', {frozenbidict(PAIRS): 'ok'}[frozenbidict(PAIRS)]) # IMMUTABLE, asserted operation by operation. There is no `__setitem__', # `__delitem__', `update' or `clear' on `BidictBase', so each of these is a # `TypeError' or an `AttributeError' -- and WHICH it is depends on whether the # operation goes through a dunder (TypeError from the protocol) or a named method # (AttributeError from the lookup). Pinned separately for that reason. emit('frozen_setitem_error', err(fb.__setitem__, 'B', 'boron') if hasattr(fb, '__setitem__') else 'no-attr') emit('frozen_has_setitem', yn(hasattr(fb, '__setitem__'))) emit('frozen_has_delitem', yn(hasattr(fb, '__delitem__'))) emit('frozen_has_update', yn(hasattr(fb, 'update'))) emit('frozen_has_clear', yn(hasattr(fb, 'clear'))) emit('frozen_has_put', yn(hasattr(fb, 'put'))) emit('frozen_has_forceput', yn(hasattr(fb, 'forceput'))) def _setitem(x, k, v): x[k] = v emit('frozen_store_error', err(_setitem, fb, 'B', 'boron')) emit('frozen_del_error', err(lambda: fb.__delitem__('H'))) emit('frozen_unchanged', yn(dict(fb.items()) == FIX)) # THE INVERSE OF A frozenbidict IS ALSO FROZEN -- and hashable. An inverse that # came back mutable would be a way round the immutability above. emit('frozen_inv_class', type(fb.inverse).__name__) emit('frozen_inv_hashable', yn(err(hash, fb.inverse) == 'none')) # A PLAIN `bidict' IS NOT HASHABLE, which is the contrast that makes the above # mean something: `__hash__' is set to None by `MutableMapping'. emit('bidict_hashable', yn(err(hash, bd(PAIRS)) == 'none')) # 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 because the mutable one is not hashable at all. emit('frozen_eq_bidict', yn(frozenbidict(PAIRS) == bd(PAIRS))) # ----------------------------------------------------------- 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: the # inverse is backed by the same linked list read from the other end. ob = OrderedBidict(PAIRS) emit('ord_keys', ','.join(ob.keys())) emit('ord_values', ','.join(ob.values())) emit('ord_items', ','.join('%s:%s' % kv for kv in ob.items())) emit('ord_inv_keys', ','.join(ob.inverse.keys())) emit('ord_inv_items', ','.join('%s:%s' % kv for kv in ob.inverse.items())) emit('ord_reversed', ','.join(reversed(ob))) emit('ord_reversed_items', ','.join('%s:%s' % kv for kv in reversed(ob.items()))) emit('ord_class', type(ob).__name__) emit('ord_inv_class', type(ob.inverse).__name__) # 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. ob['B'] = 'boron' emit('ord_after_append', ','.join(ob.keys())) emit('ord_inv_after_append', ','.join(ob.inverse.keys())) # 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. ob['He'] = 'helium-3' emit('ord_after_replace', ','.join(ob.keys())) emit('ord_inv_after_replace', ','.join(ob.inverse.keys())) # `move_to_end' BOTH WAYS, and it moves the item in the inverse's order too. ob.move_to_end('H') emit('ord_move_last', ','.join(ob.keys())) ob.move_to_end('H', last=False) emit('ord_move_first', ','.join(ob.keys())) emit('ord_move_missing_error', err(ob.move_to_end, 'Xe')) # `popitem' TAKES FROM EITHER END -- `last=True' by default, unlike `dict', and # the parameter is what an `OrderedDict' user reaches for. emit('ord_popitem_last', '%s:%s' % ob.popitem()) emit('ord_popitem_first', '%s:%s' % ob.popitem(last=False)) emit('ord_after_popitems', ','.join(ob.keys())) emit('ord_popitem_empty_error', err(OrderedBidict().popitem)) # `==' IS ORDER-INSENSITIVE, INHERITED, and `equals_order_sensitive' is the # opt-in. This is the trap the package documents: two OrderedBidicts in # different orders ARE equal, so a test relying on `==' to check ordering # silently passes. Both asserted, as a pair. oa = OrderedBidict(PAIRS) obb = OrderedBidict(list(reversed(PAIRS))) emit('ord_eq_order_insensitive', yn(oa == obb)) emit('ord_eq_order_sensitive', yn(oa.equals_order_sensitive(obb))) emit('ord_eq_order_sensitive_self', yn(oa.equals_order_sensitive( OrderedBidict(PAIRS)))) emit('ord_eq_plain_dict', yn(oa == dict(PAIRS))) # 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 type names are the assertion. emit('ord_keys_view_type', type(oa.keys()).__name__) emit('ord_items_view_type', type(oa.items()).__name__) # AND THEY ARE STILL SET-LIKE, which the override had to preserve by proxying # every `Set' method to the backing dict's view. emit('ord_keys_set_op', ','.join(sorted(oa.keys() & {'H', 'Li', 'Xe'}))) emit('ord_keys_eq', yn(oa.keys() == {'H', 'He', 'Li', 'Be'})) # ORDER SURVIVES `clear' + REFILL, which is the case that catches a `clear' that # emptied the mappings and left the linked list behind. oc = OrderedBidict(PAIRS) oc.clear() emit('ord_clear_len', str(len(oc))) oc['X'] = 'xenon' oc['Y'] = 'yttrium' emit('ord_after_clear_keys', ','.join(oc.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' (the `rollback' default) work at all. emit('ondup_is_tuple', yn(isinstance(bidict.ON_DUP_DEFAULT, tuple))) emit('ondup_fields', ','.join(OnDup._fields)) emit('ondup_default_repr', repr(bidict.ON_DUP_DEFAULT)) emit('ondup_raise_repr', repr(bidict.ON_DUP_RAISE)) emit('ondup_drop_old_repr', repr(bidict.ON_DUP_DROP_OLD)) emit('ondup_action_names', ','.join(a.name for a in bidict.OnDupAction)) emit('ondup_raise_is_singleton', yn(bidict.RAISE is bidict.OnDupAction.RAISE)) emit('ondup_raise_in_default', yn(bidict.RAISE in bidict.ON_DUP_DEFAULT)) emit('ondup_raise_in_drop_old', yn(bidict.RAISE in bidict.ON_DUP_DROP_OLD)) # THE DEFAULTS OF `OnDup' ITSELF, which is what a caller constructing a partial # policy gets for the field it left out. emit('ondup_bare_key', OnDup().key.name) emit('ondup_bare_val', OnDup().val.name) # ------------------------------------------------------------- interpreter --- # WHICH ARM THIS TRANSCRIPT IS FROM, so a control run and a target run are # distinguishable in the capture without reading the surrounding log. # # `py_maxsize_bits' is the ONE value in this file the suite does not compare # against the control: it reads 64 there and must read 32 here. Without it a # suite run against a HOST copy of `bidict' would pass in full, since every other # value here is arm-independent by construction. emit('py_version', '%d.%d' % sys.version_info[:2]) emit('py_maxsize_bits', str(sys.maxsize.bit_length() + 1)) emit('emitted_count', str(len(emitted) + 2)) emit('probe_done', 'yes') sys.exit(0)