# Probe for 99-python3-sphinxcontrib-qthelp.test -- ONE interpreter start for # the whole suite. # # Every value is emitted as `key=value' on its own line. The .test compares with # shell `test x = y', so any value that could carry a newline or a run of spaces # is escaped reversibly here: a newline becomes the two characters \n and a # space becomes \s. Nothing else is transformed, so a red prints the whole value # on one readable line. # # =========================================================================== # THIS PROBE STANDS A STUB `sphinx' UP BEFORE IT IMPORTS ANYTHING, AND THAT IS # THE CENTRAL FACT ABOUT THIS PORT. # =========================================================================== # # sphinxcontrib/qthelp/__init__.py imports eight names out of `sphinx' at MODULE # scope -- addnodes, Sphinx, StandaloneHTMLBuilder, IndexEntries, # get_translation, logging, NodeMatcher, canon_path/make_filename and # SphinxRenderer -- and this image does not install Sphinx. python3-sphinx # exists in poky (recipes-devtools/python/python3-sphinx_7.2.6.bb) and is in # neither core-image-brintos-emcraft nor any port ticket. So on this rootfs # # python3 -c 'import sphinxcontrib.qthelp' # # raises ModuleNotFoundError: No module named 'sphinx', and no measurement of # this package is possible without standing something in for its consumer. The # .test asserts that failure directly, as a case, so the fact is recorded rather # than worked around silently; this probe then supplies the stub and measures # the package's contract with Sphinx against it. # # WHAT IS NOT STUBBED: docutils. python3-docutils IS on this image (0.20.1), and # the node classes qthelp's tree walk is written against -- bullet_list, # list_item, reference -- are the real ones. `compact_paragraph' is the single # node type Sphinx adds, and the stub defines it exactly as Sphinx does, as a # nodes.paragraph subclass. So the toc walk below runs over a REAL docutils tree # and exercises the real isdocnode()/write_toc() recursion; only the names that # live in Sphinx itself are supplied. # # THE ONE PIECE OF STUB THAT DOES REAL WORK, AND WHY IT IS FENCED OFF. # `render_file' goes through sphinx.util.template.SphinxRenderer, which is # Jinja2, and neither Sphinx nor Jinja2 is on this image. The stub therefore # carries a ~40-line renderer covering exactly the constructs the two shipped # templates use: `{{ x }}', `{{ x|e }}', and one `{%- for %}/{%- endfor %}' with # `-' whitespace trimming. That renderer is the probe's, not the package's, so # the XML it produces is NOT offered as the primary evidence: `qhp_ctx' and # `qhcp_ctx' below emit the context dict the PACKAGE hands the template engine, # which is pure package output and independent of any renderer, and the rendered # XML is asserted beside it to pin the shipped templates' own text. A red in the # context cases means the package changed; a red in the XML cases alone means a # template changed. import hashlib import html as _html import os import re import sys import tempfile import types OUT = [] def emit(k, v): OUT.append("%s=%s" % (k, v)) def esc(s): return str(s).replace("\\", "\\\\").replace("\n", "\\n").replace(" ", "\\s") def guard(key, fn): """Run fn and emit its value, or emit the exception type. A probe that dies at the first surprise leaves every later case unscorable; one that records the surprise and continues leaves exactly one red beside the evidence.""" try: emit(key, fn()) except Exception as exc: # noqa: BLE001 -- the type IS the measurement emit(key, "EXC:%s" % type(exc).__name__) emit(key + "_detail", esc(str(exc))[:200]) # ------------------------------------------------ the import, twice over --- # FIRST, WITHOUT THE STUB, so the probe measures the same failure the .test # asserts from the shell rather than taking it on trust. This must be done # before the stub is installed and before any import of the package, because a # successful import is cached in sys.modules and cannot be undone cleanly. def _bare_import(): try: import sphinxcontrib.qthelp # noqa: F401 return "imported" except ModuleNotFoundError as exc: return "ModuleNotFoundError:%s" % exc.name except Exception as exc: # noqa: BLE001 return "EXC:%s" % type(exc).__name__ emit("bare_import", _bare_import()) # Whatever the line above left behind. A failed `import sphinxcontrib.qthelp' # still binds the NAMESPACE package `sphinxcontrib' -- the failure is in the # submodule -- so the submodule, and only the submodule, must be gone for the # stubbed import below to actually run the module body. for _n in [n for n in sys.modules if n.startswith("sphinxcontrib.")]: del sys.modules[_n] # DOCUTILS IS REAL, AND IS IMPORTED BEFORE THE STUB so a failure here is # distinguishable from a failure in the stub. from docutils import nodes # noqa: E402 emit("docutils_version", __import__("docutils").__version__) # ------------------------------------------------------------- the stub --- def _mod(name): """Register an empty module under `name' and bind it on its parent, so both `import a.b' and `from a import b' resolve. The parent binding is the half that is easy to forget: `from sphinx.util import logging' is an attribute lookup on an already-imported package before it is ever an import.""" m = types.ModuleType(name) sys.modules[name] = m if "." in name: parent, leaf = name.rsplit(".", 1) setattr(sys.modules[parent], leaf, m) return m _sphinx = types.ModuleType("sphinx") _sphinx.__path__ = [] _sphinx.version_info = (7, 2, 6, "final", 0) _sphinx.__version__ = "7.2.6" sys.modules["sphinx"] = _sphinx _addnodes = _mod("sphinx.addnodes") class compact_paragraph(nodes.paragraph): """Sphinx's own definition, verbatim: a paragraph subclass carrying the `toctree' attribute the builder matches on.""" _addnodes.compact_paragraph = compact_paragraph _app_mod = _mod("sphinx.application") class Sphinx(object): """Imported for the setup() annotation only; never instantiated by the package.""" _app_mod.Sphinx = Sphinx _mod("sphinx.builders") _html_mod = _mod("sphinx.builders.html") class StandaloneHTMLBuilder(object): """The base class QtHelpBuilder extends. Only the attributes and methods qthelp's own code reaches through `self' are supplied: everything else in the real class is HTML writing, which this package overrides or ignores.""" name = "html" format = "html" epilog = "" copysource = True add_permalinks = True embedded = False download_support = True search = True imagedir = "_images" out_suffix = ".html" link_suffix = ".html" supported_image_types = [] def init(self): pass def get_target_uri(self, docname, typ=None): return docname + self.link_suffix _html_mod.StandaloneHTMLBuilder = StandaloneHTMLBuilder _mod("sphinx.environment") _mod("sphinx.environment.adapters") _idx_mod = _mod("sphinx.environment.adapters.indexentries") class IndexEntries(object): """Sphinx builds this from the environment's index domain. The fixture environment below carries the index it should return, so what is measured is build_keywords()/keyword_item() over a KNOWN entry set.""" def __init__(self, env): self.env = env def create_index(self, builder, group_entries=True, _fixed_index=None): return self.env.probe_index _idx_mod.IndexEntries = IndexEntries _locale_mod = _mod("sphinx.locale") def get_translation(package, catalog="general"): _locale_mod.probe_calls.append((package, catalog)) return lambda message, *args: message _locale_mod.probe_calls = [] _locale_mod.get_translation = get_translation _mod("sphinx.util") _log_mod = _mod("sphinx.util.logging") class _Logger(object): def __init__(self, name): self.name = name self.messages = [] def info(self, msg, *args, **kw): self.messages.append(msg) _log_mod.probe_loggers = {} def getLogger(name): # noqa: N802 -- Sphinx's spelling _log_mod.probe_loggers.setdefault(name, _Logger(name)) return _log_mod.probe_loggers[name] _log_mod.getLogger = getLogger _nodes_mod = _mod("sphinx.util.nodes") class NodeMatcher(object): """Sphinx's matcher, reproduced: a callable that says whether a node is an instance of one of the given classes AND carries the given attributes. The builder uses it as `NodeMatcher(compact_paragraph, toctree=True)', so a compact_paragraph that is NOT a toctree must not match -- the fixture tree below carries one of each so the discrimination is measured, not assumed.""" def __init__(self, *node_classes, **attrs): self.classes = node_classes self.attrs = attrs def match(self, node): try: if self.classes and not isinstance(node, self.classes): return False if self.attrs: if not isinstance(node, nodes.Element): return False for key, value in self.attrs.items(): if key not in node: return False if node.get(key) != value: return False return True except Exception: # noqa: BLE001 return False def __call__(self, node): return self.match(node) _nodes_mod.NodeMatcher = NodeMatcher _osutil_mod = _mod("sphinx.util.osutil") def canon_path(native_path): return str(native_path).replace(os.path.sep, "/") def make_filename(string): return re.sub(r"[^a-zA-Z0-9_-]", "", string) or "sphinx" _osutil_mod.canon_path = canon_path _osutil_mod.make_filename = make_filename _tpl_mod = _mod("sphinx.util.template") _VAR = re.compile(r"\{\{\s*(\w+)\s*(\|\s*e\s*)?\}\}") _FOR = re.compile( r"\{%-?\s*for\s+(\w+)\s+in\s+(\w+)\s*-?%\}(.*?)\{%-?\s*endfor\s*-?%\}", re.S, ) def _jinja_escape(value): """Jinja2's `|e'. Note `'` -> ' and `"` -> ", which is Markup.escape and NOT html.escape's default -- the two differ on the quote entities, and the .qhp files this writes are compared byte-for-byte downstream.""" return ( str(value) .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("'", "'") .replace('"', """) ) def _render_str(text, ctx): def _sub(m): val = ctx.get(m.group(1), "") return _jinja_escape(val) if m.group(2) else str(val) def _loop(m): var, seq, body = m.group(1), m.group(2), m.group(3) raw = m.group(0) out = [] for item in ctx.get(seq, []) or []: inner = dict(ctx) inner[var] = item piece = _VAR.sub(lambda mm: _sub_ctx(mm, inner), body) if raw.startswith("{%-"): piece = re.sub(r"\A\s*\n", "\n", piece) out.append(piece) joined = "".join(out) if raw.endswith("-%}") or "{%- endfor" in raw: joined = re.sub(r"[ \t]+\Z", "", joined) return joined def _sub_ctx(m, c): val = c.get(m.group(1), "") return _jinja_escape(val) if m.group(2) else str(val) # `{%-' trims the whitespace that precedes the tag, newline included. text = re.sub(r"[ \t]*\n?[ \t]*(?=\{%-)", "", text) text = _FOR.sub(_loop, text) return _VAR.sub(_sub, text) class SphinxRenderer(object): """Jinja2 stands in for Jinja2. See the module header: this is the probe's renderer, the package's own output is `*_ctx' below, and the two are asserted separately so a red says which of them moved.""" probe_calls = [] @classmethod def render_from_file(cls, filename, context): cls.probe_calls.append((os.path.basename(filename), dict(context))) with open(filename, "r", encoding="utf-8") as fh: return _render_str(fh.read(), context) _tpl_mod.SphinxRenderer = SphinxRenderer # ------------------------------------------------------- the package ------ import sphinxcontrib # noqa: E402 import sphinxcontrib.qthelp as Q # noqa: E402 emit("version", Q.__version__) emit("version_info", ".".join(str(n) for n in Q.__version_info__)) emit("module_file", Q.__file__) emit("package_dir", Q.package_dir) # ------------------------------------------------- the namespace package --- emit("ns_file", getattr(sphinxcontrib, "__file__", None)) emit("ns_path_type", type(sphinxcontrib.__path__).__name__) emit("ns_path", ",".join(sphinxcontrib.__path__)) # ------------------------------------------------------ the metadata plane -- from importlib import metadata as _md # noqa: E402 def _dist(): return _md.distribution("sphinxcontrib-qthelp") guard("dist_version", lambda: _dist().version) guard("dist_name", lambda: _dist().metadata["Name"]) # Every Requires-Dist upstream declares is guarded by an extra -- `dependencies # = []' in pyproject.toml -- so the package has NO unconditional runtime # dependency and the .deb's `Depends: python3-core' does NOT contradict it. That # is the opposite of sphinxcontrib-jquery (BRINTOS-2106), whose METADATA carries # a bare `Requires-Dist: Sphinx>=1.8'; the difference is why this port needs no # bbappend and no recorded disagreement. guard("dist_requires", lambda: ",".join(_dist().metadata.get_all("Requires-Dist") or [])) guard("dist_requires_hard", lambda: ",".join(r for r in (_dist().metadata.get_all("Requires-Dist") or []) if "extra ==" not in r)) guard("dist_extras", lambda: ",".join(sorted(_dist().metadata.get_all("Provides-Extra") or []))) guard("dist_requires_python", lambda: _dist().metadata["Requires-Python"]) guard("ep_count", lambda: len(list(_dist().entry_points))) # ------------------------------------------------------------------ the API -- emit("api_surface", ",".join(sorted(n for n in dir(Q) if not n.startswith("_")))) emit("api_callables", ",".join(sorted( n for n in dir(Q) if not n.startswith("_") and callable(getattr(Q, n))))) # ------------------------------------------------------- the builder class -- B = Q.QtHelpBuilder emit("builder_name", B.name) emit("builder_base", B.__mro__[1].__name__) emit("builder_copysource", B.copysource) emit("builder_add_permalinks", B.add_permalinks) emit("builder_embedded", B.embedded) emit("builder_download_support", B.download_support) emit("builder_search", B.search) emit("builder_image_types", ",".join(B.supported_image_types)) emit("builder_epilog_head", esc(B.epilog.splitlines()[0])) # ------------------------------------------------------------- setup() ----- class _App(object): """A stub Sphinx application. It records what the extension registers and nothing else -- an extension's whole contract is the set of calls it makes here, and a richer stub would risk measuring the stub.""" def __init__(self): self.required = [] self.extensions = [] self.builders = [] self.config_values = [] self.catalogs = [] def require_sphinx(self, version): self.required.append(version) def setup_extension(self, name): self.extensions.append(name) def add_builder(self, builder): self.builders.append(builder) def add_config_value(self, name, default, rebuild, types=()): self.config_values.append((name, default, rebuild, types)) def add_message_catalog(self, name, locale_dir): self.catalogs.append((name, locale_dir)) _app = _App() _meta = Q.setup(_app) emit("setup_keys", ",".join(sorted(_meta))) emit("setup_version", _meta["version"]) emit("setup_parallel_read", _meta["parallel_read_safe"]) emit("setup_parallel_write", _meta["parallel_write_safe"]) emit("setup_require_sphinx", ",".join(_app.required)) emit("setup_extensions", ",".join(_app.extensions)) emit("setup_builders", ",".join(b.__name__ for b in _app.builders)) emit("setup_builder_is_qthelp", _app.builders == [Q.QtHelpBuilder]) emit("setup_config_names", ",".join(n for n, _, _, _ in _app.config_values)) emit("setup_config_rebuild", ",".join(r for _, _, r, _ in _app.config_values)) emit("setup_config_types", ",".join( "%s:%s" % (n, ",".join(getattr(t, "__name__", str(t)) for t in (ts or ()))) for n, _, _, ts in _app.config_values)) emit("setup_catalog_name", ",".join(n for n, _ in _app.catalogs)) emit("setup_catalog_dir", ",".join(d for _, d in _app.catalogs)) emit("setup_catalog_is_locales", _app.catalogs[0][1] == os.path.join(Q.package_dir, "locales")) # The four config defaults. `qthelp_basename' alone is a CALLABLE -- Sphinx # resolves a callable default by calling it with the config object, which is how # the basename tracks `project' without the extension reading it at setup time. # The other three are constants. def _cfg_defaults(): out = [] for name, default, _, _ in _app.config_values: if callable(default): out.append("%s:callable" % name) else: out.append("%s:%r" % (name, default)) return ";".join(out) emit("setup_config_defaults", esc(_cfg_defaults())) class _ProjectOnly(object): def __init__(self, project): self.project = project emit("basename_default_of_project", [d for n, d, _, _ in _app.config_values if n == "qthelp_basename"][0]( _ProjectOnly("My Project 2.0!"))) # ------------------------------------------------------ the project writer -- # A REAL docutils tree, walked by the package's own recursion. Two shapes are # built deliberately: # # * a `document' list_item whose children are (compact_paragraph>reference, # bullet_list) -- isdocnode() says yes, and write_toc emits an OPEN #
with the nested entries inside it and a closing tag; # * a bare reference -- the self-closing
form. # # and the titles carry `&', `<', `"' and a non-ASCII character, because the two # branches escape DIFFERENTLY: both run html.escape and turn `"' into ", # but only the self-closing branch then re-encodes with xmlcharrefreplace. A # port that had collapsed the two branches would pass a single-shape fixture. def _ref(text, uri): r = nodes.reference("", "", nodes.Text(text)) r["refuri"] = uri return r def _leaf(text, uri): li = nodes.list_item() cp = compact_paragraph() cp += _ref(text, uri) li += cp return li def _doc_item(text, uri, children): li = nodes.list_item() cp = compact_paragraph() cp += _ref(text, uri) li += cp bl = nodes.bullet_list() for c in children: bl += c li += bl return li def _fixture_toc(): doc = nodes.document(None, None) toc = compact_paragraph() toc["toctree"] = True outer = nodes.bullet_list() outer += _doc_item( 'Guide & "Notes" ', "guide.html", [_leaf("Section été", "guide.html#section"), _leaf('Quoted "bit" & more', "guide.html#quoted")]) outer += _leaf("Plain page", "plain.html") toc += outer # A compact_paragraph that is NOT a toctree. The matcher must skip it; if it # did not, `Decoy' would appear in the sections. decoy = compact_paragraph() decoy["toctree"] = False decoy += _ref("Decoy", "decoy.html") doc += toc doc += decoy return doc class _Index(object): localname = "Python Module Index" class _Env(object): def __init__(self, doctree, index): self._doctree = doctree self.probe_index = index def get_and_resolve_doctree(self, docname, builder, prune_toctrees=True): return self._doctree class _Config(object): master_doc = "index" project = "Demo Project" version = "2.1" html_title = 'Demo Project & "Docs"' html_short_title = "Demo" qthelp_basename = "DemoProject" qthelp_namespace = None qthelp_theme = "nonav" qthelp_theme_options = {} # The index the stub IndexEntries hands back. Three shapes, each one a distinct # branch of build_keywords()/keyword_item(): # # * one ref and a name that MATCHES _idpattern -> a keyword carrying an `id' # built from the parenthesised module and the shortened title; # * one ref and a name that does not -> no `id' attribute; # * two refs -> the same title emitted twice, once per ref; # * a subitem -> the recursion, one level down. # # and one entry carries `&' and `"' in both name and ref, because keyword_item # escapes with quote=True on each. _INDEX = [ ("D", [ ("dump() (class in demo.core)", ([("", "api.html#demo.core.dump")], [], None)), ("data & \"stuff\"", ([("", "api.html#d&q")], [], None)), ]), ("M", [ ("multi", ([("", "a.html#m"), ("", "b.html#m")], [], None)), ("parent", ([("", "p.html#parent")], [("child", [("", "p.html#child")])], None)), ]), ] _OUTDIR = tempfile.mkdtemp(prefix="bst-qthelp-b.") def _builder(): b = Q.QtHelpBuilder.__new__(Q.QtHelpBuilder) b.env = _Env(_fixture_toc(), _INDEX) b.config = _Config() b.domain_indices = [("py-modindex", _Index, {}, True)] b.out_suffix = ".html" b.link_suffix = ".html" b.imagedir = "_images" b.outdir = _OUTDIR return b _B = _builder() # THE TOC, as the package renders it: the matcher picks the toctree # compact_paragraph out of the fixture document and write_toc() recurses into it. def _sections(): matcher = NodeMatcher(compact_paragraph, toctree=True) parts = [] for node in _B.env._doctree.traverse(matcher): parts.extend(_B.write_toc(node)) return "\n".join(parts) guard("toc_sections", lambda: esc(_sections())) guard("toc_matched", lambda: len(list(_B.env._doctree.traverse( NodeMatcher(compact_paragraph, toctree=True))))) guard("toc_decoy_absent", lambda: "Decoy" not in _sections()) # The keyword writer, over the fixture index above. guard("keywords", lambda: esc("\n".join( kw for key, group in _INDEX for title, (refs, subitems, _k) in group for kw in _B.build_keywords(title, refs, subitems)))) guard("keyword_id_form", lambda: _B.keyword_item( "dump() (class in demo.core)", ("", "api.html#demo.core.dump")).strip()) guard("keyword_plain_form", lambda: _B.keyword_item( "plain name", ("", "p.html#plain")).strip()) guard("keyword_escaped", lambda: esc(_B.keyword_item( 'a & "b"', ("", "x.html#a&b")).strip())) # ------------------------------------------------ the namespace, normalised -- # The one piece of arithmetic in this package that a reader is likely to get # wrong, and the one the documentation states: a QtHelp namespace may carry no # character outside [a-zA-Z0-9.-], no run of dots, no leading or trailing dot, # and is lowercased. The default is built from the outname and the version when # `qthelp_namespace' is unset. def _nspace(explicit, outname="DemoProject", version="2.1"): cfg = _Config() cfg.qthelp_namespace = explicit cfg.version = version b = _builder() b.config = cfg d = tempfile.mkdtemp(prefix="bst-qthelp-ns.") b.build_qhp(d, outname) with open(os.path.join(d, outname + ".qhp"), encoding="utf-8") as fh: body = fh.read() m = re.search(r"(.*?)", body) return m.group(1) guard("nspace_default", lambda: _nspace(None)) guard("nspace_explicit", lambda: _nspace("org.example.demo")) guard("nspace_stripped", lambda: _nspace("Org..Example_Demo!! 2.1..")) guard("nspace_default_odd_version", lambda: _nspace(None, "Demo", "1.0-rc1+x")) # ------------------------------------------------------- the project files -- # get_project_files() walks the output directory and keeps every .html anywhere, # plus everything under _static/ and _images/ EXCEPT .js. The fixture carries one # of each decision. def _files_fixture(): d = tempfile.mkdtemp(prefix="bst-qthelp-files.") for rel in ("index.html", "other.txt", "sub/page.html", "sub/notes.txt", "_static/style.css", "_static/app.js", "_static/frame.html", "_images/plot.png"): p = os.path.join(d, rel) os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "w", encoding="utf-8") as fh: fh.write("x") return d _FD = _files_fixture() guard("project_files", lambda: ",".join(sorted(_B.get_project_files(_FD)))) # --------------------------------------------------- the emitted documents -- # ONE build_qhp() over the fixture, into a scratch directory, and everything # below reads its two outputs. `*_ctx' is the context the package handed the # template engine -- pure package output, no renderer involved -- and the XML is # the shipped template's own text with that context substituted. _OD = tempfile.mkdtemp(prefix="bst-qthelp-out.") for _rel in ("index.html", "_static/style.css"): _p = os.path.join(_OD, _rel) os.makedirs(os.path.dirname(_p), exist_ok=True) with open(_p, "w", encoding="utf-8") as _fh: _fh.write("x") SphinxRenderer.probe_calls = [] _BQ = _builder() guard("build_qhp_ran", lambda: (_BQ.build_qhp(_OD, "DemoProject"), "yes")[1]) emit("render_files", ",".join(n for n, _ in SphinxRenderer.probe_calls)) def _ctx(basename): for name, ctx in SphinxRenderer.probe_calls: if name == basename: return ctx return {} guard("qhp_ctx_keys", lambda: ",".join(sorted(_ctx("project.qhp")))) guard("qhp_ctx_scalars", lambda: esc(";".join( "%s=%s" % (k, _ctx("project.qhp")[k]) for k in ("outname", "title", "version", "project", "namespace", "master_doc")))) guard("qhp_ctx_files", lambda: ",".join(_ctx("project.qhp")["files"])) guard("qhcp_ctx_keys", lambda: ",".join(sorted(_ctx("project.qhcp")))) guard("qhcp_ctx_scalars", lambda: esc(";".join( "%s=%s" % (k, _ctx("project.qhcp")[k]) for k in ("outname", "title", "homepage", "startpage")))) guard("qhp_written", lambda: os.path.isfile(os.path.join(_OD, "DemoProject.qhp"))) guard("qhcp_written", lambda: os.path.isfile(os.path.join(_OD, "DemoProject.qhcp"))) def _read(name): with open(os.path.join(_OD, name), encoding="utf-8") as fh: return fh.read() guard("qhp_sha256", lambda: hashlib.sha256(_read("DemoProject.qhp").encode()).hexdigest()) guard("qhcp_xml", lambda: esc(_read("DemoProject.qhcp"))) guard("qhp_head", lambda: esc("\n".join(_read("DemoProject.qhp").splitlines()[:8]))) guard("qhp_files_block", lambda: esc("\n".join( l for l in _read("DemoProject.qhp").splitlines() if "" in l or "" in l or "" in l))) guard("qhp_virtualfolder", lambda: "doc" in _read("DemoProject.qhp")) guard("qhp_lines", lambda: len(_read("DemoProject.qhp").splitlines())) # The epilog handle_finish() leaves behind, which is what Sphinx prints to the # user at the end of a qthelp build: the basename is substituted in, the outdir # placeholder deliberately is not. _BF = _builder() guard("finish_epilog", lambda: ( _BF.handle_finish(), esc(_BF.epilog.splitlines()[1]))[1]) # ------------------------------------------------------ the wheel's RECORD -- # EVERY PAYLOAD FILE RE-HASHED ON TARGET AND SCORED AGAINST THE WHEEL'S OWN # RECORD -- a sha256 and a byte count per row. RECORD's own row carries no # digest (a file cannot hash itself) and is skipped by construction rather than # by name. This is the end-to-end statement that what OE packaged, dpkg # installed and virtiofs served is byte-for-byte what upstream shipped. import base64 # noqa: E402 import csv # noqa: E402 SITE = os.path.dirname(os.path.dirname(Q.package_dir)) DI = os.path.join(SITE, "sphinxcontrib_qthelp-1.0.7.dist-info") def _b64(digest): return base64.urlsafe_b64encode(digest).rstrip(b"=").decode() _rec = {"rows": 0, "checked": 0, "missing": [], "bad_digest": [], "bad_size": []} def _audit(): with open(os.path.join(DI, "RECORD"), newline="", encoding="utf-8") as fh: for row in csv.reader(fh): if len(row) != 3 or not row[1]: continue rel, want, size = row _rec["rows"] += 1 p = os.path.join(SITE, rel) if not os.path.isfile(p): _rec["missing"].append(rel) continue with open(p, "rb") as f: data = f.read() _rec["checked"] += 1 if want != "sha256=" + _b64(hashlib.sha256(data).digest()): _rec["bad_digest"].append(rel) if int(size) != len(data): _rec["bad_size"].append(rel) return "ok" guard("record_audit", _audit) emit("record_rows", _rec["rows"]) emit("record_checked", _rec["checked"]) emit("record_missing", ",".join(sorted(_rec["missing"]))) emit("record_bad_digest", ",".join(sorted(_rec["bad_digest"]))) emit("record_bad_size", ",".join(sorted(_rec["bad_size"]))) # The message catalogues, counted. 48 languages, each a .mo beside its .po -- # `sphinxcontrib.qthelp.mo' is what add_message_catalog() names, so a language # that lost its .mo would lose its translations silently. LOC = os.path.join(Q.package_dir, "locales") guard("locale_langs", lambda: len([d for d in os.listdir(LOC) if os.path.isdir(os.path.join(LOC, d)) and d != ".tx"])) guard("locale_mo", lambda: sum( 1 for root, _d, fs in os.walk(LOC) for f in fs if f.endswith(".mo"))) guard("locale_po", lambda: sum( 1 for root, _d, fs in os.walk(LOC) for f in fs if f.endswith(".po"))) guard("locale_mo_named", lambda: ",".join(sorted({ f for root, _d, fs in os.walk(LOC) for f in fs if f.endswith(".mo")}))) # ------------------------------------------------------------ purity ------ # The interpreter's own view of the header's claim: nothing this package pulled # in came from a compiled extension. `sphinx' is excluded because it is this # probe's stub, not code the package shipped. def _all_pure(): for name, mod in list(sys.modules.items()): f = getattr(mod, "__file__", None) if not f: continue if name == "sphinxcontrib" or name.startswith("sphinxcontrib."): if not f.endswith((".py", ".pyc")): return "no:%s" % name return "yes" emit("all_pure_python", _all_pure()) sys.stdout.write("\n".join(OUT) + "\n")