python_executable¶
TODO: Add separate FS for the concept of StateStride.
Intro¶
The stand-alone proto_code
is designed to be run by any python executable (available in PATH).
Eventually, based on the target client repo requirements, it must become:
the required
pythonversionexecuted from the initialized
venv(with all dependencies)
To achieve this, protoprimer switches python executables in a multi-stage bootstrap sequence:
./prime -v
Each executable is replaced with an os.execve call.
Details¶
That initial executable is unpredictable:
It may be any in the user
PATHenv var (via shebang_line).It may be any specified by the user directly.
Moreover, once venv is initialized, proto_code also needs
to switch to required python inside that venv to install dependencies there
(see primer_runtime).
proto_primer switches progressively to next python binary (starts a new process)
communicating that progress via EnvVar.var_PROTOPRIMER_PY_EXEC.
Required python: selecting executable path¶
Required python version is specified per environment via the config field required_python_version.
If the field is not specified, .python-version file is searched up from ref root dir (e.g. repo root).
Depending on the tools (e.g. uv, pip, ...) the required python version is installed or only validated.
python: selected vs required¶
There are two python adjectives in use:
selected (about executable):
protoprimerimplementspythonexecutable (abs path) selection process.required (about version):
protoprimerallows specifying requiredpythonversion.
Depending on config, the selected python executable does not necessarily match the required python version.
However, using uv guarantees that selected python executable matches required python version.
python selector¶
There can be many ways to select specific python executable paths in various target environments:
use some environment-specific absolute path
use some environment manager to configure required
python...
protoprimer delegates that selection to python selectors.
python selector is a stand-alone script configured in python_selector_file_rel_path.
If python selector is not specified, protoprimer handles the basic (see below) by trying python basenames.
python selector with uv¶
If uv is used, the selected python is mostly irrelevant as it is only used to deploy uv into a temporary venv.
Such selected python must be >= "3.8" for uv to deploy successfully.
Subsequently, uv is used to create the dedicated venv which guarantees required python version.
Selecting python executable¶
The selection follows this pseudo-code:
def create_venv(venv_driver, current_python, required_version):
if str(venv_driver) == "venv_uv":
selected_python = probe_python(required_version)
if selected_python:
if what_version(selected_python) >= (3, 8):
# NOTE: `required_python` has `required_version`:
required_python = create_venv_by_uv(selected_python, required_version)
else:
required_python = None
else:
required_python = None
return required_python
else:
selected_python = probe_python(required_version)
if selected_python:
# NOTE: `selected_python` may not have `required_version`:
selected_python = create_venv_by_pip(selected_python, required_version)
else:
selected_python = None
return selected_python
def probe_python(required_version):
some_python = select_python()
if some_python is None:
some_python = search_python(required_version)
return some_python
def create_venv_by_uv(given_python, required_version):
# Use `what_version(given_python)` to create an intermediate `venv` for `uv`,
# then install `uv` there,
# then use `uv` to install `python` of `required_version`,
# then create the final `venv` which uses `python` of `required_version`.
pass
def create_venv_by_pip(given_python, required_version):
# Verify that `what_version(given_python)` == `required_version`.
# Create standard `venv` via CLI using `what_path(given_python)`.
pass
def select_python():
# Run `python` selector script specified in `python_selector_file_rel_path`.
pass
def search_python(python_version):
# Use `python_version` string formatted as "X.Y.Z" to try each basename (in that order):
# `pythonX.Y.Z`
# `pythonX.Y`
# `pythonX`
# `python`
# Return the first basename found in `PATH` (e.g. via `shutil.which(...)`)
# which also succeeds when invoked with `--version` option.
pass
def what_version(some_python):
# Return version of `some_python`.
pass
def what_path(some_python):
# Return path of `some_python`.
pass