Python: Simplify Python Poe tasks and unify package selectors (#4722)

* updated automation tasks and commands, with alias for the time being

* Restore aggregate test exclusions

Preserve the legacy all-tests scope for test --all by excluding lab and devui from the default aggregate sweep, while still allowing explicit package selection. Also ignore hidden/generated test directories such as .mypy_cache during aggregate discovery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated versions in pre-commit

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-18 19:39:11 +01:00
committed by GitHub
Unverified
parent d3d0100822
commit f48c4512d3
60 changed files with 1704 additions and 527 deletions
+80 -12
View File
@@ -1,6 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
"""Shared utilities for running poe tasks across workspace packages in parallel."""
"""Shared utilities for running Poe tasks across workspace packages.
These helpers centralize workspace discovery, selector matching, and execution
mode so the root task dispatcher and dependency tooling interpret package
filters the same way.
"""
import concurrent.futures
import glob
@@ -8,6 +13,8 @@ import os
import subprocess
import sys
import time
from collections.abc import Sequence
from fnmatch import fnmatch
from pathlib import Path
import tomli
@@ -70,12 +77,67 @@ def build_work_items(projects: list[Path], task_names: list[str]) -> list[tuple[
return work_items
def _run_task_subprocess(project: Path, task: str, workspace_root: Path) -> tuple[Path, str, int, str, str, float]:
def normalize_project_filter(value: str) -> str:
"""Normalize a user-supplied workspace selector.
Strip presentation differences so short names, relative paths, and globs can
be compared with one matcher.
"""
normalized = value.strip().strip("/").replace("\\", "/")
return normalized or "."
def build_project_filter_candidates(project: Path | str, aliases: Sequence[str] = ()) -> set[str]:
"""Return accepted selector values for one workspace project.
We accept the workspace path, short package name, and any supplied aliases
so user-facing ``--package core`` stays stable even when underlying tools
still need paths or distribution names.
"""
normalized_path = normalize_project_filter(str(project))
candidates = {normalized_path}
if normalized_path == ".":
candidates.update({"./", "root"})
else:
# Accept bare short names like ``core`` alongside ``packages/core`` and
# ``./packages/core`` so callers do not have to care which form a
# downstream script prefers.
path = Path(normalized_path)
candidates.add(path.name)
candidates.add(f"./{normalized_path}")
for alias in aliases:
normalized_alias = normalize_project_filter(alias)
if normalized_alias and normalized_alias != ".":
candidates.add(normalized_alias)
return {candidate.lower() for candidate in candidates}
def project_filter_matches(project: Path | str, pattern: str, aliases: Sequence[str] = ()) -> bool:
"""Return whether a project matches a user-supplied selector or glob.
Matching happens against the normalized candidate set so CLI callers can use
the same selector vocabulary everywhere.
"""
normalized_pattern = normalize_project_filter(pattern).lower()
return any(
fnmatch(candidate, normalized_pattern)
for candidate in build_project_filter_candidates(project, aliases)
)
def _run_task_subprocess(
project: Path,
task: str,
workspace_root: Path,
task_args: Sequence[str] = (),
) -> tuple[Path, str, int, str, str, float]:
"""Run a single poe task in a project directory via subprocess."""
start = time.monotonic()
cwd = workspace_root / project
result = subprocess.run(
["uv", "run", "poe", task],
["uv", "run", "poe", task, *task_args],
cwd=cwd,
capture_output=True,
text=True,
@@ -84,20 +146,20 @@ def _run_task_subprocess(project: Path, task: str, workspace_root: Path) -> tupl
return (project, task, result.returncode, result.stdout, result.stderr, elapsed)
def _run_sequential(work_items: list[tuple[Path, str]]) -> None:
def _run_sequential(work_items: list[tuple[Path, str]], task_args: Sequence[str] = ()) -> None:
"""Run tasks sequentially using in-process PoeThePoet (streaming output)."""
from poethepoet.app import PoeThePoet
for project, task in work_items:
print(f"Running task {task} in {project}")
app = PoeThePoet(cwd=project)
result = app(cli_args=[task])
result = app(cli_args=[task, *task_args])
if result:
sys.exit(result)
def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> None:
"""Run all (package × task) combinations in parallel via subprocesses."""
def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path, task_args: Sequence[str] = ()) -> None:
"""Run all (package x task) combinations in parallel via subprocesses."""
max_workers = min(len(work_items), os.cpu_count() or 4)
failures: list[tuple[Path, str, str, str]] = []
completed = 0
@@ -107,7 +169,7 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(_run_task_subprocess, project, task, workspace_root): (project, task)
executor.submit(_run_task_subprocess, project, task, workspace_root, task_args): (project, task)
for project, task in work_items
}
for future in concurrent.futures.as_completed(futures):
@@ -123,7 +185,7 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N
if failures:
print(f"\n[red]{len(failures)} task(s) failed:[/red]")
for project, task, stdout, stderr in failures:
print(f"\n[red]{'='*60}[/red]")
print(f"\n[red]{'=' * 60}[/red]")
print(f"[red]FAILED: {task} in {project}[/red]")
if stdout.strip():
print(stdout)
@@ -134,7 +196,13 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N
print(f"\n[green]All {total} task(s) passed ✓[/green]")
def run_tasks(work_items: list[tuple[Path, str]], workspace_root: Path, *, sequential: bool = False) -> None:
def run_tasks(
work_items: list[tuple[Path, str]],
workspace_root: Path,
*,
sequential: bool = False,
task_args: Sequence[str] = (),
) -> None:
"""Run work items either in parallel or sequentially.
Single items use in-process PoeThePoet for streaming output.
@@ -145,6 +213,6 @@ def run_tasks(work_items: list[tuple[Path, str]], workspace_root: Path, *, seque
return
if sequential or len(work_items) == 1:
_run_sequential(work_items)
_run_sequential(work_items, task_args)
else:
_run_parallel(work_items, workspace_root)
_run_parallel(work_items, workspace_root, task_args)