"""Drive pytz across its whole surface, and print one key=value line per fact. ONE INTERPRETER START DOES THE WHOLE OF IT. Every python3 start on this arm is a fresh load of one large static image (99-python3 records the cost), so the suite that drives this asserts on the OUTPUT rather than calling python3 once per case. Every line is `key=value`; a key is emitted at most once. The suite reads the keys it names and ignores the rest, so adding a fact here cannot break it. Failure is reported as a key too -- `*_error` -- rather than only as an exit status, because the case that reds should say which stage stopped and why. ============================================================================ THE IMPORT CLOSURE IS MEASURED BY REFUSAL, NEVER AS A sys.modules DELTA. ============================================================================ BRINTOS-2233 is why. A `set(sys.modules)` taken AFTER minus BEFORE shrinks by whatever the interpreter had already loaded for its own reasons, so it under- reports on a warm interpreter and over-reports on a bare one -- and an expected value read off an x86-64 control is wrong on this image's pristine interpreter, which starts with a different set already resident. Two ports in one BRINTOS-2084 round went red on exactly that. So the closure here is scored ABSOLUTELY, in two ways that need no baseline: 1. A `sys.meta_path` finder is installed BEFORE pytz is imported. It RECORDS every module name the import machinery asks it to find, and REFUSES the ones this port claims are unnecessary. The recorded set is a fact about this import on this interpreter; the refusal is a claim that survives or does not. 2. The refused set is exactly the membership of the five poky python3 splits the recipe declares and `import pytz` never reaches -- python3-doctest, python3-io, python3-pickle, python3-pprint and python3-threading. If `import pytz` completes with all five refused, the declaration is over-broad by five entries and the bbappend that removes them is correct. The refusal is lifted the moment the measurement is taken, so every case after it runs on an ordinary interpreter. WHAT IS NOT IN THE REFUSED SET, DELIBERATELY: `datetime`, `bisect`, `struct`, `threading`, `os` and `sys` are all reached, and all but `datetime` live in python3-core. `threading` is the subtle one -- the MODULE is imported (pytz.lazy does `from threading import RLock`) while `threading.py` itself is in the `core` split, and the `threading` PACKAGE holds only `_threading_local.py`, `queue.py` and `_queue.*.so`, none of which this import reaches. """ import json import sys _seen = set() def emit(key, value): if key in _seen: return _seen.add(key) # One line per fact: the suite greps by key, and an embedded newline would # land as a second, unkeyed line. print("%s=%s" % (key, str(value).replace("\n", " ").replace("\r", " "))) def jout(value): return json.dumps(value) def raises(key, fn, with_message=False): """Emit the exception CLASS a call raises, or `' with its result.""" try: result = fn() except Exception as exc: # noqa: BLE001 - reported, not raised emit(key, "%s: %s" % (type(exc).__name__, exc) if with_message else type(exc).__name__) return emit(key, "") emit(key + "_result", result) # =========================================================================== # 1. THE CLOSURE, MEASURED BY REFUSAL, BEFORE pytz IS IMPORTED. # =========================================================================== # # The membership of the five splits, from poky's python3-manifest.json. These # are FILES the splits own, not modules pytz names: `python3-io' does not own # `io.py' (that is core's) and `python3-threading' does not own `threading.py'. # That is the whole reason the recipe's declaration and the import disagree. REFUSED = [ "doctest", # python3-doctest "_pyio", "socket", "_socket", "ssl", "_ssl", "tempfile", "pipes", "termios", # python3-io "pickle", "_pickle", "_compat_pickle", "pickletools", "shelve", # python3-pickle "pprint", # python3-pprint "_threading_local", "queue", "_queue", # python3-threading ] _refused = set(REFUSED) _asked = [] class _Watcher: """Record every name the import machinery looks for; refuse the claimed-absent.""" def find_spec(self, name, path=None, target=None): _asked.append(name) if name.split(".")[0] in _refused: raise ModuleNotFoundError("No module named %r" % name, name=name) return None # delegate to the ordinary finders # A module already resident is never looked for, so anything in the refused set # that the interpreter loaded for its own reasons is dropped first. Without this # the refusal would be silently partial -- and would pass for that reason. _preloaded = sorted(m for m in list(sys.modules) if m.split(".")[0] in _refused) emit("refused_preloaded", jout(_preloaded)) for _m in _preloaded: del sys.modules[_m] _RESIDENT_BEFORE = set(sys.modules) _watcher = _Watcher() sys.meta_path.insert(0, _watcher) emit("refused_names", jout(sorted(_refused))) emit("refused_count", len(_refused)) try: import pytz emit("closure_under_refusal", "ok") except BaseException as exc: # noqa: BLE001 emit("closure_under_refusal", "FAIL %s: %s" % (type(exc).__name__, exc)) sys.meta_path.remove(_watcher) emit("probe_complete", "no") raise SystemExit(1) # Every name the machinery had to FIND for `import pytz' -- an absolute set, not # a delta. pytz's own submodules are in it, which is itself the statement that # the package is a directory rather than one flat module. _pytz_asked = sorted({n for n in _asked if n.split(".")[0] == "pytz"}) _std_asked = sorted({n for n in _asked if n.split(".")[0] != "pytz"}) emit("asked_pytz", jout(_pytz_asked)) emit("asked_nonpytz", jout(_std_asked)) emit("asked_nonpytz_count", len(_std_asked)) # WHAT `asked_nonpytz' IS AND IS NOT. It is every module the import machinery had # to FIND -- an absolute set, and the honest name for it. It is NOT the set the # import REACHES: a module already resident in sys.modules is never looked for, # so the finder is never consulted for it. `threading' is the case that matters # here -- pytz.lazy does `from threading import RLock', and the interpreter has # already loaded threading for its own reasons, so it is reached and not asked. # # Recording residency separately is what keeps the two apart. Neither number is # a delta, and neither is scored against a control's warm interpreter -- which # is the failure BRINTOS-2233 records. _CANDIDATES = ["threading", "os", "sys", "datetime", "bisect", "struct", "collections", "collections.abc", "io", "pickle", "doctest", "pprint", "queue", "tempfile", "socket"] emit("resident_before", jout(sorted(_RESIDENT_BEFORE & set(_CANDIDATES)))) # Residency is INVOCATION-dependent, not just interpreter-dependent: measured # `yes' running the probe directly and `no' running it the way the suite does. # So it is emitted for the record and the SUITE asserts reached-or-resident, # which is the claim that holds either way. emit("threading_resident_before", "yes" if "threading" in _RESIDENT_BEFORE else "no") emit("threading_reached_or_resident", "yes" if ("threading" in {n.split(".")[0] for n in _std_asked} or "threading" in _RESIDENT_BEFORE) else "no") emit("datetime_reached_or_resident", "yes" if ("datetime" in set(_std_asked) or "datetime" in _RESIDENT_BEFORE) else "no") # THE ASSERTION THAT SURVIVES A DIFFERENT INTERPRETER, and the reason the raw # `asked_nonpytz' list above is emitted for the record but NOT pinned by the # suite. Which modules are ASKED for depends on which the interpreter already # had resident, and this image's python3 is a different build from the x86-64 # control every expected value was measured on -- pinning the raw list would be # the BRINTOS-2233 mistake in a new costume, an expected value read off a # control's warm interpreter. # # What does not depend on that is where the asked names LIVE. Every one of them # must fall inside `core' or `datetime'; a name outside those two is a runtime # dependency this package has and does not declare, whoever preloaded what. # Membership of `core' and `datetime' per poky's python3-manifest.json at this # pin, restricted to the top-level names an import of this shape can plausibly # reach. It is a hand-kept list and that is deliberate: a name appearing here # that is NOT on it reds, which is the right outcome -- it means the closure # MOVED, and someone should check which split owns the newcomer before deciding # it is harmless. `_weakrefset' was exactly that, found by the off-target control # run and confirmed to be core's. _CORE_DATETIME = { # python3-core "abc", "bisect", "_bisect", "builtins", "codecs", "_codecs", "collections", "_collections", "_collections_abc", "contextlib", "copy", "copyreg", "encodings", "enum", "errno", "_frozen_importlib", "functools", "_functools", "genericpath", "gettext", "importlib", "io", "_io", "_imp", "itertools", "keyword", "locale", "_locale", "marshal", "math", "operator", "_operator", "os", "posixpath", "re", "reprlib", "sre_compile", "sre_constants", "sre_parse", "_sre", "stat", "_stat", "string", "struct", "_struct", "sys", "_thread", "threading", "time", "types", "warnings", "weakref", "_weakref", "_weakrefset", "zipimport", # python3-datetime "datetime", "_datetime", "calendar", "_strptime", } _outside = sorted({n.split(".")[0] for n in _std_asked} - _CORE_DATETIME) emit("asked_outside_core_datetime", jout(_outside)) emit("asked_outside_count", len(_outside)) sys.meta_path.remove(_watcher) # The three modules the refusal did NOT cover, each reached at module scope and # each in a split this port does declare (datetime) or inherits (core). import datetime # noqa: E402 import threading # noqa: E402 emit("threading_module_file", getattr(threading, "__file__", "")) # WHY python3-threading IS NOT NEEDED even though `threading' IS imported: # threading.py falls back to `_threading_local' only when `_thread' has no # `_local'. On this image it has one, so the split's file is never reached. try: import _thread emit("thread_has_local", "yes" if hasattr(_thread, "_local") else "no") except Exception as exc: # noqa: BLE001 emit("thread_has_local", "error: %s" % type(exc).__name__) emit("threading_local_is_thread_local", "yes" if threading.local is getattr(__import__("_thread"), "_local", None) else "no") # =========================================================================== # 2. IDENTITY AND PROVENANCE. # =========================================================================== emit("version", pytz.__version__) emit("olson_version", pytz.OLSON_VERSION) emit("tzdata_version", getattr(pytz, "VERSION", "")) emit("module_file", pytz.__file__) emit("module_is_package", "yes" if hasattr(pytz, "__path__") else "no") emit("python_version", "%d.%d" % sys.version_info[:2]) try: import importlib.metadata as _md emit("dist_version", _md.version("pytz")) emit("dist_name", _md.metadata("pytz")["Name"]) except Exception as exc: # noqa: BLE001 emit("dist_error", "%s: %s" % (type(exc).__name__, exc)) # =========================================================================== # 3. THE REGISTRIES -- the four dicts/lists the package exists to carry. # =========================================================================== # # COUNTS, not membership lists: the tz database is versioned data and a count is # the one summary that changes when the data does. The suite pins them against # THIS build's zoneinfo, so a tzdata bump reds them and is read. emit("n_all_timezones", len(pytz.all_timezones)) emit("n_all_timezones_set", len(pytz.all_timezones_set)) emit("n_common_timezones", len(pytz.common_timezones)) emit("n_common_timezones_set", len(pytz.common_timezones_set)) emit("n_country_timezones", len(pytz.country_timezones)) emit("n_country_names", len(pytz.country_names)) # common is a SUBSET of all, and strictly smaller -- the property that makes the # two lists worth having separately. emit("common_subset_of_all", "yes" if set(pytz.common_timezones) <= set(pytz.all_timezones) else "no") emit("common_smaller_than_all", "yes" if len(pytz.common_timezones) < len(pytz.all_timezones) else "no") # Both are SORTED, which callers rely on for a stable menu order. emit("all_timezones_sorted", "yes" if list(pytz.all_timezones) == sorted(pytz.all_timezones) else "no") emit("common_timezones_sorted", "yes" if list(pytz.common_timezones) == sorted(pytz.common_timezones) else "no") # The lazy containers are the package's own types, not plain builtins -- which is # what pytz.lazy exists for, and the reason `threading' is in the closure at all. emit("all_timezones_type", type(pytz.all_timezones).__name__) emit("all_timezones_set_type", type(pytz.all_timezones_set).__name__) emit("country_timezones_type", type(pytz.country_timezones).__name__) emit("country_names_type", type(pytz.country_names).__name__) emit("country_tz_RU", jout(pytz.country_timezones["RU"])) emit("country_tz_GB", jout(pytz.country_timezones["GB"])) emit("country_name_RU", pytz.country_names["RU"]) emit("country_name_GB", pytz.country_names["GB"]) raises("country_tz_absent", lambda: pytz.country_timezones["ZZ"]) # =========================================================================== # 4. UTC -- the one zone that is a singleton and not read from a file. # =========================================================================== emit("utc_str", str(pytz.utc)) emit("utc_zone", pytz.utc.zone) emit("utc_is_UTC_alias", "yes" if pytz.utc is pytz.UTC else "no") emit("utc_is_timezone_UTC", "yes" if pytz.utc is pytz.timezone("UTC") else "no") emit("utc_tzname", pytz.utc.tzname(datetime.datetime(2026, 9, 2))) emit("utc_utcoffset", str(pytz.utc.utcoffset(datetime.datetime(2026, 9, 2)))) emit("utc_dst", str(pytz.utc.dst(datetime.datetime(2026, 9, 2)))) emit("utc_type", type(pytz.utc).__name__) # =========================================================================== # 5. LOOKUP -- the path that reads the zoneinfo binary off the filesystem. # =========================================================================== _moscow = pytz.timezone("Europe/Moscow") emit("tz_moscow_str", str(_moscow)) emit("tz_moscow_zone", _moscow.zone) # `type(tz).__name__' is the ZONE NAME, not a class name a reader would guess: # tzfile.py builds a fresh class per zone with `type(zone, (DstTzInfo,), ...)'. # What separates a DST zone from one that has never had DST is the BASE. emit("tz_moscow_type", type(_moscow).__name__) emit("tz_moscow_base", type(_moscow).__mro__[1].__name__) # The SAME OBJECT on a second lookup -- pytz caches, and a cache miss would be # invisible to every value-comparing assertion while costing a file read per call. emit("tz_cached", "yes" if pytz.timezone("Europe/Moscow") is _moscow else "no") # Case-insensitive on the whole name, which is a documented convenience. emit("tz_case_insensitive", "yes" if pytz.timezone("europe/moscow") is _moscow else "no") raises("tz_unknown", lambda: pytz.timezone("Mars/Olympus_Mons"), with_message=True) emit("tz_unknown_is_keyerror", "yes" if issubclass(pytz.exceptions.UnknownTimeZoneError, KeyError) else "no") # A StaticTzInfo -- a zone that has never had DST -- takes a different class from # a DstTzInfo, and the two answer `localize' by different code paths. # # `EST' -- a fixed-offset zone with exactly one transition info -- and NOT # `Asia/Kolkata', which is the example a reader reaches for and is WRONG: # India ran DST in 1942-45, and one historical transition is all it takes for # tzfile to build a DstTzInfo. Both are measured, because the wrong guess is # more instructive than the right answer alone. _est = pytz.timezone("EST") emit("tz_est_type", type(_est).__name__) emit("tz_est_base", type(_est).__mro__[1].__name__) emit("tz_est_str", str(_est)) _kolkata = pytz.timezone("Asia/Kolkata") emit("tz_kolkata_type", type(_kolkata).__name__) emit("tz_kolkata_base", type(_kolkata).__mro__[1].__name__) emit("tz_bases_differ", "yes" if type(_est).__mro__[1] is not type(_moscow).__mro__[1] else "no") emit("tz_kolkata_str", str(_kolkata)) # How many of the 596 are static at all -- a count that says the two classes are # both genuinely populated rather than one being a curiosity. from pytz.tzinfo import StaticTzInfo as _S # noqa: E402 emit("n_static_zones", sum(1 for _z in pytz.all_timezones if isinstance(pytz.timezone(_z), _S))) # =========================================================================== # 6. localize / normalize -- the API that exists because tzinfo cannot be # attached with `replace(tzinfo=...)' for a DST zone. # =========================================================================== # # THE TRAP THIS PACKAGE IS FAMOUS FOR, asserted directly: a bare # `datetime(..., tzinfo=tz)' picks the zone's FIRST historical offset -- for # Europe/Moscow that is Local Mean Time, 2h30m -- and is silently wrong. _naive = datetime.datetime(2026, 9, 2, 12, 0, 0) emit("localize_iso", _moscow.localize(_naive).isoformat()) emit("localize_tzname", _moscow.localize(_naive).tzname()) emit("localize_utcoffset", str(_moscow.localize(_naive).utcoffset())) _wrong = _naive.replace(tzinfo=_moscow) emit("replace_tzinfo_iso", _wrong.isoformat()) emit("replace_tzinfo_tzname", _wrong.tzname()) emit("replace_tzinfo_is_lmt", "yes" if _wrong.tzname() == "LMT" else "no") emit("replace_differs_from_localize", "yes" if _wrong.isoformat() != _moscow.localize(_naive).isoformat() else "no") _utc_now = pytz.utc.localize(datetime.datetime(2026, 9, 2, 9, 0, 0)) emit("astimezone_iso", _utc_now.astimezone(_moscow).isoformat()) emit("astimezone_roundtrip", _utc_now.astimezone(_moscow).astimezone(pytz.utc).isoformat()) # normalize after arithmetic -- adding a timedelta across a DST boundary leaves # the OLD offset attached, and normalize is what repairs it. _us = pytz.timezone("US/Eastern") _before = _us.localize(datetime.datetime(2026, 11, 1, 0, 30)) _after_raw = _before + datetime.timedelta(hours=2) emit("dst_before_iso", _before.isoformat()) emit("dst_before_tzname", _before.tzname()) emit("dst_add_raw_iso", _after_raw.isoformat()) emit("dst_add_raw_tzname", _after_raw.tzname()) emit("dst_add_normalized_iso", _us.normalize(_after_raw).isoformat()) emit("dst_add_normalized_tzname", _us.normalize(_after_raw).tzname()) emit("normalize_changed_offset", "yes" if _after_raw.utcoffset() != _us.normalize(_after_raw).utcoffset() else "no") # AMBIGUOUS: 2026-11-01 01:30 US/Eastern happens twice. _amb = datetime.datetime(2026, 11, 1, 1, 30) emit("amb_isdst_true", _us.localize(_amb, is_dst=True).isoformat()) emit("amb_isdst_true_tzname", _us.localize(_amb, is_dst=True).tzname()) emit("amb_isdst_false", _us.localize(_amb, is_dst=False).isoformat()) emit("amb_isdst_false_tzname", _us.localize(_amb, is_dst=False).tzname()) emit("amb_default_is_isdst_false", "yes" if _us.localize(_amb).isoformat() == _us.localize(_amb, is_dst=False).isoformat() else "no") raises("amb_isdst_none", lambda: _us.localize(_amb, is_dst=None), with_message=True) # NON-EXISTENT: 2026-03-08 02:30 US/Eastern never happens. _non = datetime.datetime(2026, 3, 8, 2, 30) raises("non_isdst_none", lambda: _us.localize(_non, is_dst=None), with_message=True) emit("non_isdst_true", _us.localize(_non, is_dst=True).isoformat()) emit("non_isdst_false", _us.localize(_non, is_dst=False).isoformat()) # The exception hierarchy, which decides what a caller's `except' catches. emit("exc_ambiguous_base", "|".join(c.__name__ for c in pytz.exceptions.AmbiguousTimeError.__mro__[:4])) emit("exc_nonexistent_base", "|".join(c.__name__ for c in pytz.exceptions.NonExistentTimeError.__mro__[:4])) emit("exc_invalid_is_valueerror", "yes" if issubclass(pytz.exceptions.InvalidTimeError, ValueError) else "no") for _n in ("AmbiguousTimeError", "InvalidTimeError", "NonExistentTimeError", "UnknownTimeZoneError", "Error"): emit("exc_has_%s" % _n, "yes" if hasattr(pytz.exceptions, _n) else "no") # =========================================================================== # 7. FixedOffset -- the only tzinfo pytz builds without reading a file. # =========================================================================== _fo = pytz.FixedOffset(330) emit("fixed_str", str(_fo)) emit("fixed_utcoffset", str(_fo.utcoffset(None))) emit("fixed_dst", str(_fo.dst(None))) emit("fixed_tzname", str(_fo.tzname(None))) emit("fixed_zero_is_utc", "yes" if pytz.FixedOffset(0) is pytz.utc else "no") emit("fixed_cached", "yes" if pytz.FixedOffset(330) is _fo else "no") # =========================================================================== # 8. THE ZONEINFO PAYLOAD -- 2.6 MB of binary tz data is what this package IS. # =========================================================================== # # Every other python3 port in this Epic ships code. This one ships DATA, and the # data is the artifact most likely to be lost by a packaging change: a recipe # that packaged only `*.py' would import cleanly and answer NOTHING. import os # noqa: E402 _zi = os.path.join(os.path.dirname(pytz.__file__), "zoneinfo") emit("zoneinfo_dir", _zi) emit("zoneinfo_isdir", "yes" if os.path.isdir(_zi) else "no") _files, _bytes = 0, 0 for _root, _dirs, _names in os.walk(_zi): for _n in _names: _files += 1 try: _bytes += os.path.getsize(os.path.join(_root, _n)) except OSError: pass emit("zoneinfo_files", _files) # Reported for the record, asserted only as a FLOOR. The file COUNT is the # structural regression signal -- a packaging change that drops the payload takes # the count with it -- while the exact byte total moves with every tzdata release # and pinning it would red on a data bump that is not this port's business. emit("zoneinfo_bytes", _bytes) emit("zoneinfo_bytes_over_800k", "yes" if _bytes > 800000 else "no") emit("zone_tab_present", "yes" if os.path.isfile(os.path.join(_zi, "zone.tab")) else "no") emit("tzdata_zi_present", "yes" if os.path.isfile(os.path.join(_zi, "tzdata.zi")) else "no") # Every name in all_timezones resolves to a file that exists -- the one assertion # that ties the registry to the payload. A partial zoneinfo would pass every # count above and fail here. _missing = [z for z in pytz.all_timezones if not os.path.exists(os.path.join(_zi, *z.split("/")))] emit("zoneinfo_missing_count", len(_missing)) emit("zoneinfo_missing_head", jout(_missing[:5])) # And every one of them LOADS -- resolving a path is not the same as parsing the # TZif binary the path points at. _bad = [] for _z in pytz.all_timezones: try: pytz.timezone(_z) except Exception as exc: # noqa: BLE001 _bad.append("%s:%s" % (_z, type(exc).__name__)) emit("zone_load_failures", len(_bad)) emit("zone_load_failures_head", jout(_bad[:5])) # The TZif magic, read off the file rather than through pytz -- the check that # the .deb carried the binaries and not, say, empty placeholders. try: with open(os.path.join(_zi, "Europe", "Moscow"), "rb") as _f: _magic = _f.read(5) emit("tzif_magic", _magic.decode("ascii", "replace")) except Exception as exc: # noqa: BLE001 emit("tzif_magic", "error: %s" % type(exc).__name__) # open_resource, the accessor a caller uses to reach the data directly. try: _s = pytz.open_resource("Europe/Moscow") emit("open_resource_magic", _s.read(4).decode("ascii", "replace")) _s.close() except Exception as exc: # noqa: BLE001 emit("open_resource_magic", "error: %s" % type(exc).__name__) emit("resource_exists_true", pytz.resource_exists("Europe/Moscow")) emit("resource_exists_false", pytz.resource_exists("Mars/Olympus_Mons")) # The path-traversal guard in open_resource, which is a security property and not # a convenience: a `..' segment is refused before the path is ever built. raises("open_resource_traversal", lambda: pytz.open_resource("../../etc/passwd"), with_message=True) # =========================================================================== # 9. THE SURFACE, AND WHAT IS NOT ON IT. # =========================================================================== emit("has_all", "yes" if hasattr(pytz, "__all__") else "no") emit("n_dunder_all", len(getattr(pytz, "__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"): emit("has_%s" % _n, "yes" if hasattr(pytz, _n) else "no") # `reference' ships in the package and is NOT imported by `__init__' -- a # documentation module, and the one file whose absence from the closure is worth # stating rather than assuming. emit("reference_imported", "yes" if "pytz.reference" in sys.modules else "no") emit("tests_importable_after_import", "yes" if "pytz.tests" in sys.modules else "no") # =========================================================================== # 10. PICKLING -- which works, and NOT because python3-pickle is declared. # =========================================================================== # # pytz never imports `pickle'. It defines `__reduce__' and exports `unpickler', # which is what makes a localized datetime picklable -- by the CALLER's pickle, # whose dependency the caller owns. The refusal above completed the import with # python3-pickle's whole membership missing; this group then imports pickle # EXPLICITLY and shows the round trip works, so the two facts stand side by side: # pytz does not need the split, and pickling still works where it is installed. try: import pickle as _pickle _pk = _pickle.loads(_pickle.dumps(_moscow.localize(_naive))) emit("pickle_roundtrip_iso", _pk.isoformat()) emit("pickle_roundtrip_tzname", _pk.tzname()) emit("pickle_tz_roundtrip_is_same", "yes" if _pickle.loads(_pickle.dumps(_moscow)) is _moscow else "no") emit("pickle_utc_roundtrip_is_same", "yes" if _pickle.loads(_pickle.dumps(pytz.utc)) is pytz.utc else "no") # The sentinel is set on FixedOffset, NOT on `unpickler' -- a reader who went # looking for it on the function pytz exports for the purpose would find # nothing. Both are recorded so the suite states where it actually is. emit("unpickler_safe_for_unpickling", "yes" if getattr(pytz.unpickler, "__safe_for_unpickling__", False) else "no") emit("fixedoffset_safe_for_unpickling", "yes" if getattr(pytz.FixedOffset, "__safe_for_unpickling__", False) else "no") except Exception as exc: # noqa: BLE001 emit("pickle_error", "%s: %s" % (type(exc).__name__, exc)) emit("probe_complete", "yes")