# python3-types-setuptools probe. ONE interpreter start, every measurement below. # # Emits `key=value' lines on stdout. The suite reads them by key, so a case added # here without a reader in the suite is inert rather than fatal, and a key the # suite reads but the probe never emits reads as the empty string -- which # `probe_done' and `emitted_count' are what catch. # # EXITS 0 WHATEVER HAPPENS, for the reason every probe in this directory does: a # non-zero exit turns one red into a probe overrun and costs the transcript. # Every result is reported as a VALUE and never as a status. # # NOTHING HERE IS A FIXTURE. This distribution is stub-only -- 88 `.pyi' files # and two `METADATA.toml' markers, with no executable line anywhere in it -- so # there is no behaviour to drive and nothing to set up. What this probe does # instead is read the installed payload and PARSE it, which is the one genuine # demand a stubs package makes of a ported interpreter. # # THE PARSE IS THE POINT, and this payload is a far larger body of it than the # stub ports beside it in this batch: 88 files against types-psutil's fourteen, # spanning the distutils command classes, the `setup'/`Distribution' entry # points and the whole `pkg_resources' API. `.pyi' syntax is a corner of the # grammar that appears almost nowhere else in an image's python payload -- # `@overload' stacks, `...' as an entire function body, PEP 604 unions in # annotation position, `TypeAlias' assignments, `if sys.version_info' at module # and class scope. # # TWO PACKAGES, NOT ONE, and that shape is asserted explicitly. `setuptools-stubs' # and `pkg_resources-stubs' are installed by one wheel, and a packaging failure # that dropped one of them would leave the other entirely intact -- which is the # most likely way this port breaks and the reason every count below is kept per # package rather than summed. # # THE FILE SET IS ASSERTED AGAINST THE WHEEL'S OWN RECORD, not against a list # typed into this file. `RECORD' carries a sha256 and a byte count for every file # the wheel installed, so re-deriving both from what is on disk answers "is the # installed set exactly what the packaging promised" without a hand-written # expectation that a release would silently outdate. import ast import base64 import hashlib import importlib import importlib.util import os import sys import traceback _emitted = [] def emit(key, value): _emitted.append(key) sys.stdout.write("%s=%s\n" % (key, value)) def yn(b): return "yes" if b else "no" def flat(s): """One line, so a value carrying newlines stays one counted result.""" return s.replace("\r", "").replace("\n", "|") def guard(key, fn): """Emit fn()'s value, or `RAISED:' if it raised. One probe case that throws must not take the remaining cases down with it.""" try: emit(key, fn()) except Exception as e: emit(key, "RAISED:%s" % type(e).__name__) DIST = "types_setuptools-69.0.0.20240125.dist-info" ST = "setuptools-stubs" PR = "pkg_resources-stubs" # ------------------------------------------------------------------ layout --- # WHERE THE PAYLOAD IS, FOUND RATHER THAN ASSUMED. The suite pins the answer to # the target's site-packages, so a payload satisfied from somewhere else on # sys.path -- a leftover in the cwd, a second install -- is a red there rather # than a green run measuring the wrong tree. Deriving it here also lets this # probe run unchanged against an x86-64 control, which is where every expected # value below was measured. _site = "" for _d in sys.path: if _d and os.path.isdir(os.path.join(_d, ST)): _site = _d break emit("site_dir", _site) emit("stubs_found", yn(bool(_site))) if not _site: emit("probe_done", "yes") emit("emitted_count", len(_emitted) + 1) sys.exit(0) SD = os.path.join(_site, ST) PD = os.path.join(_site, PR) DD = os.path.join(_site, DIST) # THE TWO-PACKAGE SHAPE, asserted as two separate facts rather than as one. A # wheel that installed only `setuptools-stubs' would satisfy `stubs_found' above # and everything derived from it; only this pair notices. emit("has_setuptools_stubs", yn(os.path.isdir(SD))) emit("has_pkg_resources_stubs", yn(os.path.isdir(PD))) def _tree(root): """Relative paths of every regular file under root, byte-sorted.""" out = [] for dirpath, _dirnames, filenames in os.walk(root): for fn in filenames: out.append(os.path.relpath(os.path.join(dirpath, fn), root)) return sorted(out) def _subdirs(root): out = [] for dirpath, dirnames, _filenames in os.walk(root): for dn in dirnames: out.append(os.path.relpath(os.path.join(dirpath, dn), root)) return sorted(out) _st = _tree(SD) if os.path.isdir(SD) else [] _pr = _tree(PD) if os.path.isdir(PD) else [] # THE SETUPTOOLS SIDE IS 76 FILES ACROSS FIVE SUBDIRECTORIES, which is too long # to read on a console as one string, so it is pinned three ways instead: the # subdirectory set (a release adding or dropping a stub subpackage), the counts, # and a sha256 over the sorted path list (any change at all). WHICH file differs # is answered by `record_missing' / `record_unlisted' below, so nothing is lost # by not printing all 76 here. emit("st_subdir_set", ",".join(_subdirs(SD))) emit("st_file_count", len(_st)) emit("st_pyi_count", sum(1 for n in _st if n.endswith(".pyi"))) emit("st_top_level_pyi_count", sum(1 for n in _st if n.endswith(".pyi") and "/" not in n)) emit("st_file_set_sha256", hashlib.sha256("\n".join(_st).encode("utf-8")).hexdigest()) # THE PKG_RESOURCES SIDE IS SIX FILES, so it is printed whole. This is the half # a partial install is most likely to drop, and a name is more use than a hash # when it does. emit("pr_file_set", ",".join(_pr)) emit("pr_file_count", len(_pr)) emit("pr_pyi_count", sum(1 for n in _pr if n.endswith(".pyi"))) emit("pr_subdir_set", ",".join(_subdirs(PD))) # NO EXECUTABLE CODE SHIPS, asserted RECURSIVELY over both trees rather than over # the recipe. A `.py' here would be a behaviour this package promises it does not # have; a `.so' would be an ImportError besides, because python3 on this image is # one static binary with an empty lib-dynload (BRINTOS-1476). _all = [(ST, n) for n in _st] + [(PR, n) for n in _pr] emit("py_files", ",".join("%s/%s" % (p, n) for p, n in _all if n.endswith(".py"))) emit("so_files", ",".join("%s/%s" % (p, n) for p, n in _all if n.endswith(".so"))) emit("pycache_dirs", ",".join( "%s/%s" % (p, d) for p, root in ((ST, SD), (PR, PD)) for d in _subdirs(root) if os.path.basename(d) == "__pycache__")) # PEP 561: a stub-only distribution is discovered by its `-stubs' suffix and must # NOT carry the `py.typed' marker an inline-typed package uses. Asserted as an # absence so a release that added one is visible. emit("py_typed_files", ",".join("%s/%s" % (p, n) for p, n in _all if os.path.basename(n) == "py.typed")) # Neither stub package may carry an `__init__.py'. `.pyi' is the whole payload; # a `.py' beside it would make the directory a real importable package. emit("init_py_files", ",".join("%s/%s" % (p, n) for p, n in _all if os.path.basename(n) == "__init__.py")) emit("st_has_init_pyi", yn(os.path.isfile(os.path.join(SD, "__init__.pyi")))) emit("pr_has_init_pyi", yn(os.path.isfile(os.path.join(PD, "__init__.pyi")))) def _read(p): with open(p, "rb") as fh: return fh.read() # --------------------------------------------------------------- METADATA --- # EACH STUB PACKAGE CARRIES ITS OWN MARKER FILE, and they are read separately # rather than assumed identical -- the pair is what says both halves of the wheel # landed and both aim at the same setuptools release. That release (`69.0.*') is # the one fact about this package not derivable from the file names. guard("st_metadata_toml_bytes", lambda: len(_read(os.path.join(SD, "METADATA.toml")))) guard("st_metadata_toml_text", lambda: flat(_read(os.path.join(SD, "METADATA.toml")).decode("utf-8").strip())) guard("pr_metadata_toml_bytes", lambda: len(_read(os.path.join(PD, "METADATA.toml")))) guard("pr_metadata_toml_text", lambda: flat(_read(os.path.join(PD, "METADATA.toml")).decode("utf-8").strip())) # ----------------------------------------------------------------- RECORD --- # THE INSTALLED SET AGAINST THE WHEEL'S OWN FILE LIST. RECORD is `path,sha256=< # urlsafe-b64, unpadded>,size' per line -- the installer's own manifest -- so # re-deriving both halves from disk asserts the payload against the packaging # instead of against an expectation typed here. # # RECORD's own line carries no digest and no size (it cannot checksum itself), so # it is counted and then skipped rather than scored. _rec_entries = 0 _rec_missing = [] _rec_size_bad = [] _rec_hash_bad = [] _rec_paths = [] try: for _line in _read(os.path.join(DD, "RECORD")).decode("utf-8").splitlines(): if not _line.strip(): continue _p, _h, _s = _line.rsplit(",", 2) _rec_entries += 1 _rec_paths.append(_p) if not _h or not _s: continue _full = os.path.join(_site, _p) if not os.path.isfile(_full): _rec_missing.append(_p) continue _blob = _read(_full) if len(_blob) != int(_s): _rec_size_bad.append(_p) _alg, _b64 = _h.split("=", 1) _want = base64.urlsafe_b64decode(_b64 + "=" * (-len(_b64) % 4)) if hashlib.new(_alg, _blob).digest() != _want: _rec_hash_bad.append(_p) emit("record_read", "yes") except Exception: emit("record_read", "no") emit("record_error", flat(traceback.format_exc().strip())) emit("record_entries", _rec_entries) emit("record_missing", ",".join(sorted(_rec_missing))) emit("record_size_mismatch", ",".join(sorted(_rec_size_bad))) emit("record_hash_mismatch", ",".join(sorted(_rec_hash_bad))) # RECORD ALSO COVERS BOTH PACKAGES, which is worth counting apart from the total: # a wheel that shipped one of the two would still produce a self-consistent # RECORD, so "the manifest matches the disk" alone cannot see it. emit("record_setuptools_entries", sum(1 for p in _rec_paths if p.startswith(ST + "/"))) emit("record_pkg_resources_entries", sum(1 for p in _rec_paths if p.startswith(PR + "/"))) # THE OTHER DIRECTION. RECORD naming every installed file does not say that every # installed file is named -- a payload carrying an EXTRA `.pyi' would satisfy # every check above. Scored over both stub trees, which is the half a release # would change. _recorded = set(p for p in _rec_paths if p.startswith(ST + "/") or p.startswith(PR + "/")) _ondisk = set("%s/%s" % (ST, n) for n in _st) | set("%s/%s" % (PR, n) for n in _pr) emit("record_unlisted", ",".join(sorted(_ondisk - _recorded))) # ------------------------------------------------------------------- parse --- # EVERY `.pyi' THROUGH THE INTERPRETER'S OWN PARSER. `type_comments=True' is the # mode a type checker uses, and it is a genuinely different code path in CPython's # parser -- it turns on the `# type:' comment grammar on top of the ordinary one, # so a red here that a plain parse would not produce is a real finding about this # arm rather than about the file. _parse_fail = [] _parse_ok = 0 _nodes = 0 _trees = {} for _pkg, _root, _rel in [(ST, SD, _st), (PR, PD, _pr)]: for _n in _rel: if not _n.endswith(".pyi"): continue _key = "%s/%s" % (_pkg, _n) try: _src = _read(os.path.join(_root, _n)).decode("utf-8") _t = ast.parse(_src, filename=_key, type_comments=True) _trees[_key] = _t _parse_ok += 1 _nodes += sum(1 for _ in ast.walk(_t)) except Exception as e: _parse_fail.append("%s:%s" % (_key, type(e).__name__)) emit("parse_ok_count", _parse_ok) emit("parse_failures", ",".join(_parse_fail)) emit("parse_node_count", _nodes) # THE PARSE HAS TEETH, and this is what says so. A harness that silently caught # everything would report `parse_ok_count' correctly for 88 good files and would # also report it for 88 empty ones. Feeding the same parser a file that MUST fail # proves the mode is really on and the exception really propagates. def _neg(): try: ast.parse("def f(:\n", filename="", type_comments=True) return "no" except SyntaxError: return "yes" guard("parse_negative_control", _neg) # `...' AS A WHOLE FUNCTION BODY is the stub form, and it is worth asserting # structurally rather than trusting the parse alone: a body that came back as # something other than a bare `Ellipsis' expression would mean the parser had # reinterpreted the construct this whole distribution is written in. Class bodies # are counted too -- `class Library(Extension): ...' is the form the command and # error hierarchies here are written in, and it is a different production. def _ellipsis_bodies(kinds): n = 0 for _t in _trees.values(): for node in ast.walk(_t): if isinstance(node, kinds): b = node.body if (len(b) == 1 and isinstance(b[0], ast.Expr) and isinstance(b[0].value, ast.Constant) and b[0].value.value is Ellipsis): n += 1 return n guard("ellipsis_body_count", lambda: _ellipsis_bodies((ast.FunctionDef, ast.AsyncFunctionDef))) guard("ellipsis_class_body_count", lambda: _ellipsis_bodies((ast.ClassDef,))) # `@overload' STACKS. The construct a stub file leans on hardest and the one a # parser is most likely to get wrong, so it is counted rather than assumed. def _overloads(): n = 0 for _t in _trees.values(): for node in ast.walk(_t): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): for d in node.decorator_list: if (isinstance(d, ast.Name) and d.id == "overload") or \ (isinstance(d, ast.Attribute) and d.attr == "overload"): n += 1 return n guard("overload_count", _overloads) # THE OTHER TWO CONSTRUCTS THIS PAYLOAD IS UNUSUALLY DENSE IN, both of which are # annotation-position grammar rather than statement grammar: PEP 604 `X | Y' # unions (a `BinOp' under `BitOr' inside an annotation) and bare annotated names # with no value (`have_pyrex: Any'). Counted so a parser that folded either into # something else is a red here rather than a silent pass. def _annotated_names(): n = 0 for _t in _trees.values(): for node in ast.walk(_t): if isinstance(node, ast.AnnAssign) and node.value is None: n += 1 return n guard("annassign_no_value_count", _annotated_names) def _pep604_unions(): n = 0 for _t in _trees.values(): for node in ast.walk(_t): if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): n += 1 return n guard("pep604_union_count", _pep604_unions) # --------------------------------------------------------------- structure --- # A SAMPLE OF THE DECLARATIONS, so a truncated install is caught rather than # passing as "it parsed". An empty file parses perfectly. def _classes(tree): return [n.name for n in tree.body if isinstance(n, ast.ClassDef)] def _functions(tree): return [n.name for n in tree.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))] def _find(tree, name, kind): for n in tree.body: if isinstance(n, kind) and n.name == name: return n return None _sinit = _trees.get(ST + "/__init__.pyi") emit("st_init_parsed", yn(_sinit is not None)) if _sinit is not None: emit("st_init_classes", ",".join(_classes(_sinit))) emit("st_init_functions", ",".join(_functions(_sinit))) # `__all__' AS THE PACKAGE'S OWN STATEMENT OF ITS SURFACE, read off the tree # rather than off a name list typed here. It is the one place the stub says # which names it means to export, and it is what the re-export aliases below # have to agree with. def _dunder_all(): for n in _sinit.body: if (isinstance(n, ast.Assign) and n.targets and isinstance(n.targets[0], ast.Name) and n.targets[0].id == "__all__"): return ",".join(e.value for e in n.value.elts) return "absent" guard("st_init_all", _dunder_all) # THE RE-EXPORTS. `Distribution' and `Extension' -- the two classes the ticket # names -- are not DECLARED in `__init__.pyi'; they are imported from # `.dist' and `.extension' under the PEP 484 `as' form that makes an import a # re-export. Reading the alias is how the suite says "the entry point is # here" without pretending it is a class definition in this file. def _reexports(): out = [] for n in _sinit.body: if isinstance(n, ast.ImportFrom): for a in n.names: if a.asname == a.name: out.append("%s:%s" % (n.module, a.name)) return ",".join(out) guard("st_init_reexports", _reexports) # `setup' -- THE ENTRY POINT THE TICKET NAMES -- BY ITS DECLARED KEYWORDS. # Upstream declares it keyword-only (a bare `*' first) with a `**attrs' # catch-all, which is the shape a caller writing a setup.py actually meets, # so both halves are asserted: the parameter names as a set, and the fact # that there is not one positional among them. _setup = _find(_sinit, "setup", ast.FunctionDef) emit("setup_declared", yn(_setup is not None)) if _setup is not None: _a = _setup.args emit("setup_kwonly_params", ",".join(x.arg for x in _a.kwonlyargs)) emit("setup_kwonly_count", len(_a.kwonlyargs)) emit("setup_positional_count", len(_a.posonlyargs) + len(_a.args)) emit("setup_kwargs", _a.kwarg.arg if _a.kwarg else "absent") emit("setup_returns", ast.unparse(_setup.returns) if _setup.returns is not None else "absent") # `Command' IS DECLARED HERE and inherits distutils' own, which is the fact # that makes the command classes below a hierarchy rather than a flat set. _cmd = _find(_sinit, "Command", ast.ClassDef) guard("st_command_bases", lambda: ",".join(ast.unparse(b) for b in _cmd.bases) if _cmd else "absent") # `Extension' AND `Distribution' AS CLASSES, in the files that declare them. The # ticket asks for both by name; each is read where it is defined rather than # where it is re-exported, and its bases are asserted because inheriting # distutils' version is the whole content of these two stubs. _ext = _trees.get(ST + "/extension.pyi") emit("extension_pyi_parsed", yn(_ext is not None)) if _ext is not None: _e = _find(_ext, "Extension", ast.ClassDef) emit("extension_is_class", yn(_e is not None)) if _e is not None: emit("extension_bases", ",".join(ast.unparse(b) for b in _e.bases)) _ei = _find(_e, "__init__", ast.FunctionDef) emit("extension_init_params", ",".join(x.arg for x in (_ei.args.args + _ei.args.kwonlyargs)) if _ei else "absent") emit("extension_classes", ",".join(_classes(_ext))) _dist = _trees.get(ST + "/dist.pyi") emit("dist_pyi_parsed", yn(_dist is not None)) if _dist is not None: _d = _find(_dist, "Distribution", ast.ClassDef) emit("distribution_is_class", yn(_d is not None)) if _d is not None: emit("distribution_bases", ",".join(ast.unparse(b) for b in _d.bases)) _dm = [n.name for n in _d.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))] emit("distribution_methods", ",".join(sorted(set(_dm)))) emit("distribution_method_count", len(_dm)) # THE DISTUTILS COMMAND CLASSES, which are the bulk of what this distribution # describes and the part the ticket calls "the command classes". Counted as the # stub modules under the two `command/' directories rather than by name, so a # release adding one is a number here and not a rewritten list. emit("st_command_modules", ",".join( sorted(n.split("/", 1)[1] for n in _st if n.startswith("command/") and n.endswith(".pyi")))) emit("st_distutils_command_modules", ",".join( sorted(n.split("/", 2)[2] for n in _st if n.startswith("_distutils/command/") and n.endswith(".pyi")))) # ----------------------------------------------------- pkg_resources side --- _pinit = _trees.get(PR + "/__init__.pyi") emit("pr_init_parsed", yn(_pinit is not None)) if _pinit is not None: _pc = _classes(_pinit) _pf = _functions(_pinit) emit("pr_class_count", len(_pc)) emit("pr_function_count", len(_pf)) emit("pr_functions", ",".join(sorted(set(_pf)))) # THE API NAMES THE TICKET ASKS FOR, and the first of them corrects the # ticket's shape. `resource_filename' is an ordinary `def' at module scope. # `require' IS NOT: upstream declares it as the module-level ASSIGNMENT # `require = working_set.require', binding the bound method of the module's # own `WorkingSet' instance. So "present" is true of both names and means two # different things, and asserting `require' among the functions -- which is # what a headcount would have done -- would have been false. Measured, not # assumed. emit("pr_has_resource_filename", yn("resource_filename" in _pf)) _rf = _find(_pinit, "resource_filename", ast.FunctionDef) guard("pr_resource_filename_params", lambda: ",".join(x.arg for x in _rf.args.args) if _rf else "absent") emit("pr_require_is_function", yn("require" in _pf)) def _require_binding(): for n in _pinit.body: if (isinstance(n, ast.Assign) and n.targets and isinstance(n.targets[0], ast.Name) and n.targets[0].id == "require"): return ast.unparse(n.value) return "absent" guard("pr_require_target", _require_binding) # AND THE METHOD THAT ALIAS POINTS AT. `require' is only meaningful because # `WorkingSet.require' is declared; asserting the alias without the target # would pass on a stub that had dropped the class. def _ws_require(): ws = _find(_pinit, "WorkingSet", ast.ClassDef) if ws is None: return "absent" for n in ws.body: if isinstance(n, ast.FunctionDef) and n.name == "require": return ast.unparse(n.returns) if n.returns is not None else "no-return" return "no-method" guard("pr_workingset_require_returns", _ws_require) # THE ERROR HIERARCHY, which is the part of `pkg_resources' a caller catches # rather than calls, and the part a truncated stub would silently lose. def _bases_of(name): c = _find(_pinit, name, ast.ClassDef) if c is None: return "absent" return ",".join(ast.unparse(b) for b in c.bases) guard("pr_distribution_not_found_bases", lambda: _bases_of("DistributionNotFound")) guard("pr_version_conflict_bases", lambda: _bases_of("VersionConflict")) # ---------------------------------------------------------------- import --- # WHAT `import' ACTUALLY DOES WITH A STUBS-ONLY DIRECTORY, measured rather than # assumed -- and the answer corrects the obvious expectation. # # AS AN IMPORT STATEMENT the stubs cannot be reached, and for a reason that has # nothing to do with this image: `setuptools-stubs' is not an identifier, so # `import setuptools-stubs' is a SyntaxError in any CPython. # # BUT `importlib.import_module("setuptools-stubs")' SUCCEEDS, on this arm and on # the control both. PEP 420 makes any directory on sys.path a NAMESPACE PACKAGE # portion whatever it is called, so the name resolves -- to a module with no # `__file__', no loader-backed source, and an entirely empty public surface. # Nothing in the payload is reachable through it, because `.pyi' is not a source # suffix, so every submodule import fails. # # So the accurate assertion is not "it does not import" but "it imports EMPTY and # reaches nothing", and that is what the keys below carry. def _stmt_syntaxerror(name): def f(): try: compile("import %s" % name, "", "exec") return "no" except SyntaxError: return "yes" except Exception as e: return "RAISED:%s" % type(e).__name__ return f guard("st_import_stmt_syntaxerror", _stmt_syntaxerror(ST)) guard("pr_import_stmt_syntaxerror", _stmt_syntaxerror(PR)) for _tag, _name in (("st", ST), ("pr", PR)): try: _m = importlib.import_module(_name) emit("%s_import_module_ok" % _tag, "yes") emit("%s_import_module_file" % _tag, str(getattr(_m, "__file__", None))) emit("%s_import_module_origin" % _tag, str(_m.__spec__.origin)) emit("%s_import_module_loader" % _tag, type(_m.__loader__).__name__) emit("%s_import_module_is_namespace" % _tag, yn(_m.__spec__.origin is None and _m.__spec__.submodule_search_locations is not None)) emit("%s_import_module_surface" % _tag, ",".join(sorted(n for n in dir(_m) if not n.startswith("_")))) except Exception as e: emit("%s_import_module_ok" % _tag, "no") emit("%s_import_module_error" % _tag, type(e).__name__) # NO SUBMODULE IS REACHABLE THROUGH EITHER. This is the half that makes the # namespace packages harmless: the directories answer to their names and deliver # nothing. `setuptools-stubs.command' is the interesting one -- it is a real # subdirectory, so it resolves as a nested namespace package, and still no `.pyi' # inside it imports. _sub_ok = [] for _s in ("dist", "extension", "command", "command.build_ext", "config", "__init__"): try: importlib.import_module("%s.%s" % (ST, _s)) _sub_ok.append(_s) except Exception: pass emit("st_importable_submodules", ",".join(_sub_ok)) _psub_ok = [] for _s in ("_vendored_packaging", "_vendored_packaging.version", "__init__"): try: importlib.import_module("%s.%s" % (PR, _s)) _psub_ok.append(_s) except Exception: pass emit("pr_importable_submodules", ",".join(_psub_ok)) # A SUBMODULE THAT RESOLVES IS STILL EMPTY. `setuptools-stubs.command' is a # directory, so PEP 420 gives it a spec; what matters is that it too carries no # declaration. Asserted apart from the list above so "it resolved" and "it # delivered something" stay two different findings. def _sub_surface(): m = importlib.import_module("%s.command" % ST) return ",".join(sorted(n for n in dir(m) if not n.startswith("_"))) guard("st_command_namespace_surface", _sub_surface) # `find_spec' OVER A `.pyi' NAME, which is the mechanism underneath the lines # above: `.pyi' is not in `importlib.machinery.SOURCE_SUFFIXES', so the finder # never offers one of these files however the name is spelled. emit("pyi_in_source_suffixes", yn(".pyi" in importlib.machinery.SOURCE_SUFFIXES)) # ------------------------------------------------------- no interference --- # THE STUBS DO NOT SHADOW THE REAL PACKAGES. `python3-setuptools' is on this image # (BRINTOS-2237) and installs BOTH `setuptools' and `pkg_resources' into this same # site-packages, so this port puts annotations for two libraries beside the two # libraries. This is the one way a stubs package can break an unrelated part of # the image, and it is measured by the RESOLVED `__file__' rather than by the # import merely succeeding. for _tag, _mod, _stubdir in (("setuptools", "setuptools", SD), ("pkg_resources", "pkg_resources", PD)): try: _r = importlib.import_module(_mod) emit("%s_import_ok" % _tag, "yes") emit("%s_file" % _tag, str(getattr(_r, "__file__", None))) emit("%s_from_stubs_dir" % _tag, yn(str(getattr(_r, "__file__", "")).startswith(_stubdir))) except Exception as e: emit("%s_import_ok" % _tag, "no") emit("%s_import_error" % _tag, type(e).__name__) # AND THE REAL `setuptools' STILL ANSWERS. Resolving to the right file says the # stubs did not displace it; reading one attribute through it says the module # actually initialised, which is the half a broken sys.path would not. def _setuptools_has_setup(): import setuptools as _s return yn(callable(getattr(_s, "setup", None))) guard("setuptools_setup_callable", _setuptools_has_setup) # ------------------------------------------------------------- dist-info --- _dnames = sorted(os.listdir(DD)) if os.path.isdir(DD) else [] emit("dist_info_file_set", ",".join(_dnames)) def _meta_line(prefix): for line in _read(os.path.join(DD, "METADATA")).decode("utf-8").splitlines(): if line.startswith(prefix): return line[len(prefix):].strip() return "absent" guard("meta_name", lambda: _meta_line("Name:")) guard("meta_version", lambda: _meta_line("Version:")) guard("meta_license", lambda: _meta_line("License:")) guard("meta_requires_python", lambda: _meta_line("Requires-Python:")) # NO RUNTIME DEPENDENCY IS DECLARED, and specifically not on `setuptools' itself # -- which is the fact that lets this package be installed on an image that does # not carry the library it describes, and what makes the recipe's `Depends: # python3-core' correct rather than thin. A `Requires-Dist:' line appearing here # is a release changing its mind. def _requires_dist(): out = [l for l in _read(os.path.join(DD, "METADATA")).decode("utf-8").splitlines() if l.startswith("Requires-Dist:")] return ",".join(out) if out else "none" guard("meta_requires_dist", _requires_dist) # `top_level.txt' NAMES BOTH PACKAGES, one per line, and the hyphens in both are # load-bearing: `setup.py' declares `packages=["setuptools-stubs", # "pkg_resources-stubs"]', names that are deliberately NOT valid python # identifiers, which is the single fact `*_import_stmt_syntaxerror' above is # about. Flattened to `|' so it stays one counted result. guard("dist_top_level", lambda: flat(_read(os.path.join(DD, "top_level.txt")).decode("utf-8").strip())) # A PURE-PYTHON WHEEL, from the wheel's own metadata. It is the packaging-side # statement of `so_files' above: `Root-Is-Purelib: true' means nothing in it is # platform-specific, which for a wasm32 image is the difference between a package # that ports and one that cannot. guard("wheel_root_is_purelib", lambda: [l.split(":", 1)[1].strip() for l in _read(os.path.join(DD, "WHEEL")).decode("utf-8").splitlines() if l.startswith("Root-Is-Purelib:")][0]) # `importlib.metadata' MUST ANSWER FOR IT. The dist-info being on disk and the # interpreter being able to FIND it are two different facts, and only the second # is what a caller asking "is types-setuptools installed" actually uses. The file # count is RECORD's own read back through the metadata API. def _im_version(): import importlib.metadata as md return md.version("types-setuptools") guard("importlib_metadata_version", _im_version) def _im_files(): import importlib.metadata as md fs = md.files("types-setuptools") or [] return str(len(fs)) guard("importlib_metadata_file_count", _im_files) emit("probe_done", "yes") emit("emitted_count", len(_emitted) + 1) sys.exit(0)