"""Resolve the RFC 6901 pointers against the document RFC 6901 §5 defines, and print one key=value per 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. THE VECTORS ARE THE STANDARD'S OWN, NOT INVENTED ONES. RFC 6901 §5 publishes a document and the twelve pointers that address it, together with the value each one must yield. That table is what this probe evaluates, so a red here is a divergence from the STANDARD rather than from a fixture somebody wrote to match the implementation. The document is inline for the same reason the expectations are literals: a shipped .json could drift from the table by an editor's trailing newline and turn a correct library into a red. VALUES ARE EMITTED AS json.dumps WITH SORTED KEYS, NEVER AS A repr. `resolve' can return a dict or a list, and a Python repr of either carries quoting that is Python's rather than JSON's -- and for the whole-document pointer it would carry DICT ORDER, which is insertion order and therefore a property of the parse rather than of the pointer. sort_keys makes the whole-document case an assertion about CONTENT; a repr case would have been pinning the parser. """ import hashlib import json 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) # ------------------------------------------------------------------ the import try: import jsonpointer from jsonpointer import (resolve_pointer, set_pointer, JsonPointer, JsonPointerException, EndOfList, escape, unescape) except Exception as exc: # noqa: BLE001 - reported, not raised fail("import_error", exc) emit("version", jsonpointer.__version__) emit("module_file", getattr(jsonpointer, "__file__", "")) # 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. try: from importlib.metadata import version as _dist_version emit("dist_version", _dist_version("jsonpointer")) 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. # # This module declares NO __all__ (unlike python3-elementpath, whose suite can # therefore assert its whole surface as one string). Without one, dir() reports # what is BOUND rather than what is exported, and that includes this module's own # imports -- `re', `copy', `chain', `tee', `Mapping', `Sequence', and the Python-2 # compatibility shims `izip' and `encode_str'. Asserting that list would be # pinning the implementation's import statements: a maintainer replacing # `itertools.tee' with something else would red a case that is supposed to be # about the API. So each name this port actually promises is asserted on its own. for _n in ("resolve_pointer", "set_pointer", "JsonPointer", "JsonPointerException", "EndOfList", "escape", "unescape"): emit("has_" + _n, "yes" if hasattr(jsonpointer, _n) else "no") # JsonPointerException's base, which is what a caller's `except' clause has to # name. A release that re-based it on ValueError or KeyError would leave every # resolve case below passing while silently breaking every caller that catches # it -- and the suite's own `miss-raised' case would still pass, because it # catches the class by name. emit("exc_base", JsonPointerException.__mro__[1].__name__) # ============================================================================ # THE FIXTURE: RFC 6901 §5, VERBATIM. # ============================================================================ # # The escaping here is JSON's, applied once, in a Python raw string -- so what # the parser sees is exactly the bytes §5 prints. `i\\j' is the two characters # `i' `\' `j'; `k\"l' is `k' `"' `l'. Getting this wrong is the one way the whole # table could be self-consistently wrong, so the document is pinned by length and # digest below and the suite asserts both before reading any result. RFC6901_DOC = r'''{ "foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }''' emit("fixture_len", len(RFC6901_DOC)) emit("fixture_sha", hashlib.sha256(RFC6901_DOC.encode("utf-8")).hexdigest()[:16]) try: DOC = json.loads(RFC6901_DOC) except Exception as exc: # noqa: BLE001 fail("fixture_parse_error", exc) # The ten keys the document defines, sorted, as the statement that the fixture # parsed into what §5 describes. If this key is wrong then every vector below is # being resolved against the wrong document, and each would red separately with # no indication of the common cause. # # A JSON ARRAY, not the `|'-joined form the other multi-valued keys here use. # One of the ten keys IS `g|h', so a `|' join would be ambiguous exactly where # this document is interesting -- `g|h' and the two keys `g', `h' would produce # the same string. json.dumps quotes each key and escapes the `\' and `"' ones, # which are the other three that a bare join would render unreadable. emit("doc_keys", json.dumps(sorted(DOC.keys()))) def jout(value): """The canonical JSON form: sorted keys, no incidental whitespace.""" return json.dumps(value, sort_keys=True, separators=(",", ":")) # ============================================================================ # THE TWELVE VECTORS OF RFC 6901 §5. # ============================================================================ # # Each key is `rfc_'; the suite asserts each against the literal §5 publishes # for it. They are numbered rather than named after the pointer because a pointer # containing `/', `~' and a space does not make a usable key. # # `rfc_00' is the WHOLE-DOCUMENT pointer, the empty string -- which is the one # vector a reader is most likely to think is a typo, and the one an # implementation is most likely to get wrong by treating "" as "/". VECTORS = [ "", # 00 -> the whole document "/foo", # 01 -> ["bar", "baz"] "/foo/0", # 02 -> "bar" (an ARRAY INDEX, not a key) "/", # 03 -> 0 (the EMPTY KEY, not the root) "/a~1b", # 04 -> 1 (~1 is `/') "/c%d", # 05 -> 2 (`%' is NOT percent-encoding here) "/e^f", # 06 -> 3 "/g|h", # 07 -> 4 "/i\\j", # 08 -> 5 "/k\"l", # 09 -> 6 "/ ", # 10 -> 7 (a SPACE is an ordinary key character) "/m~0n", # 11 -> 8 (~0 is `~') ] for _i, _p in enumerate(VECTORS): _k = "rfc_%02d" % _i try: emit(_k, jout(resolve_pointer(DOC, _p))) except Exception as exc: # noqa: BLE001 emit(_k, "" % (type(exc).__name__, exc)) # The whole-document pointer returns the document ITSELF, not a copy. `rfc_00' # above asserts the content; this asserts the identity, which is what makes # `set_pointer' on a sub-path observable through the original object. emit("root_is_doc", "yes" if resolve_pointer(DOC, "") is DOC else "no") # ============================================================================ # THE ~0/~1 ESCAPES, AND THE ORDER THEY MUST BE UNDONE IN. # ============================================================================ # # RFC 6901 §4 requires `~1' to be replaced FIRST and `~0' SECOND. The order is # the whole of the rule: undo `~0' first and `~01' becomes `~1' and then `/', # so the token would address a key it must not. `~01' therefore addresses the # literal key `~1', and a document carrying BOTH `~1' and `/' as keys is what # tells a correct implementation from one that reversed the order -- a reversed # one returns the `/' value for `/~01'. ORDER_DOC = {"~1": "tilde-one", "/": "slash"} for _k, _p in (("order_tilde01", "/~01"), ("order_tilde1", "/~1")): try: emit(_k, jout(resolve_pointer(ORDER_DOC, _p))) except Exception as exc: # noqa: BLE001 emit(_k, "" % (type(exc).__name__, exc)) emit("unescape_tilde01", unescape("~01")) emit("escape_slash", escape("a/b")) emit("escape_tilde", escape("m~n")) # escape() is the inverse and must produce the token the order rule needs: the # key `~1' escapes to `~01', which is exactly the vector asserted above. emit("escape_tilde_one", escape("~1")) # ============================================================================ # `%' IS NOT PERCENT-ENCODING. RFC 6901 POINTERS ARE NOT URI FRAGMENTS. # ============================================================================ # # §6 defines a URI-FRAGMENT form in which %-decoding applies; the plain string # form asserted here is not that, and `%20' is three ordinary characters. A # document with both `a%20b' and `a b' as keys separates the two readings: an # implementation that decoded would return the `a b' value for `/a%20b'. §5's own # `c%d' vector is the same point made without a second key, which is why both are # asserted -- that one shows `%' surviving, this one shows it NOT being decoded. PCT_DOC = {"a%20b": 1, "a b": 2} emit("pct_literal", jout(resolve_pointer(PCT_DOC, "/a%20b"))) emit("pct_space", jout(resolve_pointer(PCT_DOC, "/a b"))) # ============================================================================ # THE FAILURE PATHS, AND THE DEFAULT THAT REPLACES THEM. # ============================================================================ # # A pointer naming a member that is not there RAISES; the same pointer with a # default argument RETURNS the default. Both halves are asserted because a build # in which the default silently swallowed a real error would pass the second on # its own. try: _v = resolve_pointer(DOC, "/nope") emit("miss_raised", "no") emit("miss_unexpected_ok", jout(_v)) except JsonPointerException as exc: # this IS the assertion emit("miss_raised", "yes") emit("miss_type", type(exc).__name__) except Exception as exc: # noqa: BLE001 - wrong class is a red emit("miss_raised", "wrong-class") emit("miss_type", type(exc).__name__) # The default is returned for the SAME pointer that raised above, and it is a # sentinel no part of the document holds -- a default of None or 0 would be # indistinguishable from a document value. emit("miss_default", jout(resolve_pointer(DOC, "/nope", "DEFAULT"))) # ...and a default does NOT mask a well-formed hit: the same call over a pointer # that resolves must still return the document's value, not the default. emit("hit_with_default", jout(resolve_pointer(DOC, "/foo/0", "DEFAULT"))) # THE MESSAGE, FOR THE TWO ERRORS WHOSE MESSAGE IS A CONSTANT. # # Only these two and the malformed-pointer one below are asserted as text. The # member-not-found message interpolates the DOCUMENT'S repr -- `member 'nope' not # found in {...}' -- so asserting it would pin dict ordering and the whole # fixture's repr into a case that is supposed to be about the lookup. These three # carry no repr and are stable. for _k, _p in (("err_index_oob", "/foo/9"), ("err_index_notint", "/foo/bar")): try: resolve_pointer(DOC, _p) emit(_k, "") except Exception as exc: # noqa: BLE001 emit(_k, str(exc)) # A pointer that does not start with `/' is MALFORMED, and is refused when the # JsonPointer is constructed rather than when it is resolved -- which is the # distinction between a bad pointer and a pointer that found nothing. Both come # out as JsonPointerException, so the message is what separates them. try: resolve_pointer(DOC, "foo") emit("err_no_leading_slash", "") except Exception as exc: # noqa: BLE001 emit("err_no_leading_slash", str(exc)) # ============================================================================ # `-' IS THE END-OF-LIST TOKEN, AND IT DOES NOT RAISE. # ============================================================================ # # RFC 6901 §4 gives `-' as the index "referencing the nonexistent member after # the last array element". It exists for JSON Patch's `add', so resolving it is # not an error and this implementation returns an EndOfList marker rather than # raising. That is the surprising half -- a reader would expect an out-of-bounds # error, and `/foo/9' above shows what a genuine one looks like -- so the TYPE is # asserted rather than the value. try: emit("dash_type", type(resolve_pointer(DOC, "/foo/-")).__name__) except Exception as exc: # noqa: BLE001 emit("dash_type", "" % type(exc).__name__) # ============================================================================ # THE POINTER OBJECT: PARTS IN, TOKENS OUT, AND BACK AGAIN. # ============================================================================ # # get_parts() returns the UNESCAPED parts and .path re-emits the ESCAPED token # string, so the round trip through from_parts() is what says the two directions # agree. The parts deliberately include one member with a `/' and one with a `~', # because those are the only two characters the round trip can lose. JP = JsonPointer("/a~1b/m~0n/0") emit("parts", "|".join(JP.get_parts())) emit("path", JP.path) emit("str", str(JP)) emit("from_parts", str(JsonPointer.from_parts(["a/b", "m~n", "0"]))) # ...and the rebuilt pointer resolves to the same place the literal one does, # which is the round trip asserted as BEHAVIOUR rather than as a string. emit("from_parts_resolves", jout(resolve_pointer(DOC, str(JsonPointer.from_parts(["a/b"]))))) # ============================================================================ # set_pointer: WRITE THROUGH A POINTER, LEAVE THE REST ALONE. # ============================================================================ # # The addressing half of JSON Patch is only useful if it can WRITE, and the thing # that can go wrong is not the write but the collateral -- a rebuild that # replaces the containing object and drops its siblings. The fixture therefore # carries a sibling key and a second array element that the pointer does not # name, and the whole document is asserted afterwards rather than just the # written value. SET_DOC = {"foo": {"arr": [{"p": 44}, {"q": 9}], "keep": "untouched"}, "top": "also-untouched"} try: _r = set_pointer(SET_DOC, "/foo/arr/0/p", 55) emit("set_result", jout(_r)) # inplace=True is the default, so the ORIGINAL object must show the write. # This is what makes set_pointer usable through a reference somebody else # holds, and it is a different statement from the returned value. emit("set_inplace", jout(SET_DOC)) emit("set_returns_same", "yes" if _r is SET_DOC else "no") except Exception as exc: # noqa: BLE001 emit("set_error", "%s: %s" % (type(exc).__name__, exc)) # inplace=False leaves the original alone, which is the half a caller relies on # when the document is shared. Asserted on a FRESH document so the write above # cannot account for the result. COPY_DOC = {"a": {"b": 1}, "keep": 2} try: _c = set_pointer(COPY_DOC, "/a/b", 99, inplace=False) emit("set_copy_result", jout(_c)) emit("set_copy_original", jout(COPY_DOC)) except Exception as exc: # noqa: BLE001 emit("set_copy_error", "%s: %s" % (type(exc).__name__, exc)) emit("probe_complete", "yes")