. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. # # Everything else reads the filesystem or dpkg with shell # # builtins, or greps one probe transcript. # # # # The three: a bare `import speaklater', a second import # # whose STDERR is the assertion, and the probe -- which # # takes ONE start for 122 measurements. This package ships # # NO executable and no entry point, so there is no shipped # # program to run as a real process and no exit status to # # assert: nothing here needs a fourth start. # # # # The work inside a start is a few hundred attribute # # lookups and string operations over strings under 20 # # characters, plus one pickle round trip. It measures # # 0.003s on the x86-64 control -- 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, against the ~60s a python3 start # # has measured at here. The start dominates and nothing in # # this file is quadratic in anything. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed goes in the # # test plan rather than in this comment, which would cost an # # image build to keep the shipped suite byte-identical. # # python3-speaklater 1.3 (meta-openembedded/meta-python # recipes-devtools/python/python3-speaklater_1.3.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # speaklater is one class and three functions -- 5216 bytes of pure Python -- # and it exists for one property: a string whose VALUE is produced at the # moment it is USED, not at the moment it is defined. # # sval = 'Hello World' # string = make_lazy_string(lambda: sval) # nothing evaluated yet # sval = 'Hallo Welt' # str(string) # 'Hallo Welt' # # That is what lets a module-level translated constant -- defined at import # time, before any request has arrived and any locale has been chosen -- pick # up the locale that is active when it is finally rendered. `make_lazy_gettext' # is the same trick wrapped around a gettext-shaped lookup. # # The object forwards the whole `str' interface by proxying to `self.value', # which re-invokes the callable on EVERY access. `calls-after-3-reads' is the # case that says so: three reads, three calls, no cache. A cache would make the # library wrong and would pass every other case in this file. # # ============================================================================ # THE PROXY IS NOT A `str', AND THAT IS THE FIRST THING A CALLER TRIPS OVER. # ============================================================================ # # `isinstance(lazy, str)' is False -- asserted -- so every `isinstance' guard # in a caller's code rejects a lazy string, and that is exactly why the library # ships `is_lazy_string'. The forwarding is thorough (len, indexing, slicing, # +, *, %, in, iteration, all six comparisons, and every `str' method through # __getattr__), so the failure never looks like a missing method; it looks like # a type check that says no. # # A forwarded METHOD returns a plain `str', not a lazy one -- `upper-result- # type' and `upper-result-is-lazy' are the pair that states it. So `.upper()' # on a lazily-translated constant EVALUATES it, at import time, in whatever # locale happens to be current, and the laziness is gone with no error to say # so. That is the trap this package is most likely to be misused through, and # it is two cases rather than a paragraph in a design doc. # # ============================================================================ # `make_lazy_gettext' BINDS THE TABLE ONE LEVEL TOO EARLY. MEASURED. # ============================================================================ # # This is the one place the library does not do what the rest of it does, and # it is easy to get backwards -- this suite's own probe was written with the # wrong expectation first and corrected by the measurement. # # def lazy_gettext(string): # return make_lazy_string(lookup_func(), string) # ^^^^^^^^^^^^^ CALLED HERE, not at access # # What is held lazily is the msgid and the ALREADY-RESOLVED lookup callable. So: # # MUTATE the table translations['Yes'] = 'Si' -> seen (gettext-yes-2) # REBIND the table translations = {...} -> NOT seen # (gettext-table-rebind-unseen: still 'Tief') # # The docstring's own thread-local example only works in the first form. A # caller who swaps `l.translations' wholesale per request gets every string # handed out before the swap reading the old table, silently and forever. The # string is still lazy -- `gettext-captured-table-mutate' proves it by mutating # the captured dict through the bound method's `__self__' and seeing the change # -- it is lazy over the wrong object. # # A miss is the dict's own default, None, so `str(lazy)' renders the four # characters "None" rather than falling back to the msgid. Asserted, because a # template rendering `None' is the shape that failure actually takes. # # ============================================================================ # WHAT THIS PACKAGE STILL CARRIES FROM PYTHON 2. FOUR THINGS, EACH ASSERTED. # ============================================================================ # # speaklater 1.3 is from 2011 and never had a Python 3 release. It IMPORTS and # it WORKS, because the proxy is built from operators Python 3 kept. Four # members are Python 2 and each fails in its own way: # # 1. UNHASHABLE. The class defines `__eq__' and no `__hash__'. Python 3 -- # unlike Python 2 -- sets `__hash__ = None' for exactly that shape, so # `hash(lazy)', `{lazy}' and `{lazy: 1}' all raise TypeError. This is the # one that matters in practice: a translated constant used as a dict key # is an ordinary thing to write. The workaround is asserted beside it -- # hash the `str()', which equals the plain string's hash. # 2. `dir(lazy)' RAISES. `__dir__' returns `dir(unicode)', and `unicode' is # not a name in Python 3, so NameError comes out of a dunder that the # interpreter, a debugger and tab-completion all call. `__getattr__' # routes `__members__' to the same place, so that raises too. # 3. `__unicode__' NAMES `unicode' AS WELL. Python 3 never calls it, so it # is inert -- but a caller porting Python 2 code that calls it explicitly # gets a NameError rather than a string. # 4. `__nonzero__' IS DEAD CODE. Python 3's truth protocol is `__bool__', # which is not defined. `bool(lazy)' still works, but only because # `__len__' catches it -- so truth is answering "is it empty?" rather than # the question that method asked. Both halves are asserted, and the method # is called directly to show it is present and returns the right answer to # nobody. # # NONE OF THESE ARE THIS IMAGE'S. Every one reproduces identically on an # x86-64 CPython 3.12; none involves the wasm32 runtime, the static # interpreter, or the absent dynamic loader. They are asserted so that they are # a recorded property of the shipped package rather than something the next # caller rediscovers, and no brintos.io issue is filed for them: they are # upstream's, and `no-public-upstream-reports' governs the other direction. # # The package's own test is `doctest.testmod()' under a `__main__' guard, and # it is Python 2 prose throughout -- `>>> unicode(string)', `>>> print yes', # `lu'Hello World'' reprs. It CANNOT pass on Python 3, so it is not run; # running it would be asserting a failure. What IS asserted is that `doctest' # is never imported by the library, since that import sits inside the guard -- # which is why this package needs no python3-doctest and declares none. # # ============================================================================ # PURE PYTHON, ONE FLAT MODULE, AND NO IMPORT AT ALL. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an empty lib-dynload and no # dynamic loader (BRINTOS-1476), so a module carrying a compiled extension # could not be imported here. speaklater ships no C -- its wheel says # `Root-Is-Purelib: true' -- and `setup.py' declares `py_modules', so what # lands in site-packages is a single `speaklater.py' and NOT a package # directory. Both are asserted, because a packaging change that made it a # package would leave every case here passing and the import path different. # # The library has no import statement outside the `__main__' guard, so its # runtime closure is itself and nothing else -- `import-closure-delta' measures # that rather than describing it. That is the whole of why the recipe's # `Depends:' is `python3-core' alone, asserted here as an exact match rather # than a substring, so a scanner artefact appearing in a later release is a red # rather than a silent addition. BRINTOS-2046 -- the empty Py* export surface # that refuses a CPython extension at dlopen on this image -- does not reach a # package with no extension to refuse. # # ============================================================================ # HOW THE VALUES IN THIS FILE WERE ARRIVED AT. # ============================================================================ # # Every expectation below was MEASURED, not read off the source: the probe was # run under CPython 3.12.3 on the x86-64 build host against THIS BUILD'S OWN # python3-speaklater_1.3-r0_wasm32.deb, unpacked and put on PYTHONPATH. 119 of # the 122 probe values came out of that run and are transcribed mechanically. # The suite has NOT run on target yet -- the batch verify is its first # on-target execution -- so anything this file says about the wasm32 arm # specifically is grounded rather than measured, and says so where it matters. P=python3-speaklater # A canary that is not about speaklater at all: it says this file was reached, # parsed and executed. Without it a suite that died on its first line is # indistinguishable from one that was never installed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages MOD=$SITE/speaklater.py DI=$SITE/speaklater-1.3.dist-info PKGLIST=/var/lib/dpkg/info/python3-speaklater.list PROBE=$BST_DATA/python3-speaklater-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-speaklater.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # -------------------------------------------------------------------------- # WHAT LANDED ON THE FILESYSTEM. # -------------------------------------------------------------------------- # `setup.py' declares `py_modules=['speaklater']', so this is a FLAT MODULE: # one file in site-packages, no package directory. The negative case is the # one with teeth -- a packaging change to a package directory would leave the # import working and every behavioural case below green, with the shipped # layout silently different. check $P/site-packages-dir test -d "$SITE" check $P/module-file test -s "$MOD" check $P/no-package-dir test ! -d "$SITE/speaklater" check $P/pycache test -s "$SITE/__pycache__/speaklater.cpython-312.pyc" # -------------------------------------------------------------------------- # WHAT THE PACKAGE SHIPS, FROM dpkg's OWN FILE LIST. # -------------------------------------------------------------------------- # Pure Python means: no shared object, nothing under bin, and nothing at all # that is not a .py, a .pyc or dist-info metadata. The last of the three is the # general statement and the first two are the ones a reader recognises. 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)" check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" _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 dist-info METADATA. # -------------------------------------------------------------------------- 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.3" cat "$DI/METADATA" check_out $P/dist-info-top-level "speaklater" cat "$DI/top_level.txt" # `Root-Is-Purelib: true' is the wheel's own statement that the package carries # no compiled extension -- the property this port depends on. 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 recipe's LIC_FILES_CHKSUM names LICENSE, and unlike some sdists this one # ships it inside dist-info too. check $P/dist-info-license test -s "$DI/LICENSE" # RECORD carries a sha256 over the shipped module. Asserting that one line is a # TRUNCATION GUARD for a 5216-byte file that nothing else here checks whole: a # packaging step that copied it short would leave `import speaklater' working # and some later attribute missing, which reads as a library fault. check_out $P/record-module-digest \ "speaklater.py,sha256=f7f4x5AKD0IAwWlmYMv2FKfO5I7JF_AS3oZglQNqPpg,5216" \ cat "$DI/RECORD" # -------------------------------------------------------------------------- # THE INSTALLED PACKAGE, AND ITS ONE DEPENDENCY. # -------------------------------------------------------------------------- sed -n '/^Package: python3-speaklater$/,/^$/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-version "Version: 1.3-r0" cat "$W/st" # EXACT, not a substring. The library has no import statement outside its # `__main__' guard, so python3-core alone is the whole truth -- and asserting # it exactly means a scanner artefact appearing in a later release reds here # instead of arriving unremarked. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact "Depends: python3-core" "$(cat "$W/dep")" # -------------------------------------------------------------------------- # THE IMPORT, IN ITS OWN PROCESS. # -------------------------------------------------------------------------- # Separate from the probe deliberately: if the import itself is what fails, the # probe emits nothing and every case below it is unscorable, so the bare import # has to be its own scored result to say which of the two happened. check $P/import python3 -c 'import speaklater' # A clean import says NOTHING on stderr. A DeprecationWarning from a 2011 # package on a 3.12 interpreter is a real possibility and would otherwise pass # unnoticed. check_eq $P/import-quiet "" \ "$(python3 -c 'import speaklater' 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 bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } 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 # `probe_done' last and `emitted_count' beside it: together they separate a # probe that died two thirds of the way through from a library that returned # the wrong answers. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/emitted-count "122" "$(pv emitted_count)" # The module the interpreter actually answered with -- asserted against the # path dpkg installed, so a stray copy earlier on sys.path is caught here # rather than producing puzzling values below. check_eq $P/module-answered "$MOD" "$(pv module_file)" # -------------------------------------------------------------------------- # EVERYTHING BELOW IS ONE PROBE VALUE PER CASE. # -------------------------------------------------------------------------- # Transcribed mechanically from a run of this exact probe against this build's # own .deb under CPython 3.12.3 on x86-64. Every value is `ascii()'-escaped by # the probe, so each expectation here is a 7-bit literal that no locale on the # guest can change the reading of. # ---- identity and shape ---------------------------------------------------- # `import-closure-delta' is the measurement behind this port's dependency line: # what `import speaklater' dragged into sys.modules beyond the baseline. The # answer is the module itself and nothing else. # `public-names' pins the exported surface at exactly three functions, so a # release that added a fourth reds here and says which. check_eq $P/import-closure-delta 'speaklater' "$(pv import_closure_delta)" check_eq $P/is-package 'no' "$(pv is_package)" check_eq $P/has-dunder-version 'no' "$(pv has_dunder_version)" check_eq $P/has-all 'no' "$(pv has_all)" check_eq $P/dist-version '1.3' "$(pv dist_version)" check_eq $P/has-is-lazy-string 'yes' "$(pv has_is_lazy_string)" check_eq $P/has-make-lazy-string 'yes' "$(pv has_make_lazy_string)" check_eq $P/has-make-lazy-gettext 'yes' "$(pv has_make_lazy_gettext)" check_eq $P/has-LazyString 'yes' "$(pv has_LazyString)" check_eq $P/class-name '_LazyString' "$(pv class_name)" check_eq $P/class-slots '_func,_args,_kwargs' "$(pv class_slots)" check_eq $P/class-has-dict 'no' "$(pv class_has_dict)" check_eq $P/public-names 'is_lazy_string,make_lazy_gettext,make_lazy_string' "$(pv public_names)" # ---- the lazy property, which is the whole point --------------------------- # The same object, read three times with the source rebound between reads. If # any one of these six values were fixed at construction the library would be # pointless, and `calls-after-3-reads' is the statement that it is not merely # uncached-looking but genuinely re-invoked -- three reads, three calls. check_eq $P/lazy-type '_LazyString' "$(pv lazy_type)" check_eq $P/lazy-str-1 "'Hello World'" "$(pv lazy_str_1)" check_eq $P/lazy-repr-1 "\"l'Hello World'\"" "$(pv lazy_repr_1)" check_eq $P/lazy-value-1 "'Hello World'" "$(pv lazy_value_1)" check_eq $P/lazy-str-2 "'Hallo Welt'" "$(pv lazy_str_2)" check_eq $P/lazy-repr-2 "\"l'Hallo Welt'\"" "$(pv lazy_repr_2)" check_eq $P/lazy-value-2 "'Hallo Welt'" "$(pv lazy_value_2)" check_eq $P/lazy-upper-2 "'HALLO WELT'" "$(pv lazy_upper_2)" check_eq $P/lazy-str-3 "'Hello World'" "$(pv lazy_str_3)" check_eq $P/calls-before '0' "$(pv calls_before)" check_eq $P/calls-after-3-reads '3' "$(pv calls_after_3_reads)" # ---- positional and keyword arguments (new in 1.2) ------------------------- # Held on the object and replayed at each access. The first parameter is named # `__func', so a caller may pass a keyword called `func' through to the # callback without shadowing it -- `lazy-kwarg-named-func' is that case. check_eq $P/lazy-args "'left-right'" "$(pv lazy_args)" check_eq $P/lazy-kwargs "'left+right'" "$(pv lazy_kwargs)" check_eq $P/lazy-kwarg-named-func "'func=passed-through'" "$(pv lazy_kwarg_named_func)" # ---- the forwarded `str' interface ----------------------------------------- # len, indexing, slicing, +, *, %, `in', iteration, all six comparisons. The # reflected direction is worth its own case: `str < lazy' has no # `str.__lt__(lazy)', so Python asks the lazy object -- which defines no # reflected operators -- and gets there through `__gt__' instead. Ordering # therefore works from both sides, which is what `sorted-mixed' relies on and # what the class defines all six comparisons for. # # Truth has NO `__bool__' here (see the Python 2 residue below); it works only # because `__len__' catches it. `bool-empty' and `len-empty' are the pair that # shows the fallback giving the right answer. check_eq $P/len '6' "$(pv len)" check_eq $P/getitem-0 "'a'" "$(pv getitem_0)" check_eq $P/getitem-neg "'f'" "$(pv getitem_neg)" check_eq $P/slice "'bcd'" "$(pv slice)" check_eq $P/slice-step "'ace'" "$(pv slice_step)" check_eq $P/getitem-oob 'IndexError' "$(pv getitem_oob)" check_eq $P/add-right "'abcdefGH'" "$(pv add_right)" check_eq $P/add-left "'GHabcdef'" "$(pv add_left)" check_eq $P/mul "'abcdefabcdef'" "$(pv mul)" check_eq $P/rmul "'abcdefabcdef'" "$(pv rmul)" check_eq $P/mod "'a=one b=two'" "$(pv mod)" check_eq $P/rmod "'wrapped:abcdef'" "$(pv rmod)" check_eq $P/contains-yes 'True' "$(pv contains_yes)" check_eq $P/contains-no 'False' "$(pv contains_no)" check_eq $P/iter "'abcdef'" "$(pv iter)" check_eq $P/eq-str 'True' "$(pv eq_str)" check_eq $P/eq-other 'False' "$(pv eq_other)" check_eq $P/ne-str 'True' "$(pv ne_str)" check_eq $P/lt 'True' "$(pv lt)" check_eq $P/le 'True' "$(pv le)" check_eq $P/gt 'True' "$(pv gt)" check_eq $P/ge 'True' "$(pv ge)" check_eq $P/lt-reflected 'True' "$(pv lt_reflected)" check_eq $P/sorted-mixed "'apple,date,fig,pear'" "$(pv sorted_mixed)" check_eq $P/bool-nonempty 'True' "$(pv bool_nonempty)" check_eq $P/bool-empty 'False' "$(pv bool_empty)" check_eq $P/len-empty '0' "$(pv len_empty)" # ---- format() and f-strings ------------------------------------------------ # There is no `__format__', so these reach `object.__format__', which delegates # to `__str__' for an EMPTY format spec and REFUSES a non-empty one. So `{}' # and f'{x}' work and `f"{x:>10}"' is a TypeError naming _LazyString -- a # template author's mistake that is better found here than in a rendered page. # `{!s}' and `{!r}' go through str()/repr() first, so both work. check_eq $P/format-empty-spec "'abcdef'" "$(pv format_empty_spec)" check_eq $P/str-format "'[abcdef]'" "$(pv str_format)" check_eq $P/fstring "'[abcdef]'" "$(pv fstring)" check_eq $P/percent-s "'[abcdef]'" "$(pv percent_s)" check_eq $P/format-width-spec 'TypeError' "$(pv format_width_spec)" check_eq $P/fstring-width-spec 'TypeError' "$(pv fstring_width_spec)" check_eq $P/str-format-conv-s "'abcdef'" "$(pv str_format_conv_s)" check_eq $P/str-format-conv-r "\"l'abcdef'\"" "$(pv str_format_conv_r)" # ---- method forwarding, and the trap in it --------------------------------- # Anything `str' has, a lazy string appears to have -- through `__getattr__' on # the evaluated value. But the RESULT is a plain `str', not a lazy one: # `upper-result-type' and `upper-result-is-lazy' are that pair. `.upper()' on a # lazily-translated constant evaluates it, at import time, in whatever locale # is current, and the laziness is gone with nothing said. An attribute the # string does not have raises AttributeError, from the `str', not from here. check_eq $P/upper "'ABCDEF'" "$(pv upper)" check_eq $P/lower "'abcdef'" "$(pv lower)" check_eq $P/strip "'pad'" "$(pv strip)" check_eq $P/split "['a', 'b', 'c']" "$(pv split)" check_eq $P/replace "'ab..ef'" "$(pv replace)" check_eq $P/startswith 'True' "$(pv startswith)" check_eq $P/encode "b'abcdef'" "$(pv encode)" check_eq $P/find '2' "$(pv find)" check_eq $P/join "'x-y'" "$(pv join)" check_eq $P/upper-result-type 'str' "$(pv upper_result_type)" check_eq $P/upper-result-is-lazy 'no' "$(pv upper_result_is_lazy)" check_eq $P/missing-attr 'AttributeError' "$(pv missing_attr)" # ---- a non-ASCII payload --------------------------------------------------- # The library is FOR internationalisation, so a value outside ASCII is the # ordinary case. Every expectation here is the probe's `ascii()' escape, so the # case is a 7-bit literal whatever the guest's locale is -- which this port # does not control. `unicode-len' being 7 rather than a byte count is the # statement that the proxy is over a `str' and not over bytes. check_eq $P/unicode-str "'caf\xe9 \u4e2d\u6587'" "$(pv unicode_str)" check_eq $P/unicode-len '7' "$(pv unicode_len)" check_eq $P/unicode-upper "'CAF\xc9 \u4e2d\u6587'" "$(pv unicode_upper)" check_eq $P/unicode-encode-utf8 "b'caf\xc3\xa9 \xe4\xb8\xad\xe6\x96\x87'" "$(pv unicode_encode_utf8)" check_eq $P/unicode-getitem "'\u4e2d'" "$(pv unicode_getitem)" # ---- is_lazy_string, and why it has to exist ------------------------------- # `isinstance(lazy, str)' is False. That is the README's own warning and the # reason this function ships: every `isinstance' guard in a caller's code # rejects a lazy string, and the forwarding is thorough enough that the failure # never looks like a missing method. check_eq $P/is-lazy-true 'True' "$(pv is_lazy_true)" check_eq $P/is-lazy-false-str 'False' "$(pv is_lazy_false_str)" check_eq $P/is-lazy-false-evaluated 'False' "$(pv is_lazy_false_evaluated)" check_eq $P/is-lazy-false-none 'False' "$(pv is_lazy_false_none)" check_eq $P/is-lazy-false-bytes 'False' "$(pv is_lazy_false_bytes)" check_eq $P/isinstance-str 'False' "$(pv isinstance_str)" check_eq $P/isinstance-object 'True' "$(pv isinstance_object)" # ---- make_lazy_gettext, including the binding it gets wrong ----------------- # `gettext-yes-1'/`-2' mutate the table and see it. `gettext-table-rebind- # unseen' REBINDS the table and does not -- because `lazy_gettext' calls # `lookup_func()' when it wraps, not when it reads. `gettext-captured-table- # mutate' then reaches the dict it captured through the bound method's # `__self__' and mutates THAT, which is seen: the string is still lazy, over # the wrong object. See the header for why this is the one case in the file # that has to be read carefully. # # A miss is the dict's own default -- None -- so `str()' of it renders the four # characters "None" rather than falling back to the msgid. check_eq $P/gettext-type '_LazyString' "$(pv gettext_type)" check_eq $P/gettext-yes-1 "'Ja'" "$(pv gettext_yes_1)" check_eq $P/gettext-yes-2 "'Si'" "$(pv gettext_yes_2)" check_eq $P/gettext-no "'Nein'" "$(pv gettext_no)" check_eq $P/gettext-missing "'None'" "$(pv gettext_missing)" check_eq $P/gettext-missing-value 'None' "$(pv gettext_missing_value)" check_eq $P/gettext-rewrap-identity 'True' "$(pv gettext_rewrap_identity)" check_eq $P/gettext-of-plain-is-lazy 'True' "$(pv gettext_of_plain_is_lazy)" check_eq $P/gettext-table-mutate-1 "'Tief'" "$(pv gettext_table_mutate_1)" check_eq $P/gettext-table-rebind-unseen "'Tief'" "$(pv gettext_table_rebind_unseen)" check_eq $P/gettext-captured-table-mutate "'Tiefer'" "$(pv gettext_captured_table_mutate)" check_eq $P/gettext-rebind-still-lazy 'True' "$(pv gettext_rebind_still_lazy)" # ---- copy and deepcopy ----------------------------------------------------- # `__copy__' returns `self': a lazy string is immutable in the only sense that # matters, so a shallow copy is the object itself. `deepcopy' has no override # and goes the generic route, which still yields a lazy string. check_eq $P/copy-is-same 'True' "$(pv copy_is_same)" check_eq $P/deepcopy-is-lazy 'True' "$(pv deepcopy_is_lazy)" check_eq $P/deepcopy-str "'abcdef'" "$(pv deepcopy_str)" # ---- pickle ---------------------------------------------------------------- # `__getstate__'/`__setstate__' exist because `__slots__' leaves no `__dict__' # to pickle. Over a picklable module-level callable a lazy string round-trips # and stays lazy; over a lambda -- which is how the docstring and every other # case here builds one -- it cannot be pickled at all, and the refusal comes # from pickle rather than from speaklater. check_eq $P/pickle-roundtrip "'picklable'" "$(pv pickle_roundtrip)" check_eq $P/pickle-roundtrip-is-lazy 'True' "$(pv pickle_roundtrip_is_lazy)" check_eq $P/pickle-lambda 'PicklingError' "$(pv pickle_lambda)" check_eq $P/getstate-len '3' "$(pv getstate_len)" # ============================================================================ # PYTHON 2 RESIDUE 1 of 4 -- THE LAZY STRING IS UNHASHABLE. # ============================================================================ # The class defines `__eq__' and no `__hash__', and Python 3 sets `__hash__ = # None' for exactly that shape. So it cannot go in a set or be a dict key -- # and a translated constant used as a dict key is an ordinary thing to write. # The workaround is asserted beside it: hash the `str()', which equals the # plain string's hash. Upstream's, not this image's; reproduces on x86-64. check_eq $P/hash-slot-is-none 'yes' "$(pv hash_slot_is_none)" check_eq $P/hash 'TypeError' "$(pv hash)" check_eq $P/set-insert 'TypeError' "$(pv set_insert)" check_eq $P/dict-key 'TypeError' "$(pv dict_key)" check_eq $P/hash-of-str-equals-str 'True' "$(pv hash_of_str_equals_str)" check_eq $P/set-of-str "['abcdef']" "$(pv set_of_str)" # ============================================================================ # PYTHON 2 RESIDUE 2 of 4 -- `dir(lazy)' RAISES NameError. # ============================================================================ # `__dir__' returns `dir(unicode)' and `unicode' is not a name in Python 3, so # the NameError comes out of a dunder the interpreter, a debugger and # tab-completion all call. `__getattr__' routes `__members__' to the same # place. check_eq $P/dir 'NameError' "$(pv dir)" check_eq $P/members 'NameError' "$(pv members)" # ============================================================================ # PYTHON 2 RESIDUE 3 of 4 -- `__unicode__' NAMES `unicode' TOO. # ============================================================================ # Python 3 never calls it, so it is inert. A caller porting Python 2 code that # calls it explicitly gets a NameError rather than a string; the method being # PRESENT is what makes that a surprise rather than an AttributeError. check_eq $P/has-unicode-method 'yes' "$(pv has_unicode_method)" check_eq $P/unicode-method 'NameError' "$(pv unicode_method)" # ============================================================================ # PYTHON 2 RESIDUE 4 of 4 -- `__nonzero__' IS DEAD CODE. # ============================================================================ # Python 3's truth protocol is `__bool__', which is not defined here. Truth # still works, but only via `__len__' -- so `bool(lazy)' is answering "is it # empty?" rather than the question `__nonzero__' asked. The method is called # directly to show it is present and returns the right answer to nobody. check_eq $P/has-nonzero 'yes' "$(pv has_nonzero)" check_eq $P/has-bool-dunder 'no' "$(pv has_bool_dunder)" check_eq $P/nonzero-called-directly 'True' "$(pv nonzero_called_directly)" # ---- the package's own test is not run, and that is deliberate ------------- # `doctest.testmod()' under a `__main__' guard, written in Python 2 prose # throughout -- `>>> unicode(string)', `>>> print yes', `lu'...'' reprs. It # cannot pass on 3.12, and running it would be asserting a failure. What is # asserted instead is that the import inside that guard is never reached, which # is why this package needs no python3-doctest and declares none. check_eq $P/doctest-not-imported 'yes' "$(pv doctest_not_imported)" rm -rf "$W"