. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the bulk of the cost. # # Every other case here reads the filesystem or dpkg with # # shell builtins, or greps the second start's output. # # # # The two are: one `importlib.import_module' call (so the # # import result is legible from the console without reading # # a transcript) and the probe, which takes ONE start for a # # hundred-odd measurements. # # # # THE PROBE FORKS NOTHING. It has no fixtures to build and # # no children to spawn -- see the note in the probe about # # why a stub-only distribution has nothing to drive. It # # reads nineteen files, sha256s them, and parses thirteen. # # That is a smaller budget than the 1800 the probes with # # /bin/sh children beside it take, and 1200 is what the # # pure-python probes in this directory use. # # # # NOTHING HERE SLEEPS, opens a socket, starts a thread, or # # reaches a device, a peer or a terminal. Nothing is # # written anywhere: this suite is entirely read-only, which # # is unusual here and is a property of the package. # # python3-types-python-dateutil 2.9.0.20240316 (meta-openembedded meta-python, # recipes-devtools/python/python3-types-python-dateutil_2.9.0.20240316.bb, # `inherit pypi setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS, AND WHAT THAT MAKES THE SUITE ASSERT. # ============================================================================ # # It is the typeshed stub distribution for `python-dateutil': a `dateutil-stubs' # directory of THIRTEEN `.pyi' files, a `METADATA.toml' and a `py.typed', # carrying type declarations for the parser, relative-delta, recurrence-rule, # time-zone, Easter and zoneinfo APIs. There is NOT ONE EXECUTABLE LINE in the # whole distribution -- no `.py', no `.so', no console script, no entry point. # `install_requires' is empty and it does not require `python-dateutil' itself. # # So there is no behaviour to drive, and a suite that pretended otherwise would # be asserting things it made up. What CAN be asserted, and what this suite # does, is three things: # # 1. PACKAGING -- that the files the wheel promises are the files on disk, # byte for byte, scored against the wheel's OWN `RECORD' rather than # against a list typed into this suite; # 2. PARSE -- that this arm's interpreter can compile every `.pyi' with its # own `ast.parse' in type-comment mode. This is the one genuine demand a # stubs package makes of a ported interpreter, and it is a real one: stub # syntax is a corner of the grammar -- `@overload' stacks, `TypeAlias' # assignments, PEP 604 unions in annotation position, `Literal[1]' as a # type, positional-only `/' markers, `Protocol' bodies, `...' as an entire # function body -- that appears almost nowhere else in this image's python # payload; # 3. STRUCTURE -- that a sample of the declarations is really there, because # an empty file parses perfectly and "it parsed" would otherwise pass a # truncated install. That matters more here than for the flat stub # distributions beside it, because one of the shipped files IS empty. # # ---------------------------------------------------------------------------- # THREE THINGS THAT ARE NOT TRUE OF THE FLAT STUB PACKAGES BESIDE THIS ONE. # ---------------------------------------------------------------------------- # # 1. IT IS NESTED. `parser/', `tz/' and `zoneinfo/' are real subdirectories of # the stubs package -- five of the fifteen shipped files live in them. # Upstream's `packages=' names only `dateutil-stubs'; the subdirectories # arrive as `package_data' entries carrying a `/' in their name. So every # walk in the probe is recursive and every path it reports is relative with a # `/' in it. A flat `os.listdir' would have measured 10 of the 15 files and # reported green. `stub-dir-set' is the case that says the three arrived. # # 2. IT IS A PARTIAL STUB PACKAGE. It ships `py.typed' carrying the single word # `partial' -- the PEP 561 marker for a stub distribution that does not claim # to cover the whole library. So `py.typed' is asserted PRESENT here, with # its content, where the flat sibling asserts its absence: `has-py-typed' and # `py-typed-text' are two halves of one statement, since an EMPTY `py.typed' # would mean "complete" and would be a different package. # # 3. `__init__.pyi' IS ZERO BYTES, and that is the shipped layout rather than a # truncation: the top-level `dateutil' package declares nothing itself and # every name lives in a submodule. It is asserted with `test -f' and a pinned # size of 0 -- `test -s' would fail on a correct install -- and RECORD scores # it with the sha256 of the empty string, which `record-scored-empty-init' # confirms was not quietly skipped. # # ---------------------------------------------------------------------------- # THE ONE THING THIS SUITE SAYS THAT THE TICKET GOT WRONG. # ---------------------------------------------------------------------------- # # The ticket asks the suite to assert that "the stubs are not importable as a # runtime module". Half of that is right and half of it is not, and the # difference was MEASURED rather than reasoned about -- on an x86-64 control, # against this build's own .deb payload. # # - AS AN IMPORT STATEMENT it is right, and for a reason that has nothing to # do with this image or with stubs: `dateutil-stubs' is not an identifier, # so `import dateutil-stubs' is a SyntaxError in any CPython. # (`import-stmt-syntaxerror') # # - AS `importlib.import_module("dateutil-stubs")' IT SUCCEEDS. PEP 420 makes # any directory on sys.path a namespace-package portion whatever it is # called, so the name resolves -- to a module with `__file__' None, a # `NamespaceLoader', and an entirely EMPTY public surface. # # - AND SO DO ITS THREE SUBDIRECTORIES. `dateutil-stubs.parser', # `dateutil-stubs.tz' and `dateutil-stubs.zoneinfo' each import, as further # namespace portions, and each delivers nothing. This is the case the flat # sibling has no equivalent of, and writing "nothing under it imports" would # have been false: the accurate split is that the DIRECTORIES resolve empty # and the `.pyi' NAMES do not resolve at all, because `.pyi' is not in # `SOURCE_SUFFIXES'. `importable-dir-submodules', `importable-dir-surface' # and `importable-pyi-submodules' carry that split. # # Writing "it does not import" into the suite would therefore have put a false # statement into the record, and one that a later reader could not have told # from a measurement. # # ---------------------------------------------------------------------------- # THE LIBRARY THESE STUBS DESCRIBE IS ON THIS IMAGE, THOUGH IT NEED NOT BE. # ---------------------------------------------------------------------------- # # A PEP 561 stub distribution declares no dependency on the package it # annotates -- `install_requires=[]' upstream, which `meta-requires-dist' # asserts is still true -- and that is the whole reason it can ship alone. It # does not ship alone here: `python3-dateutil' landed on this image with # BRINTOS-2345 and is in IMAGE_INSTALL on meta-brintos `main' beside # `python3-dateutil-zoneinfo'. So this port puts annotations for a library into # the same site-packages as the library, and `dateutil-not-from-stubs' is the # case that says doing so did not change what `import dateutil' resolves to. # # THAT WAS NEARLY GOT BACKWARDS, and the near-miss is recorded beside those # cases rather than smoothed over: the layer this port was authored against is # pinned at 25d22c55, whose image recipe has no `dateutil' line, and the # batch-2271 rootfs manifest has none either -- two independent places agreeing # on a fact that stopped being true when `main' moved. The batch's integration # layer merges `origin/main' before the image is built, so the base a recipe # lane measures is not the base the suite runs on. # # THE SHARPER SHADOWING QUESTION IS A DIFFERENT ONE, and it is sharper because # of the nesting: the payload contains directories called `parser', `tz' and # `zoneinfo', and two of those are names CPython itself has used -- `zoneinfo' # is a stdlib module in 3.9+, and `parser' was one until 3.10. Had setuptools # installed them at the TOP of site-packages rather than inside # `dateutil-stubs/', `import zoneinfo' anywhere on this image would resolve to # an empty stubs directory and every caller of it would break. That is the # failure the ticket names, and `toplevel-from-stubs' and # `zoneinfo-not-from-stubs' are what rule it out -- measured through # `find_spec', so the question answered is WHICH file the finder offers rather # than whether an import happened to work. # # ---------------------------------------------------------------------------- # WHERE THE EXPECTATIONS COME FROM. # ---------------------------------------------------------------------------- # # Every value asserted below was MEASURED on an x86-64 control under CPython # 3.12.3 -- the same 3.12 minor this image ships (3.12.14) -- running THIS PROBE # against THIS BUILD's own .deb payload, unpacked with `dpkg-deb -x'. The # payload was also compared against the sdist whose sha256 the recipe pins # (`5d2f2e24...'): `diff -r' over `dateutil-stubs' is clean, so the recipe # repackages upstream without touching a byte. A red here is therefore this arm, # not a version skew and not a repackaging. # # TWO EXPECTATIONS ARE NOT FROM THE CONTROL, and they are marked where they # appear. `dateutil-file' and `dateutil-spec-origin' pin the path this image's # own `dateutil' resolves to, which the control cannot give -- its host python # finds its own copy under `dist-packages'; they are cross-checked instead # against 99-python3-dateutil.test, which pins the same path and lands green. # `toplevel-spec-*' likewise says "no other package on this image installed a # module by that name", which is a fact about the image and not about this port # -- which is why `toplevel-from-stubs' is beside them and is the one that # carries the port's own claim. # # ---------------------------------------------------------------------------- # WHAT IS PREDICTED RATHER THAN MEASURED, AND SO MIGHT BE RED. # ---------------------------------------------------------------------------- # # EXPECTED, NOT YET MEASURED -- this is a prediction and is written as one. The # recipe phase cannot build the image and so cannot run this suite; its first # on-target run is the batch verify. # # The prediction is that NOTHING here is red, and it is a weaker claim than it # looks rather than a stronger one: this package ships no code, so the suite's # only exposure to the arm is `ast.parse' and the file reads, and both are # interpreter-internal work over bytes already on the rootfs. There is no # syscall surface for the port to fall through -- no fork, no socket, no device, # no clock, no thread. # # The single place a surprise could come from is the PARSE, which is the reason # it is in the suite at all: `parse-node-count' (3281), `ellipsis-body-count' # (154), `pep604-union-count' (156) and `posonly-arg-count' (5) pin the parser's # output SHAPE, not merely its exit, so a parser that accepted the files and # built a different tree is a red here rather than a silent pass. If this arm's # parser diverges, the predicted shape is a mismatch in those counts with # `parse-failures' still empty -- not an exception. That is a prediction about # the shape, not a claim about the result. P=python3-types-python-dateutil # ============================================================================ # 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 SD=$SITE/dateutil-stubs DI=$SITE/types_python_dateutil-2.9.0.20240316.dist-info PKGLIST=/var/lib/dpkg/info/python3-types-python-dateutil.list PROBE=$BST_DATA/python3-types-python-dateutil-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-types-dateutil.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- payload --- # WHERE THE PACKAGING SAYS THE FILES LAND. These run before any import, so a red # here says the .deb is wrong rather than that something misbehaved. check $P/site-packages-dir test -d "$SITE" check $P/stubs-dir test -d "$SD" # `-f' AND NOT `-s'. `__init__.pyi' is shipped ZERO BYTES -- the top-level # `dateutil' package declares nothing of its own -- so `test -s' would go red on # a perfectly correct install. The size is pinned separately below, which is the # assertion `-s' was reaching for and could not make. check $P/init-pyi-present test -f "$SD/__init__.pyi" check $P/metadata-toml-present test -s "$SD/METADATA.toml" # THE THREE SUBPACKAGE DIRECTORIES. Upstream's `packages=' names only # `dateutil-stubs'; these arrive as `package_data' entries with a `/' in the # name, which is the part of this distribution's packaging that could plausibly # go wrong on a setuptools handling nested data differently. Five of the fifteen # shipped files are inside them. check $P/parser-dir test -d "$SD/parser" check $P/tz-dir test -d "$SD/tz" check $P/zoneinfo-dir test -d "$SD/zoneinfo" # THE WHOLE INSTALLED SET, RECURSIVELY, in one string -- rather than only the # files this suite happens to name. `find' and not `ls', because three of these # paths have a directory component. LC_ALL=C so the order is the byte order # rather than the guest's locale. check_eq $P/stubs-file-set \ "METADATA.toml,__init__.pyi,_common.pyi,easter.pyi,parser/__init__.pyi,parser/isoparser.pyi,py.typed,relativedelta.pyi,rrule.pyi,tz/__init__.pyi,tz/_common.pyi,tz/tz.pyi,utils.pyi,zoneinfo/__init__.pyi,zoneinfo/rebuild.pyi," \ "$(cd "$SD" 2>/dev/null && find . -type f | sed 's|^\./||' | LC_ALL=C sort | tr '\n' ',')" # THREE SIZES PINNED IN THE SHELL, so a truncated payload is a red that does not # depend on the probe having started. The full set is scored by sha256 against # RECORD inside the probe (`record-*' below); these three are the # console-legible version of it. # # `init-pyi-size' is 0 ON PURPOSE and is the case that makes the empty file a # measured fact rather than an ambiguity -- a lost file and an empty one look # identical to every other assertion here except this one and `init-pyi-present'. check_eq $P/init-pyi-size 0 \ "$(wc -c < "$SD/__init__.pyi" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check_eq $P/metadata-toml-size 143 \ "$(wc -c < "$SD/METADATA.toml" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check_eq $P/rrule-pyi-size 3530 \ "$(wc -c < "$SD/rrule.pyi" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # NO EXECUTABLE CODE SHIPS. Three absences, asserted over the payload rather # than over the recipe, because "stub-only" is the entire claim this package # makes and nothing else in the suite would notice it being broken. Recursive, # so a `.py' that landed in `tz/' is caught. # # The `.so' case is not decoration on this image: python3 here is ONE STATIC # BINARY with an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a # compiled extension would not be a speed-up but an ImportError. check_eq $P/no-py-files "" "$(find "$SD" -name '*.py' 2>/dev/null)" check_eq $P/no-so-files "" "$(find "$SD" -name '*.so' 2>/dev/null)" check_eq $P/no-pycache "" "$(find "$SD" -name '__pycache__' 2>/dev/null)" # `py.typed' IS PRESENT, AND ITS CONTENT IS THE ASSERTION. This is the half that # inverts against the flat stub packages beside this one, which ship none. PEP # 561 marks a stub distribution `partial' by putting that word in the file; an # EMPTY `py.typed' would claim the annotations are complete, which for these # they are not. So the word is pinned, not the file's existence. check $P/py-typed-present test -s "$SD/py.typed" check_eq $P/py-typed-text "partial" "$(cat "$SD/py.typed" 2>/dev/null | tr -d ' \r\n')" # NOTHING LANDS IN /usr/bin. setup.py declares no `scripts' and no entry points # at all -- the dist-info carries no `entry_points.txt', which # `dist-info-file-set' below pins. check $P/no-bin-script test ! -e /usr/bin/types-python-dateutil # ------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-metadata test -s "$DI/METADATA" check $P/dist-info-wheel test -s "$DI/WHEEL" # THE WHOLE METADATA FILE SET. Four files and no `entry_points.txt' -- the # absence is the assertion, and it is the packaging half of `no-bin-script'. check_eq $P/dist-info-file-set \ "METADATA,RECORD,WHEEL,top_level.txt," \ "$(LC_ALL=C ls -1 "$DI" 2>/dev/null | LC_ALL=C sort | tr '\n' ',')" # THE HYPHEN IS LOAD-BEARING and is asserted here as data. `top_level.txt' names # `dateutil-stubs', not `dateutil_stubs': setup.py declares # `packages=["dateutil-stubs"]', a name that is deliberately NOT a valid python # identifier, and that single fact is what `import-stmt-syntaxerror' below is # about. It is also NOT `dateutil', which is the shadowing this port must not do. check_eq $P/dist-info-top-level "dateutil-stubs" \ "$(cat "$DI/top_level.txt" 2>/dev/null | tr -d ' \r\n')" # --------------------------------------------------------------- dpkg --- # The package is REGISTERED, not merely unpacked. A payload that arrived some # other way would pass every path case above and fail here. check $P/dpkg-list test -s "$PKGLIST" check $P/dpkg-lists-init grep -q "^$SD/__init__\.pyi\$" "$PKGLIST" check $P/dpkg-lists-toml grep -q "^$SD/METADATA\.toml\$" "$PKGLIST" # A FILE FROM A SUBDIRECTORY, listed by dpkg. The nesting is the part of this # package's packaging that is unlike its siblings, so it is asserted in the # package manager's own record and not only on disk. check $P/dpkg-lists-tz grep -q "^$SD/tz/tz\.pyi\$" "$PKGLIST" # THE `-dev' AND `-dbg' SPLITS ARE PRODUCED AND EMPTY, so they are not installed # and must not be here. Measured on this build: `dpkg-deb -x' of each unpacks to # an empty tree. Only the main package is in IMAGE_INSTALL. check $P/no-dev-installed test ! -e /var/lib/dpkg/info/python3-types-python-dateutil-dev.list check $P/no-dbg-installed test ! -e /var/lib/dpkg/info/python3-types-python-dateutil-dbg.list # ------------------------------------------------------------------ import --- # ONE case for the import, so the result is legible from the console without # reading the probe's transcript. It is `importlib' and not an `import' # statement, which cannot be written for this name at all -- see the header, and # `import-stmt-syntaxerror' below. check $P/import python3 -c 'import importlib; importlib.import_module("dateutil-stubs")' # ------------------------------------------------------------------- probe --- if [ ! -s "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" exit 0 fi bst_ok $P/probe-present # 1000s, inside the 1200s suite budget so a hung probe is reported as a probe # overrun naming the last line it printed, rather than as a bare suite timeout. # # The probe EXITS 0 even when the payload is absent, so this case passing means # "the probe ran", not "the package is right" -- `stubs-found' below is what # carries that. bst_probe $P/probe-run "$W/probe" 1000 python3 "$PROBE" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' # because a key is emitted once and a duplicate would otherwise concatenate; # `cut -f2-' because a value may legitimately contain `='. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The probe reached its end. Every case below reads a value it printed, so a # probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" _found=$(pv stubs_found) check_eq $P/stubs-found "yes" "$_found" if [ "$_found" != "yes" ]; then # The probe found no `dateutil-stubs' anywhere on sys.path. Skip -- not red -- # every case that reads the payload, each with the reason attached: they are # all one fact, and eighty reds would bury it rather than report it. The path # cases above and `stubs-found' are the reds that carry the verdict. _why="dateutil-stubs not on sys.path; site_dir=$(pv site_dir)" for _c in probe-count site-dir stub-file-set stub-file-count pyi-count \ stub-dir-set stub-dir-count probe-no-py probe-no-so \ probe-no-pycache probe-has-py-typed probe-py-typed-bytes \ probe-py-typed-text probe-has-init-pyi probe-init-pyi-bytes \ probe-no-init-py probe-has-metadata-toml metadata-toml-bytes \ metadata-toml-text record-read record-entries record-missing \ record-size-mismatch record-hash-mismatch record-scored-empty-init \ record-unlisted parse-ok-count parse-failures parse-node-count \ parse-negative-control ellipsis-body-count ellipsis-default-count \ overload-count type-alias-names pep604-union-count \ posonly-arg-count init-parsed init-body-empty parser-parsed \ parser-classes parser-functions parse-fn-found parse-fn-args \ parse-fn-kwonly parse-fn-returns parse-fn-no-vararg \ parserinfo-found parserinfo-methods parserinfo-attrs \ parser-error-bases relativedelta-parsed relativedelta-classes \ relativedelta-constants relativedelta-class-found \ relativedelta-bases relativedelta-methods \ relativedelta-method-count relativedelta-fields \ relativedelta-weeks-decorators relativedelta-init-args \ rrule-parsed rrule-classes rrule-freq-constants \ rrule-module-names rrule-freq-annotation rrule-class-bases \ rruleset-bases rrulebase-methods rruleset-nested-classes \ tz-parsed tz-classes tz-functions tzstr-bases tz-protocol-classes \ easter-parsed easter-functions easter-constants \ easter-constant-annotations zoneinfo-parsed zoneinfo-classes \ zoneinfo-functions zoneinfo-all import-stmt-syntaxerror \ import-module-ok import-module-file import-module-origin \ import-module-loader import-module-is-namespace \ import-module-surface importable-pyi-submodules \ importable-dir-submodules importable-dir-surface \ pyi-in-source-suffixes toplevel-from-stubs \ zoneinfo-not-from-stubs toplevel-spec-parser toplevel-spec-tz \ toplevel-spec-utils toplevel-spec-easter toplevel-spec-rrule \ toplevel-spec-relativedelta dateutil-import-ok dateutil-file \ dateutil-spec-origin dateutil-not-from-stubs \ probe-dist-info-file-set meta-name meta-version meta-license \ meta-requires-python meta-requires-dist meta-classifiers \ probe-dist-top-level wheel-root-is-purelib wheel-tag \ importlib-metadata-version importlib-metadata-file-count; do bst_skip $P/$_c "$_why" done rm -rf "$W" exit 0 fi # The probe emitted every key it intended to. A probe that returned 0 having # skipped a section would otherwise leave those cases reading empty strings. # # The count is FIXED whichever way the measurements come out: the two branches # that can go either way -- the namespace import and `import dateutil' -- emit # the same key set on both paths, deliberately, so this number is evidence about # the probe rather than about the payload. check_eq $P/probe-count "121" "$(pv emitted_count)" # WHERE THE PAYLOAD WAS FOUND. Pinned to site-packages so a directory satisfied # from somewhere else on sys.path -- a leftover in the cwd, a second install -- # is a red here rather than a green suite measuring the wrong tree. check_eq $P/site-dir "$SITE" "$(pv site_dir)" # The probe's own view of the payload, which is the same set the shell asserted # above read a second way. Both are kept: the shell one is legible on the # console, this one is what the parse and structure cases below were taken from, # and a disagreement between them would itself be the finding. check_eq $P/stub-file-set \ "METADATA.toml,__init__.pyi,_common.pyi,easter.pyi,parser/__init__.pyi,parser/isoparser.pyi,py.typed,relativedelta.pyi,rrule.pyi,tz/__init__.pyi,tz/_common.pyi,tz/tz.pyi,utils.pyi,zoneinfo/__init__.pyi,zoneinfo/rebuild.pyi" \ "$(pv stub_file_set)" check_eq $P/stub-file-count "15" "$(pv stub_file_count)" check_eq $P/pyi-count "13" "$(pv pyi_count)" # THE SUBPACKAGE DIRECTORIES AS A SET, from the probe's recursive walk. This is # the case a flat `os.listdir' implementation could not have produced, and it is # what says the nesting survived packaging. check_eq $P/stub-dir-set "parser,tz,zoneinfo" "$(pv stub_dir_set)" check_eq $P/stub-dir-count "3" "$(pv stub_dir_count)" check_eq $P/probe-no-py "" "$(pv py_files)" check_eq $P/probe-no-so "" "$(pv so_files)" check_eq $P/probe-no-pycache "no" "$(pv has_pycache)" check_eq $P/probe-has-py-typed "yes" "$(pv has_py_typed)" check_eq $P/probe-py-typed-bytes "8" "$(pv py_typed_bytes)" check_eq $P/probe-py-typed-text "partial" "$(pv py_typed_text)" check_eq $P/probe-has-init-pyi "yes" "$(pv has_init_pyi)" check_eq $P/probe-init-pyi-bytes "0" "$(pv init_pyi_bytes)" check_eq $P/probe-no-init-py "no" "$(pv has_init_py)" check_eq $P/probe-has-metadata-toml "yes" "$(pv has_metadata_toml)" # THE STUB DISTRIBUTION'S OWN MARKER FILE, asserted as its whole content. It # declares which python-dateutil release these annotations aim at (`2.9.*') and # that they are partial -- the two facts about this package not derivable from # the file names, and the ones that make it meaningful rather than merely # present. Newlines are flattened to `|' by the probe so the value stays one # counted result. check_eq $P/metadata-toml-bytes "143" "$(pv metadata_toml_bytes)" check_eq $P/metadata-toml-text \ 'version = "2.9.*"|upstream_repository = "https://github.com/dateutil/dateutil"|partial_stub = true||[tool.stubtest]|ignore_missing_stub = true' \ "$(pv metadata_toml_text)" # ------------------------------------------------- payload vs the wheel --- # THE INSTALLED SET SCORED AGAINST THE WHEEL'S OWN `RECORD' -- its sha256 and # byte count for every file it installed -- rather than against a list typed # into this suite. This is the case the ticket asks for by name, and it is the # strongest one here: it makes a packaging change VISIBLE instead of leaving it # to be noticed. # # Four empties and a count, deliberately split rather than folded into one # verdict: "a file is missing", "a file is the wrong length", "a file has the # wrong bytes" and "a file is installed that RECORD does not list" are four # different faults with four different causes, and one combined red would name # none of them. check_eq $P/record-read "yes" "$(pv record_read)" check_eq $P/record-entries "19" "$(pv record_entries)" check_eq $P/record-missing "" "$(pv record_missing)" check_eq $P/record-size-mismatch "" "$(pv record_size_mismatch)" check_eq $P/record-hash-mismatch "" "$(pv record_hash_mismatch)" check_eq $P/record-unlisted "" "$(pv record_unlisted)" # THE ZERO-BYTE FILE WAS SCORED LIKE ANY OTHER. RECORD gives # `dateutil-stubs/__init__.pyi' the sha256 of the empty string and a size of 0, # and a scoring loop that skipped falsy sizes would leave every result above # green while never having looked at it. This case is what says it did. check_eq $P/record-scored-empty-init "yes" "$(pv record_scored_empty_init)" # ------------------------------------------------------------------ parse --- # EVERY `.pyi' THROUGH THIS ARM'S OWN `ast.parse', in `type_comments=True' mode # -- the mode a type checker uses, and a genuinely different code path in # CPython's parser rather than a flag it ignores. check_eq $P/parse-ok-count "13" "$(pv parse_ok_count)" check_eq $P/parse-failures "" "$(pv parse_failures)" # THE PARSER'S OUTPUT SHAPE, not merely its exit status. An empty file parses # perfectly -- and one of these thirteen IS empty -- so a parser that silently # dropped constructs would still report thirteen successes; the node count is # what makes those two different results. check_eq $P/parse-node-count "3281" "$(pv parse_node_count)" # THE PARSE HAS TEETH. A harness that caught everything would report # `parse-ok-count' 13 for thirteen good files and for thirteen empty ones alike. # Feeding the same parser, in the same mode, a file that MUST fail is what says # the mode is really on and the exception really propagates. check_eq $P/parse-negative-control "yes" "$(pv parse_negative_control)" # THE FIVE CONSTRUCTS THAT MAKE STUB SYNTAX ITS OWN CORNER OF THE GRAMMAR, # counted structurally off the tree rather than assumed from a clean parse. # # `...' is counted TWICE, in two different productions, because the token means # two different things in a stub and a parser that conflated them would be a # wash across one combined figure: as an entire function BODY (154) it is the # form this whole distribution is written in, and as a DEFAULT ARGUMENT VALUE # (12) it is the stub idiom for "there is a default and it is not worth # naming" -- seven of the twelve are in `parser.parse' alone. check_eq $P/ellipsis-body-count "154" "$(pv ellipsis_body_count)" check_eq $P/ellipsis-default-count "12" "$(pv ellipsis_default_count)" # `@overload' -- the construct a parser is most likely to get wrong. All nine are # in `relativedelta.pyi', three each on `__add__', `__radd__' and `__rsub__'. check_eq $P/overload-count "9" "$(pv overload_count)" # PEP 613 `TypeAlias' ASSIGNMENTS, by name. The right-hand side is a TYPE in # expression position, which is a stub-only shape; the names are asserted rather # than the count so a release that renamed one is legible. check_eq $P/type-alias-names \ "_FileOrStr,_MetadataType,_RRule,_Readable,_TakesAscii,_TzData,_TzInfo,_Weekday" \ "$(pv type_alias_names)" # PEP 604 UNIONS (`int | None') IN ANNOTATION POSITION, which this distribution # is written in throughout and which parse as `BinOp(BitOr)'. A parser that # folded them into something else would still parse every file cleanly. check_eq $P/pep604-union-count "156" "$(pv pep604_union_count)" # POSITIONAL-ONLY PARAMETERS (`/'), a narrow production this image's payload # otherwise barely uses. All five are in the two `Protocol' bodies in # `tz/tz.pyi'. check_eq $P/posonly-arg-count "5" "$(pv posonly_arg_count)" # -------------------------------------------------------------- structure --- # A SAMPLE OF THE DECLARATIONS, so a truncated install is caught rather than # passing as "it parsed". # `__init__.pyi' DECLARES NOTHING, and that is upstream's layout: the top-level # `dateutil' package has no names of its own. Asserted as zero statements, which # together with `init-pyi-size' makes the emptiness a measured fact rather than # the one case in the suite that cannot tell a missing file from a present one. check_eq $P/init-parsed "yes" "$(pv init_parsed)" check_eq $P/init-body-empty "0" "$(pv init_body_statements)" # --- dateutil.parser -- the module the ticket names first -------------------- check_eq $P/parser-parsed "yes" "$(pv parser_parsed)" check_eq $P/parser-classes "ParserError,_tzparser,parser,parserinfo" "$(pv parser_classes)" check_eq $P/parser-functions "parse" "$(pv parser_functions)" # `parse' -- THE FUNCTION THE TICKET NAMES -- described by its SIGNATURE rather # than by its presence. The interesting half is the keyword-only block after the # `*': seven names a caller may only pass by keyword, each defaulting to `...'. # A release that moved one across the `*' would be a red here and invisible to a # name check. `parse-fn-no-vararg' is what says there is no `*args' swallowing # them. check_eq $P/parse-fn-found "yes" "$(pv parse_fn_found)" check_eq $P/parse-fn-args "timestr,parserinfo" "$(pv parse_fn_args)" check_eq $P/parse-fn-kwonly \ "dayfirst,yearfirst,ignoretz,fuzzy,fuzzy_with_tokens,default,tzinfos" \ "$(pv parse_fn_kwonly)" check_eq $P/parse-fn-returns "datetime" "$(pv parse_fn_returns)" check_eq $P/parse-fn-no-vararg "no" "$(pv parse_fn_has_vararg)" # `parserinfo' IS THE CONFIGURATION OBJECT `parse' takes, and its method and # attribute sets are the part of this stub a caller actually reads. check_eq $P/parserinfo-found "yes" "$(pv parserinfo_found)" check_eq $P/parserinfo-methods \ "__init__,ampm,convertyear,hms,jump,month,pertain,tzoffset,utczone,validate,weekday" \ "$(pv parserinfo_methods)" check_eq $P/parserinfo-attrs \ "AMPM,HMS,JUMP,MONTHS,PERTAIN,TZOFFSET,UTCZONE,WEEKDAYS" \ "$(pv parserinfo_attrs)" # `ParserError' INHERITS FROM `ValueError'. The one inheritance in this file a # caller depends on -- it is what lets `except ValueError' catch a bad parse -- # so the base is read off the tree rather than assumed. check_eq $P/parser-error-bases "ValueError" "$(pv parser_error_bases)" # --- dateutil.relativedelta -------------------------------------------------- check_eq $P/relativedelta-parsed "yes" "$(pv relativedelta_parsed)" check_eq $P/relativedelta-classes "relativedelta" "$(pv relativedelta_classes)" # THE SEVEN WEEKDAY CONSTANTS, in declaration order, with the `_Weekday' alias # ahead of them. They are the public surface of this module beside the class. check_eq $P/relativedelta-constants "_Weekday,MO,TU,WE,TH,FR,SA,SU" \ "$(pv relativedelta_constants)" check_eq $P/relativedelta-class-found "yes" "$(pv relativedelta_class_found)" # IT INHERITS FROM NOTHING. Asserted as an empty string, which is what stops a # release that gave it a base from passing silently. check_eq $P/relativedelta-bases "" "$(pv relativedelta_bases)" # THE METHOD LIST CARRIES ITS DUPLICATES ON PURPOSE. `__add__', `__radd__' and # `__rsub__' each appear three times and `weeks' twice, because each is an # `@overload' stack or a property/setter pair -- three declarations of one name. # Deduplicating would have hidden exactly the thing `overload-count' is here to # watch, so the raw declaration list is what is pinned, and the count with it. check_eq $P/relativedelta-methods \ "__abs__,__add__,__add__,__add__,__bool__,__div__,__eq__,__hash__,__init__,__mul__,__ne__,__neg__,__nonzero__,__radd__,__radd__,__radd__,__rmul__,__rsub__,__rsub__,__rsub__,__sub__,__truediv__,normalized,weeks,weeks" \ "$(pv relativedelta_methods)" check_eq $P/relativedelta-method-count "25" "$(pv relativedelta_method_count)" # THE ANNOTATED FIELDS, which are the sixteen attributes a relativedelta carries # at runtime. `weeks' is deliberately NOT among them -- it is the property below. check_eq $P/relativedelta-fields \ "day,days,hour,hours,leapdays,microsecond,microseconds,minute,minutes,month,months,second,seconds,weekday,year,years" \ "$(pv relativedelta_fields)" # `weeks' IS A PROPERTY WITH A SETTER -- the one member of this class declared # differently from the rest, everything else being a plain annotated field. BOTH # decorators are read, because a stub that lost the setter would still declare # `weeks' and would still be counted among the methods. check_eq $P/relativedelta-weeks-decorators "property,weeks.setter" \ "$(pv relativedelta_weeks_decorators)" # THE `__init__' KEYWORD SURFACE -- twenty-one parameters after `self', in # order. This is the whole of what a caller constructs a relativedelta with, and # the ordering is load-bearing: `dt1' and `dt2' are positional in practice. check_eq $P/relativedelta-init-args \ "dt1,dt2,years,months,days,leapdays,weeks,hours,minutes,seconds,microseconds,year,month,day,weekday,yearday,nlyearday,hour,minute,second,microsecond" \ "$(pv relativedelta_init_args)" # --- dateutil.rrule ---------------------------------------------------------- check_eq $P/rrule-parsed "yes" "$(pv rrule_parsed)" check_eq $P/rrule-classes "_iterinfo,_rrulestr,rrule,rrulebase,rruleset,weekday" \ "$(pv rrule_classes)" # THE FREQUENCY CONSTANTS THE TICKET NAMES, in declaration order -- which is the # coarse-to-fine order upstream declares them in and which a set would lose. check_eq $P/rrule-freq-constants \ "YEARLY,MONTHLY,WEEKLY,DAILY,HOURLY,MINUTELY,SECONDLY" \ "$(pv rrule_freq_constants)" # ALL of the module-level names, so a constant that vanished into a class body is # a red here even though the seven above still stand. check_eq $P/rrule-module-names \ "DAILY,FR,HOURLY,MINUTELY,MO,MONTHLY,SA,SECONDLY,SU,TH,TU,WE,WEEKLY,YEARLY,_RRule,rrulestr,weekdays" \ "$(pv rrule_module_names)" # THEY ARE DECLARED `int', NOT `Literal[...]'. That is the fact rather than an # accident of the stub: upstream's rrule takes any of them as a plain integer, so # the annotation is the loose one -- and a release that tightened it to `Literal' # would change what a caller may pass. check_eq $P/rrule-freq-annotation "int" "$(pv rrule_freq_annotation)" check_eq $P/rrule-class-bases "rrulebase" "$(pv rrule_class_bases)" check_eq $P/rruleset-bases "rrulebase" "$(pv rruleset_bases)" check_eq $P/rrulebase-methods \ "__contains__,__getitem__,__init__,__iter__,after,before,between,count,xafter" \ "$(pv rrulebase_methods)" # THE ONLY NESTED CLASS IN THE DISTRIBUTION. `rruleset._genitem' is also the only # place carrying a class-scope assignment alias (`next = __next__') -- two # productions in one declaration, and the deepest the parser goes here. check_eq $P/rruleset-nested-classes "_genitem" "$(pv rruleset_nested_classes)" # --- dateutil.tz ------------------------------------------------------------- check_eq $P/tz-parsed "yes" "$(pv tz_parsed)" check_eq $P/tz-classes \ "_GetTZ,_ICalReader,_TZFileReader,_ttinfo,tzfile,tzical,tzlocal,tzoffset,tzrange,tzstr,tzutc" \ "$(pv tz_classes)" check_eq $P/tz-functions "datetime_ambiguous,datetime_exists,resolve_imaginary" \ "$(pv tz_functions)" check_eq $P/tzstr-bases "tzrange" "$(pv tzstr_bases)" # THE TWO `Protocol' BODIES, which are where the five positional-only markers # counted above live. A `Protocol' base in a stub is a structural type # declaration, and asserting the two by name is what stops `posonly-arg-count' # from being satisfied by five markers somewhere else. check_eq $P/tz-protocol-classes "_ICalReader,_TZFileReader" "$(pv tz_protocol_classes)" # --- dateutil.easter and dateutil.zoneinfo ----------------------------------- check_eq $P/easter-parsed "yes" "$(pv easter_parsed)" check_eq $P/easter-functions "easter" "$(pv easter_functions)" check_eq $P/easter-constants "EASTER_JULIAN,EASTER_ORTHODOX,EASTER_WESTERN" \ "$(pv easter_constants)" # THE NARROWEST ANNOTATION FORM IN THE DISTRIBUTION: the type IS the value. # `Literal[1]' carries its argument through the parser as a subscript on a name, # and reading it back as text is what says the argument survived -- a `Literal' # that lost it would still be a `Literal'. check_eq $P/easter-constant-annotations \ "EASTER_JULIAN=Literal[1],EASTER_ORTHODOX=Literal[2],EASTER_WESTERN=Literal[3]" \ "$(pv easter_constant_annotations)" check_eq $P/zoneinfo-parsed "yes" "$(pv zoneinfo_parsed)" check_eq $P/zoneinfo-classes "ZoneInfoFile" "$(pv zoneinfo_classes)" check_eq $P/zoneinfo-functions "get_zonefile_instance,gettz,gettz_db_metadata" \ "$(pv zoneinfo_functions)" # THE ONLY `__all__' IN THE DISTRIBUTION, read as its literal elements. check_eq $P/zoneinfo-all "get_zonefile_instance,gettz,gettz_db_metadata" \ "$(pv zoneinfo_all)" # ----------------------------------------------------------------- import --- # WHAT `import' ACTUALLY DOES WITH A STUBS-ONLY DIRECTORY. The header explains at # length why these cases say "imports empty and reaches nothing" rather than the # ticket's "not importable as a runtime module"; the short version is that the # second was measured to be false and the first to be true. # THE IMPORT STATEMENT CANNOT BE WRITTEN. `dateutil-stubs' is not an identifier, # so this is a SyntaxError in any CPython -- a fact about the name upstream # chose, asserted here because it is the accurate half of the ticket's premise. check_eq $P/import-stmt-syntaxerror "yes" "$(pv import_stmt_syntaxerror)" # BUT `importlib.import_module' RESOLVES IT, as a PEP 420 namespace package. check_eq $P/import-module-ok "yes" "$(pv import_module_ok)" check_eq $P/import-module-file "None" "$(pv import_module_file)" check_eq $P/import-module-origin "None" "$(pv import_module_origin)" check_eq $P/import-module-loader "NamespaceLoader" "$(pv import_module_loader)" check_eq $P/import-module-is-namespace "yes" "$(pv import_module_is_namespace)" # AND IT DELIVERS NOTHING. check_eq $P/import-module-surface "" "$(pv import_module_surface)" # NO `.pyi' NAME RESOLVES AT ALL. `.pyi' is not a source suffix, so the finder # never offers one of these files however the name is spelled. check_eq $P/importable-pyi-submodules "" "$(pv importable_pyi_submodules)" # THE THREE SUBDIRECTORIES DO RESOLVE, and this is the case that has no # equivalent in the flat stub packages beside this one. A subdirectory of a # namespace package is itself a namespace portion, so `dateutil-stubs.parser', # `.tz' and `.zoneinfo' each import -- and each hands back nothing, which is the # half that makes it harmless. Both are asserted, because "they import" alone # would be the misleading one and "nothing under it imports" would be false. check_eq $P/importable-dir-submodules "parser,tz,zoneinfo" \ "$(pv importable_dir_submodules)" check_eq $P/importable-dir-surface "" "$(pv importable_dir_surface)" # THE MECHANISM UNDERNEATH. `.pyi' is not in `SOURCE_SUFFIXES', which is why the # namespace packages above can be empty while thirteen parseable modules sit # inside them. check_eq $P/pyi-in-source-suffixes "no" "$(pv pyi_in_source_suffixes)" # ------------------------------------------------------------- no shadow --- # NOTHING IN THE DISTRIBUTION SHADOWS A REAL MODULE NAME, which is the failure # the ticket names -- the one that would break an unrelated import if the # packaging went wrong. It is a sharper question here than for a flat stub # package: this payload contains directories called `parser', `tz' and # `zoneinfo', and two of those are names CPython itself has used. `zoneinfo' is # a stdlib module in 3.9+, `parser' was one until 3.10. Had setuptools installed # them at the TOP of site-packages instead of inside `dateutil-stubs/', # `import zoneinfo' anywhere on this image would resolve to an empty directory. # # `toplevel-from-stubs' IS THE ONE THAT CARRIES THIS PORT'S CLAIM: of the seven # names the payload could have leaked, none resolves to a file inside the stubs # directory. It holds whatever else the image installs, which is why it is the # assertion and the seven origins below are the record. check_eq $P/toplevel-from-stubs "" "$(pv toplevel_from_stubs)" # `zoneinfo' RESOLVES TO THE STDLIB'S, stated as the property rather than as a # path so it holds whatever prefix this image's python uses. check_eq $P/zoneinfo-not-from-stubs "yes" "$(pv zoneinfo_not_from_stubs)" # THE SIX NAMES THAT MUST NOT RESOLVE AT ALL. These are measured on the control # and are a statement about THE IMAGE as much as about this port -- "no other # package installed a top-level module by this name". A red here is a finding # about whatever did install one, not necessarily about this package, and # `toplevel-from-stubs' above is what separates the two. check_eq $P/toplevel-spec-parser "none" "$(pv toplevel_spec_parser)" check_eq $P/toplevel-spec-tz "none" "$(pv toplevel_spec_tz)" check_eq $P/toplevel-spec-utils "none" "$(pv toplevel_spec_utils)" check_eq $P/toplevel-spec-easter "none" "$(pv toplevel_spec_easter)" check_eq $P/toplevel-spec-rrule "none" "$(pv toplevel_spec_rrule)" check_eq $P/toplevel-spec-relativedelta "none" "$(pv toplevel_spec_relativedelta)" # ------------------------------------------------------- the real library --- # `python3-dateutil' IS ON THIS IMAGE -- BRINTOS-2345 landed it, and it is in # IMAGE_INSTALL on meta-brintos `main' beside `python3-dateutil-zoneinfo'. So # these stubs sit in the same site-packages as the library they annotate, and # the thing worth ruling out is that adding them changed what `import dateutil' # does. Measured by the RESOLVED `__file__', not by the import merely # succeeding. # # THIS WAS NEARLY WRITTEN THE OTHER WAY ROUND, and the near-miss is worth # recording because it is exactly the shape of prediction this phase is warned # about. The layer this port was authored against is pinned at 25d22c55, whose # image recipe carries no `dateutil' line, and the batch-2271 rootfs manifest # carries none either -- so "the library is not on this image" read as a # measured fact from two independent places. It is not: the batch's integration # layer merges `origin/main' before the image is built, and `main' has carried # python3-dateutil since BRINTOS-2345. A pinned-base reading of a moving branch # would have shipped three cases that go red on a working port. # # `dateutil-file' is the one expected value here NOT taken from the x86-64 # control -- the control's `import dateutil' resolves to the host's own copy # under `dist-packages'. It is cross-checked instead against # 99-python3-dateutil.test, which pins this same path as `module-answered' and # lands green. check_eq $P/dateutil-import-ok "yes" "$(pv dateutil_import_ok)" check_eq $P/dateutil-file "$SITE/dateutil/__init__.py" "$(pv dateutil_file)" check_eq $P/dateutil-spec-origin "$SITE/dateutil/__init__.py" "$(pv dateutil_spec_origin)" # THE STUBS DO NOT SHADOW THE LIBRARY. This is the case that stays true whether # or not python3-dateutil is on the image, and it is the one that is about this # port rather than about the image's package list. check_eq $P/dateutil-not-from-stubs "no" "$(pv dateutil_from_stubs_dir)" # ---------------------------------------------------------- installed meta --- check_eq $P/probe-dist-info-file-set "METADATA,RECORD,WHEEL,top_level.txt" \ "$(pv dist_info_file_set)" check_eq $P/meta-name "types-python-dateutil" "$(pv meta_name)" check_eq $P/meta-version "2.9.0.20240316" "$(pv meta_version)" # `Apache-2.0 license' -- the trailing word is upstream's, from setup.py's # `license=' string, and the recipe's LICENSE is the clean `Apache-2.0'. Pinned # as it is rather than as it ought to be. check_eq $P/meta-license "Apache-2.0 license" "$(pv meta_license)" check_eq $P/meta-requires-python ">=3.8" "$(pv meta_requires_python)" # NO RUNTIME DEPENDENCY IS DECLARED, AND SPECIFICALLY NOT ON `python-dateutil'. # This is the fact that lets a stubs package be installed on an image that does # not carry the library it describes -- `install_requires=[]' upstream -- and it # is what makes the recipe's `Depends: python3-core' correct rather than thin. A # `Requires-Dist:' line appearing here is a release changing its mind. check_eq $P/meta-requires-dist "none" "$(pv meta_requires_dist)" # `Typing :: Stubs Only' IS DECLARED IN THE CLASSIFIERS, which is the packaging's # own statement of what `no-py-files' measures on disk. The two are kept apart # because either could be wrong without the other. check_eq $P/meta-classifiers \ "License :: OSI Approved :: Apache Software License;Programming Language :: Python :: 3;Typing :: Stubs Only" \ "$(pv meta_classifiers)" check_eq $P/probe-dist-top-level "dateutil-stubs" "$(pv dist_top_level)" # A PURE-PYTHON WHEEL, from the wheel's own metadata, twice over. # `Root-Is-Purelib: true' is the packaging-side statement of `no-so-files' above # -- nothing in it is platform-specific, which for a wasm32 image is the # difference between a package that ports and one that cannot -- and the # `py3-none-any' tag says the same thing about the build. check_eq $P/wheel-root-is-purelib "true" "$(pv wheel_root_is_purelib)" check_eq $P/wheel-tag "py3-none-any" "$(pv wheel_tag)" # `importlib.metadata' MUST ANSWER FOR IT. The dist-info being on disk and the # interpreter being able to FIND it are two different facts, and only the second # is what a caller asking "is types-python-dateutil installed" actually uses. The # file count is RECORD's 19 read back through the metadata API. check_eq $P/importlib-metadata-version "2.9.0.20240316" "$(pv importlib_metadata_version)" check_eq $P/importlib-metadata-file-count "19" "$(pv importlib_metadata_file_count)" rm -rf "$W" exit 0