. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the output of one of those three. # # # # They are a bare `import', a second one whose STDERR is # # the assertion (`import-quiet'), and the probe -- which # # takes ONE start for a hundred and forty-seven # # measurements. The work inside the probe is attribute # # lookups and two-key dict construction, microseconds # # beside an interpreter start on this arm, which is tens # # of seconds before the first line of the program runs. # # # # 1200 is 400s per start, the same budget # # 99-python3-aniso8601 and 99-python3-parse set for their # # three, and roughly six times the ~60s a python3 start has # # measured at here. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-mypy-extensions 1.0.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-mypy-extensions_1.0.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHY ITS RUNTIME HALF IS SO SMALL. # ============================================================================ # # One module of 213 lines that defines the things mypy understands and `typing' # does not. Almost all of it is meant for a TYPE CHECKER, which does not run on # the target -- so what is testable here is what each object DOES at runtime, # and for most of them the answer is deliberately "nothing". # # `Arg', `DefaultArg', `NamedArg', `DefaultNamedArg', `VarArg' and `KwArg' # return the type they were given. `trait' and `mypyc_attr' return what they # decorate. `i64'/`i32'/`i16'/`u8' construct ordinary `int's. Those are not # stubs to be filled in later: `Callable[[Arg(int, "x")], None]' has to remain a # valid `typing.Callable' at import time on every interpreter, and it only does # because `Arg' is the identity. THE NOOP IS THE CONTRACT, so the suite asserts # it as one -- by IDENTITY (`is'), never by equality, because a marker that # returned a copy would compare equal and still break `Callable'. # # ============================================================================ # THERE IS NO `NamedTuple' IN 1.0.0. THE TICKET ASKS FOR ONE. # ============================================================================ # # BRINTOS-2050 asks the suite to assert `NamedTuple' "over field access, # `_replace' and `_asdict'". `mypy_extensions.NamedTuple' was removed upstream; # 1.0.0 binds seventeen public names and that is not among them. Measured, not # read off a changelog: `public_names' below is the whole sorted list and # `api-no-namedtuple' asserts the absence by name. # # The absence is asserted rather than merely left uncovered because "the suite # has no NamedTuple cases" and "this release has no NamedTuple" look identical # from outside, and only the second is true. A release that reinstates it fails # `api-no-namedtuple' and `api-public-names' together, which is where a reader # would want to be told. # # ============================================================================ # A TypedDict INSTANCE IS A PLAIN dict -- NOT AN INSTANCE OF THE CLASS. # ============================================================================ # # `_dict_new' is installed as `__new__' on every class `TypedDict' makes, and it # `return dict(*args, **kwargs)'. A `__new__' returning something that is not an # instance of its class means `__init__' NEVER RUNS and the object that comes # back carries nothing of the class: # # Movie = TypedDict('Movie', {'name': str, 'year': int}) # type(Movie(name='Blade Runner', year=1982)) -> dict # # So the class is a declaration and the instance is a mapping, and the two share # no object. `td-instance-type' asserts it directly. # # THE CLASS ITSELF IS STILL A dict SUBCLASS (`td-mro'), which is the part that # makes the mistake easy: `issubclass(Movie, dict)' is True, so a reader # concludes instances are `Movie's. They are not. # # ============================================================================ # AND IT ACCEPTS EVERY KEY, INCLUDING THE ONES IT DECLARED AWAY. # ============================================================================ # # The ticket asks that the class "accepts and rejects keys as the docs fix". # At runtime it rejects nothing. An undeclared key is accepted # (`td-extra-key'), a declared key may be omitted (`td-missing-key'), and no # keys at all is a valid call (`td-no-keys'). The type checker is what refuses # any of the three, and the type checker is not on the target. # # The one thing that DOES raise is a check nobody expects to raise: `isinstance' # and `issubclass' against a TypedDict are a TypeError, in the package's own # words -- "TypedDict does not support instance and class checks". `_check_fails' # is installed as both `__instancecheck__' and `__subclasscheck__' and it lets # `abc', `functools' and `typing' through by inspecting the calling frame, so # the standard library never trips over it and every caller does. # # ============================================================================ # `total=False' CHANGES `__total__'. IT DOES NOT CREATE `__required_keys__'. # ============================================================================ # # The ticket asks that `total=False' be asserted "to change `__required_keys__' # and `__optional_keys__'". This implementation has neither attribute, at any # `total'. That pair belongs to `typing.TypedDict', which is a different and # much later implementation; `_TypedDictMeta' here sets `__annotations__' and # `__total__' and stops. # # Asserted as four negatives by name -- `td-no-required-keys', # `td-no-optional-keys' and the `total=False' pair -- so that a release growing # either attribute is caught, and so that a reader who arrives with the ticket's # expectation is answered rather than left to infer it from silence. # # ============================================================================ # EVERY ANNOTATION GOES THROUGH A PRIVATE typing API, AND IN 3.12 IT CHECKS # ALMOST NOTHING. # ============================================================================ # # The source imports `typing._type_check' and says in a comment that it is not # public. CPython 3.11 removed that function's `callable()' guard, so on the # 3.12 this image ships: # # TypedDict('X', {'a': 1}) -> __annotations__ == {'a': 1} # TypedDict('X', {'a': None}) -> {'a': NoneType} # TypedDict('X', {'a': 'int'}) -> {'a': ForwardRef('int')} # TypedDict('X', {'a': (int,)}) -> TypeError # # A tuple is the one shape still refused. All four are asserted, and they are # asserted as facts about THIS interpreter: `_type_check' is private, so a # python3 bump is exactly what would move them. That is the one dependency in # this package that could break without upstream touching it. # # ============================================================================ # THE NATIVE INTEGER TYPES DO NO RANGE CHECKING. # ============================================================================ # # `u8(999)' is 999. `i16(70000)' is 70000. Nothing truncates, wraps, clamps or # raises -- `__new__' calls `int()' and returns it, so the object is an `int' # and the width is a promise to mypyc alone. `isinstance(5, u8)' is True for the # same reason (`_NativeIntMeta.__instancecheck__' asks about `int'), and so is # `isinstance(True, u8)', since `bool' is an `int' subclass. # # Each of the four is asserted separately rather than argued from the shared # implementation, because they are four independent classes upstream could # diverge one at a time -- including their docstrings, which are `.format()'ed # per type in a loop at import (`int-*-doc-names-self'). # # ============================================================================ # THE PORT ADDS NO DEPENDENCY, AND THAT IS A MEASUREMENT. # ============================================================================ # # The stock recipe declares nothing; `setuptools3-base' gives it # `RDEPENDS:${PN} = "python3-core"' and the built .deb reads exactly # `Depends: python3-core'. That is CORRECT here, unlike python3-aniso8601 in # this directory, whose import chain reached `calendar' and `datetime' in the # `datetime' split and needed a bbappend. # # The whole import closure of this module is `typing' and what `typing' itself # pulls (`re', `contextlib', `copyreg', `enum', `collections.abc'), and poky's # python3-manifest.json puts every one of them in `core'. So THERE IS NO # BBAPPEND FOR THIS RECIPE -- the port is one IMAGE_INSTALL line, this suite and # its probe. # # `pkg-depends-exact' asserts the whole Depends line rather than a substring, so # a version bump that grew a dependency is caught here rather than at the first # rootfs that installed the .deb alone. # # ============================================================================ # PURE PYTHON, WHICH IS WHAT MAKES IT PORTABLE AT ALL. # ============================================================================ # # `py_modules=['mypy_extensions']' over a PyPI sdist whose entire payload is one # 213-line .py. There is no configure, no compile step and no C, so there is # nothing on this arm for an override to reach. It built at recipe scope # unchanged -- 1603 of 1603 tasks, all succeeded, zero ERROR lines -- and the # module unpacked from the built .deb is BYTE-IDENTICAL to the sdist the # SRC_URI sha256 pins, which is the measurement behind "no patch" rather than a # reading of the recipe. # # That purity is also why it can run here: 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 whatever it built # to. `pkg-no-so' and `pulled-nonstdlib' assert that negative from the packaging # side and from the import side respectively. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was measured on x86-64 against the module UNPACKED FROM # THE BUILT python3-mypy-extensions_1.0.0-r0_wasm32.deb -- not against a pip # install. The unpacked module was additionally compared to the sdist the recipe # fetches (sha256 75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af # 0782, which is the recipe's own SRC_URI[sha256sum], verified by digest) and is # byte-identical to it. # # The control interpreter is CPython 3.12.3 and the image ships 3.12.13, so the # two agree on minor version. That matters for two groups and they are the two # named above: the `td-*-ann' group, which is `typing._type_check''s behaviour # rather than this package's, and `pulled-nonstdlib', which is scored against # `sys.stdlib_module_names' (3.10+). # # The suite was then run END TO END on x86-64 against that same tree, with the # site-packages, dpkg status and dpkg file-list paths redirected at it. # # The on-target result is not yet measured -- the batch verify is this port's # first run on wasm32-hwjs. P=python3-mypy-extensions # THE CANARY, which asserts nothing about python3-mypy-extensions. # # 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 MOD=$SITE/mypy_extensions.py DI=$SITE/mypy_extensions-1.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-mypy-extensions.list PROBE=$BST_DATA/python3-mypy-extensions-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-mypy-extensions.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import mypy_extensions' 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 SINGLE MODULE, NOT A PACKAGE DIRECTORY. setup.py declares # `py_modules=['mypy_extensions']', which is the opposite shape from # python3-aniso8601's `find_packages()' tree in this same directory. This is # the case that fails a release which grew a package, loudly and here. check $P/module-file test -s "$MOD" check $P/no-package-dir test ! -d "$SITE/mypy_extensions" # THE PRECOMPILED BYTECODE, which is what `import-quiet' below turns on. A # top-level module's .pyc lands in site-packages' OWN __pycache__, shared with # every other single-module package on the image -- not in a directory of its # own, which is where a reader looks first. check $P/pycache test -s "$SITE/__pycache__/mypy_extensions.cpython-312.pyc" # NO SHIPPED SCRIPT. 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/mypy_extensions # 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. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a # name-only filter would report every directory in the package as an unexpected # shape; each entry is therefore tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # THE WHOLE SHIPPED FILE SET IS SEVEN ENTRIES AND THEY ARE ENUMERABLE. Unlike # a package tree this one is small enough to assert exactly, so a release that # added or dropped a file is named rather than inferred. Directories are # excluded -- `site-packages/__pycache__' is shared and its membership is not # this package's business. # # `LC_ALL=C' ON THE SORT, and it is load-bearing rather than tidy. A collating # locale folds punctuation away, which puts `__pycache__' LAST and `WHEEL' # after `top_level.txt' -- a different order for the same set, and a red that # says nothing about the package. Measured on an en_US.UTF-8 control, where # this case failed for exactly that reason. The guest runs under C, so pinning # it here costs nothing and removes the dependency. check_eq $P/pkg-file-set \ "/usr/lib/python3.12/site-packages/__pycache__/mypy_extensions.cpython-312.pyc /usr/lib/python3.12/site-packages/mypy_extensions-1.0.0.dist-info/LICENSE /usr/lib/python3.12/site-packages/mypy_extensions-1.0.0.dist-info/METADATA /usr/lib/python3.12/site-packages/mypy_extensions-1.0.0.dist-info/RECORD /usr/lib/python3.12/site-packages/mypy_extensions-1.0.0.dist-info/WHEEL /usr/lib/python3.12/site-packages/mypy_extensions-1.0.0.dist-info/top_level.txt /usr/lib/python3.12/site-packages/mypy_extensions.py" \ "$(while IFS= read -r _f; do [ -f "$_f" ] && printf '%s\n' "$_f"; done < "$W/pkgf" | LC_ALL=C sort | tr '\n' ' ' | sed 's/ $//')" # ----------------------------------------------------------- the dist-info --- # The installed metadata. The DIRECTORY NAME carries an UNDERSCORE -- setup.py's # `name=' is `mypy_extensions' and the dist-info is named from that, while the # recipe, the .deb and the IMAGE_INSTALL line all spell it with a hyphen. Four # names for one thing, and this is the case that pins which spelling lands on # the filesystem. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.0.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: mypy_extensions" cat "$DI/METADATA" # The one-line description, which is upstream's own. check_out $P/dist-info-summary \ "Summary: Type system extensions for programs checked with the mypy type checker." \ cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs. It is what an # `import mypy_extensions' has to match, and a wheel that renamed the module # would change it here first. check_out $P/dist-info-top-level "mypy_extensions" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT. `Root-Is-Purelib: true' and the # `py3-none-any' tag are the packaging-side statement of the header's claim -- # no platform tag, no extension module, nothing that could need the absent # dynamic loader. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # The upstream licence text ships beside the metadata, which is where this # recipe's LIC_FILES_CHKSUM points (MIT). check $P/dist-info-licence test -s "$DI/LICENSE" # RECORD names the module by its INSTALLED path and its sha256, so it is the # packaging-side statement that the .py below is the one the wheel built. check_out $P/dist-info-record-module "mypy_extensions.py,sha256=" cat "$DI/RECORD" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-mypy-extensions is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list, and # the anchor is exact -- an unanchored match would also take the -dev or -dbg # stanza of the same family. sed -n '/^Package: python3-mypy-extensions$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCY LINE, WHOLE -- see the header. `setuptools3-base' gives # python3-core and the recipe adds nothing, and the import closure says nothing # more is needed. Asserted as an EQUALITY rather than as a substring: a # substring match would pass a version bump that grew a second dependency, which # is precisely the change this case exists to catch. check_eq $P/pkg-depends-exact "Depends: python3-core" \ "$(grep '^Depends:' "$W/st" 2>/dev/null)" # NO -ptest PACKAGE EXISTS FOR THIS RECIPE. The sdist ships no tests at all -- # six files, of which one is the module -- so unlike python3-aniso8601 in this # directory there is nothing that COULD go to one. The case is here so a version # bump that grows upstream tests is noticed. check_eq $P/no-ptest-package "" \ "$(sed -n '/^Package: python3-mypy-extensions-ptest$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If mypy_extensions cannot be # imported then every probe case below reds at once and each names a different # symptom; this case is what says the cause is the import. It is also the # cheapest possible statement of the deliverable. check $P/import python3 -c 'import mypy_extensions' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. This is a statement about the # shipped bytecode: the package ships a precompiled .pyc and OE writes it with # CHECKED-HASH invalidation, so the source is hashed and never recompiled. A red # here means the shipped .pyc was REJECTED -- a stale hash, a python3 minor bump # that changed the magic, or a missing __pycache__ -- and the module is being # compiled from source on every import. # # Nothing in mypy_extensions has import-time output of its own: no logging # configuration, no warning, no print. It does run a loop at module scope # (`for _int_type in i64, i32, i16, u8'), which is the one place a release could # grow a DeprecationWarning about the private `typing' import. check_eq $P/import-quiet "" \ "$(python3 -c 'import mypy_extensions' 2>&1)" # --------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `td_total' distinguishable # from `td_total_false', and `int_i64_value' from `int_i64_value'-prefixed # neighbours -- the pairs where one name is a prefix of another. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was # taken. The word `unscorable' is PARSED -- brintos_local_verify re-measures a # suite that prints it on a fresh guest rather than trusting a shared boot's # numbers (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "147" "$(pv emitted_count)" # The module answered out of the file this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME mypy_extensions. check_eq $P/module-answered "$MOD" "$(pv module_file)" # ...and it is a MODULE, not a package -- the import-side half of # `no-package-dir' above. check_eq $P/module-not-package "no" "$(pv is_package)" check_eq $P/module-name "mypy_extensions" "$(pv module_name)" # THE VERSION LIVES ONLY IN THE METADATA. The module binds no `__version__', so # a caller asking for one gets an AttributeError rather than a wrong answer -- # asserted as a negative, because "no attribute" is a documented shape here and # not an oversight to be worked around. check_eq $P/no-version-attr "no" "$(pv has_version_attr)" check_eq $P/dist-version "1.0.0" "$(pv dist_version)" # ---------------------------------------------------------------- the API --- # THE PUBLIC SURFACE, AS ONE SORTED LIST. There is no `__all__', so this is # exactly what `from mypy_extensions import *' takes -- `Any' and `sys' # included, which are the two imports at the top of the source leaking through. # One case rather than seventeen has-attr cases, because what is worth catching # is an ADDITION or a REMOVAL, and both change this one value. check_eq $P/api-public-names \ "Any,Arg,DefaultArg,DefaultNamedArg,FlexibleAlias,KwArg,NamedArg,NoReturn,TypedDict,VarArg,i16,i32,i64,mypyc_attr,sys,trait,u8" \ "$(pv public_names)" check_eq $P/api-no-all "no" "$(pv has_all_attr)" # THE TWO LEAKED IMPORTS, named. They are part of the star-import above and # neither is this package's; pinned separately so a release that tidied them # away is legible as that rather than as an unexplained list change. check_eq $P/api-leaks-any "yes" "$(pv exports_any)" check_eq $P/api-leaks-sys "yes" "$(pv exports_sys)" # THERE IS NO `NamedTuple' -- see the header. The ticket asks for it; upstream # removed it. Asserted by name so a release that reinstates it is caught here # rather than by the absence of cases nobody wrote. check_eq $P/api-no-namedtuple "no" "$(pv has_namedtuple)" # ----------------------------------------------------- TypedDict, the class --- # IT IS A CLASS, AND ITS TYPE IS THE PACKAGE'S OWN METACLASS -- which is what # installs every behaviour the groups below assert. check_eq $P/td-meta "_TypedDictMeta" "$(pv td_meta_name)" check_eq $P/td-class-name "Movie" "$(pv td_class_name)" check_eq $P/td-is-class "yes" "$(pv td_is_class)" # THE ANNOTATIONS ARE WHAT WAS DECLARED. They are the only place the declaration # survives to -- nothing else on the class records it. check_eq $P/td-annotations "name:str,year:int" "$(pv td_annotations)" check_eq $P/td-total "True" "$(pv td_total)" # `__module__' IS THE CALLER'S, not the package's. `_typeddict_new' reads it off # the calling frame, and it is what makes the class pickleable at all. check_eq $P/td-module "__main__" "$(pv td_module)" # A dict SUBCLASS -- and see the header: this is the True that makes the plain- # dict instance below surprising. check_eq $P/td-mro "Movie,dict,object" "$(pv td_mro)" check_eq $P/td-is-dict-subclass "yes" "$(pv td_is_dict_subclass)" # NEITHER KEY-SET ATTRIBUTE EXISTS -- see the header. `__required_keys__' and # `__optional_keys__' are `typing.TypedDict''s, not this implementation's, and # the ticket asks for both. The negatives are asserted by name. check_eq $P/td-no-required-keys "no" "$(pv td_has_required_keys)" check_eq $P/td-no-optional-keys "no" "$(pv td_has_optional_keys)" # ------------------------------------------ TypedDict, what an instance IS --- # A PLAIN dict. The single most surprising thing about the runtime half of this # package, and the header explains why: `_dict_new' returns `dict(...)', which # is not an instance of the class, so `__init__' never runs. check_eq $P/td-instance-type "dict" "$(pv td_instance_type)" check_eq $P/td-instance-items "name:'Blade Runner',year:1982" "$(pv td_instance_items)" check_eq $P/td-instance-eq "yes" "$(pv td_instance_eq_dict)" # ...and it behaves as a mapping in every ordinary way, which is the whole point # of it being one. check_eq $P/td-instance-len "2" "$(pv td_instance_len)" check_eq $P/td-instance-keys "name,year" "$(pv td_instance_sorted_keys)" check_eq $P/td-instance-get-absent "None" "$(pv td_instance_get_absent)" # KEYS ARE NEITHER REQUIRED NOR RESTRICTED -- see the header. The ticket's # "accepts and rejects keys" is the two ACCEPTANCES it actually is, plus the # empty call. check_eq $P/td-extra-key "bogus:True,name:'x',year:1" "$(pv td_extra_key)" check_eq $P/td-missing-key "name:'x'" "$(pv td_missing_key)" check_eq $P/td-no-keys "" "$(pv td_no_keys)" # THE ONE THING THAT DOES RAISE, and both halves of it: the type AND the words, # because a bare TypeError here would not tell this package's refusal from the # interpreter's. check_eq $P/td-isinstance-raises "TypeError" "$(pv td_isinstance_raises)" check_eq $P/td-isinstance-msg \ "TypedDict does not support instance and class checks" "$(pv td_isinstance_msg)" check_eq $P/td-issubclass-raises "TypeError" "$(pv td_issubclass_raises)" check_eq $P/td-issubclass-msg \ "TypedDict does not support instance and class checks" "$(pv td_issubclass_msg)" # --------------------------------------- TypedDict, the other declaration forms --- # THE KEYWORD FORM is the same declaration spelled differently and must produce # the same annotations -- asserted against the dict form's own value, so the # case says "these agree" rather than repeating a literal. check_eq $P/td-kwargs-annotations "name:str,year:int" "$(pv td_kwargs_annotations)" check_eq $P/td-kwargs-agrees "yes" "$(pv td_kwargs_agrees)" # BOTH AT ONCE IS REFUSED, in the package's own words. check_eq $P/td-both-forms-raises "TypeError" "$(pv td_both_forms_raises)" check_eq $P/td-both-forms-msg \ "TypedDict takes either a dict or keyword arguments, but not both" \ "$(pv td_both_forms_msg)" # `total=False' CHANGES `__total__' AND NOTHING ELSE -- the annotations are # untouched, neither key-set attribute appears, and an instance is still a plain # dict that accepts everything. So `total' reaches nothing at runtime at all. check_eq $P/td-total-false "False" "$(pv td_total_false)" check_eq $P/td-total-false-annotations "x:int,y:int" "$(pv td_total_false_annotations)" check_eq $P/td-total-false-no-required "no" "$(pv td_total_false_has_required)" check_eq $P/td-total-false-no-optional "no" "$(pv td_total_false_has_optional)" check_eq $P/td-total-false-instance "dict" "$(pv td_total_false_instance_type)" # THE CLASS SYNTAX, which routes through `_TypedDictMeta.__new__' directly # rather than through `_typeddict_new' -- a different code path to the same # result, which is why it is asserted rather than assumed. check_eq $P/td-class-annotations "x:int,y:int" "$(pv td_class_annotations)" check_eq $P/td-class-total "True" "$(pv td_class_total)" check_eq $P/td-class-instance "dict" "$(pv td_class_instance_type)" # A SUBCLASS MERGES ITS BASE'S ANNOTATIONS. The probe sorts, so this is the SET # of fields rather than the order `anns.update(...)' happened to leave -- which # is the assertion worth making, since the merge order is an implementation # detail and the membership is not. check_eq $P/td-subclass-annotations "x:int,y:int,z:int" "$(pv td_subclass_annotations)" check_eq $P/td-subclass-total "True" "$(pv td_subclass_total)" # `total=False' ON THE CLASS SYNTAX arrives as a keyword to the metaclass rather # than as a popped kwarg -- the second of the two paths. check_eq $P/td-class-total-false "False" "$(pv td_class_total_false)" # THE BASE OBJECT ITSELF is an instance of its own metaclass with no fields, and # its `__module__' is the PACKAGE's -- unlike every class made from it, whose # module is the caller's (`td-module' above). This is what distinguishes the # factory from its products. check_eq $P/td-base-annotations "" "$(pv td_base_annotations)" check_eq $P/td-base-total "True" "$(pv td_base_total)" check_eq $P/td-base-module "mypy_extensions" "$(pv td_base_module)" check_eq $P/td-base-meta "_TypedDictMeta" "$(pv td_base_meta)" # -------------------------------- TypedDict, what the private typing API does --- # THE FOUR CASES ARE FACTS ABOUT CPython 3.12, NOT ABOUT THIS PACKAGE -- see the # header. `typing._type_check' is private, its `callable()' guard went in 3.11, # and a python3 bump is what would move these. That makes this group the port's # one genuine interpreter dependency, so it is asserted rather than left # implicit. check_eq $P/td-tuple-ann-raises "TypeError" "$(pv td_tuple_ann_raises)" check_eq $P/td-tuple-ann-msg \ "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" \ "$(pv td_tuple_ann_msg)" # A NON-TYPE IS ACCEPTED AND KEPT AS ITSELF. check_eq $P/td-int-ann "a:1" "$(pv td_int_ann)" # `None' IS CONVERTED TO NoneType -- `_type_convert''s one substitution. check_eq $P/td-none-ann "a:NoneType" "$(pv td_none_ann)" # A STRING BECOMES A ForwardRef, not the type it names. Nothing resolves it at # runtime, so a caller reading `__annotations__' gets the ref and not `int'. check_eq $P/td-str-ann "ForwardRef" "$(pv td_str_ann)" # ------------------------------------------------ the callable-arg markers --- # ALL SIX RETURN THE TYPE THEY WRAP -- see the header. Asserted by IDENTITY, and # three ways each for the four that take a name: with a name, without one, and # with no arguments at all. The name is the parameter a caller passes and it # must be IGNORED rather than wrapped, which is what `*-named' says. check_eq $P/marker-arg-named "yes" "$(pv marker_arg_named)" check_eq $P/marker-arg-unnamed "yes" "$(pv marker_arg_unnamed)" check_eq $P/marker-arg-default-any "yes" "$(pv marker_arg_default_any)" check_eq $P/marker-defaultarg-named "yes" "$(pv marker_defaultarg_named)" check_eq $P/marker-defaultarg-unnamed "yes" "$(pv marker_defaultarg_unnamed)" check_eq $P/marker-defaultarg-default-any "yes" "$(pv marker_defaultarg_default_any)" check_eq $P/marker-namedarg-named "yes" "$(pv marker_namedarg_named)" check_eq $P/marker-namedarg-unnamed "yes" "$(pv marker_namedarg_unnamed)" check_eq $P/marker-namedarg-default-any "yes" "$(pv marker_namedarg_default_any)" check_eq $P/marker-defaultnamedarg-named "yes" "$(pv marker_defaultnamedarg_named)" check_eq $P/marker-defaultnamedarg-unnamed "yes" "$(pv marker_defaultnamedarg_unnamed)" check_eq $P/marker-defaultnamedarg-default-any "yes" "$(pv marker_defaultnamedarg_default_any)" # `VarArg' AND `KwArg' TAKE NO NAME. A `*args' or `**kwargs' parameter is named # by its star, so upstream gave these two a one-argument signature -- passing a # name is a TypeError from the interpreter, not a silently ignored argument. # This is the one place the six markers are NOT interchangeable, and a caller # who reaches for the six as a set finds it here. check_eq $P/marker-vararg-typed "yes" "$(pv marker_vararg_typed)" check_eq $P/marker-vararg-default-any "yes" "$(pv marker_vararg_default_any)" check_eq $P/marker-vararg-name-raises "TypeError" "$(pv marker_vararg_name_raises)" check_eq $P/marker-kwarg-typed "yes" "$(pv marker_kwarg_typed)" check_eq $P/marker-kwarg-default-any "yes" "$(pv marker_kwarg_default_any)" check_eq $P/marker-kwarg-name-raises "TypeError" "$(pv marker_kwarg_name_raises)" # THE IDENTITY IS AN IDENTITY, over a type the interpreter does not intern. `int' # would pass an equality test even if the marker returned a copy; a TypedDict # class would not. check_eq $P/marker-arg-is-identity "yes" "$(pv marker_arg_is_identity)" # ------------------------------------------------- `trait' and `mypyc_attr' --- # `trait' RETURNS THE CLASS IT DECORATES, UNCHANGED. It is a mypyc marker with # no runtime effect at all -- which is the whole of its documented contract, and # the thing a decorator most plausibly gets wrong. check_eq $P/trait-identity "yes" "$(pv trait_identity)" check_eq $P/trait-name-kept "_Plain" "$(pv trait_name_kept)" check_eq $P/trait-on-function "yes" "$(pv trait_on_function)" # `mypyc_attr' IS THE PARAMETERISED FORM OF THE SAME NOOP -- it takes the # attributes and returns a decorator, so it is two calls rather than one, in # both the positional and the keyword spelling upstream documents. check_eq $P/mypyc-attr-identity "yes" "$(pv mypyc_attr_identity)" check_eq $P/mypyc-attr-kwargs-identity "yes" "$(pv mypyc_attr_kwargs_identity)" # -------------------------------------------------------------- `NoReturn' --- # THIS `NoReturn' IS A PLAIN CLASS OF THIS PACKAGE'S OWN, and it is NOT # `typing.NoReturn'. A caller who imports it from here and compares it with the # typing one gets False, silently -- so both halves are asserted. check_eq $P/noreturn-is-class "yes" "$(pv noreturn_is_class)" check_eq $P/noreturn-module "mypy_extensions" "$(pv noreturn_module)" check_eq $P/noreturn-mro "NoReturn,object" "$(pv noreturn_mro)" check_eq $P/noreturn-not-typing "no" "$(pv noreturn_is_typing_noreturn)" # --------------------------------------------------------- `FlexibleAlias' --- # `FlexibleAlias' IS AN INSTANCE, NOT A CLASS, and it takes TWO subscripts: # `FlexibleAlias[T, X]' returns a helper that remembers X, and subscripting THAT # returns X. A caller who expects one subscript to yield the type gets the # helper object instead and NOTHING RAISES, which is the failure worth pinning # -- `flexalias-applied-type' and `flexalias-applied-not-str' are that pair. check_eq $P/flexalias-type "_FlexibleAliasCls" "$(pv flexalias_type)" check_eq $P/flexalias-not-a-class "no" "$(pv flexalias_is_class)" check_eq $P/flexalias-applied-type "_FlexibleAliasClsApplied" "$(pv flexalias_applied_type)" check_eq $P/flexalias-applied-not-str "no" "$(pv flexalias_applied_is_str)" check_eq $P/flexalias-second-subscript "yes" "$(pv flexalias_second_subscript)" # THE LAST ARGUMENT WINS (`args[-1]'), so a three-argument alias resolves to its # third and not its second. check_eq $P/flexalias-last-wins "yes" "$(pv flexalias_last_wins)" # A SINGLE ARGUMENT DOES NOT WORK: `args' is then the bare type rather than a # tuple, `args[-1]' subscripts it, and that raises. The two-argument form is a # requirement, not a convention. check_eq $P/flexalias-single-raises "TypeError" "$(pv flexalias_single_raises)" # ------------------------------------------------- the native integer types --- # EACH `__new__' RETURNS AN `int', SO THE CLASS IS NEVER INSTANTIATED, and each # metaclass answers `isinstance' by asking about `int'. THE WIDTH IS NOT # ENFORCED -- see the header: `u8(70000)' is 70000 and nothing truncates, clamps # or raises. Asserted for all four rather than argued from the shared # implementation, because they are four classes upstream could diverge one at a # time. for _t in i64 i32 i16 u8; do check_eq $P/int-$_t-value "5" "$(pv int_${_t}_value)" check_eq $P/int-$_t-type "int" "$(pv int_${_t}_type)" check_eq $P/int-$_t-base16 "255" "$(pv int_${_t}_base16)" check_eq $P/int-$_t-from-str "7" "$(pv int_${_t}_from_str)" check_eq $P/int-$_t-isinst-int "yes" "$(pv int_${_t}_isinstance_int)" check_eq $P/int-$_t-isinst-str "no" "$(pv int_${_t}_isinstance_str)" # `bool' IS AN `int' SUBCLASS, so `isinstance(True, u8)' is True -- which a # caller reading these as C types would not expect and cannot switch off. check_eq $P/int-$_t-isinst-bool "yes" "$(pv int_${_t}_isinstance_bool)" # THE WIDTH IS A PROMISE TO mypyc AND NOTHING ELSE. check_eq $P/int-$_t-no-range-check "70000" "$(pv int_${_t}_overflow)" check_eq $P/int-$_t-negative "-1" "$(pv int_${_t}_negative)" check_eq $P/int-$_t-meta "_NativeIntMeta" "$(pv int_${_t}_meta)" # THE DOCSTRING IS `.format()'ed PER TYPE at import, in a module-scope loop -- # so each carries its OWN name. A loop upstream got wrong once would ship four # identical strings, and `doc-names-self' is what catches that. check_eq $P/int-$_t-doc-head \ "A native fixed-width integer type when used with mypyc." \ "$(pv int_${_t}_doc_head)" check_eq $P/int-$_t-doc-names-self "yes" "$(pv int_${_t}_doc_names_self)" done # THE LOOP VARIABLE IS DELETED at the foot of the source (`del _int_type'), so # it is not left bound in the module. `api-public-names' would not see it -- it # is underscore-prefixed -- so it gets a case of its own. check_eq $P/no-int-type-leftover "no" "$(pv has_int_type_leftover)" # ------------------------------------------------------------- the closure --- # NOTHING OUTSIDE THE STANDARD LIBRARY IS REACHED BY THE IMPORT. This image's # python3 is ONE STATIC BINARY with an empty lib-dynload and no dynamic loader # (BRINTOS-1476), so a release that grew a third-party dependency would fail at # `import' here rather than resolve it. # # Scored against `sys.stdlib_module_names' rather than against 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 mypy_extensions. check_eq $P/pulled-nonstdlib "" "$(pv pulled_nonstdlib)" # ...and the ONE module it does need, named positively -- with `re', which # `typing' drags in. Both are in python3-core, which is why this port needs no # bbappend and why `pkg-depends-exact' above is the packaging-side half of the # same statement. A python3 split change that moved either announces itself # here rather than as an import failure nobody can attribute. check_eq $P/needs-typing "yes" "$(pv needs_typing)" check_eq $P/needs-re "yes" "$(pv needs_re)" # THE PRIVATE IMPORT, by name. `from typing import _type_check' is not public # API and the source says so in a comment -- it is the one thing about this # package a CPython release could break outright, and the `td-*-ann' group above # is what would move when it does. check_eq $P/typing-type-check-present "yes" "$(pv typing_type_check_present)" rm -rf "$W"