. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts, and they are the whole cost. Every # # other case below reads the filesystem or dpkg with shell # # builtins, or greps the output of the second start. # # # # The two are: a bare `import' (one case, so that an import # # failure is legible without reading a transcript) and the # # probe, which takes ONE start for a hundred and fifty # # measurements. # # # # THE WORK INSIDE THE PROBE IS NOT ONLY ATTRIBUTE READS, and # # that is worth saying because it is where this suite differs # # from the other pure-Python ports on this image. `fields' # # GENERATES ITS `__init__' AS SOURCE TEXT and runs the # # interpreter's own `compile()' and `exec' on it, once per # # sealed class -- and the probe seals about thirty. That is # # still small beside an interpreter start here, which is tens # # of seconds before the first line of the program runs, but # # it is not free the way a getattr is. # # # # NOTHING HERE SLEEPS and nothing starts a thread, opens a # # socket or touches a device. # # # # 1200 is 600s per start, the same per-start budget # # 99-python3-cached-property and 99-python3-annotated-types # # give theirs. # # # # A suite-timeout red is a batch-wide cost, so if the two # # 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-fields 5.0.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-fields_5.0.0.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHAT THAT MAKES THE SUITE ASSERT. # ============================================================================ # # A metaclass (`_Factory') turns ATTRIBUTE ACCESS into a field declaration -- # `Fields.a.b.c' -- and a `sealer' turns that declaration into a real class with # a generated `__init__', a full set of comparison methods and a `__repr__'. # SUBCLASSING IS WHAT SEALS: `class Pizza(Fields.name.size)' runs the sealer, and # Pizza's base is the class the sealer built. # # So almost nothing here is a statement about a module-level object. Every # behavioural case below asserts something about a class the probe BUILT AT # RUNTIME from a declaration -- which is why the probe is the suite and the suite # is mostly `check_eq' against its transcript. # # THE ONE THING THIS PORT DOES THAT THE OTHER PURE-PYTHON PORTS ON THIS IMAGE DO # NOT: it runs `compile()' and `exec' on source text it assembles as a string, # every time a class is sealed. `init-func-name', `init-params', # `init-filename', `init-in-linecache' and `init-source-*' are the cases that say # that path works on this arm; if the wasm32 interpreter could not compile at # runtime, this package would be the one that noticed. # # ============================================================================ # WHERE THE EXPECTED VALUES COME FROM. # ============================================================================ # # MEASURED ON AN x86-64 CONTROL AT THE SAME VERSION, not read off the source. # The control ran THIS SUITE'S OWN PROBE against fields 5.0.0 unpacked from the # same PyPI sdist the recipe fetches (sha256 31d4aa03d8d4...be5727), under # CPython 3.12 -- the same interpreter minor this image ships. The control was # then RE-RUN against THIS BUILD's own .deb payload rather than the sdist, and # produced a BYTE-IDENTICAL transcript apart from the module's path: the wheel # install alters nothing, and the three shipped `.py' files are byte-identical # to the sdist's. # # READING THE SOURCE WAS NOT ENOUGH, and four of the values below are why. Each # was predicted one way from `__init__.py' and measured the other: # # * `Fields.a[1].b' IS ACCEPTED. The guard in `_Factory.__getattr__' reads as # though a required field after a defaulted one were refused there, and it is # not: the guard is `cls.__defaults and cls.__last_field is not None', and # `__getitem__' builds its successor WITHOUT a `last_field', so the second # half is false at exactly the moment it matters. The refusal lands at SEAL # time instead, from `__invert__'. `required-after-default-declares' and # `required-after-default-seal-error' are the two halves, and the point of # asserting both is that the declaration which is wrong and the line the user # is told about are different lines of their program. # # * EVERY CLASS `class_sealer' BUILDS IS NAMED `FieldsBase'. The sealer # computes `FieldsBase_for__name__size' and then writes `class FieldsBase(base)' # as a literal, using the computed name only as a key in the generated # source's `super()' call. So the per-field name never reaches `__name__' and # two sealed classes are indistinguishable in a traceback by it. # `tuple_sealer' does the opposite -- `type(baseclass_name, ...)' -- so # `class-sealer-result' and `tuple-sealer-result' below differ, from one # computed string. # # * A `Tuple' INSTANCE HAS A `__dict__'. `__slots__ = ()' on the generated base # buys nothing, because the user's own subclass declares no `__slots__' and # -- unlike `SlotsFields', which installs a metaclass to force one -- nothing # puts one there. So an UNDECLARED attribute assigns fine, and the refusal # `tuple-assign-error' catches is the read-only `property', not immutability. # `Tuple' buys the tuple's packed storage for the fields and none of the # memory saving `__slots__' is usually reached for. # # * A MUTABLE DEFAULT IS SHARED BETWEEN INSTANCES. `Fields.items[[]]' reads # like a per-instance empty list and is the classic shared mutable default: # the object goes into the generated function's `__defaults__' once, at seal # time. `mutable-default-shared' is `yes'. # # ============================================================================ # THE DECLARED DEPENDENCY IS SHORT, AND HERE IT IS ALSO CORRECT. # ============================================================================ # # The .deb's control stanza reads `Depends: python3-core' and nothing else -- # setup.py declares no `install_requires' and the wheel's METADATA carries ZERO # `Requires-Dist' lines. `fields/__init__.py' imports `linecache', `sys', # `itertools', `operator' and `zlib' at top level, all UNGUARDED. # # On this image that set is covered: `linecache' and `operator' are in the # python3-core sub-package (verified against this build's own # python3-core .deb), and `zlib' is a C extension COMPILED INTO the interpreter # rather than packaged separately -- python3 here is one static binary with an # empty lib-dynload (BRINTOS-1476), which is why no `python3-zlib' package # exists on this image at all. # # That is a different answer from `99-python3-cached-property''s, whose # `dep-no-threading' pins a genuine omission, and the difference is worth having # in writing rather than left for a reader to assume symmetry. The pair asserted # below is `dep-python3-core' -- the declared dependency, present -- and # `dep-no-zlib-package', the ABSENCE of a python3-zlib package, which is what # makes the short Depends line correct here instead of lucky. # # ============================================================================ # THE PY2 BACKPORT SHIPS AND IS DEAD. # ============================================================================ # # `py2ordereddict.py' is 4094 bytes of payload behind `try: from collections # import OrderedDict / except ImportError:', and `collections.OrderedDict' has # existed throughout 3.x -- so the fallback is never imported on any image this # distro builds. Its `.pyc' is compiled and shipped too. Asserted three ways, # because each alone reads as something else: the file IS in the payload, the # module is NOT in `sys.modules' after `import fields', and the `OrderedDict' # the package actually uses comes from `collections'. # # Recorded rather than repaired: dropping a file from an upstream sdist's # payload would be a divergence carried for ever, for 4 KB. P=python3-fields # ============================================================================ # 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/fields DI=$SITE/fields-5.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-fields.list PROBE=$BST_DATA/python3-fields-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-fields.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- payload --- # WHERE THE PACKAGING SAYS THE PAYLOAD LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the package misbehaves. check $P/site-packages-dir test -d "$SITE" # IT IS A PACKAGE, NOT A SINGLE MODULE. setup.py uses `find_packages("src")' # with `package_dir={"": "src"}', so the payload is a DIRECTORY. Asserted BOTH # ways -- the directory is there and a top-level `fields.py' is not -- because a # release that flattened to one module would satisfy `import' and every # behavioural case below while relocating the whole payload. check $P/package-dir test -d "$PKG" check $P/not-a-module test ! -e "$SITE/fields.py" # THE THREE SOURCE FILES, AND THEIR EXACT SIZES. The sizes are pinned rather # than floored: a truncated or partially written payload passes `test -s' and # fails only here. Each figure is the sdist's own byte count, is what the # wheel's RECORD declares, and the shipped files were verified byte-identical to # the sdist's. check $P/init-present test -s "$PKG/__init__.py" check_eq $P/init-size "16029" \ "$(wc -c < "$PKG/__init__.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) check_eq $P/extras-size "1685" \ "$(wc -c < "$PKG/extras.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # THE DEAD PY2 BACKPORT IS PART OF THE PAYLOAD. See THE PY2 BACKPORT SHIPS AND # IS DEAD in the header -- this is the first of that finding's three cases, and # `py2ordereddict-unimported' below is the one that says it is never loaded. check_eq $P/py2ordereddict-size "4094" \ "$(wc -c < "$PKG/py2ordereddict.py" 2>/dev/null | tr -d ' ')" # BST-REDIRECT-OK: forked -- inside $( ) # NO COMPILED EXTENSION SHIPS, and this is the case that would notice a release # that grew one. On this image a `.so' in site-packages is not a speed-up, it is # an ImportError: python3 here is one static binary with an empty lib-dynload # (BRINTOS-1476). Asserted over the payload rather than over the recipe. check_eq $P/no-so "" "$(find "$PKG" -name '*.so' 2>/dev/null)" # THE BYTE-COMPILED COPIES land in the PACKAGE'S OWN `__pycache__' -- which is # what a package does, and is why these paths differ from a single-module port's. # All three are compiled, including the one that is never imported. check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-extras test -s "$PKG/__pycache__/extras.cpython-312.pyc" check $P/pycache-py2od test -s "$PKG/__pycache__/py2ordereddict.cpython-312.pyc" # NO EXECUTABLE. setup.py declares an EMPTY `entry_points' dict and no # `scripts', so nothing lands in /usr/bin. Asserted so a future release that # started shipping one is noticed rather than silently unexercised. check $P/no-bin-script test ! -e /usr/bin/fields # --------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-wheel test -f "$DI/WHEEL" check_out $P/dist-info-version "Version: 5.0.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: fields" cat "$DI/METADATA" # THE LICENSE AND THE AUTHORS FILE BOTH LAND AT THE ROOT of dist-info -- not # under `licenses/', which is where hatchling puts them. This is setuptools' # older layout, and `LICENSE' is where the recipe's LIC_FILES_CHKSUM md5 points. # # AUTHORS.rst is there because setup.py sets `include_package_data=True' and # MANIFEST.in names it: a non-licence document carried into the installed # metadata. Pinned so the payload census below has a reason to expect it. check $P/license-file test -s "$DI/LICENSE" check $P/authors-file test -s "$DI/AUTHORS.rst" # THERE IS NO `INSTALLER' FILE. Most wheel installs drop one naming the # installer; this payload has six dist-info files and that is not one of them, # which is a fact about how the recipe installs rather than about the package. # Pinned as an absence so the census below is an exact set rather than a # permissive pattern. check $P/no-installer test ! -e "$DI/INSTALLER" # `top_level.txt' NAMES THE PACKAGE. The distribution and the import name # coincide here, so this file is uninteresting except as the thing that would # change if they stopped coinciding. check_out $P/top-level "fields" cat "$DI/top_level.txt" # PURELIB, STATED BY THE PACKAGING ITSELF -- and the wheel is UNIVERSAL. # setup.cfg carries `[bdist_wheel] universal = 1', so bdist_wheel stamps TWO # tags: `py2-none-any' AND `py3-none-any'. # # The py2 tag is dead weight on an image whose only interpreter is 3.12 -- but it # is what the package claims about itself, and it is the same py2 support that # `py2ordereddict.py' and the `PY2' branch in `make_init_func' exist for. Both # tags are pinned separately so that losing one is one red rather than a rewrite. check_out $P/wheel-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/wheel-tag-py2 "Tag: py2-none-any" cat "$DI/WHEEL" check_out $P/wheel-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/wheel-generator "Generator: bdist_wheel (0.42.0)" cat "$DI/WHEEL" # NOTHING IS REQUIRED, and the METADATA says so by saying nothing: setup.py # declares an EMPTY `install_requires' list, so there is not one `Requires-Dist' # line. Asserted as a COUNT of zero rather than as an absent string, because a # `check_out' cannot express "no line matches". # # This is the wheel's half of the dependency finding; `dep-python3-core' and # `dep-no-zlib-package' below are the .deb's. See THE DECLARED DEPENDENCY IS # SHORT in the header. check_eq $P/meta-no-requires "0" \ "$(grep -c '^Requires-Dist' "$DI/METADATA" 2>/dev/null)" # BST-REDIRECT-OK: forked -- inside $( ) # ------------------------------------------------------------ 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" # Every payload path is under site-packages and is source, bytecode or metadata. # Anything else means the build produced something this port did not expect and # did not measure. `.rst' is admitted for the AUTHORS file pinned above. _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) ;; */METADATA|*/WHEEL|*/RECORD|*/LICENSE|*/LICENSE.*) ;; */top_level.txt|*/AUTHORS.rst) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-fields$/,/^$/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" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE DEPENDENCY PAIR. See THE DECLARED DEPENDENCY IS SHORT in the header -- # these two are one finding written as two cases, and reading either alone gets # it wrong. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/dep-line test -s "$W/dep" check_out $P/dep-python3-core "python3-core" cat "$W/dep" # AND WHY THAT SHORT LINE IS CORRECT RATHER THAN LUCKY. `fields' imports `zlib' # unguarded at top level and the packaging never declares it -- which on this # image is right, because `zlib' is compiled INTO the static interpreter and # there is no python3-zlib package to depend on. Asserted as that absence: if a # later python3 recipe ever split zlib out into its own sub-package, this case # goes red and is the one that says the Depends line now has a hole in it. check_eq $P/dep-no-zlib-package "" \ "$(sed -n '/^Package: python3-zlib$/p' /var/lib/dpkg/status 2>/dev/null)" # BST-REDIRECT-OK: forked -- inside $( ) # ------------------------------------------------------------------ import --- # ONE CASE OF ITS OWN, and the first of two python3 starts. Pure Python, so this # is expected to be uneventful -- it is here so that an import failure is legible # without reading a probe transcript. check $P/import python3 -c 'import fields' # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" exit 0 fi bst_ok $P/probe-present # THE SECOND START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so 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. # # The probe EXITS 0 even when the import fails, so this case passing means "the # probe ran", not "the package works" -- `import-ok' below is what carries that. 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 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)" _imp=$(pv import_ok) check_eq $P/import-ok "yes" "$_imp" if [ "$_imp" != "yes" ]; then # The package did not import. Skip -- not red -- every behavioural case, each # with the reason attached: they are all one fact, and a hundred reds would # bury it rather than report it. `import' and `import-ok' above are the two # reds that carry the verdict, and the design doc attributes them. _why="fields did not import: $(pv import_error)" for _c in probe-count module-file is-package version-attr dist-version \ version-agrees has-all all-type all-names public-names \ all-missing-public py2-flag ordereddict-from-collections \ py2ordereddict-unimported extras-not-autoimported \ fields-is-class fields-metaclass bare-factory-name \ chain-factory-name chain-links-distinct empty-factory-error \ dup-required-error dup-default-error \ required-after-default-declares required-after-default-seal-error \ default-without-field-error dunder-not-a-field \ pizza-name pizza-base-name pizza-base-count pizza-metaclass \ pizza-pos-name pizza-pos-size pizza-kw-name pizza-kw-size \ pizza-repr pizza-str-is-repr repr-uses-repr repr-has-all-fields \ missing-field-error unexpected-field-error extra-positional-error \ init-func-name init-params init-filename init-in-linecache \ init-source-lines init-source-has-super \ eq-equal-fields eq-differing-fields ne-differing-fields \ eq-is-not-identity lt gt le-equal ge-equal eq-foreign \ lt-foreign-error hash-equal hash-differs hash-is-tuple-hash \ hash-dedups \ node-repr node-defaults-left node-mixed-repr node-init-defaults \ mutable-default-shared \ inline-repr inline-class-name seal-memoised \ seal-not-shared-across-chains \ tuple-issubclass tuple-a tuple-b tuple-as-tuple tuple-len \ tuple-index tuple-unpacks tuple-repr tuple-attr-is-property \ tuple-assign-error tuple-has-dict tuple-undeclared-attr-ok \ tuple-base-slots tuple-eq-plain-tuple tuple-eq-same-fields \ tuple-hash-equal tuple-getnewargs tuple-new-name \ slots-x slots-y slots-repr slots-subclass-slots slots-meta-name \ slots-has-dict slots-new-attr-error slots-field-writable \ slots-eq slots-hash-equal \ bare-a bare-eq bare-repr-is-default \ conv-as-dict conv-as-tuple conv-as-dict-type conv-still-comparable \ mixin-eq-on-subset mixin-ne-on-subset mixin-carries-all-fields \ mixin-base-count printable-mixin-repr \ inherit-name inherit-age inherit-field-count inherit-bogus-error \ inherit-mro-len plain-multiple-inheritance-error \ layout-conflict-error \ custom-init-ok custom-init-raises custom-init-extra-arg \ make-init-func-error make-init-func-keys \ make-init-func-global-keys \ sealer-names class-sealer-result tuple-sealer-result \ factory-sealer-fields factory-sealer-defaults factory-sealer-base \ namespace-repr namespace-eq namespace-hashable \ namespace-hash-error namespace-eq-foreign-error \ extras-import-ok extras-error extras-names \ extras-validation-error-base \ regex-accepts regex-rejects regex-requires-defaults \ regex-combined-ok regex-combined-rejects \ py-version py-maxsize-bits; do bst_skip $P/$_c "$_why" done exit 0 fi # The probe emitted every key it intended to. A probe that returned 0 having # skipped a section would otherwise be indistinguishable from one that ran whole. check_eq $P/probe-count "149" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. The path is the target's, not the control's, so it is # asserted directly rather than compared. check_eq $P/module-file "$PKG/__init__.py" "$(pv module_file)" # A PACKAGE, NOT A MODULE -- the payload half of this is `package-dir' / # `not-a-module' above; this is the same claim as the interpreter sees it. check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version-attr "5.0.0" "$(pv version_attr)" check_eq $P/dist-version "5.0.0" "$(pv dist_version)" # `version-agrees' is the case that says the wheel was cut from the source this # package is: `__version__' is carried in `__init__.py' and setup.py repeats the # number by hand, so the two CAN disagree. check_eq $P/version-agrees "yes" "$(pv version_agrees)" # ------------------------------------------------------- the export surface --- # THERE IS AN `__all__', AND IT IS A TUPLE rather than the list most packages # ship. Pinned as both, and pinned WHOLE and in order: `from fields import *' is # the documented entry, and a release that reordered or retyped it changes what # a star-import binds. check_eq $P/has-all "yes" "$(pv has_all)" check_eq $P/all-type "tuple" "$(pv all_type)" check_eq $P/all-names \ "BareFields,ComparableMixin,ConvertibleFields,ConvertibleMixin,Fields,PrintableMixin,SlotsFields,Tuple,factory,make_init_func,class_sealer,slots_class_sealer,tuple_sealer,Namespace" \ "$(pv all_names)" # THE MODULE'S PUBLIC SURFACE IS WIDER THAN `__all__', and the gap is the # finding rather than the tidiness complaint it looks like. `InheritableFields' # -- the ONLY factory that supports the multiple-inheritance form asserted by # `inherit-*' below, and the one upstream's own `test_multiple_inheritance' # uses -- is a public module attribute and is NOT in `__all__'. So the documented # `from fields import *' does not bind it, and a user who reaches for it must # know to import it by name. # # The rest of the gap is incidental: `OrderedDict', `chain', `itemgetter', # `linecache', `sys' and `zlib' are the module's own imports leaking into its # namespace, and `PY2'/`MISSING' are internals. Both sets are pinned so the # omission is a stated fact rather than something a reader has to diff for. check_eq $P/public-names \ "BareFields,ComparableMixin,ConvertibleFields,ConvertibleMixin,Fields,InheritableFields,MISSING,Namespace,OrderedDict,PY2,PrintableMixin,SlotsFields,Tuple,chain,class_sealer,factory,itemgetter,linecache,make_init_func,slots_class_sealer,sys,tuple_sealer,zlib" \ "$(pv public_names)" check_eq $P/all-missing-public \ "InheritableFields,MISSING,OrderedDict,PY2,chain,itemgetter,linecache,sys,zlib" \ "$(pv all_missing_public)" # ------------------------------------------------------- the py2 leftovers --- # See THE PY2 BACKPORT SHIPS AND IS DEAD in the header. `PY2' is false, so the # branch in `make_init_func' that would `exec' a Python 2 statement -- a # SyntaxError under 3.x, surviving only because it sits inside a string # literal -- is never taken. Every generated `__init__' below came from the # other half of that branch. check_eq $P/py2-flag "no" "$(pv py2_flag)" # THE BACKPORT IS NEVER LOADED, and the `OrderedDict' the package uses is the # stdlib's. Two cases because each alone reads as something else: a module # absent from `sys.modules' could mean the import failed, and an `OrderedDict' # from `collections' could still coexist with a loaded backport. check_eq $P/ordereddict-from-collections "yes" "$(pv ordereddict_from_collections)" check_eq $P/py2ordereddict-unimported "no" "$(pv py2ordereddict_imported)" # `extras' IS NOT IMPORTED BY THE PACKAGE'S `__init__', so `import fields' does # not bring `re' in with it and `fields.RegexValidate' does not exist until # somebody imports `fields.extras' by name. The `extras-*' and `regex-*' cases # at the end of this suite are the other half -- they import it and it works. check_eq $P/extras-not-autoimported "no" "$(pv extras_autoimported)" # ------------------------------------------------------- the factory itself --- # `Fields' IS A CLASS whose metaclass is `_Factory', not an instance of one. # That is what makes `Fields.a.b.c' work at all: each getattr returns a NEW # class, and subclassing the last one seals it. check_eq $P/fields-is-class "yes" "$(pv fields_is_class)" check_eq $P/fields-metaclass "_Factory" "$(pv fields_metaclass)" # THE FACTORY'S OWN NAME CARRIES THE SEALER AND THE CHAIN, and it is the name a # `TypeError' from an unsealed factory prints. An empty factory names only the # sealer; a chained one appends the fields in declaration order -- so this pair # is also the declaration order observed from a third side. check_eq $P/bare-factory-name "Fields" "$(pv bare_factory_name)" check_eq $P/chain-factory-name "Fields.a.b.c" "$(pv chain_factory_name)" # EACH LINK IS A DISTINCT CLASS, rebuilt on every getattr -- the chain is not # memoised and `Fields.a' twice is two objects. This is what lets two # declarations that share a prefix not interfere, and it is why the factory is # safe to keep as a module-level singleton. check_eq $P/chain-links-distinct "yes" "$(pv chain_links_distinct)" # THE REFUSALS, PINNED BY MESSAGE. All but one are `TypeError', so the TEXT is # the only thing that tells them apart; a case asserting the type alone would # pass with any of them. check_eq $P/empty-factory-error \ "TypeError: You're trying to use an empty Fields factory !" \ "$(pv empty_factory_error)" check_eq $P/dup-required-error \ "TypeError: Field 'a' is already specified as required." \ "$(pv dup_required_error)" check_eq $P/dup-default-error \ "TypeError: Field 'a' is already specified with a default value (1)." \ "$(pv dup_default_error)" check_eq $P/default-without-field-error \ "TypeError: Can't set default 1. There's no previous field." \ "$(pv default_without_field_error)" # THE DEFERRED REFUSAL, and one of the four values reading the source got wrong # -- see WHERE THE EXPECTED VALUES COME FROM. `Fields.a[1].b' is ACCEPTED at # declaration time and refused at SEAL time, so the line that is wrong and the # line the user is told about are different lines of their program. Both halves # are asserted; either alone reads as the opposite finding. check_eq $P/required-after-default-declares "none" \ "$(pv required_after_default_declares)" check_eq $P/required-after-default-seal-error \ "TypeError: Can't add required fields after fields with defaults." \ "$(pv required_after_default_seal_error)" # DUNDER LOOKUP IS NOT A FIELD DECLARATION. `__getattr__' short-circuits names # that are dunders, so a missing one raises AttributeError rather than declaring # a field called `__nonexistent__'. Without that, every protocol probe on the # factory -- `copy', `pickle', `inspect' -- would silently extend the # declaration, and the class would seal with fields nobody wrote. check_eq $P/dunder-not-a-field "AttributeError" "$(pv dunder_not_a_field)" # ---------------------------------------- Fields: the generated class --- # THE DOCUMENTED ENTRY POINT: `class Pizza(Fields.name.size)'. check_eq $P/pizza-name "Pizza" "$(pv pizza_name)" check_eq $P/pizza-base-count "1" "$(pv pizza_base_count)" # THE GENERATED BASE IS NAMED `FieldsBase' -- the second value the source reads # the other way. See WHERE THE EXPECTED VALUES COME FROM: `class_sealer' # computes `FieldsBase_for__name__size' and then discards it for a literal class # statement, so EVERY class this sealer produces shares one name and two of them # cannot be told apart in a traceback by it. `tuple-sealer-result' below is the # contrast, from the same computed string. check_eq $P/pizza-base-name "FieldsBase" "$(pv pizza_base_name)" # AND THE SEALED CLASS'S METACLASS IS PLAIN `type' AGAIN. `_Factory.__new__' # with bases present returns `type(name, ...)', so a subclass of a sealed class # does not re-enter the factory -- which is what makes `class Pizza(...)' an # ordinary class the user owns. check_eq $P/pizza-metaclass "type" "$(pv pizza_metaclass)" # POSITIONAL CONSTRUCTION BINDS IN DECLARATION ORDER. This is the claim the whole # `Fields.a.b' syntax rests on -- the order of the attribute chain is the order # of the positional parameters, and nothing else in the program records it. check_eq $P/pizza-pos-name "Pepperoni" "$(pv pizza_pos_name)" check_eq $P/pizza-pos-size "large" "$(pv pizza_pos_size)" # KEYWORDS WORK TOO, and out of order. check_eq $P/pizza-kw-name "Pepperoni" "$(pv pizza_kw_name)" check_eq $P/pizza-kw-size "large" "$(pv pizza_kw_size)" # THE GENERATED `__repr__' NAMES THE SUBCLASS, NOT THE GENERATED BASE, and it # prints every field as `name=' in declaration order. Pinned whole because # the field ORDER inside it is the same claim `pizza-pos-*' makes, observed from # the other side -- and because `pizza-base-name' above shows the base's name # would have been useless here. check_eq $P/pizza-repr "Pizza(name='Pepperoni', size='large')" "$(pv pizza_repr)" check_eq $P/pizza-str-is-repr "yes" "$(pv pizza_str_is_repr)" # THE REPR USES `repr()' ON THE VALUES, not `str()'. So an int is bare and a str # is quoted -- which is what makes the output round-trippable by eye, and what a # `str()'-based release would silently break while keeping `pizza-repr' green # for its all-string fields. check_eq $P/repr-uses-repr "Pizza(name=1, size='x')" "$(pv repr_uses_repr)" # EVERY FIELD NAME AND VALUE IS PRESENT IN THE REPR. The ticket's own # requirement, asserted as one derived boolean as well as by the literals above, # so a reader can find it as a single case. check_eq $P/repr-has-all-fields "yes" "$(pv repr_has_all_fields)" # --------------------------------------------------------------- __init__ --- # THE MISSING-FIELD AND UNEXPECTED-FIELD ERRORS the ticket asks for. They come # from the INTERPRETER, not from this library -- the generated `__init__' is an # ordinary function with ordinary parameters, so a missing argument is CPython's # own TypeError. That is why the GENERATED FUNCTION NAME is in the message, and # why it is worth pinning whole: it is the first thing a user of this library # sees when they get a call wrong, and it names neither their class nor their # field. check_eq $P/missing-field-error \ "TypeError: __fields_init_for__name__size__() missing 1 required positional argument: 'size'" \ "$(pv missing_field_error)" check_eq $P/unexpected-field-error \ "TypeError: __fields_init_for__name__size__() got an unexpected keyword argument 'crust'" \ "$(pv unexpected_field_error)" check_eq $P/extra-positional-error \ "TypeError: __fields_init_for__name__size__() takes 3 positional arguments but 4 were given" \ "$(pv extra_positional_error)" # THE GENERATED FUNCTION'S NAME AND SIGNATURE, read off the sealed base rather # than off a traceback. `make_init_func' builds `__fields_init_for__name__size__' # and rebinds it to `__init__', so `__name__' keeps the generated spelling -- # which is what the three messages above print. check_eq $P/init-func-name "__fields_init_for__name__size__" "$(pv init_func_name)" check_eq $P/init-params "self,name,size" "$(pv init_params)" # ============================================================================ # THE CASES THAT SAY THE INTERPRETER COMPILED AT RUNTIME. # ============================================================================ # # This is the one thing this port does that no other pure-Python port on this # image does: the `__init__' above does not exist in any file. `make_init_func' # assembles it as SOURCE TEXT and calls the interpreter's own `compile()' and # `exec' on it, once per sealed class. If the wasm32 interpreter could not # compile at runtime, this package would be the one that noticed -- and it would # notice as an ImportError-shaped failure a long way from the cause. # # THE FILENAME IS CONTENT-DERIVED: `' with an adler32 # (from `zlib', the unguarded top-level import) of the generated source. So it # is STABLE across runs and across arms -- the same declaration produces the # same name here and on the control -- which is the only reason it can be # asserted as a literal at all. check_eq $P/init-filename "" "$(pv init_filename)" # AND THE SOURCE IS REGISTERED WITH `linecache', which is the only reason a # traceback through the generated `__init__' can show its source: there is no # file behind that name for the traceback machinery to open. This is the case # that would notice `linecache' leaving python3-core -- the generated class would # still work and every traceback through it would go blank. check_eq $P/init-in-linecache "yes" "$(pv init_in_linecache)" check_eq $P/init-source-lines "6" "$(pv init_source_lines)" check_eq $P/init-source-has-super "yes" "$(pv init_source_has_super)" # -------------------------------------------------------------- comparison --- # `__eq__' COMPARES THE FIELD TUPLE, IN ORDER, guarded by # `isinstance(other, self.__class__)'. The ticket's requirement in two cases: # equal fields compare equal, one differing field does not -- and # `eq-is-not-identity' is what says the first is not just object identity # answering. check_eq $P/eq-equal-fields "yes" "$(pv eq_equal_fields)" check_eq $P/eq-differing-fields "no" "$(pv eq_differing_fields)" check_eq $P/ne-differing-fields "yes" "$(pv ne_differing_fields)" check_eq $P/eq-is-not-identity "yes" "$(pv eq_is_not_identity)" # THE FULL ORDERING SET. `Fields' generates all six comparisons and they compare # the field TUPLE -- so ordering is LEXICOGRAPHIC BY DECLARATION ORDER, which is # a real consequence of the syntax and an easy one to be surprised by: reordering # an attribute chain silently reorders every sort of that class. check_eq $P/lt "no" "$(pv lt)" check_eq $P/gt "yes" "$(pv gt)" check_eq $P/le-equal "yes" "$(pv le_equal)" check_eq $P/ge-equal "yes" "$(pv ge_equal)" # COMPARISON WITH A FOREIGN TYPE RETURNS `NotImplemented', which Python turns # into `False' for `==' and into a TypeError for `<'. Both halves pinned: the # delegation is what lets a subclass on the other side win, and the TypeError is # what a user actually meets. check_eq $P/eq-foreign "no" "$(pv eq_foreign)" check_eq $P/lt-foreign-error "TypeError" "$(pv lt_foreign_error)" # `__hash__' IS CONSISTENT WITH `__eq__' -- the ticket's requirement -- and it is # literally the hash of the same field tuple. `hash-is-tuple-hash' is the case # that says so; without it, a `__hash__' left to identity would fail only # `hash-equal' and could be "fixed" by anything at all. check_eq $P/hash-equal "yes" "$(pv hash_equal)" check_eq $P/hash-differs "yes" "$(pv hash_differs)" check_eq $P/hash-is-tuple-hash "yes" "$(pv hash_is_tuple_hash)" # AND THE INSTANCES DEDUPLICATE IN A SET, which is the consequence that matters # and the one a broken `__hash__'/`__eq__' pair breaks in a way no direct # comparison shows. Three instances, two distinct. check_eq $P/hash-dedups "2" "$(pv hash_dedups)" # ---------------------------------------------------------------- defaults --- # THE KEYWORD-DEFAULT FORM the ticket asks for: `Fields.value.left[None].right[None]', # the `__getitem__' half of the syntax. A defaulted field may be omitted, and # the defaults are captured ONCE at seal time into the generated function's # `__defaults__' rather than re-evaluated per call. check_eq $P/node-repr "Node(value=1, left=None, right=None)" "$(pv node_repr)" check_eq $P/node-defaults-left "None" "$(pv node_defaults_left)" check_eq $P/node-mixed-repr \ "Node(value=1, left=None, right=Node(value=2, left=None, right=None))" \ "$(pv node_mixed_repr)" check_eq $P/node-init-defaults "(None, None)" "$(pv node_init_defaults)" # A MUTABLE DEFAULT IS SHARED, NOT COPIED -- the third value the source reads the # other way, and the one most likely to bite a user of this library. # `Fields.items[[]]' reads like a per-instance empty list; the object goes into # `__defaults__' once, at seal time, so two instances that omit it get the SAME # list. Asserted by identity, which is the only way to see it before someone # mutates one. check_eq $P/mutable-default-shared "yes" "$(pv mutable_default_shared)" # ------------------------------------------------------------ the inline form --- # A FACTORY IS CALLABLE WITHOUT SUBCLASSING. `_Factory.__call__' does # `(~cls)(...)', so `Fields.name.size("a", "b")' seals and instantiates in one # step -- and the repr then names the GENERATED base, since there is no subclass # to name. That is `pizza-base-name' seen from the user's side, and it is why the # inline form is worse to debug than the subclass form. check_eq $P/inline-repr "FieldsBase(name='Pepperoni', size='large')" "$(pv inline_repr)" check_eq $P/inline-class-name "FieldsBase" "$(pv inline_class_name)" # SEALING IS MEMOISED PER FACTORY OBJECT, and per factory object only. # `__invert__' caches into `__concrete', so inverting ONE factory twice yields # the same class -- while two separately-built chains naming the same fields do # NOT, because `chain-links-distinct' above made them different objects. The two # cases together are the whole caching contract, and the second is the one that # says an identical declaration in two modules gives two unrelated classes. check_eq $P/seal-memoised "yes" "$(pv seal_memoised)" check_eq $P/seal-not-shared-across-chains "no" "$(pv seal_not_shared_across_chains)" # ------------------------------------------------------------------- Tuple --- # THE `namedtuple' ALTERNATIVE, and the ticket's "NamedTuple-style variant # asserted for the attributes it exposes". `tuple_sealer' builds a real `tuple' # subclass whose fields are `itemgetter' PROPERTIES over positions -- so the # values live in the tuple, not in an instance dict. check_eq $P/tuple-issubclass "yes" "$(pv tuple_issubclass)" check_eq $P/tuple-a "1" "$(pv tuple_a)" check_eq $P/tuple-b "2" "$(pv tuple_b)" check_eq $P/tuple-as-tuple "(1, 2)" "$(pv tuple_as_tuple)" check_eq $P/tuple-len "2" "$(pv tuple_len)" check_eq $P/tuple-index "1" "$(pv tuple_index)" check_eq $P/tuple-unpacks "[1, 2]" "$(pv tuple_unpacks)" check_eq $P/tuple-repr "Pair(a=1, b=2)" "$(pv tuple_repr)" # THE ATTRIBUTES ARE `property' OBJECTS, built from `operator.itemgetter'. This # is what "asserted for the attributes they expose" means for this variant, and # it is asserted by TYPE as well as by value: a release that switched to plain # slots would keep every value case above green and change what `Pair.a' is. check_eq $P/tuple-attr-is-property "yes" "$(pv tuple_attr_is_property)" # A FIELD CANNOT BE REBOUND -- but NOT because the instance is slotted, which is # the fourth value the source reads the other way. The refusal is the read-only # `property'; `setattr' hits a descriptor with no setter. # # THE INSTANCE STILL HAS A `__dict__', and an UNDECLARED attribute assigns fine. # `Pair' -- the user's own subclass -- declares no `__slots__', and unlike # `SlotsFields' this sealer installs no metaclass to force one. So `Tuple' buys # the tuple's packed storage for the FIELDS and none of the memory saving people # reach for `__slots__' for. Contrast `slots-has-dict' below, which is `no'. check_eq $P/tuple-assign-error "AttributeError" "$(pv tuple_assign_error)" check_eq $P/tuple-has-dict "yes" "$(pv tuple_has_dict)" check_eq $P/tuple-undeclared-attr-ok "none" "$(pv tuple_undeclared_attr_ok)" check_eq $P/tuple-base-slots "()" "$(pv tuple_base_slots)" # EQUALITY AND HASH ARE `tuple''S OWN, not generated -- so a Pair equals a PLAIN # TUPLE with the same contents, which is not true of any other variant here. # Pinned because it is the one place `Tuple' diverges from the ticket's "two # instances with equal fields compare equal" in a direction worth knowing: the # comparison is wider than the class. check_eq $P/tuple-eq-plain-tuple "yes" "$(pv tuple_eq_plain_tuple)" check_eq $P/tuple-eq-same-fields "yes" "$(pv tuple_eq_same_fields)" check_eq $P/tuple-hash-equal "yes" "$(pv tuple_hash_equal)" # `__getnewargs__' RETURNS THE FIELD TUPLE, which is what makes a `tuple' # subclass with a custom `__new__' survive a pickle round trip. Asserted as the # return value rather than by an actual round trip: the probe's classes live in # its `__main__' and pickle resolves a class by NAME, so a failure there would be # about where the class lives and not about this package. check_eq $P/tuple-getnewargs "(1, 2)" "$(pv tuple_getnewargs)" # AND `__new__' CARRIES THE GENERATED NAME, exactly as `__init__' does for the # class sealer -- the same `make_init_func', entered with a different header. check_eq $P/tuple-new-name "__fields_init_for__a__b__" "$(pv tuple_new_name)" # ------------------------------------------------------------- SlotsFields --- # THE SLOTS-BEARING VARIANT the ticket asks for. `slots_class_sealer' puts a # metaclass on the generated base that FORCES `__slots__' onto any subclass # which does not declare one -- which is what makes the saving survive # subclassing, and is the whole point of the variant. check_eq $P/slots-x "1" "$(pv slots_x)" check_eq $P/slots-y "2" "$(pv slots_y)" check_eq $P/slots-repr "Point(x=1, y=2)" "$(pv slots_repr)" # THE SUBCLASS GOT `__slots__' IT NEVER DECLARED, naming exactly the fields. # This is "asserted for the attributes they expose" for this variant. check_eq $P/slots-subclass-slots "x,y" "$(pv slots_subclass_slots)" check_eq $P/slots-meta-name "__slots_meta__" "$(pv slots_meta_name)" # AND SO THERE IS NO INSTANCE `__dict__': an undeclared attribute is refused. # The pair `slots-has-dict' / `slots-new-attr-error' is what separates this # variant from `Fields' in one reading, and from `Tuple' -- whose # `tuple-has-dict' is `yes' -- in another. check_eq $P/slots-has-dict "no" "$(pv slots_has_dict)" check_eq $P/slots-new-attr-error "AttributeError" "$(pv slots_new_attr_error)" # A DECLARED FIELD IS STILL WRITABLE -- slots restrict which names exist, not # whether they can be rebound. Without this case, `slots-new-attr-error' alone # would be satisfied by a class that refused every assignment. check_eq $P/slots-field-writable "9" "$(pv slots_field_writable)" # AND IT STILL COMPARES AND PRINTS: `slots_class_sealer' delegates to # `class_sealer', so the whole comparison set is inherited rather than reimplemented. check_eq $P/slots-eq "yes" "$(pv slots_eq)" check_eq $P/slots-hash-equal "yes" "$(pv slots_hash_equal)" # ---------------------------------------------------- the remaining factories --- # `BareFields' -- `__init__' ONLY. No comparison and no repr, so two instances # with equal fields are NOT equal and the repr is the default `<... object at # 0x...>'. This is the case that says the comparison methods asserted above are # GENERATED rather than inherited from somewhere: the same declaration through a # different factory does not have them. # # The default repr is asserted by SHAPE, not pinned: it carries an address. check_eq $P/bare-a "1" "$(pv bare_a)" check_eq $P/bare-eq "no" "$(pv bare_eq)" check_eq $P/bare-repr-is-default "yes" "$(pv bare_repr_is_default)" # `ConvertibleFields' -- the full `Fields' set plus `as_dict' and `as_tuple' # PROPERTIES. The dict is a plain `dict' in field order; the tuple is the same # tuple `__eq__' and `__hash__' compare, which is why `conv-still-comparable' is # asserted beside them rather than assumed. check_eq $P/conv-as-dict "{'a': 1, 'b': 2}" "$(pv conv_as_dict)" check_eq $P/conv-as-tuple "(1, 2)" "$(pv conv_as_tuple)" check_eq $P/conv-as-dict-type "dict" "$(pv conv_as_dict_type)" check_eq $P/conv-still-comparable "yes" "$(pv conv_still_comparable)" # THE MIXINS -- `initializer=False', so they generate NO `__init__' and are meant # to be combined with a field-bearing base. This is the documented "extend rather # than replace" form: `BareFields' supplies the fields and the initializer, the # mixin supplies methods over a SUBSET of them. # # `mixin-eq-on-subset' is the sharpest case in this suite. `Mixed' carries three # fields and compares on `a' ALONE, so `Mixed(1,2,3) == Mixed(1,3,4)' is TRUE -- # the mixin doing exactly what it says, and the clearest demonstration that a # generated method's field list is its own rather than the class's. check_eq $P/mixin-eq-on-subset "yes" "$(pv mixin_eq_on_subset)" check_eq $P/mixin-ne-on-subset "yes" "$(pv mixin_ne_on_subset)" check_eq $P/mixin-carries-all-fields "1,2,3" "$(pv mixin_carries_all_fields)" check_eq $P/mixin-base-count "2" "$(pv mixin_base_count)" check_eq $P/printable-mixin-repr "Printed(a=1, b=2)" "$(pv printable_mixin_repr)" # ============================================================================ # INHERITANCE EXTENDS THE FIELD LIST -- and which factory you must use for it. # ============================================================================ # # The ticket asks that inheritance from a generated class EXTEND the field list # rather than replace it. It does, and the factory that does it is # `InheritableFields' -- `class_sealer' with `pass_kwargs=True' and # `base=object', whose generated `__init__' takes `**__fields_kwargs__' and # passes them UP the MRO. That is what lets two independently-declared field sets # cooperate in one class. # # IT IS NOT IN `__all__' (see `all-missing-public' above), so the documented # `from fields import *' does not bind the only factory that supports the form. # # `inherit-bogus-error' is what says the extension is a real UNION of the two # field lists rather than a `**kwargs' that swallows anything: an undeclared # keyword is still refused. check_eq $P/inherit-name "hans" "$(pv inherit_name)" check_eq $P/inherit-age "43" "$(pv inherit_age)" check_eq $P/inherit-field-count "2" "$(pv inherit_field_count)" check_eq $P/inherit-bogus-error "TypeError" "$(pv inherit_bogus_error)" check_eq $P/inherit-mro-len "6" "$(pv inherit_mro_len)" # AND THE PLAIN `Fields' FACTORY DOES NOT SUPPORT IT. Its generated `__init__' # passes only its OWN fields to `super()', so the second base's initializer never # receives its argument and the construction is a TypeError. Pinned as the # contrast, because it is the whole reason `InheritableFields' exists -- and a # reader who reaches for `Fields' here should meet the error in a case rather # than in their own program. check_eq $P/plain-multiple-inheritance-error "TypeError" \ "$(pv plain_multiple_inheritance_error)" # A CONFLICTING FIELD LAYOUT IS REFUSED AT CLASS-CREATION TIME, not at # construction. `_Factory.__new__' zips the bases' field lists position by # position and raises when one position carries two different names -- so # combining two factories whose fields disagree in ORDER is a definition-time # error naming the position. Pinned by message: the position number is the only # thing that makes it actionable. check_eq $P/layout-conflict-error \ "TypeError: Field layout conflict: fields in position 0 have different names: 'a', 'b'" \ "$(pv layout_conflict_error)" # ------------------------------------------------------ a custom __init__ --- # THE FEATURE THE README LEADS WITH, and its comparison to `attrs' and # `characteristic': the subclass owns its own `__init__' and cooperates through # `super()' rather than having one generated over the top of it. Both halves # asserted -- the custom body runs and can reject, and the generated one still # binds the fields. check_eq $P/custom-init-ok "Checked(a=1, b=2)" "$(pv custom_init_ok)" check_eq $P/custom-init-raises "ValueError" "$(pv custom_init_raises)" # AND A CUSTOM `__init__' MAY TAKE ARGUMENTS OF ITS OWN, passing only the # declared fields up as keywords -- which is the pattern that makes this library # usable in a class that already has an initializer. check_eq $P/custom-init-extra-arg "42,1" "$(pv custom_init_extra_arg)" # ---------------------------------------------------- make_init_func direct --- # THE LOW-LEVEL ENTRY, exported in `__all__' and callable on its own. It is also # where the ONE non-TypeError refusal in this library lives: a positional field # declared after a defaulted one is a `ValueError' naming the offending field. # # The factory refuses the same shape earlier and differently -- see # `required-after-default-seal-error' above, a TypeError naming nothing -- so # both paths are pinned. A user who reaches the library through the factory and # a user who reaches it through `make_init_func' get different exception types # for the same mistake, and only these two cases say so. check_eq $P/make-init-func-error \ "ValueError: Cannot have positional fields after fields with defaults. Field 'c' is missing a default value!" \ "$(pv make_init_func_error)" # IT RETURNS TWO NAMESPACES, and the split is load-bearing: the generated # `__init__' comes back in the LOCAL one, while the global one carries only # `super' -- the single name the generated source needs to resolve. check_eq $P/make-init-func-keys "__init__" "$(pv make_init_func_keys)" check_eq $P/make-init-func-global-keys "__builtins__,super" "$(pv make_init_func_global_keys)" # ------------------------------------------------------------- the sealers --- # THREE SEALERS ARE EXPORTED and each is an ordinary function taking # `(fields, defaults)'. Asserted by name so a release that renamed one is a red # here rather than an AttributeError inside somebody's `factory()' call. check_eq $P/sealer-names "class_sealer,slots_class_sealer,tuple_sealer" \ "$(pv sealer_names)" # AND THE TWO SEALERS NAME THEIR CLASSES DIFFERENTLY, from ONE computed string. # `class_sealer' discards `baseclass_name' for a literal `class FieldsBase'; # `tuple_sealer' passes it to `type()'. See `pizza-base-name' -- this pair is # where that asymmetry is stated as such. check_eq $P/class-sealer-result "FieldsBase" "$(pv class_sealer_result)" check_eq $P/tuple-sealer-result "FieldsBase_for__a__b" "$(pv tuple_sealer_result)" # `factory()' BUILDS A NEW FACTORY FROM ANY SEALER -- the documented extension # point, and the mechanism every factory above is made with. These three cases # assert the CONTRACT a third-party sealer is called under: the field list in # declaration order, the defaults as an ordered mapping, and whatever it returns # used as the base. check_eq $P/factory-sealer-fields "foo,bar,lorem,ipsum" "$(pv factory_sealer_fields)" check_eq $P/factory-sealer-defaults "lorem=1,ipsum=2" "$(pv factory_sealer_defaults)" check_eq $P/factory-sealer-base "object" "$(pv factory_sealer_base)" # --------------------------------------------------------------- Namespace --- # A BACKPORT OF `types.SimpleNamespace', carried because this package still # supported 2.6 -- dead weight on 3.12 and shipped anyway, like # `py2ordereddict.py'. It is in `__all__', so `from fields import *' binds it. # # ITS `__repr__' SORTS THE KEYS, which `SimpleNamespace' does NOT, so the two are # not interchangeable in output. check_eq $P/namespace-repr "Namespace(a=1, b=2)" "$(pv namespace_repr)" check_eq $P/namespace-eq "yes" "$(pv namespace_eq)" # IT DEFINES `__eq__' WITHOUT `__hash__', so Python sets `__hash__ = None' and # the instances are UNHASHABLE. A real finding rather than a detail: it is the # one object in this package that cannot go in a set or be a dict key -- while # every generated class above can. check_eq $P/namespace-hashable "no" "$(pv namespace_hashable)" check_eq $P/namespace-hash-error "TypeError" "$(pv namespace_hash_error)" # AND ITS `__eq__' IS UNGUARDED -- it reads `other.__dict__' with no isinstance # check, so comparing to an int RAISES where every generated class returns # NotImplemented and yields `False'. Contrast `eq-foreign' above. check_eq $P/namespace-eq-foreign-error "AttributeError" \ "$(pv namespace_eq_foreign_error)" # ----------------------------------------------------------------- extras --- # THE SEPARATE SUBMODULE, imported by the probe for the first time (see # `extras-not-autoimported' above). It is the package's own worked example of a # custom sealer, and it is the only part of this payload that imports `re'. check_eq $P/extras-import-ok "yes" "$(pv extras_import_ok)" # THE REASON-CARRIER for the case above, and the same shape as `import-ok' / # `import' at the top: `extras-import-ok' says only `want yes got no', while # this one carries the exception. Empty is the pass. check_eq $P/extras-error "" "$(pv extras_error)" check_eq $P/extras-names "RegexValidate,ValidationError,re,regex_validation_sealer" \ "$(pv extras_names)" check_eq $P/extras-validation-error-base "Exception" \ "$(pv extras_validation_error_base)" # THE VALIDATION RUNS IN THE SEALER'S `__init__', BEFORE `super()' -- so a # rejected value never reaches the attribute. A matching value is accepted and a # non-matching one raises the package's own `ValidationError'. check_eq $P/regex-accepts "none" "$(pv regex_accepts)" check_eq $P/regex-rejects "ValidationError" "$(pv regex_rejects)" # THE SEALER REFUSES REQUIRED FIELDS ALTOGETHER: every field must carry its # pattern as a DEFAULT, because the pattern is where the default slot is used. # So `RegexValidate.value' with no `[...]' is a definition-time TypeError -- the # one factory in this package whose fields cannot be required. check_eq $P/regex-requires-defaults "TypeError" "$(pv regex_requires_defaults)" # COMBINED WITH `Fields', the validator runs AND the fields are still set -- the # documented composition, and the point of the whole sealer mechanism: two # independently-written sealers cooperating in one MRO. check_eq $P/regex-combined-ok "Both(value='aaa')" "$(pv regex_combined_ok)" check_eq $P/regex-combined-rejects "ValidationError" "$(pv regex_combined_rejects)" # ------------------------------------------------------------- interpreter --- # WHICH ARM THIS TRANSCRIPT IS FROM, so that a control run and a target run are # distinguishable in the capture without reading the surrounding log. # # `py-maxsize-bits' is the ONE value in this suite that is NOT the control's: it # reads 64 there and must read 32 here. There is no second such value, because # this package starts no thread, opens no socket and touches no device -- so the # platform gaps the other Python ports on this image have to work around do not # arise at all. That makes this single case load-bearing: without it every value # in this file is arm-independent by construction, and a suite run against a HOST # copy of the package would pass in full. check_eq $P/py-version "3.12" "$(pv py_version)" check_eq $P/py-maxsize-bits "32" "$(pv py_maxsize_bits)" rm -rf "$W"