"""Drive an OrderedSet through its whole surface, and print one key=value line. ONE INTERPRETER START DOES THE WHOLE OF IT. Every python3 start on this arm is a fresh load of one large static image (99-python3 records the cost), so the suite that drives this asserts on the OUTPUT rather than calling python3 once per case. Every line is `key=value`; a key is emitted at most once. The suite reads the keys it names and ignores the rest, so adding a fact here cannot break it. Failure is reported as a key too -- `*_error` -- rather than only as an exit status, because the case that reds should say which stage stopped and why. CONTENTS ARE EMITTED AS A JSON LIST, NEVER AS A repr, AND NEVER SORTED. This package's entire reason to exist is that its iteration order is a property of the insertion history rather than of the hash values, so a sorted dump would assert the one thing that cannot distinguish it from `set'. `json.dumps(list(oset))' keeps the order and gives a stable text for elements of mixed type; the class's own repr is asserted separately, once, as a fact about the repr. EXCEPTIONS ARE EMITTED AS THE CLASS NAME, and the message beside it only where the message is the case. Three of the failure paths here raise a class that neither the docstring nor this ticket predicted, so the class is the measurement and `' is the value that says nothing was raised. """ import json import pickle import sys _seen = set() def emit(key, value): if key in _seen: return _seen.add(key) # One line per fact: the suite greps by key, and an embedded newline would # land as a second, unkeyed line. print("%s=%s" % (key, str(value).replace("\n", " ").replace("\r", " "))) def fail(key, exc): emit(key, "%s: %s" % (type(exc).__name__, exc)) raise SystemExit(1) def jout(value): return json.dumps(value) def raises(key, fn, with_message=False): """Emit the exception CLASS a call raises, or `' with its result.""" try: result = fn() except Exception as exc: # noqa: BLE001 - reported, not raised emit(key, "%s: %s" % (type(exc).__name__, exc) if with_message else type(exc).__name__) return emit(key, "") emit(key + "_result", jout(result) if isinstance(result, (list, dict)) else result) # ------------------------------------------------------------------ the import try: import ordered_set from ordered_set import OrderedSet except Exception as exc: # noqa: BLE001 fail("import_error", exc) emit("version", ordered_set.__version__) emit("module_file", getattr(ordered_set, "__file__", "")) # A PACKAGE directory, not a single module -- flit's `[tool.flit.module] name' # names a directory here, and the `py.typed' marker beside __init__.py only has # meaning inside one. Every import case would pass either way. emit("module_is_package", "yes" if hasattr(ordered_set, "__path__") else "no") # The installed metadata, read independently of the module's own __version__. The # two disagreeing means a mixed install -- a dist-info from one version beside a # module from another. # # BOTH SPELLINGS ARE ASKED FOR. The distribution is `ordered-set' with a HYPHEN # and the module is `ordered_set' with an UNDERSCORE, which is the one packaging # fact about this port a caller can get wrong without the import telling them. try: from importlib.metadata import metadata as _dist_metadata from importlib.metadata import version as _dist_version emit("dist_version", _dist_version("ordered-set")) emit("dist_version_underscore", _dist_version("ordered_set")) emit("dist_name", _dist_metadata("ordered-set")["Name"]) except Exception as exc: # noqa: BLE001 emit("dist_version_error", "%s: %s" % (type(exc).__name__, exc)) # THE PUBLIC NAMES, ONE KEY EACH -- NOT dir() AS A WHOLE. # # The module declares no __all__, so dir() reports what is BOUND rather than what # is exported: `it', `json'-free but `typing' names, `SLICE_ALL', `T', `SetLike' # and `OrderedSetInitializer'. Asserting that list would pin the implementation's # import statements into a case that is supposed to be about the API. emit("has_all", "yes" if hasattr(ordered_set, "__all__") else "no") for _n in ("OrderedSet", "SLICE_ALL", "SetLike", "OrderedSetInitializer"): emit("has_" + _n, "yes" if hasattr(ordered_set, _n) else "no") for _n in ("add", "append", "update", "index", "get_loc", "get_indexer", "pop", "discard", "remove", "clear", "copy", "union", "intersection", "difference", "symmetric_difference", "issubset", "issuperset", "difference_update", "intersection_update", "symmetric_difference_update"): emit("meth_" + _n, "yes" if hasattr(OrderedSet, _n) else "no") # The pandas-compatibility aliases and `append'. Each is the SAME function object, # not a wrapper, and nothing behavioural would notice if one were rebound. emit("append_is_add", "yes" if OrderedSet.append is OrderedSet.add else "no") emit("get_loc_is_index", "yes" if OrderedSet.get_loc is OrderedSet.index else "no") emit("get_indexer_is_index", "yes" if OrderedSet.get_indexer is OrderedSet.index else "no") # IT IS A SET AND A SEQUENCE AT ONCE, and that is the whole claim of the package. # The MRO is asserted rather than only the two isinstance answers because the # ORDER of MutableSet before Sequence is what decides which mixin supplies a # method the class does not define itself. try: from collections.abc import MutableSet as _MutableSet from collections.abc import Sequence as _Sequence emit("is_mutableset", "yes" if isinstance(OrderedSet(), _MutableSet) else "no") emit("is_sequence", "yes" if isinstance(OrderedSet(), _Sequence) else "no") emit("mro", "|".join(c.__name__ for c in OrderedSet.__mro__)) except Exception as exc: # noqa: BLE001 emit("abc_error", "%s: %s" % (type(exc).__name__, exc)) # ------------------------------------------------- the mutating sequence, in order # # ONE SET, DRIVEN THROUGH A FIXED SEQUENCE, WITH THE FULL CONTENTS ASSERTED AFTER # EVERY STEP. This is the property that distinguishes the type from `set' and the # one a wrong implementation loses first: any of these operations can leave the # right MEMBERS in the wrong ORDER, and a membership-only assertion would pass. _s = OrderedSet(["a", "b", "c"]) emit("seq_00_init", jout(list(_s))) # `add' returns the INDEX, which is not what MutableSet.add promises (None), and # a second add of the same key returns the index it already had without moving it. emit("seq_01_add_new", _s.add("d")) emit("seq_01_after", jout(list(_s))) emit("seq_02_add_existing", _s.add("b")) emit("seq_02_after", jout(list(_s))) # `discard' renumbers the index map of everything after the hole -- see the # pop-vs-discard group below, where the same claim does NOT hold for `pop'. _s.discard("b") emit("seq_03_after", jout(list(_s))) emit("seq_03_index_c", _s.index("c")) # `discard' of an absent key is a documented no-op, and it is the method the # MutableSet mixin builds `remove' out of. _s.discard("zz") emit("seq_04_after", jout(list(_s))) _s.remove("c") emit("seq_05_after", jout(list(_s))) # `update' returns the index of the LAST element inserted, and re-offered keys do # not move -- so the return value is the index of "f", not of "a". emit("seq_06_update", _s.update(["e", "a", "f", "e"])) emit("seq_06_after", jout(list(_s))) # `pop' with no argument takes the LAST element, which is the only index for # which this implementation's index map survives. emit("seq_07_pop", _s.pop()) emit("seq_07_after", jout(list(_s))) emit("seq_08_len", len(_s)) emit("seq_08_index_e", _s.index("e")) emit("seq_08_contains", "yes" if "a" in _s else "no") emit("seq_08_contains_absent", "yes" if "zz" in _s else "no") # --------------------------------------------------------- the sequence half IDX = OrderedSet(["a", "b", "c", "d"]) emit("get_int", IDX[1]) emit("get_negative", IDX[-1]) # A slice gives back an OrderedSet; a LIST of indices gives back a plain list, # because the caller may ask for an index twice and the answer then has more # elements than a set could hold. The two types are asserted beside the values. emit("get_slice", jout(list(IDX[1:3]))) emit("get_slice_type", type(IDX[1:3]).__name__) emit("get_fancy", jout(IDX[[2, 0, 2]])) emit("get_fancy_type", type(IDX[[2, 0, 2]]).__name__) # `oset[:]' is special-cased to `copy()' rather than falling through the slice # path -- equal to the original and a DIFFERENT object. emit("get_all", jout(list(IDX[:]))) emit("get_all_is_same", "yes" if IDX[:] is IDX else "no") emit("get_all_equal", "yes" if IDX[:] == IDX else "no") emit("index_present", IDX.index("c")) # A non-atomic iterable of keys gives a LIST of indices -- pandas' `get_indexer' # shape. A str or a tuple is ATOMIC and is looked up whole, which is the whole # job of the module's `_is_atomic'. emit("index_fancy", jout(IDX.index(["c", "a"]))) emit("index_str_atomic", OrderedSet(["ab", "c"]).index("ab")) emit("index_tuple_atomic", OrderedSet([("a", "b"), "c"]).index(("a", "b"))) emit("reversed", jout(list(reversed(IDX)))) emit("iter", jout(list(iter(IDX)))) emit("len", len(IDX)) # ------------------------------------------------------------- the set half # # EACH OPERATION OVER THREE ARGUMENT TYPES -- a plain set, a list and another # OrderedSet -- IN BOTH THE OPERATOR AND THE METHOD FORM, with the ORDER of the # result asserted and not only its membership. The order is the part no `set' # would reproduce, and it differs per operation: `union' is first appearance # across self then the arguments, `intersection' and `difference' are self's # order, and `symmetric_difference' is self's survivors followed by other's. A = OrderedSet([3, 1, 4, 5, 7]) B_SET = {7, 1, 3, 9, 2} B_LIST = [9, 7, 1, 3, 2] B_OSET = OrderedSet([9, 7, 1, 3, 2]) for _label, _other in (("set", B_SET), ("list", B_LIST), ("oset", B_OSET)): emit("union_m_" + _label, jout(list(A.union(_other)))) emit("union_o_" + _label, jout(list(A | _other))) emit("inter_m_" + _label, jout(list(A.intersection(_other)))) emit("inter_o_" + _label, jout(list(A & _other))) emit("diff_m_" + _label, jout(list(A.difference(_other)))) emit("diff_o_" + _label, jout(list(A - _other))) emit("symdiff_m_" + _label, jout(list(A.symmetric_difference(_other)))) emit("symdiff_o_" + _label, jout(list(A ^ _other))) emit("type_union_" + _label, type(A.union(_other)).__name__) # A `set' argument has no order of its own, so `union' over one appends its # members in the SET's iteration order -- which is a property of the hash values, # not of anything this package chose. The union-over-a-set case above is # therefore asserted by the suite as a SORTED tail rather than pinned; this key # says whether the head (self's own order) survived, which is the part that is # this package's to get right. emit("union_set_head", jout(list(A.union(B_SET))[:5])) emit("union_set_tail_sorted", jout(sorted(list(A.union(B_SET))[5:]))) # `symmetric_difference' is `cls(other).difference(self)' for its second half, so # it inherits the same set-order dependence in its TAIL and none in its head. emit("symdiff_set_head", jout(list(A.symmetric_difference(B_SET))[:2])) emit("symdiff_set_tail_sorted", jout(sorted(list(A.symmetric_difference(B_SET))[2:]))) # The operator and the method are the SAME call over a plain set as over anything # else -- asserted as an agreement rather than against a literal, since the two # literals would be the set-ordered ones the two keys above decline to pin. emit("set_forms_agree", "yes" if all(( list(A | B_SET) == list(A.union(B_SET)), list(A & B_SET) == list(A.intersection(B_SET)), list(A - B_SET) == list(A.difference(B_SET)), list(A ^ B_SET) == list(A.symmetric_difference(B_SET)), )) else "no") # Variadic: more than one argument, and none at all. emit("union_multi", jout(list(OrderedSet([3, 1, 4, 1, 5]).union([1, 3], [2, 0])))) emit("inter_multi", jout(list(OrderedSet([0, 1, 2, 3]).intersection([1, 2, 3], [2, 3, 9])))) emit("diff_multi", jout(list(OrderedSet([1, 2, 3, 4, 5]).difference([2, 4], [1, 4, 6])))) emit("union_noargs", jout(list(A.union()))) emit("inter_noargs", jout(list(A.intersection()))) emit("diff_noargs", jout(list(A.difference()))) # `&' is the ONE operator this class overrides -- the mixin's version computes the # intersection the other way round and would give `other''s order. Asserted # against the method it delegates to, over an argument whose order disagrees. emit("and_equals_intersection", "yes" if list(A & B_OSET) == list(A.intersection(B_OSET)) else "no") emit("and_not_reversed_order", "yes" if list(A & B_OSET) != list(B_OSET.intersection(A)) else "no") emit("issubset_yes", "yes" if OrderedSet([1, 2, 3]).issubset({1, 2, 3, 4}) else "no") emit("issubset_no", "yes" if OrderedSet([1, 2, 3]).issubset({1, 4, 3, 5}) else "no") emit("issuperset_yes", "yes" if OrderedSet([1, 2, 3, 4]).issuperset({1, 2, 3}) else "no") emit("issuperset_no", "yes" if OrderedSet([1, 2]).issuperset([1, 2, 3]) else "no") # --------------------------------------------------- the in-place set updates _d = OrderedSet([1, 2, 3, 4, 5]) _d.difference_update([2, 4], [1, 4, 6]) emit("difference_update", jout(list(_d))) emit("difference_update_index", jout([_d.index(x) for x in _d])) _i = OrderedSet([1, 4, 3, 5, 7]) _i.intersection_update([9, 7, 1, 3, 2]) emit("intersection_update", jout(list(_i))) emit("intersection_update_index", jout([_i.index(x) for x in _i])) _x = OrderedSet([1, 4, 3, 5, 7]) _x.symmetric_difference_update([9, 7, 1, 3, 2]) emit("symdiff_update", jout(list(_x))) emit("symdiff_update_index", jout([_x.index(x) for x in _x])) # All three rebuild the index map wholesale through `_update_items', which is why # each is followed above by a full index sweep: it is the same map `pop' below # leaves wrong, and these are the paths that get it right. _c = OrderedSet([1, 2]) _c.clear() emit("clear", jout(list(_c))) emit("clear_len", len(_c)) # ------------------------------------------------------- equality, asymmetric # # THE COMPARISON IS DELIBERATELY ASYMMETRIC IN WHAT IT IGNORES. Against anything # the abc registers as a Sequence -- a list, a tuple, ANOTHER OrderedSet -- order # is compared. Against a `set' or a frozenset it is not, because those have no # order to compare. So the same two collections can be equal or unequal depending # on which container the caller reached for, and that is documented behaviour. E = OrderedSet([1, 3, 2]) emit("eq_set_reordered", "yes" if E == {1, 2, 3} else "no") emit("eq_frozenset_reordered", "yes" if E == frozenset([1, 2, 3]) else "no") emit("eq_list_same_order", "yes" if E == [1, 3, 2] else "no") emit("eq_list_reordered", "yes" if E == [1, 2, 3] else "no") emit("eq_tuple_same_order", "yes" if E == (1, 3, 2) else "no") emit("eq_tuple_reordered", "yes" if E == (1, 2, 3) else "no") # An OrderedSet IS a Sequence, so two of them compare BY ORDER -- the answer a # reader is most likely to get backwards, since both sides are sets. emit("eq_oset_same_order", "yes" if E == OrderedSet([1, 3, 2]) else "no") emit("eq_oset_reordered", "yes" if E == OrderedSet([3, 2, 1]) else "no") # The reflected direction: `set.__eq__' returns NotImplemented against a type it # does not know, so Python falls back to THIS class's __eq__ and the order-free # answer holds from the left-hand side too. emit("eq_set_reflected", "yes" if {1, 2, 3} == E else "no") emit("eq_missing_member", "yes" if E == {1, 2} else "no") # A non-iterable cannot be converted to a set, and the comparison answers False # rather than raising -- which is what `==' is required to do. emit("eq_noniterable", "yes" if E == 5 else "no") # ------------------------------------------------------------ the failure paths # # EACH RAISES A CLASS, AND THE CLASS IS THE MEASUREMENT. Two of these are not the # class the docstring or the obvious reading predicts, so the suite pins the class # rather than merely asserting that something was raised. raises("fail_remove_absent", lambda: OrderedSet([1, 2, 3]).remove(9)) # `index' resolves through `self.map[key]', so an absent key comes out as the # dict's KeyError. The method's own docstring says IndexError; it is neither that # nor the ValueError `list.index' raises, and no caller's `except' clause written # from either would catch it. raises("fail_index_absent", lambda: OrderedSet([1, 2, 3]).index(9)) raises("fail_index_absent_msg", lambda: OrderedSet(["ab"]).index("zz"), with_message=True) raises("fail_pop_empty", lambda: OrderedSet().pop(), with_message=True) # Out of range is the LIST's error, from `self.items[index]', and a different # class from the empty-set case immediately above it. raises("fail_pop_out_of_range", lambda: OrderedSet([1]).pop(5)) # `update' catches TypeError and re-raises it as ValueError with its own message # -- the only place in the class that converts an exception. raises("fail_update_noniterable", lambda: OrderedSet([1]).update(5), with_message=True) # The module's OWN "Don't know how to index" TypeError, which is reachable only # for an argument that is neither iterable nor integer-like. raises("fail_getitem_float", lambda: OrderedSet([1, 2, 3])[1.5], with_message=True) raises("fail_getitem_none", lambda: OrderedSet([1, 2, 3])[None], with_message=True) # ...and NOT for a str, which IS iterable, so the branch above it wins and the # error comes from indexing the backing list with a character. The module's own # message is unreachable here. raises("fail_getitem_str", lambda: OrderedSet([1, 2, 3])["x"], with_message=True) # An OrderedSet is mutable, so it is unhashable -- it cannot be a member of # another set, which is worth stating because `set' operations accept it happily # as an ARGUMENT. raises("fail_hash", lambda: hash(OrderedSet([1, 2]))) # THE OPERATOR AND THE METHOD DIFFER ONLY HERE. Over any iterable they agree # exactly (the set-half group above measures that). Handed a NON-iterable the # operator returns NotImplemented and Python raises "unsupported operand", while # the method reaches `set(...)' and raises "not iterable" -- two different # messages for the same mistake. raises("fail_or_int", lambda: OrderedSet([1, 2]) | 5, with_message=True) raises("fail_union_int", lambda: OrderedSet([1, 2]).union(5), with_message=True) raises("fail_sub_int", lambda: OrderedSet([1, 2]) - 5, with_message=True) raises("fail_difference_int", lambda: OrderedSet([1, 2]).difference(5), with_message=True) # `&' is overridden onto the method, so it is the one operator that raises the # METHOD's error rather than "unsupported operand" -- the asymmetry above does # not hold for it. raises("fail_and_int", lambda: OrderedSet([1, 2]) & 5, with_message=True) # ------------------------------------------- pop(index) leaves the map stale # # A DEFECT IN 4.1.0, MEASURED AND ASSERTED AS THE BEHAVIOUR THIS IMAGE SHIPS. # `discard' walks the map and decrements every index at or after the hole; # `pop' deletes the item and its own map entry AND DOES NOT RENUMBER. So after # `pop(0)' every surviving element's index is one too high: `index(x)' answers a # stale position and `oset[oset.index(x)]' -- the round trip the package's whole # claim rests on -- returns the WRONG element, or raises IndexError for the last # one. `pop()' with no argument takes the last element, where there is nothing # after the hole to renumber, which is why the defect stays invisible in ordinary # use and why the sequence group above is safe. _p = OrderedSet([1, 2, 3]) emit("pop0_returned", _p.pop(0)) emit("pop0_items", jout(list(_p))) emit("pop0_len", len(_p)) emit("pop0_index_2", _p.index(2)) # The round trip, spelled out: 2 is at position 0 and the map answers 1. emit("pop0_roundtrip_2", _p[_p.index(2)]) raises("pop0_roundtrip_3", lambda: _p[_p.index(3)]) # Iteration and membership are read off `items' and stay correct throughout, so # nothing but an index-based read can see this. emit("pop0_contains_2", "yes" if 2 in _p else "no") emit("pop0_iter", jout(list(_p))) # The same sequence through `discard', which is correct -- the pair is what makes # the case above a statement about `pop' rather than about the class. _q = OrderedSet([1, 2, 3]) _q.discard(1) emit("discard_items", jout(list(_q))) emit("discard_index_2", _q.index(2)) emit("discard_roundtrip_2", _q[_q.index(2)]) # ...and `pop()' of the last element, which is also correct. _r = OrderedSet([1, 2, 3]) _r.pop() emit("poplast_items", jout(list(_r))) emit("poplast_index_2", _r.index(2)) emit("poplast_roundtrip_2", _r[_r.index(2)]) # --------------------------------------------------------- construction, repr emit("init_none", jout(list(OrderedSet(None)))) emit("init_empty", jout(list(OrderedSet()))) # A str is iterable, so it is consumed CHARACTER BY CHARACTER and de-duplicated # -- the constructor does not treat it as atomic, unlike `index'. emit("init_str", jout(list(OrderedSet("hello")))) emit("init_dedup", jout(list(OrderedSet([1, 1, 2, 3, 2])))) emit("init_dict_keys", jout(list(OrderedSet({"b": 1, "a": 2})))) emit("repr_empty", repr(OrderedSet())) emit("repr_nonempty", repr(OrderedSet([1, 2, 3]))) emit("str_nonempty", str(OrderedSet([1, 2, 3]))) emit("bool_empty", "yes" if bool(OrderedSet()) else "no") # A one-element set holding a FALSY element is truthy: the truth value comes from # __len__, and 0 is a perfectly good member. emit("bool_zero_member", "yes" if bool(OrderedSet([0])) else "no") _orig = OrderedSet([1, 2]) _cp = _orig.copy() emit("copy_equal", "yes" if _cp == _orig else "no") emit("copy_is_same", "yes" if _cp is _orig else "no") emit("copy_type", type(_cp).__name__) # Mutating the copy leaves the original alone -- a `copy' that returned self # would satisfy both keys above and fail this one. _cp.add(3) emit("copy_independent", jout(list(_orig))) # ------------------------------------------------------------------- pickling # # The class hand-writes __getstate__/__setstate__ for ONE reason: an empty set's # natural state is `[]', which is falsy, and pickle skips __setstate__ entirely # for a falsy state -- so an empty OrderedSet would come back UNINITIALISED. # The sentinel `(None,)' is that workaround, and it is asserted directly because # a round trip alone would not say which mechanism carried it. try: emit("getstate_empty", jout(list(OrderedSet().__getstate__()))) emit("getstate_nonempty", jout(OrderedSet([1, 2, 3]).__getstate__())) _pk = pickle.loads(pickle.dumps(OrderedSet([3, 1, 2]))) emit("pickle_roundtrip", jout(list(_pk))) emit("pickle_roundtrip_type", type(_pk).__name__) _pke = pickle.loads(pickle.dumps(OrderedSet())) emit("pickle_empty_len", len(_pke)) emit("pickle_empty_usable", _pke.add("x")) except Exception as exc: # noqa: BLE001 emit("pickle_error", "%s: %s" % (type(exc).__name__, exc)) emit("python_version", "%d.%d" % sys.version_info[:2]) emit("probe_complete", "yes")