. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. # # Every other assertion below reads the filesystem or dpkg # # with shell builtins, or greps the output of one of those # # three. # # # # The three are: a bare `import', a second import whose # # STDERR is the assertion (`import-quiet'), and the probe, # # which takes ONE start for a hundred and thirty # # measurements. # # # # There is NO fourth for a shipped script. Like # # python3-pyparsing and unlike python3-cbor2, this package # # installs no executable at all -- setup.py declares no # # `entry_points' and pyproject.toml no `[project.scripts]' # # -- so there is nothing to reach through a shebang. # # `no-bin-script' below asserts that absence rather than # # leaving it unstated. # # # # The work inside the probe start is ~30 decorations and # # ~250 calls of functions of one or two parameters over # # values under twenty bytes. Measured at 40ms on an x86-64 # # control over a 73ms import -- i.e. the probe body is HALF # # an interpreter start, and the start is what costs on this # # arm, where it has measured at ~60s. # # # # 1200 is 400s per start, the same budget 99-python3-wcwidth # # and 99-python3-pyparsing set for their own three, and # # roughly six times the measured start. Nothing here is # # quadratic in anything: the two sampling loops below are # # 50 and 60 calls of a fixed three-item list. # # # # A suite-timeout red is a batch-wide cost, so if the three # # 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-beartype 0.17.2 (poky/meta # recipes-devtools/python/python3-beartype_0.17.2.bb, `inherit setuptools3 # pypi', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # beartype is a DECORATOR that turns a function's existing annotations into a # runtime contract. `@beartype' reads `def f(x: int) -> str', compiles a wrapper # that checks `x' on the way in and the result on the way out, and raises if # either disagrees. There is no separate schema, no configuration file and no # build step: the annotations a type checker already reads statically become # assertions the interpreter enforces. # # It is written to be O(1) PER CALL, and that constant is what the whole design # is arranged around -- see "THE STRATEGY IS O(1)" below, which is the single # most consequential thing about it for anyone writing a test. # # ============================================================================ # THE CONTRACT HAS EXACTLY TWO HALVES, AND THE SUITE ASSERTS BOTH EVERYWHERE. # ============================================================================ # # A call whose arguments SATISFY the annotations returns normally, and returns # the callee's own object -- not a copy and not a proxy. `return-identity' and # `return-identity-list' assert that with `is', because a decorator that rebuilt # the return value would pass every type assertion in this file and break every # caller that relies on identity. # # A call that VIOLATES a parameter annotation raises # `BeartypeCallHintParamViolation'; one that violates the RETURN annotation # raises `BeartypeCallHintReturnViolation'. The two are SIBLINGS under # `BeartypeCallHintViolation' -- neither is a subclass of the other -- so a # caller catching one does not silently catch the other, and `exc-param-not- # return' and `exc-return-not-param' are the cases that would notice a release # collapsing them. # # ============================================================================ # THE MESSAGES ARE ASSERTED AS FACTS ABOUT THEM, NEVER AS STRINGS. # ============================================================================ # # The ticket asks that the exception message name the offending parameter and # the type it failed. It does: # # Function __main__.contract() parameter x='3' violates type hint # , as str '3' not instance of int. # # But every such message embeds the REPR of the offending object, and for # anything but a scalar that repr carries a heap address -- # `<__main__.Bad object at 0x7f...>'. A case asserting the whole string would be # asserting the address and would red on every second run for a reason that has # nothing to do with beartype. So the probe emits DERIVED facts -- names the # parameter, names the hint, names the type it found -- and the `msg-*' cases # below assert those. `msg-param2-not-first' is what makes them mean something: # it violates the SECOND parameter and asserts the message does NOT name the # first, so an implementation that always said "parameter x" would fail here. # # ============================================================================ # THE STRATEGY IS O(1), AND THAT IS THE TRAP IN TESTING THIS PACKAGE. # ============================================================================ # # For a `list[int]' beartype does NOT check every item. It checks ONE # pseudo-randomly chosen item per call -- that is what `BeartypeStrategy.O1', # the default, buys. So a list with a single bad item raises only SOMETIMES: # measured at 74 of 300 calls on a three-item list with one bad item, which is # 1/3 to three digits. # # A suite that passed a partially-bad container and asserted a violation would # therefore be a COIN FLIP, and would have looked correct for however many runs # it took to lose one. Every deep-check case here passes a container in which # EVERY item is bad, which raises on whatever sample the strategy takes; # `list-allbad-trials' asserts 50 raises out of 50 calls, and # `list-partial-sampled' asserts that the partially-bad list is neither # always-raise nor never-raise over 60 trials -- a fact with a 1e-11 chance of # being wrong in either direction, which is what makes it assertable at all. # # THE INDEX THE MESSAGE NAMES IS NEVER ASSERTED. It is uniform over the list and # changes call to call: 103/103/94 over indices 0/1/2 in 300 measured calls. # # ============================================================================ # DEEP VERSUS SHALLOW: THE FINDING MOST LIKELY TO BE MISREAD AS A PORT BUG. # ============================================================================ # # beartype 0.17.2 deeply checks `list[...]' and `tuple[...]'. It does NOT deeply # check `dict[...]', `set[...]' or `frozenset[...]': for those the annotation # degrades to a plain isinstance() against the container type and the type # parameters are IGNORED. So `dict[str, int]' accepts `{1: "x"}', in which BOTH # the key and the value violate, without a word. # # That is upstream 0.17.2 -- deep dict checking landed in a later beartype -- and # it is not a gap in this port, in this image or in this arm. It is asserted here # in BOTH directions: `dict-bad-both' asserts the acceptance, and `list-allbad' # and `tuple-bad' assert the rejection beside it, so the contrast is on the # record rather than being inferred from an absence. A future beartype that # deepened dicts reds at `dict-bad-both' and gets LOOKED AT, which is the whole # point of asserting a behaviour one does not like. # # `door-dict-bad' puts the same question to `beartype.door.is_bearable', which # reaches the checker with no decorator in the way. Its answering True is what # proves the shallowness belongs to the CHECKER and not to the decorator. # # ============================================================================ # TWO SCALAR ROWS CARRY FINDINGS OF THEIR OWN. # ============================================================================ # # `bool' SATISFIES `int'. Python's bool is a subclass of int and beartype checks # with isinstance(), so `True' passes an `int' annotation. Python's, not # beartype's and not this platform's. # # `int' DOES NOT SATISFY `float'. PEP 484 asks a checker to accept an int where # a float is annotated -- the "implicit numeric tower" -- and beartype 0.17.2 # does not implement it by default. A caller who annotated `-> float' and # returned `0' gets a violation. Upstream's behaviour at this version, and the # one most likely to be reported as a bug by someone coming from mypy. # # ============================================================================ # PROTOCOLS ARE STRUCTURAL, AND SHALLOWLY SO. # ============================================================================ # # A `@runtime_checkable' Protocol is satisfied by anything carrying the named # methods, whatever its ancestry: `protocol-good' and `protocol-unrelated' pass # two classes sharing no base and both are accepted, which is exactly what a # nominal check would refuse. # # `protocol-wrong-signature' PASSES a class whose `close' takes three arguments # where the Protocol declares none, and that is CPython's Protocol rather than # beartype's doing: typing's own `__instancecheck__' looks for the ATTRIBUTE and # never at its signature. Recorded so it is not read as a port defect. # # A Protocol WITHOUT `@runtime_checkable' cannot be used as a hint at all, and # beartype refuses it at DECORATION time with # BeartypeDecorHintPep3119Exception -- before any call. That is the right # behaviour (the alternative is a TypeError from isinstance() at every call # site) and `protocol-not-runtime' records it as a decoration-time failure by # never calling the function. # # ============================================================================ # FORWARD REFERENCES RESOLVE LAZILY, WHICH IS WHY BOTH HALVES ARE ASSERTED. # ============================================================================ # # An annotation given as a STRING is resolved at the first call, not at # decoration -- which is what lets a function be annotated with a class defined # further down the file. The probe decorates `takes_later(o: "Later")' BEFORE # `Later' exists and the decoration succeeds; `forwardref-ok' then calls it. # # An UNRESOLVABLE reference is consequently a CALL-time failure, and it raises # beartype's own `BeartypeCallHintForwardRefException' naming the missing name -- # not a bare NameError. `forwardref-missing-decorated' asserts the decoration # succeeded, which is what makes `forwardref-missing-call' a statement about # laziness rather than about error handling. # # ============================================================================ # TWO PLACES WHERE THE DECORATOR IS THE IDENTITY FUNCTION. # ============================================================================ # # The ticket asks that a function with NO annotations pass through "with no # added cost". It does, in the strongest possible sense: `beartype(f) is f'. # There is no wrapper, so there is no call-time cost to measure because there is # no call-time anything. `unannotated-identity' asserts it with `is', and # `annotated-not-identity' beside it is the contrast that makes it a finding # rather than an accident. # # The same holds under `BeartypeStrategy.O0', which turns checking off: the # decorator hands the function straight back. That is the escape hatch a caller # reaches for on a hot path, and `strategy-o0-identity' says it really is free. # # ============================================================================ # @beartype ON A CLASS DECORATES ITS METHODS IN PLACE. # ============================================================================ # # Applied to a class it returns THE SAME CLASS OBJECT -- not a subclass, not a # wrapper. `class-identity' is asserted first because everything a caller does # afterwards depends on it: isinstance, pickling by qualified name, and any # registry keyed on the class all break under a wrapper. # # All five method kinds are covered, because each reaches beartype by a # different route: a plain method is a function on the class, a staticmethod and # a classmethod are descriptors wrapping one, `__init__' returns None and so # carries a return annotation nothing else does, and a property's getter is # reached through a third descriptor again. `class-*-bad' asserts each of them # ENFORCES; a decoration that reached only plain methods would pass one and fail # three. # # And an UNANNOTATED method inside a decorated class is left ALONE -- the same # identity rule as above, one level in. `class-bare-untouched' asserts it # against the method object captured before the class was decorated. # # ============================================================================ # WHY THIS PORT NEEDED A BBAPPEND, AND WHY ONLY FOR RDEPENDS. # ============================================================================ # # PURE PYTHON, and this image's python3 is ONE STATIC BINARY with an empty # lib-dynload and no dynamic loader (BRINTOS-1476), so a module carrying a # compiled extension could not be imported here at all. beartype has no # ext_modules and ships no `.so'; `pkg-no-so' asserts that off the installed # file list rather than trusting the source, and `dist-info-purelib' takes the # packaging's own word for it as well. # # WHAT THE STOCK RECIPE DECLARES IS NOT ENOUGH TO IMPORT THE MODULE. # setuptools3-base gives RDEPENDS:${PN} += "python3-core" and the recipe adds # nothing. beartype needs NINE more sub-packages of the ported python3, and it # needs them at `import beartype' -- not on a rarely-reached path, but before # the decorator exists. It builds its type registry eagerly at import: # `beartype._cave' enumerates the stdlib types it recognises, and # `beartype._util.module' reaches `importlib.metadata' and # `importlib.resources', the two most import-heavy modules in the standard # library. 153 stdlib modules are resident after `import beartype'. # # Each of the nine was MEASURED, not inferred -- blocked with a `sys.meta_path' # finder that raises on it, `import beartype' re-attempted, the import failed: # # python3-datetime calendar, datetime python3-email email # python3-numbers numbers python3-netclient base64 # python3-mime quopri python3-math random # python3-io socket, tempfile python3-stringold string # python3-compression zipfile # # On THIS image the omission is invisible, because the image installs all of # python3-modules; on any image taking the .deb alone it is an ImportError # before the first decorator is applied. `pkg-depends-*' below asserts each of # the ten names individually, because a bare "the Depends line exists" would # pass on a stanza that had lost one, and the loss would surface as an # ImportError rather than as anything nameable. # # `bz2' and `lzma' are reached by the same closure and are NOT declared: shutil # wraps both in try/except and the import survives without them. Measured the # same way as the nine, and left out for that reason. P=python3-beartype # ============================================================================ # 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/beartype DI=$SITE/beartype-0.17.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-beartype.list PROBE=$BST_DATA/python3-beartype-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-beartype.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE MODULE LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the module misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" # It is a PACKAGE, not a single module. A release that flattened it would still # import and still pass every behavioural case, so the shape is asserted here. check $P/no-flat-module test ! -f "$SITE/beartype.py" # `meta.py' is the project's own single source of truth for its version -- # `__init__.py' reads `__version__' back out of it -- and it is the only # top-level module beside `__init__' that this package ships. check $P/module-meta test -s "$PKG/meta.py" # THE SUBPACKAGES, named individually. `__init__.py' imports from `_decor' and # `_conf', so a truncated install fails at import rather than here; naming them # is what turns "the package is complete" into a measurement the packaging can # be judged by. The five without a leading underscore are the PUBLIC ones a # caller reaches for by name. for _m in roar typing door vale cave claw peps plug; do check $P/subpackage-$_m test -s "$PKG/$_m/__init__.py" done # THE PRIVATE SUBPACKAGES ARE ASSERTED WITH `-f', NOT `-s', AND THAT IS A # FINDING RATHER THAN A LOOSER TEST. Five of the six ship an EMPTY # `__init__.py' -- 0 bytes, measured off the .deb -- because they are package # markers and nothing more: every name in them is imported from a module one # level down. `_decor' is the exception at 12418 bytes, because it carries the # decorator itself. So `-s' would red on five correct files, and the split # below says which shape each one is rather than hiding the difference. for _m in _cave _check _conf _data _util; do check $P/private-subpackage-$_m test -f "$PKG/$_m/__init__.py" check $P/private-subpackage-$_m-empty test ! -s "$PKG/$_m/__init__.py" done check $P/private-subpackage-_decor test -s "$PKG/_decor/__init__.py" # The typing marker, which setuptools ships as package data. Its absence would # not break a single runtime case, which is exactly why it needs its own # assertion: it is the file that tells a static checker this package's own # annotations are meant to be believed. check $P/py-typed test -f "$PKG/py.typed" # NO EXECUTABLE. setup.py declares no `entry_points' and pyproject.toml no # `[project.scripts]', so nothing lands in /usr/bin. Asserted so that a future # release which started shipping one is noticed here. check $P/no-bin-script test ! -e /usr/bin/beartype # --------------------------------------------------------------- dist-info --- # setuptools3 writes METADATA and WHEEL; both are asserted, and the version is # read from METADATA rather than from the module so that a mismatch between what # the packaging claims and what imports is visible as a mismatch. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.17.2" cat "$DI/METADATA" check_out $P/dist-info-name "Name: beartype" cat "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" # PURE PYTHON, stated by the packaging itself, twice over. `Root-Is-Purelib: # true' is the wheel's claim that nothing in it is architecture-specific, and # `Tag: py3-none-any' says the same thing in the filename grammar -- `none' # being the ABI and `any' the platform. That property is what makes this package # portable to this arm at all. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # The one top-level name the distribution installs. A wheel that had also # shipped `beartype_test' -- which the sdist DOES carry and setup.py excludes -- # would say so here. check_eq $P/dist-info-top-level "beartype" "$(cat "$DI/top_level.txt" 2>/dev/null)" check $P/dist-info-license test -s "$DI/LICENSE" # ------------------------------------------------------------ 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" # NO COMPILED EXTENSION, asserted off the shipped file list rather than off the # source. This image's python3 has an empty lib-dynload and no dynamic loader # (BRINTOS-1476), so a single .so here would mean a package that installs # cleanly and cannot import. check_eq $P/pkg-no-so "" \ "$(grep -c '\.so$' "$W/pkgf" 2>/dev/null | grep -v '^0$')" # Every payload path the package ships is under site-packages, and is a .py, a # .pyc, or the typing marker. Anything else is a file this port did not expect # to be shipping and wants looking at. _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) ;; # dist-info metadata. Which of these setuptools writes is a property of the # BUILD BACKEND rather than of this package, so the whole standard set is # allowed: pinning the exact list would red on a setuptools change that says # nothing about beartype. `pkg-no-so' above is the case that carries the # real claim -- nothing compiled ships -- and it is exact. */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE|*/entry_points.txt|*/top_level.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-beartype$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE DEPENDENCY SET IS THE PORT'S CLAIM THAT IT WAITS ON NOTHING. Every name # here is a sub-package of the ported python3 (BRINTOS-1476), so each is # asserted individually -- a bare "the Depends line exists" would pass on a # stanza that had lost one of them, and the loss would surface as an ImportError # at `import beartype' rather than as anything nameable. Nine of the ten come # from this port's own bbappend; see the header for how each was measured. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" for _d in python3-compression python3-core python3-datetime python3-email \ python3-io python3-math python3-mime python3-netclient \ python3-numbers python3-stringold; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done # ------------------------------------------------------------------ import --- # THE FIRST OF THREE PYTHON STARTS. If the RDEPENDS set above were short by one # sub-package, this is the case that would say so -- and before the bbappend it # WAS short by nine. check $P/import python3 -c 'import beartype' # THE SECOND. A package that imports but WARNS on the way in is a different # defect from one that fails, and it is invisible to the case above because the # exit status is 0 either way. The assertion is that stderr is EMPTY. It is not # a formality for this package: beartype issues its own # `BeartypeDecorHintPep585DeprecationWarning' whenever a caller uses a PEP 484 # alias such as `typing.List[int]', so a probe or an import that tripped one # would show up here rather than as a mystery in the transcript. Nothing in the # probe uses those aliases, for that reason. check_eq $P/import-quiet "" \ "$(python3 -c 'import beartype' 2>&1 >/dev/null)" # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" exit 0 fi bst_ok $P/probe-present # THE THIRD START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so that 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. 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 several values 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 the probe 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)" check_eq $P/probe-count "130" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this the behavioural cases below would be # statements about whatever was first on sys.path. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version-attr "0.17.2" "$(pv version_attr)" check_eq $P/meta-version "0.17.2" "$(pv meta_version)" check_eq $P/meta-name "beartype" "$(pv meta_name)" check_eq $P/dist-version "0.17.2" "$(pv dist_version)" # The three are read by three different mechanisms -- an attribute on the # module, a constant in `beartype.meta', and importlib.metadata off the # dist-info -- so their agreement is a real assertion about the install and not # a tautology. check_eq $P/version-agrees-meta "$(pv version_attr)" "$(pv meta_version)" check_eq $P/version-agrees-dist "$(pv version_attr)" "$(pv dist_version)" # THE PUBLIC SURFACE. Four configuration names beside the decorator itself, plus # the three submodules re-exported at the top level. A release that dropped one # would still pass every behavioural case below, because none of the four is # needed to decorate anything. check_eq $P/public-names \ "BeartypeConf,BeartypeHintOverrides,BeartypeStrategy,BeartypeViolationVerbosity,beartype,meta,roar,typing" \ "$(pv public_names)" for _s in roar typing door vale cave claw; do check_eq $P/submodule-$_s "yes" "$(pv submodule_$_s)" done # ---------------------------------------------------------- the exceptions --- check_eq $P/exc-param-name "BeartypeCallHintParamViolation" "$(pv exc_param_name)" check_eq $P/exc-return-name "BeartypeCallHintReturnViolation" "$(pv exc_return_name)" # A caller catching broadly relies on the base classes, so the relationships are # asserted rather than assumed from the names. check_eq $P/exc-param-is-violation "yes" "$(pv exc_param_is_violation)" check_eq $P/exc-return-is-violation "yes" "$(pv exc_return_is_violation)" check_eq $P/exc-param-is-beartype "yes" "$(pv exc_param_is_beartype)" check_eq $P/exc-is-exception "yes" "$(pv exc_is_exception)" # ...and the two violations are SIBLINGS. These are the cases that would notice # a release collapsing them into one, which would silently widen every # `except BeartypeCallHintParamViolation' in the world to cover returns too. check_eq $P/exc-param-not-return "yes" "$(pv exc_param_not_return)" check_eq $P/exc-return-not-param "yes" "$(pv exc_return_not_param)" # ------------------------------------------------- the contract, both halves -- check_eq $P/contract-ok "6" "$(pv contract_ok)" check_eq $P/contract-bad-param "param" "$(pv contract_bad_param)" check_eq $P/contract-bad-param2 "param" "$(pv contract_bad_param_second)" # The same call again, after a violation was raised and caught. A wrapper that # carried state between calls -- a cache keyed wrongly, a flag left set -- would # show up here and nowhere else. check_eq $P/contract-ok-repeat "6" "$(pv contract_ok_repeat)" check_eq $P/bad-return "return" "$(pv bad_return_outcome)" # THE RETURN VALUE IS THE CALLEE'S OWN OBJECT. Asserted with `is' on both a # bare sentinel and a list, because a decorator that rebuilt or copied the # result would pass every type assertion in this file and break every caller # relying on identity. check_eq $P/return-identity "yes" "$(pv return_identity)" check_eq $P/return-identity-list "yes" "$(pv return_identity_list)" # ------------------------------------------- the messages name the offender --- # THE TICKET'S OWN REQUIREMENT. Facts about the message rather than the message, # for the reason the header gives. check_eq $P/msg-param-names-param "yes" "$(pv msg_param_names_param)" check_eq $P/msg-param-names-hint "yes" "$(pv msg_param_names_hint)" check_eq $P/msg-param-names-found "yes" "$(pv msg_param_names_found)" check_eq $P/msg-param-names-func "yes" "$(pv msg_param_names_func)" check_eq $P/msg-param-says-violates "yes" "$(pv msg_param_says_violates)" # THE CASE THAT MAKES THE FOUR ABOVE MEAN SOMETHING: the SECOND parameter is # violated, and the message must name it and must NOT name the first. check_eq $P/msg-param2-names-param "yes" "$(pv msg_param2_names_param)" check_eq $P/msg-param2-not-first "yes" "$(pv msg_param2_not_first)" # The return half, and the word that tells the two apart in a log. check_eq $P/msg-return-says-return "yes" "$(pv msg_return_says_return)" check_eq $P/msg-return-names-hint "yes" "$(pv msg_return_names_hint)" check_eq $P/msg-return-not-param "yes" "$(pv msg_return_not_param)" # SINGLE-LINE, both of them. A multi-line violation message would break every # log the platform keeps, and it is cheap to notice here. check_eq $P/msg-param-lines "1" "$(pv msg_param_lines)" check_eq $P/msg-return-lines "1" "$(pv msg_return_lines)" # ---------------------------------------------------- no annotations at all --- # THE STRONGEST STATEMENT IN THIS SUITE. For a function with no annotations # beartype returns THE FUNCTION ITSELF -- not a wrapper that checks nothing, the # identical object. There is no call-time cost to measure because there is no # call-time anything. check_eq $P/unannotated-identity "yes" "$(pv unannotated_identity)" check_eq $P/unannotated-call "(1, 'x')" "$(pv unannotated_call)" # ...and an ANNOTATED function DOES get a wrapper, which is the contrast that # makes the line above a finding rather than an accident. check_eq $P/annotated-not-identity "yes" "$(pv annotated_not_identity)" check_eq $P/annotated-has-wrapped "yes" "$(pv annotated_has_wrapped)" check_eq $P/annotated-name "_annotated" "$(pv annotated_name)" # The wrapper is transparent to introspection. A caller that reads annotations # at runtime -- which is most of the reason to have them -- must see no # difference, and `inspect.signature' must still show the defaults. check_eq $P/introspect-annotations "a:int,b:str,return:bool" \ "$(pv introspect_annotations)" check_eq $P/introspect-signature "(a: int, b: str = 'x') -> bool" \ "$(pv introspect_signature)" # ------------------------------------------------------------ scalar hints --- # Each row is one annotation against seven values. A matrix rather than a case # apiece because the interesting content is in the DISAGREEMENTS between rows, # and those are only visible side by side. # # `bool' SATISFIES `int' -- `booltrue:ok' in the first row -- because Python's # bool is a subclass of int. Python's, not beartype's. check_eq $P/scalar-int \ "int1:ok,booltrue:ok,float1:param,strx:param,bytesx:param,complex1:param,none:param" \ "$(pv scalar_int)" # `int' DOES NOT SATISFY `float' -- `int1:param' here. PEP 484's implicit # numeric tower is NOT implemented by beartype 0.17.2 by default. Upstream's # behaviour at this version, and the row most likely to be reported as a bug by # someone arriving from mypy. check_eq $P/scalar-float \ "int1:param,booltrue:param,float1:ok,strx:param,bytesx:param,complex1:param,none:param" \ "$(pv scalar_float)" check_eq $P/scalar-str \ "int1:param,booltrue:param,float1:param,strx:ok,bytesx:param,complex1:param,none:param" \ "$(pv scalar_str)" check_eq $P/scalar-bytes \ "int1:param,booltrue:param,float1:param,strx:param,bytesx:ok,complex1:param,none:param" \ "$(pv scalar_bytes)" # The converse of the first row: an `int' does NOT satisfy `bool', because the # subclass relation runs one way only. check_eq $P/scalar-bool \ "int1:param,booltrue:ok,float1:param,strx:param,bytesx:param,complex1:param,none:param" \ "$(pv scalar_bool)" check_eq $P/scalar-complex \ "int1:param,booltrue:param,float1:param,strx:param,bytesx:param,complex1:ok,none:param" \ "$(pv scalar_complex)" # ------------------------------------------------- containers: deep vs shallow -- # DEEP -- list. Every item bad, so any sample the O(1) strategy takes finds one. check_eq $P/list-ok "ok" "$(pv list_ok)" check_eq $P/list-empty "ok" "$(pv list_empty)" check_eq $P/list-allbad "param" "$(pv list_allbad)" # The container type itself is checked before its items, so a tuple where a list # is annotated fails on the OUTER check and never reaches a sample. check_eq $P/list-wrong-container "param" "$(pv list_wrong_container)" check_eq $P/list-not-container "param" "$(pv list_not_container)" # NESTED, so the deep check is shown to RECURSE rather than to stop one level # down. check_eq $P/list-nested-ok "ok" "$(pv list_nested_ok)" check_eq $P/list-nested-allbad "param" "$(pv list_nested_allbad)" # THE O(1) STRATEGY, MEASURED. 50 calls with every item bad must raise 50 times. check_eq $P/list-allbad-trials "50" "$(pv list_allbad_trials)" # ...and a PARTIALLY bad list raises only sometimes. Asserted as "neither always # nor never over 60 trials" rather than as a number, because it IS a random # variable and a suite that pinned it would red on its own tail. Either failure # mode has probability (2/3)^60 or (1/3)^60 -- about 1e-11 and 1e-29 -- so this # is assertable where the count is not. A red here means the strategy changed, # which is worth knowing and is what this case is for. check_eq $P/list-partial-sampled "yes" "$(pv list_partial_sampled)" # SHALLOW -- dict. THE FINDING. `{1: "x"}' violates BOTH the key and the value # annotations and is accepted. Upstream 0.17.2; deep dict checking landed later. check_eq $P/dict-ok "ok" "$(pv dict_ok)" check_eq $P/dict-bad-value "ok" "$(pv dict_bad_value)" check_eq $P/dict-bad-key "ok" "$(pv dict_bad_key)" check_eq $P/dict-bad-both "ok" "$(pv dict_bad_both)" # The CONTAINER type is still checked, which is what makes the three above a # statement about the type PARAMETERS being ignored rather than about the hint # being ignored entirely. check_eq $P/dict-wrong-container "param" "$(pv dict_wrong_container)" # SHALLOW -- set and frozenset, the same finding in two more places. check_eq $P/set-ok "ok" "$(pv set_ok)" check_eq $P/set-bad "ok" "$(pv set_bad)" check_eq $P/frozenset-bad "ok" "$(pv frozenset_bad)" # DEEP -- tuple, both forms. The fixed-length form checks the LENGTH too, which # no other container here does, and `tuple-short' is that case. check_eq $P/tuple-ok "ok" "$(pv tuple_ok)" check_eq $P/tuple-bad "param" "$(pv tuple_bad)" check_eq $P/tuple-short "param" "$(pv tuple_short)" check_eq $P/tuple-var-ok "ok" "$(pv tuple_var_ok)" check_eq $P/tuple-var-allbad "param" "$(pv tuple_var_allbad)" # THE SAME QUESTIONS PUT TO `beartype.door.is_bearable', which reaches the # checker with no decorator in between. `door-dict-bad' answering True is what # proves the shallowness belongs to the CHECKER and not to the decorator -- and # it is the single case a future beartype that deepened dicts would red on # first. check_eq $P/door-list-bad "False" "$(pv door_list_bad)" check_eq $P/door-list-ok "True" "$(pv door_list_ok)" check_eq $P/door-dict-bad "True" "$(pv door_dict_bad)" check_eq $P/door-set-bad "True" "$(pv door_set_bad)" check_eq $P/door-tuple-bad "False" "$(pv door_tuple_bad)" # ------------------------------------------- union, optional, Any, NoneType --- # `typing.Union[int, str]' and `int | str' are two spellings of one hint and # must behave identically. They are DIFFERENT objects -- `types.UnionType' for # the PEP 604 form, reduced by a different code path inside beartype -- so # asserting both rows is what says the two paths agree, and the PEP 604 form is # the one a reader will actually write. check_eq $P/hint-union "int1:ok,strx:ok,float1:param,none:param,list1:param" \ "$(pv hint_union)" check_eq $P/hint-pipe "int1:ok,strx:ok,float1:param,none:param,list1:param" \ "$(pv hint_pipe)" # `Optional[int]' IS `Union[int, None]', which is why it accepts None and # rejects str. The identity is asserted directly below so the reader is not # asked to take it on trust. check_eq $P/hint-optional "int1:ok,strx:param,float1:param,none:ok,list1:param" \ "$(pv hint_optional)" check_eq $P/hint-pipe-none "int1:ok,strx:param,float1:param,none:ok,list1:param" \ "$(pv hint_pipe_none)" check_eq $P/optional-is-union "yes" "$(pv optional_is_union)" check_eq $P/pipe-type "UnionType" "$(pv pipe_type)" check_eq $P/pipe-repr "int | str" "$(pv pipe_repr)" # `Any' accepts everything INCLUDING None, and so does `object'. The pair is # here because at runtime they are the same permission: a reader who expected # `Any' to be special is corrected by the two rows being identical. check_eq $P/hint-any "int1:ok,strx:ok,float1:ok,none:ok,list1:ok" "$(pv hint_any)" check_eq $P/hint-object "int1:ok,strx:ok,float1:ok,none:ok,list1:ok" "$(pv hint_object)" # ...and `None' as an annotation means the NoneType and nothing else, which is # the row that keeps `hint-any' from being read as "None is always allowed". check_eq $P/hint-nonetype "int1:param,strx:param,float1:param,none:ok,list1:param" \ "$(pv hint_nonetype)" # A DEFAULT ARGUMENT is checked against the annotation like any other value, and # `Optional[int] = None' is the one shape where the default would FAIL the # annotation if `Optional' were read as "int, may be omitted". check_eq $P/default-omitted "None" "$(pv default_omitted)" check_eq $P/default-given "5" "$(pv default_given)" check_eq $P/default-bad "param" "$(pv default_bad)" # ----------------------------------------------------------------- protocol --- # STRUCTURAL: `protocol-good' and `protocol-unrelated' pass two classes sharing # no base and both are accepted, which is exactly what a nominal check refuses. check_eq $P/protocol-good "ok" "$(pv protocol_good)" check_eq $P/protocol-unrelated "ok" "$(pv protocol_unrelated)" check_eq $P/protocol-missing "param" "$(pv protocol_missing)" # AND SHALLOWLY SO. A `close' taking three arguments where the Protocol declares # none PASSES: typing's own `__instancecheck__' looks for the ATTRIBUTE and # never at its signature. CPython's Protocol, not beartype's doing, recorded so # it is not read as a port defect. check_eq $P/protocol-wrong-signature "ok" "$(pv protocol_wrong_signature)" check_eq $P/msg-protocol-names-protocol "yes" "$(pv msg_protocol_names_protocol)" check_eq $P/msg-protocol-names-param "yes" "$(pv msg_protocol_names_param)" # A Protocol WITHOUT `@runtime_checkable' is refused at DECORATION time, before # any call. Raising early is right -- the alternative is a TypeError from # isinstance() at every call site -- and the probe records it as a # decoration-time failure by never calling the function. check_eq $P/protocol-not-runtime "BeartypeDecorHintPep3119Exception" \ "$(pv protocol_not_runtime)" # -------------------------------------------------------- forward references --- # The decoration succeeded BEFORE the class existed, which is what makes the two # cases below statements about laziness rather than about error handling. check_eq $P/forwardref-decorated "yes" "$(pv forwardref_decorated)" check_eq $P/forwardref-ok "ok" "$(pv forwardref_ok)" check_eq $P/forwardref-bad "param" "$(pv forwardref_bad)" check_eq $P/msg-forwardref-names-class "yes" "$(pv msg_forwardref_names_class)" # An UNRESOLVABLE reference is a CALL-time failure raising beartype's own # exception naming the missing name -- not a bare NameError, and not a # decoration-time failure. check_eq $P/forwardref-missing-decorated "yes" "$(pv forwardref_missing_decorated)" check_eq $P/forwardref-missing-call "ForwardRefException" \ "$(pv forwardref_missing_call)" check_eq $P/forwardref-missing-names-it "yes" "$(pv forwardref_missing_names_it)" # ------------------------------------------------------- @beartype on a class --- # THE SAME CLASS OBJECT -- not a subclass, not a wrapper. Asserted first because # everything a caller does afterwards depends on it. check_eq $P/class-identity "yes" "$(pv class_identity)" check_eq $P/class-name "Counter" "$(pv class_name)" check_eq $P/class-method-wrapped "yes" "$(pv class_method_wrapped)" # An UNANNOTATED method is left ALONE -- the module-level identity finding, one # level in -- and it still works. check_eq $P/class-bare-untouched "yes" "$(pv class_bare_untouched)" check_eq $P/class-bare-ok "yes" "$(pv class_bare_ok)" # ALL FIVE METHOD KINDS WORK... check_eq $P/class-init-ok "yes" "$(pv class_init_ok)" check_eq $P/class-add-ok "5" "$(pv class_add_ok)" check_eq $P/class-static-ok "A" "$(pv class_static_ok)" check_eq $P/class-classmethod-ok "8" "$(pv class_classmethod_ok)" check_eq $P/class-property-ok "4" "$(pv class_property_ok)" # ...AND ALL FOUR CALLABLE ONES ENFORCE. Each reaches beartype by a different # route -- a plain function on the class, two descriptors wrapping one, and an # `__init__' whose return annotation is None -- so a decoration that reached # only plain methods would pass `class-add-bad' and fail the three beside it. check_eq $P/class-init-bad "param" "$(pv class_init_bad)" check_eq $P/class-add-bad "param" "$(pv class_add_bad)" check_eq $P/class-static-bad "param" "$(pv class_static_bad)" check_eq $P/class-classmethod-bad "param" "$(pv class_classmethod_bad)" check_eq $P/msg-method-names-param "yes" "$(pv msg_method_names_param)" check_eq $P/msg-method-names-class "yes" "$(pv msg_method_names_class)" # ----------------------------------------------------------------- the conf --- # The default strategy is O(1), which is the fact every deep-check case above # rests on, so it is read off the configuration rather than inferred from the # behaviour it produces. check_eq $P/conf-strategy "O1" "$(pv conf_strategy)" check_eq $P/conf-repr "BeartypeConf()" "$(pv conf_repr)" check_eq $P/strategy-members "O0,O1,Ologn,On" "$(pv strategy_members)" check_eq $P/verbosity-members "MINIMAL,DEFAULT,MAXIMAL" "$(pv verbosity_members)" # `BeartypeStrategy.O0' turns checking OFF, and a conf carrying it makes the # decorator the identity function -- the same finding as `unannotated-identity', # reached through configuration instead. This is the escape hatch a caller # reaches for on a hot path, so it is worth knowing it really is free: not a # wrapper that skips the check, no wrapper. check_eq $P/strategy-o0-identity "yes" "$(pv strategy_o0_identity)" check_eq $P/strategy-o0-no-check "ok" "$(pv strategy_o0_no_check)" rm -rf "$W"