Source code for kstlib.pipeline.steps.python
"""Python module step executor for pipeline.
Executes Python modules via ``subprocess.run([sys.executable, "-m", module])``.
Runs in a subprocess (not ``shell=True``) for isolation and security.
"""
from __future__ import annotations
import logging
import sys
from kstlib.pipeline.models import StepConfig, StepResult, StepStatus
from kstlib.pipeline.steps._base import _run_subprocess
from kstlib.pipeline.steps._helpers import _sanitize_command
logger = logging.getLogger(__name__)
[docs]
class PythonStep:
"""Execute a Python module as a pipeline step.
Uses ``subprocess.run`` with ``[sys.executable, "-m", module, *args]``
to run a Python module in a subprocess. Does not use ``shell=True``.
Examples:
>>> from kstlib.pipeline.models import StepConfig, StepType
>>> step = PythonStep()
>>> config = StepConfig(
... name="lint",
... type=StepType.PYTHON,
... module="ruff",
... args=("check", "src/"),
... )
>>> result = step.execute(config) # doctest: +SKIP
"""
[docs]
def execute(
self,
config: StepConfig,
*,
dry_run: bool = False,
) -> StepResult:
"""Execute a Python module via subprocess.
Args:
config: Step configuration with module, args, env, timeout, etc.
dry_run: If True, log the command without executing it.
Returns:
StepResult with captured stdout, stderr, return code, and duration.
"""
module = config.module or ""
cmd = [sys.executable, "-m", module, *config.args]
# Best-effort redaction for log output; the unredacted argv is
# still passed verbatim to subprocess for execution.
safe_cmd_str = _sanitize_command(cmd)
logger.debug("PythonStep '%s': cmd=%s", config.name, safe_cmd_str)
if dry_run:
logger.info("[DRY RUN] PythonStep '%s': %s", config.name, safe_cmd_str)
return StepResult(
name=config.name,
status=StepStatus.SKIPPED,
stdout=f"[dry-run] would execute: {safe_cmd_str}",
)
return _run_subprocess(cmd, config, shell=False, log_tag="PythonStep")
__all__ = [
"PythonStep",
]