. "$BST_LIB" # BST-TIMEOUT: 1500 # THREE python3 starts, and they are the whole cost. Every # # assertion below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output. # # # # WHERE THE THREE GO: the probe, one bare import case, and # # one case asserting the module resolves out of # # site-packages rather than a stray. This package ships NO # # executable, so there is no script group and no start per # # script. # # # # 1500 rather than 99-python3-ordered-set's 1200 for ONE # # reason: the probe LOADS EVERY ZONE. 596 calls to # # pytz.timezone(), each parsing a TZif binary off the # # virtiofs root the first time -- that is the one thing in # # this suite whose cost is not a python3 start, and it is # # 596 file reads on a filesystem whose per-open cost is the # # reason 99-python3 exists. It is worth the budget: a # # zoneinfo tree that ships but does not PARSE would pass # # every count and every path assertion in this file. # # # # 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-pytz 2024.1 (poky meta/recipes-devtools/python/python3-pytz_2024.1.bb, # `inherit pypi setuptools3 ptest', PyPI sdist) on the wasm32-hwjs glibc image -- # the IANA time-zone database as a Python package. # # ============================================================================ # THIS PORT SHIPS DATA, WHICH IS WHAT MAKES IT DIFFERENT FROM ITS SIBLINGS. # ============================================================================ # # Every other python3 port in this Epic's closure ships code, and a suite over it # is a suite over behaviour. pytz is 2.6 MB of binary tz data behind ~1500 lines # of Python, and the data is the artifact a packaging change loses: a recipe that # packaged only `*.py' would import cleanly, expose the whole API, and answer # NOTHING. So the cases here are weighted at the payload -- # # - the zoneinfo tree is present, with its file COUNT pinned; # - every name in `all_timezones' resolves to a file that EXISTS; # - every one of those files PARSES, by loading all 596 zones; # - the TZif magic is read off the file directly, not through pytz, so a # tree of empty placeholders cannot pass; # # -- and the byte total is REPORTED but asserted only as a floor, because it # moves with every tzdata release and pinning it would red on a data bump that is # not this port's business. The COUNT is the structural signal. # # ============================================================================ # THE RECIPE DECLARES SIX RUNTIME DEPENDENCIES. THE IMPORT REACHES ONE. # ============================================================================ # # poky's recipe adds `python3-datetime python3-doctest python3-io python3-pickle # python3-pprint python3-threading'. Measured, five of those six are unnecessary, # and this port removes them in a bbappend. The entries were chosen by the MODULE # NAMES in the source rather than by which poky SPLIT owns each file, and those # are different questions: # # python3-io no `io' is imported anywhere -- and `io.py' is core's. The # split owns _pyio, socket, ssl, tempfile, pipes, termios. # python3-threading `from threading import RLock' IS at module scope in # pytz/lazy.py -- but `threading.py' is core's, and the split # holds only _threading_local, queue and _queue. # python3-pickle never imported; pickling works through __reduce__ and the # CALLER's pickle. # python3-doctest inside `pytz._test()', under `if __name__ == "__main__"'. # python3-pprint inside `if __name__ == "__main__"' in pytz/tzfile.py. # python3-datetime genuinely required, and kept. # # MEASURED BY REFUSAL, NEVER AS A sys.modules DELTA. BRINTOS-2233 is why: a delta # is scored AFTER minus BEFORE and shrinks by whatever the interpreter had # already loaded, and an expected value read off an x86-64 control is wrong on # this image's pristine interpreter. The probe installs a `sys.meta_path' finder # that REFUSES all 18 files of the five splits before pytz is imported, and the # import either survives or does not. That is a claim, not a subtraction. # # THE SAME FINDER RECORDS WHAT IT WAS ASKED FOR, and the suite pins that set -- # but it is named for what it is. `asked_nonpytz' is every module the machinery # had to FIND, which is not every module the import REACHES: a module already # resident in sys.modules is never looked for. `threading' is exactly that case, # so residency is recorded separately and the two are asserted apart rather than # conflated. # # ============================================================================ # THREE MEASURED FACTS A READER WOULD GUESS WRONG. # ============================================================================ # # 1. `type(pytz.timezone("Europe/Moscow")).__name__' is "Europe/Moscow", NOT # "DstTzInfo". tzfile.py builds a fresh class per zone with # `type(zone, (DstTzInfo,), ...)', so the zone name IS the class name. What # separates a DST zone from one that has never had DST is the BASE, and both # are asserted. # # 2. `pytz.exceptions.InvalidTimeError' does NOT derive from ValueError. Its # MRO is InvalidTimeError -> Error -> Exception, so a caller's # `except ValueError' round a `localize(..., is_dst=None)' catches nothing. # `UnknownTimeZoneError' DOES derive from KeyError, so the two failure modes # of this package need different `except' clauses -- which is the kind of # asymmetry a suite is for. # # 3. `__safe_for_unpickling__' is set on `pytz.FixedOffset', NOT on # `pytz.unpickler' -- which is the function pytz exports for the purpose, # and where a reader would go looking for it. # # ============================================================================ # THE TRAP THIS PACKAGE IS FAMOUS FOR IS ASSERTED DIRECTLY. # ============================================================================ # # `datetime(2026, 9, 2, 12, 0, tzinfo=pytz.timezone("Europe/Moscow"))' does not # raise, does not warn, and is WRONG: attaching a pytz DstTzInfo with # `tzinfo=' picks the zone's FIRST historical offset, which for Europe/Moscow is # Local Mean Time at +02:30. `localize' is the API that exists because of it. The # suite asserts BOTH values and that they differ, so the trap is a pinned fact # about this build rather than a warning in a docstring. # # ============================================================================ # UPSTREAM'S OWN TESTS ARE NOT ON THIS IMAGE, BY TWO INDEPENDENT ROUTES. # ============================================================================ # # setup.py uses an explicit `packages = ["pytz"]', so `pytz/tests/' is not a # package and never reaches site-packages. And although the recipe inherits # `ptest' and carries a `do_install_ptest', this distro's DISTRO_FEATURES # carries no `ptest', so ptest.bbclass adds no ${PN}-ptest. Both are asserted # as absences rather than assumed, and the cases in this file stand in their # place. P=python3-pytz SUITE_TIMEOUT_HINT=1500 bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/pytz MOD=$PKGDIR/__init__.py ZI=$PKGDIR/zoneinfo DI=$SITE/pytz-2024.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-pytz.list PROBE=$BST_DATA/python3-pytz-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pytz.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --------------------------------------------------------------- the artifact check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/module-file test -f "$MOD" # A PACKAGE directory, not a flat module -- and it could not be flat, because the # zoneinfo payload lives inside it and is reached through `__file__'s dirname. check_eq $P/no-flat-module "absent" \ "$(if [ -f "$SITE/pytz.py" ]; then echo present; else echo absent; fi)" for _f in exceptions lazy tzinfo tzfile reference; do check $P/module-$_f test -f "$PKGDIR/$_f.py" done # `reference.py' ships and is NOT imported by `__init__' -- a documentation # module. Asserted present here and asserted unreached in the closure group, so # its cost is disk and nothing else. check $P/pycache-dir test -d "$PKGDIR/__pycache__" # ----------------------------------------------------------- the tz data payload check $P/zoneinfo-dir test -d "$ZI" check $P/zoneinfo-zone-tab test -f "$ZI/zone.tab" check $P/zoneinfo-tzdata-zi test -f "$ZI/tzdata.zi" check $P/zoneinfo-iso3166 test -f "$ZI/iso3166.tab" check $P/zoneinfo-zone1970 test -f "$ZI/zone1970.tab" check $P/zoneinfo-zonenow test -f "$ZI/zonenow.tab" for _z in Europe/Moscow America/New_York Asia/Kolkata Australia/Melbourne UTC; do check $P/zoneinfo-file-$(echo "$_z" | tr / -) test -f "$ZI/$_z" done # Read the TZif magic off the file with shell tools, NOT through pytz: a tree of # empty placeholders would satisfy every `test -f' above and every path the # module resolves, and would fail only here and at the probe's load sweep. check_out $P/zoneinfo-tzif-magic "TZif" head -c 4 "$ZI/Europe/Moscow" check_eq $P/zoneinfo-nonempty "yes" \ "$(if [ -s "$ZI/Europe/Moscow" ]; then echo yes; else echo no; fi)" # The file list is COPIED to $TMPDIR before it is read, because the `while read' # below takes it on stdin: a top-level stdin redirect on the virtiofs root makes # dup2(2) return 513 and `ash' exits, ending the suite in silence. # # BRINTOS-WORKAROUND: brintos/linux#73 remove cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO COMPILED EXTENSION. This image's python3 is one static binary with an empty # lib-dynload and no dynamic loader, so a `.so' in this package could not be # loaded at all -- the assertion is about portability, not tidiness. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # Everything shipped is Python source, its bytecode, egg-info metadata, or a file # under zoneinfo/. A file outside that set is a compiled artifact or a stray. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; "$ZI"/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python-and-data "" "$_bad" # The payload is the bulk of the package, and the count is the structural signal # a packaging regression would move. Counted from the SHIPPED FILE LIST rather # than by walking the tree, so a file on disk that dpkg does not own cannot pad # it -- but the list names the 20 zone SUBDIRECTORIES as well as the files, so # entries are filtered to regular files exactly as pkg-only-python-and-data # above does. Counting the raw lines scores 623. check_eq $P/pkg-zoneinfo-file-count "603" \ "$(_n=0 while IFS= read -r _f; do case "$_f" in "$ZI"/*) if [ -f "$_f" ]; then _n=$((_n+1)); fi ;; esac done < "$W/pkgf" echo "$_n")" # ------------------------------------------------------------- the metadata # A dist-info, and built by `bdist_wheel' over setuptools -- which is the # opposite of python3-ordered-set beside it on the roster, whose flit-built # dist-info carries no `top_level.txt' at all. A port reading the module name out # of that file finds it here and finds nothing there, so the presence is asserted # rather than assumed either way. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 2024.1" cat "$DI/METADATA" check_out $P/dist-info-name "Name: pytz" cat "$DI/METADATA" check_out $P/dist-info-wheel-generator "bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-wheel-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-wheel-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_eq $P/dist-info-top-level "pytz" "$(cat "$DI/top_level.txt" 2>/dev/null)" # `zip_safe=True' in setup.py, and setuptools writes the marker for it. It is # inert on this image -- nothing here imports from a zip -- and is asserted only # because its ABSENCE would mean the wheel was cut differently from the one these # values were measured against. check $P/dist-info-zip-safe test -f "$DI/zip-safe" # `locales/pytz.pot' is named in setup.py's `resources' list and DOES NOT SHIP: # the sdist has no `pytz/locales/' directory, so the entry is dead metadata # rather than a packaging loss. Asserted absent so that a release which starts # shipping it is read rather than absorbed. check_eq $P/no-locales-dir "absent" \ "$(if [ -d "$PKGDIR/locales" ]; then echo present; else echo absent; fi)" # ---------------------------------------------------------------- the package sed -n '/^Package: python3-pytz$/,/^$/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: 2024.1-r0" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-datetime "python3-datetime" cat "$W/dep" # THE WHOLE DEPENDS LINE, PINNED -- and this is the case this port exists to # make. poky declares six runtime dependencies; the import reaches one. The five # the bbappend removes are each asserted absent by name below, so a re-acquired # entry names itself in the red rather than only moving a count. check_eq $P/pkg-depends-corrected "Depends: python3-core, python3-datetime" \ "$(sed 's/[[:space:]]*$//' "$W/dep")" for _d in doctest io pickle pprint threading; do check_eq $P/pkg-depends-no-$_d "" \ "$(grep -o "python3-$_d" "$W/dep" 2>/dev/null)" done # NO -ptest PACKAGE, and no upstream tests by the OTHER route either. The recipe # inherits ptest and carries a do_install_ptest, but DISTRO_FEATURES carries no # "ptest" so ptest.bbclass adds no ${PN}-ptest; and setup.py's explicit # `packages=["pytz"]' means pytz/tests/ was never a package to install in the # first place. Two independent closures, both asserted. check_eq $P/pkg-no-ptest "absent" \ "$(if [ -d /usr/lib/python3-pytz/ptest ]; then echo present; else echo absent; fi)" check_eq $P/pkg-no-ptest-status "absent" \ "$(if grep -q '^Package: python3-pytz-ptest$' /var/lib/dpkg/status 2>/dev/null; \ then echo present; else echo absent; fi)" check_eq $P/pkg-no-tests-dir "absent" \ "$(if [ -d "$PKGDIR/tests" ]; then echo present; else echo absent; fi)" # ----------------------------------------------------------------- the import check $P/import python3 -c 'import pytz; pytz.timezone' # The module resolves out of site-packages rather than a stray on the path -- the # case that separates "the package shipped" from "something importable is called # pytz". It matters more here than for a code-only port: the host that built this # suite's expected values had a DIFFERENT, distro-patched pytz in # /usr/lib/python3/dist-packages that shadowed the sdist, which is precisely the # mistake this case catches on target. check_out $P/import-from-site "$PKGDIR" \ python3 -c 'import pytz, os; print(os.path.dirname(pytz.__file__))' # ------------------------------------------------------------------ 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 # 1200s, not the 900s a code-only port uses: the probe loads all 596 zones, and # each first load parses a TZif binary off the virtiofs root. bst_probe $P/probe-run "$W/probe" 1200 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 check_eq $P/probe-complete "yes" "$(pv probe_complete)" # ------------------------------------------------- identity and provenance check_eq $P/version "2024.1" "$(pv version)" check_eq $P/module-answered "$MOD" "$(pv module_file)" check_eq $P/module-is-package "yes" "$(pv module_is_package)" check_eq $P/python-version "3.12" "$(pv python_version)" # The metadata read through importlib rather than off the filesystem. Unlike a # flit-built package there is no dist-info here -- setuptools writes egg-info -- # and importlib.metadata reads either, which is the point of asking it. check_eq $P/dist-version "2024.1" "$(pv dist_version)" check_eq $P/dist-name "pytz" "$(pv dist_name)" # TWO VERSIONS, and they are not the same number. `__version__' is the PyPI # release; OLSON_VERSION is the tz database release the payload was cut from. # A caller that reports one as the other reports the wrong thing. check_eq $P/olson-version "2024a" "$(pv olson_version)" check_eq $P/tzdata-version "2024.1" "$(pv tzdata_version)" # ------------------------------------------------------------- THE CLOSURE # The load-bearing group of this port. Every case here is a measurement by # refusal, taken before pytz was imported -- not a delta, and not a value read # off a control's warm interpreter. check_eq $P/closure-refused-count "18" "$(pv refused_count)" # Anything in the refused set that the interpreter had already loaded is deleted # from sys.modules first. Without that the refusal would be silently PARTIAL -- # and would pass for that reason, which is the one way this measurement could # lie. The expected value is the empty list on a pristine interpreter; a non-empty # one here is not a failure, it is the note that says the deletion did work. check_eq $P/closure-refused-preloaded "[]" "$(pv refused_preloaded)" # THE CLAIM: with the whole membership of python3-doctest, python3-io, # python3-pickle, python3-pprint and python3-threading refused, `import pytz' # still completes. This is what makes the bbappend correct. check_eq $P/closure-import-under-refusal "ok" "$(pv closure_under_refusal)" # WHERE THE ASKED NAMES LIVE -- and deliberately NOT the raw list itself. # `asked_nonpytz' is every module the machinery had to FIND, which depends on # which ones this interpreter already had resident; and this image's python3 is a # different build from the x86-64 control every expected value here was measured # on. Pinning that list would be the BRINTOS-2233 mistake in a new costume: an # expected value read off a control's warm interpreter. It is recorded in the # probe output for a reader, and asserted through the property that does not move. # # Every name asked for must fall inside `core' or `datetime'. One outside them is # a runtime dependency this package has and does not declare -- which is the # whole question this port set out to answer -- whoever preloaded what. check_eq $P/closure-nothing-outside-core-datetime "[]" \ "$(pv asked_outside_core_datetime)" check_eq $P/closure-outside-count "0" "$(pv asked_outside_count)" # datetime is REACHED, by one route or the other. It is the one split the # bbappend keeps, so a build that stopped needing it would mean the declaration # had become over-broad by six rather than five. check_eq $P/closure-datetime-reached "yes" "$(pv datetime_reached_or_resident)" # The package's own submodules, which is also the statement that it is a # directory rather than one flat file. check_eq $P/closure-asked-pytz \ '["pytz", "pytz.exceptions", "pytz.lazy", "pytz.tzfile", "pytz.tzinfo"]' \ "$(pv asked_pytz)" # `reference.py' SHIPS and is NOT imported -- asserted rather than assumed, so # its 200 lines cost disk and cannot fail an import. check_eq $P/closure-reference-unimported "no" "$(pv reference_imported)" check_eq $P/closure-tests-unimported "no" "$(pv tests_importable_after_import)" # WHY python3-threading IS NOT NEEDED THOUGH `threading' IS IMPORTED. Three # separate facts, because the conclusion rests on all three and a reader who # checked only the first would think the split was required. # - threading.py resolves out of the interpreter's own lib dir (core's), not # out of the threading split; # - it was ALREADY RESIDENT before pytz was imported, which is why it is # absent from `asked_nonpytz' above -- residency, not absence of use; # - `_thread' supplies `_local', so threading.py never falls back to # `_threading_local.py', which is the split's own file. check_eq $P/closure-threading-file "/usr/lib/python3.12/threading.py" \ "$(pv threading_module_file)" # Reached OR resident -- and not residency alone, which is invocation-dependent # (measured `yes' running the probe directly and `no' running it the way this # suite does). Either way `threading' is in the closure and `threading.py' is # core's, which is what makes python3-threading unnecessary. check_eq $P/closure-threading-reached "yes" "$(pv threading_reached_or_resident)" check_eq $P/closure-thread-has-local "yes" "$(pv thread_has_local)" check_eq $P/closure-threading-local-is-c "yes" \ "$(pv threading_local_is_thread_local)" # ------------------------------------------------------------- the registries # Counts, pinned against THIS build's zoneinfo. A tzdata bump moves them, which # is exactly when someone should look. check_eq $P/reg-n-all-timezones "596" "$(pv n_all_timezones)" check_eq $P/reg-n-all-timezones-set "596" "$(pv n_all_timezones_set)" check_eq $P/reg-n-common-timezones "433" "$(pv n_common_timezones)" check_eq $P/reg-n-common-set "433" "$(pv n_common_timezones_set)" check_eq $P/reg-n-country-timezones "247" "$(pv n_country_timezones)" check_eq $P/reg-n-country-names "249" "$(pv n_country_names)" # The list and the set agree in size -- there is no duplicate hiding in the list. # Two counts that happen to be equal is a weaker claim than the properties below, # which is why both are asserted. check_eq $P/reg-common-subset "yes" "$(pv common_subset_of_all)" check_eq $P/reg-common-smaller "yes" "$(pv common_smaller_than_all)" check_eq $P/reg-all-sorted "yes" "$(pv all_timezones_sorted)" check_eq $P/reg-common-sorted "yes" "$(pv common_timezones_sorted)" # THE LAZY CONTAINERS ARE THE PACKAGE'S OWN TYPES, not plain list/set/dict -- # which is what pytz.lazy is for, and the reason `threading' is in the closure # at all (LazyList guards itself with an RLock). check_eq $P/reg-all-type "LazyList" "$(pv all_timezones_type)" check_eq $P/reg-all-set-type "LazySet" "$(pv all_timezones_set_type)" check_eq $P/reg-country-tz-type "_CountryTimezoneDict" "$(pv country_timezones_type)" check_eq $P/reg-country-name-type "_CountryNameDict" "$(pv country_names_type)" # Two country lookups, one long and one single-entry, plus the failure path. The # RU list is ORDER-BEARING -- it is west-to-east, not alphabetical, and a # re-derivation that sorted it would pass a membership check and fail here. check_eq $P/reg-country-tz-RU \ '["Europe/Kaliningrad", "Europe/Moscow", "Europe/Kirov", "Europe/Volgograd", "Europe/Astrakhan", "Europe/Saratov", "Europe/Ulyanovsk", "Europe/Samara", "Asia/Yekaterinburg", "Asia/Omsk", "Asia/Novosibirsk", "Asia/Barnaul", "Asia/Tomsk", "Asia/Novokuznetsk", "Asia/Krasnoyarsk", "Asia/Irkutsk", "Asia/Chita", "Asia/Yakutsk", "Asia/Khandyga", "Asia/Vladivostok", "Asia/Ust-Nera", "Asia/Magadan", "Asia/Sakhalin", "Asia/Srednekolymsk", "Asia/Kamchatka", "Asia/Anadyr"]' \ "$(pv country_tz_RU)" check_eq $P/reg-country-tz-GB '["Europe/London"]' "$(pv country_tz_GB)" check_eq $P/reg-country-name-RU "Russia" "$(pv country_name_RU)" check_eq $P/reg-country-name-GB "Britain (UK)" "$(pv country_name_GB)" check_eq $P/reg-country-absent "KeyError" "$(pv country_tz_absent)" # ------------------------------------------------------------------- UTC # The one zone that is a singleton and is not read from a file at all. check_eq $P/utc-str "UTC" "$(pv utc_str)" check_eq $P/utc-zone "UTC" "$(pv utc_zone)" check_eq $P/utc-type "UTC" "$(pv utc_type)" check_eq $P/utc-alias "yes" "$(pv utc_is_UTC_alias)" # `pytz.timezone("UTC")' returns the SAME OBJECT as pytz.utc -- an identity, not # an equality, and the case that says the lookup path is short-circuited for it. check_eq $P/utc-timezone-identity "yes" "$(pv utc_is_timezone_UTC)" check_eq $P/utc-tzname "UTC" "$(pv utc_tzname)" check_eq $P/utc-utcoffset "0:00:00" "$(pv utc_utcoffset)" check_eq $P/utc-dst "0:00:00" "$(pv utc_dst)" # ------------------------------------------------------------------ lookup check_eq $P/tz-moscow-str "Europe/Moscow" "$(pv tz_moscow_str)" check_eq $P/tz-moscow-zone "Europe/Moscow" "$(pv tz_moscow_zone)" # THE CLASS NAME IS THE ZONE NAME, not "DstTzInfo": tzfile.py builds a fresh # class per zone with `type(zone, (DstTzInfo,), ...)'. A reader would guess # wrong, so both the name and the BASE are pinned. check_eq $P/tz-moscow-type "Europe/Moscow" "$(pv tz_moscow_type)" check_eq $P/tz-moscow-base "DstTzInfo" "$(pv tz_moscow_base)" # A zone that has NEVER had DST takes a different base and a different # `localize' code path. `EST' is one; there are 46 among the 596. check_eq $P/tz-est-type "EST" "$(pv tz_est_type)" check_eq $P/tz-est-base "StaticTzInfo" "$(pv tz_est_base)" check_eq $P/tz-bases-differ "yes" "$(pv tz_bases_differ)" check_eq $P/tz-n-static-zones "46" "$(pv n_static_zones)" # AND THE EXAMPLE A READER REACHES FOR IS THE WRONG ONE. `Asia/Kolkata' is the # textbook "no DST" zone and is a DstTzInfo here, because India ran DST in # 1942-45 and ONE historical transition is all it takes. Asserted at the value # it actually has, so the wrong guess is recorded rather than repeated. check_eq $P/tz-kolkata-type "Asia/Kolkata" "$(pv tz_kolkata_type)" check_eq $P/tz-kolkata-not-static "DstTzInfo" "$(pv tz_kolkata_base)" # Cached: the SAME OBJECT on a second lookup. A cache miss would be invisible to # every value-comparing assertion here while costing a TZif parse per call -- # which on this filesystem is the difference the timeout comment is about. check_eq $P/tz-cached "yes" "$(pv tz_cached)" check_eq $P/tz-case-insensitive "yes" "$(pv tz_case_insensitive)" check_eq $P/tz-unknown "UnknownTimeZoneError: 'Mars/Olympus_Mons'" "$(pv tz_unknown)" # UnknownTimeZoneError IS a KeyError, so `except KeyError' catches a bad zone # name. InvalidTimeError is NOT a ValueError (below) -- the two failure modes of # this package need different `except' clauses, and that asymmetry is the kind of # thing a caller gets wrong once. check_eq $P/tz-unknown-is-keyerror "yes" "$(pv tz_unknown_is_keyerror)" # ------------------------------------------------- localize, and the famous trap check_eq $P/localize-iso "2026-09-02T12:00:00+03:00" "$(pv localize_iso)" check_eq $P/localize-tzname "MSK" "$(pv localize_tzname)" check_eq $P/localize-utcoffset "3:00:00" "$(pv localize_utcoffset)" # `datetime(..., tzinfo=tz)' DOES NOT RAISE AND IS WRONG. It picks the zone's # first historical offset -- Local Mean Time, +02:30 for Europe/Moscow -- which # is why `localize' exists. Both values are pinned, and so is the fact that they # differ, so the trap is a measurement about this build and not a warning. check_eq $P/trap-replace-tzinfo-iso "2026-09-02T12:00:00+02:30" "$(pv replace_tzinfo_iso)" check_eq $P/trap-replace-tzinfo-tzname "LMT" "$(pv replace_tzinfo_tzname)" check_eq $P/trap-replace-is-lmt "yes" "$(pv replace_tzinfo_is_lmt)" check_eq $P/trap-replace-differs "yes" "$(pv replace_differs_from_localize)" check_eq $P/astimezone-iso "2026-09-02T12:00:00+03:00" "$(pv astimezone_iso)" check_eq $P/astimezone-roundtrip "2026-09-02T09:00:00+00:00" "$(pv astimezone_roundtrip)" # ---------------------------------------------- DST arithmetic and normalize # Adding a timedelta across a DST boundary leaves the OLD offset attached: the # result is a valid datetime with the wrong wall-clock reading, and `normalize' # is what repairs it. All four values are pinned so the repair is visible as a # change rather than asserted as a property. check_eq $P/dst-before-iso "2026-11-01T00:30:00-04:00" "$(pv dst_before_iso)" check_eq $P/dst-before-tzname "EDT" "$(pv dst_before_tzname)" check_eq $P/dst-add-raw-iso "2026-11-01T02:30:00-04:00" "$(pv dst_add_raw_iso)" check_eq $P/dst-add-raw-tzname "EDT" "$(pv dst_add_raw_tzname)" check_eq $P/dst-normalized-iso "2026-11-01T01:30:00-05:00" "$(pv dst_add_normalized_iso)" check_eq $P/dst-normalized-tzname "EST" "$(pv dst_add_normalized_tzname)" check_eq $P/dst-normalize-changed "yes" "$(pv normalize_changed_offset)" # AMBIGUOUS -- 2026-11-01 01:30 US/Eastern happens TWICE, and `is_dst' is which # one the caller means. The default is False, which is a real choice and not an # absence of one. check_eq $P/amb-isdst-true "2026-11-01T01:30:00-04:00" "$(pv amb_isdst_true)" check_eq $P/amb-isdst-true-tzname "EDT" "$(pv amb_isdst_true_tzname)" check_eq $P/amb-isdst-false "2026-11-01T01:30:00-05:00" "$(pv amb_isdst_false)" check_eq $P/amb-isdst-false-tzname "EST" "$(pv amb_isdst_false_tzname)" check_eq $P/amb-default-is-false "yes" "$(pv amb_default_is_isdst_false)" check_eq $P/amb-isdst-none "AmbiguousTimeError: 2026-11-01 01:30:00" "$(pv amb_isdst_none)" # NON-EXISTENT -- 2026-03-08 02:30 US/Eastern never happens. `is_dst=None' is the # only setting that reports it; True and False each return a real instant, and # the two DIFFER, which is what makes the silence dangerous. check_eq $P/non-isdst-none "NonExistentTimeError: 2026-03-08 02:30:00" "$(pv non_isdst_none)" check_eq $P/non-isdst-true "2026-03-08T02:30:00-04:00" "$(pv non_isdst_true)" check_eq $P/non-isdst-false "2026-03-08T02:30:00-05:00" "$(pv non_isdst_false)" # The hierarchy, which decides what a caller's `except' catches. check_eq $P/exc-ambiguous-mro \ "AmbiguousTimeError|InvalidTimeError|Error|Exception" "$(pv exc_ambiguous_base)" check_eq $P/exc-nonexistent-mro \ "NonExistentTimeError|InvalidTimeError|Error|Exception" "$(pv exc_nonexistent_base)" # NOT a ValueError -- the answer a reader would guess, and the one that decides # whether an `except ValueError' round `localize(..., is_dst=None)' catches # anything. It catches nothing. check_eq $P/exc-invalid-not-valueerror "no" "$(pv exc_invalid_is_valueerror)" for _n in AmbiguousTimeError InvalidTimeError NonExistentTimeError \ UnknownTimeZoneError Error; do check_eq $P/exc-has-$_n "yes" "$(pv exc_has_$_n)" done # ------------------------------------------------------------- FixedOffset # The only tzinfo pytz builds without reading a file. check_eq $P/fixed-str "pytz.FixedOffset(330)" "$(pv fixed_str)" check_eq $P/fixed-utcoffset "5:30:00" "$(pv fixed_utcoffset)" check_eq $P/fixed-dst "0:00:00" "$(pv fixed_dst)" # tzname is None, not a string -- a FixedOffset has an offset and no NAME, and a # caller formatting `%Z' gets an empty field rather than an abbreviation. check_eq $P/fixed-tzname "None" "$(pv fixed_tzname)" # The zero offset is not a fresh object: it IS pytz.utc. check_eq $P/fixed-zero-is-utc "yes" "$(pv fixed_zero_is_utc)" check_eq $P/fixed-cached "yes" "$(pv fixed_cached)" # --------------------------------------------------- the payload, through pytz check_eq $P/data-zoneinfo-dir "$ZI" "$(pv zoneinfo_dir)" check_eq $P/data-zoneinfo-isdir "yes" "$(pv zoneinfo_isdir)" check_eq $P/data-zoneinfo-files "603" "$(pv zoneinfo_files)" # The byte total is REPORTED by the probe and asserted only as a floor: it moves # with every tzdata release, and pinning it would red on a data bump that is not # this port's business. The COUNT above is the structural signal. check_eq $P/data-zoneinfo-bytes-floor "yes" "$(pv zoneinfo_bytes_over_800k)" check_eq $P/data-zone-tab "yes" "$(pv zone_tab_present)" check_eq $P/data-tzdata-zi "yes" "$(pv tzdata_zi_present)" # EVERY NAME IN all_timezones RESOLVES TO A FILE THAT EXISTS -- the assertion # that ties the registry to the payload. A partial zoneinfo passes every count # above and fails here. check_eq $P/data-no-missing-zones "0" "$(pv zoneinfo_missing_count)" check_eq $P/data-missing-head "[]" "$(pv zoneinfo_missing_head)" # AND EVERY ONE OF THEM PARSES. Resolving a path is not reading a TZif binary, # and this is the case that costs the suite its timeout budget -- 596 loads. It # is the difference between "the data shipped" and "the data works". check_eq $P/data-no-load-failures "0" "$(pv zone_load_failures)" check_eq $P/data-load-failures-head "[]" "$(pv zone_load_failures_head)" check_eq $P/data-tzif-magic "TZif2" "$(pv tzif_magic)" # open_resource -- the accessor a caller uses to reach the data directly. check_eq $P/resource-open-magic "TZif" "$(pv open_resource_magic)" check_eq $P/resource-exists-true "True" "$(pv resource_exists_true)" check_eq $P/resource-exists-false "False" "$(pv resource_exists_false)" # A PATH-TRAVERSAL GUARD, and a security property rather than a convenience: a # `..' segment is refused before the filename is ever built, so a caller passing # an attacker-supplied zone name cannot read outside the payload. check_eq $P/resource-traversal-refused "ValueError: Bad path segment: '..'" \ "$(pv open_resource_traversal)" # ------------------------------------------------------------------ the surface check_eq $P/surface-has-all "yes" "$(pv has_all)" check_eq $P/surface-n-all "14" "$(pv n_dunder_all)" for _n in timezone utc UTC FixedOffset all_timezones common_timezones \ country_timezones country_names open_resource resource_exists \ AmbiguousTimeError NonExistentTimeError UnknownTimeZoneError \ InvalidTimeError BaseTzInfo unpickler build_tzinfo; do check_eq $P/surface-has-$_n "yes" "$(pv has_$_n)" done # ------------------------------------------------------------------- pickling # WHICH WORKS, AND NOT BECAUSE python3-pickle IS DECLARED. pytz never imports # pickle -- the closure group above completed the import with the whole split # refused. It defines __reduce__ and exports `unpickler', and the CALLER's pickle # does the rest. These cases show the round trip working on an image that # installs the split for other reasons, so the two facts stand side by side. check_eq $P/pickle-roundtrip-iso "2026-09-02T12:00:00+03:00" "$(pv pickle_roundtrip_iso)" check_eq $P/pickle-roundtrip-tzname "MSK" "$(pv pickle_roundtrip_tzname)" # A zone survives the round trip as the SAME OBJECT, not a copy -- which is what # `unpickler' is for, and what keeps `tz is pytz.timezone(...)' true across a # process boundary. check_eq $P/pickle-tz-identity "yes" "$(pv pickle_tz_roundtrip_is_same)" check_eq $P/pickle-utc-identity "yes" "$(pv pickle_utc_roundtrip_is_same)" # THE SENTINEL IS ON FixedOffset, NOT ON `unpickler' -- which is the function # pytz exports for the purpose, and where a reader would go looking for it. Both # are asserted so the file says where it actually is. check_eq $P/pickle-unpickler-no-sentinel "no" "$(pv unpickler_safe_for_unpickling)" check_eq $P/pickle-fixedoffset-sentinel "yes" "$(pv fixedoffset_safe_for_unpickling)" rm -rf "$W"