# Generated by tools/gen_stubs.py from the package source. Do not edit. # `make stubs` regenerates; test/test_stubs.py refuses any difference. """Turn a Python codebase into `code`-module facts. The `code` reasoning module supplies the reasoning. This supplies the facts. It reads your source and writes a DKE Python program that records what it found — functions, classes, calls, imports — which you then submit to your store with the `compile` (or `run`) tool you already have: dke-ingest-python ./src > facts.dpy # then submit facts.dpy through your DKE client Nothing here talks to the network, and nothing here needs your key. The output is a program you can read before you run it: every fact this tool would record is a line you can see. Parsing is CPython's own `ast`, so what counts as a function here is what counts as a function to Python. No third-party parser, nothing vendored. This file is the PYTHON READER. Everything downstream of reading — subject ids, the row tables, the header, the CLI — is language-neutral and lives in `dke_ingest_core`, so a second language is a walker rather than a second copy of the emitter. ON CALL RESOLUTION — read this before trusting `uncalled()`. Python resolves calls at run time. A tool reading source can only resolve what the source makes plain, so the call graph this produces is a LOWER BOUND: every edge in it is real, and edges are missing. Calls through a variable, a dict of handlers, `getattr`, a decorator, or any dynamic dispatch are not visible here and are NOT recorded. That matters most for `uncalled()`, which reports functions with no recorded caller. An unresolved call makes its callee look uncalled. The tool prints its resolution rate to stderr and writes it into the generated program's header; treat `uncalled()` as a list of candidates to look at, never as a list of dead code to delete.""" import ast import io import re import sys import tokenize from pathlib import Path, PurePosixPath from dke.ingest import core from dke.ingest.core import MODULE, Call, Dependency, Facts, Function, Ids, Import, Klass, Language, UnresolvedCall, reached_by_tests, sanitize BRANCH_NODES: ... LOOP_NODES: ... def match_tests(node: ast.Match) -> int: """How many decisions a `match` makes: one per arm that TESTS. A bare `case _:` is the catch-all — control lands there when no arm above it matched, which is not a further decision, exactly as `default:` is not one in a `switch`. `case _ if cond:` DOES test, because the guard can fail. This counted 1 per `match` regardless of arm count until the rule was published. A `match` with ten arms has ten paths, so the old number was wrong on its own terms and not merely a different convention.""" ... def count_shape(fn: ast.AST) -> tuple[int, int]: """(branches, loops) within a function, not counting nested functions. A nested `def` is its own Function with its own counts, so descending into it would charge its complexity to the parent as well.""" ... def is_test_file(path: str) -> bool: """A file pytest/unittest would collect, by name or by directory.""" ... def is_test_func(name: str, class_stack: list[str]) -> bool: """A test callable: `test_*` / `test`, or any method of a `Test*` class.""" ... class ModuleScanner(ast.NodeVisitor): """One file. Collects definitions, then resolves that file's calls.""" def __init__(self, module: str, path: str, facts: Facts) -> None: ... def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: ... visit_AsyncFunctionDef = visit_FunctionDef def visit_ClassDef(self, node: ast.ClassDef) -> None: ... def visit_Assign(self, node: ast.Assign) -> None: ... def visit_Import(self, node: ast.Import) -> None: ... def visit_ImportFrom(self, node: ast.ImportFrom) -> None: ... def visit_Call(self, node: ast.Call) -> None: ... def resolve(self, defined: set[str]) -> None: """Turn this file's call sites into edges, keeping only what resolves. `defined` is every function qualified name across the whole ingest, so a call into another ingested file resolves. A call to something outside the ingested set — a stdlib call, a third-party call, a method reached dynamically — does not resolve and is NOT recorded. An unrecorded edge is a known gap; a guessed edge would be a wrong answer, and the queries cannot tell a guess from a fact.""" ... REQ_LINE: ... def parse_requirement(line: str) -> tuple[str, str, bool] | None: ... def read_dependencies(roots: list[Path], facts: Facts) -> None: """Read the project's DECLARED dependencies. Offline, always. Every source here is a file in the tree. A release date is NOT among the facts recorded, and that is deliberate: Python's core metadata carries no release date (checked — Name, Version, Requires-Dist and friends, nothing temporal), so an age could only come from asking an index. That would send the reader's dependency list to a third party, which is a different tool from the one documented as needing neither your key nor the network. `direct` is true for everything read here, because everything read here was declared by the project. Transitive dependencies live in lock files and are a separate reader; when one lands, its entries carry no `direct`.""" ... def read_pyproject_requirements(path: Path) -> list[str] | None: """`[project] dependencies` + optional-dependencies, or None if unreadable. tomllib is stdlib from 3.11. On an older interpreter this returns None and the tool still reads requirements files — a reduced answer, not a crash.""" ... def read_lockfiles(base: Path, declared: set[str], facts: Facts) -> None: """Add the TRANSITIVE set from a lock file, when the tree has one. A lock file lists everything that will be installed — what the project declared AND what those pulled in — and does not itself say which is which. So `declared` decides: a lock entry the project did not declare is transitive, and is recorded with NO `direct` cell rather than `direct = False`. Absence is the honest encoding, and it is what the `code` schema's reasoning already expects of a dependency nobody asked for directly. A version from a lock IS exact — that is what a lock is for — so unlike a declared range it is recorded as a fact, and the entry is marked pinned. Offline, like everything else here: a lock file is a file in the tree. No resolver is invoked and none is shelled out to. A lock that is absent stays absent; producing one would mean running a resolver, which is a network call wearing a subprocess.""" ... def read_toml_packages(path: Path) -> list[tuple[str, str]] | None: """poetry.lock / uv.lock — `[[package]] name = … version = …`.""" ... def read_pipfile_lock(path: Path) -> list[tuple[str, str]] | None: """Pipfile.lock — JSON, `{"default": {name: {"version": "==x"}}, …}`.""" ... def scan_rationale(path: str, source: str, spans: list[tuple[str, int, int]], facts: Facts) -> None: """Record `WHY:`/`NOTE:` comments, attributed to the definition around them. CPython's `ast` discards comments entirely — they are not in the tree at any node — so this reads the SAME source a second time with `tokenize`, which keeps them and gives each a line number. That is why attribution has to meet the AST on line numbers rather than on nodes: the two readers never see the same object. A file that tokenizes badly is skipped in silence, deliberately. It has already parsed as `ast` by the time this runs, so a tokenize failure is a disagreement between two CPython readers about a file both accepted — not something a customer can act on, and not worth a line in a report about their codebase.""" ... def module_name_for(path: Path, root: Path) -> str: ... def collect(roots: list[Path], excludes: list[str]) -> Facts: ... LANG: ... def emit(facts: Facts, ids: Ids, *, stamp: int | None=None, load_key: str | None=None) -> str: """Render Python facts as a DKE Python program.""" ... def main(argv: list[str] | None=None) -> int: ...