#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-cached-property. Emits `key=value' lines, one per line, in file order; the suite reads them with grep. A python3 start on this arm is tens of seconds and every measurement here is an attribute read on a small object -- microseconds beside the start -- so taking the start ONCE is the difference between a half-minute suite and a twenty-minute one. THE TRANSPORT RULES. The runner parses its input line by line, so no value may contain a NEWLINE; the suite splits on the FIRST `=', so a value containing `=' is safe and a key containing one is not. Nothing emitted below carries a memory address: every repr is of an int, a str, a bool or a small tuple of those. The descriptors themselves are never repr'd -- `cached_property' defines no `__repr__', so its repr WOULD carry an address and would differ on every run. WHAT THIS PACKAGE IS. A decorator that turns a method into a property computed ONCE per instance and thereafter served from that instance's `__dict__'. So every case below asserts WHEN THE WRAPPED CALLABLE RUNS, not only what it returns -- a cache that recomputed every time would satisfy a value assertion and fail its entire purpose. Each check class counts its own calls, and the counter is what is asserted. THE CLOCK IS DRIVEN, NOT SLEPT ON. `cached_property_with_ttl' calls the module-global `time' in `cached_property`'s own namespace, so this probe REBINDS `cp.time' to a counter it advances by hand. Upstream's own tests reach for `freezegun' for the same reason; that is a test dependency this image does not have and does not need. A suite that slept would trade minutes of a shared image's verify budget for a worse measurement -- it could show that a value expired, never that it did not expire EARLY. THIS PROBE EXITS 0 EVEN WHEN THE IMPORT FAILS. cached_property is pure Python, so an import failure would be a platform fact about the interpreter rather than about this package, and turning it into eighty empty-string mismatches would bury that fact rather than report it. The failure is reported as `import_ok=no' with the exception, the suite reds exactly one case for it, and every behavioural case is SKIPPED with that reason attached. WHERE THE EXPECTED VALUES COME FROM. Every value the suite compares against was MEASURED by running THIS FILE on an x86-64 control at the same version -- cached-property 1.5.2 from the same PyPI sdist (sha256 9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130, the recipe's own), under CPython 3.12 -- and then re-run there against THIS BUILD's own .deb payload rather than the sdist. This package declares no ext_modules, reads no sizeof, packs no struct and branches on nothing but whether `asyncio' imported, so there is nothing in it that can know the pointer width: a difference between the two arms is a DEFECT and not a property. TWO VALUES ARE EXPECTED TO DIFFER FROM THE CONTROL, and they are the two that say the transcript came from the wasm32 interpreter rather than from a host copy of the module: * `py_maxsize_bits' -- 32 here, 64 on the control. * `thread_ran' / `thread_refused' -- `no'/`yes' here, `yes'/`no' on the control. pthread_create does not produce a RUNNING thread on this arm: the lock and thread-local machinery work and a started thread never runs its body (brintos/linux-in-the-browser#85, the same gap `99-python3`'s own `thread-start-refused' asserts). That is why the threaded variant's concurrency is NOT asserted here as concurrency -- see THE THREADED VARIANT below. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def err(fn, *args, **kw): """The exception TYPE NAME a call raises, or `none' when it returns. The name only: a message carries quoting and, for some builtins, the repr of an argument, neither of which is worth pinning across two arms. """ try: fn(*args, **kw) except BaseException as exc: return type(exc).__name__ return 'none' # ------------------------------------------------------------------ import --- # Pure Python, so this is expected to be uneventful -- but it is the first thing # that runs on this arm and every case below depends on it. # # NOTE the module imports `threading' UNGUARDED at top level (only `asyncio' is # wrapped in try/except). The .deb declares `Depends: python3-core' and nothing # else, so this import is the one that would fail on an image without # python3-threading -- see `dep-*' in the suite, which asserts that pair. try: import cached_property as cp emit('import_ok', 'yes') emit('import_error', '') except BaseException as exc: emit('import_ok', 'no') emit('import_error', '%s: %s' % (type(exc).__name__, str(exc).replace('\n', ' ')[:200])) emit('emitted_count', str(len(emitted) + 1)) emit('probe_done', 'yes') sys.exit(0) import threading # noqa: E402 (after the gate, by design) # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this, every case below would be a statement about # whatever was first on sys.path. `module_file' differs between the control and # the target by construction, so the suite asserts it directly against the # target's own path rather than comparing it to a control value. emit('module_file', getattr(cp, '__file__', '')) # IT IS A MODULE, NOT A PACKAGE. setup.py declares `py_modules=["cached_property"]', # so the payload is ONE FILE beside site-packages rather than a directory -- # which is why the suite's payload cases stat a file where a package port's stat # a directory. A release that grew a package would change this and nothing else # visible. emit('is_package', 'yes' if hasattr(cp, '__path__') else 'no') emit('version_attr', getattr(cp, '__version__', 'no-attr')) emit('author_attr', getattr(cp, '__author__', 'no-attr')) emit('email_attr', getattr(cp, '__email__', 'no-attr')) emit('license_attr', getattr(cp, '__license__', 'no-attr')) try: from importlib.metadata import version as _dv _dist = _dv('cached-property') except BaseException as exc: _dist = 'ERR:%s' % type(exc).__name__ emit('dist_version', _dist) emit('version_agrees', 'yes' if _dist == getattr(cp, '__version__', None) else 'no') # THERE IS NO `__all__'. A hand-maintained export list is what most of this # image's Python ports pin; this module has none, so `from cached_property import # *' exports every public name the module happens to bind -- including the # `threading' and `time' it imported. Pinned as an absence AND as the resulting # public surface, so that a release which added an `__all__' is one red rather # than a rewrite of this section. emit('has_all', 'yes' if hasattr(cp, '__all__') else 'no') emit('public_names', ','.join(sorted(n for n in vars(cp) if not n.startswith('_')))) # `asyncio' IMPORTED, so the coroutine branch in `cached_property.__get__' is # LIVE on this image -- see `async_error' below, which is what that branch # actually does at 3.12. The module tolerates its absence by design # (`except (ImportError, SyntaxError)'), and this is the value that says which # of the two worlds this transcript is from. emit('asyncio_live', 'yes' if cp.asyncio is not None else 'no') # ---------------------------------------------------- the plain descriptor --- # THE PACKAGE'S WHOLE PURPOSE: the wrapped callable runs ONCE per instance. # Every class below counts its own calls, and the COUNTER is the assertion -- # a decorator that recomputed on every read would satisfy the value and fail the # contract. class Plain(object): def __init__(self): self.calls = 0 @cp.cached_property def prop(self): """the docstring, carried onto the descriptor""" self.calls += 1 return self.calls * 10 _p = Plain() # FIVE reads, ONE computation. _vals = [_p.prop for _ in range(5)] emit('plain_calls_after_5_reads', str(_p.calls)) emit('plain_values_all_equal', 'yes' if len(set(_vals)) == 1 else 'no') emit('plain_value', str(_vals[0])) # THE CACHE LIVES IN THE INSTANCE `__dict__', UNDER THE FUNCTION'S NAME, and it # is the RAW value -- no wrapper, no box. This is the mechanism the whole # package rests on, and the three cases separate the three claims: that the key # is there, that it is spelled after the function, and that the value beside it # is the value the property returns. emit('plain_in_dict', 'yes' if 'prop' in _p.__dict__ else 'no') emit('plain_dict_value', repr(_p.__dict__.get('prop'))) emit('plain_dict_keys', ','.join(sorted(_p.__dict__))) # CLASS ACCESS RETURNS THE DESCRIPTOR ITSELF (`obj is None' -> `return self'), # which is what lets `Plain.prop.func' be reached and is why the docstring below # is readable at all. emit('plain_class_access_is_descriptor', 'yes' if isinstance(Plain.prop, cp.cached_property) else 'no') emit('plain_doc', (getattr(Plain.prop, '__doc__', None) or '').replace('\n', ' ')) emit('plain_func_name', getattr(Plain.prop.func, '__name__', 'no-attr')) # IT IS A NON-DATA DESCRIPTOR, and that is not a detail -- it is the reason the # instance `__dict__' entry SHADOWS the descriptor on every later read, which is # what makes the cache free rather than a per-read `__get__' call. A release # that added `__set__' would keep every value case above green and silently make # every read go through the descriptor again. emit('plain_has_set', 'yes' if hasattr(cp.cached_property, '__set__') else 'no') emit('plain_has_delete', 'yes' if hasattr(cp.cached_property, '__delete__') else 'no') # `del obj.prop' FORCES EXACTLY ONE RECOMPUTATION. There is no `__delete__' on # this class, so the `del' removes the instance-dict entry directly and the # descriptor becomes visible again. Asserted as a counter, and asserted to be # ONE -- a `del' that dropped the cache twice, or that left it in place, both # fail here. del _p.prop emit('plain_after_del_in_dict', 'yes' if 'prop' in _p.__dict__ else 'no') _v2 = _p.prop emit('plain_calls_after_del', str(_p.calls)) emit('plain_value_after_del', str(_v2)) _ = _p.prop emit('plain_calls_after_del_second_read', str(_p.calls)) # A SECOND INSTANCE COMPUTES INDEPENDENTLY. The cache is per-instance because it # lives in the instance dict; a decorator that cached on the DESCRIPTOR (which is # per-class, and shared) would pass every case above and leak one object's value # into every other. _q = Plain() emit('second_instance_value', str(_q.prop)) emit('second_instance_calls', str(_q.calls)) emit('first_instance_calls_unchanged', str(_p.calls)) # ASSIGNING OVER THE PROPERTY REPLACES THE CACHED VALUE. The documented # consequence of caching in the instance dictionary: with no `__set__' on the # descriptor, `obj.prop = x' is an ordinary attribute store into the same slot # the cache uses, so the next read serves `x' and the function never runs again. _r = Plain() _ = _r.prop _r.prop = 999 emit('assign_read_back', str(_r.prop)) emit('assign_dict_value', repr(_r.__dict__.get('prop'))) emit('assign_calls', str(_r.calls)) # ASSIGNING BEFORE THE FIRST READ means the function NEVER runs. Same mechanism, # and the case that shows the cache is not merely overwritten but pre-empted. _s = Plain() _s.prop = 7 emit('assign_before_read_value', str(_s.prop)) emit('assign_before_read_calls', str(_s.calls)) # THE CACHE KEY IS THE FUNCTION'S NAME, NOT THE ATTRIBUTE'S. There is no # `__set_name__' on this class, so a descriptor bound under a different name # caches under the name of the callable it wraps -- and then NEVER hits the # cache, because the read looks up the attribute name and finds nothing. It # recomputes on every access, silently. Measured rather than reasoned: the # counter below is the whole point. class Misnamed(object): def __init__(self): self.calls = 0 def _compute(self): self.calls += 1 return self.calls aliased = cp.cached_property(_compute) _m = Misnamed() _ = _m.aliased _ = _m.aliased _ = _m.aliased emit('misnamed_calls_after_3_reads', str(_m.calls)) emit('misnamed_dict_keys', ','.join(sorted(_m.__dict__))) emit('misnamed_cached_under_func_name', 'yes' if '_compute' in _m.__dict__ else 'no') # A `cached_property' OVER AN `async def' RAISES AT READ TIME ON 3.12. # `__get__' routes a coroutine function to `_wrap_in_coroutine', whose body is # decorated with `@asyncio.coroutine' -- REMOVED from asyncio in Python 3.11. # The name is resolved when the wrapper is built, i.e. on the first read, so the # module imports cleanly and the failure arrives only for a caller who wraps an # async method. Not this arm's doing and not this port's: CPython 3.12 against a # package last released in 2020, reproduced identically on the x86-64 control. _async_src = ( 'class A(object):\n' ' @cp.cached_property\n' ' async def prop(self):\n' ' return 1\n' ) _ns = {'cp': cp} try: exec(_async_src, _ns) _A = _ns['A'] emit('async_class_built', 'yes') emit('async_error', err(lambda: _A().prop)) except BaseException as exc: emit('async_class_built', 'no') emit('async_error', 'BUILD:%s' % type(exc).__name__) # ------------------------------------------------------ the threaded variant --- # WHAT CAN AND CANNOT BE ASSERTED HERE. # # `threaded_cached_property' exists to make the compute-once guarantee hold when # several threads read the same instance at once: it takes an RLock and uses # `dict.setdefault' so that a loser of the race adopts the winner's value rather # than computing its own. ASSERTING THAT AS CONCURRENCY IS NOT POSSIBLE ON THIS # ARM -- a started thread never runs its body # (brintos/linux-in-the-browser#85), so a "computed once across four threads" # case here would be measuring a platform gap and calling it a cache. # # So the cases split in two, and the split is the honest one: # # * THE INVARIANT, asserted unconditionally: however many readers actually # ran, the callable ran ONCE and every reader saw the same value. True on # both arms -- on the control because the lock works, here because the # threads never ran and the main thread's own read is the only one. This is # a real assertion about the cache and it is arm-independent. # # * THE WITNESS, `thread_ran', which records WHICH of those two worlds # produced it. `no' here and `yes' on the control; the suite asserts `no' # and attributes it, exactly as `99-python3`'s own `thread-start-refused' # does. If #85 is ever fixed, that case goes red and says so, which is the # regression signal worth having. class Threaded(object): def __init__(self): self.calls = 0 self.seen = [] @cp.threaded_cached_property def prop(self): self.calls += 1 return self.calls * 100 _t = Threaded() _threads = [] for _i in range(4): _th = threading.Thread(target=lambda o=_t: o.seen.append(o.prop)) _threads.append(_th) _start_err = 'none' for _th in _threads: _e = err(_th.start) if _e != 'none': _start_err = _e break # BOUNDED joins. A thread that never runs must not be waited on for ever: this # probe carries the suite, and a hang here is a wedged guest rather than a red. for _th in _threads: try: _th.join(5) except BaseException: pass _alive = sum(1 for _th in _threads if _th.is_alive()) # THE ROBUST WITNESS. `thread_refused' is true when the arm did not give us a # running thread, BY EITHER MECHANISM -- `start()' raising, or returning a thread # whose body never runs. Both readings of #85 appear in this repo's own prose and # they imply different values for `thread_start_error' and # `thread_alive_after_join'; this key is the one that is `yes' here under either, # and `no' on the control under neither. It is what the suite asserts. # # The two mechanism keys below are EMITTED BUT NOT ASSERTED. They were not # measured on this arm -- only on the control, where they read `none' and `0' -- # so pinning either would be a prediction wearing the grammar of a result. They # ride in the transcript so the batch verify RECORDS which mechanism this arm # actually uses, which is the measurement that lets a later port assert it. emit('thread_refused', 'no' if _t.seen else 'yes') emit('thread_ran', 'yes' if _t.seen else 'no') emit('thread_seen_count', str(len(_t.seen))) emit('thread_start_error', _start_err) emit('thread_alive_after_join', str(_alive)) # The invariant, measured after the main thread's own read -- which is the ONLY # read that happened on this arm and the fifth on the control. _t.seen.append(_t.prop) emit('threaded_calls', str(_t.calls)) emit('threaded_values_all_equal', 'yes' if len(set(_t.seen)) == 1 else 'no') emit('threaded_value', str(_t.seen[0])) emit('threaded_in_dict', 'yes' if 'prop' in _t.__dict__ else 'no') emit('threaded_dict_value', repr(_t.__dict__.get('prop'))) # THE LOCK IS REAL AND IT IS REENTRANT. `RLock' rather than `Lock' because the # wrapped callable may itself read the same property; a `Lock' would deadlock # where this one recurses. Its type name is the assertion -- `_thread.RLock' is # the C implementation, which is what a CPython with `_thread' built in gives. emit('threaded_has_lock', 'yes' if hasattr(Threaded.prop, 'lock') else 'no') emit('threaded_lock_type', type(getattr(Threaded.prop, 'lock', None)).__name__) emit('threaded_lock_acquires', 'yes' if getattr(Threaded.prop, 'lock', None) is not None and Threaded.prop.lock.acquire(False) else 'no') try: Threaded.prop.lock.release() except BaseException: pass # ALSO A NON-DATA DESCRIPTOR, for the same reason the plain one is: after the # first read the instance dict shadows it and the lock is never taken again. emit('threaded_has_set', 'yes' if hasattr(cp.threaded_cached_property, '__set__') else 'no') emit('threaded_has_delete', 'yes' if hasattr(cp.threaded_cached_property, '__delete__') else 'no') # `del' RESETS IT, exactly as it resets the plain one. del _t.prop _ = _t.prop emit('threaded_calls_after_del', str(_t.calls)) # ----------------------------------------------------------- the TTL variant --- # THE CLOCK IS OURS FROM HERE. `cached_property_with_ttl.__get__' calls the # module-global `time' in `cached_property`'s namespace, so rebinding that name # is what lets every expiry case below be exact rather than approximate: the # value is asserted NOT to have expired one tick early and TO have expired one # tick late, which no amount of sleeping could establish. _clock = [1000.0] cp.time = lambda: _clock[0] class Ttl(object): def __init__(self): self.calls = 0 @cp.cached_property_with_ttl(ttl=10) def prop(self): self.calls += 1 return self.calls # IT IS A DATA DESCRIPTOR -- the opposite of the two above, and the reason it can # expire at all. With `__set__' and `__delete__' present the instance dict does # NOT shadow it, so every read goes through `__get__' and gets the chance to # check the timestamp. A cache that could expire but was never consulted again # would be the bug this pair of cases catches. emit('ttl_has_set', 'yes' if hasattr(cp.cached_property_with_ttl, '__set__') else 'no') emit('ttl_has_delete', 'yes' if hasattr(cp.cached_property_with_ttl, '__delete__') else 'no') _x = Ttl() _ = _x.prop _ = _x.prop emit('ttl_calls_after_2_reads', str(_x.calls)) # THE STORED SHAPE IS A `(value, timestamp)' PAIR, not a bare value -- the one # place the three variants' on-instance representations differ, and what a # consumer poking at `__dict__' directly would trip over. _stored = _x.__dict__.get('prop') emit('ttl_dict_is_tuple', 'yes' if isinstance(_stored, tuple) else 'no') emit('ttl_dict_len', str(len(_stored)) if isinstance(_stored, tuple) else 'not-tuple') emit('ttl_dict_value', repr(_stored)) # NOT EXPIRED ONE TICK EARLY. The test is `self.ttl < now - last_updated', a # STRICT comparison, so at exactly ttl the value is still fresh. Both sides of # that boundary are asserted -- an implementation that used `<=' would pass the # `before' case and fail the `at' case, which is precisely the off-by-one worth # pinning. _clock[0] = 1009.0 _ = _x.prop emit('ttl_calls_before_expiry', str(_x.calls)) _clock[0] = 1010.0 _ = _x.prop emit('ttl_calls_at_boundary', str(_x.calls)) _clock[0] = 1010.5 _ = _x.prop emit('ttl_calls_past_boundary', str(_x.calls)) emit('ttl_value_after_expiry', str(_x.prop)) # A ttl OF ZERO NEVER EXPIRES. `self.ttl and self.ttl < ...' short-circuits on a # FALSY ttl, and `0' is falsy -- so `ttl=0', which reads like "expire # immediately", is in fact "cache for ever". Measured, not deduced, and pinned # because it is the kind of thing a later release fixes. class TtlZero(object): def __init__(self): self.calls = 0 @cp.cached_property_with_ttl(ttl=0) def prop(self): self.calls += 1 return self.calls _z = TtlZero() _ = _z.prop _clock[0] = 99999.0 _ = _z.prop emit('ttl_zero_calls', str(_z.calls)) # A ttl OF `None' NEVER EXPIRES EITHER, by the same short circuit -- and this one # is the DEFAULT, which is what the bare-decorator form below produces. class TtlNone(object): def __init__(self): self.calls = 0 @cp.cached_property_with_ttl(ttl=None) def prop(self): self.calls += 1 return self.calls _n = TtlNone() _ = _n.prop _clock[0] = 999999.0 _ = _n.prop emit('ttl_none_calls', str(_n.calls)) # THE BARE DECORATOR FORM. `__init__' takes `ttl' positionally and asks whether # it is CALLABLE: applied bare, the decorated function arrives where a ttl was # expected and is recognised as the function. So `@cached_property_with_ttl' # and `@cached_property_with_ttl(ttl=N)' are the same class entered two ways, # and the bare one has no expiry. class TtlBare(object): def __init__(self): self.calls = 0 @cp.cached_property_with_ttl def prop(self): self.calls += 1 return self.calls _b = TtlBare() _ = _b.prop _clock[0] = 9999999.0 _ = _b.prop emit('ttl_bare_calls', str(_b.calls)) emit('ttl_bare_ttl_is_none', 'yes' if TtlBare.prop.ttl is None else 'no') # `__delete__' POPS, AND POPS FORGIVINGLY. `obj.__dict__.pop(name, None)' -- so # deleting an unread property is a no-op rather than the AttributeError an # ordinary `del' would raise. The second `del' below is the case: it asserts an # absence of an exception, which is the whole difference from the plain variant. _d = Ttl() _ = _d.prop del _d.prop emit('ttl_after_del_in_dict', 'yes' if 'prop' in _d.__dict__ else 'no') emit('ttl_del_twice_error', err(lambda: _d.__class__.prop.__delete__(_d))) _ = _d.prop emit('ttl_calls_after_del', str(_d.calls)) # `__set__' STORES A PAIR AND STAMPS IT WITH `now'. Assigning over a TTL # property is therefore NOT the same operation as assigning over a plain one: # there it replaces a bare value, here it writes a freshly timestamped tuple # that will itself expire. Both halves asserted -- the value served, and the # expiry that follows it. _w = Ttl() _clock[0] = 2000.0 _w.prop = 42 emit('ttl_set_dict_value', repr(_w.__dict__.get('prop'))) emit('ttl_set_read_back', str(_w.prop)) emit('ttl_set_calls', str(_w.calls)) _clock[0] = 2011.0 emit('ttl_set_then_expires', str(_w.prop)) emit('ttl_set_calls_after_expiry', str(_w.calls)) # THE METADATA IS COPIED OFF THE FUNCTION by `_prepare_func' -- and `__name__' is # load-bearing rather than cosmetic, because it is the dict key this variant # reads and writes. emit('ttl_name', getattr(Ttl.prop, '__name__', 'no-attr')) emit('ttl_module', getattr(Ttl.prop, '__module__', 'no-attr')) # --------------------------------------------- the threaded TTL variant --- # A SUBCLASS, and the only thing it adds is the lock around the parent's # `__get__'. Asserted as a subclass rather than as a re-measurement of expiry: # if it is the subclass it says it is, every TTL case above already covers it. class TtlThreaded(object): def __init__(self): self.calls = 0 @cp.threaded_cached_property_with_ttl(ttl=10) def prop(self): self.calls += 1 return self.calls emit('tttl_is_subclass', 'yes' if issubclass(cp.threaded_cached_property_with_ttl, cp.cached_property_with_ttl) else 'no') emit('tttl_has_lock', 'yes' if hasattr(TtlThreaded.prop, 'lock') else 'no') emit('tttl_lock_type', type(getattr(TtlThreaded.prop, 'lock', None)).__name__) _tt = TtlThreaded() _clock[0] = 3000.0 _ = _tt.prop _ = _tt.prop emit('tttl_calls_after_2_reads', str(_tt.calls)) _clock[0] = 3011.0 _ = _tt.prop emit('tttl_calls_after_expiry', str(_tt.calls)) # THE ALIASES. Four extra module-level names, and they are `is'-identical to the # two classes rather than subclasses or wrappers -- so a consumer importing any # of them gets the same object and the same behaviour. Pinned by identity # because that is the claim; a release that made one of them a thin subclass # would keep every behavioural case green. emit('alias_cached_property_ttl', 'yes' if cp.cached_property_ttl is cp.cached_property_with_ttl else 'no') emit('alias_timed_cached_property', 'yes' if cp.timed_cached_property is cp.cached_property_with_ttl else 'no') emit('alias_threaded_cached_property_ttl', 'yes' if cp.threaded_cached_property_ttl is cp.threaded_cached_property_with_ttl else 'no') emit('alias_timed_threaded_cached_property', 'yes' if cp.timed_threaded_cached_property is cp.threaded_cached_property_with_ttl else 'no') # ------------------------------------------------------------- interpreter --- # WHICH ARM THIS TRANSCRIPT IS FROM, so that a control run and a target run are # distinguishable in the capture without reading the surrounding log. # # `py_maxsize_bits' is one of the two values in this file the suite does NOT # compare against the control: it reads 64 there and must read 32 here. Without # it -- and without `thread_ran' above -- a suite run against a HOST copy of the # module would pass in full, since every other value here is arm-independent by # construction. emit('py_version', '%d.%d' % sys.version_info[:2]) emit('py_maxsize_bits', str(sys.maxsize.bit_length() + 1)) emit('emitted_count', str(len(emitted) + 2)) emit('probe_done', 'yes') sys.exit(0)