Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The architecture follows a clean separation of concerns with three main layers:

**ExecutionContext** is the shared state container passed to all steps (see [pypeline-steps skill](.agent/skills/pypeline-steps/SKILL.md) for usage examples):

- `install_dirs: List[Path]`: Binary directories automatically added to subprocess PATH
- `install_dirs: List[Path]`: Binary directories automatically added in front of the subprocess PATH, in registration order (`add_install_dirs(dirs, prepend=True)` to precede the earlier steps' directories, as `CreateVEnv` does)
- `data_registry: DataRegistry`: Type-safe key-value store for arbitrary data exchange
- `inputs: Dict[str, Any]`: User parameters from CLI (`-i key=value`) or config defaults
- `env_vars: Dict[str, Any]`: Environment variables injected into all subprocess calls
Expand Down
7 changes: 7 additions & 0 deletions docs/explanation/execution_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,10 @@ The executor automatically:
- Adds `install_dirs` to PATH
- Injects `env_vars`
- Handles Windows/Unix shell differences

### PATH order

`install_dirs` are placed before the inherited PATH, in the order the steps registered them.
A step can call `add_install_dirs(dirs, prepend=True)` to be placed before the directories of
the steps that ran earlier. `CreateVEnv` does this, so the project virtual environment always
takes precedence over tools installed by other steps, whatever order the steps run in.
13 changes: 11 additions & 2 deletions src/pypeline/domain/execution_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@ class ExecutionContext:
def get_input(self, name: str) -> Optional[Any]:
return self.inputs.get(name, None)

def add_install_dirs(self, install_dirs: List[Path]) -> None:
self.install_dirs.extend(install_dirs)
def add_install_dirs(self, install_dirs: List[Path], prepend: bool = False) -> None:
"""
Register directories to be added to the PATH of the subsequent steps.

prepend: place these directories ahead of the ones registered by earlier steps,
so that their PATH precedence does not depend on the pipeline step order.
"""
if prepend:
self.install_dirs[:0] = install_dirs
else:
self.install_dirs.extend(install_dirs)

def add_env_vars(self, env_vars: Dict[str, Any]) -> None:
self.env_vars.update(env_vars)
Expand Down
4 changes: 3 additions & 1 deletion src/pypeline/steps/create_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,9 @@ def get_config(self) -> Optional[dict[str, str]]:
return None

def update_execution_context(self) -> None:
self.execution_context.add_install_dirs(self.install_dirs)
# The project virtual environment shall take precedence over tools installed by other steps,
# independent of the order in which the steps run.
self.execution_context.add_install_dirs(self.install_dirs, prepend=True)

def get_needs_dependency_management(self) -> bool:
# Always return False - the bootstrap script handles dependency management internally
Expand Down
20 changes: 20 additions & 0 deletions tests/test_execution_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@ def test_create_process_executor_with_env_vars(project: Path) -> None:
assert "value" in subprocess_executor.env.values()


def test_install_dirs_keep_call_order(project: Path) -> None:
context = ExecutionContext(project_root_dir=project)
context.add_install_dirs([Path("first")])
context.add_install_dirs([Path("second")])

path = context.create_process_env()["PATH"]
assert path.index("first") < path.index("second")


def test_prepended_install_dirs_win_over_earlier_steps(project: Path) -> None:
context = ExecutionContext(project_root_dir=project)
# A tool installer (e.g. ScoopInstall) runs first and provides its own Python
context.add_install_dirs([Path("scoop/apps/python311/current")])
# The virtual environment is created afterwards but must still come first on PATH
context.add_install_dirs([Path(".venv/Scripts")], prepend=True)

path = context.create_process_env()["PATH"]
assert path.index(".venv/Scripts") < path.index("python311")


class SomeData:
def __init__(self, data: str) -> None:
self.data = data
Expand Down
Loading