. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # measurement in this suite is `ast.parse' plus a tree walk # # over a handful of statements -- free beside a start of # # this image's ~63 MB static interpreter -- so the number is # # 400s per start, the per-start budget the neighbouring # # python3 suites use. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for all 149 measurements. # # # # 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-gast 0.5.4 (meta-openembedded meta-python/recipes-devtools/python, # python3-gast_0.5.4.bb, `inherit pypi setuptools3', PyPI sdist) on the # wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # gast is A BIJECTION CLAIM BETWEEN TWO TREE SHAPES. `ast_to_gast' converts the # interpreter's own `ast' tree into a version-independent representation and # `gast_to_ast' converts it back, so a tool that walks Python syntax does not # have to track the node-shape changes between interpreter releases. # # The round trip is therefore the assertion that matters, and it is asserted # THREE WAYS for each snippet, because the three can disagree and the whole # finding of this port is a place where they do: # # dump_ `ast.dump' of the original equals `ast.dump' of the round trip # -- the ticket's own wording, and the STRICTEST of the three. # dumpnorm_ the same comparison with PEP 695's empty `type_params=[]' # discounted -- "structurally identical apart from a field gast # predates". # bytecode_ both trees `compile' and produce THE SAME `co_code' -- the # strongest statement available about whether the difference # costs a caller anything. # # ============================================================================ # THE FINDING: gast 0.5.4 PREDATES PEP 695, AND THIS IMAGE'S PYTHON IS 3.12. # ============================================================================ # # gast 0.5.4 was released in April 2023; Python 3.12 shipped that October with # PEP 695, which added a `type_params' field to `FunctionDef', # `AsyncFunctionDef' and `ClassDef' and four new node types (`TypeAlias', # `TypeVar', `ParamSpec', `TypeVarTuple'). gast's node table is a static list in # `gast/gast.py' and has none of them. # # That splits the corpus cleanly in two, and BOTH halves are asserted: # # EVERY def AND class LOSES `type_params', SILENTLY. `dump_plaindef', # `dump_defaults', `dump_posonly', `dump_starargs', # `dump_decorator', `dump_nestedcls', `dump_docstring' and # `dump_asyncdef' are all `differ' -- the ticket's premise that # the round trip reproduces `ast.dump' is WRONG on this # interpreter, for every function and every class. It costs the # caller nothing they can see: `dumpnorm_*' is `same', # `bytecode_*' is `same', and `unparse_*' is `same', all eight # times. # # A `type' STATEMENT IS REPLACED BY None AND TAKES THE MODULE WITH IT. # `astn.py''s `generic_visit' does `getattr(to, cls)' and # `except AttributeError: return' -- so a node gast has never # heard of becomes a literal `None' inside its parent's body # list. `pep695_typealias_body0' is `NoneType', # `pep695_typealias_compile' is `ValueError' ("None disallowed # in statement list") and `pep695_typealias_unparse' is # `AttributeError'. THIS IS THE UNCOMPILABLE TREE the ticket # says must not be produced, and it is produced without a word. # # A GENERIC def OR class SURVIVES WITH ITS PARAMETERS DELETED. `def f[T](x: T)' # comes back as `def f(x: T)' and compiles -- into a function # whose annotation names a `T' that no longer exists. # `pep695_genericdef_unparse' pins that text, and # `pep695_genericcls_unparse' pins `class C:\n pass' for # `class C[T]'. # # NONE OF THIS IS AN ARM RESULT. It is a fact about gast 0.5.4 against CPython # 3.12 and reproduces identically on x86-64; see the design doc. It is asserted # here rather than worked around because a suite that quietly normalised it away # would also stop noticing the day a gast bump fixes it. # # ============================================================================ # THE TICKET'S SECOND EXPECTATION IS ALSO WRONG, AND IN THE SAME MECHANISM. # ============================================================================ # # "handing `gast_to_ast' a foreign object raises rather than producing an # uncompilable tree". IT DOES NOT RAISE. `gast_to_ast' is # `GAstToAst3().visit(node)', an `ast.NodeTransformer', and the same # `except AttributeError: return' turns `42', `"x"', `object()' and `[1, 2]' # each into `None' -- ten cases, `err_g2a_*' and `err_a2g_*', all `NoneType'. # Neither direction type-checks its argument at all: `err_g2a_ast_tree' and # `err_a2g_gast_tree' show that a tree handed to the WRONG translator comes back # looking plausible. # # Exactly one entry point does check: `get_docstring' raises `TypeError' on a # node that cannot carry one (`err_docstring_wrong_node'). It is asserted so # that the silence of the other two reads as a design choice rather than as a # limit of the shape. # # ============================================================================ # THIS IS ALSO A DIRECT ASSERTION ABOUT THIS ARM'S OWN `ast' MODULE. # ============================================================================ # # gast ships no C and does no arithmetic -- its whole payload is 2496 lines of # Python that rebuild one node type as another. Every value in the corpus is # therefore produced by the INTERPRETER's `ast.parse', `ast.dump', `compile' # over a node tree and `ast.unparse'. A red in the corpus is read as a result # about the ported interpreter on this arm and attributed, not as a port defect # -- which is what the ticket asks for, and why the corpus deliberately spans # the constructs whose node shape is newest: `match' (3.10), `except*' (3.11), # the walrus (3.8), positional-only parameters (3.8) and f-string conversion # and format specs (re-parsed under PEP 701 in 3.12). # # ============================================================================ # PURE PYTHON, WHICH IS WHY IT PORTS AT ALL. # ============================================================================ # # 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 whatever it built to. gast ships seven `.py' files and # nothing else; `pkg-no-so' and `pkg-only-python' assert that through the # packaged file list rather than by reading the recipe. # # A PACKAGE, NOT A FLAT MODULE -- `packages=["gast"]' in setup.py, so # site-packages gets a DIRECTORY. That is the opposite shape from the # single-file python3 ports beside this one, and `is-package' plus the # per-module `mod-*' cases are what fail a release that changed it. # # NO PATCH, NO BBAPPEND AND NO SITE ANSWER. The recipe is meta-openembedded's # exactly as written -- no configure step, no compile step and no C, so there is # nothing on this arm for an override to reach, and no declared runtime # dependency to correct: the built .deb's line is exactly # "Depends: python3-core" and `pkg-depends-exact' asserts it verbatim. MEASURED # rather than read off the recipe: all seven installed modules are # BYTE-IDENTICAL to the sdist the SRC_URI sha256 pins (`cmp' against the # unpacked tarball on the build host). # # NO -ptest ANYWHERE, BY TWO INDEPENDENT ROUTES, and neither is the distro's # doing. This recipe does NOT `inherit ptest' -- unlike several of its # neighbours -- so no split could be created even if the distro carried the # feature; and `packages=["gast"]' keeps the sdist's `tests/' directory out of # the distribution, so upstream's three test modules never land in # site-packages either. Both absences are asserted rather than left to be # discovered. # # ============================================================================ # WHERE THE EXPECTED VALUES COME FROM. # ============================================================================ # # EVERY expected value in this file was MEASURED on an x86-64 control (CPython # 3.12.3, the same minor this image ships) against the tree the wasm32 build # actually installed -- extracted from `python3-gast_0.5.4-r0_wasm32.deb', not # from a pip install and not from the sdist. # # NOTHING here has been measured on wasm32-hwjs: this port's first on-target run # is the batch verify. The expectation that the two agree is a PREDICTION, and # the corpus is precisely where it could fail. P=python3-gast # THE CANARY, which asserts nothing about python3-gast. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require' guards deliberately, # because a `require' that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/gast DI=$SITE/gast-0.5.4.dist-info PKGLIST=/var/lib/dpkg/info/python3-gast.list PROBE=$BST_DATA/python3-gast-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-gast.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import gast' further down is the functional # statement; these are the ones that say WHICH copy answered, so a package that # imported out of some other path on sys.path is distinguishable from one that # imported out of what this image installed. check $P/site-packages-dir test -d "$SITE" # A DIRECTORY, NOT A FILE. setup.py declares `packages=["gast"]'. A release that # flattened the package would change every path assertion below, loudly and # here, rather than nowhere. check $P/package-dir test -d "$PKG" check $P/no-flat-module test ! -e "$SITE/gast.py" # ALL SEVEN SHIPPED MODULES, NAMED. Enumerated rather than counted: a release # that dropped `ast2.py' (the Python 2 translator, which this interpreter never # imports) and one that dropped `gast.py' are very different events, and a count # would report them identically. for _m in __init__ ast2 ast3 astn gast unparser version; do check $P/mod-$_m test -s "$PKG/$_m.py" done # The precompiled bytecode, which is what `import-quiet' below turns on. for _m in __init__ ast2 ast3 astn gast unparser version; do check $P/pyc-$_m test -s "$PKG/__pycache__/$_m.cpython-312.pyc" done # NO SHIPPED SCRIPT. setup.py declares no `entry_points' and no `scripts', so # this package is a library and installs no executable. Asserted so a release # that grew a console_scripts entry point announces itself here. check $P/no-cli test ! -e /usr/bin/gast # UPSTREAM'S OWN TESTS ARE NOT SHIPPED BY THIS PACKAGE, and the reason is # `packages=' and not the distro. The sdist carries `tests/test_api.py', # `tests/test_compat.py' and `tests/test_self.py' beside the package; # `packages=["gast"]' keeps all three out of the distribution. Asserted # individually, because a release that switched to `find_packages()' would ship # them and double this package's footprint. # # ASSERTED OVER THIS PACKAGE'S OWN FILE LISTS, NEVER OVER $SITE/tests. That path # is SHARED and image-wide, and on this image it is already occupied by a # neighbour: `python3-reedsolo' installs a top-level `tests' package there # (`/var/lib/dpkg/info/python3-reedsolo.list' carries `site-packages/tests', # `tests/test_reedsolo.py', `tests/test_creedsolo.py', `tests/perf.py'). A # `test ! -d "$SITE/tests"' therefore measures whichever OTHER package got there # first and reports the result against gast -- which is exactly what it did on # the batch-2271 verify of 2026-09-08, the single red in an otherwise 193/193 # suite. The wheel's `RECORD' is the list that is about THIS package, so it is # the one asked; `no-tests-packaged' below asks dpkg the same question. check_eq $P/no-tests-dir "" \ "$(grep -E '^tests/' "$DI/RECORD" 2>/dev/null)" check_eq $P/no-test-api "" \ "$(grep -E '^tests/test_(api|compat|self)\.py' "$DI/RECORD" 2>/dev/null)" # ...and the second closed route: this recipe does not `inherit ptest' at all, # so no -ptest split exists whatever the distro's DISTRO_FEATURES say. check_eq $P/no-ptest-package "" \ "$(dpkg -s python3-gast-ptest 2>/dev/null | grep '^Status: install ok installed')" check $P/no-ptest-dir test ! -d /usr/lib/python3-gast/ptest # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. On this image a .so # could not be loaded at all -- python3 is ONE STATIC BINARY with an empty # lib-dynload and no dynamic loader (BRINTOS-1476) -- so this is the case that # fails a version bump which grew one, at install rather than at `import'. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The case above # names the failure that is expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. # # DIRECTORIES ARE SKIPPED BY ASKING THE FILESYSTEM, not by pattern. A dpkg # .list carries every path the package owns, directories included and WITHOUT a # trailing slash, so a regex that excluded a trailing `/' would report this # package's own `gast/' and `gast/__pycache__/' as stray files; `test -d' is # what actually answers the question. # # The loop reads from the tmpfs copy staged above and NOT from the rootfs list, # for the dup2(2) reason given above; and it is at the top level so `_bad' # survives it. The metadata directory is version-stamped # (gast-0.5.4.dist-info), never a bare dist-info, so the glob anchors on the # suffix. _bad= while IFS= read -r _p; do [ -d "$_p" ] && continue case "$_p" in *.py|*.pyc) continue ;; *.dist-info/*) continue ;; esac _bad="$_bad $_p" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # NO UPSTREAM TEST FILE REACHED THE IMAGE FROM THIS PACKAGE. The RECORD pair # above asks the wheel; this asks dpkg, which is what actually put files on the # rootfs, and it is the list that would show a distro-side `FILES' addition the # wheel knows nothing about. Both are per-package, which is the whole point -- # see the comment on `no-tests-dir'. check_eq $P/no-tests-packaged "" \ "$(grep -E '/site-packages/tests(/|$)' "$W/pkgf" 2>/dev/null)" # ----------------------------------------------------------- the metadata --- check $P/dist-info-dir test -d "$DI" check_out $P/dist-info-name "Name: gast" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 0.5.4" cat "$DI/METADATA" check_out $P/dist-info-summary \ "Summary: Python AST that abstracts the underlying Python version" \ cat "$DI/METADATA" # A PURE-PYTHON WHEEL. `Root-Is-Purelib: true' is the packaging-side statement # of the same fact `pkg-no-so' asserts through the file list, and `Tag: # py3-none-any' is the statement that nothing here is arch-specific -- which is # why this port needed no patch for wasm32. 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" check_out $P/dist-info-top-level "gast" cat "$DI/top_level.txt" check_out $P/dist-info-license "License: BSD 3-Clause" cat "$DI/METADATA" check $P/dist-info-license-file test -s "$DI/LICENSE" # ------------------------------------------------------- the dpkg status --- dpkg -s python3-gast > "$W/st" 2>/dev/null || : > "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 0.5.4-r0" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" # THE DEPENDS LINE IS `python3-core' AND NOTHING ELSE, ASSERTED WHOLE. # # `setuptools3-base' contributes `RDEPENDS:${PN} += "python3-core"' and the # recipe adds nothing, which is correct here rather than merely accepted: the # probe's `pulled_nonstdlib' and `unreached_*' group measure that the import # closure is `ast' and the stdlib only. Asserted as the WHOLE line, not as a # substring, so a release that ACQUIRED a dependency reds here. check_eq $P/pkg-depends-exact "Depends: python3-core" "$(cat "$W/dep")" # ------------------------------------------------------------- the import --- check $P/import python3 -c 'import gast' # The three public entry points reachable straight off the package root, before # anything about what they return. check $P/import-api python3 -c 'from gast import parse, ast_to_gast, gast_to_ast, dump' # THE IMPORT IS SILENT. gast reaches for `ast.slice', `ast.Index' and # `ast.TypeIgnore' at module scope, several of which are deprecated aliases on a # 3.12 interpreter -- a DeprecationWarning would still let the import SUCCEED # while printing to stderr, so the case above cannot see it. This one asserts # the stderr is empty, which is also what makes a bytecode magic mismatch or a # missing __pycache__ visible here. check_eq $P/import-quiet "" \ "$(python3 -c 'import gast' 2>&1)" # ------------------------------------------------------------- the probe --- # ONE interpreter start for every measurement below; see the header for why. # The bound is generous because a python3 start on this arm is tens of seconds # and the tree walking inside the probe is free beside it. check $P/probe-present test -s "$PROBE" bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE PROBE EITHER RAN OR EVERY CASE BELOW IS UNSCORABLE. Without this guard a # probe that never started emits 149 reds all reading got:'' -- which looks like # a broken package and is actually a broken interpreter start. if ! test -s "$OUT"; then bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" else bst_ok $P/probe-output fi # ...and that it ran to the END, so a probe killed half way through is # distinguishable from one whose values disagree. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "149" "$(pv emitted_count)" # ----------------------------------------------------------- the identity --- # WHICH COPY ANSWERED. If this ever names the probe's own directory instead of # site-packages, the probe has shadowed the package it is measuring -- see the # probe's header for why it is not called gast.py. 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.5.4" "$(pv version_attr)" # WHAT AN `import gast' ACTUALLY COSTS: five of the seven shipped modules, and # the two that are absent are the interesting half. `gast.ast2' is the Python 2 # translator and `gast.gast' selects on `sys.version_info.major', so it is dead # weight on the rootfs and never imported; `gast.unparser' is imported lazily # inside `gast.unparse'. A release that made either eager reds here. check_eq $P/import-closure "gast,gast.ast3,gast.astn,gast.gast,gast.version" \ "$(pv submodules)" # THE INTERPRETER MINOR, asserted rather than assumed. Every `type_params' # finding below is a fact about 3.12 specifically, so a suite red after a # python3 bump is readable as "the interpreter moved" without anyone guessing. check_eq $P/py-minor "12" "$(pv py_minor)" # ---------------------------------------------------------------- the API --- # Every public callable and re-export reachable at the package root, asserted # before anything about what they return. for _n in parse ast_to_gast gast_to_ast dump unparse literal_eval \ get_docstring NodeVisitor NodeTransformer iter_fields \ copy_location fix_missing_locations increment_lineno \ walk iter_child_nodes; do check_eq $P/api-$_n "yes" "$(pv has_$_n)" done # `gast.dump' IS `ast.dump' -- the same function object. gast nodes subclass # ast.AST, so the stdlib dumper walks them unchanged and gast ships no dumper of # its own. Stated because the `gdump-*' group below pins its output, and a # reader should know that what is pinned is the stdlib's formatter over gast's # field lists. check_eq $P/api-dump-is-ast-dump "yes" "$(pv dump_is_ast_dump)" # NO `__all__', so `from gast import *' takes `gast.gast''s whole public # surface. The one namespace surprise here, so it is stated rather than met. check_eq $P/api-no-all "no" "$(pv has_all_attr)" # ------------------------------------------------ the field lists, the point --- # gast's claim is that ONE node shape serves every interpreter version, and it # buys that by WIDENING some nodes and DELETING others. These are the # measurement of that trade rather than a description of it. # # `gast.Name' has FOUR fields where `ast.Name' has two, because gast folds # `ast.arg' into `Name': a parameter is a Name carrying `ctx=Param()' and the # annotation the `arg' used to hold. `gast_has_arg_node=no' is the other half of # that statement. check_eq $P/fields-name "id,ctx,annotation,type_comment" "$(pv gast_name_fields)" check_eq $P/fields-arguments \ "args,posonlyargs,vararg,kwonlyargs,kw_defaults,kwarg,defaults" \ "$(pv gast_arguments_fields)" # THE ONE-FIELD DIFFERENCE BEHIND EVERY `rt-*=differ' BELOW, asserted from both # sides so the diff is in the suite rather than in a reader's head. check_eq $P/fields-functiondef \ "name,args,body,decorator_list,returns,type_comment" \ "$(pv gast_functiondef_fields)" check_eq $P/fields-ast-functiondef \ "name,args,body,decorator_list,returns,type_comment,type_params" \ "$(pv ast_functiondef_fields)" # THE FOUR PEP 695 NODE TYPES gast 0.5.4 HAS NEVER HEARD OF. Named negatively, # because their absence is what the `pep695-*' group measures the cost of. A # gast bump that added them flips these four and the `rt-*' group together, # which is exactly the coupling a reader needs to see. for _n in TypeAlias TypeVar ParamSpec TypeVarTuple; do check_eq $P/no-node-$_n "no" "$(pv gast_has_$_n)" done check_eq $P/no-arg-node "no" "$(pv gast_has_arg_node)" # ============================================================================ # THE ROUND TRIP, SNIPPET BY SNIPPET. # ============================================================================ # # Read the four keys of each snippet TOGETHER: `rt-' is the ticket's strict # assertion, `rtnorm-' is the same thing with PEP 695's empty `type_params' # discounted, `compile-' says the round-tripped tree is still compilable, and # `bc-' says it compiles to the same bytecode. # # EIGHT OF THE SIXTEEN ARE `differ' AND THAT IS THE MEASURED ANSWER, not a # tolerance: every one of them contains a `def' or a `class', and every one of # them is `same' on the other three keys. The eight that are `same' on all four # contain neither. # --- the eight that carry a def or a class: strict differs, the rest agree --- for _k in plaindef defaults posonly starargs decorator nestedcls docstring asyncdef; do check_eq $P/rt-$_k "differ" "$(pv dump_$_k)" check_eq $P/rtnorm-$_k "same" "$(pv dumpnorm_$_k)" check_eq $P/compile-$_k "ok" "$(pv compile_$_k)" check_eq $P/bc-$_k "same" "$(pv bytecode_$_k)" done # --- the eight that carry neither: the round trip is exact on all four --- # These are the constructs the ticket names for their between-version node-shape # churn, and they are where a wasm32 `ast' that diverged would show: `match' # (3.10), `except*' (3.11), the walrus and slices (3.8), and an f-string with # both a conversion and a nested format spec, which 3.12 re-parses under PEP 701 # so that its `JoinedStr' children carry real source positions. for _k in annassign compmulti withitems fstring match trystar lambda walrus; do check_eq $P/rt-$_k "same" "$(pv dump_$_k)" check_eq $P/rtnorm-$_k "same" "$(pv dumpnorm_$_k)" check_eq $P/compile-$_k "ok" "$(pv compile_$_k)" check_eq $P/bc-$_k "same" "$(pv bytecode_$_k)" done # THE ROUND TRIP STATED AS SOURCE. `ast.unparse' over both trees is the case # that says a `rt-*=differ' cost the caller nothing they can read back -- the # dropped field is an empty list on a non-generic definition, so the text is # byte-identical. Four snippets rather than sixteen: it is a second whole walk # of the tree and it adds a distinct fact only where a field was dropped. for _k in plaindef decorator nestedcls asyncdef; do check_eq $P/unparse-$_k "same" "$(pv unparse_$_k)" done # ============================================================================ # PEP 695 -- WHERE THE ROUND TRIP GENUINELY BREAKS. # ============================================================================ # # Split out of the corpus because these fail in TWO DIFFERENT WAYS and the # difference is what a caller needs to know. # # A `type' STATEMENT IS DESTROYED. `ast.TypeAlias' has no gast counterpart, so # `astn.py''s `generic_visit' returns None and the module body ends up holding a # literal `None'. `compile' then refuses the whole module and `ast.unparse' # raises on the None. This is the uncompilable tree the ticket says must not be # produced -- produced, and without a word. check_eq $P/pep695-typealias-node "NoneType" "$(pv pep695_typealias_body0)" check_eq $P/pep695-typealias-compile "ValueError" "$(pv pep695_typealias_compile)" check_eq $P/pep695-typealias-unparse "AttributeError" "$(pv pep695_typealias_unparse)" # A GENERIC def OR class SURVIVES, WITH ITS TYPE PARAMETERS DELETED. Worse than # the case above in one respect: it compiles. `def f[T](x: T) -> T' comes back as # a function whose annotation names a `T' that no longer exists anywhere, and # nothing reports it. The unparsed text is pinned so the loss is legible in the # suite output rather than inferable from a verdict word. `\n' is the probe's # escape for the newline -- see its header. check_eq $P/pep695-genericdef-node "FunctionDef" "$(pv pep695_genericdef_body0)" check_eq $P/pep695-genericdef-compile "ok" "$(pv pep695_genericdef_compile)" check_eq $P/pep695-genericdef-unparse 'def f(x: T) -> T:\n return x' \ "$(pv pep695_genericdef_unparse)" check_eq $P/pep695-genericcls-node "ClassDef" "$(pv pep695_genericcls_body0)" check_eq $P/pep695-genericcls-compile "ok" "$(pv pep695_genericcls_compile)" check_eq $P/pep695-genericcls-unparse 'class C:\n pass' \ "$(pv pep695_genericcls_unparse)" # ------------------------------------------------- the unification, measured --- # gast's central move, asserted directly: `ast.arg' DOES NOT EXIST in gast. A # parameter is a `gast.Name' with `ctx=Param()' carrying the annotation the # `arg' used to hold -- which is the whole reason `gast.Name' has four fields. check_eq $P/param-is-name "Name" "$(pv param_node_type)" check_eq $P/param-ctx-is-param "Param" "$(pv param_ctx_type)" check_eq $P/param-keeps-annotation "Name" "$(pv param_annotation_type)" # ---------------------------------------------- gast.dump, pinned exactly --- # The ticket asks for `gast.dump' output asserted against literals for the small # cases. Each of these carries the widened fields above in its text -- the # `annotation=None, type_comment=None' on every Name, and the `kind=None' on # every Constant -- so a release that narrowed `gast.Name' back to ast's two # fields reds here with the difference visible in the message rather than only # denied. check_eq $P/gdump-num \ "Module(body=[Expr(value=Constant(value=1, kind=None))], type_ignores=[])" \ "$(pv gdump_num)" check_eq $P/gdump-name \ "Module(body=[Expr(value=Name(id='x', ctx=Load(), annotation=None, type_comment=None))], type_ignores=[])" \ "$(pv gdump_name)" check_eq $P/gdump-assign \ "Module(body=[Assign(targets=[Name(id='x', ctx=Store(), annotation=None, type_comment=None)], value=Constant(value=1, kind=None), type_comment=None)], type_ignores=[])" \ "$(pv gdump_assign)" check_eq $P/gdump-annassign \ "Module(body=[AnnAssign(target=Name(id='x', ctx=Store(), annotation=None, type_comment=None), annotation=Name(id='int', ctx=Load(), annotation=None, type_comment=None), value=Constant(value=3, kind=None), simple=1)], type_ignores=[])" \ "$(pv gdump_annassign)" # ---------------------------------------------------------------- the errors --- # A SYNTAX ERROR RAISES `SyntaxError' WITH A POSITION. `gast.parse' is # `ast.parse' followed by the translation, so the raise comes out of the # interpreter BEFORE gast is reached -- which makes these three a direct # assertion about this arm's parser, and among the likeliest places in this # suite for the arm to disagree. check_eq $P/err-syntax "SyntaxError" "$(pv err_syntax)" check_eq $P/err-syntax-lineno "1" "$(pv err_syntax_lineno)" check_eq $P/err-syntax-offset "7" "$(pv err_syntax_offset)" # A FOREIGN OBJECT DOES NOT RAISE -- IT COMES BACK AS None. Ten cases, both # directions, five argument shapes, all `NoneType'. The ticket expects a raise; # there is none, and the silent None is the same mechanism that destroys a # `type' statement above. Asserted in BOTH directions because `ast_to_gast' has # the identical `except AttributeError: return' and neither is safer than the # other. for _k in int str object none list; do check_eq $P/err-g2a-$_k "NoneType" "$(pv err_g2a_$_k)" check_eq $P/err-a2g-$_k "NoneType" "$(pv err_a2g_$_k)" done # ...AND A TREE OF THE WRONG SHAPE IS ACCEPTED WITHOUT COMPLAINT. gast nodes ARE # ast.AST subclasses, so neither translator can tell which shape it was handed # and each rebuilds what it recognises. A caller who double-translates gets a # plausible tree back rather than an error. check_eq $P/err-g2a-ast-tree "Module" "$(pv err_g2a_ast_tree)" check_eq $P/err-a2g-gast-tree "Module" "$(pv err_a2g_gast_tree)" # `get_docstring' IS THE ONE ENTRY POINT THAT TYPE-CHECKS ITS ARGUMENT. It is # asserted so that the silence of the two above reads as a design choice rather # than as a limit of the shape -- gast CAN raise, and here it does. check_eq $P/err-docstring-wrong-node "TypeError" "$(pv err_docstring_wrong_node)" check_eq $P/ok-docstring-module "d" "$(pv ok_docstring_module)" # THE CONSTRUCTORS ARE ALL-OR-NOTHING, AND THE MESSAGE CARRIES THE ARITY. # `_make_node' builds an `__init__' that accepts either zero positional # arguments or exactly as many as the node has fields. The `4' in the message is # `gast.Name''s field count, so this case moves the day the node shape does -- # which is why the message is pinned whole rather than just the exception type. check_eq $P/err-ctor-arity "TypeError" "$(pv err_ctor_arity)" check_eq $P/err-ctor-arity-msg \ "Name constructor takes either 0 or 4 mandatory arguments" \ "$(pv err_ctor_arity_msg)" check_eq $P/ok-ctor-full "Name" "$(pv ok_ctor_full)" # ------------------------------------------------------- the import closure --- # NOTHING OUTSIDE THE STANDARD LIBRARY IS PULLED IN, which is what makes # `Depends: python3-core' the whole of the line and this a one-package port. # # Scored against `sys.stdlib_module_names' rather than a literal list: what the # import delta CONTAINS depends on what the interpreter had already loaded, so a # literal here would red on target for a reason that has nothing to do with gast. check_eq $P/import-no-thirdparty "" "$(pv pulled_nonstdlib)" check_eq $P/import-needs-ast "yes" "$(pv needs_ast)" # ...and the splits a heavier port would have reached, named NEGATIVELY. None is # touched, which is the packaging half of the Depends line above. for _n in gzip tarfile zipfile socket ssl tempfile pprint shlex json subprocess; do check_eq $P/import-unreached-$_n "yes" "$(pv unreached_$_n)" done rm -rf "$W"