module

Krikri::ControllerPython

Which Python interpreter a CONTROLLER-side wrapper run (the custom lookup/filter/test/action plugin mechanisms) gets spawned under, and the one-place retry when the plain python3 on PATH cannot serve it.

The runners used to spawn the first python3/python on PATH and stamp {"kind": "unavailable"} when that interpreter could not import the ansible package - which every caller degrades to its pre-custom-plugin behavior, i.e. a lookup()/query() of a plugin that DOES exist silently becomes null. On a controller where Ansible is not installed for that interpreter (the common Homebrew/pipx layouts: python3 on PATH is Xcode's or a Homebrew python without the ansible package, while ansible-playbook runs under its own venv interpreter) that produced exactly the issue #778 symptom - real ansible-playbook runs the plugin, krikri returns null.

So: when a wrapper answers "unavailable", retry once under the interpreter the user's own ansible-playbook/ansible runs under (its shebang line), then any other python3* on PATH that can import ansible. Nothing changes when the first interpreter works, and nothing changes when NONE of them can import ansible (the degradation stays exactly as before) - a wrapper that answered anything other than "unavailable" is never retried, so a fake/dying interpreter keeps the #704/#705 spawn classification.

Probing is cached: per interpreter path (a path's answer never changes) and per PATH/HOME pair (which candidates exist at all), so a run pays for at most one probe per candidate, not one per lookup.

Constants

PROBE = "import ansible.plugins"

The capability probe. ansible.plugins is the package every wrapper imports from (ansible.plugins.lookup.LookupBase and friends), so an interpreter that cannot import it cannot serve any of them.

PYTHON_NAMES = ["python3", "python", "python3.14", "python3.13", "python3.12", "python3.11", "python3.10", "python3.9", "python3.8", "python3.7"] of ::String

Interpreter basenames tried after the ansible binaries' own shebangs: the plain names first (what the runners always used), then the versioned ones, newest first, so a controller whose python3 lacks ansible but whose python3.13 has it still works.

Instance methods

ansible_capable(primary : String | Nil = nil) : String | Nil

The first interpreter OTHER than primary that can import ansible, or nil when the controller has none. Order: the shebang interpreter of ansible-playbook then ansible (the interpreter real ansible itself runs under - the pipx/Homebrew case), then every python3* on PATH.

Source
ansible_importable?(python : String) : Bool

Whether python can import the ansible package. Cached per path; a probe that cannot even spawn the interpreter is a plain false.

Source
ansible_unavailable?(answer : JSON::Any | Nil) : Bool

A wrapper answer that says only "the ansible package is not importable by this interpreter" - the one retry-worthy verdict. Callers that read their wrapper's answer as JSON (the interactive action IPC loop) share the same test.

Source
executable

The plain controller interpreter - unchanged behavior, the first python3/python on PATH.

Source
run_wrapper(python : String | Nil, wrapper : String, stdin : String) : Captured | Nil

Runs wrapper (the -c program) against stdin under python, retrying once under an ansible-capable interpreter when the first one answered {"ok": false, "kind": "unavailable"} (it ran, it just cannot import ansible - the issue #778 case). Spawn classification is the runners' own: only a PRE-spawn failure raises Krikri::SpawnError, a wrapper that ran keeps its answer.

Source

Nested types